The first time a developer sees HTTP 402 in production, they usually assume it's a misconfigured proxy. It's not. It's a deliberate challenge: "the resource exists, but you haven't paid yet." We've spent the last several months building EVIDIQ around that one status code, and what follows is the actual wire protocol our x402 pay-per-call MCP server speaks — every header, every field, every failure mode we've debugged ourselves.
The Wire Protocol, Byte by Byte
An x402 pay-per-call MCP server is an HTTP endpoint that gates Model Context Protocol tool calls behind an on-chain micropayment. A client POSTs to /mcp, gets a 402 challenge, signs an EIP-3009 transferWithAuthorization off-chain, retries with an X-PAYMENT header, and the server verifies, settles on X Layer in USDT0, then runs the call. EVIDIQ operates exactly this flow at evidiq.dev/mcp.
The short version: HTTP 402 stops being a joke the moment it carries a settlement path.
Here's the round-trip as it actually happens on our server:
- Client opens a JSON-RPC request —
tools/callforverify_agent, with{ agent_url: "..." }. - We respond
402 Payment Requiredwith aWWW-Authenticateheader containing a base64-encoded payment challenge. - Client parses the challenge, signs a
transferWithAuthorizationfor the quoted amount, and sends the same request again — this time withX-PAYMENT: <base64-signed-payload>. - Our settler verifies the signature, pulls the funds on X Layer, runs the trust probe, and returns the JSON-RPC result.
No session cookies. No API keys sitting in .env. Every call is a fresh, atomically-priced transaction. Our settler either succeeds in full — payment cleared, probe ran, signed report returned — or it fails closed and the client gets a clean 402 with an error code.
If you want to try it without writing a client first, the EVIDIQ playground lets you paste a challenge, sign it, and watch the retry happen in the browser. Honest take: most people learn the protocol fastest by breaking it once on purpose.
Reading an accepts[] Object
The challenge that comes back in WWW-Authenticate is x402 v2, and its body is an accepts array. We send exactly one entry today, but a client should still loop over the array — that's the spec, and ignoring the [] has bitten integrators more than once.
Each accepts[i] object looks roughly like this:
{
"scheme": "exact",
"network": "x-layer",
"amount": "10000",
"asset": "USDT0",
"payTo": "0xEVIDIQ...settler",
"extra": {
"name": "USD₮0",
"version": "1"
}
}
Field by field:
- scheme —
"exact". The only scheme we accept right now. Anything else ("upto","stream", custom) gets a hard reject before we even look at the signature. If a client blindly signs, it pays for a call that will never run. - network —
"x-layer". The chain id. Don't infer it from a chainlist lookup — read it from the challenge. We chose X Layer for sub-cent fees and a USDT0 contract that's already deployed and audited. - amount — base-10 string of the smallest unit. For USDT0 that's 6 decimals, so
10000is$0.01. Off-by-decimal bugs here are catastrophic; a client that treats this as a float will eventually pay$10,000for a$0.01probe. - asset —
"USDT0". The token symbol. The real on-chain address is the EIP-712 domain, not this string — never resolve a token fromassetalone. - payTo — the recipient. This is our settler address on X Layer. The funds land here and are then routed per our settlement policy.
- extra.name / extra.version — used to construct the EIP-712 domain separator (
USD₮0,1). If your client hardcodes"USDT"from a copy-paste, the signature will verify against the wrong domain and fail settlement.
The biggest mistake we've seen in integration logs: clients treat accepts[] as a single object. It isn't. Loop it, prefer the lowest-amount match, and only sign when scheme === "exact" is in the set.

EIP-3009 Without the Cryptography Lecture
EIP-3009 gives us transferWithAuthorization, a message that says: "I, the holder of this balance, authorize address X to move N units of token T from me, at any time before timestamp Y, exactly once, identified by nonce Z." The signature is over an EIP-712 typed struct, which means the client can produce it offline with no gas and no chain access. That's the entire trick — the payer doesn't need ETH on X Layer, just USDT0.
Our settler takes that signed blob and calls transferWithAuthorization on the USDT0 contract on the payer's behalf. The contract checks the signature against ecrecover, verifies the nonce hasn't been used, checks validBefore, and moves the tokens. One transaction, fully on-chain, atomic with the MCP call's settlement.
The sequence our settler runs:
- Decode the base64
X-PAYMENTheader into the EIP-3009 payload. - Reconstruct the EIP-712 domain from
accepts[].extra. - Call
transferWithAuthorizationon USDT0 with the payer's signature. - Wait for
receipt.status === 1on X Layer. - Only then run
verify_agent()and return the JSON-RPC result.
If step 4 fails, step 5 never happens. The client gets a 402 with an error sub-code, and the funds either stayed put (revert) or were moved but never settled against a tool call — that path is logged and surfaced in the EVIDIQ Sentinel docs, our tamper-evident audit trail.
Worth noting: EIP-3009 is what makes x402 viable for sub-cent calls. Without it, every probe would burn more on gas than the probe itself costs. With it, $0.01 is a real, profitable price point for an agent operator to charge — and the math on a $0.01 verification is what makes our x402 pay-per-call MCP server worth running as a service at all.
What Happens on a Bad Signature or Late Nonce
Things go wrong. Most of the time, they go wrong in the client's signer, not on our side. Here's a field guide so you can debug without pinging us.
- Expired
validBefore— the EIP-3009 message included a deadline. If the client retries too late, the USDT0 contract reverts withauthorization is expired. We surface this as a 402 witherror: "expired". Fix: setvalidBeforetonow + 60seconds, and re-fetch the challenge if you're outside that window. - Reused nonce — EIP-3009 nonces are single-use, on-chain. A client that retries the same signed blob twice will see the first call succeed and the second revert with
authorization already used. Fix: every retry must use a freshly signed payload with a fresh nonce. Don't cache. - Wrong domain separator — the EIP-712 hash includes
name,version,chainId,verifyingContract. Our challenge usesname: "USD₮0",version: "1",chainId: 196(X Layer). Hardcode any of those wrong andecrecoverreturns a garbage address. The USDT0 contract then reverts oninvalid signature. Fix: read every domain field fromaccepts[].extraand the chain id fromaccepts[].network— never from a constant. - Insufficient balance or allowance —
transferWithAuthorizationstill needs the payer to hold USDT0. If the wallet is empty, you get a clean ERC-20 revert. Fix: top up and try again; this isn't an x402 issue. - Amount mismatch — our settler checks that the signed
amountmatchesaccepts[].amountexactly. If the client signs9999against a challenge for10000, the call is rejected before any gas is spent. Fix: sign the amount from the challenge, character for character.
We log all of these (without payload content) so integrators can correlate. For the full forensic flow including how we anchor evidence on 0G Storage, see the EVIDIQ Notary docs. If you're the operator of the agent being verified, the EVIDIQ Operator docs walks through the other side of the call.
The signature we return — an EIP-191 signed trust score report — is itself verifiable. Any client can recover the EVIDIQ public key from it, re-hash the canonical report, and confirm the contents match. We made it deterministic on purpose. A trust score that nobody can re-derive is just an opinion.

