Skip to main content
Audits
LIQD.com/Plinko
Provisional audit

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

LIQD: Plinko Audit

Plinko

Provisional

Independent verification, backed by open-source code.

PF-2026-LQ03Audited July 2026 · Framework 1.1 · Release v1.0.1July 2026 · Framework 1.1 · v1.0.1
AUDIT RESULTPassedPassed 21/21 · Hard fails 0 · Flags 0VERDICT: PROVABLY FAIR — Full Pass, as printed by npm test.
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
21 / 21Verification steps passed

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.

LIQD PLINKO$0.10 · 15 rows · High risk
620620×8383×2727×88×33×0.50.5×0.20.2×0.20.2×0.20.2×0.20.2×0.50.5×33×88×2727×8383×620620×
Bet #-NwBa_JjdoPkRL_dt9HVKNonce 0 · Phase D
50 real bets · Phase DBet 1 of 50
✓ Exact match
0.5×

The casino result, reproduced.

Casino outcome
Slot 10 · 0.5×
Our reproduction
Slot 10 · 0.5×
Recorded payout
$0.0500
Open source

Run this audit yourself

Open GitHub ↗

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.

npm testCopy includes clone & installView reproduction commands ↓
Plinko Audit Overview

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.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPassPROVABLY 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 VerifiedPass98.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 ParityPass100% — 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 SystemPass202 / 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 SeedPassPlayer-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 AnalysisPassOur 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 LogicPassAll 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 ModePassFourth risk level verified on all 2,000 captured drops — each recomputes at 13 rows and pays from the WTF table (Step 15)
Anti-CircularityPassBinomial 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 IntegrityPass29 fairness integrity tests across 11 categories — 27 pass, 2 remediated (input hygiene, Low — fixed by LIQD and re-verified), none open
DeterminismPassFull reproducibility confirmed — 10,100 / 10,100 drops recompute from the recorded inputs (Step 5)
Overall StatusPass
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 VerifiedPass
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 ParityPass
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 SystemPass
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 SeedPass
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 AnalysisPass
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 LogicPass
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 ModePass
Fourth risk level verified on all 2,000 captured drops — each recomputes at 13 rows and pays from the WTF table (Step 15)
Anti-CircularityPass
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 IntegrityPass
29 fairness integrity tests across 11 categories — 27 pass, 2 remediated (input hygiene, Low — fixed by LIQD and re-verified), none open
DeterminismPass
Full reproducibility confirmed — 10,100 / 10,100 drops recompute from the recorded inputs (Step 5)
Audit scope & reference

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
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 server seed hashing, pre-bet commitment, next-seed pre-commitment, reveal on rotation
Client Seed OriginPlayer-controlled seed, set by the capture at every rotation — 192 auditor-generated seeds and 10 custom pfaudit- seeds in Phase D
Seed HandlingClient seed control, nonce lifecycle, seed epoch rotation
RNG AnalysisHMAC-SHA256 per-row algorithm verification, bias analysis, external entropy assessment
Payout LogicMultiplier table accuracy (standard + WTF), exact stake × multiplier settlement, bet-size invariance (Phase C)
Live ParityIndependent path and slot recomputation vs live results across all five phases, plus the ProvablyFair.org verifier on the same 10,100 drops
RTP ValidationAnti-circularity proof, the RTP claim rule over all 28 boards, simulated RTP (28M rounds), cherry-pick probe (Pass 2)
Fairness IntegrityIntegrity 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.

Slot outcomes are determined cryptographically before the ball animation plays. The visual drop is cosmetic — the result is fixed at the moment the bet is placed.
Win Conditions

The win condition depends on which slot the ball lands in — and on the mode.

3 columns
OutcomeConditionExample
Jackpot (edge slot)Ball lands in slot 0 or slot rows16r / HIGH: 1000× — ~1 in 65,536 per side
Win (outer slots)Ball lands near an edge16r / HIGH slot 1: 130×
Partial return (standard)Ball lands near the centre16r / HIGH slot 8: 0.2×
Zero (WTF only)Ball lands in any of WTF's 10 middle slotsWTF slots 2–11: 0× — ~99.7% of drops
Standard mode always returns something — the lowest table value is 0.2×. WTF is different: only 4 of its 14 slots pay (1000× / 235× at the edges), and the other 10 pay nothing.
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
3 columns
ParameterValueNotes
Row Count8–16 (13 forced in WTF)Player selects; sets slot count = rows + 1
Risk Levels1 = LOW · 2 = MEDIUM · 3 = HIGH · 4 = WTFInteger field on the bet request; WTF is a first-class fourth level
House Edge1.00% advertisedExact per-board edge 0.84%–1.09% from the pinned tables
Proven RTP98.9063%–99.1602% (mean 99.0152%)Per board, from the pinned tables (S4); WTF 98.9990%
Configurations289 row counts × 3 risk levels + WTF
RNG AlgorithmHMAC-SHA256One 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.

4 columns
Seed TypeFormatExamplePurpose
Server Seed32-char hex (16 bytes)fc9a1d6fad00e083…Casino-provided randomness, revealed on rotation
Server Seed Hash64-hex SHA-256 digest3b34a0fce5732f2d…SHA-256(utf8(serverSeed)) — published before any bet
Client SeedPlayer-editable stringauditb1141320acdePlayer-contributed entropy
NonceInteger counter from 00Uniqueness per bet under a seed pair — this capture rotated every 50 bets (nonces 0–49)
Two different encodings, deliberately: the HMAC key is the seed hex-decoded to 16 raw bytes, but the commitment hashes the UTF-8 text of the 32-character hex string (the seed text is hashed, not its decoded bytes). Confirmed against all 202 revealed seeds and all 10,100 recomputed drops.
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]
5 columns
RowsRiskEdge Multiplier (slot 0/N)Centre MultiplierRTP
81 (LOW)5.6×0.5×98.9844%
83 (HIGH)29×0.2×99.0625%
161 (LOW)16×0.5×98.9987%
163 (HIGH)1000×0.2×98.9764%
13 (WTF)4 (WTF)1000×0×98.9990%
Multiplier tables are symmetric — slot k and slot (rows − k) always carry the same value, mirroring the binomial. Confirmed live: two symmetric 29× edge hits landed in the same epoch (slot 0 and slot 8 on the 8-row HIGH board). WTF's 1000× tails did not land in the capture (≈ 0.24 expected per tail); their value comes from the hash-pinned table.
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

2 columns
TestDescription
Server seed commit exists before playSHA-256 commitment shown before betting; 64-hex hash of the seed text
Server seed reveal matches commitSHA-256(utf8(revealed)) == hashedServerSeed — 202 / 202
Next-seed pre-commitment chainnextHashedServerSeed links every epoch to the next — consistent on 202 / 202 (201 transitions plus the pre-capture link)
Client seed controlPlayer-controlled and editable — the rotation call submits it and the server echoes it back
Nonce increments correctly0…49 per epoch, +1 per bet, resets on rotation
Full determinismSame inputs → same path and slot, 10,100/10,100

2. Randomness & Entropy Model

2 columns
TestDescription
RNG depends only on seeds + noncePath is a pure function of (serverSeed, clientSeed, nonce, rows)
No mixed entropy sourcesNo timestamps, Math.random, drand, block hash, or VRF
Unbiased mappingFirst four digest bytes as a uint32, mod 2 — 2 divides 2^32, so each bounce is an exact 50/50
No state leakageFresh HMAC per row (cursor = row + 1); no cross-bet memory

3. Verifier ↔︎ Live Parity

2 columns
TestDescription
Live outcomes match verifier10,100 / 10,100 paths + slots recomputed with 0 mismatches
Multi-phase verificationPhases 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

2 columns
TestDescription
Anti-circularity proofIndependent binomial × pinned tables = 98.9063%–99.1602% per config
House edge audit1.00% advertised; exact per-board edge 0.84%–1.09%
RTP claim ruleLIQD’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 correctnesswinningAmount = betAmount × multiplier exactly, in 1e-8 units, on all 10,100
Simulated RTP98.9249% over 28M rounds — consistent with the 99.0152% proven mean
Cherry-pick detection1 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

2 columns
TestDescription
Player can reproduce results offlineUsing the revealed seed, client seed, nonce, and row count
Verifier logic matches live logicSame HMAC-SHA256 per-row derivation, cursor = row + 1 — the ProvablyFair.org verifier reproduces all 10,100 drops (Step 5)
Verifier publicly accessibleProvablyFair.org verifier — no login required
No reliance on private APIsFully client-side verification
Fairness-integrity matrix29 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

High-Level Flow — illustrated flow
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-Reveal Model — illustrated flow

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)
nonce-lifecycle
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 pair
Determinism Guarantee

Given identical inputs, the output is always identical:

determinism
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 payout
Phase Map

The capture is engineered — five phases, each isolating one claim:

