Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

x402: selling and buying an API

x402 revives HTTP status 402, Payment Required: a server quotes a price, the client pays and retries, and the server serves once the payment settles. Siontology implements x402 version 2 with two schemes:

  • exact: one ledger transaction per request. The server serves only after verifying the payment’s receipt as a light client would, and an agent can pay under an Agent Charter, so its limits hold whatever it is told.
  • stream: one hash per request. The client pays from a payment stream it opened to the server; the server checks each payment with one hash and settles thousands of them in one transaction (below).

How a request is paid

  1. The client requests the resource. The server answers 402 with a PAYMENT-REQUIRED header: base64 JSON with the price, the payee, the network sion:<first 16 bytes of the chain identity, in hex>, and a fresh nonce.
  2. The client signs a transfer, or a charter payment, whose purpose is x402:<nonce>, and retries with it in a PAYMENT-SIGNATURE header.
  3. The server’s facilitator submits the transaction, waits for it to execute, and verifies the receipt: the Pulse’s committee certificate, the certified order and the receipt’s inclusion proof.
  4. Only then does the server answer 200, with a PAYMENT-RESPONSE header describing the settlement.

Each quote and each transaction pays for one request. A quote is valid for 300 seconds. Replaying a payment fails: the nonce is used up.

Sell an API

Serve one resource for a price per request, paid to PAY_TO

Usage: sion x402 serve [OPTIONS] <PAY_TO>

Arguments:
  <PAY_TO>  Who gets paid (wallet name or account ID)

Options:
      --listen <LISTEN>              Listen address [default: 127.0.0.1:8402]
      --node <NODE>                  The node's API [env: SION_NODE=] [default: http://127.0.0.1:8730]
      --path <PATH>                  The resource's path [default: /v1/resource]
      --price <PRICE>                Price per request (USD) [default: 0.25]
      --body <BODY>                  What every paid request returns [default: {"paid":true}]
      --chain <CHAIN>                The chain identity receipts must verify against. Without it the node vouches for itself [env: SION_CHAIN=]
      --streams                      Also accept payments from streams (scheme `stream`), settled by PAY_TO's wallet, which must be a wallet name
      --settle-every <SETTLE_EVERY>  With --streams: settle once a stream has this many unsettled steps [default: 1000]
      --settle-after <SETTLE_AFTER>  With --streams: settle a stream's payments at the latest this many seconds after the first unsettled one [default: 30]
  -h, --help                         Print help
$ sion x402 serve carol --price 0.25 --chain 94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a
selling http://127.0.0.1:8402/v1/resource at 0.25 USD per request, paid to 31338b14 on sion:94425d7211b21492c4a9147ba3b67dc0

sion x402 serve serves one resource with a fixed body. It is a working reference for the server side; a real API would use the same flow from its own code (sion_cli::x402).

An unpaid request:

$ curl -si http://127.0.0.1:8402/v1/resource
HTTP/1.1 402 Payment Required
Server: tiny-http (Rust)
Date: Fri, 30 Oct 2026 00:47:14 GMT
Content-Type: application/json
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwicmVzb3VyY2UiOnsidXJsIjoiL3YxL3Jlc291cmNlIiwiZG…
Content-Length: 535

{"x402Version":2,"resource":{"url":"/v1/resource","description":"/v1/resource, 0.25 USD per request","mimeType":"application/json"},"accepts":[{"scheme":"exact","network":"sion:94425d7211b21492c4a9147ba3b67dc0","asset":"USD","amount":"250000","payTo":"31338b143b630889b7c189bdb6aeef9683bba23d992bf8f66d6f22bc364f2353","maxTimeoutSeconds":300,"extra":{"chain":"94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a","decimals":6,"nonce":"f30aa975f0a9dddef5d55091360a7b70","purpose":"x402:f30aa975f0a9dddef5d55091360a7b70"}}]}

The header carries the same JSON as the body, base64-encoded (cut here). amount is in micro-units; extra.chain is the full chain identity, and extra.purpose is what the payment must name.

Buy from it

Fetch a URL, paying through x402 if the server asks

Usage: sion x402 fetch [OPTIONS] <URL>

Arguments:
  <URL>  The URL

