Not yet certified. Pre-launch tests passed; final certification requires public launch and an independent recheck of live bets using anonymous, self-funded accounts.

Plinko
Independent verification, backed by open-source code.
- Live Bets Verified
- 10,100
- Parity Rate
- 100%
- Simulated Rounds
- 28M
- Simulated RTP 98.9249%
- Proven RTP (mean)
- 99.0152%
- 98.9063%–99.1602% across the 28 boards
Verification pipeline
Real bets. Independently reproduced.
Captured on LIQD’s pre-launch environment. We rebuilt the game logic from scratch and matched all 10,100 recorded outcomes.
The casino result, reproduced.
- Casino outcome
- Slot 10 · 0.5×
- Our reproduction
- Slot 10 · 0.5×
- Recorded payout
- $0.0500
- Seed Hash IntegrityPass
- Next-Seed Pre-Commitment ChainPass
- Hash Consistency Within EpochPass
- Nonce AuditPass
- Path + Slot RecomputationPass
- Client Seed InfluencePass
- Multiplier LookupPass
- Payout Math (betAmount × multiplier == winningAmount)Pass
- Config Completeness (27 standard + WTF)Pass
- House Edge / RTP Audit (analytical, per-config)Pass
- Phase LabelsPass
- Epoch SizePass
- Anti-Circularity (Binomial Slot Probabilities)Pass
- Phase D — Client Seed VariationPass
- Phase E — WTF Mode VerificationPass
- Simulation — Pass 1 Slot Uniformity (FWER)Pass
- Simulation — Pass 2 Cherry-Pick TestPass
- Bet-Size InvariancePass
- Multiplier Table SymmetryPass
- Artifact Hash + Value-Domain IntegrityPass
- Epoch-Window Slot Fit (live drops vs binomial)Pass
The repo is the credential. You don’t have to trust us — every scored check ships as code. Run npm test to re-run the full audit: 57 unit tests and the 21 verification steps — 10,100 live bets re-verified and all 28M simulated rounds replayed — with nothing installed but Node.js 22.
This audit independently validates the Plinko game operated by LIQD under the ProvablyFair.org Audit Framework 1.1. We placed 10,100 real bets across 202 seed epochs — all 27 standard boards, two stakes, ten custom client seeds and 2,000 drops in WTF mode — and recomputed every outcome with our own implementation, written from a specification without LIQD’s code. The capture ran on LIQD’s pre-production environment, qa.liqd.com, so the certificate is provisional: it extends to liqd.com after an anonymous production capture passes review.
Audit Verdict
| Check | Result | Evidence |
|---|---|---|
| Overall Status | Pass | PROVABLY FAIR — Full Pass · Passed 21/21 · Hard fails 0 · Flags 0. Provisional: captured on LIQD’s pre-production environment, qa.liqd.com; certification extends to liqd.com after an anonymous production capture passes review |
| RTP Verified | Pass | 98.9063%–99.1602% Proven RTP per board (mean 99.0152%) · 98.9249% simulated (28M rounds) · RTP claim rule: LIQD’s published 99% holds on all 28 boards — none more than 0.1 pp below it, worst gap −0.09375 pp (RTP-CLAIM 1.2.0, Step 10) |
| Live ↔︎ Verifier Parity | Pass | 100% — 10,100 / 10,100 drops matched, full path and landing slot, and the ProvablyFair.org verifier matches the same 10,100 (Step 5) |
| Commit-Reveal System | Pass | 202 / 202 revealed seeds hash to their commitments; the next-seed chain is consistent on 201 / 201 rotations plus the pre-capture link into epoch 0 (Steps 1–3) |
| Client Seed | Pass | Player-controlled and customizable — a wrong client seed changes 1,008 / 1,010 sampled paths, and 500 / 500 custom-seed bets recompute (Steps 6, 14) |
| RNG Analysis | Pass | Our own implementation, written from the specification: one HMAC-SHA256 per row, the bounce bit = the first four digest bytes mod 2 — an exact 50/50, bias 0 (Step 5) |
| Payout Logic | Pass | All 10,100 payouts verified — every multiplier matches the pinned tables (Step 7) and every win amount equals stake × multiplier exactly, in 1e-8 units (Step 8) |
| WTF Mode | Pass | Fourth risk level verified on all 2,000 captured drops — each recomputes at 13 rows and pays from the WTF table (Step 15) |
| Anti-Circularity | Pass | Binomial P(slot) × the pinned tables = 98.9063%–99.1602% on all 28 boards, cross-checked by enumerating every path — no operator RTP figure used (Steps 10, 13) |
| Fairness Integrity | Pass | 29 fairness integrity tests across 11 categories — 27 pass, 2 remediated (input hygiene, Low — fixed by LIQD and re-verified), none open |
| Determinism | Pass | Full reproducibility confirmed — 10,100 / 10,100 drops recompute from the recorded inputs (Step 5) |
Overall StatusPass
RTP VerifiedPass
Live ↔︎ Verifier ParityPass
Commit-Reveal SystemPass
Client SeedPass
RNG AnalysisPass
Payout LogicPass
WTF ModePass
Anti-CircularityPass
Fairness IntegrityPass
DeterminismPass
What Was Audited
✓ Audited
- The RNG is deterministic: every drop path reproduces from server seed, client seed, nonce and row count
- Server seeds are committed by SHA-256 hash, and each rotation pre-commits the next seed’s hash
- The client seed is player-controlled and changes the path
- Nonces run 0–49 in every seed epoch with no gaps or reuse
- Each row’s bounce is one HMAC-SHA256 bit — the first four digest bytes mod 2, an exact 50/50
- Every paid multiplier matches LIQD’s tables for all 27 standard boards and WTF mode, and every payout is stake × multiplier exactly
- Proven RTP is 98.9063%–99.1602% per board (mean 99.0152%) — a 0.84%–1.09% house edge
- LIQD’s published 99% RTP holds on all 28 boards under the RTP claim rule — none more than 0.1 pp below it
- WTF mode runs on the same algorithm as the standard game — only the row count and payout table differ
- Drop paths follow the same wager-free derivation at both captured stakes, $0.10 and $10
- Players can independently verify every bet
What Audit Excludes
- Infrastructure or server security
- Wallet, payments, or operational systems outside game logic
- Cross-account sampling
What Audit Covers
| Area | Description |
|---|---|
| Commit-Reveal System | SHA-256 server seed hashing, pre-bet commitment, next-seed pre-commitment, reveal on rotation |
| Client Seed Origin | Player-controlled seed, set by the capture at every rotation — 192 auditor-generated seeds and 10 custom pfaudit- seeds in Phase D |
| Seed Handling | Client seed control, nonce lifecycle, seed epoch rotation |
| RNG Analysis | HMAC-SHA256 per-row algorithm verification, bias analysis, external entropy assessment |
| Payout Logic | Multiplier table accuracy (standard + WTF), exact stake × multiplier settlement, bet-size invariance (Phase C) |
| Live Parity | Independent path and slot recomputation vs live results across all five phases, plus the ProvablyFair.org verifier on the same 10,100 drops |
| RTP Validation | Anti-circularity proof, the RTP claim rule over all 28 boards, simulated RTP (28M rounds), cherry-pick probe (Pass 2) |
| Fairness Integrity | Integrity tests on LIQD’s running service — 29 across 11 categories, including race conditions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, and the provably fair surface |
What Audit Guarantees
- Outcomes are deterministic and reproducible from the recorded inputs
- Live game results match independent recomputation for the verified sample (10,100 / 10,100)
- The slot-distribution and serial screens find no sign of bias across 28M rounds simulated with our implementation
- RTP is proven analytically: binomial P(slot) × multiplier = 98.9063%–99.1602% per board (mean 99.0152%), cross-checked by enumerating every path
- Simulated RTP of 98.9249% across 28 million rounds (1M per board) is consistent with the proven 99.0152% mean, 1.0 standard error below it
- The client seed is a genuine, player-controlled input — a wrong seed changes 1,008 of 1,010 sampled paths
- The house edge is 0.84%–1.09% per board (mean 0.98%) against the advertised 1.00%, and every payout is stake × the table multiplier exactly
- The 202 revealed casino seeds show no early-window selection signature in Pass 2 — 1 seed below z = −1.645 against a calibrated 1.41 expected (P = 0.76)
- All fairness integrity tests passed or were remediated and re-verified at audit time — none is open
- The dataset and multiplier tables are SHA-256 pinned — the audit stops if either changes by a single byte
References
Plinko — Game Rules7 sections▶
Plinko drops a ball down a triangular peg board. You pick the number of rows (8–16) and a risk level — LOW, MEDIUM, HIGH, or the extreme WTF mode. The ball bounces left or right at each row — a fair 50/50 each time — and lands in one of the slots at the bottom. The slot it lands in decides your payout multiplier.
Game Rules
Plinko is played on a triangular peg board with a selectable number of rows. At each row the ball bounces left or right; after rows bounces it lands in one of rows + 1 slots, and the landing slot pays a fixed multiplier. The bounce sequence is not visual chance: each bounce is one bit drawn from HMAC-SHA256(serverSeed, clientSeed:nonce:cursor), so the full path — LIQD records it as dropDetails — is fixed at the moment the bet is accepted and can be recomputed by anyone after the server seed is revealed. The ball animation is cosmetic.
How to Play
1. Choose row count — Select between 8 and 16 rows. More rows means more bounces and more slots — and, at medium and high risk, bigger edge multipliers.
2. Choose risk level — LOW, MEDIUM, or HIGH shifts payout weight toward the edge slots; WTF is a fourth, extreme mode that locks the board to 13 rows.
3. Enter bet amount — Choose how much to wager. The stake is not an input to the path — confirmed at both captured stakes, $0.10 and $10 (Step 18).
4. Drop the ball — The ball bounces through the peg board and lands in a slot.
5. Outcome — Payout = bet amount × the landing slot's table multiplier.
Win Conditions
The win condition depends on which slot the ball lands in — and on the mode.
| Outcome | Condition | Example |
|---|---|---|
| Jackpot (edge slot) | Ball lands in slot 0 or slot rows | 16r / HIGH: 1000× — ~1 in 65,536 per side |
| Win (outer slots) | Ball lands near an edge | 16r / HIGH slot 1: 130× |
| Partial return (standard) | Ball lands near the centre | 16r / HIGH slot 8: 0.2× |
| Zero (WTF only) | Ball lands in any of WTF's 10 middle slots | WTF slots 2–11: 0× — ~99.7% of drops |
Risk vs Reward
The core mechanic is the trade-off between row count, risk level, and payout shape.
- Row count controls granularity — more rows means more slots and, at medium and high risk, higher multipliers at the edges (16r / HIGH tops out at 1000×)
- Risk level controls payout shape — LOW is flat; HIGH concentrates value at the edges with a 0.2× centre floor; WTF is all-or-nothing (1000× / 235× / 0×)
- RTP is near-constant — every board’s proven RTP is 98.9063%–99.1602% (mean 99.0152%); risk mainly changes variance — every board’s edge stays within 0.84%–1.09%
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Row Count | 8–16 (13 forced in WTF) | Player selects; sets slot count = rows + 1 |
| Risk Levels | 1 = LOW · 2 = MEDIUM · 3 = HIGH · 4 = WTF | Integer field on the bet request; WTF is a first-class fourth level |
| House Edge | 1.00% advertised | Exact per-board edge 0.84%–1.09% from the pinned tables |
| Proven RTP | 98.9063%–99.1602% (mean 99.0152%) | Per board, from the pinned tables (S4); WTF 98.9990% |
| Configurations | 28 | 9 row counts × 3 risk levels + WTF |
| RNG Algorithm | HMAC-SHA256 | One HMAC per row, cursor = row + 1; key = hex-decoded 16-byte server seed |
Seed Formats
Every Plinko bet uses three cryptographic inputs to generate the result.
| Seed Type | Format | Example | Purpose |
|---|---|---|---|
| Server Seed | 32-char hex (16 bytes) | fc9a1d6fad00e083… | Casino-provided randomness, revealed on rotation |
| Server Seed Hash | 64-hex SHA-256 digest | 3b34a0fce5732f2d… | SHA-256(utf8(serverSeed)) — published before any bet |
| Client Seed | Player-editable string | auditb1141320acde | Player-contributed entropy |
| Nonce | Integer counter from 0 | 0 | Uniqueness per bet under a seed pair — this capture rotated every 50 bets (nonces 0–49) |
Multiplier Tables & Payout
Payouts are a direct table lookup — PLINKO_FIXED_ODDS[rows][riskLevel][slot] for standard play, PLINKO_WTF_ODDS[slot] for WTF. The tables are hash-pinned in plinkoConfig.json and every one of the 10,100 captured payouts matches them exactly. Expected return is 98.9063%–99.1602% per configuration.
winningAmount = betAmount × PLINKO_FIXED_ODDS[rows][riskLevel][winningSlot]| Rows | Risk | Edge Multiplier (slot 0/N) | Centre Multiplier | RTP |
|---|---|---|---|---|
| 8 | 1 (LOW) | 5.6× | 0.5× | 98.9844% |
| 8 | 3 (HIGH) | 29× | 0.2× | 99.0625% |
| 16 | 1 (LOW) | 16× | 0.5× | 98.9987% |
| 16 | 3 (HIGH) | 1000× | 0.2× | 98.9764% |
| 13 (WTF) | 4 (WTF) | 1000× | 0× | 98.9990% |
Why Provably Fair Matters▶
Traditional online casinos require players to trust that games are fair. Provably fair systems eliminate this trust requirement by allowing players to mathematically verify that outcomes were not manipulated. In a Provably Fair system:
- The casino commits to a result before the player bets
- The player contributes randomness that the casino cannot predict
- Anyone can verify the outcome after the fact
High-Level Overview8 sections▶
Checklist Reference
Based on the ProvablyFair.org Audit Execution Checklist, here are the tests covered under this audit document.
1. Commit-Reveal System & Seed Handling
| Test | Description |
|---|---|
| Server seed commit exists before play | SHA-256 commitment shown before betting; 64-hex hash of the seed text |
| Server seed reveal matches commit | SHA-256(utf8(revealed)) == hashedServerSeed — 202 / 202 |
| Next-seed pre-commitment chain | nextHashedServerSeed links every epoch to the next — consistent on 202 / 202 (201 transitions plus the pre-capture link) |
| Client seed control | Player-controlled and editable — the rotation call submits it and the server echoes it back |
| Nonce increments correctly | 0…49 per epoch, +1 per bet, resets on rotation |
| Full determinism | Same inputs → same path and slot, 10,100/10,100 |
2. Randomness & Entropy Model
| Test | Description |
|---|---|
| RNG depends only on seeds + nonce | Path is a pure function of (serverSeed, clientSeed, nonce, rows) |
| No mixed entropy sources | No timestamps, Math.random, drand, block hash, or VRF |
| Unbiased mapping | First four digest bytes as a uint32, mod 2 — 2 divides 2^32, so each bounce is an exact 50/50 |
| No state leakage | Fresh HMAC per row (cursor = row + 1); no cross-bet memory |
3. Verifier ↔︎ Live Parity
| Test | Description |
|---|---|
| Live outcomes match verifier | 10,100 / 10,100 paths + slots recomputed with 0 mismatches |
| Multi-phase verification | Phases A (all configs) · B (deep sample) · C ($10 stake) · D (custom seeds) · E (WTF) |
| Bet-size invariance | $10.00 drops recompute under the identical engine as $0.10 drops |
4. Game Logic & RTP Validation
| Test | Description |
|---|---|
| Anti-circularity proof | Independent binomial × pinned tables = 98.9063%–99.1602% per config |
| House edge audit | 1.00% advertised; exact per-board edge 0.84%–1.09% |
| RTP claim rule | LIQD’s published 99% holds on all 28 boards — none more than 0.1 pp below it (worst gap −0.09375 pp); 3 sit more than 0.1 pp above it, in the player’s favour |
| Payout rules correctness | winningAmount = betAmount × multiplier exactly, in 1e-8 units, on all 10,100 |
| Simulated RTP | 98.9249% over 28M rounds — consistent with the 99.0152% proven mean |
| Cherry-pick detection | 1 seed below z = −1.645 at the 50-nonce window played, against a calibrated 1.41 expected (P = 0.76); early − late t = −0.19 — no selection signature in this test |
5. Fairness Integrity & Player Verification
| Test | Description |
|---|---|
| Player can reproduce results offline | Using the revealed seed, client seed, nonce, and row count |
| Verifier logic matches live logic | Same HMAC-SHA256 per-row derivation, cursor = row + 1 — the ProvablyFair.org verifier reproduces all 10,100 drops (Step 5) |
| Verifier publicly accessible | ProvablyFair.org verifier — no login required |
| No reliance on private APIs | Fully client-side verification |
| Fairness-integrity matrix | 29 integrity tests across 11 categories — 27 pass · 2 remediated · 0 open (see S5) |
High-Level Flow
To get an overview of how the process works, here is a high-level breakdown:
1. Player Bets — Selects row count (8–16), risk level (LOW/MEDIUM/HIGH/WTF), and bet amount
2. Seeds Combined — HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) once per row, cursor = row + 1
3. RNG Output — Each digest's first 4 bytes form a big-endian uint32; mod 2 gives the bounce bit (0 = left, 1 = right)
4. Path Accumulation — The bits concatenate into dropDetails; winningSlot = count of 1 bits (0…rows)
5. Multiplier Lookup — winningSlot maps to a multiplier via PLINKO_FIXED_ODDS[rows][riskLevel] (or PLINKO_WTF_ODDS)
6. Payout Result — winningAmount = betAmount × multiplier
Provably Fair Model
The model splits control of the randomness. The operator controls the server seed but must commit to it before play; the player controls the client seed and can change it after seeing the commitment; the nonce is a public counter. Because the path is a pure function of those three inputs plus the row count — and the risk level and stake enter only afterwards, as the table and the price — neither side can steer an outcome without the other side’s input, and everything needed to check the result is disclosed by design.
Commit-Reveal Model
The Commit-Reveal model is integral to ensuring fairness and transparency in online gambling. This model involves several key phases:
Commit Phase:
Before any bets are placed, the casino generates a random server seed. To prove its authenticity and prevent later manipulation, only a SHA-256 commitment is sent to the player — for LIQD, SHA-256(utf8(serverSeed hex string)). This ensures that while the player cannot know the seed initially, they can verify it later.
Bet Phase:
The player places their bet. The server combines the server seed with the player's client seed and nonce via HMAC-SHA256 to draw each row's bounce bit. Every bet under a pair uses the same seeds; this capture rotated the pair after every 50 bets.
Reveal Phase:
On rotation the server reveals the retiring plaintext server seed and promotes the pre-committed next pair. After the reveal, the player can independently verify SHA-256(utf8(serverSeed)) == hashedServerSeed.
Verify Phase:
Anyone can recompute every bet's dropDetails path and winningSlot from the revealed server seed, client seed, nonce and row count, using the HMAC-SHA256 construction this audit documents in docs/ALGORITHM.md.
Player-Controlled Client Seed
The client seed is supplied by the player: the rotation call submits it to the server, which echoes it back. Players can set their own client seed at any time:
- Editable at any time in the fairness panel — including right after a new commitment is published
- Mixed into every HMAC message (
clientSeed:nonce:cursor) — each of the 10,100 recorded paths recomputes under its recorded seed (Step 5) - Phase D played 500 drops under 10 auditor-chosen
pfaudit-seeds — all recomputed exactly - A wrong client seed changes 1,008 of 1,010 sampled paths; the two that match are expected chance coincidences (Step 6)
Nonce Lifecycle
The nonce is a counter that increments with each bet within an epoch:
- Starts at 0 for each new seed pair
- Increments by exactly 1 per bet
- Never reused within the same seed epoch
- Resets to 0 when the seed pair rotates (every epoch in this capture is exactly 50 bets, nonces 0–49)
Seed Epoch: (serverSeed, clientSeed) Bet 1: nonce = 0 → path 01101111 → slot 6Bet 2: nonce = 1 → new path (same seeds, new nonce)Bet 3: nonce = 2 → new path…Bet 50: nonce = 49 → epoch complete[Rotation — prior server seed revealed]Next bet: nonce = 0 → new seed pairDeterminism Guarantee
Given identical inputs, the output is always identical:
HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) → Always same digestSame digest per row → Always same bounce bitSame bits across all rows → Always same dropDetails and winningSlotSame slot + same config → Always same multiplier and payoutPhase Map
The capture is engineered — five phases, each isolating one claim:
| Phase | Drops | What it isolates |
|---|---|---|
| A | 5,400 | All 27 standard configs (rows 8–16 × risk 1–3), exactly 200 bets each |
| B | 2,000 | Deep sample of the highest-variance standard config (rows 16, risk 3) |
| C | 200 | Stake independence — identical logic at 100× the base stake (rows 16, risk 3) |
| D | 500 | Client-seed variation — 10 distinct auditor-chosen pfaudit- seeds, 18–19 bets per standard config |
| E | 2,000 | WTF mode — riskLevel = 4, rows forced to 13, PLINKO_WTF_ODDS |
Technical Glossary7 categories▶
Core Concepts
| Term | Definition |
|---|---|
| Provably Fair | A cryptographic system that allows players to mathematically verify that game outcomes were not altered. Relies on commit-reveal schemes and deterministic algorithms. |
| Commit-Reveal Protocol | A two-phase process in which the casino commits to its randomness (by publishing a hash) before the player bets, then reveals the actual seed after rotation. The next epoch's seed hash is also published before rotation, chaining every epoch to the next — a baseline requirement of commit-reveal, consistent here on 202 / 202 links (201 epoch transitions plus the pre-capture link). |
| Determinism | The property that identical inputs always produce identical outputs. Same server seed, client seed, nonce, and row count must always generate the same path and slot. |
| Client Seed Origin | How the client seed is set. On LIQD it is player-controlled and editable — the rotation call submits the player's seed and the server echoes it back. |
| RTP (Return to Player) | 1 − houseEdge. For LIQD Plinko the proven RTP is set by each board’s table: 98.9063%–99.1602% across the 28 boards (mean 99.0152%; WTF 98.9990%) — proven analytically from binomial probabilities and the pinned tables, cross-checked by enumerating every path, and consistent with 28M simulated rounds. Live-bet RTP (80.6843%) is dominated by Phase C/E variance and the unrealised 1000× tails, so it is informational only (S4.8). |
| RTP Claim Rule | RTP-CLAIM 1.2.0, part of the audit method: the RTP a casino publishes — on the game page, fairness page, help page, bet panel, rules or game settings — must hold at every setting to within ±0.1 percentage points; a setting above the figure favours the player and never fails. A house-side miss ends the verdict as RTP FIGURE OVERSTATED (exit 5). LIQD’s published 99% holds on all 28 Plinko boards. |
Seed System
| Term | Definition |
|---|---|
| Server Seed | A 32-character hexadecimal string (16 bytes) generated by LIQD. Secret during betting, revealed on rotation. Hex-decoded to raw bytes for use as the HMAC key. |
| Hashed Server Seed | SHA-256(utf8(serverSeed hex string)) — the text-hash commitment published before betting. Deliberately distinct from the HMAC key encoding. |
| Next Server Seed Hash | The commitment for the following epoch’s server seed, published before rotation. Chaining it to the next epoch’s active hash shows the chain is consistent — 202 / 202 pre-commitment links (201 transitions plus the pre-capture link); our capture records show each was published before its rotation. |
| Client Seed | A player-editable string mixed into every drop as the first HMAC message field. Phase D verified 10 auditor-chosen pfaudit- seeds live. |
| Nonce | A sequential counter identifying a bet’s position under a seed pair — 0–49 in this capture. The middle HMAC message field. |
| Cursor | The per-row index in the HMAC message, cursor = row + 1, running 1…rows. One fresh HMAC per row, one bit per HMAC. |
| Epoch | The bets under one seed pair — 50 in this capture, which rotated the pair after every 50 bets. At rotation the prior server seed is revealed; the dataset holds 202 epochs, all revealed. |
Cryptographic Functions
| Term | Definition |
|---|---|
| HMAC-SHA256 | Keyed hash producing a 32-byte deterministic digest. LIQD Plinko draws one bit per row: key = hex-decoded server seed, message clientSeed:nonce:cursor, first 4 bytes as a big-endian uint32, mod 2. |
| SHA-256 | Secure Hash Algorithm 256-bit. Used for the seed commitment: SHA-256(utf8(serverSeed)) — the seed text, not its decoded bytes. |
| Hex Decoding | Converting the 32-character hex string to 16 raw bytes — required for the HMAC key. Using the text as the key would produce entirely different paths; the dual convention is confirmed by 10,100/10,100 recomputation. |
| Modulo-Bias Rejection | Discarding 32-bit values at or above floor(2^32/range)×range before mod range — the usual guard against modulo bias. Plinko needs none: its range is 2, which divides 2^32 exactly, so u mod 2 is an exact 50/50 and the specification has no rejection step. |
Verification Terms
| Term | Definition |
|---|---|
| Verifier | A tool that independently recomputes drops from the disclosed inputs. The ProvablyFair.org verifier is run unmodified on all 10,100 captured drops by Step 5; LIQD’s own panel has a built-in verify tab. |
| Parity | Degree of matching between verifier and live game results. This audit: 10,100/10,100 exact path-and-slot matches — 100%. |
| Anti-Circularity | Proof that the RTP derivation uses no operator-supplied RTP figure: independently computed binomial B(rows, 0.5) probabilities × the pinned payout tables = 98.9063%–99.1602% per config. |
| Cherry-Pick Detection | Pass 2 extends all 202 revealed casino seeds to 10,000 nonces on one board each and compares an early window with a late one. Scored at the 50-nonce window played: 1 seed below z = −1.645 against a calibrated 1.41 expected (P = 0.76), early − late t = −0.19. Reported at 5,000 nonces: 6 against 5.86 (P = 0.53); slot test 11 flags against 9.6 nominal (P = 0.37). No sign of seed selection on this probe. |
Game Mechanics
| Term | Definition |
|---|---|
Path / dropDetails | The per-row bit string recorded on every bet — 0 = left, 1 = right (e.g. 01101111). Recomputable from the seeds; verified verbatim on all 10,100 drops. |
| Winning Slot | The ball's landing position, 0…rows — the count of 1 bits in the path. Follows the binomial B(rows, 0.5) on a fair board. |
| Row Count | Player-selected parameter (8–16) setting the number of peg rows, HMAC draws, and slots. WTF forces 13. |
| Risk Level | Integer parameter — { LOW: 1, MEDIUM: 2, HIGH: 3, WTF: 4 }. Selects the multiplier table only; the drop physics never change. |
| WTF Mode | A first-class fourth risk level: rows forced to 13, payouts from PLINKO_WTF_ODDS = [1000, 235, 0, …, 0, 235, 1000] — only 4 of 14 slots pay. Same commit-reveal RNG; captured as Phase E. |
| Rapid Fire | An auto-bet speed control on the LIQD UI. It fires drops rapidly using the ordinary risk-level tables — it is not a payout mode and introduces no new odds table. |
| Binomial Distribution | B(rows, 0.5) — the slot-outcome distribution. P(slot = k) = C(rows, k) × 0.5^rows. Each row is an independent 50/50 event. |
Audit Terms
| Term | Definition |
|---|---|
| Phase | A structured capture segment. A: all 27 standard configs × 200. B: deep 16r/HIGH sample. C: $10.00 stake check. D: custom client seeds. E: WTF mode. |
| Full Pass (provisional) | All 21 scored steps pass with 0 flags and 0 hard fails. The capture ran on LIQD’s pre-production environment, so the certificate is provisional: it extends to liqd.com after an anonymous production capture passes review. |
| Hard Fail | Any FAIL on a scored step — a broken commitment or chain link, a path, slot, multiplier or payout mismatch, a nonce gap, a population or artifact-integrity fault. A hard fail ends the verdict as NOT PROVABLY FAIR (exit 1). None triggered: 21 PASS, 0 FLAG, 0 FAIL. |
| Master Dataset | data/plinko-master-10100bets.json — 10,100 drops and 202 fully revealed seed epochs across five phases. |
| Capture Flag | A flag that marks a gap caused by our capture script rather than by the casino — for example, a nonce skipped by a retried request. It is disclosed with a limitation row and does not change the verdict. This package attributes no flag to the capture: Step 4 fails on a nonce gap of any cause, so every gap would count against the casino. The run recorded 0 flags of either kind. |
Data Formats
| Term | Definition |
|---|---|
| plinko-master-10100bets.json | The 10,100-drop dataset used throughout this audit. SHA-256: 871c485ef0d55ca38af2cece4880abdc32fe8b62584468c5d4cfdfd01506fc95. |
| plinkoConfig.json | The hash-pinned multiplier tables — PLINKO_FIXED_ODDS (27 standard boards) + PLINKO_WTF_ODDS — transcribed from LIQD’s client bundle (2026-07-06); 285 of the 365 cells are corroborated by live payouts, the other 80 come from the same pinned transcription. SHA-256: f8eaf67e1ffc63ee0dc21123447581c55f443c167dcb451b45ab7cd108fe712f. |
| verification-results.json | Output of the 21-step verification suite: per-step results with machine-readable data blocks, the SHA-256 of six committed artifacts it checks (dataset, plinkoConfig.json, simulation, calibration, verification-stats, coverage), the payout-table coverage block, and informational items. |
| simulation-results.json | Two-pass simulation output: Pass 1 (28M rounds under auditor-generated crypto-random seeds, recorded per board) and Pass 2 (202 casino seeds × 10,000 nonces, the cherry-pick test). The committed file is the scored run, pinned by SHA-256 (3d94a510…): every npm test replays all 28,000,000 Pass 1 rounds from the saved seeds inside Step 16 and Step 17 re-executes Pass 2 for all 202 epochs. npm run simulate writes a new experiment to outputs/fresh/ instead of touching it. |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Plinko drop on LIQD is generated from four inputs: server seed, client seed, nonce, and row count. LIQD commits to its server seed by publishing a SHA-256 hash before the seed is used — and pre-commits the next seed’s hash at every rotation. After you rotate, the retiring seed is revealed in plaintext, and anyone can check it against both commitments. A matching hash shows which seed goes with which bets and in what order, and our capture records show each hash was published before its bets — so the casino cannot change where the ball lands after you bet.
What we verified
- Every revealed server seed hashes to the commitment published for its epoch — 202 / 202
- The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 201 / 201 links, plus the pre-capture link into epoch 0
- Client seed is player-controlled and set after the server commitment
- Players can set or change their client seed at any time via the fairness panel
- Nonce increments by 1 per bet with no gaps or duplicates across 202 seed epochs
- Drop paths are fully determined by the seed inputs before the ball drops
- Identical inputs always produce the same path and slot — confirmed across all 10,100 drops
- Your client seed is a genuine input — changing it changes the path
What this means for you
- With the commitment shown first, the casino cannot change where the ball lands after you bet
- Set your own unpredictable client seed after the commitment is shown, and the server cannot pre-select against it
- Every bet has its own derivation — the nonce guarantees no two bets share the same inputs
- Any result can be independently verified using the public tools and repo
- Outcomes stay verifiable long after the round — the seeds and the code are all you need
- Server-seed cherry-picking is structurally contained when you set an unpredictable client seed after the commitment is published — the operator cannot pre-evaluate server seeds against a seed it cannot predict
- Every revealed server seed hashes to the commitment published for its epoch — 202 / 202
- The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 201 / 201 links, plus the pre-capture link into epoch 0
- Client seed is player-controlled and set after the server commitment
- Players can set or change their client seed at any time via the fairness panel
- Nonce increments by 1 per bet with no gaps or duplicates across 202 seed epochs
- Drop paths are fully determined by the seed inputs before the ball drops
- Identical inputs always produce the same path and slot — confirmed across all 10,100 drops
- Your client seed is a genuine input — changing it changes the path
- With the commitment shown first, the casino cannot change where the ball lands after you bet
- Set your own unpredictable client seed after the commitment is shown, and the server cannot pre-select against it
- Every bet has its own derivation — the nonce guarantees no two bets share the same inputs
- Any result can be independently verified using the public tools and repo
- Outcomes stay verifiable long after the round — the seeds and the code are all you need
- Server-seed cherry-picking is structurally contained when you set an unpredictable client seed after the commitment is published — the operator cannot pre-evaluate server seeds against a seed it cannot predict
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
pfaudit- seeds in Phase D; the rotation call submits the seed and the server echoes it backClient seed controlPass
Client seed influencePass
Deterministic outputPass
All 202 revealed seeds match their commitments, and the next-seed chain is consistent across 201 rotations plus the pre-capture link into epoch 0. Nonces run 0–49 in every epoch with no gaps. Outcomes are fully deterministic — the same server seed, client seed, nonce and row count always produce the same path and slot. With each commitment shown before its bets, as our capture records show, the casino cannot change your result after you bet.
| Check | Result | Evidence |
|---|---|---|
| Seed hash integrity | Pass | 202 / 202 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1) |
| Next-seed chain | Pass | Consistent on 201 / 201 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2) |
| One commitment per epoch | Pass | All 50 bets in each of 202 epochs carry the same commitment — no mid-epoch seed change (Step 3) |
| Nonce sequencing | Pass | Nonces 0–49 in every epoch: 0 gaps, 0 duplicates, one client seed per epoch (Step 4) |
| Client seed origin | Pass | Set by the capture at every rotation — 192 auditor-generated “audit”-prefixed seeds and 10 custom pfaudit- seeds in Phase D; the rotation call submits the seed and the server echoes it back |
| Client seed control | Pass | Player can set or change the client seed at any time — 500 / 500 Phase D bets recompute under their custom seeds (Step 14) |
| Client seed influence | Pass | A wrong client seed changes 1,008 / 1,010 sampled paths; the 2 matches sit inside the exact chance-coincidence null (Step 6) |
| Deterministic output | Pass | Same (serverSeed, clientSeed, nonce, rows) always produces the same path and slot — 10,100 / 10,100 confirmed (Step 5) |
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
pfaudit- seeds in Phase D; the rotation call submits the seed and the server echoes it backClient seed controlPass
Client seed influencePass
Deterministic outputPass
All 202 revealed seeds match their commitments, and the next-seed chain is consistent across 201 rotations plus the pre-capture link into epoch 0. Nonces run 0–49 in every epoch with no gaps. Outcomes are fully deterministic — the same server seed, client seed, nonce and row count always produce the same path and slot. With each commitment shown before its bets, as our capture records show, the casino cannot change your result after you bet.
How It Works — Seed, Nonce & Determinism7 sections
1.1Server Seed Commitment
Before any bet is placed under a seed pair, LIQD shows the SHA-256 hash of a secret 32-character hex server seed as hashedServerSeed. The commitment hashes the seed text as a UTF-8 string, not its 16 decoded bytes: SHA-256(utf8(serverSeed)). When the pair rotates, the server seed is revealed and anyone can recompute the hash. A match shows the revealed seed is the committed one: a substitute would need a second preimage of SHA-256, and would fail every recomputation that follows.
// SHA-256 over raw bytes, returned as lowercase hex.export function sha256Buffer(buf: Buffer): string { return createHash("sha256").update(buf).digest("hex");} // The published commitment (ALGORITHM.md §1.2): SHA-256 of the seed's ASCII// text exactly as written, NOT of the bytes the hex decodes to. Note the// deliberate asymmetry with the HMAC key in §2.export function commitHash(serverSeedHexString: string): string { if (typeof serverSeedHexString !== "string") { throw new Error("serverSeed must be a string"); } return sha256Buffer(Buffer.from(serverSeedHexString, "ascii"));}Real Example from Live Data:
{ "epoch": 0, "phase": "A", "at": "2026-07-07T02:12:06.712Z", "clientSeed": "auditb1141320acde", "hashedServerSeed": "3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36", "nextHashedServerSeed": "63703846b7a22f7deafcb0d944a119ffd03b4510857b1a33842b99f3b11a322c", "serverSeed": "fc9a1d6fad00e0832fddfdea45435c85", "nonceStart": 0, "nonceEnd": 49, "commitVerified": true, "chainLinkOk": true}Verification:
const crypto = require('crypto'); const serverSeed = "fc9a1d6fad00e0832fddfdea45435c85"; const hashedServerSeed = crypto .createHash('sha256') .update(serverSeed, 'utf8') // hash the TEXT, not the decoded bytes .digest('hex'); console.log(hashedServerSeed);// Output: 3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36 ✅1.2Next-Seed Pre-Commitment Chain
At every seed rotation, the nextHashedServerSeed LIQD pre-commits during an epoch must equal the hashedServerSeed that becomes active after the rotation, so the successor seed is fixed before the previous epoch ends. Step 2 walks all 201 epoch transitions and checks each link, and also checks the pre-capture record: the seed active before the first audited bet (a83779dd…) hashes to its own commitment (de6441eb…), and its pre-committed next hash is epoch 0’s hashedServerSeed (3b34a0fc…). LIQD’s own Provably Fair panel shows the pre-committed next hash alongside the active one (E02, listed in S6.10; the S6.2 walkthrough screenshot shows the same fields). A hash chain shows the values are consistent and fixes their order; it does not carry a date. Our capture records show each commitment was published before the bets it covers.
// ── Step 2: Next-seed pre-commitment chain ────────────────────────────────── // Each epoch's nextHashedServerSeed must equal the following epoch's hashedServerSeed. // Prepended: the pre-capture link (meta.preCapture) — its revealed serverSeed hashes // to its committed hashedServerSeed, and its nextHashedServerSeed equals epoch 0's // hashedServerSeed. This is the evidence that epoch 0's seed was committed BEFORE the // first captured bet, which the epoch-to-epoch chain alone does not show. const byEpoch = [...seeds].sort((a, b) => a.epoch - b.epoch); let promoChecked = 0, promoFails = 0; const preFails: string[] = []; // The pre-capture link is scored evidence in F2 (it is what proves epoch 0's seed was // committed before the first captured bet), so it is MANDATORY, not optional: a dataset // without meta.preCapture cannot pass this step. if (!preCapture) { promoFails++; preFails.push('meta.preCapture missing (required — it anchors the chain before epoch 0)'); } else { promoChecked++; // (a) pre-capture commit: SHA-256(utf8(revealedServerSeed)) == its hashedServerSeed. if (!preCapture.revealedServerSeed || commitHash(preCapture.revealedServerSeed) !== preCapture.hashedServerSeed) { promoFails++; preFails.push('pre-capture commit mismatch'); } // (b) chain link: pre-capture nextHashedServerSeed == epoch 0's hashedServerSeed. if (byEpoch.length > 0 && preCapture.nextHashedServerSeed !== byEpoch[0].hashedServerSeed) { promoFails++; preFails.push('pre-capture → epoch 0 chain link mismatch'); } } const epochTransitions = Math.max(byEpoch.length - 1, 0); for (let i = 0; i + 1 < byEpoch.length; i++) { promoChecked++; if (byEpoch[i].nextHashedServerSeed !== byEpoch[i + 1].hashedServerSeed) promoFails++; }1.3Hash Consistency Within Epoch
For each of the 202 epochs, the hashedServerSeed field was extracted from every bet response and checked for uniqueness. More than one distinct hash within a single epoch would indicate a mid-epoch seed substitution — a critical commit-reveal violation. What binds each bet to its seed is the recomputation: the seed record’s hash matches the reveal (Step 1), and the revealed seed reproduces every path in the epoch (Step 5).
// ── Step 3: Hash consistency within epoch ──────────────────────────────────── // Group by epoch (NOT by hash) so this can genuinely FAIL if any epoch mixed // hashedServerSeed values. Grouping by hash would be tautological. const betsByEpoch = new Map<number, typeof bets>(); for (const b of bets) { const arr = betsByEpoch.get(b.epoch) ?? []; arr.push(b); betsByEpoch.set(b.epoch, arr); } let epochsMultipleHashes = 0; for (const [, epochBets] of betsByEpoch) { const hashes = new Set(epochBets.map(b => b.hashedServerSeed)); if (hashes.size !== 1) epochsMultipleHashes++; }hashedServerSeed constant within every epoch. Zero mid-epoch changes detected.1.4Client Seed Origin & Control
Players control their client seed through the rotation call: the client submits the new seed in the request body and the server echoes it back. The client seed is the first field of every HMAC message, so changing it produces a completely different path. The capture rotated to a new client seed at every epoch — 202 distinct seeds. 192 epochs used audit followed by 12 hex characters (48 bits, all distinct), from a generator that does not ship with the audit package. Phase D bet 500 drops under 10 auditor-chosen pfaudit- seeds to confirm each is genuinely consumed; those follow a predictable pfaudit-<run-id>-<epoch> pattern, so the operator could predict nine of them (epochs 153–161) when it committed their server-seed hashes. For those nine epochs, and conservatively for epoch 152, the defence against seed selection is statistical (S4.5), not structural.
auditb1141320acdeauditc680c9435be0pfaudit-1783390325646-152pfaudit-1783390325646-153pfaudit-1783390325646-161
pfaudit- seeds (192 auditor-generated · 10 custom, treated as predictable). The rotation response echoes the submitted value. Evidence: plinko-master-10100bets.json1.5Nonce Incrementation
The nonce begins at 0 and increments by 1 for each bet under the active seed pair — one nonce per drop. It resets to 0 when the pair rotates. Every epoch in this capture is exactly 50 bets (nonces 0–49), with no gaps, no duplicates, and a single client seed per epoch. Both boundary nonces recompute: the epoch-0 drops at nonce 0 and nonce 49 both reproduce from the same revealed seed.
// ── Step 4: Nonce audit ────────────────────────────────────────────────────── const hardFailures: string[] = []; let epochsChecked = 0; for (const [hash, epochBets] of byHash) { const sorted = [...epochBets].sort((a, b) => a.nonce - b.nonce); const shortHash = hash.substring(0, 16); const nonces = sorted.map(b => b.nonce); const clientSeeds = new Set(sorted.map(b => b.clientSeed)); if (clientSeeds.size !== 1) { hardFailures.push(`Epoch ${shortHash}: ${clientSeeds.size} distinct client seeds`); } if (nonces[0] !== 0) { hardFailures.push(`Epoch ${shortHash}: first nonce is ${nonces[0]} (expected 0)`); } const maxNonce = Math.max(...nonces); const nonceSet = new Set(nonces); // The nonce stream of an epoch must be EXACTLY {0,1,…,EXPECTED_EPOCH_SIZE−1}, each once. A // membership-only scan (0..max present) misses a DUPLICATE — e.g. a second copy of // nonce 0 replacing the last nonce still spans 0..max. Require the distinct-nonce // count to equal the bet count (no duplicates) and the max to be EXPECTED_EPOCH_SIZE−1 (no // truncation / off-by-one), in addition to first==0 and 0..max fully covered. if (nonceSet.size !== sorted.length) { hardFailures.push(`Epoch ${shortHash}: ${sorted.length - nonceSet.size} duplicate nonce(s)`); } if (maxNonce !== EXPECTED_EPOCH_SIZE - 1) { hardFailures.push(`Epoch ${shortHash}: max nonce is ${maxNonce} (expected ${EXPECTED_EPOCH_SIZE - 1})`); } for (let n = 0; n <= maxNonce; n++) { if (!nonceSet.has(n)) { hardFailures.push(`Epoch ${shortHash}: missing nonce ${n}`); break; } } epochsChecked++; }01101111 (slot 6); nonce 49 → path 00010110 (slot 3).1.6Deterministic Mapping
The RNG is fully deterministic: given the same server seed, client seed, nonce, and row count, it always produces the same path. One fresh HMAC-SHA256 is drawn per row, with the cursor running 1 … rows; the key is the hex-decoded 16-byte server seed and the message is clientSeed:nonce:cursor. The first 4 bytes of each digest form a big-endian uint32, and that value mod 2 is the bounce bit (0 = left, 1 = right) — no rejection step is needed, because 2 divides 2^32 exactly. The path is the concatenated bits; the landing slot is the count of 1 bits.
// One drop (ALGORITHM.md §2 Steps 1-3): one HMAC per row with the cursor// running 1..rows (cursor base 1, not 0), bit_r = u_r mod 2, path = the bits// in row order (row 1 first), slot = the count of 1-bits ("count the rights").export function revealPlinko( serverSeed: string, clientSeed: string, nonce: number, numberOfRows: number,): { path: string; winningSlot: number } { // §1.1: rows is an integer 8..16. if (!Number.isInteger(numberOfRows) || numberOfRows < 8 || numberOfRows > 16) { throw new Error("numberOfRows must be an integer in [8, 16]"); } let path = ""; let winningSlot = 0; for (let r = 1; r <= numberOfRows; r++) { const bit = generateProvablyFairNumber(serverSeed, clientSeed, nonce, r, 2); path += bit === 1 ? "1" : "0"; winningSlot += bit; } return { path, winningSlot };}dropDetails and winningSlot on every bet. Zero mismatches, zero skipped — every server seed in the dataset was revealed.Real Bet Verified:
// Source: data/plinko-master-10100bets.json// Bet SLOjmT9dyuUEi1que576R (Phase A, epoch 0, nonce 0, 8 rows, risk 1)// ✅ VERIFIED — path + slot recomputed from the revealed server seed// serverSeed is joined from the epoch’s seed record (seeds[], by hashedServerSeed); the other fields are the bet record { "serverSeed": "fc9a1d6fad00e0832fddfdea45435c85", "hashedServerSeed": "3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36", "clientSeed": "auditb1141320acde", "nonce": 0, "numberOfRows": 8, "riskLevel": 1, "dropDetails": "01101111", "winningSlot": 6, "multiplier": 1.1, "betAmount": "0.10000000", "winningAmount": "0.11000000"}Verification:
// revealPlinko("fc9a1d6f…", "auditb1141320acde", 0, 8) // For each row, cursor = 1 … 8:// message = "auditb1141320acde:0:{cursor}"// digest = HMAC-SHA256(hexDecode(serverSeed), message)// bit = first 4 bytes (big-endian uint32) mod 2 // Path: 01101111 → slot 6 ✅// Multiplier (8r / risk 1, slot 6): 1.1× ✅// Payout: 0.10 × 1.1 = 0.11 ✅1.7Client Seed Influence
To confirm the client seed is a genuine HMAC input, the suite samples five bets from every revealed epoch — 1,010 bets — and recomputes each under a deliberately wrong client seed. 1,008 of 1,010 paths change (99.8%). The two unchanged paths are expected: a different seed reproduces an identical path with probability 2^−rows per bet (0.5425 expected collisions across the sample — Σ 2^−rows; the observed 2 has Poisson-binomial P(X ≥ 2) = 0.103 against the step’s α = 0.001). This check runs on our implementation; Step 5 ties it to the outcomes LIQD recorded, where every one of the 10,100 recorded paths recomputes under its recorded client seed. Phase D additionally verified 500 drops across 10 distinct pfaudit- client seeds — 500/500 recomputed.
// ── Step 6: Client seed influence ───────────────────────────────────────────── // Sample up to 5 bets per revealed epoch; expected coverage is derived from the // dataset (Σ min(5, epoch size) over revealed epochs), so the step cannot pass // vacuously when reveals are missing (tested collapses to 0). let tested = 0, changed = 0, expectedTested = 0; // Per-bet coincidence probability under H0: the wrong-seed re-drop reproduces all // `rows` bits by chance. Collected per bet because the sample mixes board sizes // (8–16 rows), so a single pooled p would be wrong. const samePathProbs: number[] = []; const byEpoch = new Map<string, typeof bets>(); for (const b of bets) { const arr = byEpoch.get(b.hashedServerSeed) ?? []; arr.push(b); byEpoch.set(b.hashedServerSeed, arr); } for (const [hash, epochBets] of byEpoch) { const ss = seedMap.get(hash); if (!ss) continue; expectedTested += Math.min(5, epochBets.length); for (const b of epochBets.slice(0, 5)) { tested++; samePathProbs.push(Math.pow(2, -b.numberOfRows)); const correct = revealPlinko(ss, b.clientSeed, b.nonce, b.numberOfRows); const wrong = revealPlinko(ss, WRONG_CLIENT, b.nonce, b.numberOfRows); if (correct.path !== wrong.path) changed++; } } const samePath = tested - changed; const expectedSamePath = samePathProbs.reduce((s, p) => s + p, 0); // P(X >= observed coincidences) under the exact Poisson-binomial null. const samePathSurvivalP = tested > 0 ? poissonBinomialSurvival(samePathProbs, samePath) : 0;Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary7 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Seed hash integrity (Step 1) | 202/202 revealed seeds | Pass |
| Pre-commitment chain (Step 2) | 202 / 202 links consistent (201 transitions + the pre-capture link) | Pass |
| Hash consistency (Step 3) | 202/202 epochs | Pass |
| Nonce audit (Step 4) | 0 gaps, 0 duplicates, single seed per epoch | Pass |
| Path + slot recomputation (Step 5) | 10,100/10,100 | Pass |
| Client seed influence (Step 6) | 1,008/1,010 sampled (99.8%) | Pass |
| Epoch size (Step 12) | 202 epochs, exactly 50 bets each | Pass |
1.9Code References6 source files
| File | Purpose |
|---|---|
| tests/verify.ts | 21-step verification pipeline (Steps 1–6, 12 cover S1) |
| tests/steps/commitment.ts | Steps 1–4: commit-reveal integrity checks |
| tests/steps/determinism.ts | Steps 5–6: drop recomputation and client-seed influence |
| src/rng.ts | commitHash · generateProvablyFairNumber · revealPlinko |
| src/loader.ts | Dataset loading, SHA-256 hash guard and the pinned population constants (10,100 bets · 202 seeds · 50 per epoch · phase plan) |
| capture/capture-plinko.reference.mjs | Illustrative capture reference (phase plan, endpoint sequence, rotation discipline); the capture tool itself is not published |
1.10Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live capture from qa.liqd.com — 10,100 real drops across 5 phases, gameId fast-games-5, USDC |
| Capture environment | LIQD’s pre-production environment, qa.liqd.com · one authenticated session, one account |
| Total Records | 10,100 drops · 202 revealed seed epochs of exactly 50 bets each |
| Captured | 7 July 2026 (02:12–04:16 UTC, single session) |
| SHA-256 | 871c485ef0d55ca38af2cece4880abdc32fe8b62584468c5d4cfdfd01506fc95 |
| Pre-flight check | src/loader.ts recomputes this hash and aborts the suite before any step runs on a mismatch |
The capture methodology ships as an illustrative reference (capture/capture-plinko.reference.mjs, with no credentials): it documents the phase plan, endpoint sequence and rotation discipline; the capture tool used its own config ordering, Phase D seed naming and field schema. Every computational claim downstream of the dataset is reproducible from the file itself. Fields used: serverSeed, hashedServerSeed, nextHashedServerSeed, clientSeed, nonce, numberOfRows, riskLevel, dropDetails, winningSlot, multiplier, betAmount, winningAmount
1.11Verified Invariants9 invariants
| Invariant | Result |
|---|---|
| SHA-256(utf8(serverSeed)) = hashedServerSeed for all 202 revealed seeds | Pass |
| Next-seed pre-commitment chain consistent on all 201 transitions and the pre-capture link (202 / 202) | Pass |
| hashedServerSeed constant within epoch for all 202 epochs | Pass |
| Zero nonce gaps within any epoch | Pass |
| Zero nonce duplicates within any epoch | Pass |
| Exactly 50 bets per epoch (min = max = 50) | Pass |
| Same inputs produce same path and slot for all 10,100 drops | Pass |
| Wrong client seed changes the path in 1,008/1,010 sampled bets (99.8%) | Pass |
| Client seed player-submitted and echoed back by the server | Pass |
1.12Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinko && git checkout v1.0.1npm cinpm run verify# Expected output: Steps 1–6 and 12 all PASSSteps 1–6 and 12 cover all Seed, Nonce & Determinism checks. Expected output:
[PASS] Step 1 — Seed Hash Integrity
[PASS] Step 2 — Next-Seed Pre-Commitment Chain
[PASS] Step 3 — Hash Consistency Within Epoch
[PASS] Step 4 — Nonce Audit
[PASS] Step 5 — Path + Slot Recomputation
[PASS] Step 6 — Client Seed Influence
[PASS] Step 12 — Epoch SizeRNG & Entropy Model
Is the randomness genuinely random, or could it be rigged?
This section verifies that LIQD’s Plinko paths come from the disclosed inputs alone, through a construction that is unbiased by design: one HMAC-SHA256 digest per row (cursor 1 … rows), each row’s bounce taken as the digest’s first 4 bytes, read as a big-endian uint32, mod 2 — and because 2 divides 2^32, the left/right split is exact. The implementation is our own, written from a specification without LIQD’s code; it reproduces all 10,100 live drops, and the ProvablyFair.org verifier serves as a second check.
What we verified
- HMAC-SHA256 produces cryptographically sound, unpredictable output for each row
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Each row’s left/right decision is an exact 50/50 — reducing a 32-bit value to one bit has zero modulo bias
- The slot-distribution screen finds no sign of bias on any of the 28 boards (0 / 28 chi-squared failures over 28M simulated rounds)
- The serial screen finds no sign of dependence between consecutive drops (0 / 28 failures at the corrected threshold)
- 99.8% of sampled paths change with a different client seed (1,008 / 1,010)
What this means for you
- Each row’s bounce is an even coin flip — the arithmetic leaves no room for skew
- Left and right bounces are equally likely — no directional bias
- No hidden randomness or server-side tricks influence the ball’s path
- Consecutive bets are not correlated — past results don’t affect future outcomes
- The algorithm depends only on seeds you can verify
- HMAC-SHA256 produces cryptographically sound, unpredictable output for each row
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Each row’s left/right decision is an exact 50/50 — reducing a 32-bit value to one bit has zero modulo bias
- The slot-distribution screen finds no sign of bias on any of the 28 boards (0 / 28 chi-squared failures over 28M simulated rounds)
- The serial screen finds no sign of dependence between consecutive drops (0 / 28 failures at the corrected threshold)
- 99.8% of sampled paths change with a different client seed (1,008 / 1,010)
- Each row’s bounce is an even coin flip — the arithmetic leaves no room for skew
- Left and right bounces are equally likely — no directional bias
- No hidden randomness or server-side tricks influence the ball’s path
- Consecutive bets are not correlated — past results don’t affect future outcomes
- The algorithm depends only on seeds you can verify
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
Key encoding verifiedPass
Serial independencePass
Client seed influencePass
Every captured path comes from the disclosed inputs alone, and our own implementation — written from the specification — reproduces all 10,100. Its bit draw is unbiased by construction, and the slot-distribution and serial screens find no sign of bias across 28M simulated rounds on all 28 boards. The client seed is a genuine input — 99.8% of sampled paths change with a different seed.
| Check | Result | Evidence |
|---|---|---|
| RNG derived only from disclosed inputs | Pass | HMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — no hidden entropy |
| Entropy purity | Pass | No timestamps, external APIs, Math.random, or server-side state |
| Algorithm independently implemented | Pass | Our implementation, written from the specification, reproduces all 10,100 drops; the ProvablyFair.org verifier matches the same 10,100 (Step 5) |
| Modulo bias | Pass | u mod 2 over the whole 2^32 domain — 2 divides 2^32, so each bit value has exactly 2^31 preimages: bias 0, and all 131,970 recomputed row draws are drawn that way (Step 5) |
| Key encoding verified | Pass | Server seed hex-decoded to bytes for the HMAC key, confirmed by the 10,100-drop recomputation (Step 5); the commitment hashes the seed text, confirmed by 202 / 202 seed hashes (Step 1) |
| Serial independence | Pass | 0 / 28 boards fail the lag-1 / runs screen at Bonferroni, 1M rounds each (Step 16) |
| Client seed influence | Pass | A different client seed changes 1,008 of 1,010 sampled paths (Step 6) |
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
Key encoding verifiedPass
Serial independencePass
Client seed influencePass
Every captured path comes from the disclosed inputs alone, and our own implementation — written from the specification — reproduces all 10,100. Its bit draw is unbiased by construction, and the slot-distribution and serial screens find no sign of bias across 28M simulated rounds on all 28 boards. The client seed is a genuine input — 99.8% of sampled paths change with a different seed.
How It Works — RNG & Entropy Model7 sections
2.1RNG Function Implementation
Each Plinko bet resolves one row at a time. For a board of rows rows, the algorithm runs rows HMAC-SHA256 computations and concatenates one bit per row into the path. The HMAC key is the server seed hex-decoded to 16 raw bytes. The message for each row is clientSeed:nonce:cursor, with the cursor running 1 … rows. The first 4 bytes of the digest, read as a big-endian uint32, are taken mod 2: 1 = right, 0 = left. There is no rejection step — none is needed for a range of 2.
| Component | Detail |
|---|---|
| Hash function | HMAC-SHA256 |
| Key | hex_decode(serverSeed) — 16 bytes |
| Message | clientSeed:nonce:cursor · cursor = 1 … rows |
| Extraction | First 4 digest bytes → big-endian uint32 (no rejection step) |
| Reduction | uint32 mod 2 → 0 (left) or 1 (right) |
| Accumulation | bits concatenate into dropDetails; slot = count of 1 bits |
| Output range | 0 to rows (inclusive) — rows + 1 slots |
// The HMAC message (ALGORITHM.md §2 Step 1): clientSeed ":" nonce ":" cursor,// with nonce and cursor in plain base-10 (no padding, sign or separators).// Encoding to bytes is UTF-8 and happens at the HMAC call (§2 Step 1, O5).export function getProvablyFairHmacSalt(clientSeed: string, nonce: number, cursor: number): string { if (typeof clientSeed !== "string") throw new Error("clientSeed must be a string"); assertNonNegInt("nonce", nonce); assertNonNegInt("cursor", cursor); return `${clientSeed}:${nonce.toString(10)}:${cursor.toString(10)}`;} // One draw for a single cursor (ALGORITHM.md §2 Steps 1-2):// digest = HMAC-SHA256(key = hex-decoded server seed, message = salt as UTF-8)// u = first 4 digest bytes as an unsigned 32-bit big-endian integer// result = u mod range// The spec only defines range = 2 (the row bit), which is exactly unbiased on// the 2^32 domain. Other ranges use the same u mod range rule, with no// rejection step, since the spec defines none; any range that does not divide// 2^32 is then slightly biased (see the ambiguity note in the report).export function generateProvablyFairNumber( serverSeed: string, clientSeed: string, nonce: number, cursor: number, range: number,): number { if (!Number.isSafeInteger(range) || range < 1 || range > TWO_POW_32) { throw new Error("range must be an integer in [1, 2^32]"); } const key = seedKey(serverSeed); const message = getProvablyFairHmacSalt(clientSeed, nonce, cursor); const digest = createHmac("sha256", key).update(Buffer.from(message, "utf8")).digest(); const u = digest.readUInt32BE(0); // §2 Step 2: bytes 0..3, big-endian return u % range;}docs/ALGORITHM.md) without LIQD’s code, matches all 10,100 live drops with zero mismatches; the ProvablyFair.org verifier, run unmodified, matches the same 10,100 (Step 5).2.2Entropy Sources
Every bit of randomness in a LIQD Plinko drop comes from one deterministic HMAC-SHA256 pipeline over four cleanly separated inputs — keyed by the 16-byte decoded server seed, messaged by clientSeed:nonce:cursor:
| Source | Controlled By | Purpose |
|---|---|---|
| Server Seed | Casino | Base randomness (committed via SHA-256(utf8(seed)) before betting) |
| Client Seed | Player | Player-contributed entropy (player-set, editable) |
| Nonce | System | Uniqueness per bet (increments automatically; 0–49 in this capture) |
| Cursor | System | Per-row isolation (runs 1 … rows) |
How we know: 10,100/10,100 drops were recomputed using only the four declared inputs. If any hidden entropy source existed, recomputation would fail. It does not.
Verified absent: no timestamps, no Math.random(), no external APIs, no drand / block hash / VRF, no server-side mutable state. Only: HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor)
2.3Modulo Bias Analysis
Each row’s bounce is u mod 2, where u is the first four digest bytes as an unsigned 32-bit integer. Modulo bias appears when the number of possible values is not a multiple of the range. Here it is: 2^32 = 2 × 2^31, so each bit value has exactly 2^31 preimages and the split is exact. The specification has no rejection step because none is needed, and Step 5 checks this on our implementation itself: on every row of every recomputed drop, the path bit equals the range-2 draw, which equals the full 32-bit value mod 2 — 131,970 / 131,970 row draws.
range = 2
domain = 2^32 = 4,294,967,296 values of u
Even values of u: 2,147,483,648 → left (50.000%)
Odd values of u: 2,147,483,648 → right (50.000%)
Bias: 0 — 2 divides 2^32 exactly/** Worst residual bias of `u mod range` over the 2^32 domain: (max − min preimages) / 2^32. */export function modBias(range: number): number { if (!Number.isSafeInteger(range) || range < 1 || range > RNG_DOMAIN) throw new Error(`range ${range} outside [1, 2^32]`); return (RNG_DOMAIN % range === 0 ? 0 : 1) / RNG_DOMAIN;}// … for (const d of drops) { c.drops++; const { path } = revealPlinko(d.serverSeed, d.clientSeed, d.nonce, d.rows); for (let cursor = 1; cursor <= d.rows; cursor++) { c.rowDraws++; const bit = generateProvablyFairNumber(d.serverSeed, d.clientSeed, d.nonce, cursor, EXTRACTION_RANGE); const u = generateProvablyFairNumber(d.serverSeed, d.clientSeed, d.nonce, cursor, RNG_DOMAIN); if (path[cursor - 1] === String(bit)) c.pathEqualsRangeDraw++; if (bit === u % EXTRACTION_RANGE) c.rangeDrawEqualsDomainDrawMod++; } }Only the first 4 bytes of each per-row digest are consumed. Because each row uses its own fresh HMAC, a 16-row board simply issues 16 independent HMACs — it never needs a second chunk from any digest.
2.4RNG Isolation
Each row within a single bet uses a unique cursor value in the HMAC message (clientSeed:nonce:cursor, cursor = row + 1), ensuring per-row outputs are cryptographically independent. Each bet uses a unique nonce, ensuring per-bet independence. HMAC-SHA256 is a pseudorandom function — knowing one row’s output gives no usable information about any other row’s. There is no shared state between rows, bets, or epochs.
Evidence: the revealPlinko implementation in S1.6 is a pure function — no class state, no external calls, no cross-bet memory. It takes (serverSeed, clientSeed, nonce, rows) explicitly and returns the path and slot. Same inputs always produce the same output. Row count and risk level never seed the bit stream; risk only selects the payout table.
2.5Monte Carlo Simulation (28M Rounds)
A 28,000,000-round Monte Carlo simulation (1,000,000 rounds per board, all 27 standard boards plus WTF) tests the slot distribution at scale. Pass 1 drew fresh crypto-random seed pairs (crypto.randomBytes) and stores each pair in the committed run; every npm test replays all 28M rounds from those saved seeds inside Step 16, and the replayed figures match the committed ones bit for bit (784 / 784). A new npm run simulate is a new experiment with different numbers, written to outputs/fresh/.
| Metric | Value |
|---|---|
| Configurations | 28 (27 standard + WTF) |
| Rounds per config | 1,000,000 |
| Total rounds | 28,000,000 |
| Mean proven RTP (authoritative) | 99.0152% |
| Mean simulated RTP | 98.9249% (committed run) |
| Chi-squared (Bonferroni α/28) | 0/28 fail |
| Serial independence (Bonferroni) | 0/28 fail |
/** One round: winningSlot = count of right-bits over `rows` (src/simulate.ts simulateSlot). */export function simulateSlot(serverSeed: string, clientSeed: string, nonce: number, rows: number): number { let slot = 0; for (let i = 0; i < rows; i++) { slot += generateProvablyFairNumber(serverSeed, clientSeed, nonce, i + 1, 2); } return slot;}// … for (let nonce = 0; nonce < rounds; nonce++) { const slot = simulateSlot(serverSeed, clientSeed, nonce, rows); slotFreq[slot]++; payoutSum += table[slot]; winSequence[nonce] = slot > rows / 2 ? 0 : 1; // binary for serial test if ((nonce + 1) % convergenceStep === 0) convergence.push(payoutSum / (nonce + 1)); }npm test replays the whole run from those seeds.Scored result: 0/28 boards fail chi-squared and 0/28 fail serial independence at the Bonferroni threshold α/28 = 3.5714×10⁻⁴. Uncorrected: 0/28 chi-squared at α = 0.01 and 1/28 serial at α = 0.05, against about 1.4 expected by chance — on this binarisation the lag-1 and runs statistics are one test (corr = −0.99999996), so the family is 28 tests.
Methodology: one seed pair per board; chi-squared goodness-of-fit on slot counts against the independent binomial B(rows, 0.5) with df = rows; serial independence via lag-1 autocorrelation and the Wald-Wolfowitz runs test.
2.6Serial Independence
Serial independence means consecutive outcomes are not correlated — winning on one drop does not affect the next. Two tests were applied to each configuration in the Pass 1 simulation (1,000,000 rounds per config):
Lag-1 autocorrelation: Measures correlation between consecutive outcomes on the binarised above/below-midpoint sequence (the same series the runs test uses). Expected r ≈ 0 for independent sequences; scored at the Bonferroni-corrected z threshold.
Wald-Wolfowitz runs test: Tests whether the sequence of above/below-midpoint outcomes has the expected number of runs; scored at the Bonferroni-corrected threshold.
export function lag1Autocorrelation(series: number[]): number { const n = series.length; let mean = 0; for (let i = 0; i < n; i++) mean += series[i]; mean /= n; let num = 0, den = 0; for (let i = 0; i < n - 1; i++) num += (series[i] - mean) * (series[i + 1] - mean); for (let i = 0; i < n; i++) den += (series[i] - mean) ** 2; return den === 0 ? 0 : num / den;}2.7Worked Example — Full RNG Trace
Real drop from the dataset — bet SLOjmT9dyuUEi1que576R, epoch 0, nonce 0, 8 rows, risk 1. Verified from plinko-master-10100bets.json:
serverSeed = fc9a1d6fad00e0832fddfdea45435c85
clientSeed = auditb1141320acde
nonce = 0
rows = 8 (risk 1 · LOW)
cursor = row + 1 → 1…8| cursor | HMAC[0:4] | uint32 | % 2 | direction | slot so far |
|---|---|---|---|---|---|
| 1 | 1bc5cc26 | 465,947,686 | 0 | left | 0 |
| 2 | 0f9d6173 | 261,972,339 | 1 | right | 1 |
| 3 | dbdb6f8d | 3,688,591,245 | 1 | right | 2 |
| 4 | f460931e | 4,099,969,822 | 0 | left | 2 |
| 5 | ce51a3ed | 3,461,456,877 | 1 | right | 3 |
| 6 | 056cc631 | 91,014,705 | 1 | right | 4 |
| 7 | 0cf8aab5 | 217,623,221 | 1 | right | 5 |
| 8 | 136bb6c5 | 325,826,245 | 1 | right | 6 |
SLOjmT9dyuUEi1que576R — dropDetails = 01101111 and winningSlot = 6 match the HMAC-SHA256 recomputation exactly. Multiplier (8r / risk 1, slot 6): 1.1×. Payout: 0.10 × 1.1 = 0.11.Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Our implementation (Step 5) | 10,100 / 10,100 drops — written from the specification | Pass |
| ProvablyFair.org verifier (Step 5) | 10,100 / 10,100 paths, slots and multipliers; paytable 365 / 365 cells equal to the pinned tables | Pass |
| Key/commitment encoding (hex key vs utf8 commitment) | Confirmed via 10,100-drop + 202-seed recomputation | Pass |
| Modulo bias analysis | 2 divides 2^32 — bias 0; 131,970 / 131,970 row draws taken as u mod 2 (Step 5) | Pass |
| External entropy non-participation (Step 5) | 10,100 / 10,100 reproduced from disclosed inputs alone | Pass |
| Simulation chi-squared (Step 16) | 0 / 28 boards fail at Bonferroni α/28 | Pass |
| Serial independence (Step 16) | 0 / 28 boards fail (lag-1 + runs test, corrected) | Pass |
| Client-seed influence (Steps 6, 14) | 1,008 / 1,010 sampled + 500 / 500 Phase D | Pass |
2.9Code References8 source files
| File | Purpose |
|---|---|
| src/rng.ts | commitHash · generateProvablyFairNumber · revealPlinko — written from docs/ALGORITHM.md |
| docs/ALGORITHM.md | The written specification of the commitment and the bit derivation |
| src/bias.ts | The bit-extraction bias bound and the census on our implementation’s own draw |
| src/simulate.ts · src/pass1.ts | Two-pass Monte Carlo (28M rounds + cherry-pick probe); pass1.ts is the loop Step 16 replays |
| src/stats.ts | Chi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test |
| tests/steps/determinism.ts | Steps 5–6: drop recomputation (with the ProvablyFair.org verifier) and client-seed influence |
| tests/steps/simulation.ts | Steps 16–17: the Pass 1 replay and gates, the Pass 2 re-execution |
| tests/plinko/*.ts | Mocha unit tests — 57 across rngSpecTests (the specification self-test), rngTests, rtpPinTests, calibrationTests and domainTests |
2.10Verified Invariants9 invariants
| Invariant | Result |
|---|---|
| HMAC-SHA256 output matches the live game for all 10,100 drops | Pass |
| Key is the hex-decoded 16-byte seed; commitment hashes the seed text — both encodings confirmed | Pass |
2 divides 2^32 — u mod 2 is an exact 50/50, zero modulo bias | Pass |
| No external entropy sources required for path computation | Pass |
| Slot distribution consistent with binomial B(rows, 0.5) on all 28 configs (1M rounds each) | Pass |
| Lag-1 autocorrelation within corrected bounds for all 28 configs | Pass |
| Runs test within corrected bounds for all 28 configs | Pass |
| Simulated RTP (98.9249%) is consistent with the 99.0152% proven mean | Pass |
| Client-seed change produces a different path in 1,008/1,010 sampled drops | Pass |
2.11Datasets UsedSource artifacts
2.12Reproduction InstructionsRun locally
Clone the repository, install dependencies, and replay the simulation:
git clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinko && git checkout v1.0.1npm cinpm run replay # replays all 28M Pass 1 rounds from their saved seeds (writes nothing)npm run verify # Steps 5, 6 and 16 cover S2S2-related steps:
[PASS] Step 5 — Path + Slot Recomputation
[PASS] Step 6 — Client Seed Influence
[PASS] Step 16 — Simulation — Pass 1 Slot Uniformity (FWER)
Note: `npm run simulate` draws fresh seeds — a new experiment whose simulated RTP differs from this run’s by sampling noise around the 99.0152% proven mean. `npm run replay` and every `npm test` instead replay the committed run from its saved seeds and must reproduce it.Verifier Parity
Does the live game actually follow its own rules?
This section validates that our implementation produces the exact same bit path and slot as the live game for every drop, and that the ProvablyFair.org verifier does too. It also confirms the payout math and that every paid multiplier — standard and WTF — matches the hash-pinned tables. Any mismatch would invalidate the fairness guarantee.
What we verified
- Every drop independently recomputed from seeds — the full path and landing slot verified, not just the payout
- Payout correctness: win amount = bet × multiplier for all 10,100 drops, exactly, in 1e-8 units
- Every paid multiplier matches the pinned tables — all 27 standard boards and WTF mode
- Bet amount is not an input to the RNG — the path depends only on seeds, nonce, and row count
- All five capture phases recomputed identically (config coverage, deep sample, elevated stake, client seed variation, WTF mode)
- The ProvablyFair.org verifier, run unmodified, reproduces all 10,100 drops
What this means for you
- The verifier isn’t a simulation — it produces the exact same path and slot as the live game
- Every bet you play can be independently recomputed by anyone
- At both captured stakes, $0.10 and $10, and in WTF mode, the path follows the same derivation
- All 10,100 captured outcomes match the specified algorithm exactly
- Every drop independently recomputed from seeds — the full path and landing slot verified, not just the payout
- Payout correctness: win amount = bet × multiplier for all 10,100 drops, exactly, in 1e-8 units
- Every paid multiplier matches the pinned tables — all 27 standard boards and WTF mode
- Bet amount is not an input to the RNG — the path depends only on seeds, nonce, and row count
- All five capture phases recomputed identically (config coverage, deep sample, elevated stake, client seed variation, WTF mode)
- The ProvablyFair.org verifier, run unmodified, reproduces all 10,100 drops
- The verifier isn’t a simulation — it produces the exact same path and slot as the live game
- Every bet you play can be independently recomputed by anyone
- At both captured stakes, $0.10 and $10, and in WTF mode, the path follows the same derivation
- All 10,100 captured outcomes match the specified algorithm exactly
Path + slot recomputationPass
ProvablyFair.org verifierPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 10,100 drops matched our implementation exactly — paths and slots across all five capture phases — and the ProvablyFair.org verifier matched the same 10,100. Payout math is exact at both stakes. Every paid multiplier matches the pinned tables, WTF included.
| Check | Result | Evidence |
|---|---|---|
| Path + slot recomputation | Pass | 10,100 / 10,100 exact match — full path and landing slot verified for every drop (Step 5) |
| ProvablyFair.org verifier | Pass | Run unmodified on all 10,100 drops — same path, slot and multiplier on every one; its paytable equals the pinned tables on all 365 cells (Step 5) |
| Payout correctness | Pass | All 10,100 drops: win amount = bet × multiplier exactly, compared as integers in 1e-8 units (Step 8) |
| Multiplier table integrity | Pass | Every paid multiplier matches the pinned tables at 1e-9 — 285 of the 365 cells were paid in the capture (Step 7) |
| Bet-size independence | Pass | Bet amount is absent from the RNG input — same path derivation at $0.10 and $10. Tested in Phase C (200 / 200) |
| Config completeness | Pass | All 27 standard configurations (8–16 rows × low/medium/high) plus WTF covered in live data |
| Multi-phase coverage | Pass | 5 structured phases: config coverage (A), deep sample (B), elevated stake (C), client seed variation (D), WTF mode (E) |
Path + slot recomputationPass
ProvablyFair.org verifierPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 10,100 drops matched our implementation exactly — paths and slots across all five capture phases — and the ProvablyFair.org verifier matched the same 10,100. Payout math is exact at both stakes. Every paid multiplier matches the pinned tables, WTF included.
How It Works — Verifier Parity8 sections
3.1Why Parity Matters
If the verifier produces results that differ from the live game, players cannot trust the verification — the entire provably fair system becomes meaningless. 100% parity is required because even a single discrepancy would indicate either a bug in the verification logic, manipulation in the live game, or an inconsistent RNG implementation. Players must be able to take the revealed seeds after gameplay, input them into the independent verifier, and receive the exact same path and slot they experienced during live play. Plinko parity here is stronger than a payout check: the entire per-row bit sequence (dropDetails) must reproduce, not just the final number.
3.2Five-Phase Collection Design
Data was collected across five structured phases, each designed to isolate a specific fairness property. The phases are complementary — together they cover configuration breadth, statistical depth, bet-size invariance, client-seed verification, and the WTF mode.
| Phase | Bets | Config | Bet Amount | Purpose |
|---|---|---|---|---|
| A — Configuration coverage | 5,400 | All 27 standard | $0.10 | Verify every standard config behaves correctly (200 bets each) |
| B — High-variance sampling | 2,000 | 16r / risk 3 | $0.10 | Deep sample of the highest-variance standard config |
| C — Bet-size invariance | 200 | 16r / risk 3 | $10.00 | Confirm bet amount is not in the RNG (100× stake) |
| D — Client-seed verification | 500 | All 27 standard (18–19 each) | $0.10 | Confirm 10 custom pfaudit- seeds are genuinely consumed |
| E — WTF mode | 2,000 | WTF (13r / risk 4) | $0.10 | Exercise the fourth risk level |
3.3Path + Slot Recomputation (Step 5)
For every bet, the verifier independently computed the full path with revealPlinko(serverSeed, clientSeed, nonce, numberOfRows) and compared both the bit string and the slot count to the server-reported dropDetails and winningSlot. The same step runs the ProvablyFair.org verifier’s code, pinned by SHA-256, unmodified on every captured bet: it must return the same path, slot and multiplier as our implementation and the capture, and its paytable must equal the pinned tables cell for cell.
// ── Step 5: Path + slot recomputation ──────────────────────────────────────── let mismatches = 0, skipped = 0; for (const b of bets) { const ss = seedMap.get(b.hashedServerSeed); if (!ss) { skipped++; continue; } const { path, winningSlot } = revealPlinko(ss, b.clientSeed, b.nonce, b.numberOfRows); if (path !== b.dropDetails || winningSlot !== b.winningSlot) mismatches++; } // … // ── Player-verifier parity (PLINKO-24) ───────────────────────────────────────── // The LIQD Plinko verifier on the ProvablyFair.org docs site (derive.js + the two files it // imports + the source card the page displays) is vendored byte for byte and pinned in // src/site-verifier.ts. It is RUN here, unmodified but for its module keywords, on every // captured bet, and must return the same L/R path and slot as src/rng.ts revealPlinko AND as // the capture, and the multiplier the casino served. Its paytable must equal plinkoConfig.json // cell for cell over all 28 boards, and the source card must BE derive.js. The population is // bound to EXPECTED_BETS, so a verifier that silently skipped bets cannot pass. // … const d = v.derivePlinko({ clientSeed: b.clientSeed, serverSeed: ss, nonce: b.nonce, rows: b.numberOfRows, riskLevel: b.riskLevel }); const ours = revealPlinko(ss, b.clientSeed, b.nonce, b.numberOfRows); const bad: string[] = []; if (d.path !== ours.path || d.path !== b.dropDetails) { sitePathBad++; bad.push(`path ${d.path} vs ours ${ours.path} vs served ${b.dropDetails}`); } if (d.slot !== ours.winningSlot || d.slot !== b.winningSlot) { siteSlotBad++; bad.push(`slot ${d.slot} vs ours ${ours.winningSlot} vs served ${b.winningSlot}`); } if (d.rows !== b.numberOfRows) { siteRowsBad++; bad.push(`rows ${d.rows} vs served ${b.numberOfRows}`); } if (d.multiplier !== b.multiplier) { siteMultBad++; bad.push(`multiplier ${d.multiplier} vs served ${b.multiplier}`); }3.4Payout Math (Step 8)
For each of the 10,100 bets, the verifier computes betAmount × multiplier and compares it with the server-reported winningAmount as integers in 1e-8 settlement units — no tolerance on money. Both amounts are 8-decimal strings, so the comparison is exact: 0.10 × 1.1 is 0.11000000000000001 as a binary double, but 11,000,000 units on the grid. Step 7 binds multiplier to the recorded winningSlot, which Step 5 binds to the seed-derived slot; Step 8 binds winningAmount to that multiplier.
// ── Step 8: Payout math (betAmount × multiplier == winningAmount) ───────────── // EXACT, in integer 1e-8 settlement units — not a tolerance on money. betAmount and // winningAmount are 8-dp strings, so the grid is exact and the comparison is integer // equality; the only tolerance is inside unitsOf(), snapping a JSON double back onto // the grid. Framework floor: exact, or <= 1e-8. // … let payErrors = 0, offGrid = 0, worstUnits = 0, worstExample = ''; for (const b of bets) { const betU = unitsOf(b.betAmount); const winU = unitsOf(b.winningAmount); if (betU === null || winU === null || !Number.isFinite(b.multiplier)) { offGrid++; payErrors++; continue; } // The multiplier is a genuine quotient, so the product is rounded onto the grid the // operator settles on; any residual after that is a money difference, not noise. const expectedU = Math.round(betU * b.multiplier); const deltaU = Math.abs(expectedU - winU); if (deltaU !== 0) { payErrors++;winningAmount = betAmount × multiplier exactly — 0 amounts off the grid, 0 errors, including all 200 drops at $10.00. The bet response carries no running-balance field, so the credit is checked against the credited winningAmount field.3.5Multiplier Table Provenance (Step 7)
For each of the 10,100 bets, the observed multiplier was compared against the hash-pinned table entry for its (rows, riskLevel, winningSlot) — PLINKO_FIXED_ODDS for standard drops, PLINKO_WTF_ODDS for WTF. The tables were transcribed from LIQD’s client bundle (0pm_um5zrrwwn.js, qa.liqd.com/originals/plinko, 2026-07-06) and pinned in plinkoConfig.json (SHA-256 f8eaf67e1ffc63ee…); 285 of the 365 table cells are corroborated by live payouts, the other 80 come from the same pinned transcription; src/config.ts re-hashes the file at startup and aborts on any change.
| Metric | Count |
|---|---|
| Total bets checked (incl. 2,000 WTF) | 10,100 |
| Matching the pinned tables (1e-9) | 10,100 |
| Matching neither table | 0 |
PLINKO_FIXED_ODDS[rows][riskLevel][slot] + PLINKO_WTF_ODDS[slot]. Every live payout multiplier matches plinkoConfig.json. The proven RTP of these tables is 98.9063%–99.1602% per board (S4).3.6Multiplier Symmetry (Step 19)
For all 28 configurations the multiplier table was verified to be left/right symmetric: table[k] = table[rows − k] for every slot k. This mirrors the symmetric binomial — slots k and (rows − k) have identical probabilities, so their multipliers must match. Symmetry was also observed live: two symmetric 29× edge hits landed in the same epoch on the 8-row HIGH board — slot 0 (path 00000000) and slot 8 (path 11111111) under the same revealed seed.
// ── Step 19: Multiplier Table Symmetry ────────────────────────────────────── // A fair Plinko board is left/right symmetric: multiplier[k] == multiplier[rows−k] for // every slot. Verified across all standard tables (each rows × risk 1–3) plus WTF. { let tables = 0, asymmetric = 0; const fails: string[] = []; const check = (arr: number[], label: string) => { tables++; for (let k = 0; k < arr.length; k++) { if (arr[k] !== arr[arr.length - 1 - k]) { asymmetric++; if (fails.length < 4) fails.push(label); break; } } }; for (const rowsKey of Object.keys(cfg.payout_tables)) { const rows = Number(rowsKey); for (const risk of [1, 2, 3]) { check(payoutTable(cfg, rows, risk), `${rows}r/risk${risk}`); } } check(payoutTable(cfg, cfg.wtf_mode.rows, WTF_RISK), `WTF(${cfg.wtf_mode.rows}r)`); const ok = tables > 0 && asymmetric === 0;VKKaYIxBU4NPLU_vc01v9 (slot 0, 29×) and U10OkvkfU3FwjCy17z3tC (slot 8, 29×), same epoch, seed 0d4d79f7eb58d2831817a81415a1fd2f.3.7Phase C — Bet-Size Invariance (Step 18)
Phase C placed 200 bets at $10.00 on the same 16-row / risk-3 board as Phase B’s $0.10 bets, so stake is the only variable that moved. All 200 paths and slots recompute from the revealed seeds, and all 200 multipliers match the same pinned table as Phase B. The structural point: revealPlinko takes no wager argument, so in our implementation the path cannot depend on stake — and the live game matched it at both captured stakes. At n = 200 no distributional test is applied; the deterministic 200/200 recomputation is the evidence.
// ── Step 18: Bet-Size Invariance ──────────────────────────────────────────── // revealPlinko(serverSeed, clientSeed, nonce, rows) takes no wager argument, so the // drop path and slot cannot depend on stake. Proven at the isolated $10 stake: Phase C // is 200 drops at $10 (16 rows, risk 3), recomputed here path-and-slot alongside the // $0.10 drops scored in Step 5 — same engine, both stakes, identical determinism. // Deterministic; no distributional test at n=200. { const c = ctx.phaseC; let chk = 0, bad = 0; for (const b of c) { const ss = seedMap.get(b.hashedServerSeed); if (!ss) continue; chk++; const { path, winningSlot } = revealPlinko(ss, b.clientSeed, b.nonce, b.numberOfRows); if (path !== b.dropDetails || winningSlot !== b.winningSlot) bad++; } // … // A SINGLE Phase C board is not enough; it has to be the PHASE B board. Phase C exists // to isolate STAKE, and on a board Phase B never ran, stake and board move together and // the comparison isolates neither — the deep-sampled board is one thing and the // high-stake drops are another. The two boards are read from the bets and required // equal; naming either one here would let a capture that drifted keep passing. const phaseBConfigs = boardOf(ctx.phaseB ?? []); const phaseBLabel = phaseBConfigs.join(', ') || 'none'; const sameBoard = phaseBConfigs.length > 0 && phaseCConfigs.join(',') === phaseBConfigs.join(','); // … const ok = chk === c.length && chk > 0 && bad === 0 && singleConfig && sameBoard;3.8Worked Example — Full Parity Verification
Real drop from Phase B — bet xrCFhXxywfY7R9_O9gMLK, epoch 113, nonce 4, 16 rows, risk 3. Verified from plinko-master-10100bets.json:
serverSeed = ea78c284099479c0624dcc65bd49a6b4
clientSeed = auditfdbdfd859cad
nonce = 4
rows = 16 (risk 3 · HIGH)| Step | Process | Output |
|---|---|---|
| 1 | HMAC-SHA256(hexDecode(serverSeed), "auditfdbdfd859cad:4:1") through cursor 16 | 16 bounce bits |
| 2 | Concatenate bits into the path | 1111111111011111 |
| 3 | winningSlot = count of 1 bits = 15 | Matches live game ✅ |
| 4 | Multiplier lookup: 16r / risk 3, slot 15 | 130× |
| 5 | Payout: 0.10000000 × 130 | 13.00000000 ✅ |
// revealPlinko("ea78c284…", "auditfdbdfd859cad", 4, 16)// 16 HMAC calls (cursor 1…16) → path = 1111111111011111// winningSlot = count of '1' bits = 15//// Multiplier lookup: PLINKO_FIXED_ODDS[16][3][15] = 130//// Payout: 0.10000000 × 130 = 13.00000000// Live winningAmount: 13.00000000 ✅xrCFhXxywfY7R9_O9gMLK — path, slot, multiplier, and payout all match exactly between the live game and our recomputation. A 130× edge-region hit reproduced bit-for-bit.Technical Evidence & Verification5 sections
3.9Evidence Coverage Summary9 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Path + slot recomputation (Step 5) | 10,100 / 10,100 — full bit path verified | Pass |
| ProvablyFair.org verifier (Step 5) | 10,100 / 10,100; paytable 365 / 365 cells | Pass |
| Payout math (Step 8) | 10,100 / 10,100 exact, in 1e-8 units | Pass |
| Multiplier table integrity (Step 7) | All 28 configs — observed multipliers match the pinned tables at 1e-9 | Pass |
| Multiplier symmetry (Step 19) | 28/28 tables — slot k = slot (rows − k) | Pass |
| Phase C recomputation (Step 18) | 200 / 200 at $10.00 | Pass |
| Config completeness (Step 9) | 27/27 standard + WTF present in live data | Pass |
| Phase labels (Step 11) | A 5,400 · B 2,000 · C 200 · D 500 · E 2,000 = 10,100 | Pass |
| WTF mode (Step 15) | 2,000/2,000 recomputed, 2,000/2,000 table-conformant | Pass |
3.10Code References9 source files
| File | Purpose |
|---|---|
| tests/steps/determinism.ts | Step 5: path + slot recomputation, with the ProvablyFair.org verifier |
| tests/steps/payouts.ts | Steps 7–10: multiplier lookup, payout math, config completeness, RTP audit |
| tests/steps/dataset.ts | Steps 11–12: phase labels, epoch size |
| tests/steps/wtf.ts | Step 15: WTF-mode verification |
| tests/steps/standardization.ts | Steps 18–20: bet-size invariance, multiplier symmetry, artifact hashes |
| src/rng.ts | HMAC-SHA256 path computation |
| src/config.ts | Pinned-table lookup and RTP calculation |
| src/money.ts | Integer 1e-8 settlement units for the payout check |
| src/site-verifier.ts · verifier-parity/ | The vendored ProvablyFair.org verifier, pinned by SHA-256 and run by Step 5 |
3.11Datasets UsedSource artifacts
3.12Verified Invariants11 invariants
| Invariant | Result |
|---|---|
| Computed path matches live dropDetails for all 10,100 drops | Pass |
| Computed slot matches live winningSlot for all 10,100 drops | Pass |
| The ProvablyFair.org verifier reproduces the path, slot and multiplier of all 10,100 drops | Pass |
| winningAmount = betAmount × multiplier exactly (1e-8 units) for all 10,100 drops | Pass |
| Every multiplier matches the pinned tables at 1e-9 — all 28 configurations | Pass |
| Multiplier tables symmetric for all 28 configs: slot k = slot (rows − k) | Pass |
| Phase C ($10.00) produces identical derivations to $0.10 bets | Pass |
| No hidden inputs beyond (serverSeed, clientSeed, nonce, rows) | Pass |
| All 27 standard configs + WTF present in live data | Pass |
| Phase D: 500/500 drops verified across 10 custom client seeds | Pass |
| Phase E: 2,000/2,000 WTF drops verified at forced 13 rows | Pass |
3.13Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinko && git checkout v1.0.1npm cinpm run verify# Expected output: Steps 5, 7–9, 11, 15, 18–19 all PASSS3-related steps:
[PASS] Step 5 — Path + Slot Recomputation
[PASS] Step 7 — Multiplier Lookup
[PASS] Step 8 — Payout Math (betAmount × multiplier == winningAmount)
[PASS] Step 9 — Config Completeness (27 standard + WTF)
[PASS] Step 11 — Phase Labels
[PASS] Step 15 — Phase E — WTF Mode Verification
[PASS] Step 18 — Bet-Size Invariance
[PASS] Step 19 — Multiplier Table SymmetryRTP & Payout Logic
Is the house edge what the casino claims?
This section verifies the advertised 1.00% house edge against what the published tables actually pay on all 28 boards. The key test is anti-circularity: we derive the RTP by applying independently computed binomial probabilities to the payout tables — no operator RTP figure is used. We hold LIQD’s published 99% to every board under the RTP claim rule, compare it against 28 million simulated rounds, and test whether the casino pre-selected seeds against the player.
What we verified
- House edge 0.84%–1.09% per board (mean 0.98%) against the advertised 1.00% — derived independently from the payout tables
- RTP derived from first principles: binomial P(slot) × multiplier = 98.9063%–99.1602% on every board, cross-checked by enumerating every path
- LIQD’s published 99% RTP holds on all 28 boards — none more than 0.1 pp below it (worst −0.09375 pp); 3 sit more than 0.1 pp above it, in the player’s favour
- Every payout equals stake × the table multiplier exactly, at both captured stakes
- 28M-round simulation: mean 98.9249%, consistent with the 99.0152% theoretical mean (1.0 standard error below)
- Cherry-pick probe: LIQD’s 202 revealed seeds show no early-window shortfall — 1 seed below z = −1.645 against a calibrated 1.41 expected (P = 0.76)
- Every captured WTF drop paid from the pinned WTF table — 98.9990% RTP while paying on only 4 of 14 slots
What this means for you
- The house edge is about 1% of every bet on every board — no board returns more than 0.1 pp below the 99% LIQD advertises
- The RTP is derived from independently computed probabilities applied to the payout tables — it doesn’t rely on the casino’s own RTP claim
- The casino’s seeds show no sign of being chosen against the player
- At both captured stakes, the bet amount made no difference to where the ball landed
- WTF’s all-or-nothing shape is variance, not a different house edge — its RTP matches the standard game
- House edge 0.84%–1.09% per board (mean 0.98%) against the advertised 1.00% — derived independently from the payout tables
- RTP derived from first principles: binomial P(slot) × multiplier = 98.9063%–99.1602% on every board, cross-checked by enumerating every path
- LIQD’s published 99% RTP holds on all 28 boards — none more than 0.1 pp below it (worst −0.09375 pp); 3 sit more than 0.1 pp above it, in the player’s favour
- Every payout equals stake × the table multiplier exactly, at both captured stakes
- 28M-round simulation: mean 98.9249%, consistent with the 99.0152% theoretical mean (1.0 standard error below)
- Cherry-pick probe: LIQD’s 202 revealed seeds show no early-window shortfall — 1 seed below z = −1.645 against a calibrated 1.41 expected (P = 0.76)
- Every captured WTF drop paid from the pinned WTF table — 98.9990% RTP while paying on only 4 of 14 slots
- The house edge is about 1% of every bet on every board — no board returns more than 0.1 pp below the 99% LIQD advertises
- The RTP is derived from independently computed probabilities applied to the payout tables — it doesn’t rely on the casino’s own RTP claim
- The casino’s seeds show no sign of being chosen against the player
- At both captured stakes, the bet amount made no difference to where the ball landed
- WTF’s all-or-nothing shape is variance, not a different house edge — its RTP matches the standard game
Anti-circularityPass
House edge auditPass
RTP claim (RTP-CLAIM 1.2.0)Pass
Simulated RTP (Pass 1)Pass
Cherry-pick probe (Pass 2)Pass
Bet-size invariancePass
WTF mode RTPPass
Config completenessPass
The RTP is proven mathematically from binomial probabilities — P(slot) × multiplier = 98.9063%–99.1602% per board, using independently computed probabilities and the pinned tables: an exact identity, not a statistical estimate. LIQD’s published 99% holds on all 28 boards: none returns more than 0.1 pp below it. 28M simulated rounds are consistent with it, and the cherry-pick probe finds no sign of seed selection.
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | Binomial P(slot) × multiplier = 98.9063%–99.1602% on all 28 boards — the probabilities checked against an independent integer Pascal recurrence (Step 13); the multipliers are the pinned tables |
| House edge audit | Pass | 1.00% advertised; exact per-board edge 0.84%–1.09%; every board’s RTP matches an exhaustive enumeration of all 2^rows paths to 8.76e-13 (Step 10) |
| RTP claim (RTP-CLAIM 1.2.0) | Pass | LIQD publishes 99% RTP on every board (Plinko game page); 0 of 28 boards more than 0.1 pp below it — worst −0.09375 pp, rows 8 and 10 at medium risk (Step 10) |
| Simulated RTP (Pass 1) | Pass | 28M rounds — mean 98.9249% vs 99.0152% theoretical (1.0 standard error below); 0/28 chi-squared and 0/28 serial failures at Bonferroni; all 28M rounds replayed on every run (Step 16) |
| Cherry-pick probe (Pass 2) | Pass | 202 casino seeds — at the 50-nonce window played: 1 below z = −1.645 vs a calibrated 1.41 (P = 0.76), early − late t = −0.19; at 5,000 nonces: 6 vs 5.86 (P = 0.53) (Step 17) |
| Bet-size invariance | Pass | Bet amount is not an input to the RNG — same path derivation at $0.10 and $10. Tested in Phase C (200 / 200, Step 18) |
| WTF mode RTP | Pass | 98.9990% from the WTF table — same edge, extreme variance; 2,000 / 2,000 drops conform (Step 15) |
| Config completeness | Pass | All 27 standard boards plus WTF covered (Step 9) |
Anti-circularityPass
House edge auditPass
RTP claim (RTP-CLAIM 1.2.0)Pass
Simulated RTP (Pass 1)Pass
Cherry-pick probe (Pass 2)Pass
Bet-size invariancePass
WTF mode RTPPass
Config completenessPass
The RTP is proven mathematically from binomial probabilities — P(slot) × multiplier = 98.9063%–99.1602% per board, using independently computed probabilities and the pinned tables: an exact identity, not a statistical estimate. LIQD’s published 99% holds on all 28 boards: none returns more than 0.1 pp below it. 28M simulated rounds are consistent with it, and the cherry-pick probe finds no sign of seed selection.
How It Works — RTP & Payout Logic9 sections
4.1Anti-Circularity Proof (Step 13)
The anti-circularity proof establishes the RTP by applying independently-computed binomial probabilities to the hash-pinned payout tables, using no operator-supplied RTP figure. This is what separates a mathematical proof from a statistical estimate. The proof has two components — each derived independently:
| Component | Formula | Source |
|---|---|---|
| Slot probability | P(slot=k) = C(rows, k) × 0.5^rows | Binomial distribution — pure math, not from casino |
| Multiplier | PLINKO_FIXED_ODDS[rows][riskLevel][k] / PLINKO_WTF_ODDS[k] | Transcribed from LIQD’s client bundle — hash-pinned in plinkoConfig.json |
| RTP computation | Σ P(k) × multiplier(k) for k = 0 to rows | Independent probability × pinned multiplier |
| Result | 98.9063%–99.1602% per configuration (mean 99.0152%) | Exact identity from independent probabilities × pinned tables |
| Rows | Risk | P(slot=0) | Multiplier(slot=0) | RTP |
|---|---|---|---|---|
| 8 | 1 (LOW) | 0.39063% | 5.6× | 98.9844% |
| 8 | 3 (HIGH) | 0.39063% | 29× | 99.0625% |
| 11 | 3 (HIGH) | 0.04883% | 120× | 99.1602% |
| 16 | 3 (HIGH) | 0.00153% | 1000× | 98.9764% |
| 13 (WTF) | 4 (WTF) | 0.01221% | 1000× | 98.9990% |
Anti-Circularity Verification:
export function run(_ctx: VerifyContext): StepResult[] { // Pascal's triangle in integers, no division, no floats — the independent reference. let rowInts: number[] = [1]; let maxErr = 0; let sumDev = 0; let worstRows = 8; for (let n = 1; n <= 16; n++) { const next = new Array(n + 1).fill(0); for (let k = 0; k <= n; k++) next[k] = (rowInts[k - 1] ?? 0) + (rowInts[k] ?? 0); rowInts = next; if (n < 8) continue; let sum = 0; for (let k = 0; k <= n; k++) { const p = binomP(n, k); sum += p; // binomP(n,k)·2^n is exactly C(n,k) in float64 for n ≤ 16 (dyadic). const err = Math.abs(p * Math.pow(2, n) - rowInts[k]); if (err > maxErr) { maxErr = err; worstRows = n; } } sumDev = Math.max(sumDev, Math.abs(sum - 1)); } // … const SUM_TOL = 1e-12; const s13 = step(13, 'Anti-Circularity (Binomial Slot Probabilities)', maxErr === 0 && sumDev < SUM_TOL ? 'PASS' : 'FAIL',Independent probability + RTP derivation:
export function binomP(rows: number, k: number): number { let c = 1; for (let i = 0; i < k; i++) c = (c * (rows - i)) / (i + 1); return c * Math.pow(0.5, rows);} /** Theoretical RTP = Σ_k P(k)·multiplier(k), using independent binomial P(k). */export function theoreticalRTP(cfg: PlinkoConfigData, rows: number, riskLevel: number): number { const t = payoutTable(cfg, rows, riskLevel); let rtp = 0; for (let k = 0; k <= rows; k++) rtp += binomP(rows, k) * t[k]; return rtp;}Why this proof is non-circular: P(slot) comes from the binomial distribution B(rows, 0.5) — a mathematical property of fair coin flips, not from casino data. The multiplier table is the only casino-sourced input, and it is hash-pinned. When we multiply independent probabilities by pinned multipliers and get 98.9063%–99.1602% for every configuration, the RTP is proven — not estimated. The proof uses the tables pinned in plinkoConfig.json: the 80 never-paid cells (including both WTF 1000× tails) come from that file, not from live payouts.
4.2RTP Claim Rule (Step 10)
The RTP claim rule (RTP-CLAIM 1.2.0) is part of the audit method: the RTP a casino publishes must hold at every setting the game offers. A published figure counts wherever a player can find it — on the game page, the fairness page, the help page, the bet panel, the rules, or in the game settings the site loads. Each board’s RTP is computed exactly, as a rational number, from the binomial probabilities and the pinned table — never from the casino’s figure — and must sit within ±0.1 percentage points of the claim. A board below that band on the house side is a miss: the verdict then reads RTP FIGURE OVERSTATED and the suite exits with code 5, even when every draw check passes. A board above the claim favours the player and never fails. The rule is scored on Step 10, the house-edge step.
| Item | Detail |
|---|---|
| Claim | 99% RTP on every board — a flat 1.00% house edge |
| Where LIQD publishes it | The Plinko game page, liqd.com/originals/plinko: header “Edge: 1.00%”, stats panel “99% on every board”, and the page text “Every combination of rows and risk returns 99%” — read logged out on 1 October 2026 (evidence/rtp-page-labels-2026-10-01.json) |
| Settings tested | 28 — rows 8–16 × low, medium and high risk, plus WTF (13 rows) |
| Method | Exact rational: Σ C(rows, k) / 2^rows × the pinned multiplier for slot k |
| Band | ±0.1 percentage points (house side fails, player side never fails) |
| True minimum · maximum | 98.906250% (rows 8 and 10, medium) · 99.160156% (rows 11, high) |
| Worst gap | −0.093750 pp (rows 8 and 10, medium) |
| Boards beyond the band | 0 on the house side · 3 on the player side (rows 9 medium, 11 high, 12 high) |
| Scored on | Step 10 (profile check PLINKO-19), rule enforced |
| Result | Pass — the claim holds on every board |
| Board | Exact RTP | RTP | Gap to 99% | Side of the band | Result |
|---|---|---|---|---|---|
| 8 rows, low | 1267/1280 | 98.984375% | −0.015625 pp | within ±0.1 pp | Pass |
| 8 rows, medium | 633/640 | 98.906250% | −0.093750 pp | within ±0.1 pp | Pass |
| 8 rows, high | 317/320 | 99.062500% | +0.062500 pp | within ±0.1 pp | Pass |
| 9 rows, low | 1267/1280 | 98.984375% | −0.015625 pp | within ±0.1 pp | Pass |
| 9 rows, medium | 1269/1280 | 99.140625% | +0.140625 pp | above — player-favourable | Pass |
| 9 rows, high | 317/320 | 99.062500% | +0.062500 pp | within ±0.1 pp | Pass |
| 10 rows, low | 5069/5120 | 99.003906% | +0.003906 pp | within ±0.1 pp | Pass |
| 10 rows, medium | 633/640 | 98.906250% | −0.093750 pp | within ±0.1 pp | Pass |
| 10 rows, high | 317/320 | 99.062500% | +0.062500 pp | within ±0.1 pp | Pass |
| 11 rows, low | 5069/5120 | 99.003906% | +0.003906 pp | within ±0.1 pp | Pass |
| 11 rows, medium | 507/512 | 99.023438% | +0.023438 pp | within ±0.1 pp | Pass |
| 11 rows, high | 5077/5120 | 99.160156% | +0.160156 pp | above — player-favourable | Pass |
| 12 rows, low | 20271/20480 | 98.979492% | −0.020508 pp | within ±0.1 pp | Pass |
| 12 rows, medium | 20273/20480 | 98.989258% | −0.010742 pp | within ±0.1 pp | Pass |
| 12 rows, high | 20299/20480 | 99.116211% | +0.116211 pp | above — player-favourable | Pass |
| 13 rows, low | 4055/4096 | 98.999023% | −0.000977 pp | within ±0.1 pp | Pass |
| 13 rows, medium | 10137/10240 | 98.994141% | −0.005859 pp | within ±0.1 pp | Pass |
| 13 rows, high | 20293/20480 | 99.086914% | +0.086914 pp | within ±0.1 pp | Pass |
| 14 rows, low | 81101/81920 | 99.000244% | +0.000244 pp | within ±0.1 pp | Pass |
| 14 rows, medium | 10137/10240 | 98.994141% | −0.005859 pp | within ±0.1 pp | Pass |
| 14 rows, high | 81083/81920 | 98.978271% | −0.021729 pp | within ±0.1 pp | Pass |
| 15 rows, low | 162203/163840 | 99.000854% | +0.000854 pp | within ±0.1 pp | Pass |
| 15 rows, medium | 162199/163840 | 98.998413% | −0.001587 pp | within ±0.1 pp | Pass |
| 15 rows, high | 32449/32768 | 99.026489% | +0.026489 pp | within ±0.1 pp | Pass |
| 16 rows, low | 324399/327680 | 98.998718% | −0.001282 pp | within ±0.1 pp | Pass |
| 16 rows, medium | 64873/65536 | 98.988342% | −0.011658 pp | within ±0.1 pp | Pass |
| 16 rows, high | 162163/163840 | 98.976440% | −0.023560 pp | within ±0.1 pp | Pass |
| 13 rows, WTF | 4055/4096 | 98.999023% | −0.000977 pp | within ±0.1 pp | Pass |
| All 28 boards | — | 98.906250% – 99.160156% | worst −0.093750 pp | 0 below the band · 3 above | Pass — 28 / 28 |
export const RTP_CLAIM: RtpClaim = { kind: 'flat', rtp: '0.99', // …}; /** The scored step that carries PLINKO-19 (src/profile-map.ts). */export const RTP_CLAIM_STEP = 10;// …export function computeRtpClaim(): RtpClaimResult { const cfg = loadPlinkoConfig(); const acc = new RtpClaimAccumulator(RTP_CLAIM); for (const { rows, riskLevel } of allConfigs(cfg)) { const table = payoutTable(cfg, rows, riskLevel); if (table.length !== rows + 1) throw new Error(`${rows}/${riskLevel}: ${table.length} multipliers`); let num = 0n, den = 1n; table.forEach((m, k) => { const r = parseRational(String(m)); // the literal's exact decimal (shortest round-trip form) num = num * r.d + binom(rows, k) * r.n * den; den = den * r.d; }); acc.add(`rows=${rows} risk=${riskName(cfg, riskLevel)}`, num, den * (1n << BigInt(rows))); } return acc.result();}outputs/rtp-claim-table.json, pinned by SHA-256 and re-derived on every run.4.3House Edge / RTP Audit (Step 10)
LIQD advertises houseEdge: 1 in every bet response’s currentGameSettings — a flat 1.00%. The audit never uses that field as evidence: Step 10 derives every board’s RTP analytically from the pinned tables. The published tables use whole-number or one-decimal multipliers (whole numbers only for WTF), so the exact edge varies slightly per board — 0.84% to 1.09%; the largest deviation from 99% is 0.1602 pp (rows 11, high).
The model behind those figures is anchored inside the step, not just band-checked. Every board’s binomial RTP is recomputed a second way — by exhaustive enumeration of all 2^rows equally likely bit-paths (popcount → slot, payouts averaged), pure counting in which no binomial coefficient, power, or probability is ever formed, so a coefficient or normalisation error in the closed form cannot be expressed. Disagreement above 1e-12 hard-FAILs the step; the committed run agrees to 8.76e-13. The published min / max / mean / WTF figures are additionally pinned at 1e-12 by tests/plinko/rtpPinTests.ts.
The edge is the same at both captured stakes — the derivation ignores the wager (Step 18) and the tables carry no bet-size dimension — and it holds in every mode: WTF’s extreme table yields the same ~1% edge, with the payout mass concentrated in 4 of 14 slots. The client bundle’s runtime scaling term (1 − houseEdge/100)/0.99 collapses to exactly 1.000 at the 1.00% edge, so the pinned table value is the paid multiplier.
/** * Exhaustive-enumeration RTP: iterate every one of the 2^rows equally likely * bit-paths, take its popcount as the slot, and average the payouts. Shares NO * arithmetic with `binomP`/`theoreticalRTP` — no binomial coefficient, no power, * no probability is ever formed, so a coefficient or normalisation error in the * closed form is inexpressible here. rows ≤ 16 ⇒ at most 65,536 paths per config. */function enumeratedRTP(cfg: PlinkoConfigData, rows: number, riskLevel: number): number { const table = payoutTable(cfg, rows, riskLevel); const total = 1 << rows; let sum = 0; for (let p = 0; p < total; p++) { let bits = p, slot = 0; while (bits) { slot += bits & 1; bits >>>= 1; } sum += table[slot]; } return sum / total;}// … // ── Step 10: House edge / RTP audit (anti-circularity + enumeration anchor) ─── // … const configRTP = allConfigs(cfg).map(c => ({ label: isWtf(c.riskLevel) ? 'WTF' : `rows ${c.rows} risk ${c.riskLevel}`, rtp: theoreticalRTP(cfg, c.rows, c.riskLevel), enum: enumeratedRTP(cfg, c.rows, c.riskLevel), })); const ENUM_TOL = 1e-12; const enumWorst = Math.max(...configRTP.map(r => Math.abs(r.rtp - r.enum))); const enumOk = enumWorst < ENUM_TOL; const minRTP = Math.min(...configRTP.map(r => r.rtp)); const maxRTP = Math.max(...configRTP.map(r => r.rtp)); const EPS = 1e-12; const argmin = configRTP.filter(r => Math.abs(r.rtp - minRTP) < EPS).map(r => r.label); const argmax = configRTP.filter(r => Math.abs(r.rtp - maxRTP) < EPS).map(r => r.label); const worstDev = Math.max(Math.abs(minRTP - 0.99), Math.abs(maxRTP - 0.99)); const worstIsMax = Math.abs(maxRTP - 0.99) >= Math.abs(minRTP - 0.99); const worstConfigs = worstIsMax ? argmax : argmin; const s10 = step(10, 'House Edge / RTP Audit (analytical, per-config)', !enumOk ? 'FAIL' : worstDev < 0.01 ? 'PASS' : 'FLAG',4.4Simulation Pass 1 — Fresh Seeds (Step 16)
Section 4.1 proves the RTP mathematically. Does our implementation of the same algorithm behave that way at scale? Pass 1 simulated 28 million rounds — 1,000,000 per board under auditor-generated crypto.randomBytes seed pairs, each stored in the committed run. Every npm test replays all 28M rounds from those seeds inside Step 16, and the replayed figures match the committed ones bit for bit. The simulated payouts are consistent with the theoretical RTP, and the slot counts pass family-wise-corrected uniformity testing.
Chi-squared test: Compares observed slot frequencies to expected binomial frequencies (df = rows). Scored result: 0/28 fail at Bonferroni α/28 = 3.5714×10⁻⁴; uncorrected single-test flags all clear the correction.
Serial independence: Lag-1 autocorrelation and the Wald-Wolfowitz runs test on the binarised slot sequence (each drop marked left or right of centre) — on that sequence the two are one statistic. Scored result: 0/28 fail at the corrected threshold.
RTP convergence: Committed run: mean simulated 98.9249% vs 99.0152% theoretical — 1.0 standard error below on the pooled ±0.09 pp SE; the 27 standard boards average 98.9318% vs 99.0158%, and the largest single-board gap is 16r/HIGH at −0.90 pp on a ±0.66 pp SE — within sampling variance at 1M rounds per board. The chart on this page plots the committed run’s mean RTP across all 28 boards at seven checkpoints, with a ±2 SE band.
| Board | Theoretical RTP | Simulated RTP (committed run) | Deviation |
|---|---|---|---|
| 8r · risk 1 | 98.9844% | 98.9275% | −0.0569 pp |
| 8r · risk 2 | 98.9063% | 98.6168% | −0.2895 pp |
| 8r · risk 3 | 99.0625% | 99.0433% | −0.0192 pp |
| 9r · risk 1 | 98.9844% | 98.9902% | +0.0058 pp |
| 9r · risk 2 | 99.1406% | 99.0052% | −0.1354 pp |
| 9r · risk 3 | 99.0625% | 99.0206% | −0.0419 pp |
| 10r · risk 1 | 99.0039% | 99.0805% | +0.0766 pp |
| 10r · risk 2 | 98.9063% | 98.8956% | −0.0107 pp |
| 10r · risk 3 | 99.0625% | 99.0225% | −0.0400 pp |
| 11r · risk 1 | 99.0039% | 99.0030% | −0.0009 pp |
| 11r · risk 2 | 99.0234% | 98.9260% | −0.0974 pp |
| 11r · risk 3 | 99.1602% | 99.1425% | −0.0176 pp |
| 12r · risk 1 | 98.9795% | 98.9766% | −0.0029 pp |
| 12r · risk 2 | 98.9893% | 99.0740% | +0.0848 pp |
| 12r · risk 3 | 99.1162% | 99.0813% | −0.0349 pp |
| 13r · risk 1 | 98.9990% | 99.0480% | +0.0489 pp |
| 13r · risk 2 | 98.9941% | 99.1283% | +0.1342 pp |
| 13r · risk 3 | 99.0869% | 99.2117% | +0.1248 pp |
| 14r · risk 1 | 99.0002% | 99.0046% | +0.0044 pp |
| 14r · risk 2 | 98.9941% | 98.7936% | −0.2006 pp |
| 14r · risk 3 | 98.9783% | 98.4904% | −0.4878 pp |
| 15r · risk 1 | 99.0009% | 99.0671% | +0.0663 pp |
| 15r · risk 2 | 98.9984% | 98.8487% | −0.1498 pp |
| 15r · risk 3 | 99.0265% | 98.7490% | −0.2775 pp |
| 16r · risk 1 | 98.9987% | 98.9785% | −0.0202 pp |
| 16r · risk 2 | 98.9883% | 98.9544% | −0.0340 pp |
| 16r · risk 3 | 98.9764% | 98.0790% | −0.8974 pp |
| WTF (13r · risk 4) | 98.9990% | 98.7375% | −0.2615 pp |
// ── Step 16: Pass 1 (fresh seeds, FWER) ────────────────────────────────────── const pass1 = sim.pass1_fresh_seeds ?? {}; // … const expectedConfigs = allConfigs(cfg); const configs = expectedConfigs.length; const bonAlpha = 0.01 / configs; const bonZCrit = inverseCriticalZ(bonAlpha); // … // The gates read the REPLAYED statistics wherever the replay produced them; the artifact's // copies are used only on a local prefix-replay run (PF_P1_REPLAY=off, capped at FLAG). const gateRows: Record<string, number>[] = results1.map(r => { const d = derivedByKey.get(key(r.rows as number, (r.riskLevel ?? NaN) as number)); return d ? { ...r, slotChi2: d.slotChi2, slotDf: d.slotDf, slotPValue: d.slotPValue, r1Z: d.r1Z, runsPValue: d.runsPValue, simRTP: d.simRTP } : r; }); const chi2BonFails = gateRows.filter(r => r.slotPValue < bonAlpha); const serialUncorrected = gateRows.filter(r => Math.abs(r.r1Z ?? 0) > 1.96 || (r.runsPValue !== undefined && r.runsPValue < 0.01) ); const serialBonFails = gateRows.filter(r => Math.abs(r.r1Z ?? 0) > bonZCrit || (r.runsPValue !== undefined && r.runsPValue < bonAlpha) ); const statOk = chi2BonFails.length === 0 && serialBonFails.length === 0; // … const SIM_RTP_K = 6; // … // Against the RECOMPUTED mean — never against the artifact's own theory field. const dev = Math.abs(r.simRTP - mu); if (!(dev < SIM_RTP_K * se)) rtpBad.push(`${label} ${(dev / se).toFixed(1)}σ`); // … const s16Status: 'PASS' | 'FLAG' | 'FAIL' = (!resultsLenOk || !roundsOk || !statOk || !finiteOk || !rtpConvergeOk || !theoryOk || !aggOk || !s16DomainOk || !dispersionOk || !replayOk || !traceOk) ? 'FAIL' : (scaleReduced || p1ReplayOff) ? 'FLAG' : 'PASS';What is a Monte Carlo simulation? Instead of relying on algebra alone, we simulate millions of rounds. Each round computes a path from scratch using the same HMAC-SHA256 derivation as the live game. We record payouts, then compare the aggregate return to the theoretical figure. Because npm run simulate draws fresh seeds, the simulated RTP of a new run is not a pinned number — the scored result is the corrected-failure count, which must be 0/28 on every run. The committed run itself is pinned: every npm test replays it from its saved seeds and must reproduce all 784 scored figures, and npm run replay re-checks 812 numeric fields, including the 560 convergence points.
4.5Cherry-Pick Probe — Pass 2 (Step 17)
Commit-reveal without an external beacon cannot structurally prevent an operator from pre-evaluating candidate server seeds and committing to an unfavourable one. Pass 2 is the simulation-level probe: each of the 202 captured casino seeds is extended to 10,000 nonces on one board — the board of that epoch’s lowest-nonce bet — and an early window is compared with the late one that follows it. The scored window is the 50 nonces actually played under each seed (early 0–49, late 50–99); a 5,000-nonce window, with far more power against a seed biased across its whole stream, is reported beside it. The payout-weighted statistic scores each window’s RTP as a z-value against its exact per-board mean and SD; the count of seeds below z = −1.645 is scored against a calibrated null (exact lattice convolution of the window payout distribution, combined across epochs as a Poisson-binomial), together with the paired early − late t.
Flag criterion: Payout-weighted (scored): a seed’s early-window RTP sits below z = −1.645 of its exact per-board mean — the signature of a commitment chosen for a short session; the expected count comes from the calibrated null, not 0.05 × 202. Step 17 passes when the calibrated survival P ≥ 0.01 and |t| ≤ 2.58. Slot test (reported, nominal): the early half’s slot distribution deviates from binomial (p < 0.05) while the late half’s does not; its 0.0475 per-seed rate is the nominal rate at a 5% test size.
Coverage: At the 50-nonce window the count cannot trigger on eight high-variance boards — rows 10 to 16 at high risk, and WTF — because 50 drops cannot produce a window RTP below z = −1.645 there. Those boards carry 115 of the 202 epochs, so the paired early − late t carries the result. At the 5,000-nonce window the count covers every board.
Execution attestation: Pass 2 is deterministic given the committed casino seeds, so Step 17 re-executes all 202 epochs at verify time — chi-squared p-values, payout-weighted z-scores, window RTPs and flags — re-derives 14 summary and header values, and recomputes the calibrated null from the pinned tables against outputs/calibration-results.json. The per-epoch values and the calibrated null must match within 1e-9, the 14 summary values within 1e-6, and the edge-hit counts exactly.
Player mitigation: Set your own client seed after seeing the server-seed commitment — that removes the operator’s foreknowledge channel.
| Test | Result |
|---|---|
| Casino seeds tested | 202 (every revealed seed in the capture) |
| Nonces per seed | 10,000, on one board per seed |
| Scored window (50 nonces): seeds below z = −1.645 | 1, against a calibrated 1.4099 ± 1.1721 |
| Poisson-binomial survival P | 0.7603 — not significant |
| Scored window: early − late paired t | −0.19 |
| Extended window (5,000 nonces): seeds below z = −1.645 | 6, against a calibrated 5.8556 ± 2.3708 (P = 0.5337) |
| Extended window: early − late paired t | 0.60 (two-sided p = 0.5518) |
| Slot-test flags, extended window (nominal 5% size) | 11 vs 9.595 expected (P = 0.3652) |
| Edge hits, early / late (extended window) | 1,063 / 1,020 |
// ── PRIMARY vs EXTENDED window ─────────────────────────────────────────────── // The scored window is EXPECTED_EPOCH_SIZE nonces — what was played under each server seed // in this capture (the capture rotated the seed pair every 50 bets; nothing in the captured // records shows LIQD limiting a seed pair to 50). Those are the drops a cherry-picked seed // paid in this capture; a statistic over 5,000 nonces measures a session nobody here played, // and its power is spent on a population that was never observed. // … const primaryWindow = EXPECTED_EPOCH_SIZE; const seedPairs = revealedSeedsByEpoch(seeds); const primaryStats = primaryWindow > 0 && calEpochs.length > 0 ? windowStats(cfg, calEpochs, seedPairs, primaryWindow, Z_LEFT_TAIL) : []; const primaryCount = countBelowThreshold(primaryStats); // … const primaryT = primarySeDiff > 0 ? primaryMeanDiff / primarySeDiff : NaN; const calibration = primaryWindow > 0 && calEpochs.length > 0 ? calibrateLeftTailCount(cfg, calEpochs, primaryWindow, Z_LEFT_TAIL) : null; // … const calSurvival = calibration ? calibratedSurvival(calibration, primaryCount) : NaN; // … const pwOk = pwPresent && calOk && Number.isFinite(calSurvival) && calSurvival >= 0.01 && Number.isFinite(primaryT) && Math.abs(primaryT) <= 2.58;4.6Bet-Size Invariance (Step 18)
Phase C placed 200 bets at $10.00 on 16r/HIGH — the same configuration as Phase B's $0.10 bets. All 200 paths recompute from revealed seeds through the identical HMAC path, and all 200 multipliers match the same pinned table. The invariance is structural: revealPlinko(serverSeed, clientSeed, nonce, rows) takes no wager argument, so bet amount cannot reach the derivation — it enters only the final winningAmount = betAmount × multiplier.
| Metric | Phase B ($0.10) | Phase C ($10.00) |
|---|---|---|
| Bets | 2,000 | 200 |
| Config | 16r / risk 3 | 16r / risk 3 |
| Paths recomputed | 2,000/2,000 | 200/200 |
| Multiplier table | PLINKO_FIXED_ODDS[16][3] | PLINKO_FIXED_ODDS[16][3] |
| RNG path | HMAC-SHA256 · cursor = row + 1 | HMAC-SHA256 · cursor = row + 1 |
4.7WTF Mode — The Fourth Risk Level (Step 15)
WTF is a first-class fourth risk level (riskLevel = 4), not a cosmetic skin: the server forces the board to 13 rows and swaps in a dedicated 14-slot table, PLINKO_WTF_ODDS = [1000, 235, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 235, 1000]. Only 4 of the 14 slots pay — the outermost at 1000×, the next-in at 235× — and the middle 10 return nothing. The bounce derivation is byte-identical to the standard game; WTF changes only the row count and the payout table. Its anti-circular RTP under the same binomial weighting is 98.9990% — the same ~1% edge as the standard game, expressed as extreme variance.
All 2,000 Phase E drops are 13-row / risk-4 and recompute 2,000/2,000, with 2,000/2,000 multipliers matching PLINKO_WTF_ODDS[winningSlot]. Six drops paid in-sample (all 235×: four at slot 12, two at slot 1); the 1000× tails need an all-0 or all-1 13-row path (P = 0.5¹³ ≈ 1 in 8,192, ≈ 0.24 expected per tail across the phase) and did not occur — their value comes from the hash-pinned table, exercised by the simulation, which includes WTF as its 28th configuration.
How it differs: Rows forced to 13 (14 slots) · dedicated integer table · ~99.7% of drops return 0 · same commit-reveal, same HMAC, same nonce stream.
What it does not change: The house edge (98.9990% RTP ≈ the standard band), the drop physics (binomial B(13, 0.5)), or any seed mechanics.
| Slot | 0 | 1 | 2–11 | 12 | 13 |
|---|---|---|---|---|---|
| Multiplier | 1000× | 235× | 0× | 235× | 1000× |
| P(slot) | 0.0122% | 0.1587% | 99.6582% (combined) | 0.1587% | 0.0122% |
const { phaseE, seedMap, cfg } = ctx; if (phaseE.length === 0) { return [step(15, 'Phase E — WTF Mode Verification', 'FAIL', 'No Phase E (WTF) bets found in dataset')]; } const wtfOdds = cfg.wtf_mode.odds; const wtfRTP = theoreticalRTP(cfg, cfg.wtf_mode.rows, cfg.wtf_mode.riskLevel); // Structural risk profile of the WTF table, from the independent binomial: // probability of landing a zero-paying slot vs any paying slot. let pPaying = 0; for (let k = 0; k <= cfg.wtf_mode.rows; k++) { if (wtfOdds[k] > 0) pPaying += binomP(cfg.wtf_mode.rows, k); } const pZero = 1 - pPaying; const failures: string[] = []; let recomputeChecked = 0, recomputeFails = 0; let multChecked = 0, multFails = 0; for (const b of phaseE) { if (b.riskLevel !== 4) failures.push(`bet ${b.id}: riskLevel ${b.riskLevel} (expected 4)`); if (b.numberOfRows !== 13) failures.push(`bet ${b.id}: numberOfRows ${b.numberOfRows} (expected 13)`); const ss = seedMap.get(b.hashedServerSeed); if (ss) { const { path, winningSlot } = revealPlinko(ss, b.clientSeed, b.nonce, b.numberOfRows); recomputeChecked++; if (path !== b.dropDetails || winningSlot !== b.winningSlot) { recomputeFails++; if (failures.length < 5) failures.push(`bet ${b.id}: dropDetails/slot recompute mismatch`);PLINKO_WTF_ODDS. WTF theoretical RTP 98.9990%. Observed slot span [1, 12]; both 1000× tails unhit in-sample (expected ≈ 0.24 each); their value comes from the hash-pinned table.4.8Informational Items (Not Scored)
These items are reported for transparency but are not scored audit steps. The live capture is deliberately variance-heavy (deep high-risk sampling, $10 stakes, 2,000 WTF drops), so its raw RTP is a variance illustration — never RTP evidence. The authoritative figures are the analytical per-config RTP and the 28M-round simulation.
| Item | Value | Context |
|---|---|---|
| Live RTP (all 10,100 drops) | 80.6843% | Dominated by Phase C/E variance and unrealised 1000× tails — informational only |
| Phase A live RTP | 96.59% | 5,400 bets at $0.10 across all 27 standard configs |
| Phase B live RTP | 115.59% | 2,000 bets on 16r/HIGH — two 130× hits landed |
| Phase C live RTP | 73.20% | 200 bets at $10.00 on 16r/HIGH — expected spread at N=200 |
| Phase D live RTP | 109.38% | 500 bets under custom seeds — incl. a 120× edge hit |
| Phase E live RTP | 70.50% | 2,000 WTF bets — six 235× wins, no 1000× tail |
| Payout-table cell coverage | 285/365 (78.1%) | Property of capture design, not an integrity check — the 80 unexercised cells (incl. both WTF 1000× tails) come from the hash-pinned config and carry 7.41% of the summed theoretical return; 3 configs (8r/LOW, 8r/HIGH, 9r/MEDIUM) were fully exercised |
| Pooled lag-1 autocorrelation | r₁ = 0.0899 | Between-config composition artifact; within-config autocorrelation consistent with independence — authoritative evidence is Pass 1 |
4.9Worked Example — Payout Verification
Real drop from Phase E — bet 2tGmOv7FZHfyCwhX7yCS3, epoch 175, nonce 32, WTF mode (13 rows forced, risk 4). Verified from plinko-master-10100bets.json:
serverSeed = 0d249f81e03a903d6b17c71ebc82817a
clientSeed = auditc680c9435be0
nonce = 32
rows = 13 (forced by WTF · riskLevel 4)Step 1 — commitment: SHA-256(utf8("0d249f81e03a903d6b17c71ebc82817a")) = 1e74e08a9dd50f50ef689e15b52a37ca… — equals the bet's hashedServerSeed ✅
Step 2 — path: 13 HMAC draws (cursor 1…13) → dropDetails = 1111101111111 → winningSlot = 12 (twelve 1 bits)
Step 3 — multiplier: PLINKO_WTF_ODDS[12] = 235
Step 4 — payout: 0.10000000 × 235 = 23.50000000
| Step | Process | Output |
|---|---|---|
| 1 | Commitment check on the revealed seed | matches hashedServerSeed ✅ |
| 2 | HMAC-SHA256 × 13 rows | path 1111101111111, slot 12 ✅ |
| 3 | Multiplier: WTF slot 12 | 235× |
| 4 | RTP check: Σ P(k) × PLINKO_WTF_ODDS[k] | 98.9990% |
| 5 | Payout: 0.10000000 × 235 | 23.50000000 ✅ |
// WTF — anti-circularity proof// P(slot=k) = C(13,k) × 0.5^13 for k = 0..13//// Σ binomP(13, k) × PLINKO_WTF_ODDS[k]// = 2 × (1/8192 × 1000) + 2 × (13/8192 × 235)// = 8110/8192 = 0.989990 → 98.9990%//// A mathematical identity, not a measurement.// The table is our hash-pinned transcription; the probabilities are independent.2tGmOv7FZHfyCwhX7yCS3 — path, slot, multiplier, and payout all match. A 235× WTF win reproduced bit-for-bit; WTF RTP 98.9990% proven from independent binomial probabilities.Technical Evidence & Verification5 sections
4.10Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Anti-circularity (Step 13) | binomP·2^n = exact C(n,k) (Pascal recurrence) for rows 8–16, masses sum to 1 | Pass |
| House edge / RTP audit (Step 10) | 28 boards — 98.9063%–99.1602%; binomial vs exhaustive path enumeration agree to 8.76e-13 | Pass |
| RTP claim rule (Step 10) | 28 / 28 boards within the ±0.1 pp band or above it — worst −0.09375 pp | Pass |
| Simulation Pass 1 (Step 16) | 28M rounds — 0/28 chi-squared, 0/28 serial (corrected) | Pass |
| Simulation Pass 2 (Step 17) | 202 seeds × 10K nonces — scored window: 1 below z = −1.645 vs 1.41 calibrated (P = 0.7603), t = −0.19; extended: 6 vs 5.86 (P = 0.5337); re-executed for 202/202 epochs | Pass |
| Epoch-window slot fit (Step 21) | 10,100 live drops pooled by board size (9 groups) vs binomial — Fisher combined p = 0.3615 (X² = 19.50, 18 df), smallest group p = 0.081 (15 rows) vs Bonferroni 1.11×10⁻³; p-values nominal | Pass |
| Bet-size invariance (Step 18) | 200/200 Phase C at $10.00 | Pass |
| WTF mode (Step 15) | 2,000/2,000 recomputed + table-conformant | Pass |
4.11Code References11 source files
| File | Purpose |
|---|---|
| tests/steps/anti-circularity.ts | Step 13: binomial probability independence |
| tests/steps/payouts.ts | Steps 7–10: multiplier lookup, payout math, RTP audit |
| tests/steps/simulation.ts | Steps 16–17: simulation integrity and the cherry-pick test (calibrated payout-weighted count) |
| src/rtp-claim.ts · src/rtp-settings.ts | The RTP claim rule (RTP-CLAIM 1.2.0, enforced) and LIQD’s published claim with the per-board table |
| tests/steps/wtf.ts | Step 15: WTF-mode verification |
| tests/steps/standardization.ts | Step 18: bet-size invariance |
| tests/steps/live-fit.ts | Step 21: epoch-window slot fit of the live drops vs binomial (Fisher-combined) |
| src/simulate.ts | Two-pass Monte Carlo (28M rounds, fresh seeds); src/pass1.ts holds the Pass 1 loop that Step 16 replays |
| src/calibration.ts · src/calibrate.ts | Calibrated null for the payout-weighted Pass 2 count (exact lattice convolution + Poisson-binomial) → outputs/calibration-results.json |
| src/stats.ts | combination, chiSquaredTest (regularized-gamma p-value), lag1Autocorrelation, runsTest, inverseCriticalZ, normalCDF, binomialSurvival |
| src/config.ts | binomP · theoreticalRTP · pinned-table loader |
4.12Datasets UsedSource artifacts
4.13Verified Invariants14 invariants
| Invariant | Result |
|---|---|
| Σ P(slot) × multiplier ∈ [98.9063%, 99.1602%] for all 28 configs (non-circular) | Pass |
| Binomial masses sum to 1.0 exactly for every row count 8–16 | Pass |
| Exhaustive 2^rows path enumeration matches the binomial RTP within 1e-12 for all 28 configs (Step 10 anchor) | Pass |
| LIQD’s published 99% holds on all 28 boards — none more than 0.1 pp below it (RTP-CLAIM 1.2.0) | Pass |
| Every per-config edge within 1 pp of the advertised 1.00% | Pass |
| WTF RTP 98.9990% under PLINKO_WTF_ODDS | Pass |
| 0/28 configs reject at Bonferroni α/28 (chi-squared) | Pass |
| 0/28 serial-independence failures at the corrected threshold | Pass |
| Live slot distribution by board size consistent with binomial — Fisher combined p = 0.3615, no group below the Bonferroni alpha (Step 21) | Pass |
| No early-window selection signature across 202 casino seeds at the 50-nonce window played (Pass 2) | Pass |
| Step 17 re-derivation matches the committed Pass 2 rows within 1e-9 | Pass |
| Calibrated payout-weighted null recomputed from the pinned tables matches outputs/calibration-results.json (Step 17) | Pass |
| Bet amount absent from the RNG input by construction | Pass |
| Phase C ($10.00) derivations identical to $0.10 derivations | Pass |
4.14Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinko && git checkout v1.0.1npm cinpm run verify # Steps 9, 10, 13, 15–18, 21 and the RTP claim rule cover S4cat outputs/rtp-claim-table.jsonS4-related steps:
[PASS] Step 9 — Config Completeness (27 standard + WTF)
[PASS] Step 10 — House Edge / RTP Audit (analytical, per-config)
[PASS] Step 13 — Anti-Circularity (Binomial Slot Probabilities)
[PASS] Step 15 — Phase E — WTF Mode Verification
[PASS] Step 16 — Simulation — Pass 1 Slot Uniformity (FWER)
[PASS] Step 17 — Simulation — Pass 2 Cherry-Pick Test
[PASS] Step 18 — Bet-Size Invariance
[PASS] Step 21 — Epoch-Window Slot Fit (live drops vs binomial)
rtp-claim: ENFORCED — PASS
rtp-claim: claim flat 99.000000% — source: …
rtp-claim: band ±0.10 pp · 28 settings · true min 98.906250% (rows=8 risk=medium) · true max 99.160156% (rows=11 risk=high)
rtp-claim: house side beyond band: 0 · player-favourable beyond band: 3 · worst rows=8 risk=medium at -0.093750 pp
rtp-claim: outcome PASS · minimal fix: none — the claim holds for every setting it states a figure for
Step 21 can flag a deviation but cannot fail the verdict. A fresh-seeded npm run simulate gives a different simulated RTP and can fail Step 16 by chance about 1 time in 50, the gate’s designed false-alarm rate; the Step 17 re-execution and the RTP claim table reproduce exactly, and npm test replays the committed run from its saved seeds.Fairness Integrity Testing
Does the implementation maintain fairness under non-standard conditions?
Sections 1–4 establish that the game is mathematically fair; Section 5 tests whether the live implementation keeps its integrity under non-standard conditions. It covers 29 integrity tests run against LIQD’s running service across 11 categories: race conditions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, and the provably fair surface itself. 27 passed. Two were remediated: wrong-type row-count, risk and stake values, and sub-precision stakes, were converted into valid bets instead of being refused (Low, input hygiene). Every range held throughout, and LIQD fixed both; we re-verified them. No test altered, replayed or injected an outcome.
What we verified
- Race conditions — can parallel bets, or a seed rotation racing a live drop, over-commit funds or change which seed a bet settles against?
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed handling — can unusual client seeds or server seed fields be overridden?
- Outcome replay — can a completed bet be replayed for a duplicate payout?
- Cross-player isolation — can one player act on another’s account or session?
- Payout tampering — can multiplier, slot, or payout values be injected?
- Parameter limits — can invalid row counts, risk levels, stakes or currencies be submitted?
What this means for you
- Across the 29 tests, no request path let anyone alter, replay or inject an outcome
- Once a bet is placed, its path and slot cannot be changed or replayed
- Each bet gets its own server-assigned nonce under a committed seed
- Your bets are bound to your session — no other account can act on them
- The one gap found — wrong-type and sub-precision values converted instead of refused, inside the valid ranges — was fixed by LIQD and re-verified
- Race conditions — can parallel bets, or a seed rotation racing a live drop, over-commit funds or change which seed a bet settles against?
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed handling — can unusual client seeds or server seed fields be overridden?
- Outcome replay — can a completed bet be replayed for a duplicate payout?
- Cross-player isolation — can one player act on another’s account or session?
- Payout tampering — can multiplier, slot, or payout values be injected?
- Parameter limits — can invalid row counts, risk levels, stakes or currencies be submitted?
- Across the 29 tests, no request path let anyone alter, replay or inject an outcome
- Once a bet is placed, its path and slot cannot be changed or replayed
- Each bet gets its own server-assigned nonce under a committed seed
- Your bets are bound to your session — no other account can act on them
- The one gap found — wrong-type and sub-precision values converted instead of refused, inside the valid ranges — was fixed by LIQD and re-verified
Nonce integrityPass
Seed integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
Parameter limitsRemediated
29 integrity tests: 27 pass and 2 remediated (input hygiene — wrong-type and sub-precision values converted inside the valid ranges, fixed by LIQD and re-verified). No test altered, replayed or injected an outcome. Test procedures and payloads are withheld by policy — see 5.5.
Category Coverage
| Check | Result | Evidence |
|---|---|---|
| Nonce integrity | Pass | Server-assigned and sequential: injected reused, skipped, zero or negative and overflow-sized nonces are all ignored (NONCE-001 … NONCE-004); the nonce advances by one per bet (PF-002) and restarts at 0 on rotation (ENTROPY-004) |
| Seed integrity | Pass | In 70 / 70 rounds raced by a rotation, the revealed seed matched its pre-race snapshot and no revealed seed returned (EXTRA-001); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003) |
| Outcome determinism | Pass | Every live round checked reproduces its path and slot through LIQD’s verifier (EXTRA-002, PF-001, PARAM-003) |
| Round & player isolation | Pass | Bets carrying another account’s identifier stay on the caller’s own account; post-logout reuse rejected; parallel and duplicate bets settle once each, with no over-commit |
| Payout integrity | Pass | Injected nonce, payout, multiplier and slot fields ignored — the server computes every outcome itself; 200 consecutive payouts matched the multiplier formula on the board tested |
| Parameter limits | Remediated | Ranges held on every test; wrong-type row-count, risk and stake values, and sub-precision stakes, were converted into valid bets instead of refused (Low) — fixed by LIQD and re-verified (PARAM-001, PARAM-002) |
Nonce integrityPass
Seed integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
Parameter limitsRemediated
29 integrity tests: 27 pass and 2 remediated (input hygiene — wrong-type and sub-precision values converted inside the valid ranges, fixed by LIQD and re-verified). No test altered, replayed or injected an outcome. Test procedures and payloads are withheld by policy — see 5.5.
How It Works — Fairness Integrity Testing2 sections
5.1Framework Overview
Testing follows the ProvablyFair.org Fairness Integrity Framework — a structured methodology derived from real, historically observed failures in provably fair systems. Cryptographic verification (S1–S3) shows the game computed every captured drop from its disclosed inputs; fairness-integrity testing asks whether the surface around that computation holds up under non-standard use — seeds swapped, rounds replayed, parameters injected, settlements raced. These tests ran against LIQD’s running service, not against a dataset. This section covers 29 tests across 11 categories. Each card below gives the control assessed, the broad testing approach, the observed result, its impact and any limitation.
| Category | Tests | What It Catches |
|---|---|---|
| Race Conditions | 4 | Parallel bets racing placement, settlement or seed rotation |
| Input Validation | 4 | Malformed, wrong-type or out-of-range row counts, risk levels, stakes and currencies |
| Atomic Balance / Idempotency | 3 | Double debits, orphaned debits and unsafe retry behaviour after a timeout or disconnect |
| Broken Authorization | 2 | Acting on another account, or after the session has ended |
| Max-win Bypass | 3 | Client-supplied outcome, payout or slot fields, and stake limits |
| Nonce Manipulation | 4 | Reused, skipped, zero, negative or oversized nonces supplied by the client |
| RNG / PF Verification | 3 | Provably fair fields, the nonce sequence, and paths and slots against LIQD’s verifier |
| Entropy / PF Verification | 1 | Server-seed rotation behaviour: fresh seeds, a new commitment and the nonce reset |
| Precision / Payout | 1 | Settlement arithmetic and wallet reconciliation over many rounds |
| Event Replay | 1 | Duplicate or replayed requests settling more or less than once |
| Adversarial Seeds | 3 | Unusual client seeds: length, character sets, Unicode forms and rapid rotation |
5.2Severity Framework & Hard Fail Criteria
Two scales apply in this section. The audit framework decides whether a result can pass at all — any test that lets money settle against a tampered or unverifiable outcome is an automatic hard fail. The integrity matrix records each test’s status — Pass, Remediated, Warning or Fail — and each finding is graded Critical, High, Medium or Low. On this page: two Remediated tests, both graded Low — wrong-type and sub-precision values converted inside the valid ranges, fixed by LIQD and re-verified.
| Status | Meaning | Action |
|---|---|---|
| Pass | The service handled the scenario correctly: the control held | None — recorded with its result |
| Remediated | A failed control that LIQD has since fixed, re-tested by ProvablyFair.org on the current build | Recorded with the fix and its re-test |
| Warning | A real defect, or a property not yet proven, without a direct effect on funds or fairness | Recorded with its impact; open until a re-test resolves it |
| Fail | A defect affecting funds, fairness or security | Remediation and a re-test required |
| Condition | Consequence |
|---|---|
| A substituted server seed is accepted against a published commitment | Hard fail |
| A revealed seed fails its commitment and play continues | Hard fail |
| Any settled drop's path cannot be reproduced from disclosed inputs | Hard fail |
| A credited payout deviates from the pinned table multiplier | Hard fail |
| Nonce reuse or gaps that could conceal discarded outcomes | Hard fail |
29 tests · 27 pass · 2 remediated
Race Conditions4/4
LIQD-PLINKO-EXTRA-001PassSeed rotation racing live rounds
- Control assessed
- A rotation racing rounds cannot reveal a seed early or inconsistently, or bring a revealed seed back into use.
- Testing approach
- Seventy rounds, each with a rotation fired against it; every revealed seed checked against that round’s pre-race snapshot.
- Observed result
- In all 70 rounds the revealed seed matched its pre-race snapshot, and no seed revealed in one round reappeared as a later active seed; rotation and the hash chain held throughout.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-RACE-001PassParallel bets over-committing funds
- Control assessed
- Parallel bets cannot drive the balance negative, and every one settles consistently.
- Testing approach
- Ten bets sent in parallel whose combined stake exceeds the balance.
- Observed result
- All 10 settled with sequential nonces and distinct bet IDs, and the balance never went negative: settlement is serialised server-side.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-RACE-002PassIdentical bets in parallel, at scale
- Control assessed
- Each accepted bet settles exactly once, and the balance reconciles.
- Testing approach
- Fifteen identical bets sent in parallel; settlements and balance reconciled.
- Observed result
- All 15 settled independently, exactly once, and the aggregate balance change matched the expected value.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-RACE-003PassHigh-stake parallel over-commit
- Control assessed
- Concurrent high-stake bets settle cleanly: no over-commit, no negative balance, no phantom settlement.
- Testing approach
- Seventy attempts, each sending two parallel bets at 75% of the current balance — together more than the balance holds.
- Observed result
- In 50 of 70 attempts both bets were accepted, settling one after the other, the second against the balance the first left; in the other 20, exactly one was correctly refused for insufficient funds. No negative wallet event, no payout mismatch, and no lost or phantom bet in the history.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Input Validation4/4
LIQD-PLINKO-PARAM-001LowRemediatedRow count and risk level: input types
- Control assessed
- Out-of-range values and wrong-type values are both refused, and the board played always matches the multiplier table that pays it.
- Testing approach
- Row counts and risk levels in range, out of range and of the wrong type.
- Observed result
- Ranges were always enforced — every out-of-range row count or risk level was rejected — but a few wrong-type values were converted into valid in-range bets instead of being refused. After LIQD’s fix, wrong-type values are rejected and the ranges remain enforced.
- Impact
- Low · input hygiene: no value outside the ranges was ever accepted, so no board or payout was affected. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-PARAM-002LowRemediatedSub-precision stakes
- Control assessed
- Sub-precision and wrong-type stakes are refused, or handled through the explicit free-play path; no invalid amount is accepted.
- Testing approach
- Stakes below the currency’s precision and of the wrong type, alongside the explicit zero-stake free-play value.
- Observed result
- No negative or out-of-range amount was ever accepted, but a sub-precision stake was recorded as a zero stake and played as a live bet, and wrong-type values were converted into valid bets. After LIQD’s fix, zero is accepted only as explicit free play, and sub-precision and wrong-type stakes are rejected.
- Impact
- Low · a free-play grey area, with no effect on funds. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-PARAM-003PassMaximum rows, recomputed
- Control assessed
- Sixteen rows are accepted and seventeen refused, and the accepted round reproduces from its revealed seed.
- Testing approach
- A 16-row and a 17-row bet; the 16-row round reproduced through LIQD’s verify endpoint.
- Observed result
- Sixteen rows were accepted and seventeen rejected; the accepted round’s path and slot reproduced exactly.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-TOKEN-001PassCurrency identifier manipulation
- Control assessed
- No client-supplied currency identifier is honoured.
- Testing approach
- Bets carrying seven malformed or foreign currency identifiers.
- Observed result
- No injected currency was ever honoured: the account’s own currency was always used. A few malformed identifiers drew a generic server error rather than a clean rejection.
- Impact
- None on funds or fairness; the generic errors are an input-hygiene note.
- Limitations
- None specific to this test; the general scope is in 5.5.
Atomic Balance / Idempotency3/3
LIQD-PLINKO-IDEMP-001PassRetry after a client timeout
- Control assessed
- Retrying a bet after a client timeout does not debit twice.
- Testing approach
- A bet sent with a very short client timeout, then retried normally.
- Observed result
- The first attempt errored client-side and the retry succeeded; the balance moved by exactly one settlement.
- Impact
- None — no double debit.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-IDEMP-002PassOrphaned-debit check at scale
- Control assessed
- Every debit belongs to a settled round.
- Testing approach
- Twenty consecutive bets reconciled against their individual outcomes.
- Observed result
- All 20 reconciled exactly against the sum of their outcomes.
- Impact
- None — no orphaned debit.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-IDEMP-003PassDisconnect while a bet is processed
- Control assessed
- A client disconnect mid-bet leaves no orphaned or lost debit.
- Testing approach
- Bets abandoned by the client at different moments; balance reconciled.
- Observed result
- Only the earliest abandonment errored client-side; every other variant completed normally, and the balance reconciled with no orphaned or lost debit.
- Impact
- None — no orphaned or lost debit.
- Limitations
- None specific to this test; the general scope is in 5.5.
Broken Authorization2/2
LIQD-PLINKO-AUTH-001PassAnother player’s identifier in the request
- Control assessed
- Every bet is attributed to the caller’s own session; another player’s balance is never touched.
- Testing approach
- Bets sent with fake identifiers and with a second account’s real identifier; the second account’s balance compared before and after.
- Observed result
- All 8 bets were attributed to the caller’s own session, and the second account’s balance was identical before and after.
- Impact
- None — account isolation held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-AUTH-003PassSession reuse after logout
- Control assessed
- A session ends at logout: its cookie cannot place another bet.
- Testing approach
- A bet attempted with a session cookie after logout.
- Observed result
- The logged-out session was rejected; no bet was placed and no balance changed.
- Impact
- None — the session was invalidated server-side.
- Limitations
- None specific to this test; the general scope is in 5.5.
Max-win Bypass3/3
LIQD-PLINKO-INJECT-001PassOutcome fields in the bet request
- Control assessed
- Extra outcome fields in a bet request cannot affect the server-computed outcome.
- Testing approach
- Bets carrying four injected outcome fields.
- Observed result
- The four fields were accepted in the request body but never affected the server-computed outcome.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-MAXWIN-001PassStake boundary enforcement
- Control assessed
- Stakes outside the account’s limits are refused or floored.
- Testing approach
- Above-maximum, below-minimum, negative, non-numeric and extremely large stakes, using the account’s real limits.
- Observed result
- Every boundary and invalid case was correctly rejected or floored.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-MAXWIN-002PassOutcome fields injected: nonce, amount, multiplier, slot
- Control assessed
- The server ignores client-supplied outcome values and computes its own.
- Testing approach
- Bets each carrying an injected nonce, payout amount, multiplier or landing slot.
- Observed result
- All four injected fields were ignored; the server computed its own values in every case.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Nonce Manipulation4/4
LIQD-PLINKO-NONCE-001PassReused nonce
- Control assessed
- The client cannot choose the nonce: an injected value is ignored, and the server uses its own.
- Testing approach
- A bet carrying an already-used nonce.
- Observed result
- The replayed nonce was ignored; the server used its own, correctly incremented nonce.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-NONCE-002PassSkipped-ahead nonce
- Control assessed
- An injected nonce cannot move the server’s sequence.
- Testing approach
- A bet carrying a nonce ten ahead of the current one.
- Observed result
- The injected nonce was ignored; the server’s own progression was unaffected.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-NONCE-003PassZero and negative nonces
- Control assessed
- Implausible injected nonces are ignored.
- Testing approach
- Bets carrying zero and negative nonces.
- Observed result
- All three values were ignored.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-NONCE-004PassOverflow-sized nonces
- Control assessed
- Oversized injected nonces are ignored without a server error.
- Testing approach
- Bets carrying nonces at integer-overflow scale.
- Observed result
- All three were ignored, with no server error.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
RNG / PF Verification3/3
LIQD-PLINKO-EXTRA-002PassPath and slot verification
- Control assessed
- Every live round’s path and landing slot reproduce from the revealed inputs.
- Testing approach
- Five rounds reproduced through LIQD’s verify endpoint after the reveal.
- Observed result
- All five reproduced their landing slot exactly, with no hash or outcome mismatch.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-PF-001PassProvably-fair data and the reveal flow
- Control assessed
- Each bet carries every provably-fair field, and every round verifies once its seed is revealed.
- Testing approach
- Three bets checked field by field; after rotation, each round verified through LIQD’s verify endpoint.
- Observed result
- All fields were present and the nonce incremented each time; after rotation, all three rounds verified.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-PF-002PassNonce sequence within a seed period
- Control assessed
- The nonce advances by exactly one per bet while the commitment stays unchanged.
- Testing approach
- Ten consecutive bets on one server seed.
- Observed result
- The nonce advanced by exactly 10 with no gap, and the commitment was unchanged.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Entropy / PF Verification1/1
LIQD-PLINKO-ENTROPY-004PassNonce reset after seed rotation
- Control assessed
- Rotating the seed changes the commitment and restarts the nonce at 0.
- Testing approach
- Bets, a rotation, then further bets.
- Observed result
- The nonce reset from 4 to 0, the commitment changed, and betting continued normally.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Precision / Payout1/1
LIQD-PLINKO-PREC-001PassPayout consistency at scale
- Control assessed
- Every payout matches the multiplier table, and the wallet reconciles.
- Testing approach
- Two hundred consecutive bets; each payout and the aggregate balance change checked.
- Observed result
- All 200 payouts matched the multiplier formula exactly, and the aggregate balance change matched the expected sum.
- Impact
- None — the control held.
- Limitations
- One board configuration was tested; Section 4 checks the full multiplier table.
Event Replay1/1
LIQD-PLINKO-REPLAY-001PassDuplicate identical bets sent together
- Control assessed
- Two identical requests sent at the same moment are processed as two distinct rounds.
- Testing approach
- A pair of identical bet requests sent in parallel.
- Observed result
- Both were processed as distinct rounds with distinct bet IDs.
- Impact
- None — each request settled exactly once.
- Limitations
- None specific to this test; the general scope is in 5.5.
Adversarial Seeds3/3
LIQD-PLINKO-SEED-001PassClient-seed length and character sets
- Control assessed
- A length limit is enforced, and seeds are stored as literal text with no effect on the game.
- Testing approach
- Client seeds of increasing length and unusual character sets — markup, query fragments, path-like strings, emoji, CJK, line breaks — each followed by a bet.
- Observed result
- A consistent 255-character limit applies; every unusual seed was stored as a literal string with no injection or crash, and betting worked after every change.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-SEED-002PassUnicode forms of one seed
- Control assessed
- Composed and decomposed Unicode forms of the same seed are handled consistently.
- Testing approach
- The same visible seed submitted in both forms.
- Observed result
- Both forms were stored identically: the server normalises Unicode consistently.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-PLINKO-SEED-003PassRapid seed rotation
- Control assessed
- Each bet uses the seed set by the rotation immediately before it.
- Testing approach
- Three back-to-back rotations, each followed by a bet.
- Observed result
- Every bet used exactly the seed set just before it; nothing carried over between seed periods.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Technical Evidence & Verification4 sections
5.3Coverage Summary11 categories
Coverage by category — 29 tests: 27 pass, 2 remediated.
| Category | Tests | Result |
|---|---|---|
| Race Conditions | 4 | All pass |
| Input Validation | 4 | 2 pass · 2 remediated (PARAM-001, PARAM-002) |
| Atomic Balance / Idempotency | 3 | All pass |
| Broken Authorization | 2 | All pass |
| Max-win Bypass | 3 | All pass |
| Nonce Manipulation | 4 | All pass |
| RNG / PF Verification | 3 | All pass |
| Entropy / PF Verification | 1 | All pass |
| Precision / Payout | 1 | All pass |
| Event Replay | 1 | All pass |
| Adversarial Seeds | 3 | All pass |
5.4Findings & Remediation1 record
1 finding from 2 tests, remediated and re-verified. Each test’s card in the matrix above gives its control, approach, result, impact and limitations.
| Tests | Finding | Status |
|---|---|---|
| PARAM-001 · PARAM-002 | Wrong-type and sub-precision values were converted into valid bets instead of refused, inside the valid ranges. Fixed by LIQD and re-verified. | Remediated · Low |
5.5Scope & LimitationsScope note
5.6Evidence & Disclosure
Live-surface record: Section 5 tests LIQD’s running service, so unlike Sections 1–4 it does not reproduce from the audit repository. The published record is the test cards above.
Retained under the engagement record: Per-test procedures, request and response captures, re-test captures, run timestamps and build identifiers — available to LIQD and to regulators on request.
Integrity tests ....................... 29Categories ............................ 11Pass .................................. 27Remediated ............................ 2Warning (open) ........................ 0Findings .............................. 1 · 1 remediated, 0 openProcedures and payloads ............... withheld (5.5)Player Verification
Can a player verify their own bets without trusting anyone?
Every Plinko outcome can be independently reproduced using publicly disclosed inputs. No hidden variables, no private backend data. If the revealed seed matches the hash you saw before betting and your computed path and slot match the game result, the bet was provably fair. This section walks you through the process — with LIQD’s own panel, with the independent ProvablyFair.org verifier, or with a few lines of code you run yourself.
Key principles
- Every Plinko outcome can be independently reproduced
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saw before betting and your computed path and slot match the game result, the bet was provably fair
- Most players can verify directly through LIQD’s Provably Fair panel
What you need
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the bet number in sequence (ensures uniqueness)
- Row Count — the configuration you played (8–16; WTF is always 13)
- Every Plinko outcome can be independently reproduced
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saw before betting and your computed path and slot match the game result, the bet was provably fair
- Most players can verify directly through LIQD’s Provably Fair panel
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the bet number in sequence (ensures uniqueness)
- Row Count — the configuration you played (8–16; WTF is always 13)
Player can access all required inputsPass
Client seed origin transparentPass
Verification process documentedPass
MANIFEST.md, Verify a bet); the scripts in 6.8–6.9 reproduce all 10,100 captured dropsCasino UI exposes verification panelPass
Independent verifier availablePass
Manual recomputation possiblePass
No hidden or proprietary stepsPass
Public algorithm documentationInfo
docs/ALGORITHM.md) gives them, from the standard construction and the captured drops. Publishing the byte-level algorithm is our first recommendation to the operatorOnly disclosed inputs are used. Identical inputs always produce identical output.
- 1
Place a Plinko Bet
Choose your row count, risk level (or WTF), and bet amount. The game generates a result using the provably fair algorithm — the ball animation only replays a path that is already locked.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the game page. You’ll see the active client seed, the hashed server seed, the number of bets made with the pair — your next nonce — and the pre-committed hash of the next server seed. Save the hashed server seed before you bet — it is the commitment you will check the revealed seed against. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saved before betting — across this audit’s 202 epochs it always did.
- 3
Enter the Seeds in the Verify Tab
In the panel’s Verify tab, enter the revealed server seed, your client seed, the bet’s nonce, and the row count and risk you played.
- 4
Recompute & Confirm
Press Verify — or enter the same inputs in the ProvablyFair.org verifier, or run the scripts under Manual Verification. If the commitment check in step 2 passed and the recomputed path and slot match your live result, the bet was provably fair — and your payout equals your stake × that slot’s multiplier.
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
Verify with the audit code
Independent verification using the implementation behind this report.
Open independent verifierBuilt-in verification
The operator’s own verification page. This tool is controlled by LIQD.com.
Open LIQD verifierVisual Walkthroughstep-by-step
Select your row count and risk level — LOW, MEDIUM, HIGH, or WTF — and place a bet. The ball drops through the peg board and lands in a slot. The server has already committed to the outcome before the animation plays.