3 columns
PhaseDropsWhat it isolates
A5,400All 27 standard configs (rows 8–16 × risk 1–3), exactly 200 bets each
B2,000Deep sample of the highest-variance standard config (rows 16, risk 3)
C200Stake independence — identical logic at 100× the base stake (rows 16, risk 3)
D500Client-seed variation — 10 distinct auditor-chosen pfaudit- seeds, 18–19 bets per standard config
E2,000WTF mode — riskLevel = 4, rows forced to 13, PLINKO_WTF_ODDS
Why phases matter: a random sample measures averages, but a designed capture tests mechanisms. Each phase removes one degree of freedom an unfair implementation would need — every standard config at volume, the highest-variance standard board thickened, stake varied 100×, ten auditor-chosen seeds, and the WTF mode exercised on 2,000 drops.
Technical Glossary7 categories▶
Core Concepts
2 columns
TermDefinition
Provably FairA cryptographic system that allows players to mathematically verify that game outcomes were not altered. Relies on commit-reveal schemes and deterministic algorithms.
Commit-Reveal ProtocolA 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).
DeterminismThe 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 OriginHow 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 RuleRTP-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
2 columns
TermDefinition
Server SeedA 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 SeedSHA-256(utf8(serverSeed hex string)) — the text-hash commitment published before betting. Deliberately distinct from the HMAC key encoding.
Next Server Seed HashThe 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 SeedA player-editable string mixed into every drop as the first HMAC message field. Phase D verified 10 auditor-chosen pfaudit- seeds live.
NonceA sequential counter identifying a bet’s position under a seed pair — 0–49 in this capture. The middle HMAC message field.
CursorThe per-row index in the HMAC message, cursor = row + 1, running 1…rows. One fresh HMAC per row, one bit per HMAC.
EpochThe 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
2 columns
TermDefinition
HMAC-SHA256Keyed 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-256Secure Hash Algorithm 256-bit. Used for the seed commitment: SHA-256(utf8(serverSeed)) — the seed text, not its decoded bytes.
Hex DecodingConverting 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 RejectionDiscarding 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
2 columns
TermDefinition
VerifierA 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.
ParityDegree of matching between verifier and live game results. This audit: 10,100/10,100 exact path-and-slot matches — 100%.
Anti-CircularityProof 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 DetectionPass 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
2 columns
TermDefinition
Path / dropDetailsThe 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 SlotThe 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 CountPlayer-selected parameter (8–16) setting the number of peg rows, HMAC draws, and slots. WTF forces 13.
Risk LevelInteger parameter — { LOW: 1, MEDIUM: 2, HIGH: 3, WTF: 4 }. Selects the multiplier table only; the drop physics never change.
WTF ModeA 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 FireAn 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 DistributionB(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
2 columns
TermDefinition
PhaseA 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 FailAny 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 Datasetdata/plinko-master-10100bets.json — 10,100 drops and 202 fully revealed seed epochs across five phases.
Capture FlagA 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
2 columns
TermDefinition
plinko-master-10100bets.jsonThe 10,100-drop dataset used throughout this audit. SHA-256: 871c485ef0d55ca38af2cece4880abdc32fe8b62584468c5d4cfdfd01506fc95.
plinkoConfig.jsonThe 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.jsonOutput 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.jsonTwo-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.
01

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.

Commit-Reveal Verification

202 / 202seeds verified

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
202 / 202seeds 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
HMAC-SHA256 Pipeline — Seed inputs through per-row hash to slot position — illustrated flow
CheckResultEvidence
Seed hash integrityPass202 / 202 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1)
Next-seed chainPassConsistent on 201 / 201 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPassAll 50 bets in each of 202 epochs carry the same commitment — no mid-epoch seed change (Step 3)
Nonce sequencingPassNonces 0–49 in every epoch: 0 gaps, 0 duplicates, one client seed per epoch (Step 4)
Client seed originPassSet 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 controlPassPlayer can set or change the client seed at any time — 500 / 500 Phase D bets recompute under their custom seeds (Step 14)
Client seed influencePassA wrong client seed changes 1,008 / 1,010 sampled paths; the 2 matches sit inside the exact chance-coincidence null (Step 6)
Deterministic outputPassSame (serverSeed, clientSeed, nonce, rows) always produces the same path and slot — 10,100 / 10,100 confirmed (Step 5)
Seed hash integrityPass
202 / 202 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1)
Next-seed chainPass
Consistent on 201 / 201 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPass
All 50 bets in each of 202 epochs carry the same commitment — no mid-epoch seed change (Step 3)
Nonce sequencingPass
Nonces 0–49 in every epoch: 0 gaps, 0 duplicates, one client seed per epoch (Step 4)
Client seed originPass
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 controlPass
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 influencePass
A wrong client seed changes 1,008 / 1,010 sampled paths; the 2 matches sit inside the exact chance-coincidence null (Step 6)
Deterministic outputPass
Same (serverSeed, clientSeed, nonce, rows) always produces the same path and slot — 10,100 / 10,100 confirmed (Step 5)
✓  Commit-reveal verified

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.

Server Seed Commitment — illustrated flow
src/rng.ts · commitHash · Verified
// 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"));}
Result: 202 / 202 revealed seeds hash to their published commitment (Step 1). Zero mismatches. The dataset’s pre-capture record matches too: the seed active before the first audited epoch hashes to its commitment, and its pre-commitment links to epoch 0.

Real Example from Live Data:

plinko-master-10100bets.json · epoch 0 seed entry
{  "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:

verify-seed.js · Verified
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.

tests/steps/commitment.ts · Step 2 · Verified
  // ── 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++;  }
Result: 202 / 202 links consistent: 201 epoch transitions across all five phases plus the pre-capture link. The pre-committed next server-seed hash matched the promoted active hash at every rotation boundary — the chain has no breaks (Step 2).
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).

tests/steps/commitment.ts · Step 3 · Verified
  // ── 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++;  }
Result: 202/202 epochs — 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.

  • auditb1141320acde
  • auditc680c9435be0
  • pfaudit-1783390325646-152
  • pfaudit-1783390325646-153
  • pfaudit-1783390325646-161
Result: 202 distinct client seeds observed — one per epoch, including the 10 custom pfaudit- seeds (192 auditor-generated · 10 custom, treated as predictable). The rotation response echoes the submitted value. Evidence: plinko-master-10100bets.json
1.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.

Nonce Incrementation — illustrated flow
tests/steps/commitment.ts · Step 4 · Verified
  // ── 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++;  }
Result: 0 gaps, 0 duplicates across all 202 epochs; exactly 50 bets per epoch (min = max = 50). Epoch-0 boundaries: nonce 0 → path 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.

Deterministic Mapping — illustrated flow
src/rng.ts · revealPlinko · Verified
// 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 };}
Result: All 10,100 drops recompute: HMAC-SHA256 reproduction matches dropDetails and winningSlot on every bet. Zero mismatches, zero skipped — every server seed in the dataset was revealed.

Real Bet Verified:

plinko-master-10100bets.json · bet SLOjmT9dyuUEi1que576R · 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:

verify-drop.js · Verified
// 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.

tests/steps/determinism.ts · Step 6 · Verified
  // ── 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;
Result: 1,008/1,010 sampled bets (99.8%) produce a different path under a substituted client seed; the 2 collisions sit inside the exact Poisson-binomial null (0.5425 expected, P(X ≥ 2) = 0.103). Phase D: 500/500 recomputed across 10 custom seeds. The client seed is a genuine, material input.
Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Seed hash integrity (Step 1)202/202 revealed seedsPass
Pre-commitment chain (Step 2)202 / 202 links consistent (201 transitions + the pre-capture link)Pass
Hash consistency (Step 3)202/202 epochsPass
Nonce audit (Step 4)0 gaps, 0 duplicates, single seed per epochPass
Path + slot recomputation (Step 5)10,100/10,100Pass
Client seed influence (Step 6)1,008/1,010 sampled (99.8%)Pass
Epoch size (Step 12)202 epochs, exactly 50 bets eachPass
1.9Code References6 source files
2 columns
FilePurpose
tests/verify.ts21-step verification pipeline (Steps 1–6, 12 cover S1)
tests/steps/commitment.tsSteps 1–4: commit-reveal integrity checks
tests/steps/determinism.tsSteps 5–6: drop recomputation and client-seed influence
src/rng.tscommitHash · generateProvablyFairNumber · revealPlinko
src/loader.tsDataset loading, SHA-256 hash guard and the pinned population constants (10,100 bets · 202 seeds · 50 per epoch · phase plan)
capture/capture-plinko.reference.mjsIllustrative capture reference (phase plan, endpoint sequence, rotation discipline); the capture tool itself is not published
1.10Datasets UsedSource artifacts
Primary: data/plinko-master-10100bets.json
2 columns
PropertyValue
SourceLive capture from qa.liqd.com — 10,100 real drops across 5 phases, gameId fast-games-5, USDC
Capture environmentLIQD’s pre-production environment, qa.liqd.com · one authenticated session, one account
Total Records10,100 drops · 202 revealed seed epochs of exactly 50 bets each
Captured7 July 2026 (02:12–04:16 UTC, single session)
SHA-256871c485ef0d55ca38af2cece4880abdc32fe8b62584468c5d4cfdfd01506fc95
Pre-flight checksrc/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
2 columns
InvariantResult
SHA-256(utf8(serverSeed)) = hashedServerSeed for all 202 revealed seedsPass
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 epochsPass
Zero nonce gaps within any epochPass
Zero nonce duplicates within any epochPass
Exactly 50 bets per epoch (min = max = 50)Pass
Same inputs produce same path and slot for all 10,100 dropsPass
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 serverPass
1.12Reproduction InstructionsRun locally

