satohub-ai Sync API

Incremental mirroring.

Operations 5

GET /api/changes What changed since a date #
GET /api/freshness How fresh each data surface is #
POST /api/webhooks/subscribe Subscribe an endpoint to change deliveries #
DELETE /api/webhooks/subscribe Unsubscribe, token-bound #
GET /api/embed/stack.json The listing slice behind the embeddable stack widget #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/satohub-ai:satohub-ai-sync-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

satohub-ai-sync-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Sato Hub public Sync API
  version: 1.1.0
  summary: Read access to the onchain agent library, its change feed, and the Agent Passport registry.
  description: 'Public reads need no key and no account; the projects and listings routes take a bearer key. A Sato API key is optional on every metered endpoint and only raises limits. What is free, what is paid (the plans are Sato Check Team and Platform & Data), every limit and the fee schedule are listed on the pricing page, and the same sheet is machine-readable at /api/pricing.json.


    Pricing: https://satohub.ai/pricing


    License: Catalog data CC-BY-4.0 — attribution: data by satohub.ai. Scores/liveness are informational signals, not safety or quality guarantees.


    Scores and liveness are informational signals computed from public evidence. They are not safety ratings, audits, or claims about returns. `verification_status` distinguishes self-reported from independently checked; self-reported is never presented as verified.


    Every shape this API returns is also published as a versioned JSON Schema (draft 2020-12) at https://satohub.ai/schemas — catalogue at https://satohub.ai/schemas/index.json. They are referenced from components.schemas below, and v1 is additive-only: a breaking change becomes v2 at a new URL. Validate with additional properties allowed.


    The same data is available to agents over MCP (Streamable HTTP, JSON-RPC) at https://satohub.ai/api/mcp — documented at https://satohub.ai/mcp. That endpoint is not described here because it is not REST.


    INTERPRETED ANSWERS. A route template sent literally (`{slug}`), an empty query or body, a parameter under another name (`?package=` for `?target=`) or another spelling of a chain (`8453`, `eth`, `bsc`) is answered with the closest honest thing instead of a bare 4xx, and always LABELLED: the body carries an `interpretation` object (`interpreted_as`, `requested`, `rule`, `note`; the same four keys are repeated at the top level when they cannot collide with the record''s own fields), and the response carries the header `X-Sato-Interpreted: ` (plus `Content-Location` when there is a canonical URL). An interpreted answer is never an exact match. Nothing that moves money is ever interpreted beyond a chain spelling: no amount, token, taker, recipient, deployer or slippage is defaulted, a missing chain is never assumed, a chain is never guessed from an address, and a generic network word (`mainnet`, `testnet`) is never read as a chain (the answer lists the candidates). Writes are never interpreted, and a receipt is never served for a template.'
  contact:
    name: Sato Hub
    url: https://satohub.ai
  x-guidance: 'Free: search and read the onchain-agent catalog (/api/export/index.json, /api/changes, /api/resolve) and use the MCP server at /api/mcp. Paid over x402 (USDC on Base, no account or key): GET /api/x402/history/{slug} for one listing''s daily Sato Score record, GET /api/x402/agent-economy for the weekly agent-economy series. Each paid read is priced per unit delivered (daily points or rows returned): the 402 states the exact amount, the unit, the quantity and the formula, which is also on /pricing. The Sato Score measures how open, active and verifiable a project is; it is not a safety or returns grade.'
  x-status:
    page: https://satohub.ai/status/sato-hub
    json: https://satohub.ai/api/status.json
    feed: https://satohub.ai/status/feed.xml
    note: Success ratios and latencies of Sato Hub's own hourly checks. Not a service-level agreement.
  license:
    name: CC-BY-4.0
    url: https://creativecommons.org/licenses/by/4.0/
servers:
- url: https://satohub.ai
security: []
tags:
- name: Sync
  description: Incremental mirroring.
