Your agent just wants to verify another agent's identity before paying it. It fires off a tool call to EVIDIQ's MCP server, fully expecting an answer — and gets back an HTTP 402. No result, no friendly error message that says "give me money," just an accepts[] array sitting in the JSON body like a price tag the server expects the client to honor. Most tutorials on the x402 pattern stop right there with a hand-wave about "payment headers." We built EVIDIQ's x402 pay-per-call MCP server, so let's walk the actual bytes.
The Wire Protocol, Byte by Byte
An x402 pay-per-call MCP server is an HTTP endpoint that returns a 402 Payment Required challenge when a tool call needs payment, accepts EIP-3009 signed authorizations on retry, and only then executes the request. EVIDIQ's MCP server at evidiq.dev/mcp exposes exactly this pattern for its verify_agent tool, settled in USDT0 on X Layer.
Here's the literal sequence our verify_agent flow runs through:
- Client POSTs a JSON-RPC
tools/calltohttps://evidiq.dev/mcpwith the tool nameverify_agentand the target agent's identity as arguments. - Because the request carries no payment proof, EVIDIQ's server responds with HTTP 402 and a body shaped per x402 v2 — the
accepts[]array. - The client parses
accepts[], picks the asset and network it can actually pay on (USDT0 on X Layer for us, but the spec allows several), and signs atransferWithAuthorizationpayload off-chain. - The client retries the exact same JSON-RPC call, this time adding an
X-PAYMENTheader that carries the base64-encoded signed payload. - The server validates the signature, hands it to our settler contract, and only on a clean on-chain settlement does it run the actual trust evaluation — then returns the report.
That's the whole loop. The trick — and the part most implementations fumble — is that step 4 has to be byte-for-byte identical to step 1 in the JSON-RPC body. Otherwise the server can't replay the call against the payment it just verified. The 402 challenge itself carries no request-specific data; the binding between payment and call is reconstructed server-side from the header and the body hash.
A few details worth knowing before you start:
- HTTP 402 is real, not a 200-wrapped custom error. Middleware and proxies that strip "weird" status codes will silently break your client.
accepts[]is plural because x402 v2 lets a server advertise multiple rails in parallel (different chains, different tokens). The client picks one.- The
X-PAYMENTheader is the v2 transport. Older drafts used different names; if you're copy-pasting from a 2026-era tutorial, double-check. - The settle step is what gates execution. We never run the trust evaluation before the on-chain receipt confirms. That ordering is non-negotiable for us.
If you want to see the protocol on the wire before writing code, the playground at evidiq.dev/playground shows every step including the raw signed payload.
Reading an accepts[] Object
The accepts[] array is the contract between server and client, and getting any field wrong burns time. Here's what EVIDIQ returns for a verify_agent call on USDT0 / X Layer, with each field explained.
{
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "x-layer",
"amount": "100000",
"asset": "0x...USDT0",
"payTo": "0x...EVIDIQsettler",
"extra": {
"name": "USDT0",
"version": "1"
}
}
]
}
Field by field:
scheme— payment scheme."exact"means pay exactly the quotedamount. x402 also defines"upto"(variable metering), which we don't use yet.network— the chain slug."x-layer"is X Layer mainnet. v2 uses simplified slugs; if your code expects CAIP-2 IDs (eip155:196), normalize first.amount— smallest unit. USDT0 has 6 decimals, so"100000"is 0.1 USDT. Passing"0.1"gets you rejected.asset— the ERC-20 contract address. Don't hardcode it; read it from the challenge so a future change doesn't break your client.payTo— destination address. Not EVIDIQ's hot wallet directly — it's the per-call settlement address tied to the verification request.extra.nameandextra.version— required for the EIP-712DOMAIN_SEPARATOR. Hardcodingname = "USDT"when we send"USDT0"invalidates every signature. We'll see this exact failure in the next section.
The most common mistakes integrators hit, in order:
- Treating
amountas a human-readable float. - Hardcoding
assetorpayToinstead of reading from the challenge. - Forgetting to validate
payToagainst a policy ("am I about to pay an address I trust?"). - Ignoring
extraentirely because it looks optional.