Options:
      --node <NODE>            The node's API [env: SION_NODE=] [default: http://127.0.0.1:8730]
      --wallet <WALLET>        The paying wallet (the agent's, when paying under a charter; not needed with --stream)
      --charter <CHARTER>      Pay under this Agent Charter (ID) instead of by transfer
      --allotment <ALLOTMENT>  The agent's allotment (ID), with --charter
      --stream <STREAM>        Pay from this payment stream (ID) instead: one chain element per request, no transaction
      --count <COUNT>          Fetch this many times (with --stream, every request after the first is paid up front) [default: 1]
      --max <MAX>              The most to pay (USD) [default: 1]
      --chain <CHAIN>          The chain identity to pay on. Without it the node vouches for itself [env: SION_CHAIN=]
  -h, --help                   Print help

By transfer:

$ sion x402 fetch http://127.0.0.1:8402/v1/resource --wallet dana --max 1 --chain 94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a
402 Payment Required: 0.25 USD to 31338b143b630889b7c189bdb6aeef9683bba23d992bf8f66d6f22bc364f2353 on sion:94425d7211b21492c4a9147ba3b67dc0 (exact)
paid: transaction e752103dbe53fc8ecc9c6303b7518fd09fe360a2f7100f8f037b82b13125f30f settled and verified by the server
200 {"paid":true}

The whole round trip took 16 seconds on the devnet (four validators on one 2-core machine), most of it waiting for the Pulse. A client refuses to pay more than --max:

$ sion x402 fetch http://127.0.0.1:8402/v1/resource --wallet dana --max 0.1 --chain 94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a
error: the server asks 0.25 USD, more than the 0.1 USD this client may pay

Buy as an agent, under a charter

The principal charters the agent to pay only carol, at most 0.50 USD per payment and 1 USD at once, its bucket refilled by 0.001 USD a Pulse:

$ sion charter create acme agent8 --payees carol --max-payment 0.5 --burst 1 --rate 0.001 --funding 5
acme signs: Give agent8 an Agent Charter funded with 5 USD: only to carol, at most 0.5 USD per payment, 1 USD at once, refilled by 0.001 USD a Pulse, until Pulse 100008, a receipt claim for every payment, may delegate narrower limits. Fee 0.0012 USD; valid until Pulse 28.
Pulse 11: Done: charter ebff065f created; fee 0.0012 USD.
  digest 3ab480c38f0cfd24926098573f787d5f8be301212386a54f765cefbf864f26e0
charter ebff065f040db6df7b5bb72fc821113082bdf13d47c31613d7c7e38b78ad16d2
allotment f4f79fcf6bc0c291e8c43023c8ef91d335575d3088131db41af11d8dd7f2da17
$ sion x402 fetch http://127.0.0.1:8402/v1/resource --wallet agent8 --charter ebff065f040db6df7b5bb72fc821113082bdf13d47c31613d7c7e38b78ad16d2 --allotment f4f79fcf6bc0c291e8c43023c8ef91d335575d3088131db41af11d8dd7f2da17 --max 1 --chain 94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a
402 Payment Required: 0.25 USD to 31338b143b630889b7c189bdb6aeef9683bba23d992bf8f66d6f22bc364f2353 on sion:94425d7211b21492c4a9147ba3b67dc0 (exact)
paid: transaction e13b38d39c88aa8bced300dcc4a6a30d8dc84fa352e9c0f01232f8e074ef81b5 settled and verified by the server
200 {"paid":true}

The payment is an AgentPay from the agent’s allotment. If the server had asked for more than the charter allows, or named another payee, the ledger would have refused the payment and the server would have served nothing. The bucket is a limit too: the agent paid for four requests in a row, 1 USD in all, and the next one found its bucket nearly empty (the server’s 402 body is cut here):

$ sion x402 fetch http://127.0.0.1:8402/v1/resource --wallet agent8 --charter ebff065f040db6df7b5bb72fc821113082bdf13d47c31613d7c7e38b78ad16d2 --allotment f4f79fcf6bc0c291e8c43023c8ef91d335575d3088131db41af11d8dd7f2da17 --max 1 --chain 94425d7211b21492c4a9147ba3b67dc08bf05d315c8f89dc4c2a299d5d596e5a
402 Payment Required: 0.25 USD to 31338b143b630889b7c189bdb6aeef9683bba23d992bf8f66d6f22bc364f2353 on sion:94425d7211b21492c4a9147ba3b67dc0 (exact)
payment refused: the ledger refused the payment: charter caveat violated: the bucket holds 0.012, below the payment 0.25
402 {"x402Version":2,"error":"the ledger refused the payment: charter caveat violated: the bucket holds 0.012, below the payment 0.25",…

The x402 demo shows a payment above the limit per payment:

           agent: GET /v1/report (5 USD) → pays → the ledger refused the payment: charter caveat violated: payment 5 above per-payment limit 1; carol's API answers 402 and serves nothing

The stream scheme: one hash per request

A client that calls the same API many times opens a stream to it once (Streams from the command line), then pays each request with the next element of its chain. Nothing per request goes to the ledger, so a request costs the time of an HTTP round trip, not of a Pulse.

Sell with --streams. The server then accepts both schemes, and settles stream payments as the payee, so PAY_TO must be a wallet name:

$ sion x402 serve bob --streams --price 0.001 --path /v1/price --body '{"gpu_hour_usd":1.95}' --settle-every 100 --settle-after 5 --listen 127.0.0.1:8403
selling http://127.0.0.1:8403/v1/price at 0.001 USD per request, paid to a27e5e28 on sion:94425d7211b21492c4a9147ba3b67dc0 (exact transfers, or streams settled in batches)

Buy with --stream. The first request learns the price from the 402; with --count, every request after it pays up front, without asking first:

$ sion x402 fetch http://127.0.0.1:8403/v1/price --stream 4de1bcf42ffdede89fb0ee8aaa74b876bc5a9eef98747b43fff2abbe2e701374 --count 3
402 Payment Required: 0.001 USD to a27e5e28be5f6fcef56c7adbd682ceb0bc6966f666e87696998c743dc6f88b37 on sion:94425d7211b21492c4a9147ba3b67dc0 (exact)
402 Payment Required: 0.001 USD to a27e5e28be5f6fcef56c7adbd682ceb0bc6966f666e87696998c743dc6f88b37 on sion:94425d7211b21492c4a9147ba3b67dc0 (stream)
paid from the stream: position 1, checked by the server with one hash per step
200 {"gpu_hour_usd":1.95}
paid from the stream: position 2, checked by the server with one hash per step
200 {"gpu_hour_usd":1.95}
paid from the stream: position 3, checked by the server with one hash per step
200 {"gpu_hour_usd":1.95}

On the devnet, 200 such requests took 1.1 seconds in all from the command line, and the server had settled every one of them about ten seconds later.

On the wire

The stream requirement in a 402 carries no nonce: a chain element pays for one position only, so it cannot be replayed.

{
  "scheme": "stream",
  "network": "sion:94425d7211b21492c4a9147ba3b67dc0",
  "asset": "USD",
  "amount": "1000",
  "payTo": "a27e5e28be5f6fcef56c7adbd682ceb0bc6966f666e87696998c743dc6f88b37",
  "maxTimeoutSeconds": 300,
  "extra": { "chain": "94425d72…6e5a", "decimals": 6, "margin": 10 }
}

The client’s PAYMENT-SIGNATURE payload is the payment instead of a transaction. The first payment past one of the chain’s checkpoints also carries the checkpoint’s jump:

{
  "x402Version": 2,
  "accepted": { "scheme": "stream", "…": "as quoted" },
  "payload": {
    "stream": "4de1bcf42ffdede89fb0ee8aaa74b876bc5a9eef98747b43fff2abbe2e701374",
    "position": 64,
    "value": "…",
    "jump": { "index": 1, "path": ["…", "…"] }
  }
}

What the server checks

The server’s stream desk (sion_cli::stream::StreamDesk) accepts a payment only if:

  1. the stream pays this server, in USD, and is open;
  2. the price is a whole number of the stream’s steps, and the payment is exactly that many steps past the last one it accepted;
  3. the element hashes down to the last one it accepted (one hash per step);
  4. the escrow covers everything accepted and not yet settled, and the stream lasts at least margin more Pulses (10 by default), so the server has time to settle.

It reads the stream from the ledger the first time it sees it, and again only when a payment needs more escrow or time than it last saw (the payer may have topped up). A jump it receives is checked once and kept for settling.

It settles a stream when --settle-every steps are unsettled, when the oldest unsettled payment is --settle-after seconds old, when the stream is within twice the margin of its last Pulse, and when the server stops. It settles by jump when that covers more steps than a walk, or the same steps for fewer hashes.

When a payment is refused

The server answers 402, and its PAYMENT-RESPONSE names the position it had accepted up to:

{ "success": false, "errorReason": "expected the element at position 7, not 8", "transaction": "", "network": "sion:…", "extensions": { "sion": { "stream": "…", "position": 6 } } }

An honest client goes back to that position and pays again. This re-sends an element the server has already seen, so it gives nothing away; if the client had been ahead of the server (a request lost on the way), the retry succeeds. The client retries once.

What each side risks

The server is paid for every payment it accepted, as long as it settles before the stream’s last Pulse: the escrow cannot leave the stream before then. The client risks one request’s price per request, as with exact: a server can take a payment and not serve.

For agents over MCP

The MCP tool sion_fetch_paid does what sion x402 fetch does, with a cap the agent sets per call; see MCP tools.

Limits

  • The adapter speaks x402’s headers and JSON, but it has not been tested against third-party x402 clients or servers.
  • EVM payment schemes are out of scope; the schemes are exact and stream, on sion: networks.
  • An exact quote pays for one request. Prepaid balances are streams.
  • A stream desk keeps its tabs in memory: a server that crashes before it settles loses what it had not settled. Settle often (--settle-after), or persist the latest payment per stream.
  • MCP’s sion_fetch_paid pays with exact only.