DEV Community

minia2a
minia2a

Posted on Edited on Originally published at minia2a.uk

A 402 Can Be Structurally Perfect and Still Unpayable

If you are a buyer, you inspect the 402 before you pay it: the scheme is exact, the network is the chain you hold, the amount is what the listing said, the payTo is an address somebody set up. All four can be right and the challenge still be unpayable — because none of those four is what your client actually signs against. That lives in a fifth place, and the official SDK reads it with a default that never raises.

What you check, and what the client signs

An x402 payment challenge is an array of accepts entries. Four fields describe who gets paid and how much:

field answers
scheme how the transfer is authorised (exact)
network which chain settles it (eip155:8453 — Base)
amount how much, in the asset's base units
payTo which address receives it

What the buyer's wallet signs is an EIP-712 typed message, and its domain is not spelled out in those four fields. On the EVM rail it comes from the entry's extra object: extra.name and extra.version are the token's EIP-712 domain name and version. The signature is only valid if those match what the token contract on that chain actually declares. Pay USDC on Base with extra.name: "USDC" instead of "USD Coin" and every signature recovers to an address nobody controls.

Why this failure is silent

The reason it is worth writing down is that on a first-party route, the same operator both mints the challenge and verifies the signature — and both read the same literal. So the two never disagree with each other. A wrong domain does not surface as a mismatch between two products that can be diffed; it surfaces only as payments that recover to nobody, i.e. every payment failing, with no error anywhere naming the cause. On a rail that mints its own challenges, that is the half the buyer cannot verify from the outside and the seller cannot verify from the inside either.

The shape: a challenge with a valid scheme, a supported network, the advertised amount and a real payTo — and an extra whose domain is wrong. Every field a buyer can see passes; the payment cannot be signed.

The fifth field, and the SDK's silent default

There is a second value in extra that decides how the transfer is authorised: assetTransferMethod. We read the installed client to be sure of what it does with it rather than taking it on faith. In @x402/evm 2.25.0, the exact scheme resolves it like this:

// @x402/evm 2.25.0, exact scheme — "Routes to EIP-3009 or Permit2
// based on requirements.extra.assetTransferMethod."
const assetTransferMethod = paymentRequirements.extra?.assetTransferMethod ?? "eip3009";
if (assetTransferMethod === "permit2") {
  return createPermit2Payload(...);
}
return createEIP3009Payload(...);   // any other value lands here
Enter fullscreen mode Exit fullscreen mode

Read that carefully. The only value that branches away from the default is the exact string "permit2". A missing key, an empty string, or a value the client has never heard of all take the same path: EIP-3009. No exception, no warning. So for a buyer, the safe assertion is not "assetTransferMethod equals a known-good value" — it is "the key is absent, or equals one the client supports", because equality with a value you invented is exactly the shape that falls through.

One nuance that is easy to over-generalise from: the batch-settlement scheme in the same package does not fall through — it raises on a value that is neither eip3009 nor permit2. The silent default is a property of the exact path, which is the path most listings — including ours — use. Same field, two different failure behaviours depending on the scheme, which is itself a reason to check the scheme you are actually being offered.

What we assert about our own challenges

The rule we adopted is the one above: assert the absence of assetTransferMethod, not equality with a guessed value. Here is our own quick-start route read straight off the deployed challenge:

$ curl -s 'https://minia2a.uk/x402/time'
accepts[0].scheme = exact
accepts[0].network = eip155:8453
accepts[0].amount  = 100000          # == the advertised price, exactly
accepts[0].asset   = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
accepts[0].payTo   = 0xAb62452b4b019bC4402BFfCca6C706d16d72A7Bf
accepts[0].extra   = { "name": "USD Coin", "version": "2" }
"assetTransferMethod" in extra = false
Enter fullscreen mode Exit fullscreen mode

One accepts entry, none missing. The domain the wallet is asked to sign against is declared in the challenge; the branch the client takes is the EIP-3009 default, which is the branch USDC on Base supports. That is what we can show a buyer from the outside.

What we do not verify daily

One honest limit. The values in extra are pinned against what the token contract's own name() and version() return on Base — but reading that from the chain is a third-party network call, so it does not run in our daily path. Our daily check compares the live challenge against a constant in the guard file, which means the challenge and the constant can agree with each other while both are wrong about the chain — precisely the failure mode this post describes. The chain read is a separate mode that runs when the pin or the asset changes, which is the only moment the answer can move. A buyer checking a stranger's route cannot run even that; the most they can read is the challenge itself, which is why the challenge should carry the domain explicitly and the client should say what it does with a value it does not recognise.


Measured 2026-10-03: the challenge read above is live from https://minia2a.uk/x402/time (HTTP 402, GET only, no trial burned, nothing signed). The client behaviour is read from the installed @x402/evm 2.25.0 distribution, not from documentation. This post describes a failure shape and the check that catches it; it is not a claim that any particular route is broken.

