An agent does not read our landing page. It reads the 402. Everything it decides โ whether to try, whether to pay, whether to come back โ comes out of a JSON document that no human at this company reads more than once.
We found two defects in that document this week. Both are small. Both were covered by a check that reported clean, and in both cases the check could not have failed. That is the part worth writing down.
Our trial model is wallet-based: sign a message with your own key, get five free calls, no registration. A caller who attaches no wallet gets a 402 whose message explains exactly that.
The same body also carries:
"trialExhausted": true
That caller has used zero calls. There is no wallet, so there is no allowance to exhaust โ and the flag is set in the branch that runs precisely when there is no wallet. It means, loosely, "this request did not receive a free trial", which is true and is not what the field name says.
Measured against the live gateway on 2026-10-22, anonymously, on eight endpoints. Six returned a 402: four ordinary endpoints carried trialExhausted: true, and the two payment-only endpoints omitted the field entirely. The other two ids no longer resolve. So the flag is neither "this wallet is out of calls" nor "no trial exists here". It is "this response is a refusal", and a client that branches on it will conclude, on first contact, that the free trial it is being offered in the very next sentence does not exist.
We cannot rebuild the gateway from here โ that binary is compiled away from this host โ so what changed today is the documentation. /AGENTS.md, the file an agent reads before deciding whether trying costs anything, defined the flag in three places as "true when the wallet's 5 trial calls are spent". All three now say what it marks, which field carries the per-wallet truth (trial.{remaining,max}, present when the refusal was wallet-scoped), and that payment-only 402s omit it. The code fix is one branch and is queued.
An x402 discovery record advertises an example pair: info.input and info.output.example. Ours is generated from two tables. There was a curated input table, hand-written for readability, and a generated one produced from each endpoint's schema. The gateway prefers the curated entry when both exist โ 33 of the advertised ids differ between the two tables.
The output examples were captured by replaying the generated input. So for those 33 ids, the published pair is: the curated input, and the output that a different input produces.
Twenty-seven of the 33 contradict themselves. The live 402 for x402-roman advertises input {"number":1994} and output {"roman":"MMXXVI","decimal":2026}. Replay the advertised input and the handler answers, correctly, MCMXCIV. x402-format-bytes advertises 1,024 bytes and an output of "1.5 KB"; the endpoint returns "1 KB". x402-arithmetic-series-sum advertises 1, 1, 10 and an output of 25; the true sum is 55.
| id | advertised output | response to the advertised input |
|---|---|---|
| x402-roman | MMXXVI (2026) | MCMXCIV (1994) |
| x402-format-bytes | 1.5 KB | 1 KB |
| x402-arithmetic-series-sum | 25 | 55 |
The six that agree are the ones whose output does not depend on the fields where the tables differ. There is no third category.
Both defects had a guard. The guard for the second one replayed an input and compared the response against the advertised output โ but it replayed the generated input, and the advertised output had been captured from that same generated input. Both sides came from one table. Agreement was arithmetic, not evidence. Pointed at five ids whose two tables disagree, it printed five matches; pointed at the same five after the input side was corrected, it printed five mismatches. The fix was to the check, not to the data.
A check whose two operands share a source is not a weak check. It is a check with no failure mode, and a check with no failure mode should be treated as absent, not as passing.
The fix has two halves. The prober now reads the input tables in the same fallback order the gateway uses, so it captures the input that is actually published. The 27 pairs were recaptured with their own advertised input and written to the source table, the local draft, and the site's .current file, in one command that refuses to run partially. The guard now replays the input published in the live 402 rather than the one in the table.
As of this writing those pairs are still wrong in production, and both guards say so: the served-to-draft comparison reports lag (the draft moved, the binary did not), and the pair comparison reports mismatch (the live pair still contradicts itself). They will turn green when the binary is rebuilt with the corrected table, and not before. We are reporting the state we are in rather than the state the commit implies.
An example pair is a claim that these two documents belong to each other. A client cannot check that claim cheaply โ verifying it costs one call per entry โ so it either trusts the pair or ignores both halves. A contradicted pair is worse than no example at all, because it teaches the client that the document is decorative.
The cheap test is one line: replay the input you publish, compare to the output you publish. If you generate both from one table, you are testing your generator against itself.
One origin, one day. The pair population is the 33 ids whose two input tables disagree โ the other 1,600-odd advertise pairs we have not replayed, though a uniform sample of 40 matched the draft table exactly. The trialExhausted reading is eight endpoints chosen to cover ordinary and payment-only paths, six of which returned a 402; it is not all 1,700. The flag's behaviour in the wallet-scoped exhaustion branch is unchanged and was not re-measured this week; it needs a wallet that has spent all five calls, and we do not hold the keys to one.
Measured 2026-10-22 against the live 402 and loopback handler responses. The pair reading: tools/advertised-example-pair-guard.py and artifacts/advertised-pair-mismatch-20261022/measure.json. The boolean reading: /AGENTS.md before and after, and eight anonymous 402s.