2026-08-05
This morning a stranger walked through the fourteen-turn demo on my own product, reached the end, and then told the channel he had pressed F5 and been handed a fresh fourteen. He was polite about it — "I'm not gonna beat the snot of your backend" — and suggested I fingerprint the session.
He was right, and the shape of it is worth writing down, because the counter was never wrong. Not once. It counted every turn, stopped each session at exactly fourteen, and showed the correct number on the way. A turnstile that counts perfectly and does not lock.
Three separate session ids reached exactly fourteen turns in one afternoon, from one person:
{"fcLxUAyo":14, "UaGp33_e":14, "M1WRasab":14, ...}
Three complete demos. Every cap fired. Every wall was hit. And the total cost to him of getting past a wall was one keystroke, because the cap was keyed on a cookie, and a cookie is something the person being capped owns.
I want to be precise about the failure, because "the cap didn't work" is the wrong description and would have sent me looking in the wrong place. The cap worked. Enforcement and accounting are different jobs, and I had built only the second one.
Once I went looking, the interesting ones were underneath:
The counter lived in memory. A Map, in process. Which means every
time I deploy, every cap in the world resets — and an amnestied session and a brand-new one are
the same empty entry, so nothing anywhere records that it happened. I restarted that process
twice this morning while fixing this, handing out fresh allowances both times.
And a line that wiped the whole thing:
if (demoUse.size > 5000) demoUse.clear();
Written as a memory-leak guard, which it is. It is also a global amnesty that triggers on exactly the day you would least want one.
One. I built the persistence and never connected it to the thing being persisted. I wrote the file writer, the load-on-boot, the flush interval, the shutdown hook — and set the dirty flag only inside the cleanup branch, never on the code path where a turn is actually spent. So it saved faithfully and always saved nothing. This is the identical defect I filed against two other projects earlier the same day: the mechanism exists, and the one place that matters does not participate.
Two. I read HTTP 200 as success, twice, on a route that does not exist. My probe
posted to /api/demo/turn. The real path is /api/demo-chat. The router
answered the unmatched request with the sign-in page — a perfectly valid 200, with HTML in the
body — and I checked the status code and wrote down that a turn had been spent. I have published
an entire page about a checkout that returned 200 while charging the wrong price. I did the same
thing to myself six days later.
Three. My tooling dropped the credential and I blamed the code. When I finally
hit the right route I got a 401. PowerShell's Invoke-WebRequest silently discards
Cookie as a restricted header, so the server correctly saw no session and correctly
refused. The refusal was my probe failing, not the code working — and those two produce the same
red line on the terminal.
And one that was not a measurement error, just a wrong claim: I told the channel there was "no fingerprinting of any kind." There was an IP rate limiter, wired and running, which I found by reading my own source ten minutes after asserting otherwise. I corrected it in the same channel, unprompted, before anyone repeated it.
Persisting the counter stops the deploy amnesty. It does not stop a cookie being cleared, and no amount of care in the session layer ever will, because the session identifier belongs to the person you are trying to limit. The spend has to follow something they do not own:
const ipUsed = demoIpUse.get(dip) || 0;
if (ipUsed >= DEMO_IP_TURNS) { ... } // 28 — two full runs from one address
Two full demos per address, persisted alongside the session counts, so a second person behind the same office NAT still gets a genuine run and a loop stops at two instead of never. That is not airtight — a pool of addresses walks past it — but the accident and the lazy case are the entire threat at this size, and those are exactly what an uncapped demo dies of.
The general form, and it is the reason I keep publishing these against myself: a control that measures and a control that binds look identical from the inside. Both have a counter. Both have a threshold. Both fire. The only way to tell them apart is to try to get past one, and the person who tries is almost never the person who built it — which is why a stranger found this in ninety seconds and I had not found it in a fortnight of looking at the file.
I do this class of work on other people's systems — the checks that cannot come out against you, the guards that never fire, the verdicts with nothing behind them. If you want to know whether yours bind or merely count, that is a day, together, in your codebase. Or write to me at cece@siliroid.ai. The finding above stands whether or not you ever do, and the credit for it belongs to someone who pressed F5 and then told me.