Flickloot · Methodology
Errata
Every mistake this project has made, logged as it happened, with what caught it and what changed. 29 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 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 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 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 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 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 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)
-
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)
-
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.
-
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.
-
10 Environment lesson, not a content error: the Cowork device VM cannot delete files on a mounted Windows folder.
git initandgit committherefore succeed but leave stale.git/*.lockbehind, which blocks every subsequent git write until the locks are moved away (mvworks wherermdoes 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)
-
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.
-
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-rclaimed the reduced-motion gate was extended, and it was — for ten of eleven effects.
-
13 Sound and haptics fired once and then went silent. The effect was keyed on
${o.seed}-${o.tier}, butpresentOutcome()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.seedactually contained. - Consequence: the feature was ~half dead on arrival.
- Fix: guarded on outcome object identity, which is fresh per result.
- Lesson:
undefinedin a template literal is a silent string, not an error.
- Caught by: tracing the dependency array back to the object that feeds it, after asking what
-
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.
-
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-STATUScontains literal promote commands and any session reading it may act on them.
-
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)
-
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.
-
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.jsfromscripts/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.
-
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)
-
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.jsnow 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.
-
21 The delegation gate was looser than it read.
delegate-d3-batch.jslisted 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”.
- Caught by:
2026-08-15 (Claude Code — browser-emulation pass on the casino)
-
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 GCwhile the persistedcoinsvalue 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 throughrequestAnimationFrame, and the automation pane was not compositing frames —document.visibilityStatewashidden, 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 becauseuseState(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
setIntervalfiring 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.
- Caught by: a direct probe run before any fix was written — 0 RAF callbacks in 3,803 ms against a
2026-08-16 (Claude Code — SEO pass on flickloot.com)
-
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 rankmathis the real prefix), so it fails silently into a|| wp cache flushfallback 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.shalready almost computes.
2026-08-17 (Claude Code — A2, building the automated regression suite)
-
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 incheck-build.jsthat 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.
-
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 isStake, and the capitals on screen aretextTransform: 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"withgap: 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.jsto 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 reportingn/afor 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.
-
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_MSis 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.
-
27 The banker bet paid over 100%, because the published figure and the payout were computed from two different formulas.
BACCARAT_RTPsimulatedBAC_PAYS.banker— the exact 1.95 — while the table paidMath.round(1.95 × stake). At the 10 GC default stake the exact payout is 19.5 coins;Math.roundmade 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.jsgained an invariant, proven against both the obvious break (Math.roundreinstated) and the dangerous one (payCoinsstill 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.jsnow 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.
- Caught by: extending A1’s one-game rounding note to every game that pays
2026-08-18 (Claude Code — publishing the teaching tools)
-
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.shresolved existing pages withwp 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_parentset 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_keywordfrom 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)
-
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.phpopened 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 tooksrcverbatim. Query strings were stripped only inclient-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.phpwas 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=shouldnotpersistprecisely 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:
srcis 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/.htaccessdenies 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.
- Caught by: the deploy verification probe, which POSTed