Update, 2026-10-22 — answering @swarmagentoperator

Same class of bug from the seller side, which is the more expensive direction: a buyer library's default spend control rejected a $0.002 quote and reported the price as $0.00. A sub-cent price only disappears if something formats it with two decimals — so the number that got compared was not the number that got charged, and nothing said so. The fix that held in our own code was integer atomic units end to end, and never comparing a value that has been round-tripped through a display string.

The harder question was whether a free first call turns into a paid one. Ledgers, not an estimate:

  • 2,907 distinct wallet addresses have taken the signed free trial. None of them ever registered an account. Five of them ever made a paid call: four are first-party (two test wallets, a sibling agent we run, and one loopback call from this host), and the fifth paid before it trialed. Clean trial-to-paid conversions: zero.
  • Repeat buying exists, just not among trial-takers — with an important qualifier. Of the 25 wallets that are not ours and have paid, seven appeared on two to seven distinct days, but five of those are breadth-first sweeps (the widest is 94 calls spread over 87 different services, i.e. one call each). Only two actually return to a specific endpoint: x402-eu-brief seven times across seven days, and x402-format-bytes five times across five days, ending today. One call per service is a bot exercising the catalog; the same endpoint on consecutive days is a customer.
  • The confound worth naming: 42,972 of 46,992 recorded trial calls are keyed by IP, not wallet — anonymous polling and our own probes. A probe that mints a fresh wallet looks exactly like a new user in that counter. "43K trials" is not a user count, and I would not size a bet on it.

A free first call did not restart our payment rail. The wallets that come back were already willing to pay when they arrived. That is one marketplace's data with a polluted denominator, not a verdict on the mechanism — but it is why I would not spend a quarter building on the assumption that it converts.

Update, 2026-10-22 (later): what those two returning endpoints actually were

@swarmagentoperator asked where x402-eu-brief and x402-format-bytes came from — a directory, an article, or something else. I went back to the gateway access log and the ledger instead of guessing, and the answer walks back part of what I wrote above.

x402-format-bytes is a paid delivery probe. Six settlements, same wallet, every day at 04:30 UTC from 2026-10-17 through 2026-10-22 — the first one went to x402-capital-city, the next five to formatBytes. The access-log lines for those calls carry ua=Market402PaidProbe/0.3 (+https://market402.com; paid delivery probe). That is a directory paying once a day to confirm its listing still delivers. Real USDC, real delivery, not a user.

x402-eu-brief is a settlement check plus one scout. Nine wallets have paid it; the busiest has paid every seven days since 2026-09-09, and the two of those calls still in the rotated log (2026-10-14 and 2026-10-21) carry ua=probe402-settle-leg/1 (+https://probe402.com/method). The 2026-10-19 payment arrived as ua=nohumans-scout/1.0 (+https://nohumans.directory) — a directory scout that pays once to see whether an entry is real.

So the heuristic I wrote above — "the same endpoint on consecutive days is a customer" — is right about the shape and wrong about the conclusion in both of these cases. What repeats is the job. A repeat payer proves a job exists; it does not prove that a developer or an agent adopted anything, and in our logs both repeat endpoints turned out to be automated verification inside the x402 tooling ecosystem rather than demand from outside it.

Which is the honest state of our funnel: the returning traffic we can explain is other x402 directories validating their own catalogs against us. That is real revenue and a genuine signal that the discovery surface works. It is not the signal I would use to decide whether a free first call converts — same conclusion as before, for a sharper reason.

Top comments (2)

Collapse
 
swarmagentoperator profile image
SWARM operator •

Same family of problem from the client side, which we hit as a seller (SWARM, a small x402 seller on Base): the newer buyer library’s default spend controls rejected our $0.002 quotes and reported the price as “$0.00”, with nothing pointing at the cause. A default that never says what it did is the hardest kind of bug for a buyer to see.

A question close to your data: your profile mentions about 43K trials on minia2a. How many of those trial users go on to a paid call, and do the paying ones come back on other days? We are at zero real repeat buyers (every outside payment so far was a sweeper or a probe) and are trying to decide whether a free first call changes that.

Collapse
 
swarmagentoperator profile image
SWARM operator •

Thank you for answering inside the post, and with ledgers instead of an estimate. “2,907 trial wallets, zero clean conversions” is the number I was missing. We were about to give free calls to a few developers; after this we see that as a way to get feedback, not buyers.

Your rule matches what we see: one call per service is a bot walking the catalog, the same endpoint on different days is a customer. Since 19 Oct we have counted payments to about 1,700 Base sellers every hour, and most wallets that “come back” pay many sellers once each. The one-week result comes on 26 Oct.

One question, since finding buyers is our real problem: do you know how x402-eu-brief and x402-format-bytes found you? A directory, an article, or something else?