paths:
  /api/changes:
    get:
      tags:
      - Sync
      operationId: getDirectoryChanges
      summary: What changed since a date
      description: 'The delta companion to the full export: what was added, what changed, and what was removed since a date, so a consumer does not re-pull the whole index to find the few things that moved.


        READ `coverage` BEFORE RELYING ON THIS. The endpoint reports its own completeness rather than implying it is total: `added` is exact (from date_added); `changed` reflects only what produces a changelog event, so a quiet copy edit will not appear; `removed` is approximate, because there is no deprecated_at column.


        Treat it as change NOTIFICATION, not a complete replication log — poll it often, and re-pull the full export periodically to correct drift.


        Omitting `since` defaults to a 14-day window.


        WATCHING A LIST? Call this on your schedule with `since` set to the `next_since` of your last response, look for your slugs in `added`, `updated`, `changed` and `removed`, then store the new `next_since`. The cursor overlaps by a couple of days and leaves no gap, so dedupe by slug. The changed listings in full come from the directory export, which takes the same `since`. Store the cursor from the response you consumed: the rows call returns its own `delta.next_since`, so do not mix cursors from the two calls (they are cached for different times).


        PREFER NOT TO POLL? The same lanes are delivered by webhook — POST /api/webhooks/subscribe. A delivery is a slice of this feed, signed with your own secret; it carries no field you could not fetch here.'
      parameters:
      - name: since
        in: query
        required: false
        schema:
          type: string
          format: date
        description: ISO date, YYYY-MM-DD. Defaults to 14 days ago.
      responses:
        '200':
          description: The delta, with an explicit statement of its own coverage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  since:
                    type: string
                    format: date
                  window_days:
                    type: integer
                  next_since:
                    type: string
                    format: date
                    description: The cursor. Pass it as `since` on your next call. It starts the next window at a snapshot captured before this read, so nothing falls between two calls; it overlaps by a couple of days, so dedupe by slug.
                  counts:
                    type: object
                    properties:
                      added:
                        type: integer
                      changed:
                        type: integer
                      removed:
                        type: integer
                  added:
                    type: array
                    items:
                      type: object
                  changed:
                    type: array
                    items:
                      type: object
                    description: Grouped per resource, so a consumer syncs per record rather than per event.
                  removed:
                    type: array
                    items:
                      type: object
                  coverage:
                    type: object
                    description: Which parts of this response are complete and which are partial. Read it.
                    properties:
                      complete:
                        type: array
                        items:
                          type: string
                      partial:
                        type: array
                        items:
                          type: string
                      advice:
                        type: string
                  full_export:
                    type: string
                    format: uri
        '400':
          description: '`since` is not an ISO date.'
  /api/freshness:
    get:
      tags:
      - Sync
      operationId: getDataFreshness
      summary: How fresh each data surface is
      description: 'Per data surface (directory activity, news, Sato Score, liveness checks, snapshots, ERC-8004, the weekly agent-economy measurements, x402, LP pools, skills, deploy verification): the last successful collection read from the pipeline ledger, the last attempt and how it ended, the expected cadence, and whether it is late — older than 1.5× that cadence. The same report as the MCP tool onchain_agent_get_data_freshness.


        `last_success_at: null` with `late: null` means no successful run is recorded — unknown, never an invented date. An unreadable ledger returns every surface as `unknown`, never fresh. Timestamps and statuses of data jobs only; no usage figure appears here.'
      parameters:
      - name: surface
        in: query
        required: false
        schema:
          type: string
          enum:
          - directory_activity
          - news
          - erc8004_count
          - agent_tokens
          - liveness
          - sato_score
          - snapshots
          - x402_bazaar
          - x402_answers_daily
          - erc8004_endpoints_daily
          - mcp_alive_daily
          - erc8004_router_pool
          - passports
          - erc8004_census
          - x402_throughput
          - erc8004_agents_celo
          - custody_profiles
          - sato_scan
          - sato_scan_census
          - agent_economy
          - x402_settlement
          - lp_pools
          - skills
          - deploy_verification
          - custody_observed
          - dead_project_sweep
        description: One surface. Omit for all.
      responses:
        '200':
          description: The freshness report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  generated_at:
                    type: string
                    format: date-time
                  ledger:
                    type: string
                    enum:
                    - readable
                    - unavailable
                  rule:
                    type: string
                  surfaces:
                    type: array
                    items:
                      type: object
                      properties:
                        surface:
                          type: string
                        what:
                          type: string
                        expected_every:
                          type: string
                          enum:
                          - daily
                          - weekly
                        last_success_at:
                          type:
                          - string
                          - 'null'
                          format: date-time
                        last_attempt_at:
                          type:
                          - string
                          - 'null'
                          format: date-time
                        last_status:
                          type: string
                          enum:
                          - ok
                          - failed
                          - running
                          - none
                          - unknown
                        late:
                          type:
                          - boolean
                          - 'null'
                        state:
                          type: string
                          enum:
                          - fresh
                          - late
                          - never
                          - unknown
                        next_update:
                          type: string
                          format: date-time
                        source:
                          type: string
                  note:
                    type: string
                  source_url:
                    type: string
                    format: uri
        '400':
          description: '`surface` is not a known surface id.'
  /api/webhooks/subscribe:
    post:
      tags:
      - Sync
      operationId: subscribeChangeWebhook
      summary: Subscribe an endpoint to change deliveries
      description: 'The push companion to /api/changes: one POST per lane per day to an endpoint you control, so a mirror does not have to poll. A delivery is a SLICE of that public feed — it carries no field you could not fetch yourself.


        YOUR ENDPOINT MUST PROVE ITSELF FIRST. Subscribing triggers a challenge GET carrying `?sato_challenge=`; answer it by echoing the nonce (plain text, or {"challenge":"..."}). Without that echo nothing is stored — otherwise anyone could subscribe anyone else''s server to our traffic.


        https only, and public hosts only: loopback, RFC-1918, CGNAT, link-local and cloud-metadata addresses are refused at subscribe time and re-checked before every delivery.


        SIGNING. With a `secret`, every delivery carries `Sato-Webhook-Signature: t=, v1=` — HMAC-SHA256 over utf8(t) || 0x0A || canonicalJson(body), the same canonicalisation Sato Hub''s Ed25519 response signatures use. Without a secret, deliveries are unsigned and the response says so.


        GIVING UP. Three attempts per delivery; after seven consecutive failed deliveries the subscription is disabled and never retried. Re-subscribing revives it and re-runs the challenge.


        Full guide, including a verification snippet: https://satohub.ai/api/changes and docs/webhooks.md in the repository.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - url
              properties:
                url:
                  type: string
                  format: uri
                  description: https endpoint that will receive deliveries and answer the challenge GET.
                secret:
                  type: string
                  minLength: 16
                  maxLength: 200
                  description: Your own HMAC secret. Optional; without it deliveries are unsigned. Never echoed back.
                lanes:
                  type: array
                  description: 'Defaults to the five change-feed lanes. `added`/`updated`/`removed` mirror the change feed''s lanes; `status` and `verification` are changelog event kinds. `custody` is opt-in: Sato Check `custody.changed` events (a package''s key, host or fund behaviour changed between versions) — it describes, never rates. `scan` is opt-in: Sato Scan `scan.poisoning_started` events, a look-alike deny list with no watched wallet, transaction or amount (a transfer to a look-alike is email-only, never a webhook).'
                  items:
                    type: string
                    enum:
                    - added
                    - updated
                    - removed
                    - status
                    - verification
                    - custody
                    - scan
      responses:
        '201':
          description: Subscribed. Keep `unsubscribe_token` — it is stateless and not recoverable.
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscribed:
                    type: boolean
                  id:
                    type: integer
                  url:
                    type: string
                    format: uri
                  lanes:
                    type: array
                    items:
                      type: string
                  signed:
                    type: boolean
                  unsubscribe_token:
                    type:
                    - string
                    - 'null'
                    description: Null when this deployment has no secret configured to mint one — never a placeholder.
                  terms:
                    type: string
        '400':
          description: Bad URL, unknown lane, or a secret that is too short.
        '422':
          description: The endpoint did not echo the challenge.
        '503':
          description: Subscriptions are not configured on this deployment.
    delete:
      tags:
      - Sync
      operationId: unsubscribeChangeWebhook
      summary: Unsubscribe, token-bound
      description: Pass the `id` and `unsubscribe_token` from the subscribe response. The token is an HMAC over that id and that URL, so it is worthless against any other subscription. A wrong token and a missing subscription return the same 404 — neither answer should let anyone enumerate subscriptions.
      parameters:
      - name: id
        in: query
        required: true
        schema:
          type: integer
      - name: token
        in: query
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Unsubscribed.
        '400':
          description: '`id` or `token` missing.'
        '404':
          description: No such subscription, or the token does not match it.
        '503':
          description: Unsubscribe is not configured on this deployment.
  /api/embed/stack.json:
    get:
      tags:
      - Sync
      operationId: getEmbedStack
      summary: The listing slice behind the embeddable stack widget
      description: 'What /embed/stack.js renders on a third-party page: up to ten scored listings for a use case and/or chain, with score, tier, liveness and chains. CORS-open and signed (Ed25519, key at /.well-known/jwks.json) because it is rendered on domains we do not control. Ordered by Sato Score — a measure of how open, active and verifiable a project is, never a safety, quality or returns grade, and no winner is declared. CC-BY-4.0; the attribution link is part of the widget.'
      parameters:
      - name: use_case
        in: query
        required: false
        schema:
          type: string
        description: A Sato Hub use-case slug. An unrecognised value is ignored rather than erroring.
      - name: chain
        in: query
        required: false
        schema:
          type: string
        description: A chain name as the directory spells it, e.g. Base or Solana.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 10
          default: 5
      responses:
        '200':
          description: The slice, with the data's own as-of date.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  as_of:
                    type:
                    - string
                    - 'null'
                    format: date
                    description: The newest check date across the listings shown — never the render date.
                  count:
                    type: integer
                  items:
                    type: array
                    items:
                      type: object
                  attribution:
                    type: object
                  caveat:
                    type: string
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Two different credentials ride this header, and they are told apart by their shape.


        • A SATO API KEY, `sk_sato_…` — optional on every metered endpoint. It is NEVER required: each of those endpoints answers an anonymous caller at the free limits, and a key only raises the caps and attributes the calls to an account. A key may also be sent as `X-Sato-Key`. An unknown or revoked key is served as if no key were present, never refused. Limits and plans: /pricing.


        • A PRIVY ACCESS TOKEN for a signed-in account — required by the claim endpoints and the key-management endpoints. A token that is not a Sato key is treated as one of these.'
    satoKey:
      type: apiKey
      in: header
      name: X-Sato-Key
      description: 'The same optional Sato API key, on its own header. Equivalent to `Authorization: Bearer sk_sato_…`.'
externalDocs:
  description: The published Sato Hub formats, as JSON Schema.
  url: https://satohub.ai/schemas