x402 wire format
x402 is Coinbase’s HTTP 402 Payment Required specification. Relaystation implements version 2 to-spec, so standard x402 client libraries work against Baton without modification. This page is the wire reference.
The payment header
A lodestone call carries an X-Payment header: a base64-encoded JSON object holding an EIP-712-signed EIP-3009 TransferWithAuthorization for an accepted payment asset — USDC or EURC.
{
"x402Version": 2,
"accepted": [{
"scheme": "exact",
"network": "base-sepolia",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"maxAmountRequired": "100000",
"payTo": "0x...",
"resource": {
"url": "https://api.relaystation.ai/v1/baton",
"description": "create a baton"
}
}],
"payload": {
"scheme": "exact",
"authorization": {
"from": "0x...",
"to": "0x...",
"value": "100000",
"validAfter": "0",
"validBefore": "<unix-timestamp>",
"nonce": "0x..."
},
"signature": "0x..."
}
}
Amounts are micros of the asset you pay in — USDC or EURC, 1:1 (100000 is $0.10 either way). The 402 challenge’s accepted[] advertises every asset accepted for the active network (each with its own asset contract); pick the one you’ll pay in and sign for it. validBefore is a Unix timestamp a few minutes out; nonce is 32 random bytes.
Signing domain
The signature is an EIP-712 typed-data signature over the EIP-3009 TransferWithAuthorization struct, using the chosen asset’s contract domain. For USDC:
{
"name": "USD Coin", // USDC; for EURC the name is "EURC"
"version": "2",
"chainId": "<chain-id>",
"verifyingContract": "<asset-contract-address>"
}
Both USDC and EURC use EIP-712 domain version: "2" on Base. The name field is "USD Coin" for USDC and "EURC" for EURC. Verify the live address for each asset at the per-chain registry exposed by /v1/baton/system/info.
EURC is also accepted and signs the same way — only the domain name and verifyingContract differ (EURC’s token name + its own contract). Don’t hardcode either: read the exact asset contract for your chosen asset and the active network from the 402 challenge’s accepted[], and use that asset’s EIP-712 domain.
TransferWithAuthorization(
address from, address to, uint256 value,
uint256 validAfter, uint256 validBefore, bytes32 nonce
)
Variable-amount payments (upto)
Some routes — the LLM tools (/v1/llm/*) — don’t know the exact price until the work runs (you pay for the tokens actually used). These use the x402 upto scheme: you authorize a maximum, and the server settles only the actual amount (≤ max).
The 402 challenge for an upto route advertises a scheme: "upto" entry whose extra carries the Permit2 details to sign against:
{
"scheme": "upto",
"network": "base-sepolia",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "<max-micros>",
"payTo": "0x...",
"extra": {
"assetTransferMethod": "permit2",
"spender": "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002",
"facilitatorAddress": "0x..."
}
}
spender is the x402 upto Permit2 proxy (a deterministic CREATE2 address, the same on every chain); facilitatorAddress is the settlement facilitator the witness binds. You sign a Permit2 permitWitnessTransferFrom (EIP-712, domain { "name": "Permit2", "chainId": <id>, "verifyingContract": "<Permit2>" } — no version field) with spender set to the proxy and a witness binding the merchant (payTo) + the facilitator.
The X-Payment envelope is the standard x402 wire — payload.permit2Authorization, with the payer carried inside as from:
{
"x402Version": 2,
"accepted": { "scheme": "upto", "...": "..." },
"payload": {
"signature": "0x...",
"permit2Authorization": {
"from": "0x...",
"permitted": { "token": "0x...", "amount": "<max-micros>" },
"spender": "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002",
"nonce": "<uint256>",
"deadline": "<unix-seconds>",
"witness": { "to": "0x...", "facilitator": "0x...", "validAfter": "0" }
}
}
}
Interop. This is the published x402 envelope — if you sign with the stock @x402/evm client, its permit2Authorization object is exactly what Baton reads; off-the-shelf x402 clients work directly, no translation. The signature is the canonical permitWitnessTransferFrom EIP-712.
The response carries the real on-chain tx hash and the actual amount charged (≤ your max) in PAYMENT-RESPONSE; the unused portion of your authorization is never pulled.
The success receipt
On success the response is 200/201 with the baton body and a PAYMENT-RESPONSE header — a base64-encoded receipt:
{"success": true, "transaction": "0x...", "network": "eip155:<chainId>", "payer": "0x...", "amount": "100000"}
Settlement is synchronous: the token transfer (USDC or EURC, whichever you signed for) is submitted on-chain as part of serving your call, and transaction carries the real on-chain hash. amount is what was actually charged (for upto, the actual ≤ your authorized max). The charge is recorded the moment the request is admitted.
How sub-cent calls settle (the sawtooth)
On-chain settlement costs gas — more than a sub-cent call is worth — so Relaystation doesn’t settle every penny-scale call on its own transaction. Instead:
- Every x402 call carries a signed authorization for a settlement chunk of $0.01 (the 402 challenge advertises this as
maxAmountRequired). - A call priced at or above $0.01 settles on-chain individually, on its own authorization.
- A call priced below $0.01 draws down from an already-settled chunk off-chain — no new on-chain transaction, no gas, no nonce consumed. When that chunk’s residual is exhausted, the next call settles a fresh $0.01 chunk on-chain, and subsequent sub-cent calls draw from it.
You authorize $0.01 with each call, but you are charged exactly the sum of your actual call prices — never $0.01 per call. On-chain settlement is batched at the chunk; your charges are exact. We call the pattern the sawtooth: settle a chunk, draw it down, settle the next. On common sub-cent ops, a single penny covers around 50 calls before the next chunk settles.
Rejection
A payment that fails admission returns 402:
{"x402Version": 2, "error": "PAYMENT_REQUIRED", "errorReason": "<reason>"}
Reasons include replay (the nonce was already used), authorization_expired, authorization_not_yet_valid, invalid_max_amount, invalid_token, merchant_not_configured, and settler_not_configured. The full list is in Errors.
Network
Baton runs on Base. The active asset list (USDC and EURC) is reported by /v1/baton/system/info; the contract addresses surface in the accepted[] array of the 402 response. The network field reads base for mainnet and base-sepolia for testnet (testnet remains available via a separate environment) — read the exact per-asset contract for the active network from the live 402 challenge’s accepted[] rather than hardcoding it.