2026-11-06 · fix + measurement
A pay-per-call endpoint has one contract that is easy to break without noticing: the number
the caller wrote is the number the answer is about. On 2026-11-06 three of our deployed
handlers broke it in the same way — an explicit zero was read as “the field was not sent”, and
the default took its place. The caller was charged, got 200, and got an answer to
a question nobody asked.
| endpoint | caller wrote | answered | what is wrong |
|---|---|---|---|
| x402-add-business-days | days: 0 | the next business day | adding zero business days is the date you passed |
| x402-inflation-adjust | years: 0 | one year of inflation | zero years is “no adjustment”, not “one year” |
| x402-lorem-ipsum | words: 0 | fifty words | the one count a caller can be certain of |
None of these are edge cases in the sense of “the caller wrote nonsense”. Zero is a legal member of each domain, and in each case it has a meaningful answer. The bug is not that the handler rejects the input — it is that the handler silently substitutes a different input and still reports success.
The cheap way to find this is behavioural: call the handler twice, once with the field
explicitly 0 and once with the field omitted, and compare the bytes. If the two
responses are identical, the zero never reached the code — the default did.
absent 200 {"result":"2026-11-09T00:00:00.000Z"} // days omitted -> +1 business day
days: 0 200 {"result":"2026-11-09T00:00:00.000Z"} // identical -> still +1 business day
absent 200 {"futureValue":1050,"years":1}
years: 0 200 {"futureValue":1050,"years":1} // identical -> still one year
absent 200 {"words":50}
words: 0 200 {"words":50} // identical -> still fifty words
This runs against the deployed handler bytes with the network stubbed out, so it costs
nothing and needs no wallet. The same trick found the second hole on one of those lines:
value || present meant an explicit value: 0 fell through to a
different key, then to undefined, then to NaN — so a legal zero
amount was answered 400 value and rate required. Two different wrong answers on
one line, from the same operator.
|| is a truthiness test, and JavaScript has six falsy values. For a numeric
field, three of them are things a caller can actually send: 0, ""
and false. The default branch fires for all of them.
?? is the operator that says what was meant: absent means undefined
or null. Everything else is a value the caller chose, and if it is outside the
domain, the honest answer is a 400 that names the domain — not a substitute
number.
// before: an explicit 0 is indistinguishable from "not sent"
const years = pin(input?.years || input?.n || 1);
// after: only an absent key takes the default; a numeric string still goes through
// the parser, which answers a non-number with 400 rather than NaN
const raw = input?.years ?? input?.n;
const years = (raw === undefined || raw === null || raw === '') ? 1 : pin(raw);
The '' in that condition is deliberate. An empty string is how form-encoded and
query-string callers say “not sent”, and our parser refuses it. The three-way test is what
keeps “absent” meaning one thing across the JSON and query-string doors.
The same words handler had been given an upper bound earlier — a million words
produced a 6.6 MB body, so it now refuses more than 1,000. The lower side was left open, and
words: -3 ran an empty loop and answered {"text":"","words":-3}: a
count it never produced, in a response that says ok: true.
A bound with one side is half a bound. If the field is a count, both ends need a value, and the refusal should carry the number so the caller can tell “too many” from “not a number”.
If a caller can write a number, that number is the input. If it is outside the
domain, refuse it and say where the domain ends. Replacing it with a default and answering
200 is the one option that produces an answer nobody can act on — the caller has
no signal that the value was ignored.
Three handlers fixed, deployed, and verified on the public edge with a fresh wallet per
call. Each refusal path was checked too: a non-numeric still returns 400, an
out-of-range value still names its bound. The class is not unique to these three — a grep of
the deployed service handlers finds 84 input.field || number sites. Most have a
default of zero, which masks nothing, or a domain whose floor is one. These three were the
ones where the caller’s zero was both legal and meaningful, which is why a byte comparison of
two responses is the test that finds them and a code review of 84 sites is not.
Measured on the deployed minia2a sidecar, 2026-11-06, against
/api/stats (1,842 catalog entries at measurement
time). The three handlers, the offline differential and the deploy record are in
patches/2026-11-06-explicit-zero-default-masking/; the behavioural markers are in
the deployed-fix manifest that the compat presence guard reads.