Flickloot · Methodology

Errata

Every mistake this project has made, logged as it happened, with what caught it and what changed. 39 entries. None have been deleted.

A published return that nobody can check is a claim. So is a clean record. This site’s argument is not that an AI builds software without errors — it is that errors are inevitable and the method that catches them is the thing worth showing. That argument is only testable if the failures are on the page next to the successes.

How to read this

  • Entries are numbered permanently and never renumbered. A gap would mean something was removed.
  • Each one records, where it applies: what the error was, what caught it, what it cost, and what changed so it cannot recur.
  • Errors caught before deployment are logged the same as errors that reached production. Near-misses are the evidence the checks work.
  • This page is generated from the ledger file itself, so its count cannot drift from its contents — which is entry 18’s fix, applied to entry 18’s own page.

2026-08-05 (night session, Claude Code takeover)

  1. 1 Plinko RTP 1410.82% — the multiplier array was assembled reversed, putting the 40× jackpot in the most likely centre slot instead of the 1-in-4096 edges.

    • Caught by: the build’s own rule that every published RTP is computed at load from the game’s constants — the absurd number was on screen at first local render.
    • Consequence: none; never deployed.
    • Fix: array rebuilt edge→centre→edge; recomputed 96.33%, verified against a hand calculation. This is the flagship story: a hand-typed label would have said 96% over a broken game forever.
  2. 2 Baccarat suits reshuffled on every re-render — card suits (cosmetic) were randomized inside the render path, so hands visibly flickered as the ledger clock ticked.

    • Caught by: partner design review AFTER b8 deployed — a miss that reached production for ~20 minutes.
    • Consequence: cosmetic only; results and payouts were unaffected (rank math was separate).
    • Fix: b8.1 freezes display cards once at deal time.
  3. 3 Dead code shipped to the working file — a garbage half-written RTP constant (AND_RTP) was left in the first draft of b8 alongside the correct one.

    • Caught by: self-review before first test.
    • Consequence: none; deleted.
  4. 4 Wrong first SFTP path — first deploy attempt used the SSH shell’s absolute path; SFTP is chrooted to the webspace root, so it failed.

    • Caught by: the error message.
    • Consequence: one failed upload, no side effects.
    • Fix: relative paths in SFTP; fact recorded in .deploy-credentials.txt.
  5. 5 Assumed the smaller console file might be an incomplete export — early in the session the 24.7 KB console build was flagged as possibly truncated vs the 37 KB live file. They were different artefacts (console vs app), fully explained by the calendar.

    • Caught by: reading the project’s own records before acting.
    • Consequence: none — but the lesson is recorded: check the project docs before theorising.
  6. 6 Batch product-delete ran without surfacing its error — the first attempt to trash the 49 Shein products exited with a suppressed error (product post type had lost trash support with WooCommerce deactivated); a blind retry loop then reported 49 failures.

    • Caught by: re-running one delete with stderr visible.
    • Consequence: a confusing minute, nothing broken.
    • Fix: status-based trashing; rule of thumb — when a batch fails, reproduce ONE case with errors visible before retrying the batch.

Historical (pre-AI, kept because the methodology is the same)

  1. 7 2026-07 — Faro Dorado published RTP 94.7% vs measured 92.38% (the operator’s own build, caught by the 5M-spin audit; paytable re-solved to 94.646%, published 94.6%). The audit catching its own author was already a queued blog piece — the AI errata continue that tradition, not start it.