Clone the repository, install dependencies, and run the verification suite:

reproduce-s1.sh · 5 lines · Verified
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 PASS
Steps 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 Size
02

RNG & 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.

Cryptographic Randomness Verification

28 / 28configs verified

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
28 / 28configs 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)
RNG Pipeline — HMAC-SHA256 → uint32 → mod 2 → left/right → slot accumulation — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — no hidden entropy
Entropy purityPassNo timestamps, external APIs, Math.random, or server-side state
Algorithm independently implementedPassOur implementation, written from the specification, reproduces all 10,100 drops; the ProvablyFair.org verifier matches the same 10,100 (Step 5)
Modulo biasPassu 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 verifiedPassServer 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 independencePass0 / 28 boards fail the lag-1 / runs screen at Bonferroni, 1M rounds each (Step 16)
Client seed influencePassA different client seed changes 1,008 of 1,010 sampled paths (Step 6)
RNG derived only from disclosed inputsPass
HMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — no hidden entropy
Entropy purityPass
No timestamps, external APIs, Math.random, or server-side state
Algorithm independently implementedPass
Our implementation, written from the specification, reproduces all 10,100 drops; the ProvablyFair.org verifier matches the same 10,100 (Step 5)
Modulo biasPass
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 verifiedPass
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 independencePass
0 / 28 boards fail the lag-1 / runs screen at Bonferroni, 1M rounds each (Step 16)
Client seed influencePass
A different client seed changes 1,008 of 1,010 sampled paths (Step 6)
✓  Disclosed Inputs Only, Unbiased by Construction

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.