EIP-3009 Without the Cryptography Lecture
EIP-3009 is a token standard that defines two off-chain signed messages: transferWithAuthorization and receiveWithAuthorization. The "authorization" part is the trick — instead of the payer calling a contract function (which needs gas and a live nonce), the payer signs a structured EIP-712 message saying "I authorize the contract to move N tokens from me to address X, valid until time T, with a unique nonce Nn." Anyone holding that signature can submit it to the contract later. The contract verifies the signature against ecrecover, checks the nonce hasn't been used, and executes the transfer — and crucially, the submitter pays the gas.
For our x402 pay-per-call MCP server this matters because:
- The agent making the call doesn't need to hold any native gas token on X Layer. It only needs the USDT0 balance.
- EVIDIQ's settler submits the authorization, pays gas, and only after the token actually moves do we run the verification.
- The nonce is bound to the signature — replaying the same authorization a second time fails the contract's
authorizationStatecheck, so it's safe to log the signed payload.
Concretely, the X-PAYMENT header carries a JSON document with the EIP-712 type hash, the message struct (from, to, value, validAfter, validBefore, nonce), the signature (v, r, s), and a client identifier. On our side:
- We reconstruct the EIP-712 digest using
extra.name,extra.version,chainId, andverifyingContractfrom theaccepts[]we returned. - We run
ecrecover(digest, v, r, s)and confirm it equals thefromfield. - We call
transferWithAuthorization(...)on the USDT0 contract and wait for the receipt. - If the receipt succeeds, we run the trust evaluation and return the report.
If the receipt reverts, EVIDIQ returns 402 again with a more specific error in the body. That's the debug loop — bad payment, fix, retry — and it's why we put structured errors in the response rather than opaque 500s. Honest engineering over optimistic 200s.
What Happens on a Bad Signature or Late Nonce
We've shipped this for over a year in 2026 and these are the failure modes that actually come up. Each one produces a distinct error code you can match against in your client.
-
Expired
validBefore— The signed message includes a unix timestamp after which the authorization is void. If the client signs withvalidBefore = now + 30sand X Layer is congested, by the time our settler submits, the contract reverts withFiatTokenV2_2: authorization expired. We surface this as HTTP 402 with body{"error": "authorization_expired", "validBefore": 1234567890}. -
Reused nonce — Each
transferWithAuthorizationcarries a 32-byte nonce. Once used, the contract'sauthorizationState[from][nonce]flips totrueand any later submit reverts. We return{"error": "nonce_already_used"}. Most often this is a client retrying the same call after a network blip without re-signing. -
Wrong domain separator — The single biggest time sink for integrators. The EIP-712 domain is
(name, version, chainId, verifyingContract). If a client hardcodesname = "USDT"but ouraccepts[].extra.nameis"USDT0", the digest is wrong andecrecoverreturns the zero address. The contract then reverts onfrom != address(0). We return{"error": "invalid_signature", "hint": "check domain separator"}. -
Insufficient balance — USDT0's
transferWithAuthorizationpulls directly; no allowance dance. But if the payer's balance is belowamount, the underlyingtransferreverts with a generic ERC-20 underflow. We catch this and return{"error": "insufficient_balance"}. -
Chain mismatch — The signed
chainIdin EIP-712 doesn't match the network our settler is actually submitting to. We return{"error": "wrong_chain", "expected": "x-layer", "signed": "<other>"}. -
Replay with mutated body — Client re-signs but changes a field of the JSON-RPC call. The server detects the hash mismatch and returns
{"error": "payment_call_mismatch"}. Don't suppress this; it's there to prevent paying for one thing and asking the server to do another.
For each case, EVIDIQ's response includes the specific field the integrator needs to fix. Giving the client a precise token rather than a bare "payment failed" cuts debug time by an order of magnitude — and it's why we have a small army of structured error codes instead of one catch-all.

