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 Swap 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: Swap
description: 'Sato Swap. One call chooses the venue, discloses that venue''s own fee sentence verbatim, checks four targets — both tokens, the venue endpoint and the recipient — against the caller''s policy, simulates the transaction, and then returns either a recommendation or an UNSIGNED transaction. NON-CUSTODIAL: nothing in this tag holds a key, signs, or broadcasts. UNKNOWN REFUSES BY DEFAULT and every refusal names the rule and the reading that produced it, so "we did not check" and "we checked and it is fine" never look alike. A refusal arrives as HTTP 200 carrying `withheld.reason`: it is an answer, not a failure, and retrying it unchanged will refuse again. A recipient with no Agent Passport is `no_record` — the ordinary case, never a finding against the address — and there is no list of trusted counterparties here. Receipts record what was checked; they are never a claim a trade filled, and they are never summed into volume.'
paths:
/api/token/resolve:
get:
tags:
- Swap
operationId: resolveToken
summary: What an address, a mint or a link is
description: 'Takes a contract address on Base, BNB Chain, Arbitrum, Polygon and Solana (name the chain with `chain`; a bare 0x with none is read as Base and the answer says so), a Solana mint or a link from dexscreener.com, geckoterminal.com, birdeye.so, pump.fun, gmgn.ai, basescan.org, bscscan.com, arbiscan.io, polygonscan.com, solscan.io, jup.ag, app.uniswap.org, aerodrome.finance, zora.co, clanker.world, and returns the chain, address, name, symbol, decimals, token program, the main pool with its own liquidity and the token''s USD price. The token is read from the link''s path with no request for the link itself; a DexScreener or GeckoTerminal POOL link resolves to the pool''s BASE token. Every figure has an entry in `sources` (where it was read, and when) and every null has an entry in `gaps` (why): null is unknown, never zero. Decimals and the token program come from the chain when it answers; on Solana the mint authority, freeze authority and Token-2022 extensions are read from the mint. Price and liquidity are ONE pool''s listing from a market-data API, never summed across pools or venues, and not a quote or a fill. It describes and recommends nothing, and says nothing about whether a token is safe — run /api/preflight for evidence on the token itself. Answers are held for 60 seconds per token; a lookup the cache answers is never counted against the burst limit.'
parameters:
- name: input
in: query
required: false
schema:
type: string
maxLength: 600
description: A contract address (0x…) on one of the chains in `chain`, a Solana mint (base58), or a link. A bare symbol is refused, never guessed. Also read, labelled `param-alias`, from ?address=, ?mint= or ?url=. With none of them the answer is a 200 needs_input.
- name: chain
in: query
required: false
schema:
type: string
description: Optional; one of base, bnb, arbitrum, polygon, solana, or another spelling of one (BSC, 56, eip155:8453, matic), read and labelled `chain-spelling`. A generic network word (mainnet, testnet) is refused with candidates. Must agree with the input; it supplies the chain a bare 0x address or a link leaves open. A bare 0x with no chain at all is read as Base (`chain_assumed`).
responses:
'200':
description: The resolution (signed; the Link header names the token-resolve schema), OR a `needs_input` help body when no input was sent (no Link).
headers:
Link:
schema:
type: string
description: <https://satohub.ai/schemas/token-resolve/v1.json>; rel="describedby"
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/token-resolve'
- $ref: '#/components/schemas/needs-input'
'400':
description: The input did not parse, names a chain or site this resolver does not read, or is a symbol. The body carries `code` and the supported hosts.
'404':
description: Nothing exists at the address, or a pool link resolves to no token.
'429':
description: Too many lookups from this address that needed a live read.
'503':
description: The chain and the market-data APIs did not answer.
/api/swap/quote:
post:
tags:
- Swap
operationId: swapQuote
security:
- {}
- bearerAuth: []
- satoKey: []
summary: Quote a swap, check it against a policy, and return an unsigned transaction
description: 'The Sato Swap call. Chooses the venue the way /api/route/swap does, then CHECKS the trade before anything is built.
TWO MODES. `recommend` (default) returns the choice, the fee sentence, the verdicts and the policy in force, and NEVER a transaction — whatever the gate said. `build-tx` returns an UNSIGNED transaction, and only when the gate allowed it AND a simulation did not revert.
WHAT IS CHECKED: four targets — `token_in`, `token_out`, the venue endpoint and the recipient — each with its own verdict (go | caution | no | unknown), the rule id that produced it and the reason. Then the caller’s policy: caps per trade and per period, allowed chains, tokens, venues and recipients, maximum slippage and deadline. Manage policies at /api/swap/policies; with no stored policy the default applies, and the default refuses on unknown.
UNKNOWN REFUSES BY DEFAULT. A target that could not be read, or a simulation that could not run, withholds the transaction unless the policy says otherwise, and `withheld.reason` names the lane that could not be read.
A REFUSAL IS A 200. Gate refusals, policy limits and unsimulatable transactions all come back as 200 with `tx: null` and `withheld`. 4xx is reserved for a body we could not read, and for auth and rate limits.
COUNTERPARTY: `no_record` means we hold no Agent Passport for the recipient. That is the ordinary case and is not a finding against it; `found` is not an endorsement, since a Passport is self-registered and `wallet_verified` proves control of a key rather than anything about the product.
THE FEE is the venue’s own sentence, quoted verbatim in `disclosure`, before anything is signed. A trade that is never signed pays nothing.
CUSTODY: none. This endpoint holds no keys, signs nothing and broadcasts nothing.
The response is Ed25519-signed (`meta.signature`, `Sato-Signature` header) when a signing key is configured, and carries `Link: ; rel="describedby"`. Responses are `no-store`: a cached quote is a stale price.
RATE LIMITS: anonymous callers are bounded per hour per caller — 30 `recommend`, 10 `build-tx`. A Sato API key replaces that with the tier’s attributed daily budget plus a per-hour `build-tx` allowance; a key only ever raises a limit.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- chain_in
- token_in
- token_out
- amount_in
properties:
chain_in:
type: string
maxLength: 40
description: Source chain as the directory writes it, e.g. Base, Ethereum, Solana.
chain_out:
type: string
maxLength: 40
description: Destination chain. Omit, or repeat chain_in, for a same-chain swap. A different value is the cross-chain lane; the two lanes are never compared with each other.
token_in:
type: string
maxLength: 120
description: Contract address, Solana mint, or a symbol for the well-known stablecoins.
token_out:
type: string
maxLength: 120
amount_in:
type: string
pattern: ^[0-9]{1,78}$
description: Base units of the INPUT token, as a string — a uint256 does not survive a JSON number.
recipient:
type:
- string
- 'null'
maxLength: 120
description: 'Where the output goes. Omit to send to the taker. On a cross-chain request this is the destination address (0x + 40 hex, or a Solana address) and only the deBridge venue builds an order for it: any other venue is reported unavailable for a recipient other than the taker. A Solana destination needs it.'
recipient_owner:
type:
- string
- 'null'
enum:
- self
- null
description: The caller declares `recipient` is ITS OWN address on `chain_out`. Sato Hub does not check that. It is read only on a cross-chain `build-tx`, where it lets a destination address that is not the taker pass the recipient check as rule RCPT5 (the look-alike and mark checks still run first and still refuse). Ignored on a same-chain swap and in `recommend` mode.
taker:
type:
- string
- 'null'
maxLength: 120
description: The address that would sign. Some venues only return a transaction when it is given; nothing is signed here.
slippage_bps:
type: integer
minimum: 0
maximum: 5000
default: 50
deadline:
type:
- integer
- 'null'
description: Unix seconds the quote should stay good until. Omit for the venue default.
usd_notional:
type:
- number
- 'null'
description: 'USD notional of amount_in when the CALLER already holds a price. Omitted is unknown, never zero: a USD cap does not bite without it.'
mode:
type: string
enum:
- recommend
- build-tx
default: recommend
venue:
type: string
enum:
- 0x-protocol
- 1inch
- jupiter-aggregator
- odos
- kyberswap
- relay
- debridge
- lifi
- cow
description: Pin ONE venue. Only it is quoted; if it cannot answer, is not offered on the chain or is refused, the answer names it and says why, and no other venue is tried. Omit and the chooser ranks every venue that quotes. A response to a pinned request carries `pinned_venue`.
referrer:
type:
- string
- 'null'
maxLength: 64
description: 'Optional referral: a referral name (like `sunny-otter`, case-insensitive, an ACTIVE name at Sato Hub), or a payout address (a Base 0x address, normalized to lower case, or a Solana address, exact case). Name a referrer (a payout address, or a referral name) and the taker that will sign a same-chain swap, and 30% of the Sato fee on it is accrued to the referrer, paid weekly in USDC on the chain of the payout address once the balance reaches $10, and only on swaps whose fee Sato Hub has read onchain in a transaction sent by that taker. The trade and the fee address do not change, so the swap costs you no more. A bad address, or a name Sato Hub has no record of, is refused 400 `{ error: "referrer_invalid", message }`; one of Sato Hub''s own fee addresses is ignored (`referral: null`), and so is a retired name, or any name while the name registry cannot answer in time. The answer''s `referral` echoes what was recorded (the name, or the normalized address). The referrer is admin-only on Sato Hub''s side: it is never on the public receipt.'
sato_agent_id:
type: string
description: An Agent Passport to act as. Honoured ONLY when a signed-in session proves ownership of it; an unproven id is dropped rather than recorded.
responses:
'200':
description: 'The quote, the verdicts, the simulation and either an unsigned transaction or the reason one was withheld. Also the shape a gate or policy refusal arrives in.
NO ROUTE is the one 200 that is not a quote: when no adapter returned a quote for the pair (or none serves the chain), the body is `{ unavailable, tried, checked_at, caveat }` — the reason, and every venue asked with what it said. It carries NO `Link: rel="describedby"`, because it is not a swap-quote; tell the two apart by the presence of `unavailable`.'
headers:
Link:
description: rel="describedby" pointing at /schemas/swap-quote/v1.json. Present on a quote only — absent on the no-route answer and on 4xx.
schema:
type: string
Sato-Signature:
description: Ed25519 over the exact response bytes, when a signing key is configured.
schema:
type: string
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/swap-quote'
- type: object
title: No route
required:
- unavailable
- tried
- checked_at
properties:
unavailable:
type: string
description: Why no route is offered. A reading of this moment, not a statement that the pair cannot be traded.
code:
type: string
enum:
- fee_side_unavailable
description: 'Set when the pair itself is refused: neither side is a stablecoin or the chain''s native coin, so Sato Route has no asset to take its fee in. Swap through USDC or the chain''s own native coin.'
tried:
type: array
description: Every adapter that was asked and what it said. Empty when no adapter serves the chain.
items:
type: object
properties:
venue:
type: string
reason:
type: string
source_url:
type: string
checked_at:
type: string
format: date-time
caveat:
type: string
'400':
description: The body did not parse, a field was out of range, or token_in equalled token_out on a same-chain swap.
'429':
description: Over the anonymous burst limit for this mode, or over the key’s daily budget.
/api/swap/receipts/{id}:
get:
tags:
- Swap
operationId: getSwapReceipt
summary: The public record of one swap decision
description: 'What was checked for one `route_id`, under which policy, at what instant: the quote, the mode and lane, the gate verdicts with their digest, the simulation digest, and whether the recipient held an Agent Passport.
WHAT IT IS NOT: a claim the trade filled, a claim anyone was paid, or a safety verdict. A refusal recorded here is a statement about what we could read. ONE ROW IS ONE DECISION — rows are never summed into volume, fills or a fee total.
The payload is an allowlist, not a delete list: a column added later stays invisible until someone decides it should be public. It never carries an IP hash, the row id, or the raw props bag. A receipt marked non-public answers 404, the same as an unknown id. Every 404 says how a receipt is minted (POST /api/swap/quote) and links the schema; the route TEMPLATE itself (`{id}` sent literally) is a 404 `template_not_a_receipt` and is never answered with a receipt, sample or otherwise.'
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: ^rt_[a-z2-9]{12}$
example: rt_2b3c4d5e6f7g
description: A route_id returned by POST /api/swap/quote. The example shows the form; ids are minted, never listed.
responses:
'200':
description: The public receipt.
content:
application/json:
schema:
$ref: '#/components/schemas/swap-receipt'
'404':
description: Unknown id, a receipt that is not public, or no receipt store configured.
/api/swap/policies:
get:
tags:
- Swap
operationId: listSwapPolicies
security:
- bearerAuth: []
- satoKey: []
summary: The swap policies bound to the calling subject
description: 'A policy decides what a caller may swap: allowed chains, tokens, venues and recipients, caps per trade and per period, maximum slippage and deadline, and what happens on a `caution` or `unknown` verdict.
FAILS CLOSED. There is no anonymous policy — a policy you cannot attribute is a policy someone else can widen — so an unidentified caller gets 401 and a plain statement that the DEFAULT policy applies, which refuses on unknown. THE SUBJECT COMES FROM THE AUTH, never from the body: a caller cannot write a policy for another key, another Passport or another wallet.'
responses:
'200':
description: The caller’s live policies, and the default when there are none.
'401':
description: No identified subject. The default policy applies.
'503':
description: No policy store configured on this deployment.
post:
tags:
- Swap
operationId: putSwapPolicy
security:
- bearerAuth: []
- satoKey: []
summary: Store a swap policy (a new version; the previous one is revoked)
description: Each write stores a NEW VERSION as a new row and revokes the previous one in the same breath, so a receipt citing (policy_id, version) always points at stored bytes. Revocation is a timestamp, never a delete. The subject is set from the auth and the three subject columns in the body are overwritten before validation.
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: The stored policy.
'400':
description: The policy did not validate, or it named no subject or more than one.
'401':
description: No identified subject.
'429':
description: Too many policy writes for this subject.
'503':
description: No policy store configured on this deployment.
delete:
tags:
- Swap
operationId: revokeSwapPolicy
security:
- bearerAuth: []
- satoKey: []
summary: Revoke one policy
parameters:
- name: id
in: query
required: true
schema:
type: string
responses:
'200':
description: Revoked.
'401':
description: No identified subject.
'404':
description: No live policy with that id for this subject.
components:
schemas:
swap-receipt:
type: object
description: 'JSON Schema: https://satohub.ai/schemas/swap-receipt/v1.json'
externalDocs:
url: https://satohub.ai/schemas/swap-receipt/v1.json
token-resolve:
type: object
description: 'JSON Schema: https://satohub.ai/schemas/token-resolve/v1.json'
externalDocs:
url: https://satohub.ai/schemas/token-resolve/v1.json
swap-quote:
type: object
description: 'JSON Schema: https://satohub.ai/schemas/swap-quote/v1.json'
externalDocs:
url: https://satohub.ai/schemas/swap-quote/v1.json
needs-input:
type: object
description: 'The 200 answer to a call that sent nothing usable: the accepted forms and exact example calls. It carries NO verdict, NO profile and NO pick. Header `X-Sato-Interpreted: needs-input`.'
required:
- status
- needs_input
- interpretation
- accepted
- examples
properties:
status:
type: string
enum:
- needs_input
needs_input:
type: boolean
enum:
- true
interpretation:
$ref: '#/components/schemas/interpretation'
endpoint:
type: string
format: uri
accepted:
type:
- object
- array
examples:
type: array
docs:
type: string
format: uri
additionalProperties: true
interpretation:
type: object
description: 'How the request was read. Present only on an interpreted answer; the response also carries `X-Sato-Interpreted: <rule>`.'
required:
- interpreted_as
- requested
- rule
- note
properties:
interpreted_as:
type: string
description: 'What was served: a URL, a canonical value, `needs_input`, `supported` or `candidates`.'
requested:
type: string
description: What was sent, decoded and cut to 200 characters.
rule:
type: string
enum:
- template-placeholder
- near-miss
- alias
- needs-input
- example-goal
- param-alias
- body-alias
- bare-body
- chain-spelling
- candidates
note:
type: string
additionalProperties: true
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