2 columns
ComponentDetail
Hash functionHMAC-SHA256
Keyhex_decode(serverSeed) — 16 bytes
MessageclientSeed:nonce:cursor · cursor = 1 … rows
ExtractionFirst 4 digest bytes → big-endian uint32 (no rejection step)
Reductionuint32 mod 2 → 0 (left) or 1 (right)
Accumulationbits concatenate into dropDetails; slot = count of 1 bits
Output range0 to rows (inclusive) — rows + 1 slots
src/rng.ts · getProvablyFairHmacSalt · generateProvablyFairNumber · Verified
// 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;}
Result: Our implementation, written from the specification (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:

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness (committed via SHA-256(utf8(seed)) before betting)
Client SeedPlayerPlayer-contributed entropy (player-set, editable)
NonceSystemUniqueness per bet (increments automatically; 0–49 in this capture)
CursorSystemPer-row isolation (runs 1 … rows)
No mixed entropy sources detected. Run the same inputs multiple times — results are always identical. Pure HMAC-SHA256 must always produce identical outputs. 10,100/10,100 confirmed.

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
src/bias.ts · modBias · extractionCensus · Verified
/** 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++;    }  }
Result: Zero modulo bias. Each row is an exact, independent coin flip; all 10,100 drops reproduce from the first four bytes of each row’s digest.

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.

RNG Isolation — illustrated flow
Result: RNG isolation confirmed. No state leakage between rows, bets, or epochs. Each HMAC call is independently keyed and messaged.

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/.

2 columns
MetricValue
Configurations28 (27 standard + WTF)
Rounds per config1,000,000
Total rounds28,000,000
Mean proven RTP (authoritative)99.0152%
Mean simulated RTP98.9249% (committed run)
Chi-squared (Bonferroni α/28)0/28 fail
Serial independence (Bonferroni)0/28 fail
src/pass1.ts · simulateSlot · playPass1Config · Verified
/** 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));  }
Result: 28M rounds simulated. All 28 boards pass corrected chi-squared and serial independence; the simulated RTP is consistent with the proven mean. The committed artifact is the run the verdict scores (SHA-256 3d94a510…, listed in S7): the seed pair used for each board is recorded inside it and every 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.

src/stats.ts · lag1Autocorrelation · Verified
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;}
Result: 0/28 configurations fail serial independence at the corrected threshold. The live capture's pooled r₁ = 0.0899 is a between-config composition artifact (the capture runs in contiguous config blocks with win rates from ~0.3% to ~45.9%); within each configuration, live lag-1 autocorrelation is consistent with independence (1 of 28 configs exceeds |z| > 1.96 vs ~1.4 expected by chance). The authoritative evidence is the Pass 1 simulation.
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
6 columns
cursorHMAC[0:4]uint32% 2directionslot so far
11bc5cc26465,947,6860left0
20f9d6173261,972,3391right1
3dbdb6f8d3,688,591,2451right2
4f460931e4,099,969,8220left2
5ce51a3ed3,461,456,8771right3
6056cc63191,014,7051right4
70cf8aab5217,623,2211right5
8136bb6c5325,826,2451right6
Parity verified: Bet 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.
Live GamedropDetails = 01101111, slot = 6
Verifierpath = 01101111, slot = 6
Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary8 checks
3 columns
Verification AreaCoverageResult
Our implementation (Step 5)10,100 / 10,100 drops — written from the specificationPass
ProvablyFair.org verifier (Step 5)10,100 / 10,100 paths, slots and multipliers; paytable 365 / 365 cells equal to the pinned tablesPass
Key/commitment encoding (hex key vs utf8 commitment)Confirmed via 10,100-drop + 202-seed recomputationPass
Modulo bias analysis2 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 alonePass
Simulation chi-squared (Step 16)0 / 28 boards fail at Bonferroni α/28Pass
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 DPass
2.9Code References8 source files
2 columns
FilePurpose
src/rng.tscommitHash · generateProvablyFairNumber · revealPlinko — written from docs/ALGORITHM.md
docs/ALGORITHM.mdThe written specification of the commitment and the bit derivation
src/bias.tsThe bit-extraction bias bound and the census on our implementation’s own draw
src/simulate.ts · src/pass1.tsTwo-pass Monte Carlo (28M rounds + cherry-pick probe); pass1.ts is the loop Step 16 replays
src/stats.tsChi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test
tests/steps/determinism.tsSteps 5–6: drop recomputation (with the ProvablyFair.org verifier) and client-seed influence
tests/steps/simulation.tsSteps 16–17: the Pass 1 replay and gates, the Pass 2 re-execution
tests/plinko/*.tsMocha unit tests — 57 across rngSpecTests (the specification self-test), rngTests, rtpPinTests, calibrationTests and domainTests
2.10Verified Invariants9 invariants
2 columns
InvariantResult
HMAC-SHA256 output matches the live game for all 10,100 dropsPass
Key is the hex-decoded 16-byte seed; commitment hashes the seed text — both encodings confirmedPass
2 divides 2^32 — u mod 2 is an exact 50/50, zero modulo biasPass
No external entropy sources required for path computationPass
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 configsPass
Runs test within corrected bounds for all 28 configsPass
Simulated RTP (98.9249%) is consistent with the 99.0152% proven meanPass
Client-seed change produces a different path in 1,008/1,010 sampled dropsPass
2.11Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json — the committed two-pass run
Primary dataset: data/plinko-master-10100bets.json — 10,100 live drops for recomputation verification
Simulation output: outputs/simulation-results.json — Pass 1 (28M rounds, per-board chi-squared and serial tests, saved seeds and convergence traces) + Pass 2 (202 casino seeds × 10,000 nonces). The committed artifact is the run the verdict scores (SHA-256 3d94a510…): every npm test replays Pass 1 from its saved seeds inside Step 16, and Step 17 re-executes Pass 2
Multiplier config: plinkoConfig.json — hash-pinned tables used for the simulation's payout accounting (SHA-256 f8eaf67e1ffc63ee…)
2.12Reproduction InstructionsRun locally

Clone the repository, install dependencies, and replay the simulation:

reproduce-s2.sh · 5 lines · Verified
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 S2
S2-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.
03

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.

Live ↔︎ Verifier Parity

10,100 / 10,100drops matched

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
10,100 / 10,100drops matched
  • 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
Parity Verification Flow — seeds → recompute → compare → exact match — illustrated flow
10,100Live Bets Tested
100%Parity Rate
$0.10 & $10Bet Sizes Tested
0Mismatches
CheckResultEvidence
Path + slot recomputationPass10,100 / 10,100 exact match — full path and landing slot verified for every drop (Step 5)
ProvablyFair.org verifierPassRun 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 correctnessPassAll 10,100 drops: win amount = bet × multiplier exactly, compared as integers in 1e-8 units (Step 8)
Multiplier table integrityPassEvery paid multiplier matches the pinned tables at 1e-9 — 285 of the 365 cells were paid in the capture (Step 7)
Bet-size independencePassBet amount is absent from the RNG input — same path derivation at $0.10 and $10. Tested in Phase C (200 / 200)
Config completenessPassAll 27 standard configurations (8–16 rows × low/medium/high) plus WTF covered in live data
Multi-phase coveragePass5 structured phases: config coverage (A), deep sample (B), elevated stake (C), client seed variation (D), WTF mode (E)
Path + slot recomputationPass
10,100 / 10,100 exact match — full path and landing slot verified for every drop (Step 5)
ProvablyFair.org verifierPass
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 correctnessPass
All 10,100 drops: win amount = bet × multiplier exactly, compared as integers in 1e-8 units (Step 8)
Multiplier table integrityPass
Every paid multiplier matches the pinned tables at 1e-9 — 285 of the 365 cells were paid in the capture (Step 7)
Bet-size independencePass
Bet amount is absent from the RNG input — same path derivation at $0.10 and $10. Tested in Phase C (200 / 200)
Config completenessPass
All 27 standard configurations (8–16 rows × low/medium/high) plus WTF covered in live data
Multi-phase coveragePass
5 structured phases: config coverage (A), deep sample (B), elevated stake (C), client seed variation (D), WTF mode (E)
✓  Live game and verifier fully aligned

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.

Why Parity Matters — illustrated flow
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.

5 columns
PhaseBetsConfigBet AmountPurpose
A — Configuration coverage5,400All 27 standard$0.10Verify every standard config behaves correctly (200 bets each)
B — High-variance sampling2,00016r / risk 3$0.10Deep sample of the highest-variance standard config
C — Bet-size invariance20016r / risk 3$10.00Confirm bet amount is not in the RNG (100× stake)
D — Client-seed verification500All 27 standard (18–19 each)$0.10Confirm 10 custom pfaudit- seeds are genuinely consumed
E — WTF mode2,000WTF (13r / risk 4)$0.10Exercise the fourth risk level
Total: 10,100 bets across 202 epochs. Total wagered: $2,990.00 ($540.00 A + $200.00 B + $2,000.00 C + $50.00 D + $200.00 E).
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.

Path + Slot Recomputation (Step 5) — illustrated flow
tests/steps/determinism.ts · Step 5 · Verified
  // ── 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}`); }
Result: 10,100 / 10,100 drops verified, 0 skipped — every server seed in the dataset was revealed — and zero mismatches on either the path or the slot. ProvablyFair.org verifier: 4 / 4 files match their pins; 10,100 / 10,100 bets with 0 path, 0 slot, 0 multiplier and 0 row-count mismatches; paytable 365 / 365 cells equal.
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.

Payout Math (Step 8) — illustrated flow
tests/steps/payouts.ts · Step 8 · Verified
  // ── 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++;
Result: All 10,100 bets: 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.

2 columns
MetricCount
Total bets checked (incl. 2,000 WTF)10,100
Matching the pinned tables (1e-9)10,100
Matching neither table0
Result: Reference tables: 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.

tests/steps/standardization.ts · Step 19 · Verified
  // ── 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;
Result: 28/28 tables symmetric (27 standard + WTF) — 0 asymmetric slots. Live confirmation: 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.

tests/steps/standardization.ts · Step 18 · Verified
  // ── 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;
Result: Phase C: 200/200 drops at $10.00 recompute path-and-slot from revealed seeds — 0 mismatches. Stakes present in the dataset: $0.10 and $10.00; both resolve through the identical engine and the identical table.
Variance context: Phase C's live RTP was 73.20% over its 200 drops — small-sample variance on the highest-variance standard board (the 1000× tail was not hit), not payout behaviour. See S4's informational items.
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)
3 columns
StepProcessOutput
1HMAC-SHA256(hexDecode(serverSeed), "auditfdbdfd859cad:4:1") through cursor 1616 bounce bits
2Concatenate bits into the path1111111111011111
3winningSlot = count of 1 bits = 15Matches live game ✅
4Multiplier lookup: 16r / risk 3, slot 15130×
5Payout: 0.10000000 × 13013.00000000 ✅
HMAC-SHA256 recomputation · Verified
// 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 ✅
Parity verified: Bet 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.
Live Gameslot = 15, payout = 13.00000000
Verifierslot = 15, payout = 13.00000000
Technical Evidence & Verification5 sections
3.9Evidence Coverage Summary9 checks
3 columns
Verification AreaCoverageResult
Path + slot recomputation (Step 5)10,100 / 10,100 — full bit path verifiedPass
ProvablyFair.org verifier (Step 5)10,100 / 10,100; paytable 365 / 365 cellsPass
Payout math (Step 8)10,100 / 10,100 exact, in 1e-8 unitsPass
Multiplier table integrity (Step 7)All 28 configs — observed multipliers match the pinned tables at 1e-9Pass
Multiplier symmetry (Step 19)28/28 tables — slot k = slot (rows − k)Pass
Phase C recomputation (Step 18)200 / 200 at $10.00Pass
Config completeness (Step 9)27/27 standard + WTF present in live dataPass
Phase labels (Step 11)A 5,400 · B 2,000 · C 200 · D 500 · E 2,000 = 10,100Pass
WTF mode (Step 15)2,000/2,000 recomputed, 2,000/2,000 table-conformantPass
3.10Code References9 source files
2 columns
FilePurpose
tests/steps/determinism.tsStep 5: path + slot recomputation, with the ProvablyFair.org verifier
tests/steps/payouts.tsSteps 7–10: multiplier lookup, payout math, config completeness, RTP audit
tests/steps/dataset.tsSteps 11–12: phase labels, epoch size
tests/steps/wtf.tsStep 15: WTF-mode verification
tests/steps/standardization.tsSteps 18–20: bet-size invariance, multiplier symmetry, artifact hashes
src/rng.tsHMAC-SHA256 path computation
src/config.tsPinned-table lookup and RTP calculation
src/money.tsInteger 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
Primary dataset: data/plinko-master-10100bets.json — 10,100 live drops across 202 epochs (SHA-256 871c485ef0d55ca3…, loader-guarded)
Game config: plinkoConfig.json — 27 standard multiplier tables + PLINKO_WTF_ODDS; transcribed from LIQD’s client bundle (0pm_um5zrrwwn.js, qa.liqd.com/originals/plinko, 2026-07-06) — 285 of the 365 table cells are corroborated by live payouts, the other 80 come from the same pinned transcription. SHA-256 f8eaf67e1ffc63ee0dc21123447581c55f443c167dcb451b45ab7cd108fe712f
Verification output: outputs/verification-results.json — Steps 5, 7–9, 11, 15, 18–19
3.12Verified Invariants11 invariants
2 columns
InvariantResult
Computed path matches live dropDetails for all 10,100 dropsPass
Computed slot matches live winningSlot for all 10,100 dropsPass
The ProvablyFair.org verifier reproduces the path, slot and multiplier of all 10,100 dropsPass
winningAmount = betAmount × multiplier exactly (1e-8 units) for all 10,100 dropsPass
Every multiplier matches the pinned tables at 1e-9 — all 28 configurationsPass
Multiplier tables symmetric for all 28 configs: slot k = slot (rows − k)Pass
Phase C ($10.00) produces identical derivations to $0.10 betsPass
No hidden inputs beyond (serverSeed, clientSeed, nonce, rows)Pass
All 27 standard configs + WTF present in live dataPass
Phase D: 500/500 drops verified across 10 custom client seedsPass
Phase E: 2,000/2,000 WTF drops verified at forced 13 rowsPass
3.13Reproduction InstructionsRun locally

Clone the repository, install dependencies, and run the verification suite:

reproduce-s3.sh · 5 lines · Verified
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 PASS
S3-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 Symmetry
04

RTP & 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.

Return to Player Verification

99.0152%theoretical RTP (mean)

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
99.0152%theoretical RTP (mean)
  • 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
INDEPENDENT DERIVATION
Σ P(slot) × multiplier= 0.989063–0.991602
Binomial B(rows, 0.5) computed independently · Hash-pinned payout tables · All 28 configurations (27 standard + WTF) · Mean 99.0152%
99.0152%Theoretical RTP (mean of all 28 boards)
98.9249%Simulated — committed 28M run
1.00%House Edge (advertised)
28 / 28Boards meet LIQD’s published 99%
CheckResultEvidence
Anti-circularityPassBinomial 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 auditPass1.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)PassLIQD 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)Pass28M 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)Pass202 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 invariancePassBet 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 RTPPass98.9990% from the WTF table — same edge, extreme variance; 2,000 / 2,000 drops conform (Step 15)
Config completenessPassAll 27 standard boards plus WTF covered (Step 9)
Anti-circularityPass
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 auditPass
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 invariancePass
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 RTPPass
98.9990% from the WTF table — same edge, extreme variance; 2,000 / 2,000 drops conform (Step 15)
Config completenessPass
All 27 standard boards plus WTF covered (Step 9)
✓  RTP Claim Rule Passed on All 28 Boards

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:

Anti-Circularity Proof (Step 13) — illustrated flow
3 columns
ComponentFormulaSource
Slot probabilityP(slot=k) = C(rows, k) × 0.5^rowsBinomial distribution — pure math, not from casino
MultiplierPLINKO_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 rowsIndependent probability × pinned multiplier
Result98.9063%–99.1602% per configuration (mean 99.0152%)Exact identity from independent probabilities × pinned tables
5 columns
RowsRiskP(slot=0)Multiplier(slot=0)RTP
81 (LOW)0.39063%5.6×98.9844%
83 (HIGH)0.39063%29×99.0625%
113 (HIGH)0.04883%120×99.1602%
163 (HIGH)0.00153%1000×98.9764%
13 (WTF)4 (WTF)0.01221%1000×98.9990%
Result: For every row count 8–16, binomP(n,k)·2^n equals the exact integer C(n,k) from an independent Pascal recurrence (max error 0) and the masses sum to 1.0 (max |Σ−1| = 0) — the same binomP that theoreticalRTP uses, audited against no operator data. Step 10 then checks every board’s RTP against an exhaustive enumeration of all 2^rows paths (28 / 28). The theoretical RTP proof is non-circular.

Anti-Circularity Verification:

tests/steps/anti-circularity.ts · Step 13 · Verified
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:

src/config.ts · binomP + theoreticalRTP · Verified
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.

2 columns
ItemDetail
Claim99% RTP on every board — a flat 1.00% house edge
Where LIQD publishes itThe 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 tested28 — rows 8–16 × low, medium and high risk, plus WTF (13 rows)
MethodExact 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 · maximum98.906250% (rows 8 and 10, medium) · 99.160156% (rows 11, high)
Worst gap−0.093750 pp (rows 8 and 10, medium)
Boards beyond the band0 on the house side · 3 on the player side (rows 9 medium, 11 high, 12 high)
Scored onStep 10 (profile check PLINKO-19), rule enforced
ResultPass — the claim holds on every board
6 columns
BoardExact RTPRTPGap to 99%Side of the bandResult
8 rows, low1267/128098.984375%−0.015625 ppwithin ±0.1 ppPass
8 rows, medium633/64098.906250%−0.093750 ppwithin ±0.1 ppPass
8 rows, high317/32099.062500%+0.062500 ppwithin ±0.1 ppPass
9 rows, low1267/128098.984375%−0.015625 ppwithin ±0.1 ppPass
9 rows, medium1269/128099.140625%+0.140625 ppabove — player-favourablePass
9 rows, high317/32099.062500%+0.062500 ppwithin ±0.1 ppPass
10 rows, low5069/512099.003906%+0.003906 ppwithin ±0.1 ppPass
10 rows, medium633/64098.906250%−0.093750 ppwithin ±0.1 ppPass
10 rows, high317/32099.062500%+0.062500 ppwithin ±0.1 ppPass
11 rows, low5069/512099.003906%+0.003906 ppwithin ±0.1 ppPass
11 rows, medium507/51299.023438%+0.023438 ppwithin ±0.1 ppPass
11 rows, high5077/512099.160156%+0.160156 ppabove — player-favourablePass
12 rows, low20271/2048098.979492%−0.020508 ppwithin ±0.1 ppPass
12 rows, medium20273/2048098.989258%−0.010742 ppwithin ±0.1 ppPass
12 rows, high20299/2048099.116211%+0.116211 ppabove — player-favourablePass
13 rows, low4055/409698.999023%−0.000977 ppwithin ±0.1 ppPass
13 rows, medium10137/1024098.994141%−0.005859 ppwithin ±0.1 ppPass
13 rows, high20293/2048099.086914%+0.086914 ppwithin ±0.1 ppPass
14 rows, low81101/8192099.000244%+0.000244 ppwithin ±0.1 ppPass
14 rows, medium10137/1024098.994141%−0.005859 ppwithin ±0.1 ppPass
14 rows, high81083/8192098.978271%−0.021729 ppwithin ±0.1 ppPass
15 rows, low162203/16384099.000854%+0.000854 ppwithin ±0.1 ppPass
15 rows, medium162199/16384098.998413%−0.001587 ppwithin ±0.1 ppPass
15 rows, high32449/3276899.026489%+0.026489 ppwithin ±0.1 ppPass
16 rows, low324399/32768098.998718%−0.001282 ppwithin ±0.1 ppPass
16 rows, medium64873/6553698.988342%−0.011658 ppwithin ±0.1 ppPass
16 rows, high162163/16384098.976440%−0.023560 ppwithin ±0.1 ppPass
13 rows, WTF4055/409698.999023%−0.000977 ppwithin ±0.1 ppPass
All 28 boards—98.906250% – 99.160156%worst −0.093750 pp0 below the band · 3 abovePass — 28 / 28
src/rtp-settings.ts · RTP_CLAIM · computeRtpClaim · Verified
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();}
Result: Pass — no board returns more than 0.1 pp below LIQD’s published 99%: the worst gap is −0.093750 pp (rows 8 and 10 at medium risk), and three boards sit more than 0.1 pp above it, in the player’s favour. No board returns exactly 99.00% — 15 return slightly less and 13 slightly more — but none falls outside the band on the house side (Step 10). The full per-board table ships as 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.

tests/steps/payouts.ts · enumeratedRTP + Step 10 · Verified
/** * 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',
Result: Analytical RTP over 28 boards: min 98.9063% (rows 8 risk 2 and rows 10 risk 2 — an exact tie), max 99.1602% (rows 11 risk 3), mean 99.0152%, WTF 98.9990%. Max deviation from 99%: 0.1602 pp. Exhaustive 2^rows path enumeration agrees with the binomial RTP on all 28 boards to 8.76e-13 (tolerance 1e-12).
Why a range: RTP is stated per-config with its range, never as a flat 99.0000% — integer/decimal tables cannot hit exactly 99% at every row count, so the true edge floats around 1.00% by design.
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.

Simulation Pass 1 — Fresh Seeds (Step 16) — illustrated flow

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.

4 columns
BoardTheoretical RTPSimulated RTP (committed run)Deviation
8r · risk 198.9844%98.9275%−0.0569 pp
8r · risk 298.9063%98.6168%−0.2895 pp
8r · risk 399.0625%99.0433%−0.0192 pp
9r · risk 198.9844%98.9902%+0.0058 pp
9r · risk 299.1406%99.0052%−0.1354 pp
9r · risk 399.0625%99.0206%−0.0419 pp
10r · risk 199.0039%99.0805%+0.0766 pp
10r · risk 298.9063%98.8956%−0.0107 pp
10r · risk 399.0625%99.0225%−0.0400 pp
11r · risk 199.0039%99.0030%−0.0009 pp
11r · risk 299.0234%98.9260%−0.0974 pp
11r · risk 399.1602%99.1425%−0.0176 pp
12r · risk 198.9795%98.9766%−0.0029 pp
12r · risk 298.9893%99.0740%+0.0848 pp
12r · risk 399.1162%99.0813%−0.0349 pp
13r · risk 198.9990%99.0480%+0.0489 pp
13r · risk 298.9941%99.1283%+0.1342 pp
13r · risk 399.0869%99.2117%+0.1248 pp
14r · risk 199.0002%99.0046%+0.0044 pp
14r · risk 298.9941%98.7936%−0.2006 pp
14r · risk 398.9783%98.4904%−0.4878 pp
15r · risk 199.0009%99.0671%+0.0663 pp
15r · risk 298.9984%98.8487%−0.1498 pp
15r · risk 399.0265%98.7490%−0.2775 pp
16r · risk 198.9987%98.9785%−0.0202 pp
16r · risk 298.9883%98.9544%−0.0340 pp
16r · risk 398.9764%98.0790%−0.8974 pp
WTF (13r · risk 4)98.9990%98.7375%−0.2615 pp
tests/steps/simulation.ts · Step 16 (scored gate) · Verified
  // ── 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';
Result: 28M rounds. 0/28 chi-squared failures and 0/28 serial-independence failures at the Bonferroni threshold; every board’s simulated RTP within 6 SE of its recomputed theory (28 / 28). Slot distribution consistent with binomial expectations on every board, WTF included.

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.

Variance note: High-variance configs show the largest deviations because rare tails dominate at 1M rounds — and WTF is the extreme case: only 4 of 14 slots pay, the per-round payout SD is 20.5× the stake, and the standard error of its simulated RTP is ±2.05 pp — about 37× the 8-row LOW board's ±0.056 pp (the 27 standard boards' SEs run from ±0.03 pp on 14r/LOW to ±0.66 pp on 16r/HIGH). In the committed run the largest deviation is 16r/HIGH at −0.90 pp (z = −1.37 on its ±0.66 pp SE): its two 1000× corners landed 10 and 12 times against 15.3 expected each, and those 8.5 missing jackpots are −0.85 pp of it. WTF's −0.26 pp is z = −0.13 — its 1000× corners landed 123 and 130 times against 122.1 expected, and the shortfall sits in the 235× slots (1,588 and 1,537 vs 1,586.9). The slot-frequency chi-squared is the sensitive instrument and is clean on both (16r/HIGH 21.25 on 16 df, p = 0.169; WTF 12.59 on 13 df, p = 0.480). Matching the 8-row LOW board's ±0.056 pp would take ~1.3 billion WTF rounds; the exact closed-form 98.9990% is the authoritative figure, with the corrected uniformity tests as the scored evidence.
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.

2 columns
TestResult
Casino seeds tested202 (every revealed seed in the capture)
Nonces per seed10,000, on one board per seed
Scored window (50 nonces): seeds below z = −1.6451, against a calibrated 1.4099 ± 1.1721
Poisson-binomial survival P0.7603 — not significant
Scored window: early − late paired t−0.19
Extended window (5,000 nonces): seeds below z = −1.6456, against a calibrated 5.8556 ± 2.3708 (P = 0.5337)
Extended window: early − late paired t0.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
tests/steps/simulation.ts · Step 17 (scored window) · Verified
  // ── 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;
Result: Scored window: 1 seed below z = −1.645 against a calibrated 1.41 expected (P = 0.76), early − late t = −0.19; the extended window agrees (6 vs 5.86, P = 0.53; t = 0.60) and so does the nominal slot test (11 vs 9.6, P = 0.37). No sign of seed selection across the operator’s own 202 seeds. The test targets early-window selection; for the ten mixed-board Phase D epochs it evaluates one board per epoch rather than the schedule actually played.
Why this test exists: LIQD’s client seed is player-controlled and editable — when a player sets it after the commitment, the server cannot know it when it commits. Cherry-picking is therefore structurally contained by client-seed control and scanned statistically by this test; without an external entropy beacon it cannot be cryptographically eliminated. For the Phase D epochs 152–161 the defence is the statistics alone.
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.

3 columns
MetricPhase B ($0.10)Phase C ($10.00)
Bets2,000200
Config16r / risk 316r / risk 3
Paths recomputed2,000/2,000200/200
Multiplier tablePLINKO_FIXED_ODDS[16][3]PLINKO_FIXED_ODDS[16][3]
RNG pathHMAC-SHA256 · cursor = row + 1HMAC-SHA256 · cursor = row + 1
Result: Same RNG, same pinned table, same derivation at 100× the stake — 200 / 200 Phase C drops recompute.
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.

6 columns
Slot012–111213
Multiplier1000×235×0×235×1000×
P(slot)0.0122%0.1587%99.6582% (combined)0.1587%0.0122%
tests/steps/wtf.ts · Step 15 (core checks) · Verified
  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`);
Result: 2,000/2,000 WTF drops recompute at forced 13 rows; 2,000/2,000 multipliers match 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.

3 columns
ItemValueContext
Live RTP (all 10,100 drops)80.6843%Dominated by Phase C/E variance and unrealised 1000× tails — informational only
Phase A live RTP96.59%5,400 bets at $0.10 across all 27 standard configs
Phase B live RTP115.59%2,000 bets on 16r/HIGH — two 130× hits landed
Phase C live RTP73.20%200 bets at $10.00 on 16r/HIGH — expected spread at N=200
Phase D live RTP109.38%500 bets under custom seeds — incl. a 120× edge hit
Phase E live RTP70.50%2,000 WTF bets — six 235× wins, no 1000× tail
Payout-table cell coverage285/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 autocorrelationr₁ = 0.0899Between-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

3 columns
StepProcessOutput
1Commitment check on the revealed seedmatches hashedServerSeed ✅
2HMAC-SHA256 × 13 rowspath 1111101111111, slot 12 ✅
3Multiplier: WTF slot 12235×
4RTP check: Σ P(k) × PLINKO_WTF_ODDS[k]98.9990%
5Payout: 0.10000000 × 23523.50000000 ✅
RTP proof for the WTF table · Verified
// 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.
Parity verified: Bet 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.
Live Gameslot = 12, payout = 23.50000000
Verifierslot = 12, payout = 23.50000000
Technical Evidence & Verification5 sections
4.10Evidence Coverage Summary8 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Step 13)binomP·2^n = exact C(n,k) (Pascal recurrence) for rows 8–16, masses sum to 1Pass
House edge / RTP audit (Step 10)28 boards — 98.9063%–99.1602%; binomial vs exhaustive path enumeration agree to 8.76e-13Pass
RTP claim rule (Step 10)28 / 28 boards within the ±0.1 pp band or above it — worst −0.09375 ppPass
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 epochsPass
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 nominalPass
Bet-size invariance (Step 18)200/200 Phase C at $10.00Pass
WTF mode (Step 15)2,000/2,000 recomputed + table-conformantPass
4.11Code References11 source files
2 columns
FilePurpose
tests/steps/anti-circularity.tsStep 13: binomial probability independence
tests/steps/payouts.tsSteps 7–10: multiplier lookup, payout math, RTP audit
tests/steps/simulation.tsSteps 16–17: simulation integrity and the cherry-pick test (calibrated payout-weighted count)
src/rtp-claim.ts · src/rtp-settings.tsThe RTP claim rule (RTP-CLAIM 1.2.0, enforced) and LIQD’s published claim with the per-board table
tests/steps/wtf.tsStep 15: WTF-mode verification
tests/steps/standardization.tsStep 18: bet-size invariance
tests/steps/live-fit.tsStep 21: epoch-window slot fit of the live drops vs binomial (Fisher-combined)
src/simulate.tsTwo-pass Monte Carlo (28M rounds, fresh seeds); src/pass1.ts holds the Pass 1 loop that Step 16 replays
src/calibration.ts · src/calibrate.tsCalibrated null for the payout-weighted Pass 2 count (exact lattice convolution + Poisson-binomial) → outputs/calibration-results.json
src/stats.tscombination, chiSquaredTest (regularized-gamma p-value), lag1Autocorrelation, runsTest, inverseCriticalZ, normalCDF, binomialSurvival
src/config.tsbinomP · theoreticalRTP · pinned-table loader
4.12Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json — Pass 1 (28M rounds, saved seeds + convergence traces) + Pass 2 (202 seeds × 10K nonces). Committed run, SHA-256 3d94a510…: Pass 1 is replayed from its saved seeds on every npm test, Pass 2 is re-executed by Step 17 at verify time
RTP claim table: outputs/rtp-claim-table.json — LIQD’s published 99% against all 28 boards, each with exact RTP, gap and outcome · SHA-256 84f48588…
Calibration artifact: outputs/calibration-results.json — calibrated null for the payout-weighted Pass 2 count at both windows (SHA-256 53dabd47…), recomputed field by field by Step 17
Game config: plinkoConfig.json — 27 standard tables + PLINKO_WTF_ODDS, transcribed from LIQD’s client bundle; 285 of the 365 table cells are corroborated by live payouts, the other 80 come from the same pinned transcription. SHA-256 f8eaf67e1ffc63ee0dc21123447581c55f443c167dcb451b45ab7cd108fe712f
Primary dataset: data/plinko-master-10100bets.json — payout verification and Phase C/E evidence
4.13Verified Invariants14 invariants
2 columns
InvariantResult
Σ 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–16Pass
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_ODDSPass
0/28 configs reject at Bonferroni α/28 (chi-squared)Pass
0/28 serial-independence failures at the corrected thresholdPass
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-9Pass
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 constructionPass
Phase C ($10.00) derivations identical to $0.10 derivationsPass
4.14Reproduction InstructionsRun locally

Clone the repository, install dependencies, and run the verification suite:

reproduce-s4.sh · 5 lines · Verified
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.json
S4-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.
05

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.

Fairness Integrity Testing

27pass2remediated

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
27pass2remediated
  • 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?
4 / 4Race Conditions
4 / 4Input Validation
3 / 3Atomic Balance / Idempotency
2 / 2Broken Authorization
3 / 3Max-win Bypass
4 / 4Nonce Manipulation
3 / 3RNG / PF Verification
1 / 1Entropy / PF Verification
1 / 1Precision / Payout
1 / 1Event Replay
3 / 3Adversarial Seeds

Category Coverage

Race Conditions4/4
Input Validation4/4
Atomic Balance / Idempotency3/3
Broken Authorization2/2
Max-win Bypass3/3
Nonce Manipulation4/4
RNG / PF Verification3/3
Entropy / PF Verification1/1
Precision / Payout1/1
Event Replay1/1
Adversarial Seeds3/3
CheckResultEvidence
Nonce integrityPassServer-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 integrityPassIn 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 determinismPassEvery live round checked reproduces its path and slot through LIQD’s verifier (EXTRA-002, PF-001, PARAM-003)
Round & player isolationPassBets 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 integrityPassInjected 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 limitsRemediatedRanges 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
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 integrityPass
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 determinismPass
Every live round checked reproduces its path and slot through LIQD’s verifier (EXTRA-002, PF-001, PARAM-003)
Round & player isolationPass
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 integrityPass
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 limitsRemediated
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)
✓  29 of 29 Pass or Remediated

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.

3 columns
CategoryTestsWhat It Catches
Race Conditions4Parallel bets racing placement, settlement or seed rotation
Input Validation4Malformed, wrong-type or out-of-range row counts, risk levels, stakes and currencies
Atomic Balance / Idempotency3Double debits, orphaned debits and unsafe retry behaviour after a timeout or disconnect
Broken Authorization2Acting on another account, or after the session has ended
Max-win Bypass3Client-supplied outcome, payout or slot fields, and stake limits
Nonce Manipulation4Reused, skipped, zero, negative or oversized nonces supplied by the client
RNG / PF Verification3Provably fair fields, the nonce sequence, and paths and slots against LIQD’s verifier
Entropy / PF Verification1Server-seed rotation behaviour: fresh seeds, a new commitment and the nonce reset
Precision / Payout1Settlement arithmetic and wallet reconciliation over many rounds
Event Replay1Duplicate or replayed requests settling more or less than once
Adversarial Seeds3Unusual 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.

3 columns
StatusMeaningAction
PassThe service handled the scenario correctly: the control heldNone — recorded with its result
RemediatedA failed control that LIQD has since fixed, re-tested by ProvablyFair.org on the current buildRecorded with the fix and its re-test
WarningA real defect, or a property not yet proven, without a direct effect on funds or fairnessRecorded with its impact; open until a re-test resolves it
FailA defect affecting funds, fairness or securityRemediation and a re-test required
2 columns
ConditionConsequence
A substituted server seed is accepted against a published commitmentHard fail
A revealed seed fails its commitment and play continuesHard fail
Any settled drop's path cannot be reproduced from disclosed inputsHard fail
A credited payout deviates from the pinned table multiplierHard fail
Nonce reuse or gaps that could conceal discarded outcomesHard fail
Hard fail criteria: These conditions are absolute — a single occurrence fails the game regardless of every other result. None of them occurred in testing: no test altered, replayed or injected an outcome, and the one finding never touched the provably fair layer.

29 tests · 27 pass · 2 remediated

Race Conditions4/4
LIQD-PLINKO-EXTRA-001Pass

Seed 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-001Pass

Parallel 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-002Pass

Identical 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-003Pass

High-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-001LowRemediated

Row 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-002LowRemediated

Sub-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-003Pass

Maximum 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-001Pass

Currency 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-001Pass

Retry 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-002Pass

Orphaned-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-003Pass

Disconnect 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-001Pass

Another 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-003Pass

Session 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-001Pass

Outcome 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-001Pass

Stake 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-002Pass

Outcome 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-001Pass

Reused 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-002Pass

Skipped-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-003Pass

Zero 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-004Pass

Overflow-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-002Pass

Path 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-001Pass

Provably-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-002Pass

Nonce 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-004Pass

Nonce 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-001Pass

Payout 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-001Pass

Duplicate 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-001Pass

Client-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-002Pass

Unicode 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-003Pass

Rapid 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.

3 columns
CategoryTestsResult
Race Conditions4All pass
Input Validation42 pass · 2 remediated (PARAM-001, PARAM-002)
Atomic Balance / Idempotency3All pass
Broken Authorization2All pass
Max-win Bypass3All pass
Nonce Manipulation4All pass
RNG / PF Verification3All pass
Entropy / PF Verification1All pass
Precision / Payout1All pass
Event Replay1All pass
Adversarial Seeds3All pass
Method: Every test ran against LIQD’s running service on its pre-launch environment; remediations were re-tested on LIQD’s current build. Sections 1–4 verify the captured rounds offline — independent derivation, commitment and chain checks, nonce continuity and credit arithmetic — while Section 5 tests the live service around them. Per-test procedures, payloads and captures are retained under the engagement record and are not published — see 5.5.
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.

3 columns
TestsFindingStatus
PARAM-001 · PARAM-002Wrong-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
What is published here is coverage, not method. Each test in this section appears with its identifier and category, the control it assessed, the broad testing approach, the observed result, its impact and any limitation; a finding also carries its severity, where graded, and its remediation state. Request payloads, preconditions, step sequences, endpoints, timings and response captures are withheld: they show how to repeat a test, and add nothing a reader needs to judge its result. The full record is retained and is available to LIQD and to regulators on request.
Coverage published, method withheld: The classes of issue tested are standard and publicly documented. What would help someone repeat a test — volumes, timings, settings and request sequences — stays in the engagement record.
Point-in-time, per build: Each result is scoped to the build tested; both remediations were re-tested on LIQD’s current build.
Scope: The tests target the provably fair surface and the money path around it — seeds, rounds, placement and settlement. Wallet custody, account management and infrastructure hardening are outside their scope.
Test environment: Every test ran against LIQD’s pre-launch environment (qa.liqd.com), not the public site.
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.

Section 5 coverage · published on this page
Integrity tests ....................... 29Categories ............................ 11Pass .................................. 27Remediated ............................ 2Warning (open) ........................ 0Findings .............................. 1 · 1 remediated, 0 openProcedures and payloads ............... withheld (5.5)
Why the runner is not published: The tests run against LIQD’s real-money service, here its pre-launch environment. A published runner, with its payloads, would let anyone repeat them against LIQD, or against another operator built the same way; coverage and results are published instead.
06

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.

Independent Result Verification

4 Stepsto verify any bet

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)
4 Stepsto verify any bet
  • 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
From Bet to Independent Verification — 4-step flow — illustrated flow
Server SeedRevealed after rotation
Client SeedPlayer-controlled input
NonceRecorded bet sequence
Row Countthe configuration you played (8–16; WTF is always 13)
  1. 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. 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. 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. 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
6.1Step 1: Place a Plinko Bet

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.

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.

6.2Step 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 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 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.

6.3Step 3: Enter the Seeds in the Verify Tab

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.

The Verify tab filled in for that drop — client seed, the server seed revealed in step 2, nonce 0, 8 rows and Low risk.

6.4Step 4: Recompute & Confirm

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×.

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:

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureBy the live integrity tests (LIQD-PLINKO-EXTRA-002, S5); their output is not part of the audit packageDirectly — run unmodified by Step 5 on all 10,100 captured drops: same path, slot and multiplier on every one
If they disagreeSuspect the casino’s tool or a tampered roundThe 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 Node.js
// 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.11
6.9Python Verification Script

The same verifier in Python 3, standard library only — an independent check on the JavaScript version:

verify-plinko.py · Standalone Python
# 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.11
6.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.

2 columns
FileShows
E01-plinko-game-ui.pngThe Plinko interface on the pre-production environment — rows/risk controls and the per-slot bucket multipliers (Low/Medium/High tabs)
E02-provably-fair-panel.pngThe 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.jpgA 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.jpgA 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.jsonThe 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)
From Bet to Independent Verification — 4-step flow — illustrated flow
How it works
  1. 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. 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. 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. 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
6.1Step 1: Place a Plinko Bet

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.

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.

6.2Step 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 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 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.

6.3Step 3: Enter the Seeds in the Verify Tab

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.

The Verify tab filled in for that drop — client seed, the server seed revealed in step 2, nonce 0, 8 rows and Low risk.

6.4Step 4: Recompute & Confirm

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×.

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:

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureBy the live integrity tests (LIQD-PLINKO-EXTRA-002, S5); their output is not part of the audit packageDirectly — run unmodified by Step 5 on all 10,100 captured drops: same path, slot and multiplier on every one
If they disagreeSuspect the casino’s tool or a tampered roundThe 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 Node.js
// 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.11
6.9Python Verification Script

The same verifier in Python 3, standard library only — an independent check on the JavaScript version:

verify-plinko.py · Standalone Python
# 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.11
6.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.

2 columns
FileShows
E01-plinko-game-ui.pngThe Plinko interface on the pre-production environment — rows/risk controls and the per-slot bucket multipliers (Low/Medium/High tabs)
E02-provably-fair-panel.pngThe 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.jpgA 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.jpgA 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.jsonThe 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)
✓  Any Player Can Reproduce Plinko Results

Only disclosed inputs are used. Identical inputs always produce identical output.

07

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.

Open-Source Audit Repository

v1.0.1release audited

Repository Details

Prerequisites

  • Node.js 22.x (package.json engines and .nvmrc) — npm test needs 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)
v1.0.1release audited
github.com/ProvablyFair-org/liqd-plinko

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).

sh
git clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinkogit checkout v1.0.1npm ci # Run the entire audit end-to-end (57 unit tests + 21-step verification + output and release-manifest checks)npm test
Expected output
▶ [test] stale-build and report-count gate: node scripts/check-standalone.mjs[standalone] Source inventory, unit-test inventory and all three compiled entry points match the build manifest. ▶ [test] bundled unit specs: node test.js…# tests 57# pass 57# fail 0 ▶ [test] bundled verifier (verdict): node verify.js…  [REPLAY] Pass 1: 28/28 configs × 1,000,000 rounds regenerated from the saved seeds in 673.0 s…  Passed:     21/21  Hard fails: 0  Flags:      0   VERDICT: PROVABLY FAIR — Full Pass…  Reproduction vs the committed artifacts on Node 22.22.0: REPRODUCED (2 within tolerance, largest 5.55e-16 at steps[16].data.attestMaxAbsDiff) ▶ [test] committed outputs (check-outputs): node scripts/check-outputs.mjs  [check-outputs] OK: 21/21 steps, hardFail 0, verdict "PROVABLY FAIR — Full Pass"  [check-outputs] step identity OK: ids 1..21 each present exactly once, every name matching the 21 step(s) declared in tests/steps/  …  [check-outputs] Pass 1 replay OK: full, 28 configs × 1000000 rounds, 784 committed figures compared, 0 beyond tolerance, census 8c8402436292…  [check-outputs] calibrated null, PRIMARY window 50 nonces (the nonces played per seed in this capture — scored): expected 1.409877 vs observed 1, P(X>=obs)=0.760280  [check-outputs] calibrated null, EXTENDED window 5000 nonces (secondary): expected 5.855567 vs observed 6, P(X>=obs)=0.533730 ▶ [test] release manifest and release bar: node scripts/release-manifest.mjs --check  [release-manifest] OK: release bar met; 8 artifact hashes match disk, verdict "PROVABLY FAIR — Full Pass", scored 21/21 ── run-stages: test ── every stage ran  stale-build and report-count gate            ok  bundled unit specs                           ok  bundled verifier (verdict)                   exit 0 (verdict: Full Pass)  committed outputs (check-outputs)            ok  release manifest and release bar             ok  RESULT: exit 0 — every other stage passed; the exit code is the verifier's verdict code

Audit Reproducibility Pinning

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 in RELEASE-MANIFEST.json
Node Version
22.x (package.json engines 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 in tests/mutations-extra.json, one of them the declared survivor S00) — 79 / 79 behaved as declared in the package’s recorded run (npm run mutate, outside npm test because 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
3 columns
FileContentsSize
verification-results.jsonSteps 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.jsonThe numerics behind every figure quoted in the report, emitted by the scored steps and info items as machine values~13 KB
coverage-results.jsonObserved payout-cell coverage per board (285 of 365 cells; 7.41% of the summed RTP on cells never paid)~12 KB
rtp-claim-table.jsonThe RTP claim rule’s table — LIQD’s published 99% against all 28 boards, each with exact RTP, gap and outcome~5 KB
simulation-results.jsonPass 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.jsonCalibrated 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.htmlPer-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
3 columns
StepTest NameReport Section
1Seed Hash IntegrityS1
2Next-Seed Pre-Commitment ChainS1
3Hash Consistency Within EpochS1
4Nonce AuditS1
5Path + Slot RecomputationS1, S2, S3
6Client Seed InfluenceS1, S2
7Multiplier LookupS3, S4
8Payout Math (betAmount × multiplier == winningAmount)S3, S4
9Config Completeness (27 standard + WTF)S3, S4
10House Edge / RTP Audit (analytical, per-config)S4 (also scores the RTP claim rule)
11Phase LabelsS3
12Epoch SizeS1, S3
13Anti-Circularity (Binomial Slot Probabilities)S4
14Phase D — Client Seed VariationS1, S2
15Phase E — WTF Mode VerificationS3, S4
16Simulation — Pass 1 Slot Uniformity (FWER)S2, S4
17Simulation — Pass 2 Cherry-Pick TestS4
18Bet-Size InvarianceS3, S4
19Multiplier Table SymmetryS3
20Artifact Hash + Value-Domain IntegrityS7
21Epoch-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.

4 columns
CheckWhat it requiresCovered byCoverage
PLINKO-01Every revealed seed hashes to its commitmentStep 1Full
PLINKO-02Next-seed pre-commitment chain, including the pre-capture linkStep 2Full
PLINKO-03One commitment and one client seed per epochSteps 3, 4Full
PLINKO-04Nonce contiguity — no gaps, duplicates or reuseStep 4Full
PLINKO-05Audited population bound to code constants, not the dataset headerSteps 11, 12Full
PLINKO-06Client-seed control and sensitivitySteps 6, 14Full
PLINKO-07Every captured path and slot recomputesStep 5Full
PLINKO-08Bit-extraction bias bound, checked on the implementation’s own drawStep 5Full
PLINKO-09Reference RNG provenance and known-input vectorsPackage check: tests/plinko/rngSpecTests.ts (the 55 checks of the docs/ALGORITHM.md specification) and tests/plinko/rngTests.tsFull
PLINKO-10Every paid multiplier matches the pinned tableStep 7Full
PLINKO-11Settlement — payout equals stake × multiplier, exactlyStep 8Full
PLINKO-12Bet-size invarianceStep 18Full
PLINKO-13Multiplier tables pinned by SHA-256 before any step scoresStep 20Full
PLINKO-14Multiplier-table symmetryStep 19Full
PLINKO-15Coverage — every board present at the Phase A floor; never-paid cells enumeratedStep 9Full
PLINKO-16Wallet 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-17WTF mode verified on every captured WTF dropStep 15Full
PLINKO-18Anti-circularity — independent probabilities and an enumeration anchorSteps 10, 13Full
PLINKO-19Effective edge against the claimed edge; the RTP claim ruleStep 10Full
PLINKO-20Simulation Pass 1 — replayed, not trustedStep 16Full
PLINKO-21Cherry-pick test on a calibrated nullStep 17Full
PLINKO-22Live-sample slot fit (informational)Step 21Full
PLINKO-23Framework fairness-integrity rows S5-1 … S5-8, kept as a package checklist (Section 5 reports the live integrity tests)Package check: MANIFEST.md matrixFull
PLINKO-24Player-verifier parity — the ProvablyFair.org verifier, hash-matched and executedPackage check: verifier-parity/, src/site-verifier.ts, scripts/check-site-verifier.mjs; executed in Step 5Full
PLINKO-25Player verification path with a worked betPackage check: README.md, MANIFEST.md Verify a betFull
PLINKO-26Dataset pinned by SHA-256 before any step runsStep 20Full
PLINKO-27Artifacts pinned in source and re-hashedStep 20Full
PLINKO-28Release gate — zero-install npm test, pinned runtime, field-by-field reproduction diffPackage check: tests/verify.ts, src/diff.ts, src/diff-simulation.ts, npm testFull
PLINKO-29Mutation battery with a declared survivorPackage check: tests/mutations.json, tests/mutations-extra.json, scripts/mutate.mjsFull
PLINKO-30Limitations register and numbered evidencePackage check: MANIFEST.mdFull
PLINKO-31Dispersion guard — a slot histogram that fits too well fails tooStep 16Full
PLINKO-32Convergence traces bound to the replayed histograms; the chart re-rendered byte for byteStep 16Full
PLINKO-33The committed run carries steps 1–21 once each, under their declared namesPackage check: scripts/check-outputs.mjsFull
PLINKO-34Run mode recorded; only a full run is accepted for publicationPackage check: tests/verify.ts, scripts/check-outputs.mjsFull
PLINKO-35Published hashes match the pins; every numeric value of the emitted artifacts inside a declared domainStep 20Full
PLINKO-36Client-seed influence scored against the exact chance-coincidence nullStep 6Full
PLINKO-37Zero-install bundles compiled from the checked-in sources and pinnedPackage check: scripts/build-standalone.mjs, scripts/check-standalone.mjsFull
PLINKO-38Multi-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-39Edge 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
reproduce-audit.sh
# Clone and setupgit clone https://github.com/ProvablyFair-org/liqd-plinko.gitcd liqd-plinkogit checkout v1.0.1npm ci # Run the full audit end-to-end (57 unit tests + the 21 verification steps + output and release-manifest checks)npm test # Or run individual componentsnpm run verify     # 21-step verification from source — writes outputs/run/, never the committed resultsnpm run replay     # the 28M-round Pass 1 replay, stand-alone (read-only)npm run simulate   # a new 28M-round experiment into outputs/fresh/ under fresh seeds # View the committed resultscat outputs/verification-results.jsoncat outputs/rtp-claim-table.json
provablyfair.org/verify-bets/liqd
This is an open-source audit. Every scored finding, dataset, and line of verification code is publicly accessible. You don't need to trust us or the casino. Clone the repo, run the tests, and verify every claim yourself.
✓  Fully Reproducible

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.