satohub-ai Swap API

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.

Operations 6

GET /api/token/resolve What an address, a mint or a link is #
POST /api/swap/quote Quote a swap, check it against a policy, and return an unsigned transaction #
GET /api/swap/receipts/{id} The public record of one swap decision #
GET /api/swap/policies The swap policies bound to the calling subject #
POST /api/swap/policies Store a swap policy (a new version; the previous one is revoked) #
DELETE /api/swap/policies Revoke one policy #

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-swap-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-swap-api-openapi.yml Raw ↑
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