LIQD Plinko on liqd.com — Low risk and 8 rows, with that board’s slot multipliers along the bottom: 5.6×, 2.1×, 1.1×, 1×, 0.5×, 1×, 1.1×, 2.1×, 5.6×. The path is fixed when the bet is accepted.
Open the Provably Fair panel on the game page. You’ll see the active client seed, the hashed server seed (the casino’s commitment), the number of bets made with the pair — your next nonce — and, under Rotate Seed Pair, the next client seed (editable) and the pre-committed hash of the next server seed. Save the hashed server seed before you bet. The next-seed hash is what chains epochs together: the seed you rotate to was fixed before you rotated. Rotating the seed pair ends the epoch and reveals the outgoing server seed in plaintext — the rotate response returns it as revealedServerSeed, alongside the new pair's commitments shown in the panel. Check the commitment first:
Server Seed (revealed) — the 32-character plaintext the casino committed to before your bets. SHA-256(utf8(seed)) must equal the pre-published hash.

The Seeds tab before rotating — Active Server Seed (Hashed) fd9fde97…6a0f7b after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 88c023cb…3b69d0, hashes to fd9fde97…6a0f7b.
The Verify tab takes the client seed, the server seed, the nonce, the rows and the risk, and recomputes a drop on demand. Enter the server seed revealed in step 2, your client seed, the bet’s nonce, and the row count and risk you played.
Client Seed — your player-editable entropy input for the finished epoch.
Nonce — the sequential bet counter (0, 1, 2, …; 0–49 in this capture) identifying each drop under the pair.

