Anatomy of an x402 payment: every header, every signature, every settlement
Short answer: An x402 payment is four HTTP messages and one signed EIP-3009 authorization. The server quotes, the client signs locally, replays with X-PAYMENT, the facilitator settles on Base, the server returns data. End to end: ~600 ms.
The previous post — Why x402, not API keys — argued that autonomous agents need a different auth model. This post zooms into the wire. We trace exactly one paid call to Luxe Oracle and show what each side does at each step.
The four messages at a glance
Agent Server Facilitator Base
│ │ │ │
│ ① GET /api/v1/monitor │ │ │
│ ───────────────────────► │ │ │
│ │ │ │
│ ② 402 + quote JSON │ │ │
│ ◄─────────────────────── │ │ │
│ │ │ │
│ (sign EIP-3009 locally) │ │ │
│ │ │ │
│ ③ GET ... + X-PAYMENT │ │ │
│ ───────────────────────► │ verify+settle │ │
│ │ ───────────────────────► │ broadcast │
│ │ │ ────────► │
│ │ ◄─────────────────────── │ receipt │
│ ④ 200 OK + data │ │ │
│ ◄─────────────────────── │ │ │
① The unpaid request
The agent makes a vanilla GET. No auth header. No payment. The request is identical to a free endpoint:
GET /api/v1/monitor/sg/hermes/H086962CK0G HTTP/1.1
Host: api.luxe-oracle.com
Accept: application/json
User-Agent: my-agent/1.0The server's middleware (in our case @x402/next) sees no X-PAYMENT header and short-circuits: it returns a quote instead of the data.
② The 402 quote
HTTP/1.1 402 Payment Required
Content-Type: application/json
Access-Control-Expose-Headers: WWW-Authenticate
WWW-Authenticate: x402
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"maxAmountRequired": "1000", // 0.001 USDC, 6 decimals
"payTo": "0xMerchant…",
"resource": "https://api.luxe-oracle.com/api/v1/monitor/sg/hermes/H086962CK0G",
"description": "Real-time Hermès stock query",
"maxTimeoutSeconds": 300,
"extra": { "name": "USD Coin", "version": "2" }
}]
}The fields the agent cares about:
scheme: "exact"— pay the exact amount; no overpayment, no haggling.network+asset— Base mainnet, USDC contract address.maxAmountRequired— already in token base units (USDC has 6 decimals, so 1000 = $0.001).resource— the canonical URL bound into the signature; replay against a different URL fails.
③ Sign locally, replay with X-PAYMENT
The agent constructs an EIP-3009 transferWithAuthorization payload — the standard meta-transaction format USDC supports — and signs it with the agent's private key. Crucially, signing is local: no RPC call, no gas, no on-chain footprint yet.
// Authorization payload (EIP-712 typed data)
{
from: "0xAgentWallet…",
to: "0xMerchant…",
value: "1000", // 0.001 USDC
validAfter: 0,
validBefore: 1714723200,
nonce: "0x…32-byte-random…"
}
// Signed → 65-byte signature, base64-encoded into the headerThe agent replays the original GET, this time with the header:
GET /api/v1/monitor/sg/hermes/H086962CK0G HTTP/1.1
Host: api.luxe-oracle.com
X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZSIs…The header value is a base64-encoded JSON wrapper containing the scheme, network, the signed authorization, and the same resource URL. Bind-by-resource is what stops a leaked header from being replayed against a different endpoint.
Verify + settle: the facilitator's job
The server does not need a private key. Instead, it forwards the signed authorization to a facilitator— typically Coinbase's CDP. The facilitator does two checks before broadcasting:
| Check | Catches |
|---|---|
EIP-712 signature recovers from | Tampering with amount, recipient, nonce |
from has ≥ value USDC | Underfunded wallet, late settlement risk |
nonce not yet used on-chain | Replay of a previously settled authorization |
resource matches request URL | Header lifted to a different (cheaper) endpoint |
Verified, the facilitator broadcasts the transferWithAuthorization on Base. USDC moves from agent to merchant. The facilitator returns the tx hash to the API server, which now has cryptographic proof of payment.
④ The data response
HTTP/1.1 200 OK
Content-Type: application/json
X-Payment-Settlement: 0x9f1c…
{
"status": "ok",
"brand": "hermes",
"region": "sg",
"model": "H086962CK0G",
"in_stock": true,
"last_update": "2026-05-03T12:14:08Z"
}The settlement tx hash echoes back in a custom header so the agent can prove on-chain it paid. From the agent's code, the entire two-round-trip dance is one call:
const res = await fetchWithPayment(url);What latency actually looks like
Empirically, on Base mainnet with the CDP facilitator, a Luxe Oracle paid query has settled in 400–800 ms end-to-end. The breakdown:
| Step | Typical |
|---|---|
| ① Initial GET → 402 quote | ~50 ms |
| Local EIP-712 signature | < 5 ms |
| ③ Replay GET → server | ~50 ms |
| Facilitator verify + broadcast | ~150 ms |
| Base inclusion (1 block ~2 s, but tx surfaces faster) | ~200–500 ms |
| ④ 200 OK + data | ~50 ms |
Sessions cut this drastically: pre-pay once, then every subsequent request takes the bottom row only (~50 ms — no settlement at all). That's why our session endpoint exists for high-volume agents.
Threats the protocol stops
- Replay across endpoints. Resource binding in the signed payload means the same X-PAYMENT cannot be used against a more expensive route.
- Replay against same endpoint. EIP-3009 nonces are recorded on-chain. A second settlement of the same authorization fails at the contract level.
- Server with stolen funds. The server never holds the agent's key. Authorization specifies
payTo; nothing else can be drained. - Quote tampering. The agent signs the quote it accepted. A man-in-the-middle that flipped the amount would invalidate the signature.
What to read next
- Why x402, not API keys — the higher-level case for adopting it.
- Luxe Oracle OpenAPI spec — exact endpoints, prices, and Bazaar metadata.
- Integration docs — copy-paste client code in TS / Python.