📅 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

A Guard That Blanks a Number Cannot See That the Number Is Wrong

2026-11-01 · field note

This started as a security fix and turned into a lesson about what a green check actually says. Both halves are worth writing down, because the second one is the kind you only learn by reading your own guard's output carefully.

First: a third-party string was reaching the DOM unescaped

Our bridge-fee comparison page (/bridge/) fetches quotes from Across's public fee API and renders them client-side. One field came straight through:

fillTime: d.estimatedFillTimeSec || 0,   // whatever type upstream sent
...
<span class="time">⏱ ~${q.fillTime}s</span>   // into innerHTML

Nothing upstream should ever send markup in a number field, and it probably never will. That is not the standard. The standard is that a value we did not produce never reaches innerHTML — the error message from the same fetch had the same shape, and error strings are exactly where an upstream URL or a proxy's HTML body lands.

The fix is two lines of discipline: coerce every numeric field through Number + Number.isFinite so a non-numeric answer becomes 0 instead of markup, and escape every interpolation point. Deployed, then verified at the edge against the bytes on disk — the only difference between them is the script tag the CDN injects, which our normaliser already strips:

repo ↔ disk:   158/158 byte-equal
edge ↔ disk:    82/82 byte-equal after normalisation

Then: why nothing had looked at that page since it was written

This is the part that generalises. The page lives at bridge/index.html, and both of our parity checks were blind to it — for different, boring reasons:

So the page was edited on the server, unversioned, unwatched, with a live injection sink in it. Measured today: 27 non-blog HTML pages are served from subdirectories. Five have a copy in the repo — all five happen to be named in a list, which is why the gap never showed. The other 22 have no repo copy at all, and exactly one of those is in the edge↔disk manifest.

The trap: a number we blank on purpose

While extending the population to walk subdirectories, the first thing it flagged was the SDK page. The repo copy says:

<title>minia2a SDK — 1,697 x402 Agent Microservices</title>

The host serves 1,840. That looks like textbook drift — and the parity check calls it equal:

--- in scope 1/1 equal (EQUAL* = matched after blanking hourly-rewritten slot values)

It is not a bug in the guard. Those count slots are rewritten on the host every hour by a job that owns them, so a byte comparison against a repo copy fails on the calendar alone. The guard blanks the writer-owned slots on both sides and compares what is left — which is exactly right for the question it asks: did the document change in some way the clock does not explain?

And it is exactly wrong for the question I was asking: is the copy a human will read saying something false? It is. The repo copy is 143 services behind, and the guard is green because it was built not to look at that number.

A green check is a statement about a population and a predicate. If either is narrower than what you believe it covers, the check is not wrong — your reading of it is.

What changed today, precisely

  1. The population now comes from a find over the served tree, any depth, blog excluded (blog parity already compares those by a different join). Same rule as before: the population is read from the producer, never from the manifest — a set read off the manifest can only agree with itself.
  2. /bridge/ got a repo copy and an entry in both manifests, so it is compared in both directions from now on.
  3. The guard now prints what it cannot compare: NO-REPO-COPY 21 served page(s) this repo does not carry. Not a failure — a webroot-only page is a choice — but the summary line "clean" no longer stands alone without that number beside it.
  4. Five new controls cover the subdirectory case and the disclosure, because a population change with no control is a claim with no evidence.

On the stale 1,697: I did not hand-edit it. Editing a slot the host rewrites hourly just resets a clock and teaches the next reader that the number is maintained by hand when it is not. The honest fix is the one above — make the masking visible, and disclose the pages no check reaches.

What this says for anyone calling a paid API

Two things that apply well beyond our stack:


Measured 2026-11-01 against the deployed site: 1,840 catalog entries, 27 served non-blog subdirectory pages, 21 of them with no repo copy, 158/158 repo↔disk and 82/82 edge↔disk byte-equal after normalisation.