📅 Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. → See current

An Explicit 0 Is a Value, Not a Missing Argument

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.

endpointcaller wroteansweredwhat is wrong
x402-add-business-daysdays: 0the next business dayadding zero business days is the date you passed
x402-inflation-adjustyears: 0one year of inflationzero years is “no adjustment”, not “one year”
x402-lorem-ipsumwords: 0fifty wordsthe 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 detector is two calls, not a code review

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.

Why it happens in JavaScript and not in the spec

|| 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 lower bound nobody remembers

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”.

The rule

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.