Relaystation logo Relaystation

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.