The Verify tab filled in for that drop — client seed, the server seed revealed in step 2, nonce 0, 8 rows and Low risk.
Press Verify. The tab recomputes the landing slot from the disclosed inputs and marks it on the multiplier row; your payout is that slot’s multiplier on the board you played. If the revealed seed hashed to the commitment you saved and the recomputed slot matches your drop, the drop was provably fair. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python — and print the per-row bounce bits as well — pre-loaded with the report’s featured bet; enter this drop’s inputs to reproduce it.

The result: “Bucket 3 pays 1x” — slot 3 on Low, 8 rows. Our scripts in 6.8–6.9 reproduce it exactly: bounce bits 10100100, slot 3, 1×.
Manual Verification (Advanced)6 sections
6.5Why Manual Verification Matters
Hosted verifiers — including ours — are conveniences. The guarantee only becomes unconditional when you can reproduce a round with tools nobody else controls. The algorithm below is small enough to audit by eye and run anywhere: if the revealed seed matches the hash you saw before betting and your recomputation matches the round you played, no one in the chain — casino, auditor, or tool host — changed it after you bet.
6.6How the Algorithm Works (Plain English)
Before you play, the server locks the outcome using three ingredients:
- The server's secret seed — committed by publishing its SHA-256 hash before you bet, and chained by a pre-committed next-seed hash
- Your client seed — set by you and editable at any rotation; the server echoes it back
- The nonce — a counter that makes each bet under the pair unique
These ingredients are combined with HMAC-SHA256 once per row — the cursor runs 1 … rows — to produce a bounce direction at each peg row: the first four bytes of the digest, mod 2. The ball bounces left (bit 0) or right (bit 1) at each row; the final slot is the count of right-bounces. Because the server committed to its seed before you bet, it cannot steer the outcome afterwards; set your own client seed after the commitment and it cannot pre-select a seed against you either — and after the reveal, anyone can recompute it. Note the two encodings: the commitment hashes the hex string as text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game.
6.7Casino Verifier vs ProvablyFair.org Verifier4 records
Both should agree — the point of having an independent one is what happens if they ever don't:
| LIQD Built-in (in-game panel) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino | ProvablyFair.org — open source |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | By the live integrity tests (LIQD-PLINKO-EXTRA-002, S5); their output is not part of the audit package | Directly — run unmodified by Step 5 on all 10,100 captured drops: same path, slot and multiplier on every one |
| If they disagree | Suspect the casino’s tool or a tampered round | The repo + scripts are the tiebreaker anyone can run |
6.8JavaScript Verification Script
Complete, dependency-free, and pre-loaded with the featured bet as its default inputs — run it as-is to see a full match, then swap in your own round. Run on every captured drop, it reproduces the commitment, path and slot of all 10,100 drops:
// verify-plinko.js — standalone LIQD Plinko verifier (Node.js, no dependencies)// Usage: node verify-plinko.js <serverSeed> <clientSeed> <nonce> <rows> (WTF mode is always 13 rows)const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, rowsArg] = argv.length ? argv : ['fc9a1d6fad00e0832fddfdea45435c85', 'auditb1141320acde', '0', '8'];const nonce = Number(nonceArg), rows = Number(rowsArg); // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — compare it with the hash you saved before bettingconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One bit per row — HMAC-SHA256 keyed on the 16 DECODED bytes, message clientSeed:nonce:cursor with// cursor = 1 … rows; the bit is the first four digest bytes (big-endian) mod 2: 0 = left, 1 = rightconst key = Buffer.from(serverSeed, 'hex');let path = '';for (let cursor = 1; cursor <= rows; cursor++) { const digest = createHmac('sha256', key).update(`${clientSeed}:${nonce}:${cursor}`).digest(); path += String(digest.readUInt32BE(0) % 2);}console.log('path =', path); // 3) The ball lands in the slot numbered by its right bounces (0 = far left … rows = far right)console.log('slot =', [...path].filter(bit => bit === '1').length);// Look the slot up in the multiplier row for your rows and risk; the payout is stake × multiplier.// Expected for the defaults (bet SLOjmT9dyuUEi1que576R, epoch 0 nonce 0, 8 rows, low risk, $0.10):// commitment = 3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36// path = 01101111// slot = 6 → 1.1× on the 8-row low-risk row, so the payout is 0.10 × 1.1 = 0.116.9Python Verification Script
The same verifier in Python 3, standard library only — an independent check on the JavaScript version:
# verify-plinko.py — standalone LIQD Plinko verifier (Python 3, stdlib only)# Usage: python3 verify-plinko.py <serverSeed> <clientSeed> <nonce> <rows> (WTF mode is always 13 rows)import hashlib, hmac, sys args = sys.argv[1:] or ['fc9a1d6fad00e0832fddfdea45435c85', 'auditb1141320acde', '0', '8']server_seed, client_seed, nonce, rows = args[0], args[1], int(args[2]), int(args[3]) # 1) Commitment — SHA-256 of the hex string as UTF-8 text; compare it with the hash you saved before bettingprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One bit per row — HMAC-SHA256 keyed on the 16 decoded bytes, message client_seed:nonce:cursor with# cursor = 1 … rows; the bit is the first four digest bytes (big-endian) mod 2: 0 = left, 1 = rightkey = bytes.fromhex(server_seed)path = ''for cursor in range(1, rows + 1): digest = hmac.new(key, f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest() path += str(int.from_bytes(digest[:4], 'big') % 2)print('path =', path) # 3) The ball lands in the slot numbered by its right bounces (0 = far left … rows = far right)print('slot =', path.count('1'))# Look the slot up in the multiplier row for your rows and risk; the payout is stake × multiplier.# Expected for the defaults (bet SLOjmT9dyuUEi1que576R, epoch 0 nonce 0, 8 rows, low risk, $0.10):# commitment = 3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36# path = 01101111# slot = 6 → 1.1× on the 8-row low-risk row, so the payout is 0.10 × 1.1 = 0.116.10Evidence Screenshots & Records5 records
E01 and E02 are views from the auditor’s session and E04 and E05 public, logged-out views; none of them shows an epoch of the audited dataset. The walkthrough’s screenshots are page captures from liqd.com; the evidence files below ship in the package.
| File | Shows |
|---|---|
| E01-plinko-game-ui.png | The Plinko interface on the pre-production environment — rows/risk controls and the per-slot bucket multipliers (Low/Medium/High tabs) |
| E02-provably-fair-panel.png | The in-game Provably Fair panel — active and next server-seed hashes, client seed and nonce; its identifiers are not from the audited dataset |
| E04-multiplier-tables.jpg | A public, logged-out view of the 8-row high-risk table (29× / 4 / 1.5 / 0.3 / 0.2 / 0.3 / 1.5 / 4 / 29×) — equal to the pinned table |
| E05-wtf-mode.jpg | A public view of WTF mode — the fourth risk tab, 13 rows, and the 1000× (“1k”) and 235× edge buckets |
| rtp-page-labels-2026-10-01.json | The RTP and house-edge figures on LIQD’s Plinko game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2) |
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
How it works
- 1
Place a Plinko Bet
Choose your row count, risk level (or WTF), and bet amount. The game generates a result using the provably fair algorithm — the ball animation only replays a path that is already locked.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the game page. You’ll see the active client seed, the hashed server seed, the number of bets made with the pair — your next nonce — and the pre-committed hash of the next server seed. Save the hashed server seed before you bet — it is the commitment you will check the revealed seed against. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saved before betting — across this audit’s 202 epochs it always did.
- 3
Enter the Seeds in the Verify Tab
In the panel’s Verify tab, enter the revealed server seed, your client seed, the bet’s nonce, and the row count and risk you played.
- 4
Recompute & Confirm
Press Verify — or enter the same inputs in the ProvablyFair.org verifier, or run the scripts under Manual Verification. If the commitment check in step 2 passed and the recomputed path and slot match your live result, the bet was provably fair — and your payout equals your stake × that slot’s multiplier.
Visual Walkthroughstep-by-step
Select your row count and risk level — LOW, MEDIUM, HIGH, or WTF — and place a bet. The ball drops through the peg board and lands in a slot. The server has already committed to the outcome before the animation plays.

