Every API here is available over the APIs.io API and to AI agents over MCP.
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