2026-08-06 (Cowork session — game-page integrity audit)

  1. 8 Twelve published game pages described features that were never built. The /games/ child articles were written 24 Jul as forward-looking specs while the games were still “In build”. When the twelve games actually shipped on 5 Aug they shipped differently, the hub cards were corrected the same night, and the twelve article bodies were not.

    • Caught by: a page-by-page audit against the shipped constants in b10-private-club, run at the start of this session.
    • Consequence: for roughly 24 hours the site’s own game pages contradicted the games behind them, on a site whose entire argument is that it publishes only what it has built. Eight of twelve needed a full rewrite, not an edit.
    • Fix: all twelve rewritten from the shipped constants, every RTP either exact-computed or explicitly labelled measured, and every removed claim named in the page rather than quietly deleted. Four findings inside that audit worth recording separately: 8a. /games/poker/ described the wrong game. Written as Texas Hold’em — hole cards, a flop, heads-up play against the house, downloadable hand histories. What shipped at that address is Video Poker, Jacks or Better 9/6. Not drift; a different game entirely. 8b. /games/scratch/ argued against its own mechanism. It sold a finite depleting prize pool and disparaged the alternative in these words: “A digital instant-win typically draws each card independently against a probability table.” That alternative is exactly what shipped — 512 cards, drawn independently. The page was criticising itself. 8c. /games/faro-dorado/ still published 94.7% — the figure errata entry 7 recorded as wrong and corrected to 94.6% in July. It stayed live on the page for weeks after the correction. The same page also said the audit runs 100,000 spins; the build runs 400,000.
    • Lesson: correcting a number in one place is not correcting it. The July fix changed the app and the ledger and missed the marketing page. 8d. /games/blackjack/ claimed a rule that cannot exist. “Double after split permitted” on a build with no splits. Logically impossible rather than merely inaccurate, which is the signature of a page written from an intention instead of from the code.
  2. 9 Not one of the twelve pages mentioned the geo-gate. An omission rather than an error, recorded because of what was omitted: the statute-citing geo-gate across fifteen jurisdictions is the strongest compliance differentiator the platform has, and it appeared on none of the pages a prospect would actually read. RTP was also absent from nine of twelve, including six where it is mathematically exact.

    • Fixed in the rewrite; both now appear on every game page.
  3. 10 Environment lesson, not a content error: the Cowork device VM cannot delete files on a mounted Windows folder. git init and git commit therefore succeed but leave stale .git/*.lock behind, which blocks every subsequent git write until the locks are moved away (mv works where rm does not).

    • Caught by: the next git command failing.
    • Consequence: a few minutes, no data loss, repo verified clean afterwards by git fsck.
    • Rule: git work on this project belongs in Claude Code, not Cowork. —

2026-08-10 (Claude Code — verifying what was shipped unverified)

  1. 11 Reel anticipation fired on 78.17% of spins. The Faro trigger accepted any live line, and because the five paylines use every row of both leading reels, any wild anywhere qualified.

    • Caught by: computing the rate exactly over the strip weights instead of eyeballing it, then confirming in-page over 20,000 sampled grids.
    • Consequence: the tell was live and meaningless for two days — a signal that fires four spins in five is not a signal.
    • Fix: restricted to lines live for a symbol paying 26× or better. 23.32% computed, 23.30% measured, against Andaluz’s 24.44% so both slots read alike.
    • Lesson: a presentation feature can be “working” and still be wrong. Nothing errored.
  2. 12 prefers-reduced-motion stopped covering the coin burst. The rule hid .fl-coin; the radial burst introduced on 6 Aug is .fl-coin-r, which that selector does not match. Users who ask for no motion got flying coins.

    • Caught by: reading the shipped CSS rule against the shipped class list rather than trusting the commit message that said the gate was extended.
    • Consequence: an accessibility regression, live two days.
    • Fix: selector updated.
    • Lesson: renaming a class silently escapes every selector that named the old one. The commit that introduced .fl-coin-r claimed the reduced-motion gate was extended, and it was — for ten of eleven effects.
  3. 13 Sound and haptics fired once and then went silent. The effect was keyed on ${o.seed}-${o.tier}, but presentOutcome() emits no seed, so the key was "undefined-big" for every big win. Two big wins in a row buzzed once.

    • Caught by: tracing the dependency array back to the object that feeds it, after asking what o.seed actually contained.
    • Consequence: the feature was ~half dead on arrival.
    • Fix: guarded on outcome object identity, which is fresh per result.
    • Lesson: undefined in a template literal is a silent string, not an error.
  4. 14 A confidence interval that got narrower the luckier the sample was. Video Poker’s plug-in variance estimate reported ±2.90 on one load and an implied ±1.5 on another. The royal pays 800 and lands about once in 40,000 hands, so a 60,000-hand run that misses it reports a confidently narrow interval that is simply false — the error bar shrank precisely when the sample was least representative.

    • Caught by: the range rendering visibly tighter than a figure measured minutes earlier, and asking why.
    • Consequence: caught before the interval was ever published.
    • Fix: standard error floored at the royal’s own contribution, pay × sqrt(p(1−p)/N), which no run’s luck can move; figure published as a range, 88–94%, not a point. Baccarat, Blackjack and Faro were then tested for the same failure and do not have it — only a payout both rare and large relative to the bulk breaks the estimate.
    • Lesson: an error bar is a measurement too, and this one was wrong in the flattering direction.
  5. 15 Process, not content: two Claude sessions wrote to the same production webspace with the same key. The 8 Aug session promoted the staged build while this session was mid-verification; the promotion was discovered by finding a backup file this session had not created.

    • Caught by: an md5 that changed between two checks minutes apart.
    • Consequence: none — the bytes promoted were the correct tested build, and the access investigation confirmed no unauthorised access (publickey only, no password auth since the 5 Aug setup).
    • Rule: one session owns deploys. SESSION-STATUS contains literal promote commands and any session reading it may act on them.
  6. 16 Documentation drift: this session appended its findings to archive/SESSION-STATUS-2026-08-06.md for a full day without noticing that archive/SESSION-STATUS-2026-08-08.md existed.

    • Caught by: the operator asking whether the recent sessions had been read.
    • Consequence: duplicated effort — the render check performed today had already been done on the 8th — and a status trail split across two files.
    • Fix: 10 Aug status written as its own file; the 8 Aug file’s four open items closed explicitly rather than re-litigated.
    • Lesson: read the newest status file before writing to any status file.

2026-08-11 (Claude Code — game-page mathematics)

  1. 17 /games/poker/ published a return that did not overlap the game’s actual return.

    • The page stated “≈ 96–98% under heuristic holds”. The app measures the heuristic at 90.46% ±3.20, published as the range 87–94%. The two ranges do not intersect at any point.
    • Caught by: extracting the app’s own maths block and executing it, then comparing every game page’s headline figure against what the game produces. Nothing found it before because nothing had ever compared the two — the page rewrite of 6 Aug set the game’s identity right and left the number stale.
    • Fix: page now reads 87–94% and names the ≈99.5% optimal-play figure alongside it.
    • Lesson: correcting a page’s subject is not the same as correcting its numbers, and the audit that catches one will not catch the other unless it is told to.
  2. 18 Stale figures survived on two more pages. /games/faro-dorado/ published a bare 94.6% after the app had started publishing intervals (94.6% ±0.7), and both faro-dorado and baccarat still read “six of the twelve games” after the exact count reached eight.

    • Fix: corrected on all three.
    • Prevention: the twelve maths sections are generated by scripts/build-maths-sections.js from scripts/extract-game-maths.js, which reads the figures out of the deployed app rather than accepting typed ones. A page can now only drift if someone edits it by hand.
  3. 19 CookieYes was carried as an open item for six days after it had been fixed.

    • The 5–10 Aug status files and the FLICKLOOT calendar events all listed “CookieYes banner still declares affiliate links the site does not load” under STILL OPEN. the operator confirmed on 11 Aug that the banner had been corrected long before.
    • Consequence: no production harm — the site was correct throughout — but the item consumed a slot on six consecutive session plans and was raised with the operator again on the 11th.
    • Cause: the item was browser-gated, so no session could verify it from the shell, and “cannot check” was recorded as “still open” rather than as “unknown, ask”.
    • Fix: removed from the status trail and from the calendar.
    • Lesson: an unverifiable item should be marked unverifiable and put to the person who can answer it, not carried as an open defect. The two states look identical on a checklist and are not the same claim.

2026-08-14 (Claude Code — verifying the delegated D3 teaching tools)

  1. 20 A teaching tool invented a real-casino comparison number. The first draft of the Faro Dorado teaching panel (delegated to Gemini, 12 Aug) hardcoded commercialSlotEdge = 12.0, labelled as a typical commercial slot at roughly 88% RTP, with no source behind either figure.

    • Caught by: review of the delegated output before it shipped — the brief had explicitly forbidden inventing a comparison, on the same grounds that made the poker page’s figure a problem the day before: an unsourced number reads as a claim, and this site trades on every number tracing to source.
    • Consequence: none; never deployed.
    • Fix: the panel now shows only Flickloot’s own edge, which is derived from the game’s constants. The failure class was added to the delegation gate as a forbidden-pattern scan, and every one of the eleven files generated afterwards was screened against it.
    • Second-order fix, 14 Aug. The corrected file carried a comment quoting the removed strings verbatim, so it tripped its own forbidden-pattern scan on every subsequent run — a file flagged for explaining its own correction, which trains you to delete the explanation. Two changes ended it: the record moved here, where records belong, and the scan in scripts/check-teaching-tool.js now strips comments before matching and reports a comment-only hit as a note rather than a failure. A gate that punishes documentation is a gate people learn to route around.
  2. 21 The delegation gate was looser than it read. delegate-d3-batch.js listed the source constants that had to appear in each generated file, but only failed a file when none of them were found. Three files passed while missing the specific constant naming the published RTP (ANDALUZ_RTP, VP_RTP, BACCARAT_RTP — none of which exist in the source under those names).

    • Caught by: scripts/check-teaching-tool.js, 14 Aug, when its stricter per-file probe could not find the constants the marker list claimed were mandatory.
    • Consequence: none — the four affected files derive their figures correctly, inside component hooks rather than at module scope. The marker list was wrong, not the files.
    • Fix: markers corrected to names that exist, and the new gate records for each game whether its figure is reachable statically or needs a browser, so “not checked here” can never again be silently recorded as “checked”.

2026-08-15 (Claude Code — browser-emulation pass on the casino)

  1. 22 A test-harness artifact was registered as a major defect in the product. A mobile-emulation pass reported that the casino’s balance display “never moves during a session”: it read 5,000 / 5,000 GC while the persisted coins value fell to 4,986, and only a page reload showed the true figure. That observation was correct and reproducible. The diagnosis attached to it was wrong. The balance roll-up (useCountUp) animates through requestAnimationFrame, and the automation pane was not compositing frames — document.visibilityState was hidden, so RAF was suspended and the displayed figure could never leave the value it was seeded with at mount. The reload only appeared to correct it because useState(target) re-initialises to the current number.

    • Caught by: a direct probe run before any fix was written — 0 RAF callbacks in 3,803 ms against a setInterval firing normally in the same page, on the same tick.
    • Consequence: none shipped. The entry was withdrawn the same day, roughly twenty minutes after it was written, and nothing was changed in the deployed app. Had it not been probed, the 25 Aug fix session would have spent its time rewriting a correctly-working animation, and Phase A’s exit gate would have been held open by a defect that does not exist.
    • Fix: the checklist entry is struck through with the full reasoning rather than deleted, and the pass now carries a standing limitation — in a frame-less environment, anything RAF-driven (roll-ups, reel spins, the coin burst, anticipation timing) cannot be observed at all, and its absence is indistinguishable from a freeze. DOM text, storage, timers and layout remain trustworthy there; animation does not. What made the error convincing, and is worth remembering: every other field in the same header block — refill, wagered, returned, session RTP — updated live and correctly, which read as proof that the block re-rendered and the balance alone was stale. In fact the balance is the only field in that block that is animation-driven rather than printed directly, so it was the single field a frame-less environment could freeze. The differential diagnosis that ruled out the refill mechanic was sound, and aimed one layer too high. Errata 19’s rule — an unrecorded pass is indistinguishable from one that never happened — has a mirror image: an unobservable failure is indistinguishable from a real one, and the environment must be cleared before the product is blamed.

2026-08-16 (Claude Code — SEO pass on flickloot.com)

  1. 23 A bug found and documented on 6 Aug was re-shipped twice, and cost the two most strategic pages on the site ten days of invisibility. The publish scripts end with wp rank-math sitemap generate — a command that does not exist (wp rankmath is the real prefix), so it fails silently into a || wp cache flush fallback that does not touch Rank Math’s sitemap cache. The 6 Aug session hit this, recorded it in its session status, and ran the regeneration by hand — but never fixed the script line, and the 13 Aug and 14 Aug publish scripts were both written by copying that script, broken line included. Result: /ai-powered-casino/ (the Phase D1 definitional page, the entire “AI powered casino” bet) and /methodology/errata/ went live on 13–14 Aug and never entered the sitemap, which kept serving a cache from 6 Aug. Both pages were also linked from nowhere — no menu, no page, no hub — so they were fully orphaned: invisible to crawlers by either route while GSC coverage sat at 29 indexed / 13 not, and nothing in any status check watched for it.

    • Caught by: the operator asking why pages were not indexed; a curl of the live sitemap against the live page list.
    • Consequence: the definitional page the AI-answer strategy rests on spent its first ten days undiscoverable.
    • Fix: sitemap cache invalidated live (46 → 48 URLs), both pages added to the footer menu (sitewide internal links), and all three publish scripts now invalidate the cache through \RankMath\Sitemap\Cache::invalidate_storage() directly.
    • The standing lesson: a fix applied by hand and recorded in prose is not a fix — the next script is copied from the broken source, not from the session notes. The check that would have caught it in a morning: sitemap URL count against published page count, which flickloot-status.sh already almost computes.

2026-08-17 (Claude Code — A2, building the automated regression suite)

  1. 24 Two card rows never wrapped, and every pass that could have caught them looked exactly once. Blackjack’s dealer row and Baccarat’s player/banker rows are flex containers holding a hand whose length is decided at runtime — the dealer draws to 17, baccarat draws a third card in about a third of coups — and neither declared flexWrap. Blackjack ran 36px off a 375px screen and 91px off a 320px one; Baccarat ran 33px off at 320px. The player’s row directly below the dealer’s had always had the property, so the fix was one word, in a line already written correctly three lines away.

    • Caught by: A2’s new hand-length stress phase — the same twelve games at the same width, but played twelve rounds instead of one.
    • Consequence: both have been live since those games were written. A1’s 48 human cells, and this harness’s own first 96-configuration sweep, both passed them: a dealer hand reaches four cards in roughly one round in six, so a single deal per configuration catches it one run in six, and a run that misses it is indistinguishable from a clean one.
    • Fix: flexWrap: "wrap" on both rows, plus two gates — the 12-round stress phase, and a static lint in check-build.js that fails any flex row whose cards come from a .map() and which does not wrap, regardless of what was dealt.
    • Lesson, and it is errata 19’s again wearing different clothes: a check that samples a random variable once has a pass rate, not a verdict. The 48-cell grid was not too small — it was measuring at the wrong moment. Where a defect only appears in a state the software reaches sometimes, the gate has to either force the state or repeat until it occurs, and a gate that does neither should say what it actually saw. That is why the stress phase prints the widest card row each run reached alongside its PASS: a pass at two cards and a pass at five are different claims and should not read alike.
  2. 25 A probe written to check for a layout collision reported a clean result twice, and both results were worthless — in the same session that added a tool specifically to stop that happening. The 48-screenshot vision review raised a question no assertion covers: in landscape, a long result line sits almost flush against the STAKE label, so does it ever actually collide? Run one measured the gap between the result text and an element matching textContent === "STAKE" and reported zero overlaps across all 24 configurations — because the label’s DOM text is Stake, and the capitals on screen are textTransform: uppercase. It had matched nothing, twenty-four times, and printed a pass for each. Run two, with the case fixed, reported a -41px overlap whose offending text was "Stake" — the label matching a filter loose enough to select the label itself, and then measured against itself.

    • Caught by: reading the actual output instead of the summary line. Both runs ended in a tidy verdict — “0 genuine overlaps”, then “1 overlap found” — and both verdicts were about nothing.
    • Consequence: none shipped; roughly twenty minutes, and the probe was deleted rather than fixed a third time. The question it was asking was answered in one line of source: the row is flexWrap: "wrap" with gap: 12, so the two elements cannot collide — they hold 12px apart and then wrap to separate lines, which another screenshot in the same set shows them doing.
    • Lesson: this is errata 19’s rule turned on the checker instead of the product — the same session had already written that a break which fails to apply produces a pass that reads like a proof, and built break-build.js to refuse exactly that. The guard was aimed at the deliberate breaks and not at the ad-hoc probe, because the probe felt like measurement rather than testing. A measurement that cannot report “I found nothing to measure” is indistinguishable from a measurement that found nothing wrong. Any probe reporting n/a for every input has failed, not passed, and should say so in its exit code.
    • And the cheaper lesson underneath: the question was structural, and structural questions are answered faster by reading the rule than by measuring the render.
  3. 26 The regression suite reported a defect in the product that was the suite’s own assumption, and it is errata 22’s exact shape eight days later. Phase 1 asserted that a page reload preserves the stored balance identically: read the balance, reload, require the same number. It failed on the deployed-bytes run — hi-lo (webkit): reload changed coins: 4990 -> 4991 — on a build that was correct. The balance refills one coin every 7.2 seconds while under the cap (REFILL_PER_HOUR = 500), a deliberate mechanic the header advertises on screen as “REFILL +1 GC in 7s”. A reload that straddles a refill tick legitimately gains a coin.

    • Caught by: the failure naming a delta of exactly +1, which is the wrong size for a persistence bug and exactly the size of the mechanic.
    • Consequence: none shipped — but it would have been a red that appears in roughly one run in twenty, on a random cell, which is the worst kind: too rare to chase, frequent enough to teach the reader that a red on this check means nothing.
    • Fix: the assertion now says what it actually cares about — the balance must not FALL, and must not rise by more than the refill can account for over the measured wall-clock of the reload. A reset to the 5,000 default or a lost wager still fails, being orders of magnitude larger than one tick. REFILL_MS is read from the build rather than hardcoded, so changing the refill rate cannot silently widen the tolerance.
    • Lesson: errata 22 said a test artifact must not be registered as a product defect. The deeper version, learned here: an assertion of equality against a system with a clock in it is a bet that the clock will not tick during the measurement, and that bet loses on a schedule. The check was written the same day as a document arguing that a check sampling a random variable once has a pass rate rather than a verdict — and this is the mirror image, a check with a failure rate rather than a verdict. Both come from the same root: writing down what is easy to compare instead of what is meant.
  4. 27 The banker bet paid over 100%, because the published figure and the payout were computed from two different formulas. BACCARAT_RTP simulated BAC_PAYS.banker — the exact 1.95 — while the table paid Math.round(1.95 × stake). At the 10 GC default stake the exact payout is 19.5 coins; Math.round made it 20. So the banker bet returned 101.23% against a published 98.94% — a positive expectation, on the bet every baccarat guide tells you to make, at the chip most players never change. It was not uniform either: 20 and 100 GC were exact, 50 GC was +0.46pp, so the same bet returned four different figures depending only on which chip was selected.

    • Caught by: extending A1’s one-game rounding note to every game that pays Math.round(mult × stake), which was A2’s job and had been deferred as a footnote about Hi-Lo. The Baccarat number was not suspected — it came out of computing all of them rather than the one that had been raised. The product had also been printing it: the bet button reads “Banker 1.95×” directly above a result line reading “+20 GC
    • 2.00× stake”, in the same frame, and had done since the game shipped.
    • Consequence: no money is at risk — no cash-out exists and the drift favoured the player throughout — but on a site whose entire argument is that the published number is the real number, the flagship bet returned something else for as long as the game has existed.
    • Fix: the fractional coin is now paid proportionally — 19.5 pays 19 half the time and 20 the other half — so the expected payout is exactly the published rate at every stake, with no stake-dependent asterisk. Dithering, the same trick that requantises audio without shifting its level. Disclosed on the table rather than buried, with the percentage computed from the fraction so the sentence cannot go stale. Verified over 2,000,000 rounds through the build’s own payout function: 98.86% at all four stakes, flat. check-build.js gained an invariant, proven against both the obvious break (Math.round reinstated) and the dangerous one (payCoins still present and still called, but quietly made biased — which nothing else in the file would have noticed).
    • Lesson: no rounding rule could have fixed this, and hunting for the right one was the trap. 1.95 × 10 is 19.5 and coins are whole; round up and the edge runs backwards, round down and the player is shorted, and every rule is biased at some stake. The choice was never between rounding rules — it was between biasing the payout and biasing nothing. The deeper one is errata 1’s again: the site’s whole method is that published figures are computed from the constants the game plays with, and this is what it looks like when two formulas for one number are allowed to drift apart four decimal places from where anyone was looking. Extended the same day, on instruction: Hi-Lo, Crash and Mines had the identical defect and took the identical one-line change. Hi-Lo now returns 97.00% at every stake against 97.75% at the default before — and it was the game the whole question was first raised in, back when it read as a footnote about one game rather than as four. Crash and Mines have no fixed distribution to average (the multiplier is wherever the player cashes out), so they cannot be tabulated; what changed is that their fractional payouts are no longer biased in one direction. Plinko, Scratch and Blackjack’s natural were deliberately left on Math.round, because their multipliers land on whole coins at every selectable stake — and rather than leave that as something someone once checked, check-build.js now recomputes it from the real constants against the real ladder every build. Add a 25 GC chip and it fires: Scratch’s 0.5× refund becomes 12.5 coins and Plinko’s 0.3× bucket becomes 7.5, and the rounding would be back, silently, in three games nobody had touched. Deployed 18 Aug as b21, and measured on the deployed bytes rather than the branch. Before: 101.23% realised against 98.94% published at the default stake. After: 98.84% realised against 98.84% published, flat at all four selectable stakes. The published figure is itself a 200,000-round simulation recomputed on every page load, so it moves by roughly a tenth of a point between loads — what changed is that realised and published are now the same number, at every stake, instead of differing by 2.29 points at the one most players use. The fix sat committed and green for a day before it shipped: approval was given, priority moved, and the deploy fell out. Fixed in code is not fixed — that gap is now a step in the close-out procedure.

2026-08-18 (Claude Code — publishing the teaching tools)

  1. 28 A publish script overwrote all twelve game-maths pages, retitled them, and moved them under a new parent — 404-ing every one of their URLs. publish-teaching.sh resolved existing pages with wp post list --name="$SLUG", which searches the whole site. The twelve teaching pages use the same slugs as the twelve game pages (faro-dorado, plinko…), so every “create” became an update of the existing top-level page: content replaced, title changed to “X odds explained”, and --post_parent set to the new /teaching/ hub. WordPress permits the same slug under different parents; the lookup did not. flickloot.com/faro-dorado/ and eleven others returned 404.

    • Caught by: the run’s own output saying updated where it should have said created — twelve pages that had never existed cannot be updated.
    • Consequence: roughly four minutes of 404s on the twelve most substantive pages on the site, mid-morning. No data lost.
    • Fix: restored all twelve from the WordPress revision immediately preceding the overwrite, cleared the parent, and separately restored rank_math_title / rank_math_description / rank_math_focus_keyword from the 6 Aug publisher — revisions do not cover post meta, so content came back looking right while the SEO titles still read “Teaching Tools”. The lookup now resolves by slug and parent, and refuses to create a child at all if its parent is missing.
    • Lesson: a create-or-update script must identify the row as precisely as the thing it is about to overwrite. A slug is not an identity when the CMS scopes slugs by parent. And the warning was in the output the whole time — “updated” for a page that should not have existed is the same signal as a test passing before it was written.

2026-08-22 (Claude Code — deploying the A3 error beacon)

  1. 29 The error beacon promised “no query string” in its own comments and did not enforce it, and the log it wrote was a public URL. beacon.php opened by stating its constraints — “no PII in the payload”, “no cookies, no session, no query string, no referrer, no stack” — and the allow-list below it took src verbatim. Query strings were stripped only in client-snippet.js. But the endpoint is public and unauthenticated: anything can POST to it, so the guarantee held only for traffic that chose to honour it. Separately, beacon.php was careful that it would never serve the log — a GET returns 204 — while the file it appended to, /beacon/log/errors-YYYY-MM-DD.jsonl, was a guessable URL the web server would happily serve, hashed IPs and user-agent strings included.

    • Caught by: the deploy verification probe, which POSTed src=https://play.flickloot.com/?secret=shouldnotpersist precisely because the comment claimed it could not persist — and then read the log back over SSH and found it there, in full.
    • Consequence: none. The endpoint had been live for about ten minutes, the client half had not shipped yet, and the only traffic was the probe. Both were fixed before b22 carried the beacon to a single player.
    • Fix: src is now split on ? inside the PHP allow-list, so stripping happens where the data lands rather than where it is sent; re-probed with ?secret=MUSTNOTPERSIST&email=… and confirmed zero occurrences. beacon/log/.htaccess denies the directory outright, verified live as 403 on the directory, on the log file and on .salt. The salt is now 32 random bytes generated on the server rather than the hard-coded fallback.
    • Lesson: a guard that covers the endpoint does not cover what the endpoint writes, and a constraint stated in a comment is documentation until something enforces it. Both halves of this were the same mistake made twice in one file — the code described a discipline it was trusting someone else to keep. The probe found it only because it was written to test the claim rather than the happy path: it sent the exact thing the file said would never survive.

2026-08-24 (Claude Code — the compliance console during the Supabase pause)

  1. 30 The compliance console told every visitor its data was “read live from a verified database” while the database had been paused for a day, and its own badge sat on “connecting…” for the best part of a minute before admitting otherwise. The Supabase project was paused on 23 Aug after seven days of inactivity. The console’s fallback was never the problem: it drops to a bundled 23-row snapshot, flips its badge to “cached data”, and refuses to show a stale snapshot of the monitoring panel at all — all of that was designed deliberately and worked. Two things around it did not. The header paragraph asserted live-database provenance as static text, so it made the claim regardless of what the badge below it said. And a paused project does not refuse a connection, it hangs — so the badge, which reads “connecting…” until the query settles, stayed there far longer than a visitor would wait.

    • Caught by: loading play.flickloot.com/console/ logged out and timing it. The badge still read “connecting…” at 15 seconds and had flipped to “cached data” by 59 seconds; the header sentence claimed live data throughout, and continued to contradict its own badge afterwards.
    • Consequence: unknown but small — the console is a low-traffic methodology page and the pause is a day old. The claim was false for every visit in that window, which is the part that matters on a site whose method is labelling its own claims.
    • Fix: every database query now runs through a 6-second deadline, so the honest state is reached in seconds rather than a minute; and the header sentence is generated from the observed state instead of being written into the page — live, connecting and not-answering each get their own wording, and the live-data claim exists only in the branch that has actually seen live data. A new gate, scripts/check-data-claims.js, fails if any query bypasses the deadline, if the deadline exceeds ten seconds, or if the live-data claim appears anywhere outside that branch. Proved with three deliberate breaks.
    • Lesson: a fallback being honest is not the same as the page being honest. The engineering here was careful — snapshot, badge, and a written comment explaining why the monitoring panel refuses a stale fallback — and the sentence sitting above all of it still said something the system could not support. The prose was the only part nothing checked, because it was the only part nobody thought of as code.

2026-08-31 (Claude Code — measuring what /teaching/ was actually earning)

  1. 31 All thirteen /teaching/ pages served a complete second HTML document inside the first — <!doctype>, <html>, <head>, <title>, canonical, description, charset and viewport, twice each — for seventeen days. publish-teaching.sh handed wp post create the standalone .html files whole, so post_content received each file’s head along with its body. WordPress emitted its own head and then rendered the file’s head again inside <body>: on /teaching/plinko/, Rank Math’s title on line 11 and the file’s own on line 284. The two canonicals happened to agree in value, so this was never a conflicting-canonical case — it was invalid markup and two competing titles and descriptions.

    • Caught by: nothing, for seventeen days, which is the entry. check-teaching-live.js passed 12/0 throughout, because it reads the figures off these pages and a figure is not a page. It surfaced only when the pages were inspected for an unrelated reason — why they earn no impressions — and a <title> count came back 2. check-teaching-published.js existed, fetched all thirteen, checked HTTP 200 and console errors, and was wired into nothing and named in no gate table.
    • Consequence: not measurable, and deliberately not claimed. Only four of the thirteen were indexed and all twelve tools earned zero impressions in 28 days, but twelve new pages on a low-authority host explains that on its own; both facts are true and neither is evidence for the other.
    • Fix: scripts/build-teaching-content.js strips the head at publish time — keeping <style> and the ld+json, because dropping either would change the page rather than repair it — and checks its own output for head markup, refusing to write if any survived. publish-teaching.sh builds that content first and aborts the run if the guard refuses, so a bad transform cannot reach WordPress. The head-uniqueness invariant joined check-teaching-published.js, counted in the raw served bytes: a browser silently repairs a stray <title>, so a DOM check would have reported one title and been wrong. All thirteen pages rewritten the same day; every figure in the outgoing content is present in the incoming.
    • Lesson: a gate that checks what a page says is not checking the page. Twelve tools were verified figure-by-figure, to two decimals, while the document carrying them was malformed in a way no figure could reveal — and the script that would have caught it was already written, already correct about what it did check, and simply never run by anything. An unwired gate is indistinguishable from no gate, and it is worse, because its existence reads as coverage.

2026-09-07 (Claude Code — re-verifying Indiana and Michigan before publishing them)

  1. 32 Indiana’s page claimed a legislative event that never happened, in its own headline. The live page asserted that Indiana’s legislature “killed two separate attempts to regulate” sweepstakes during HB 1052’s passage — in the <h1> (“Regulation Defeated Twice”), the lede, a facts row (“Two amendments proposing a licensing framework were defeated during passage”), an entire H2 section (“The regulation option died here”), and a closing line in a second section (“Indiana is the state where the industry’s Plan B was voted down in public”). Its source was labelled Verified and was a commercial aggregator, the “Sweepsy legislative tracker”.

    • Caught by: re-verification against the Indiana General Assembly’s own amendment record, run because the page’s last_verified date was too old for the generator to accept it. The primary record shows House amendments 1–6 with only #6 failed, Senate amendments 1–3 with only #2 passed, and no committee amendments at all. The one failed amendment (House Motion MO105206, Rep. Burton) moved to delete page 13, lines 4–28 of the 27 January 2026 print — which is IC 7.1-3-1-18, publication of alcoholic-beverage permit notices. It concerned neither sweepstakes nor licensing. No licensing amendment was defeated; none was filed.
    • Consequence: a fabricated legislative event was live on the site for weeks as the headline claim of a state page, on a tracker whose entire argument is that it reads primary law.
    • Fix: h1, lede, facts row and both affected sections rewritten from the enrolled act itself; the aggregator source replaced by two primary ones (the enrolled PDF, and the bill’s own amendment record) with the failed amendment’s actual subject stated so nobody re-derives the error. The page now carries what the act really is — a four-limb definition added as SECTION 15 of an omnibus administrative-law act, enacted as Public Law 153.
    • Lesson: DECISIONS 77 banned aggregators as sources and this page was already citing one, labelled Verified. A ban recorded in the register does not retro-scan the pages written before it. The same day this was found, the Michigan page beside it survived the same re-verification intact — so the defect was never “old pages are unreliable”, it was one unchecked citation.
  2. 33 A fidelity check passed a page containing a fabrication, and was right to. SPEC-0040’s transcription of Indiana was accepted on 7 September on a 60-string check: every sentence in the content file traced verbatim to the live page, 0 invented. That check was correct and the executor did nothing wrong — the fabrication was already in the live page it was copying.

    • Caught by: nothing in the transcription pipeline; only the separate re-verification against primary sources.
    • Consequence: none beyond the delay, because the date rule held the page back before publication.
    • Fix: none needed to the checker.
    • Lesson: fidelity to a source is not accuracy, and the two checks answer different questions. A transcription gate asks “does this match the page”; only re-verification asks “is the page true”. The 2026-08-31 entry’s lesson was that a gate checking what a page says is not checking the page. This is its twin: a gate checking that two documents agree is not checking either one. The date rule that refused Indiana — last_verified older than the status date — was written for a different purpose entirely and is what forced the read that caught this.
  3. 34 The homepage quoted a sentence that is not in the statute it cited, and four other pages repeated it. From before 24 July 2026 until 8 September 2026 the homepage displayed, as a cited pull-quote under the heading “The legislatures wrote the exemption themselves”, the sentence “A sweepstakes game does not include a game which does not award cash prizes or cash equivalents”, attributed to New York S5935A and Racing, Pari-Mutuel Wagering and Breeding Law §912. No such sentence is in §912. The codified section, most recent revision 12 December 2025, has four subdivisions — definitions, prohibition, penalties, enforcement — and no exclusion clause; the Assembly’s text of S05935 has three sections and no such clause. The wording appears to come from an earlier draft that did not survive amendment. It was live on five surfaces: the homepage, the New York state page (where it was the whole argument, under an h1 reading “The Statute That Legalised Our Model”), the tracker hub, the calendar, and a journal post that carried it under a Verified label.

    • Caught by: reading §912 verbatim from the legislature’s own site during a re-verification, after a question about something else entirely.
    • Consequence: the site’s central commercial claim rested on a quotation that does not exist, on the page that receives more impressions than any other state page.
    • Fix: all five surfaces rewritten to argue from what §912 actually says — a conjunctive three-limb definition whose middle limb requires currency exchangeable for cash — and republished the same day. A second Verified source, the codified section, added to the New York page.
    • Lesson: the Indiana defect of 7 September was not a one-off, and the sweep it triggered did not reach far enough. That day found a fabricated legislative event sourced to an aggregator on one state page, and the response swept the ten generated state pages. This one had been sitting on the homepage the whole time, because the homepage is not a state page and nothing in the sweep’s scope included it. A correction scoped to the shape of the last defect will not find the same defect in a different shape.
  4. 35 The same homepage paragraph mis-stated Maine’s statute, and mis-stated it in the direction that flattered us. The page said “Maine’s LD 2007 reaches for indirect consideration, which cannot exist where nothing was ever sold”, and the hub, calendar and journal post carried variants. Maine’s enacted law — Public Law c.645, approved 6 April 2026, adding 8 MRSA c. 38 — defines “indirect consideration” as a token that “is provided for free through a promotion or bonus or with the purchase of a related product”. Free provision is the first alternative, so indirect consideration does not require a sale. More importantly the chapter does not turn on that term alone: it turns on a “dual-currency system of payment”, defined to include a system allowing play with direct consideration, which is itself defined to include a token “received through a bonus or promotion”.

    • Caught by: reading the enacted Maine PDF while re-verifying the New York quotation.
    • Consequence: a state described as clearly out of reach is, on the face of its text, unsettled — and the definition is circular, since “direct consideration” is defined by reference to an “online sweepstakes game” which is itself defined by the dual-currency term.
    • Fix: every surface now says the reach is unsettled rather than claiming the point.
    • Lesson: an error found while chasing another error is the cheapest one you will ever get, and it is worth widening the read while you are already in the source. Both of these were in the same paragraph.
  5. 36 Fifteen pages stated «lawful in all fifty» as a fact for a day after the homepage had been corrected for the same claim. On 7 September the homepage’s unhedged «legal in all fifty states» was replaced with the Readiness Framework hedge (DECISIONS 111). The same conclusion, in nearly the same words, stayed live on the twelve game pages, the games hub, the about page and one journal post until 8 September — and by then the register had recorded a state whose gambling definition has no consideration element (Alabama, DECISIONS 127) and one whose currency definition reaches granted coins on its face (Maine, DECISIONS 123). The sentence was not merely unhedged; it was contradicted by the site’s own register.

    • Caught by: a whole-site pattern scan during the 8 September review, not by any gate.
    • Consequence: the platform’s central claim was published in two versions at once, one hedged and one not, for a day.
    • Fix: all fifteen rewritten to the approved hedge the same day; the phrase is now a blocking failure in check-links.js.
    • Lesson: a correction applied to the page where the defect was found is not a correction of the defect. The 7 September fix was scoped to one page because the claim was found on one page. A claim that load-bearing lives wherever the site repeats it, and the fix has to be a sweep, then a gate.
  6. 37 Every teaching page’s main call to action was a dead link, and had been since the explorers were built. The twelve teaching pages each ended with «Open the explorer» pointing at play.flickloot.com/teaching/teaching-<game>.html. The explorers existed — thirteen files in the repo’s handoffs, gated by check-teaching-tool.js on every run — but the teaching/ directory had never been created on the play host. Every one of the twelve links answered 404.

    • Caught by: the 8 September link crawl.
    • Consequence: the interactive half of the teaching layer was unreachable for the whole of its life; a reader who followed the page’s argument to its demonstration hit an error.
    • Fix: the twelve explorers deployed to the play host, md5-identical to the gated files; check-links.js added to the live tier and registered, so an internal link that does not answer 200 is now a blocking failure.
    • Lesson: two gates checked the two ends of the link and neither checked the link. check-teaching-live.js opened the local files; check-teaching-published.js opened the WordPress pages. Both passed every run. A gate that proves both halves exist has not proved they are joined — the same shape as the 2026-08-31 entry, where a gate checked what a page said and not the page.
  7. 38 Thirteen teaching pages served two heads for about twenty minutes on 8 September, because the regenerated pages were published as whole documents. The teaching generator writes complete HTML files; the 31 August repair (entry for that date) had published only the style block and the body, and nothing recorded that. When the pages were regenerated on 8 September to remove a dead link, the whole file went into WordPress and every page carried two <head>, two <title> and two canonicals — the exact defect the 31 August entry describes.

    • Caught by: a spot check in the same session, before any crawler could have read it, and only because the check counted title tags rather than trusting the publish notice.
    • Consequence: none measured; corrected within the session.
    • Fix: republished as fragments; the fragment step is now written into the generator’s own header comment.
    • Lesson: a fix that lives in a session’s hands and not in the tool is re-broken by the next hands. The 31 August session did the right thing and did not write down that the generator’s output is not what gets published; the next publish did what the file suggested.
  8. 39 The error beacon ran for seventeen days with its write path unproven, and its rate limiter leaked a file a minute. From 22 August the beacon was described as «live and silent». Live was true: the endpoint answered. Silent was assumed: no errors-*.jsonl file existed on the server at any point, and the August deploy probes — which should have written one — had left no trace. Nobody had asked the directory; the access log was read instead, and a POST in an access log proves receipt, not writing. Separately, the per-minute rate file was created and never deleted, so the directory held 626 of them by 8 September.

    • Caught by: listing the log directory during a review of whether the click count could be trusted.
    • Consequence: none measured — no client error is known to have gone unrecorded — but the claim «zero client errors» rested on an unverified path for the whole period.
    • Fix: one attended probe, which wrote the beacon’s first real row; and ten-minute housekeeping in the endpoint, bounded to once a minute.
    • Lesson: «silent» has two causes and only one of them is good. An instrument that has never reported is either healthy or broken, and the two are indistinguishable until you make it report once. The play instrument beside it was proven the day it shipped, by the operator’s own visits; the error beacon should have been proven the same way, on the same day.

Generated from ERRATA.md by scripts/build-errata-page.js · 39 entries