LIQD Plinko on liqd.com — Low risk and 8 rows, with that board’s slot multipliers along the bottom: 5.6×, 2.1×, 1.1×, 1×, 0.5×, 1×, 1.1×, 2.1×, 5.6×. The path is fixed when the bet is accepted.
Open the Provably Fair panel on the game page. You’ll see the active client seed, the hashed server seed (the casino’s commitment), the number of bets made with the pair — your next nonce — and, under Rotate Seed Pair, the next client seed (editable) and the pre-committed hash of the next server seed. Save the hashed server seed before you bet. The next-seed hash is what chains epochs together: the seed you rotate to was fixed before you rotated. Rotating the seed pair ends the epoch and reveals the outgoing server seed in plaintext — the rotate response returns it as revealedServerSeed, alongside the new pair's commitments shown in the panel. Check the commitment first:
Server Seed (revealed) — the 32-character plaintext the casino committed to before your bets. SHA-256(utf8(seed)) must equal the pre-published hash.

The Seeds tab before rotating — Active Server Seed (Hashed) fd9fde97…6a0f7b after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 88c023cb…3b69d0, hashes to fd9fde97…6a0f7b.
The Verify tab takes the client seed, the server seed, the nonce, the rows and the risk, and recomputes a drop on demand. Enter the server seed revealed in step 2, your client seed, the bet’s nonce, and the row count and risk you played.
Client Seed — your player-editable entropy input for the finished epoch.
Nonce — the sequential bet counter (0, 1, 2, …; 0–49 in this capture) identifying each drop under the pair.

