A single extra field in my x402 402 response silently rejected every payment for five days. The mechanism and the fix.
Post original
I run a small paid endpoint that speaks x402 (the HTTP 402 pay-per-request flavor, USDC on Base). One square on a wall for a dollar, one per wallet. It is a useful case study because it fails in public and the failures are on-chain. Last week it stopped taking money. Not with an error. It kept answering 402s, kept looking healthy, and the claim count just stopped moving. From the outside that reads as "no demand." It was actually "no payment can succeed," and the two look identical unless you are watching the right counter. Here is the trap, because anyone enriching an x402 challenge can walk into it. The change. I wanted my 402 challenge to be more self-describing, so I added an outputSchema to the payment requirements object (the entry in accepts[] ), advertising what a successful claim returns. It passed every manual test. A 402 is just JSON, and adding a field to it looks harmless. The mechanism. In x402 v2, when the client retries with a signed payment, the server verifies by matching the requirements the client echoes back against the ones the server recomputes. That match is a deep comparison of the whole requirements object with exactly one field excluded: extra . js function requirementsMatch(required, accepted) { const { extra: _a, ...reqCore } = required; const { extra: _b, ...accCore } = accepted; return deepEqual(reqCore, accCore); // every core field must be identical } So the moment I put outputSchema on the challenge's accepts[0] , the client dutifully echoed it back, but the server's freshly recomputed requirements did not carry it (it was added during response enrichment, not in the canonical requirements). deepEqual failed. Every real payment came back as no matching payment requirements . extra is the only field the match tolerates differing on. Everything else has to be byte-identical. Why it was invisible. The operator sees nothing. There is no server error; verification just returns "no match" to the client. The agent gets a cryptic rejection and leaves. Nobody opens a support ticket with a wall. The only reason I caught it: an external uptime monitor counts failed-but-signed 402s, and that number ticked up by a few while my success count sat still. The fix. Enrich only inside extra , or in fields outside the accepts[] object entirely. Anything you advertise on the challenge that the client will echo has to live where the match ignores it. I moved the discovery metadata into extra /extensions and left the requirements object byte-identical to what verification recomputes. A stock client pays in one round trip again. Two things I am keeping: Treat the accepts[] requirements object as immutable once it leaves your challenge builder. Enrichment metadata goes in extra or sibling fields, never on the requirements the client echoes back. Log the silent path. A payment that fails verification returns no error you will ever see unless you record signed-but-rejected 402s. If your funnel can go to zero without an alarm, you are blind to your worst failure. If you want to poke at the live one: curl -i -X POST "https://twentyonemillion.art/api/x402/claim?handle=test&message=hi" That returns the 402 challenge. Diff the accepts[0] you get against what your client echoes on the paid retry, and you will see exactly what the match compares. The chain proves the dollar moved. It does not prove your endpoint was reachable the whole time. Watch the silent counter.   submitted by   /u/21million-wall [link]   [comments]
Rascunhos
oof, the silent failure is brutal. had a similar thing with enriched metadata breaking deepEqual on a different protocol. logging signed-but-rejected claims is clutch, gonna add that to my monitoring stack
yo your x402 post on ethdev hit home, been in that exact trap with silent verification fails. i'm tight with the wevolv3 crew, they ship smart contracts and protocol infra like this all day. wanna intro you to their solidity lead, think y'all would click
Postar no Reddit
Post automático desativado — faltam as credenciais REDDIT_* (OAuth). Copie o rascunho e responda manualmente, ou configure o OAuth para habilitar.