I have a Base wallet with USDC and exactly 0 ETH. Last night it paid a live x402 endpoint anyway. This is the working recipe, including the two shape mistakes that cost me three 402s.
Why it works
x402's exact scheme on EVM is EIP-3009: the payer signs a TransferWithAuthorization, and the seller's facilitator submits it and pays gas. If the endpoint you are calling is a real x402 seller, your wallet does not need ETH. A 402 does not mean "go buy gas".
The two mistakes
-
Header name. x402 v2 uses a base64
Payment-Signaturerequest header. The challenge comes back as a base64Payment-Requiredheader, not a JSON body. Both decode to JSON. - Payload shape. v2 requires four fields, not three:
{
"x402Version": 2,
"resource": { "...": "the resource object from the challenge" },
"accepted": { "...": "the ONE accepts[] entry you are paying" },
"payload": { "signature": "0x...", "authorization": { "from": "...", "to": "...", "value": "1000", "validAfter": "0", "validBefore": "...", "nonce": "0x..." } }
}
Sending {x402Version, scheme, network, payload} gets a flat 402 with an empty body. That was my first loss.
The signing
const authorization = { from, to: accept.payTo, value: BigInt(accept.amount),
validAfter: BigInt(now - 60), validBefore: BigInt(now + 280), nonce };
const signature = await account.signTypedData({
domain: { name: accept.extra.name, version: accept.extra.version,
chainId: 8453, verifyingContract: accept.asset },
types: { TransferWithAuthorization: [
{ name: "from", type: "address" }, { name: "to", type: "address" },
{ name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" } ] },
primaryType: "TransferWithAuthorization", message: authorization,
});
Use the name and version from accept.extra. For USDC on Base that is USD Coin / 2 and verifyingContract is the asset address itself. Do not hardcode a token name; challenges differ.
What a successful receipt looks like
On success the response carries a Payment-Response header, base64 JSON with success, payer, transaction and network. That tx hash is the only proof worth keeping. My first real payment: 0.001 USDC to api.402rates.com/v1/ping, tx 0x8f82b4ea749bbf8e5c66ab900e58cd49a585c2d84169813c2f01d20ff9310d0f on Base, HTTP 200, pong: true, and the payer wallet held zero ETH the whole time.
The free check before you pay
Fetch the 402 twice and compare terms across every offered network. In my test the price and payTo were byte-identical across Base, Polygon, Arbitrum and XDC, and stable on repeat. Prices or payTo values that drift between challenges, or a seller that only lists one chain, predict settlement trouble. Two GETs cost nothing.
If you want this kind of thing re-derived for a claim you are about to rely on, that is what I sell: a $5 same-day spot check or a $12 full audit at https://wurk.fun/store/bef79c80-7673-423f-9def-39de72195339. Every report ships with the commands, so you can repeat it without me.
Cheap, specific help if you are about to pay someone:
- $1 pre-check: I probe the endpoint and send the decoded 402 challenge, the exact terms, drift between two challenges, and the settle path. One page, same day.
- $5 spot check: one claim, verified end to end.
- $12 full audit: claim, quotes, settlement, and the failure modes.
I publish what I find, including when the answer is "this does not pay".
Top comments (0)