The Verify tab filled in for that drop — client seed, the server seed revealed in step 2, nonce 0, 8 rows and Low risk.
Press Verify. The tab recomputes the landing slot from the disclosed inputs and marks it on the multiplier row; your payout is that slot’s multiplier on the board you played. If the revealed seed hashed to the commitment you saved and the recomputed slot matches your drop, the drop was provably fair. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python — and print the per-row bounce bits as well — pre-loaded with the report’s featured bet; enter this drop’s inputs to reproduce it.

The result: “Bucket 3 pays 1x” — slot 3 on Low, 8 rows. Our scripts in 6.8–6.9 reproduce it exactly: bounce bits 10100100, slot 3, 1×.
Technical evidence
6.5Why Manual Verification Matters
Hosted verifiers — including ours — are conveniences. The guarantee only becomes unconditional when you can reproduce a round with tools nobody else controls. The algorithm below is small enough to audit by eye and run anywhere: if the revealed seed matches the hash you saw before betting and your recomputation matches the round you played, no one in the chain — casino, auditor, or tool host — changed it after you bet.
6.6How the Algorithm Works (Plain English)
Before you play, the server locks the outcome using three ingredients:
- The server's secret seed — committed by publishing its SHA-256 hash before you bet, and chained by a pre-committed next-seed hash
- Your client seed — set by you and editable at any rotation; the server echoes it back
- The nonce — a counter that makes each bet under the pair unique
These ingredients are combined with HMAC-SHA256 once per row — the cursor runs 1 … rows — to produce a bounce direction at each peg row: the first four bytes of the digest, mod 2. The ball bounces left (bit 0) or right (bit 1) at each row; the final slot is the count of right-bounces. Because the server committed to its seed before you bet, it cannot steer the outcome afterwards; set your own client seed after the commitment and it cannot pre-select a seed against you either — and after the reveal, anyone can recompute it. Note the two encodings: the commitment hashes the hex string as text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game.
6.7Casino Verifier vs ProvablyFair.org Verifier4 records
Both should agree — the point of having an independent one is what happens if they ever don't:
| LIQD Built-in (in-game panel) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino | ProvablyFair.org — open source |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | By the live integrity tests (LIQD-PLINKO-EXTRA-002, S5); their output is not part of the audit package | Directly — run unmodified by Step 5 on all 10,100 captured drops: same path, slot and multiplier on every one |
| If they disagree | Suspect the casino’s tool or a tampered round | The repo + scripts are the tiebreaker anyone can run |
6.8JavaScript Verification Script
Complete, dependency-free, and pre-loaded with the featured bet as its default inputs — run it as-is to see a full match, then swap in your own round. Run on every captured drop, it reproduces the commitment, path and slot of all 10,100 drops:
// verify-plinko.js — standalone LIQD Plinko verifier (Node.js, no dependencies)// Usage: node verify-plinko.js <serverSeed> <clientSeed> <nonce> <rows> (WTF mode is always 13 rows)const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, rowsArg] = argv.length ? argv : ['fc9a1d6fad00e0832fddfdea45435c85', 'auditb1141320acde', '0', '8'];const nonce = Number(nonceArg), rows = Number(rowsArg); // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — compare it with the hash you saved before bettingconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One bit per row — HMAC-SHA256 keyed on the 16 DECODED bytes, message clientSeed:nonce:cursor with// cursor = 1 … rows; the bit is the first four digest bytes (big-endian) mod 2: 0 = left, 1 = rightconst key = Buffer.from(serverSeed, 'hex');let path = '';for (let cursor = 1; cursor <= rows; cursor++) { const digest = createHmac('sha256', key).update(`${clientSeed}:${nonce}:${cursor}`).digest(); path += String(digest.readUInt32BE(0) % 2);}console.log('path =', path); // 3) The ball lands in the slot numbered by its right bounces (0 = far left … rows = far right)console.log('slot =', [...path].filter(bit => bit === '1').length);// Look the slot up in the multiplier row for your rows and risk; the payout is stake × multiplier.// Expected for the defaults (bet SLOjmT9dyuUEi1que576R, epoch 0 nonce 0, 8 rows, low risk, $0.10):// commitment = 3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36// path = 01101111// slot = 6 → 1.1× on the 8-row low-risk row, so the payout is 0.10 × 1.1 = 0.116.9Python Verification Script
The same verifier in Python 3, standard library only — an independent check on the JavaScript version:
# verify-plinko.py — standalone LIQD Plinko verifier (Python 3, stdlib only)# Usage: python3 verify-plinko.py <serverSeed> <clientSeed> <nonce> <rows> (WTF mode is always 13 rows)import hashlib, hmac, sys args = sys.argv[1:] or ['fc9a1d6fad00e0832fddfdea45435c85', 'auditb1141320acde', '0', '8']server_seed, client_seed, nonce, rows = args[0], args[1], int(args[2]), int(args[3]) # 1) Commitment — SHA-256 of the hex string as UTF-8 text; compare it with the hash you saved before bettingprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One bit per row — HMAC-SHA256 keyed on the 16 decoded bytes, message client_seed:nonce:cursor with# cursor = 1 … rows; the bit is the first four digest bytes (big-endian) mod 2: 0 = left, 1 = rightkey = bytes.fromhex(server_seed)path = ''for cursor in range(1, rows + 1): digest = hmac.new(key, f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest() path += str(int.from_bytes(digest[:4], 'big') % 2)print('path =', path) # 3) The ball lands in the slot numbered by its right bounces (0 = far left … rows = far right)print('slot =', path.count('1'))# Look the slot up in the multiplier row for your rows and risk; the payout is stake × multiplier.# Expected for the defaults (bet SLOjmT9dyuUEi1que576R, epoch 0 nonce 0, 8 rows, low risk, $0.10):# commitment = 3b34a0fce5732f2d9c89b16fca9c2b6a231c2b7f2de29bbc41e141861dc39d36# path = 01101111# slot = 6 → 1.1× on the 8-row low-risk row, so the payout is 0.10 × 1.1 = 0.116.10Evidence Screenshots & Records5 records
E01 and E02 are views from the auditor’s session and E04 and E05 public, logged-out views; none of them shows an epoch of the audited dataset. The walkthrough’s screenshots are page captures from liqd.com; the evidence files below ship in the package.
| File | Shows |
|---|---|
| E01-plinko-game-ui.png | The Plinko interface on the pre-production environment — rows/risk controls and the per-slot bucket multipliers (Low/Medium/High tabs) |
| E02-provably-fair-panel.png | The in-game Provably Fair panel — active and next server-seed hashes, client seed and nonce; its identifiers are not from the audited dataset |
| E04-multiplier-tables.jpg | A public, logged-out view of the 8-row high-risk table (29× / 4 / 1.5 / 0.3 / 0.2 / 0.3 / 1.5 / 4 / 29×) — equal to the pinned table |
| E05-wtf-mode.jpg | A public view of WTF mode — the fourth risk tab, 13 rows, and the 1000× (“1k”) and 235× edge buckets |
| rtp-page-labels-2026-10-01.json | The RTP and house-edge figures on LIQD’s Plinko game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2) |
Only disclosed inputs are used. Identical inputs always produce identical output.
Reproducibility & Artifacts
Can anyone independently reproduce every scored check in this audit?
This section consolidates the open-source repository, datasets, output artifacts, and reproducibility posture of the audit. Every scored finding, statistic and pass/fail result can be independently reproduced by anyone with a computer and an internet connection — only the procedures of the live integrity tests in S5 are withheld, by policy. The repository is the credential — not this report.
Repository Details
- GitHub: ProvablyFair-org/liqd-plinko
- Release: v1.0.1 · Framework 1.1
- Game: Plinko (liqd-plinko)
- Public Verifier: provablyfair.org/verify-bets/liqd
Prerequisites
- Node.js 22.x (
package.jsonengines and.nvmrc) —npm testneeds no dependency install - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.8.0) — for the source commands (npm run verify,replay,simulate,mutate)
- GitHub: ProvablyFair-org/liqd-plinko
- Release: v1.0.1 · Framework 1.1
- Game: Plinko (liqd-plinko)
- Public Verifier: provablyfair.org/verify-bets/liqd
- Node.js 22.x (
package.jsonengines and.nvmrc) —npm testneeds no dependency install - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.8.0) — for the source commands (npm run verify,replay,simulate,mutate)
All audit-package results reproduce from release v1.0.1, the pinned artifacts and the commands above. The dataset and multiplier-table hashes are checked before any step runs, so you are provably testing the same 10,100 drops and the same tables; the simulation, calibration, RTP claim table and emitted results are pinned by SHA-256 and re-checked on every run. Every npm test replays all 28,000,000 Pass 1 rounds from their saved seeds and re-executes Pass 2 for all 202 epochs.
Commands to Reproduce
The 10,100-drop dataset, the pinned multiplier tables and the committed results ship inside the repo — no separate download; the dataset and table hashes are checked before any step runs, and every other artifact’s hash during the run. npm test needs only Node.js 22: it checks that the bundles match the sources, runs the 57 unit tests and the 21-step verification — which replays all 28M simulated rounds — then checks the committed outputs and the release manifest. Output as run on Node 22.22.0 with two CPU cores (about 13 minutes, 11 of them the Pass 1 replay).
Every run writes only to outputs/run/ and compares its results field by field with the committed results (outputs/run/diff.json) — that comparison is the reproduction gate. Exit codes: 0 Full Pass · 2 a flag with no fail (Conditional Pass) · 5 RTP FIGURE OVERSTATED (the RTP claim rule finds a published figure overstated) · 1 any hard fail, or a reproduction beyond tolerance. Output of node verify.js as run on Node 22.22.0; npm run verify runs the same steps from source. The informational context the suite prints before the summary and the RTP claim’s source note are left out here (…).
Every npm test already replays all 28,000,000 Pass 1 rounds from their saved seed pairs inside Step 16 and re-executes Pass 2 for all 202 epochs inside Step 17. npm run replay runs the Pass 1 replay on its own and writes nothing; npm run simulate starts a new experiment under fresh crypto.randomBytes seeds and writes it to outputs/fresh/, never over the committed results, so its numbers differ by design. With two Bonferroni families of 28 tests, each held at family-wise 0.01, a new experiment fails Step 16 by chance about 1 time in 50; that is the gate’s designed false-alarm rate, and npm test has no fresh draws to trip on. Output of node replay.js (the zero-install form of npm run replay) as run on Node 22.22.0, repeated progress lines left out (…): the stand-alone replay runs on one thread, about 18 minutes here, while Step 16 inside npm test spreads the same rounds over worker threads.
Audit Reproducibility Pinning
- Release Tag
- v1.0.1 — github.com/ProvablyFair-org/liqd-plinko/tree/v1.0.1
- Framework
- ProvablyFair.org Audit Framework 1.1 · Plinko profile 1.0.2 · RTP-CLAIM 1.2.0
- Release Signature
- Release commit and tag signed with the ProvablyFair.org release key (SSH,
SHA256:gEujiJm2pRyq7lQsfYQfQ1r97H/J7GdaKHbOW8GtikY), recorded inRELEASE-MANIFEST.json - Node Version
- 22.x (
package.jsonengines and.nvmrc); run here on 22.22.0 - Primary Dataset
- data/plinko-master-10100bets.json (10,100 drops, 202 seed epochs of exactly 50 bets)
- Primary Dataset Hash (SHA-256)
- 871c485ef0d55ca38af2cece4880abdc32fe8b62584468c5d4cfdfd01506fc95
- Multiplier Config
- plinkoConfig.json — 27 standard tables + WTF, transcribed from LIQD’s client bundle on 2026-07-06
- Multiplier Config Hash (SHA-256)
- f8eaf67e1ffc63ee0dc21123447581c55f443c167dcb451b45ab7cd108fe712f
- Simulation Artifact Hash (SHA-256)
- 3d94a510b760abfe2356d96d821f09dfe2bff40866da4bd47669fe8e3df6e0c6
- Calibration Artifact Hash (SHA-256)
- 53dabd4778e1d2562cb2ad136f6121253502eaac9fcf0cb70a064551bdaf3ca6
- RTP Claim Table Hash (SHA-256)
- 84f48588314bef5c38c35f0c97d94c58f01c4bd39ea089e0c9e8c2308461e239
- Verification Results Hash (SHA-256)
- e09d67a1f83f35306032c94db256f11c3800ff1bc5fd622cde30e4dc3c01d265
- Pass 1 Census (SHA-256)
- 8c840243629258e29aeeb9c3356ee637c232889bc1b5b3b2ea5b8b8a1285d1a5 — the replayed slot counts of all 28 boards, pinned in
src/artifact-pins.ts - Reproduction Gate
- Every run writes only to outputs/run/ and is compared field by field with the committed artifacts (outputs/run/diff.json) — exact content byte for byte, a derived floating-point statistic within 1e-14 + 1e-12 relative; anything beyond tolerance exits 1
- Exit Codes
- 0 Full Pass · 2 a flag with no fail (Conditional Pass) · 5 RTP FIGURE OVERSTATED · 1 hard fail or reproduction beyond tolerance
- Mutation Battery
- 79 declared counterexamples (52 in
tests/mutations.json, 27 intests/mutations-extra.json, one of them the declared survivor S00) — 79 / 79 behaved as declared in the package’s recorded run (npm run mutate, outsidenpm testbecause it re-runs the verifier once per entry) - Runtime
npm test≈ 13 min on 2 CPU cores (Node 22.22.0), 11 min of it the 28M-round replay on worker threads- Capture Environment
- LIQD’s pre-production environment, qa.liqd.com · one authenticated session, one account
- Audit Date
- July 2026 (dataset captured 7 July 2026, 02:12–04:16 UTC); RTP page read 1 October 2026
- Audit ID
- PF-2026-LQ03
Output Artifacts7 files committed
| File | Contents | Size |
|---|---|---|
| verification-results.json | Steps 1–21 results — per-step pass states with evidence detail strings and machine data, profile coverage (PLINKO-01 … PLINKO-39), the SHA-256 of six committed artifacts it checks (dataset, tables, simulation, calibration, stats, coverage), the payout-table coverage block, info items, verdict | ~60 KB |
| verification-stats.json | The numerics behind every figure quoted in the report, emitted by the scored steps and info items as machine values | ~13 KB |
| coverage-results.json | Observed payout-cell coverage per board (285 of 365 cells; 7.41% of the summed RTP on cells never paid) | ~12 KB |
| rtp-claim-table.json | The RTP claim rule’s table — LIQD’s published 99% against all 28 boards, each with exact RTP, gap and outcome | ~5 KB |
| simulation-results.json | Pass 1 per-board results (saved seed pair, slot χ², lag-1, runs test, exact and simulated RTP, 20-point convergence trace) + Pass 2 rows for all 202 casino seeds | ~126 KB |
| calibration-results.json | Calibrated null for the payout-weighted Pass 2 count at both windows: per-board left-tail probabilities and the Poisson-binomial expectation Step 17 recomputes | ~75 KB |
| rtp-convergence.html | Per-board running-mean RTP traces of the committed Pass 1 (28 traces sampled every 50,000 rounds), re-rendered byte for byte by Step 16 | ~13 KB |
Repository Structure
liqd-plinko/ ├── src/ │ ├── rng.ts → commitHash · HMAC draw (generateProvablyFairNumber) · revealPlinko — written from docs/ALGORITHM.md │ ├── bias.ts → the bit-extraction bias bound and the census on the module’s own draw (Step 5) │ ├── config.ts → pinned-table loader (SHA-256 pin) · binomP · theoreticalRTP · CONVERGENCE_STEP │ ├── loader.ts → dataset loader + SHA-256 pin + the audited population constants (10,100 · 202 · 50 · phase plan) │ ├── money.ts → integer 1e-8 settlement units for every money check │ ├── rtp-claim.ts · rtp-claim-switch.ts → the RTP claim rule (RTP-CLAIM 1.2.0) and its enforcement switch (on) │ ├── rtp-settings.ts → the 28-board exact RTP table and LIQD’s published claim │ ├── profile-map.ts → which Plinko profile check (PLINKO-01 … PLINKO-39) each step implements │ ├── site-verifier.ts → loads the vendored ProvablyFair.org verifier for the Step 5 parity arm │ ├── simulate.ts · pass1.ts · pass2.ts → two-pass Monte Carlo (28M rounds + cherry-pick probe), fresh seeds → outputs/fresh/ │ ├── replay.ts → the stand-alone 28M-round Pass 1 replay from its saved seeds (one thread) │ ├── parallel.ts → the worker threads Step 16 replays the same rounds on │ ├── calibrate.ts · calibration.ts → calibrated null for the payout-weighted Pass 2 count (exact lattice convolution + Poisson-binomial) │ ├── mc-tail.ts → Monte Carlo cross-check of that null’s window tail, run by the unit tests │ ├── convergence-chart.ts → the per-board convergence chart, re-rendered byte for byte by Step 16 │ ├── stats.ts → chi-squared, lag-1 autocorrelation, runs test, normalCDF, binomialSurvival │ ├── domains.ts → value domains for every numeric field in the committed outputs │ ├── artifact-pins.ts → SHA-256 pins of the simulation and calibration artifacts and the replayed census │ ├── diff.ts · diff-simulation.ts → field-level diff of a run vs the committed artifacts │ └── types.ts → Bet / Seed / PreCapture type definitions ├── docs/ │ ├── ALGORITHM.md → the specification the RNG is written from │ └── INTERFACE.md → the module interface the RNG implements ├── verifier-parity/ → the ProvablyFair.org site verifier’s Plinko module, vendored byte for byte and pinned (Step 5) ├── tests/ │ ├── verify.ts → 21-step verification runner (results under outputs/run/; PF_EMIT=1 to regenerate the committed record) │ ├── plinko/ → 57 unit tests (rngSpecTests — the specification’s 55 checks — rngTests, rtpPinTests, calibrationTests, domainTests) │ ├── mutations.json · mutations-extra.json → 79 declared counterexamples (52 scored-step + 27 guard / full-replay / declared survivor) │ └── steps/ │ ├── commitment.ts → Steps 1–4: commit-reveal integrity (incl. the pre-capture link) │ ├── determinism.ts → Steps 5–6: recomputation + bias census + site-verifier parity, client seed (exact Poisson-binomial null) │ ├── payouts.ts → Steps 7–10: multiplier, payout, config, RTP audit (+ the RTP claim rule) │ ├── dataset.ts → Steps 11–12: phase labels, epoch size (population guard) │ ├── anti-circularity.ts → Step 13: binomP vs an independent Pascal recurrence │ ├── phase-d.ts → Step 14: client-seed variation + predictable-seed disclosure │ ├── wtf.ts → Step 15: WTF-mode verification │ ├── simulation.ts → Steps 16–17: Pass 1 replay and gates, Pass 2 re-execution and calibration │ ├── standardization.ts → Steps 18–20: bet size, symmetry, artifact hashes + value domains │ ├── live-fit.ts → Step 21: epoch-window slot fit (live drops vs binomial) │ ├── statistical.ts · coverage.ts → informational items and payout-table cell coverage │ └── context.ts → shared context + pass/fail helpers ├── data/ │ └── plinko-master-10100bets.json → 10,100 drops · 202 epochs (5 phases) · SHA-256 pinned ├── outputs/ → committed run; every run writes its own results under outputs/run/ │ ├── verification-results.json → Steps 1–21 + profile coverage + artifact hashes + coverage + info items + verdict │ ├── verification-stats.json → machine-readable numerics behind every quoted figure │ ├── coverage-results.json → observed payout-cell coverage (285/365) │ ├── rtp-claim-table.json → the RTP claim rule over all 28 boards │ ├── simulation-results.json → Pass 1 (28M, saved seeds + convergence traces) + Pass 2 (cherry-pick probe) │ ├── calibration-results.json → calibrated null for the payout-weighted Pass 2 count, both windows │ └── rtp-convergence.html → per-board running-mean traces of the committed Pass 1 ├── evidence/ → E01, E02, E04, E05 UI screenshots · RTP page labels ├── capture/ → illustrative capture references (no credentials) ├── scripts/ → run-stages (npm test), standalone build + check, check-outputs, release manifest + release bar, mutation runner, site-verifier check ├── .github/workflows/reproduce.yml → second-machine reproduction on a clean runner ├── test.js · verify.js · replay.js → dependency-free bundles run by `npm test` and `npm run replay:standalone` ├── standalone-manifest.json → source, unit-test and bundle hashes ├── plinkoConfig.json → hash-pinned multiplier tables (27 standard + WTF) ├── README.md · MANIFEST.md → the report (findings, evidence register, worked examples, limitations) ├── RELEASE-MANIFEST.json → release identity: tag, framework, artifact hashes, verdict, signing key └── package.json · package-lock.json · tsconfig.json · .mocharc.yml · .nvmrc · .gitattributes · .gitignore · LICENSE
Step-to-Section Cross-Reference21 verification steps mapped
| Step | Test Name | Report Section |
|---|---|---|
| 1 | Seed Hash Integrity | S1 |
| 2 | Next-Seed Pre-Commitment Chain | S1 |
| 3 | Hash Consistency Within Epoch | S1 |
| 4 | Nonce Audit | S1 |
| 5 | Path + Slot Recomputation | S1, S2, S3 |
| 6 | Client Seed Influence | S1, S2 |
| 7 | Multiplier Lookup | S3, S4 |
| 8 | Payout Math (betAmount × multiplier == winningAmount) | S3, S4 |
| 9 | Config Completeness (27 standard + WTF) | S3, S4 |
| 10 | House Edge / RTP Audit (analytical, per-config) | S4 (also scores the RTP claim rule) |
| 11 | Phase Labels | S3 |
| 12 | Epoch Size | S1, S3 |
| 13 | Anti-Circularity (Binomial Slot Probabilities) | S4 |
| 14 | Phase D — Client Seed Variation | S1, S2 |
| 15 | Phase E — WTF Mode Verification | S3, S4 |
| 16 | Simulation — Pass 1 Slot Uniformity (FWER) | S2, S4 |
| 17 | Simulation — Pass 2 Cherry-Pick Test | S4 |
| 18 | Bet-Size Invariance | S3, S4 |
| 19 | Multiplier Table Symmetry | S3 |
| 20 | Artifact Hash + Value-Domain Integrity | S7 |
| 21 | Epoch-Window Slot Fit (live drops vs binomial) | S4 |
Game Profile CoveragePlinko profile 1.0.2 · 39 checks
Every check in the ProvablyFair.org Plinko profile, version 1.0.2, is covered by a scored step, by a package check, or marked n/a with a reason — 26 by scored steps, 10 by package checks, 3 n/a. No check has partial coverage. The map ships in src/profile-map.ts and is written into each run’s verification results.
| Check | What it requires | Covered by | Coverage |
|---|---|---|---|
| PLINKO-01 | Every revealed seed hashes to its commitment | Step 1 | Full |
| PLINKO-02 | Next-seed pre-commitment chain, including the pre-capture link | Step 2 | Full |
| PLINKO-03 | One commitment and one client seed per epoch | Steps 3, 4 | Full |
| PLINKO-04 | Nonce contiguity — no gaps, duplicates or reuse | Step 4 | Full |
| PLINKO-05 | Audited population bound to code constants, not the dataset header | Steps 11, 12 | Full |
| PLINKO-06 | Client-seed control and sensitivity | Steps 6, 14 | Full |
| PLINKO-07 | Every captured path and slot recomputes | Step 5 | Full |
| PLINKO-08 | Bit-extraction bias bound, checked on the implementation’s own draw | Step 5 | Full |
| PLINKO-09 | Reference RNG provenance and known-input vectors | Package check: tests/plinko/rngSpecTests.ts (the 55 checks of the docs/ALGORITHM.md specification) and tests/plinko/rngTests.ts | Full |
| PLINKO-10 | Every paid multiplier matches the pinned table | Step 7 | Full |
| PLINKO-11 | Settlement — payout equals stake × multiplier, exactly | Step 8 | Full |
| PLINKO-12 | Bet-size invariance | Step 18 | Full |
| PLINKO-13 | Multiplier tables pinned by SHA-256 before any step scores | Step 20 | Full |
| PLINKO-14 | Multiplier-table symmetry | Step 19 | Full |
| PLINKO-15 | Coverage — every board present at the Phase A floor; never-paid cells enumerated | Step 9 | Full |
| PLINKO-16 | Wallet balance ledger reconciliation | — | N/A — the captured bet record carries no balance field, so there is no balance change to reconcile; payouts are checked against winningAmount in Step 8 |
| PLINKO-17 | WTF mode verified on every captured WTF drop | Step 15 | Full |
| PLINKO-18 | Anti-circularity — independent probabilities and an enumeration anchor | Steps 10, 13 | Full |
| PLINKO-19 | Effective edge against the claimed edge; the RTP claim rule | Step 10 | Full |
| PLINKO-20 | Simulation Pass 1 — replayed, not trusted | Step 16 | Full |
| PLINKO-21 | Cherry-pick test on a calibrated null | Step 17 | Full |
| PLINKO-22 | Live-sample slot fit (informational) | Step 21 | Full |
| PLINKO-23 | Framework fairness-integrity rows S5-1 … S5-8, kept as a package checklist (Section 5 reports the live integrity tests) | Package check: MANIFEST.md matrix | Full |
| PLINKO-24 | Player-verifier parity — the ProvablyFair.org verifier, hash-matched and executed | Package check: verifier-parity/, src/site-verifier.ts, scripts/check-site-verifier.mjs; executed in Step 5 | Full |
| PLINKO-25 | Player verification path with a worked bet | Package check: README.md, MANIFEST.md Verify a bet | Full |
| PLINKO-26 | Dataset pinned by SHA-256 before any step runs | Step 20 | Full |
| PLINKO-27 | Artifacts pinned in source and re-hashed | Step 20 | Full |
| PLINKO-28 | Release gate — zero-install npm test, pinned runtime, field-by-field reproduction diff | Package check: tests/verify.ts, src/diff.ts, src/diff-simulation.ts, npm test | Full |
| PLINKO-29 | Mutation battery with a declared survivor | Package check: tests/mutations.json, tests/mutations-extra.json, scripts/mutate.mjs | Full |
| PLINKO-30 | Limitations register and numbered evidence | Package check: MANIFEST.md | Full |
| PLINKO-31 | Dispersion guard — a slot histogram that fits too well fails too | Step 16 | Full |
| PLINKO-32 | Convergence traces bound to the replayed histograms; the chart re-rendered byte for byte | Step 16 | Full |
| PLINKO-33 | The committed run carries steps 1–21 once each, under their declared names | Package check: scripts/check-outputs.mjs | Full |
| PLINKO-34 | Run mode recorded; only a full run is accepted for publication | Package check: tests/verify.ts, scripts/check-outputs.mjs | Full |
| PLINKO-35 | Published hashes match the pins; every numeric value of the emitted artifacts inside a declared domain | Step 20 | Full |
| PLINKO-36 | Client-seed influence scored against the exact chance-coincidence null | Step 6 | Full |
| PLINKO-37 | Zero-install bundles compiled from the checked-in sources and pinned | Package check: scripts/build-standalone.mjs, scripts/check-standalone.mjs | Full |
| PLINKO-38 | Multi-window shape analysis of flagged seeds | — | N/A — Pass 2 is decided by an exact Poisson-binomial survival over all seeds, not by a count of flagged seed-board combinations, so there is no flag count to reinterpret |
| PLINKO-39 | Edge per stake range, where a casino publishes more than one | — | N/A — LIQD’s Plinko settings publish one house edge for every stake |
reproduce-audit.sh
All audit-package results reproduce from release v1.0.1, the pinned artifacts and the commands above. The dataset and multiplier-table hashes are checked before any step runs, so you are provably testing the same 10,100 drops and the same tables; the simulation, calibration, RTP claim table and emitted results are pinned by SHA-256 and re-checked on every run. Every npm test replays all 28,000,000 Pass 1 rounds from their saved seeds and re-executes Pass 2 for all 202 epochs.