
Plinko
Independent verification, backed by open-source code.
audit and dataset.
- Live bets verified
- 8,100
- 162 active seed pairs
- Verifier parity
- 100%
- 0 mismatched outcomes
- Simulation rounds
- 27M
- 27 configurations
- Proven RTP · bracket 0
- 99.9%
- Bracket 0 · 0.1% house edge
Verification pipeline
Real bets. Independently reproduced.
Placed anonymously at Duel.com. We rebuilt the game logic from scratch and matched all 8,100 recorded outcomes.
The casino result, reproduced.
- Casino outcome
- Slot 6 / 9
- Our reproduction
- Slot 6 / 9
- Recorded payout
- $0.0101
- Seed Hash IntegrityPass
- Next-Seed PromotionPass
- Hash Consistency Within EpochPass
- Nonce AuditPass
- Slot Recomputation (RNG + drand absent)Pass
- Client Seed InfluencePass
- Payout MathPass
- Multiplier Table ProvenancePass
- Phase C Code-Path EquivalencePass
- Zero Edge AuditPass
- Config CompletenessPass
- Epoch SizePass
- Multiplier Table + SymmetryPass
- Phase LabelsPass
- Dataset HashPass
- Scaling Edge AnalysisPass
- Probability IndependencePass
- Phase D — Client Seed VariationPass
- Simulation Results — Pass 1 IntegrityPass
- Simulation Results — Pass 2 Cherry-Pick TestPass
The repo is the credential. You don't have to trust us — every finding ships as code. Run npm test to re-run the full audit: 20 verification steps, 27M simulated rounds, 8,100 live bets re-verified.
This audit independently validates the Plinko game operated by Duel.com across five domains: deterministic outcome generation, entropy integrity, live-to-verifier parity, RTP mathematical accuracy, and fairness integrity testing. We placed 8,100 real bets across 162 seed pairs and independently verified every single outcome using our own implementation of the algorithm.
Audit Verdict
Tap a result to inspect the evidence.
| Check | Result | Reference |
|---|---|---|
| RTP Verified | Pass | 99.9% proven · 99.917% simulated (27M) · 0.1% house edge |
| Live ↔︎ Verifier Parity | Pass | 100% — 8,100 / 8,100 bets matched |
| Commit-Reveal System | Pass | SHA-256 verified, 162 / 162 seeds — commitment chain intact across all rotations |
| Client Seed | Pass | Browser-generated + player customizable — server commits before client seed is known |
| RNG Analysis | Pass | HMAC-SHA256 per-row — each bounce is an unbiased 50/50, no hidden inputs |
| Payout Logic | Pass | All 8,100 payouts verified — win_amount = bet × multiplier, exact to 8 decimal places |
| Scaling House Edge | Info | 0.1% house edge at bracket 0, scaling to 2.0% at extreme bet sizes. Thresholds vary by config — see S4.7 |
| Anti-Circularity | Pass | Binomial P(slot) × observed multipliers = 99.9% for all 27 configs — derivation uses no operator RTP figure |
| Integrity Checks | Pass | 15 standard fairness integrity tests — 14 pass, 1 N/A |
| Determinism | Pass | Full reproducibility confirmed |
Overall StatusPass
RTP VerifiedPass
Live ↔︎ Verifier ParityPass
Commit-Reveal SystemPass
Client SeedPass
RNG AnalysisPass
Payout LogicPass
Scaling House EdgeInfo
Anti-CircularityPass
Integrity ChecksPass
DeterminismPass
What Was Audited
✓ Audited
- The RNG algorithm is deterministic and verifiable
- Server seeds are cryptographically committed via SHA-256 before play
- Client seed is browser-generated and players can customize it
- Nonces increment correctly and are never reused
- Slot outcomes are computed via HMAC-SHA256 with one call per row
- Slot positions are reproducible from server seed, client seed, nonce, and row count
- Payout logic matches the published multiplier tables for all 27 configurations
- Theoretical RTP is 99.9% across all configurations (0.1% house edge at bracket 0)
- Bet amount does not influence the RNG or slot outcome
- Players can independently verify every bet
— What Audit Excludes
- Infrastructure or server security
- Wallet, payments, or operational systems outside game logic
- Rakeback layer — 99.9% is the certified RTP; rakeback is operator-side
- Scaling house edge at bet amounts above bracket 0 — see S4.7 for full analysis
- Cross-account sampling
- Max win cap enforcement — not embedded in game logic
- The RNG algorithm is deterministic and verifiable
- Server seeds are cryptographically committed via SHA-256 before play
- Client seed is browser-generated and players can customize it
- Nonces increment correctly and are never reused
- Slot outcomes are computed via HMAC-SHA256 with one call per row
- Slot positions are reproducible from server seed, client seed, nonce, and row count
- Payout logic matches the published multiplier tables for all 27 configurations
- Theoretical RTP is 99.9% across all configurations (0.1% house edge at bracket 0)
- Bet amount does not influence the RNG or slot outcome
- Players can independently verify every bet
- Infrastructure or server security
- Wallet, payments, or operational systems outside game logic
- Rakeback layer — 99.9% is the certified RTP; rakeback is operator-side
- Scaling house edge at bet amounts above bracket 0 — see S4.7 for full analysis
- Cross-account sampling
- Max win cap enforcement — not embedded in game logic
- Outcomes are deterministic and reproducible from the recorded inputs
- Live game results match independent recomputation for the verified sample (8,100 / 8,100)
- Slot distribution follows binomial B(rows, 0.5) for all 27 configurations
- RTP is proven analytically: binomial P(slot) × multiplier = 99.9% for all 27 configurations
- Client seed is a genuine, browser-generated input that materially influences results (84.2% slot change rate)
- The house edge is 0.1% at bracket 0 as documented
- All standard fairness integrity checks passed at audit time
- Progressive house edge is disclosed and documented — 191 bet-size brackets per config, scaling from 0.1% to 2.0%
What Audit Covers
| Area | Description |
|---|---|
| Commit-Reveal System | SHA-256 server seed hashing, pre-bet commitment, reveal on rotation |
| Client Seed Origin | Player-controlled seed, browser-generated — server commits before your seed is known |
| Seed Handling | Client seed control, nonce lifecycle, seed pair rotation |
| RNG Analysis | HMAC-SHA256 per-row algorithm verification, bias analysis, external entropy assessment |
| Payout Logic | Multiplier table accuracy, house edge verification, bet-size invariance (Phase C) |
| Live Parity | Independent slot recomputation vs live game results |
| RTP Validation | Anti-circularity proof, simulated RTP (27M rounds), cherry-pick detection (Pass 2) |
| Fairness Integrity | Standard integrity matrix — 15 tests across commit-reveal, determinism, payout, isolation, and parameter enforcement |
What Audit Guarantees
- Outcomes are deterministic and reproducible from the recorded inputs
- Live game results match independent recomputation for the verified sample (8,100 / 8,100)
- Slot distribution follows binomial B(rows, 0.5) for all 27 configurations
- RTP is proven analytically: binomial P(slot) × multiplier = 99.9% for all 27 configurations
- Client seed is a genuine, browser-generated input that materially influences results (84.2% slot change rate)
- The house edge is 0.1% at bracket 0 as documented
- All standard fairness integrity checks passed at audit time
- Progressive house edge is disclosed and documented — 191 bet-size brackets per config, scaling from 0.1% to 2.0%
References
Plinko — Game Rules7 sections▶
Plinko is a ball-drop game on a triangular peg board. You pick the number of rows (8–16) and a risk level (low, medium, or high). 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.
How to Play
1. Choose row count — Select between 8 and 16 rows. More rows means more bounces and more possible slot positions.
2. Choose risk level — Select low, medium, or high. Higher risk shifts payout weight toward edge slots.
3. Enter bet amount — Choose how much to wager.
4. Drop the ball — The ball bounces through the peg board and lands in a slot.
5. Outcome — Payout = bet amount × multiplier for the landing slot.
Win Conditions
The win condition in Plinko depends on which slot the ball lands in.
| Outcome | Condition | Example (16r/high) |
|---|---|---|
| Jackpot (edge slot) | Ball lands in slot 0 or 16 | 1009.33× — ~1 in 65,536 chance |
| Win (outer slots) | Ball lands near edges | Slot 4 → 4.04× |
| Partial return | Ball lands near center | Slot 8 → 0.20× |
Risk vs Reward
The core mechanic of Plinko is the tradeoff between row count, risk level, and payout shape.
- Row count controls granularity — more rows means more possible slots and higher maximum multipliers at the edges
- Risk level controls payout shape — low risk has flatter payouts; high risk concentrates value at edge slots with a 0.2× center floor
- RTP is constant — all 27 configurations have the same theoretical RTP (99.9% at bracket 0) regardless of rows or risk
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Row Count | 8–16 | Player selects; determines slot count |
| Risk Levels | Low, Medium, High | Affects multiplier table, not slot probabilities |
| House Edge | 0.1% (bracket 0) | Progressive — increases at higher bet amounts |
| Theoretical RTP | 99.9% | Verified across all 27 configurations |
| Configurations | 27 | 9 row counts × 3 risk levels |
| RNG Algorithm | HMAC-SHA256 | One HMAC call per row; key = hex-decoded server seed |
Seed Formats
Every Plinko bet uses three cryptographic inputs to generate the result.
| Seed Type | Format | Example | Purpose |
|---|---|---|---|
| Server Seed | 64-char hex (32 bytes) | 5de225f630d2de83… | Casino-provided randomness |
| Client Seed | Alphanumeric string | MAAxNnpj1uB4AobA | Player-contributed entropy |
| Nonce | Integer (0, 1, 2…) | 23 | Ensures uniqueness per bet within epoch |
Multiplier Tables & Payout
Payouts in Plinko are determined by the landing slot, row count, and risk level. The multiplier is calibrated so that the expected return is approximately 99.9% across all 27 configurations at bracket 0.
win_amount = bet_amount × multiplier_table[risk][rows][slot]| Rows | Risk | Edge Multiplier (slot 0/N) | Center Multiplier | RTP |
|---|---|---|---|---|
| 8 | Low | 5.65× | 0.50× | 99.9% |
| 8 | High | 29.25× | 0.20× | 99.9% |
| 16 | Low | 16.15× | 0.50× | 99.9% |
| 16 | High | 1009.33× | 0.20× | 99.9% |
Why Provably Fair Matters▶
Traditional online casinos require players to trust that games are fair. Provably fair systems eliminate this trust requirement by allowing players to mathematically verify that outcomes were not manipulated. In a Provably Fair system:
- The casino commits to a result before the player bets
- The player contributes randomness that the casino cannot predict
- Anyone can verify the outcome after the fact
High-Level Overview8 sections▶
Checklist Reference
Based on the ProvablyFair.org Audit Execution Checklist, here are the tests covered under this audit document.
1. Commit-Reveal System & Seed Handling
| Test | Description |
|---|---|
| Server seed commit exists before play | SHA-256 hash shown to player before betting |
| Server seed reveal matches commit | SHA-256(hexDecode(revealed)) = committed hash |
| Client seed control | Player can set/change client seed via rotation UI |
| Nonce increments correctly | Starts at 0, +1 per bet, resets at epoch boundary |
| Full determinism | Same inputs → same result |
2. Randomness & Entropy Model
| Test | Description |
|---|---|
| RNG depends only on seeds + nonce | No external inputs — slot is a pure function of (server seed, client seed, nonce) |
| No mixed entropy sources | No timestamps, Math.random, etc. |
| Unbiased mapping | int % 2 on uint32 has zero modulo bias |
| No state leakage | RNG isolated per round — each row uses unique cursor |
3. Verifier ↔︎ Live Parity
| Test | Description |
|---|---|
| Live outcomes match verifier | 8,100 / 8,100 slots recomputed with 0 mismatches |
| Multi-phase verification | Phases A (all configs), B (deep sample), C (bet-size check), D (client seed) |
| Bet-size invariance | $10 bets produce same slots as $0.01 bets |
4. Game Logic & RTP Validation
| Test | Description |
|---|---|
| Anti-circularity proof | Binomial P(slot) × observed multipliers = 99.9% |
| House edge audit | 0.1% confirmed across all 27 configs at bracket 0 |
| Payout rules correctness | Win amount matches multiplier × bet within 1e-8 |
| Simulated RTP convergence | 27M rounds converge on theoretical 99.9% |
| Cherry-pick detection | 10 flags, binomial p = 0.773 (not significant) |
5. Fairness Integrity & Player Verification
| Test | Description |
|---|---|
| Player can reproduce results offline | Using seeds + nonce + row count |
| Verifier logic matches live logic | Same HMAC-SHA256 per-row algorithm |
| Verifier publicly accessible | ProvablyFair.org verifier — no login required |
| No reliance on private APIs | Fully client-side verification |
| 20 fairness integrity tests | Commit-reveal, determinism, payout, distribution, anti-manipulation |
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, and bet amount
2. Seeds Combined — HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) for each row
3. RNG Output — Each row produces a uint32; int % 2 determines left (0) or right (+1)
4. Slot Accumulation — Sum of right-bounces across all rows = final slot (0 to rows)
5. Multiplier Lookup — final_slot maps to multiplier via scaling_edge[0].multipliers[rows][risk]
6. Payout Result — win_amount = bet_amount × payout_multiplier
Provably Fair Model
Provably fair gambling systems use cryptographic primitives to guarantee the integrity of outcomes. The model relies on three components: a server seed committed via hash before play, a player-controlled client seed, and an incrementing nonce. These inputs are combined using HMAC-SHA256 to produce deterministic, verifiable results. This section documents the global provably fair architecture used by almost all casinos and all relevant games.
Commit-Reveal Model
The Commit-Reveal model is integral to ensuring fairness and transparency in online gambling. This model involves several key phases:
Commit Phase:
Before any bets are placed, the casino generates a random server seed. To prove its authenticity and prevent later manipulation, only the SHA-256 hash of the hex-decoded seed is sent to the player. 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 via HMAC-SHA256 to compute each row's outcome. Each epoch uses the same server seed and client seed pair.
Reveal Phase:
After the epoch ends, the server rotates seeds and reveals the plaintext server seed. The player can now independently verify SHA-256(hexDecode(serverSeed)) = committedHash.
Verify Phase:
Anyone can recompute every bet's slot from the revealed server seed, client seed, and nonce using the published HMAC-SHA256 algorithm.
Player-Controlled Client Seed
The player's client seed is generated by the browser and submitted to the server via the seed rotation UI. Players can set their own client seed at any time. This ensures:
- The casino cannot predict the full RNG input
- Players contribute entropy that they control
- Results depend on both parties' inputs
Nonce Lifecycle
The nonce is a counter that increments with each bet within an epoch:
- Starts at 0 for each new server seed
- Increments by exactly 1 per bet
- Never reused within the same seed epoch
- Resets to 0 when the server rotates to a new server seed
Seed Epoch: (serverSeed, clientSeed) Bet 1: nonce = 0 → Slot ABet 2: nonce = 1 → Slot BBet 3: nonce = 2 → Slot C ...[Player rotates seed — epoch complete]Next bet: nonce = 0 → Slot X (new seed pair)Determinism Guarantee
Given identical inputs, the output is always identical:
HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) → Always same hashSame hash per row → Always same left/right bounceSame bounces across all rows → Always same final slotSame slot + same config → Always same payoutTechnical Glossary7 categories▶
Core Concepts
| Term | Definition |
|---|---|
| Provably Fair | A cryptographic system that allows players to mathematically verify that game outcomes were not altered. Relies on commit-reveal schemes and deterministic algorithms. |
| Commit-Reveal Protocol | A two-phase process in which the casino commits to a result (by showing its hash) before the player bets, then reveals the actual value after the bet. |
| Determinism | The property that identical inputs always produce identical outputs. Same server seed, client seed, nonce, and row count must always generate the same slot position. |
| Client Seed Origin | The method by which the client seed is generated. Full Pass: browser-generated default + player customizable seed. Conditional Pass: server-assigned default + player customizable seed. Hard Fail: player cannot set own seed. |
Seed System
| Term | Definition |
|---|---|
| Server Seed | A random 32-byte value generated by Duel.com, transmitted as a 64-character hex string. Hashed and shown to players before betting, revealed after epoch rotation. |
| Client Seed | A random value controlled by the player, generated by the browser. Players can change it at any time via the seed rotation UI. |
| Nonce | A sequential counter that increments per bet within an epoch. Resets to 0 when the server rotates to a new server seed. |
| Hashed Server Seed | SHA-256(hexDecode(serverSeed)) — the commitment hash shown before betting. After rotation, players verify the revealed seed produces this hash. |
| Cursor | Zero-based row index (0 to rows−1) used in the HMAC message. Each cursor value produces one row's left/right bounce. |
Cryptographic Functions
| Term | Definition |
|---|---|
| HMAC-SHA256 | Hash-based Message Authentication Code using SHA-256. Duel Plinko uses HMAC-SHA256 with the hex-decoded server seed as key and clientSeed:nonce:cursor as message. One call per row. |
| SHA-256 | Secure Hash Algorithm 256-bit. Used for server seed commitment: SHA-256(hexDecode(serverSeed)) = serverSeedHashed. |
| Hex Decoding | Converting a 64-character hex string to 32 raw bytes. Critical for the HMAC key — using the hex string as UTF-8 produces wrong outputs. |
Verification Terms
| Term | Definition |
|---|---|
| Verifier | A tool that independently calculates slot outcomes using provided seeds, nonce, and row count. The ProvablyFair.org verifier is built from the audit codebase. |
| Parity | Degree of matching between verifier and live game results. 100% parity = every slot matches. This audit: 8,100/8,100 exact match. |
| Anti-Circularity | Proof that the RTP derivation uses no operator-supplied RTP figure. Independently-computed binomial B(rows, 0.5) probabilities × the published payout table = 99.9%. |
Game Mechanics
| Term | Definition |
|---|---|
| Slot | The ball's final landing position (0 to rows). Slot = count of right-bounces across all rows. Each slot has a corresponding payout multiplier. |
| Row Count | Player-selected parameter (8–16) determining the number of peg rows and HMAC calls per bet. More rows = more possible slots. |
| Risk Level | Player-selected parameter (low, medium, high) that determines the multiplier table. Higher risk = more value at edge slots, less at center. |
| Binomial Distribution | B(rows, 0.5) — the probability distribution for slot outcomes. P(slot = k) = C(rows, k) × 0.5^rows. Each row is an independent 50/50 event. |
| Progressive House Edge | Duel.com applies higher house edges at larger bet amounts via 191 bet-size brackets per config. Bracket 0 (0.1% edge) covers bets up to ~$336 for the most restrictive config. |
Audit Terms
| Term | Definition |
|---|---|
| Epoch | A sequence of bets using the same server seed and client seed pair. Ends when the player rotates seeds. |
| Phase | A structured data collection segment. Phase A: all 27 configs. Phase B: deep 16r/high sample. Phase C: $10 bet-size check. Phase D: client seed verification. |
| Cherry-Pick Detection | Test for selective seed deployment. Pass 2 simulation runs 162 revealed seeds × 10,000 nonces, comparing early vs late distributions to detect biased seed selection. |
| Master Dataset | data/plinko-master-8100bets.json — the primary dataset containing all 8,100 bets and 166 seed entries across four phases. |
Data Formats
| Term | Definition |
|---|---|
| plinko-master-8100bets.json | The 8,100-bet dataset used throughout this audit. SHA-256: 3cf9359d88220bc800bb32edfe04399f55b909302262928ee3a0582715635267. |
| plinkoConfig.json | Duel.com's game configuration file containing all 27 multiplier tables, 191 bet-size brackets per config, and probability arrays. Captured from https://duel.com/api/v2/games/plinko/config during the April 2026 audit window. SHA-256: 78f0a39201a8c24fd1577143732e004f77e27d4d36efeedc6b3f7b93b2078fed. |
| verification-results.json | Output of the 20-step verification suite. Contains all step results, seed hash checks, slot recomputation logs, and simulation artifact verification. |
| simulation-results.json | Two-pass simulation output: Pass 1 (27M rounds, fresh seeds) and Pass 2 (162 casino seeds × 10K nonces, cherry-pick detection). |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Plinko game on Duel.com is generated from four inputs: server seed, client seed, nonce, and row count. The casino commits to its server seed by publishing a SHA-256 hash before you place any bets. After you rotate your seed, the server reveals the actual seed — and anyone can verify that the hash matches. This cryptographic commitment makes it impossible for the casino to secretly change your outcome after you bet.
The server commits before you play. Revealed seeds reproduce the exact result.
What we verified
- Casino commits to the server seed hash before any bet is placed
- Client seed is browser-generated — server cannot know it at commitment time (Full Pass origin)
- Players can set or change their client seed at any time via the rotation UI
- Nonce increments by 1 per bet with no gaps or duplicates across 162 seed pairs
- Slot positions are fully determined by the seed inputs before the ball drops
- Identical inputs always produce the same slot — confirmed across all 8,100 bets
- Your client seed is a genuine input — changing it changes the outcome
What this means for you
- The casino cannot change where the ball lands after you bet
- You contribute randomness the server cannot predict or pre-select against
- Every bet is unique — the nonce ensures no two bets share an outcome
- Any result can be independently verified using the public tools and repo
- Outcomes are tamper-proof and verifiable even months later
- Cherry-picking favourable seeds is structurally impossible
- Casino commits to the server seed hash before any bet is placed
- Client seed is browser-generated — server cannot know it at commitment time (Full Pass origin)
- Players can set or change their client seed at any time via the rotation UI
- Nonce increments by 1 per bet with no gaps or duplicates across 162 seed pairs
- Slot positions are fully determined by the seed inputs before the ball drops
- Identical inputs always produce the same slot — confirmed across all 8,100 bets
- Your client seed is a genuine input — changing it changes the outcome
- The casino cannot change where the ball lands after you bet
- You contribute randomness the server cannot predict or pre-select against
- Every bet is unique — the nonce ensures no two bets share an outcome
- Any result can be independently verified using the public tools and repo
- Outcomes are tamper-proof and verifiable even months later
- Cherry-picking favourable seeds is structurally impossible
Server seed committed before betPass
Client seed originPass
Client seed controlPass
Nonce sequencingPass
Hash consistency within seed pairPass
Seed hash integrityPass
Deterministic outputPass
Client seed participationPass
All 162 revealed seeds hash-verified. Every seed rotation was verified — the next seed the casino pre-committed always matched what was actually used. Outcomes are fully deterministic — the same server seed, client seed, nonce, and row count always produce the same slot. The casino cannot change your result after you bet.
| Check | Result | Evidence |
|---|---|---|
| Server seed committed before bet | Pass | SHA-256 hash of server seed published before play — casino cannot change randomness after betting |
| Client seed origin | Pass | Browser-generated (Full Pass) — server commits before client seed is known |
| Client seed control | Pass | Player can set/change client seed via rotation UI at any time |
| Nonce sequencing | Pass | Sequential within each seed pair, 0 gaps, 0 duplicates across 162 seed pairs |
| Hash consistency within seed pair | Pass | server_seed_hashed constant across all bets within each of 162 seed pairs |
| Seed hash integrity | Pass | 162 / 162 revealed seeds hash-verified — commitment chain intact |
| Deterministic output | Pass | Same (serverSeed, clientSeed, nonce, rows) always produces same slot — 8,100/8,100 confirmed |
| Client seed participation | Pass | Client seed is a genuine input — changing it changes the slot outcome |
Server seed committed before betPass
Client seed originPass
Client seed controlPass
Nonce sequencingPass
Hash consistency within seed pairPass
Seed hash integrityPass
Deterministic outputPass
Client seed participationPass
All 162 revealed seeds hash-verified. Every seed rotation was verified — the next seed the casino pre-committed always matched what was actually used. Outcomes are fully deterministic — the same server seed, client seed, nonce, and row count always produce the same slot. 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, the casino generates a secret server seed and publicly commits to it by displaying its SHA-256 hash. The server seed is a 64-character hex string (32 bytes), and the commitment hash is computed as SHA-256(hexDecode(serverSeed)). This cryptographic commitment locks the server's randomness before the player acts. After the epoch ends, the actual server seed is revealed — and anyone can verify that the hash matches.
export function verifyHash(serverSeed: string, serverSeedHashed: string): boolean { const hash = crypto .createHash('sha256') .update(Buffer.from(serverSeed, 'hex')) .digest('hex'); return hash === serverSeedHashed;}Real Example from Live Data:
{ "clientSeed": "MAAxNnpj1uB4AobA", "serverSeedHashed": "9fae5bb7897874cb71d7bd1be6dca522ddcd8f0c8f18ecaab9c5474d63b9be5a", "serverSeed": "5de225f630d2de838265252752ab4ba050a3f20a53711e23a0a43979abf755bd", "nonce": 0}Verification:
const crypto = require('crypto'); const serverSeed = "5de225f630d2de838265252752ab4ba050a3f20a53711e23a0a43979abf755bd"; const hashedServerSeed = crypto .createHash("sha256") .update(Buffer.from(serverSeed, 'hex')) .digest("hex"); console.log(hashedServerSeed);// Output: 9fae5bb7897874cb71d7bd1be6dca522ddcd8f0c8f18ecaab9c5474d63b9be5a ✅1.2Commitment Linkage (Next-Seed Promotion)
At every seed rotation, the nextServerSeedHash pre-committed before rotation must match the serverSeedHashed that becomes active after rotation. This chain ensures the server cannot generate seeds at rotation time after seeing the player's client seed — the next seed was already locked before the previous epoch began.
// Step 2: Next-Seed Promotion (Commitment Linkage)for (const s of seeds) { if (!s.nextSeedPromotion) continue; checked++; // recompute linkage — don't trust a captured flag: const { previousNextHash, newActiveHash } = s.nextSeedPromotion; if (previousNextHash !== newActiveHash) { promoFails++; }}1.3Hash Consistency Within Epoch
For each of the 162 epochs, the server_seed_hashed 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.
// Step 3: Hash Consistency Within Epochfor (const [hash, epochBets] of byHash) { const hashes = new Set(epochBets.map(b => b.response.server_seed_hashed)); if (hashes.size !== 1) { failures.push(`Epoch ${hash.substring(0, 16)}: ${hashes.size} distinct hashes among ${epochBets.length} bets`); }}server_seed_hashed constant within every epoch. Zero mid-epoch changes detected.1.4Client Seed Origin & Control
Players have full control over their client seed through the Duel.com fairness UI, allowing them to view, modify, or randomize it at any time before placing bets. The client seed is an input to every HMAC-SHA256 computation — changing it produces a completely different slot outcome. The client seed is browser-generated (Full Pass origin) — the server commits to its seed before the client seed for that epoch is known, making cherry-picking structurally impossible. Phase D specifically tested 10 distinct pfaudit-prefixed client seeds to confirm each is actively used.
0fFB4mFVNIQOSDzR3FfHxJWEayg31gaNKWooaSzlek9LYZzcLPzKd5K1ULzrG2XcMAAxNnpj1uB4AobA
plinko-master-8100bets.json1.5Nonce Incrementation
The nonce begins at 0 and increments by 1 for each bet under the same server seed. In Plinko, the nonce advances once per bet — each bet uses one nonce value. The nonce resets to 0 when the player rotates their seed, starting a new epoch. In two epochs our capture script dropped a single bet response (a network blip on our side, not the casino's). The platform's nonce sequence on either side of the drop was perfectly intact, and we recomputed the missing slot from the revealed server seed to confirm it.
// Step 4: Nonce Auditfor (const [hash, epochBets] of byHash) { const sorted = [...epochBets].sort((a, b) => a.response.nonce - b.response.nonce); const nonces = sorted.map(b => b.response.nonce); const clientSeeds = new Set(sorted.map(b => b.response.client_seed)); 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)`); } // Check for capture-retry pattern (nonce 50 present, one missed) const hasNonce50 = nonces.includes(50); const missingNonces = Array.from({ length: 51 }, (_, i) => i).filter(i => !nonces.includes(i)); const isRetryPattern = hasNonce50 && missingNonces.length === 1 && nonces.length === 50;}1.6Deterministic Mapping
The RNG algorithm is fully deterministic: given the same server seed, client seed, nonce, and row count, it always produces the exact same slot position. The hash function is HMAC-SHA256 with one call per row. The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. HMAC message format: clientSeed:nonce:cursor where cursor is the zero-based row index. Each row produces a uint32 from the first 4 bytes of the HMAC output; int % 2 determines left (0) or right (+1). The final slot is the sum of right-bounces across all rows.
export function computeSlot( serverSeed: string, clientSeed: string, nonce: number, rows: number): number { return computeSlotFromBuffer(Buffer.from(serverSeed, 'hex'), clientSeed, nonce, rows);} export function computeSlotFromBuffer( key: Buffer, clientSeed: string, nonce: number, rows: number): number { let slot = 0; for (let cursor = 0; cursor < rows; cursor++) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); slot += parseInt(hmac.substring(0, 8), 16) % 2; } return slot;}final_slot. Zero mismatches.Real Bet Verified:
// Source: data/plinko-master-8100bets.json// Bet ID: 71591629 (Phase A, 8 rows, high risk, nonce 0)// ✅ VERIFIED — slot recomputed from revealed server seed { "serverSeed": "5de225f630d2de838265252752ab4ba050a3f20a53711e23a0a43979abf755bd", "serverSeedHashed": "9fae5bb7897874cb71d7bd1be6dca522ddcd8f0c8f18ecaab9c5474d63b9be5a", "clientSeed": "MAAxNnpj1uB4AobA", "nonce": 0, "rows": 8, "risk_level": "high", "final_slot": 3, "payout_multiplier": "0.30253628", "win_amount": "0.0030253628"}Verification:
// computeSlot("5de225f6...", "MAAxNnpj1uB4AobA", 0, 8) // For each cursor 0..7:// message = "MAAxNnpj1uB4AobA:0:{cursor}"// hmac = HMAC-SHA256(hexDecode("5de225f6..."), message)// slot += parseInt(hmac[0..7], 16) % 2 // Final slot: 3 ✅// Multiplier (high/8, slot 3): 0.30253628× ✅// Payout: 0.01 × 0.30253628 = 0.0030253628 ✅1.7Client Seed Influence
To confirm the client seed is a genuine input to the HMAC-SHA256 computation, all 8,100 bets across 162 epochs were recomputed using a deliberately incorrect client seed (WRONG_CLIENT_SEED_FOR_AUDIT_TEST). 6,819 of 8,100 slots (84.2%) changed. No epoch produced identical outcomes with the wrong seed. The unchanged slots are expected: for any given row, there is a 50% probability the HMAC bit coincidentally matches under a different key input. Phase D additionally verified 500 bets across 10 distinct pfaudit-prefixed client seeds — 434/500 slots changed (86.8%).
// Step 6: Client Seed Influenceconst wrongClientSeed = 'WRONG_CLIENT_SEED_FOR_AUDIT_TEST';for (const [hash, epochBets] of byHash) { const seedEntry = revealedMap.get(hash); if (!seedEntry || epochBets.length !== 50) continue; const serverSeed = seedEntry.seed.serverSeed!; let changed = 0; for (const bet of epochBets) { const { client_seed, nonce } = bet.response; const { rows } = bet.request; const correctSlot = computeSlot(serverSeed, client_seed, nonce, rows); const wrongSlot = computeSlot(serverSeed, wrongClientSeed, nonce, rows); if (wrongSlot !== correctSlot) changed++; } totalChanged += changed;}Technical Evidence & Verification5 evidence groups
1.8Evidence Coverage Summary6 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Seed hash integrity (Step 1) | 162/162 revealed | Pass |
| Commitment linkage (Step 2) | 166/166 next-seed promotions | Pass |
| Hash consistency (Step 3) | 162/162 epochs | Pass |
| Nonce audit (Step 4) | 0 gaps, 0 duplicates, 2 capture-retry verified | Pass |
| Slot recomputation (Step 5) | 8,100/8,100 | Pass |
| Client seed influence (Step 6) | 6,819/8,100 (84.2%) | Pass |
1.9Code References6 source files
| File | Purpose |
|---|---|
| tests/verify.ts | 20-step verification pipeline (Steps 1–6 cover S1) |
| tests/steps/commitment.ts | Steps 1–4: Commit-reveal integrity checks |
| tests/steps/determinism.ts | Steps 5–6: Slot recomputation and client seed influence |
| src/rng.ts | HMAC-SHA256 slot computation (computeSlot, verifyHash) |
| src/loader.ts | Dataset loading + seed/bet parsing |
| capture/plinko-capture.reference.js | Browser-based data collection script |
1.10Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live Plinko game data from Duel.com |
| Total Records | 8,100 bets across 162 epochs (166 seed entries) |
| SHA-256 | 3cf9359d88220bc800bb32edfe04399f55b909302262928ee3a0582715635267 |
Fields used: serverSeed, serverSeedHashed, clientSeed, nonce, rows, risk_level, final_slot, payout_multiplier
1.11Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| SHA-256(hexDecode(serverSeed)) = serverSeedHashed for all 162 revealed seeds | Pass |
| Next-seed promotion chain intact for all 166 transitions | Pass |
| server_seed_hashed constant within epoch for all 162 epochs | Pass |
| Zero nonce gaps within any epoch | Pass |
| Zero nonce duplicates within any epoch | Pass |
| Same inputs produce same slot for all 8,100 bets | Pass |
| Wrong client seed changes slot in 84.2% of tests (6,819/8,100) | Pass |
| Client seed is browser-generated (Full Pass origin) | Pass |
1.12Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/duel-plinko.gitcd duel-plinko && npm installnpm run verify# Expected output: Steps 1–6 all PASSSteps 1–6 cover all Seed, Nonce & Determinism checks. Expected output:
[PASS] Step 1 — Seed Hash Integrity
[PASS] Step 2 — Next-Seed Promotion (Commitment Linkage)
[PASS] Step 3 — Hash Consistency Within Epoch
[PASS] Step 4 — Nonce Audit
[PASS] Step 5 — Slot Recomputation (RNG determinism)
[PASS] Step 6 — Client Seed InfluenceRNG & Entropy Model
Is the randomness genuinely random, or could it be rigged?
This section verifies that Duel.com's Plinko random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. The RNG uses HMAC-SHA256 with one call per row — each row's bounce direction is derived from the first 4 bytes of the HMAC output, reduced to a binary left/right decision via int % 2. We independently implemented this algorithm, verified it produces the same results as the live game, and confirmed no hidden inputs can influence outcomes.
One HMAC-SHA256 draw per row. Left and right are equally likely.
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
int % 2on a uint32 has mathematically zero modulo bias — exact 50/50 per row- Slot distribution follows binomial B(rows, 0.5) for all 27 configurations (confirmed over 27M simulated rounds)
- Consecutive outcomes are statistically independent — no patterns, no streaks
- 84.2% of outcomes change with a different client seed (6,819/8,100 tested bets)
What this means for you
- Each row's bounce is generated fairly and cannot be skewed
- Left and right bounces are equally likely — no directional bias
- No hidden randomness or server-side tricks influence the ball's path
- Consecutive bets are not correlated — past results don't affect future outcomes
- The algorithm depends only on seeds you can verify
- HMAC-SHA256 produces cryptographically sound, unpredictable output for each row
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
int % 2on a uint32 has mathematically zero modulo bias — exact 50/50 per row- Slot distribution follows binomial B(rows, 0.5) for all 27 configurations (confirmed over 27M simulated rounds)
- Consecutive outcomes are statistically independent — no patterns, no streaks
- 84.2% of outcomes change with a different client seed (6,819/8,100 tested bets)
- Each row's bounce is generated fairly and cannot be skewed
- Left and right bounces are equally likely — no directional bias
- No hidden randomness or server-side tricks influence the ball's path
- Consecutive bets are not correlated — past results don't affect future outcomes
- The algorithm depends only on seeds you can verify
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
int % 2 on uint32 has zero bias — 2^32 is exactly divisible by 2Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Plinko RNG uses only the disclosed inputs, produces uniform slot distribution across all 27 configurations, and shows no serial dependence across 27M simulated rounds. The client seed is a genuine input — 84.2% of outcomes change with a different seed.
| Check | Result | Evidence |
|---|---|---|
| RNG derived only from disclosed inputs | Pass | HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) — no hidden entropy |
| Entropy purity | Pass | No timestamps, external APIs, Math.random, or server-side state |
| Algorithm independently implemented | Pass | Independent implementation produces identical results for all 8,100 bets |
| Modulo bias | Pass | int % 2 on uint32 has zero bias — 2^32 is exactly divisible by 2 |
| Key encoding verified | Pass | Server seed hex-decoded to bytes (not UTF-8) — confirmed via 8,100-bet recomputation |
| Serial independence | Pass | Lag-1 autocorrelation near zero and runs tests pass across all 27 configs at 1M rounds each |
| Client seed influence | Pass | 84.2% of outcomes change with an alternate client seed — confirmed across all 8,100 bets |
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
int % 2 on uint32 has zero bias — 2^32 is exactly divisible by 2Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Plinko RNG uses only the disclosed inputs, produces uniform slot distribution across all 27 configurations, and shows no serial dependence across 27M simulated rounds. The client seed is a genuine input — 84.2% of outcomes 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 game with rows rows, the algorithm runs rows HMAC-SHA256 computations and accumulates a slot value. The HMAC key is the server seed hex-decoded to 32 raw bytes. The message for each row is clientSeed:nonce:cursor where cursor is the zero-based row index. The first 8 hex characters (4 bytes) of each HMAC output are parsed as a big-endian uint32, and int % 2 determines the bounce direction: 1 = right (slot increments), 0 = left (slot unchanged).
| Component | Detail |
|---|---|
| Hash function | HMAC-SHA256 |
| Key | Buffer.from(serverSeed, 'hex') — 32 bytes |
| Message | clientSeed:nonce:cursor |
| Extraction | First 8 hex chars → parseInt(hex, 16) → uint32 |
| Reduction | uint32 % 2 → 0 (left) or 1 (right) |
| Accumulation | slot += bit for each cursor 0 to rows−1 |
| Output range | 0 to rows (inclusive) |
export function computeSlotFromBuffer( key: Buffer, clientSeed: string, nonce: number, rows: number): number { let slot = 0; for (let cursor = 0; cursor < rows; cursor++) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); slot += parseInt(hmac.substring(0, 8), 16) % 2; } return slot;}2.2Entropy Sources
All randomness derives exclusively from the deterministic HMAC-SHA256 function combining four cleanly separated inputs:
| Source | Controlled By | Purpose |
|---|---|---|
| Server Seed | Casino | Base randomness (committed via SHA-256 hash before betting) |
| Client Seed | Player | Player-contributed entropy |
| Nonce | System | Uniqueness per bet (increments automatically) |
| Cursor | System | Per-row isolation (0 to rows-1) |
How we know: 8,100/8,100 bets 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 server-side mutable state. Only: HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor)
2.3Modulo Bias Analysis
Each row's bounce direction is determined by parseInt(hmac[0..7], 16) % 2 — a uint32 modulo 2 operation. Modulo bias occurs when 2^32 is not evenly divisible by the modulus. For modulus 2, there is mathematically zero bias: 2^32 = 4,294,967,296 divides evenly by 2, producing exactly 2,147,483,648 even values and 2,147,483,648 odd values. No rejection sampling is needed.
2^32 = 4,294,967,296
4,294,967,296 / 2 = 2,147,483,648 (exact, no remainder)
Even values in [0, 2^32 - 1]: 2,147,483,648 → left (50.000%)
Odd values in [0, 2^32 - 1]: 2,147,483,648 → right (50.000%)
Bias: 0.000%This contrasts with moduli that are not powers of 2 — for example, int % 37 on a 32-bit integer would have a non-uniform residue distribution requiring rejection sampling. int % 2 requires none. Each HMAC-SHA256 output is uniform over the uint32 range (HMAC-SHA256 is a pseudorandom function), so each row produces an exact 50/50 left/right decision.
2.4RNG Isolation
Each row within a single bet uses a unique cursor value in the HMAC message (clientSeed:nonce:cursor), ensuring per-row outputs are cryptographically independent. Each bet uses a unique nonce, ensuring per-bet outputs are independent. HMAC-SHA256 is a pseudorandom function — knowing one row's output gives zero information about other rows' outputs. There is no shared state between rows, between bets, or between epochs.
Evidence: The computeSlot implementation in 2.1 confirms this — it is a pure function with no class state, no external calls, and no cross-bet memory. The function takes (serverSeed, clientSeed, nonce, rows) explicitly and returns a slot. Same inputs always produce the same output.
2.5Monte Carlo Simulation (27M Rounds)
A 27,000,000-round Monte Carlo simulation (1,000,000 rounds per config, all 27 configurations) verified that the algorithm produces the expected slot distribution at scale.
| Metric | Value |
|---|---|
| Average simulated RTP | 99.917% |
| Average theoretical RTP | 99.900% |
| Chi-squared (α=0.01) | 27/27 configs pass |
| Bonferroni-corrected (α/27) | 27/27 pass |
| Serial independence | 27/27 pass |
// Simulation parameters// Per-config pinned seeds via crypto.randomBytes// Rounds: 1,000,000 per config × 27 configs = 27,000,000// Slot distribution tested against binomial B(rows, 0.5)All 27 configurations pass individual chi-squared tests. Serial independence: 0 failures at Bonferroni-corrected threshold across 27M rounds.
Methodology: Per-config pinned seeds for reproducibility. Chi-squared goodness-of-fit on slot distribution vs independent binomial B(rows, 0.5). Serial independence tested via lag-1 autocorrelation and Wald-Wolfowitz runs test.
2.6Serial Independence
Serial independence ensures consecutive bet outcomes are not correlated — winning or losing on one bet 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 slot values. Expected r ≈ 0 for independent sequences. Threshold: |z| > 3 (where z = r × √n).
Wald-Wolfowitz runs test: Tests whether the sequence of above/below-median outcomes has the expected number of runs. p < 0.01 indicates non-random structure.
export function lag1Autocorrelation(values: number[]): number { const N = values.length; if (N < 3) return 0; const mu = values.reduce((a, b) => a + b, 0) / N; const variance = values.reduce((acc, x) => acc + Math.pow(x - mu, 2), 0) / N; if (variance === 0) return 0; let cov = 0; for (let i = 0; i < N - 1; i++) { cov += (values[i] - mu) * (values[i + 1] - mu); } return cov / ((N - 1) * variance);}2.7Worked Example — Full RNG Trace
Real bet from dataset — Bet ID 71596330, 16 rows, high risk, nonce 0. Verified from plinko-master-8100bets.json:
serverSeed = b22261c6dc661d17db31417fb4a2dd1e58eb351ae7d8c77c6f51a2cd81dc84b9
clientSeed = 9sysyk9uhCw09FzD
nonce = 0
rows = 16
risk = high| cursor | HMAC[:8] | uint32 | % 2 | direction | slot |
|---|---|---|---|---|---|
| 0 | a9c2f13f | 2,848,125,247 | 1 | right | 1 |
| 1 | 14895bdb | 344,546,267 | 1 | right | 2 |
| 2 | 4739337b | 1,194,931,067 | 1 | right | 3 |
| 3 | d74021d8 | 3,611,304,408 | 0 | left | 3 |
| 4 | 54078d16 | 1,409,781,014 | 0 | left | 3 |
| 5 | 6074bdba | 1,618,263,482 | 0 | left | 3 |
| 6 | 09ba0b81 | 163,187,585 | 1 | right | 4 |
| 7 | a0b6ea00 | 2,696,342,016 | 0 | left | 4 |
| 8 | ef0695f6 | 4,010,186,230 | 0 | left | 4 |
| 9 | 9a93d48e | 2,593,379,470 | 0 | left | 4 |
| 10 | 3ff81328 | 1,073,222,440 | 0 | left | 4 |
| 11 | 0ea47ffb | 245,661,691 | 1 | right | 5 |
| 12 | 869a92ab | 2,258,277,035 | 1 | right | 6 |
| 13 | 87a5194b | 2,275,744,075 | 1 | right | 7 |
| 14 | fab89728 | 4,206,401,320 | 0 | left | 7 |
| 15 | 32fcaf56 | 855,420,758 | 0 | left | 7 |
final_slot = 7 matches HMAC-SHA256 recomputation exactly. Multiplier (16r/high, slot 7): 0.20186622×. Payout: 0.01 × 0.20186622 = 0.0020186622.Technical Evidence & Verification5 evidence groups
2.8Evidence Coverage Summary6 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Algorithm implementation (Step 5) | 8,100/8,100 bets | Pass |
| Key encoding (hex vs UTF-8) | Confirmed via recomputation | Pass |
| Modulo bias analysis | Mathematical proof: 2^32 % 2 = 0 | Pass |
| External entropy non-participation (Step 5) | 8,100/8,100 reproduced from disclosed inputs alone | Pass |
| Simulation chi-squared (Step 19) | 0/27 configs fail at α=0.01 | Pass |
| Serial independence (Step 19) | 0/27 configs fail (lag-1 + runs test) | Pass |
2.9Code References5 source files
| File | Purpose |
|---|---|
| src/rng.ts | HMAC-SHA256 implementation (computeSlot, computeSlotFromBuffer) |
| src/simulate.ts | Monte Carlo simulation (27M rounds, two-pass) |
| src/stats.ts | Chi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test |
| tests/steps/determinism.ts | Steps 5–6: Slot recomputation and client seed influence |
| tests/steps/simulation.ts | Steps 19–20: Simulation integrity verification |
2.10Verified Invariants9 invariants
| Invariant | Result |
|---|---|
| HMAC-SHA256 output matches live game for all 8,100 bets | Pass |
| Key is hex-decoded (not UTF-8) — wrong encoding produces wrong slots | Pass |
| int % 2 has zero modulo bias (2^32 exactly divisible by 2) | Pass |
| No external entropy sources required for slot computation | Pass |
| Slot distribution matches binomial B(rows, 0.5) for all 27 configs (1M rounds each) | Pass |
| Lag-1 autocorrelation near zero for all 27 configs | Pass |
| Runs test p > 0.01 for all 27 configs | Pass |
| Per-config simulated RTP converges to 99.9% theoretical | Pass |
| Client seed change produces different slot in 84.2% of bets (6,819/8,100) | Pass |
2.11Datasets UsedSource artifacts
2.12Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the simulation and verification:
git clone https://github.com/ProvablyFair-org/duel-plinko.gitcd duel-plinko && npm installnpm run simulate # 27M-round simulation (~6 min)npm run verify # Steps 5, 19 cover S2S2-related steps:
[PASS] Step 5 — Slot Recomputation (RNG determinism)
[PASS] Step 19 — Simulation Results — Pass 1 IntegrityVerifier Parity
Does the live game actually follow its own rules?
This section validates that the independent verifier produces the exact same slot position as the live game for every single bet. It also confirms the payout math and that every multiplier matches Duel’s published tables. Any mismatch would invalidate the fairness guarantee.
The independent verifier reproduced every captured slot and payout exactly.
What we verified
- Every bet independently recomputed from seeds — full slot position verified, not just the payout
- Payout correctness: win_amount = bet × multiplier, exact to 8 decimal places for all 8,100 bets
- Multiplier table produces the correct value for all 27 row/risk configurations
- Bet amount is not an input to the RNG — slot position depends only on seeds, nonce, and row count
- All four capture phases recomputed identically (config coverage, stress test, elevated stake, client seed variation)
What this means for you
- The verifier isn’t a simulation — it produces the exact same slot as the live game
- Every bet you play can be independently recomputed by anyone
- No hidden logic alters outcomes based on how much you bet or how you play
- The game engine in production matches the published algorithm exactly
- Every bet independently recomputed from seeds — full slot position verified, not just the payout
- Payout correctness: win_amount = bet × multiplier, exact to 8 decimal places for all 8,100 bets
- Multiplier table produces the correct value for all 27 row/risk configurations
- Bet amount is not an input to the RNG — slot position depends only on seeds, nonce, and row count
- All four capture phases recomputed identically (config coverage, stress test, elevated stake, client seed variation)
- The verifier isn’t a simulation — it produces the exact same slot as the live game
- Every bet you play can be independently recomputed by anyone
- No hidden logic alters outcomes based on how much you bet or how you play
- The game engine in production matches the published algorithm exactly
Slot recomputationPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 8,100 bets matched the independent verifier exactly — slot positions verified across all four capture phases. Payout math correct. Multiplier table confirmed across all 27 configurations.
| Check | Result | Evidence |
|---|---|---|
| Slot recomputation | Pass | 8,100/8,100 exact match — slot position verified for every bet |
| Payout correctness | Pass | All 8,100 bets: win_amount = bet × multiplier, exact to 8 decimal places |
| Multiplier table integrity | Pass | All observed multipliers match the published table for all 27 configurations |
| Bet-size independence | Pass | Bet amount is absent from the RNG input — slot position depends only on seeds, nonce, and row count |
| Config completeness | Pass | All 27 row/risk configurations (8–16 rows × low/medium/high) covered in live data |
| Multi-phase coverage | Pass | 4 structured phases: config coverage (A), stress test (B), elevated stake (C), client seed variation (D) |
Slot recomputationPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 8,100 bets matched the independent verifier exactly — slot positions verified across all four capture phases. Payout math correct. Multiplier table confirmed across all 27 configurations.
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 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 slot position they experienced during live play.
3.2Four-Phase Collection Design
Data was collected across four structured phases, each designed to test a specific fairness property. The phases are complementary — together they cover configuration breadth, statistical depth, bet-size invariance, and client seed verification.
| Phase | Bets | Config | Bet Amount | Purpose |
|---|---|---|---|---|
| A — Configuration coverage | 5,400 | All 27 configs | $0.01 | Verify every config behaves correctly |
| B — High-variance sampling | 2,000 | 16r/high | $0.01 | Deep binomial distribution test on highest-variance config |
| C — Code-path equivalence | 200 | 16r/high | $10.00 | Confirm bet amount is not in the RNG |
| D — Client seed verification | 500 | 10 configs | $0.01 | Confirm client seed is genuinely used in HMAC computation |
3.3Slot Recomputation (Step 5)
For every bet belonging to an epoch with a revealed server seed, the verifier independently computed the slot using computeSlot(serverSeed, clientSeed, nonce, rows) and compared it to the server-reported final_slot. The computation uses HMAC-SHA256 with the hex-decoded server seed as key and clientSeed:nonce:cursor as message for each row.
// Step 5: Slot Recomputationfor (const [hash, epochBets] of byHash) { const seedEntry = revealedMap.get(hash); if (!seedEntry) { skipped += epochBets.length; continue; } const serverSeed = seedEntry.seed.serverSeed!; for (const bet of epochBets) { const { client_seed, nonce, final_slot } = bet.response; const { rows } = bet.request; const computed = computeSlot(serverSeed, client_seed, nonce, rows); if (computed !== final_slot) { failures.push(`bet ${bet.response.id}: computed=${computed} actual=${final_slot}`); } checked++; }}final_slot in the dataset.3.4Payout Math (Step 7)
For each of the 8,100 bets, the verifier computed amount_currency × payout_multiplier and compared the result to the server-reported win_amount. The tolerance is 1×10⁻⁸ — any difference larger than this would indicate the server is applying hidden fees, rounding errors, or incorrect multipliers.
// Step 7: Payout Mathconst TOLERANCE = 1e-8;for (const bet of bets) { const amount = parseFloat(bet.response.amount_currency); const mult = parseFloat(bet.response.payout_multiplier); const win = parseFloat(bet.response.win_amount); const expected = amount * mult; const diff = Math.abs(win - expected); if (diff > TOLERANCE) { failures.push(`bet ${bet.response.id}: win=${win} expected=${expected} diff=${diff.toExponential(2)}`); }}win_amount = amount_currency × payout_multiplier within tolerance 1e-8. Zero mismatches. Payout math is exact.3.5Multiplier Table Provenance (Step 8)
For each of the 8,100 bets, the observed payout_multiplier was compared against both multiplier sources in plinkoConfig.json: the compact payout_tables and the per-bracket scaling_edge[0].multipliers. Both sources agree within 1.49×10⁻⁶ (floating-point rounding). The server uses scaling_edge[0].multipliers as the reference table — confirmed by 8,100/8,100 exact matches.
| Metric | Count |
|---|---|
| Total bets checked | 8,100 |
| Matching scaling_edge[0] | 8,100 |
| Matching payout_tables | 8,100 |
| Matching neither | 0 |
scaling_edge[0].multipliers. Every live payout multiplier matches plinkoConfig.json. Theoretical RTP from this table: 99.900% across all 27 configurations.3.6Multiplier Symmetry & Floor (Step 13)
For all 27 configurations, the multiplier table was verified to be symmetric: multiplier[k] = multiplier[rows − k] for every slot k. This follows from the symmetric binomial distribution — slots k and (rows − k) have identical probabilities, so their multipliers must match. Additionally, the 16r/high configuration was verified to have a 0.2× floor at center slots (slots 6–10).
// Step 13: Multiplier Table + Symmetryfor (const { rows, risk } of cfg.allConfigs()) { // EC-14: Symmetry check for (let k = 0; k <= rows; k++) { const mk = cfg.scalingEdgeMultiplier(rows, risk as RiskLevel, k); const mk_mirror = cfg.scalingEdgeMultiplier(rows, risk as RiskLevel, rows - k); if (Math.abs(mk - mk_mirror) > TOLERANCE) { failures.push(`EC-14: ${rows}r/${risk} slot ${k} vs ${rows - k}: ${mk} ≠ ${mk_mirror}`); } } // EC-13: 16r/high center floor if (rows === 16 && risk === 'high') { const center = cfg.scalingEdgeMultiplier(16, 'high', 8); if (center < 0.2) { failures.push(`EC-13: 16r/high center slot multiplier=${center} < 0.2`); } }}3.7Phase C — Bet-Size Equivalence (Step 9)
Phase C placed 200 bets at $10 on the same 16r/high configuration as Phase B's $0.01 bets. All 200 slots were recomputed correctly from revealed seeds. All 200 payout multipliers matched the same scaling_edge[0].multipliers table as Phase B. Equivalence is proven deterministically — 200/200 exact slot and multiplier matches. No distributional test is applied: at n=200, statistical comparisons have near-zero power and add nothing over the deterministic proof.
// Step 9: Phase C Code-Path Equivalencefor (const bet of phaseC) { const seedEntry = revealedMap.get(bet.response.server_seed_hashed); if (!seedEntry) { slotSkipped++; continue; } const computed = computeSlot( seedEntry.seed.serverSeed!, bet.response.client_seed, bet.response.nonce, bet.request.rows ); if (computed !== bet.response.final_slot) { failures.push(`bet ${bet.response.id} slot mismatch`); }}3.8Worked Example — Full Parity Verification
Real bet from Phase B — Bet ID 71644262, 16 rows, high risk, nonce 5. Verified from plinko-master-8100bets.json:
serverSeed = 7b8c38d84c6d187c472c4e3f1e668222d64cfca099ca6127ad0e1cd198faf5b6
clientSeed = S8Wfb9k9Uyie6M9D
nonce = 5
rows = 16
risk = high| Step | Process | Output |
|---|---|---|
| 1 | HMAC-SHA256(hexDecode(serverSeed), "S8Wfb9k9Uyie6M9D:5:0") through cursor 15 | 16 uint32 values |
| 2 | Each uint32 % 2 → left/right bounce | 9 right bounces |
| 3 | final_slot = 9 | Matches live game ✅ |
| 4 | Multiplier lookup: 16r/high slot 9 | 0.20186622× |
| 5 | Payout: 0.01 × 0.20186622 | 0.0020186622 ✅ |
// computeSlot("7b8c38d8...", "S8Wfb9k9Uyie6M9D", 5, 16)// 16 HMAC calls → slot = 9//// Multiplier lookup: scaling_edge[0].multipliers["16"]["high"][9]// = 0.20186622//// Payout: 0.01 × 0.20186622 = 0.0020186622// Live win_amount: 0.0020186622 ✅Technical Evidence & Verification5 evidence groups
3.9Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Slot recomputation | 8,100 / 8,100 bets — full slot position verified | Pass |
| Payout math | 8,100 / 8,100 bets (exact to 8 decimal places) | Pass |
| Multiplier table integrity | All 27 configs — observed multipliers match published table | Pass |
| Multiplier symmetry | 27/27 configs — slot k = slot (rows − k) | Pass |
| Phase C slot recomputation | 200 / 200 at $10 | Pass |
| Phase C multiplier match | 200 / 200 bets — identical to $0.01 table | Pass |
| Config completeness | All 27 configs (8–16 × low/medium/high) covered in live data | Pass |
| Multi-phase coverage | 4 phases: A (5,400) + B (2,000) + C (200) + D (500) | Pass |
3.10Code References5 source files
| File | Purpose |
|---|---|
| tests/steps/determinism.ts | Step 5: Slot recomputation |
| tests/steps/payouts.ts | Steps 7–9: Payout math, multiplier provenance, Phase C equivalence |
| tests/steps/dataset.ts | Steps 10–16: Config completeness, epoch size, multiplier symmetry, scaling edge |
| src/rng.ts | HMAC-SHA256 slot computation |
| src/config.ts | PlinkoConfig multiplier lookup and RTP calculation |
3.11Datasets UsedSource artifacts
3.12Verified Invariants9 invariants
| Invariant | Result |
|---|---|
| Computed slot matches live final_slot for all 8,100 bets | Pass |
| win_amount = bet × multiplier, exact to 8 decimal places for all 8,100 bets | Pass |
| Every payout_multiplier matches the published table for all 27 configurations | Pass |
| Multiplier tables are symmetric for all 27 configs: slot k = slot (rows − k) | Pass |
| Phase C ($10) produces identical slot positions to $0.01 bets | Pass |
| Phase C multipliers match Phase B table (same config, different bet size) | Pass |
| No hidden inputs beyond (serverSeed, clientSeed, nonce, rows) | Pass |
| All 27 row/risk configurations (8–16 × low/medium/high) present in live data | Pass |
| Phase D: 500/500 slots verified across 10 custom client seeds | Pass |
3.13Reproduction InstructionsRun locally
git clone https://github.com/ProvablyFair-org/duel-plinko.gitcd duel-plinko && npm installnpm run verify# Expected output: Steps 5, 7–9, 11, 13 all PASSS3-related steps:
[PASS] Step 5 — Slot Recomputation (RNG determinism)
[PASS] Step 7 — Payout Math
[PASS] Step 8 — Multiplier Table Provenance
[PASS] Step 9 — Phase C Code-Path Equivalence
[PASS] Step 11 — Config Completeness
[PASS] Step 13 — Multiplier Table + SymmetryRTP & Payout Logic
Is the house edge what the casino claims?
This section mathematically verifies that the 0.1% house edge is exactly what's advertised across all 27 configurations. The key test is anti-circularity: we confirm the RTP by applying independently-computed binomial probabilities to the published payout table — the derivation uses no operator-supplied RTP figure. We then confirm it against 27 million simulated rounds and test whether the casino pre-selected favourable seeds.
The return is derived from binomial probabilities and the published payout table.
What we verified
- House edge is exactly 0.1% at bracket 0 — confirmed from the published configuration and independently derived
- RTP confirmed against the published payout table: independent binomial P(slot) × multiplier = 99.9% for every configuration
- 27M-round simulation converges on theoretical RTP (mean 99.917%)
- Cherry-pick detection: 162 casino seeds tested — no evidence of seed pre-selection
- Bet amount does not influence slot position — confirmed at $0.01 and $10
What this means for you
- Within bracket 0, the game-engine house edge is a flat 0.1%
- The RTP is confirmed using independently-computed probabilities applied to the published payout table — it doesn't rely on the casino's own RTP claim
- The casino's seeds show no evidence of being chosen to produce favourable early outcomes
- Your bet amount doesn't affect where the ball lands
- Higher bet amounts face a scaling house edge — see S4.7 for full analysis
- House edge is exactly 0.1% at bracket 0 — confirmed from the published configuration and independently derived
- RTP confirmed against the published payout table: independent binomial P(slot) × multiplier = 99.9% for every configuration
- 27M-round simulation converges on theoretical RTP (mean 99.917%)
- Cherry-pick detection: 162 casino seeds tested — no evidence of seed pre-selection
- Bet amount does not influence slot position — confirmed at $0.01 and $10
- Within bracket 0, the game-engine house edge is a flat 0.1%
- The RTP is confirmed using independently-computed probabilities applied to the published payout table — it doesn't rely on the casino's own RTP claim
- The casino's seeds show no evidence of being chosen to produce favourable early outcomes
- Your bet amount doesn't affect where the ball lands
- Higher bet amounts face a scaling house edge — see S4.7 for full analysis
Anti-circularityPass
House edge auditInfo
Simulated RTP (Pass 1)Pass
Cherry-pick detection (Pass 2)Pass
Bet-size invariancePass
Multiplier formulaPass
Config completenessPass
Zero Edge auditPass
The 99.9% RTP is proven mathematically from binomial probabilities — P(slot) × multiplier = 0.999 for all 27 configurations, using independently-computed probabilities and the published payout table — an exact identity, not a statistical estimate. 27M simulated rounds and cherry-pick detection confirm no anomalies.
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | Binomial P(slot) × multiplier = 99.9% for all 27 configs — binomial P from B(rows, 0.5) computed independently; multipliers are the published table |
| House edge audit | Info | 0.1% house edge at bracket 0, scaling to 2.0% at extreme bet sizes. Thresholds vary by config — see S4.7 |
| Simulated RTP (Pass 1) | Pass | 27M rounds, avg RTP = 99.917%, 0/27 chi-squared failures, 0/27 serial independence failures |
| Cherry-pick detection (Pass 2) | Pass | 162 casino seeds tested — no evidence of seed pre-selection |
| Bet-size invariance | Pass | Bet amount is not an input to the RNG — same slot distribution at $0.01 and $10. Tested in Phase C (200/200) |
| Multiplier formula | Pass | Binomial B(rows, 0.5) × scaling_edge[0].multipliers = 99.9% independently verified for all 27 configs |
| Config completeness | Pass | All 27 row/risk configurations covered |
| Zero Edge audit | Pass | Zero Edge bets use identical multiplier tables — no separate payout structure |
Anti-circularityPass
House edge auditInfo
Simulated RTP (Pass 1)Pass
Cherry-pick detection (Pass 2)Pass
Bet-size invariancePass
Multiplier formulaPass
Config completenessPass
Zero Edge auditPass
The 99.9% RTP is proven mathematically from binomial probabilities — P(slot) × multiplier = 0.999 for all 27 configurations, using independently-computed probabilities and the published payout table — an exact identity, not a statistical estimate. 27M simulated rounds and cherry-pick detection confirm no anomalies.
How It Works — RTP & Payout Logic9 sections
4.1Anti-Circularity Proof (Step 17)
The anti-circularity proof establishes the 99.9% RTP by applying independently-computed binomial probabilities to the published payout table, using no operator-supplied RTP figure. This is what separates a mathematical proof from a statistical estimate. The proof has two components — each derived independently:
| Component | Formula | Source |
|---|---|---|
| Slot probability | P(slot=k) = C(rows,k) × 0.5^rows | Binomial distribution — pure math, not from casino |
| Multiplier | scaling_edge[0].multipliers[rows][risk][k] | Casino-supplied — observed from plinkoConfig.json |
| RTP computation | Σ P(k) × multiplier(k) for k = 0 to rows | Independent probability × observed multiplier |
| Result | = 0.999 for all 27 configurations | Exact identity from independent probabilities × published table — not a statistical estimate |
| Rows | Risk | P(slot=0) | Multiplier(slot=0) | Σ P×M | RTP |
|---|---|---|---|---|---|
| 8 | low | 0.00391 | 5.65180× | 0.99900 | 99.9% |
| 8 | high | 0.00391 | 29.24517× | 0.99900 | 99.9% |
| 16 | low | 0.00002 | 16.15× | 0.99900 | 99.9% |
| 16 | high | 0.00002 | 1009.33× | 0.99900 | 99.9% |
Anti-Circularity Verification:
// Step 17: Probability Independence — Anti-Circularityfor (const { rows, risk } of cfg.allConfigs()) { const configProbs = cfg.probabilities(rows, risk); const slotCount = rows + 1; for (let k = 0; k < slotCount; k++) { const independent = binomProb(rows, k); const fromConfig = configProbs[k]; if (fromConfig !== independent) { failures.push( `${rows}r/${risk} slot ${k}: config=${fromConfig} independent=${independent}` ); } }}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. When we multiply independent probabilities by observed multipliers and get exactly 0.999 for every configuration, the RTP is proven — not estimated.
4.2House Edge Audit (Step 10, Step 16)
The game-engine house edge at bracket 0 is 0.1% (RTP = 99.9%), implemented by the multiplier table values themselves — the payout_multiplier returned for each (rows, risk, slot) combination is drawn from a table constructed to yield 99.9% RTP. This is the audit's primary RTP finding and is confirmed by the anti-circularity proof in Step 17.
The captured dataset also exposes an effective_edge field on every bet response. Per the operator's published mechanism, this field represents the player's net edge after Zero Edge rakeback is applied. Bets under $1,000 placed while the account is within its $50,000 daily Zero Edge allowance are eligible for a 0.1% instant rakeback, applied asynchronously after settlement via an operator-side rewards queue. The field updates from 0.1 to 0 once the queue processes the bet.
At capture time, 537 of 8,100 bets (6.6%) carried effective_edge: 0 and 7,563 carried 0.1. A sample re-fetch of bets initially captured as 0.1 showed the field had since been updated to 0 — consistent with the operator's description of the rewards mechanism as asynchronous post-settlement. We did not re-fetch every bet, and the audit's scope does not include verifying the rewards transaction layer itself — we audit the game-engine RTP from the multiplier tables, which both effective_edge values draw from identically.
Step 10 tested whether the two effective_edge groups receive different multipliers — they do not. Across all 8,100 bets, a Zero Edge-tagged bet and a 0.1%-tagged bet on the same (rows, risk, slot) receive the exact same payout_multiplier. All test bets settled within the operator's $50,000 daily Zero Edge allowance; post-cap behaviour was not empirically exercised by this audit.
// Step 10: Zero Edge Auditconst edgeGroups = new Map<number, typeof bets>();for (const bet of bets) { const e = bet.response.effective_edge; if (!edgeGroups.has(e)) edgeGroups.set(e, []); edgeGroups.get(e)!.push(bet);}// Both edge groups use identical multiplier tables — 0 mismatcheseffective_edge groups resolve through identical multiplier tables, with zero mismatches. The game-engine 0.1% house edge is confirmed by the multiplier table values themselves (verified by Step 17 anti-circularity). The rewards transaction layer that brings net edge to 0% via rakeback is operator settlement-side and outside this audit's scope.plinkoConfig.json contains 191 bet-size brackets per configuration. House edge scales from 0.1% at bracket 0 upward at larger bet sizes, applied per-bet via bracket lookup at multiplier time. All test bets ($0.01 and $10) fall within bracket 0 — see subsection 4.7 for full bracket analysis.4.3Full RTP Table — All 27 Configurations
All 27 configurations produce the same theoretical RTP of 99.900% at bracket 0. The simulated RTP varies per config due to sampling variance, particularly for high-risk configurations where jackpot multipliers (up to 1009×) create large per-bet variance.
| Config | Theoretical RTP | Simulated RTP | Deviation |
|---|---|---|---|
| 8r/low | 99.900% | 99.894% | −0.006% |
| 8r/medium | 99.900% | 99.677% | −0.223% |
| 8r/high | 99.900% | 99.997% | +0.097% |
| 9r/low | 99.900% | 99.900% | −0.000% |
| 9r/medium | 99.900% | 99.994% | +0.094% |
| 9r/high | 99.900% | 99.566% | −0.334% |
| 10r/low | 99.900% | 99.902% | +0.002% |
| 10r/medium | 99.900% | 99.815% | −0.085% |
| 10r/high | 99.900% | 99.460% | −0.440% |
| 11r/low | 99.900% | 99.876% | −0.024% |
| 11r/medium | 99.900% | 99.921% | +0.021% |
| 11r/high | 99.900% | 100.093% | +0.193% |
| 12r/low | 99.900% | 99.916% | +0.016% |
| 12r/medium | 99.900% | 99.895% | −0.005% |
| 12r/high | 99.900% | 99.852% | −0.048% |
| 13r/low | 99.900% | 99.912% | +0.012% |
| 13r/medium | 99.900% | 99.748% | −0.152% |
| 13r/high | 99.900% | 100.361% | +0.461% |
| 14r/low | 99.900% | 99.872% | −0.028% |
| 14r/medium | 99.900% | 99.945% | +0.045% |
| 14r/high | 99.900% | 99.908% | +0.008% |
| 15r/low | 99.900% | 99.917% | +0.017% |
| 15r/medium | 99.900% | 99.923% | +0.023% |
| 15r/high | 99.900% | 100.003% | +0.103% |
| 16r/low | 99.900% | 99.881% | −0.019% |
| 16r/medium | 99.900% | 99.786% | −0.114% |
| 16r/high | 99.900% | 100.736% | +0.836% |
4.4Simulation Pass 1 — Fresh Seeds (Step 19)
Section 4.1 proves RTP = 99.9% mathematically. But does the game engine actually produce that in practice? To find out, we simulated 27 million rounds locally using the same RNG algorithm and independent auditor-generated seeds. If the simulated payouts converge on 99.9%, the implementation matches the math.
Chi-squared test: Compares observed slot frequencies to expected binomial frequencies. 0/27 fail at uncorrected α=0.01. 0/27 fail at Bonferroni α/27 ≈ 0.00037.
Serial independence: Lag-1 autocorrelation measures correlation between consecutive slots. Runs test checks for non-random run structure. 0/27 configs fail either test.
RTP convergence: Avg simulated RTP = 99.917% vs 99.900% theoretical. Deviation of +0.017% is within expected sampling variance.
// Pass 1 — Fresh random seeds// 1,000,000 rounds × 27 configs = 27,000,000 total// Per-config pinned seeds for reproducibility (S7)const { server: simServer, client: simClient } = SIM_SEEDS[ci];for (let nonce = 0; nonce < ROUNDS_PER_CONFIG; nonce++) { const slot = computeSlot(simServer, simClient, nonce, rows); slotCounts[slot]++; totalPayout += cfg.scalingEdgeMultiplier(rows, risk, slot);}What is a Monte Carlo simulation? Instead of proving fairness with algebra alone, we simulate millions of real game rounds. Each round computes a slot position from scratch using the same HMAC-SHA256 pipeline as the live game. We record payouts, then compare the aggregate return to the theoretical 99.9%. If they match, the math holds in practice — not just on paper.
4.5Cherry-Pick Detection — Pass 2 (Step 20)
Could the casino have chosen server seeds that produce worse outcomes for players? Pass 2 takes every server seed the casino actually used and simulates thousands of rounds to check whether any of them are statistically biased against players.
Test A — Overall RTP: Does any individual seed produce lower-than-expected RTP across all 10,000 nonces? A cherry-picked seed would show a statistically significant RTP deficit.
Test B — Early vs late window: Does any seed produce worse outcomes in the early nonces (where real players bet) compared to later nonces? A sophisticated cherry-pick could target only the first few hundred rounds.
| Test | Result |
|---|---|
| Seeds tested | 162 |
| Seed × row combinations | 242 |
| Test A fails (p<0.01) | 1 / 242 — binomial p = 0.912 |
| Early-window statistical flags (Test B) | 10 / 242 — binomial p = 0.773 |
| Multi-window analysis | 8 isolated-early, 2 multi-window — consistent with H₀ |
// Pass 2 — Casino seedsfor (const [seedHash, info] of seedEntries) { for (const rows of rowList) { for (let nonce = 0; nonce < NONCES_PER_SEED; nonce++) { const slot = computeSlotFromBuffer(keyBuffer, info.clientSeed, nonce, rows); fullCounts[slot]++; if (nonce < EPOCH_LENGTH) { earlyCounts[slot]++; } else { lateCounts[slot]++; } } // Cherry-picking signature: early deviates from binomial, late does not const cherryPickFlag = earlyTest.pValue < 0.05 && lateTest.pValue >= 0.05; }}4.6Bet-Size Invariance (Step 9)
Phase C placed 200 bets at $10 on 16r/high — the same configuration as Phase B's $0.01 bets. All 200 slots were recomputed correctly from revealed seeds using the same HMAC-SHA256 path. All 200 multipliers matched the same scaling_edge[0].multipliers table. Bet amount is not an input to the RNG and does not affect the slot distribution.
| Metric | Phase B ($0.01) | Phase C ($10) |
|---|---|---|
| Bets | 2,000 | 200 |
| Config | 16r/high | 16r/high |
| Slots recomputed | 2,000/2,000 | 200/200 |
| Multiplier table | scaling_edge[0] | scaling_edge[0] |
| RNG path | HMAC-SHA256 | HMAC-SHA256 |
4.7Progressive House Edge — Scaling Edge Analysis (Step 16)
plinkoConfig.json contains a scaling_edge structure with 191 bet-size brackets per configuration. As bet amounts increase, the game-engine house edge scales from 0.1% (bracket 0) up to 2.0% (bracket 190). Higher brackets apply lower multipliers — the slot distribution is unchanged, but the payout per slot decreases. For example, the 16r/high jackpot multiplier drops from 1009.33× at bracket 0 to 990.13× at the highest bracket. This is a static, disclosed structure embedded in the game configuration, not a dynamic adjustment.
The brackets describe the game-engine edge — the edge before rakeback. Per the operator's disclosed Zero Edge mechanism (see 4.2), the bracket tier is selected on the total bet amount, and rakeback rebates the edge to 0% net on the portion of a bet up to the lesser of a $1,000 per-bet cap or the account's remaining $50,000 daily Zero Edge allowance; any portion above that retains the bracket's game-engine edge. This audit verifies the game-engine multiplier tables; it does not verify the operator-side rewards layer that applies the rakeback, so the figures below should be read as game-engine edge, not guaranteed net edge.
How it works: The server selects the bracket based on amount_currency. Each bracket contains a complete multiplier table for all slots. Higher brackets reduce every multiplier proportionally, increasing the effective house edge.
Impact on players: A player betting $500 on 16r/high lands in a bracket above 0, carrying a higher game-engine house edge than the 0.1% at bracket 0. Per the operator's disclosed Zero Edge rakeback (see 4.2), that edge is rebated to 0% net on the portion of a bet up to the lesser of a $1,000 per-bet cap or the remaining daily allowance — so the bracket determines the game-engine edge, not necessarily the edge the player ultimately pays. The game UI does not prominently surface the bracket thresholds themselves — but when an entered bet size triggers a higher bracket, the multiplier shown on the game board updates to the scaled multiplier, so the payout displayed always reflects the bracket actually in effect for that bet. The audit did not test post-allowance behaviour or the rewards layer that applies the rakeback.
Audit scope: All test bets ($0.01 and $10) fall within bracket 0. The RTP figures in this audit apply to bracket 0 only.
| Config | Bracket 0 Ceiling | Notes |
|---|---|---|
| 8r/low | $46,898.81 | Highest ceiling among 8-row configs |
| 8r/high | $2,061.27 | |
| 12r/medium | $8,809.34 | |
| 16r/low | $99,038.25 | Highest ceiling among 16-row configs |
| 16r/medium | $6,731.19 | |
| 16r/high | $335.95 | Most restrictive — lowest ceiling across all 27 configs |
| Bet Range | House Edge | RTP | Jackpot Multiplier (slot 0) |
|---|---|---|---|
| $0 – $335.95 | 0.1% | 99.9% | 1009.33× |
| $335.95 – $662.00 | 0.1% – 0.2% | 99.9% – 99.8% | 1009.33× – 1008.32× |
| $662.00 – $1,493.44 | 0.2% – 0.5% | 99.8% – 99.5% | 1008.32× – 1004.28× |
| $1,493.44 – $1,500.99 | 0.5% – 1.0% | 99.5% – 99.0% | 1004.28× – 999.23× |
| $1,500.99 – $1,516.48 | 1.0% – 2.0% | 99.0% – 98.0% | 999.23× – 990.13× |
| Risk | Bracket 0 Ceiling | Bracket 190 Max Bet | Edge Range |
|---|---|---|---|
| Low | $99,038.25 | $101,087.78 | 0.1% – 2.0% |
| Medium | $6,731.19 | $13,901.54 | 0.1% – 2.0% |
| High | $335.95 | $1,516.48 | 0.1% – 2.0% |
plinkoConfig.json. It is not evidence of manipulation — the multiplier tables are consistent with the declared algorithm. However, it materially affects effective RTP for higher-stakes players.4.8Informational Items (Not Scored)
These items are reported for transparency but are not scored audit steps. They provide context on the empirical RTP observed during data collection.
| Item | Value | Context |
|---|---|---|
| Phase A empirical RTP | 99.303% | 5,400 bets at $0.01 — within expected variance |
| Phase B empirical RTP | 92.313% | 2,000 bets at $0.01 (16r/high) — high jackpot variance |
| Phase C empirical RTP | 74.993% | 200 bets at $10 (16r/high) — expected at N=200 |
| Phase D empirical RTP | 104.636% | 500 bets at $0.01 — within expected variance |
4.9Worked Example — Payout Verification
Real bet from Phase A — Bet ID 71602647, 12 rows, medium risk, nonce 3. Verified from plinko-master-8100bets.json:
serverSeed = a7775148ccf4ba93bbaba4605c9b055bf6d0971ca1328777d69407fa52584ff6
clientSeed = OFsDIaHWohCPjh0t
nonce = 3
rows = 12
risk = mediumStep 1 — Slot computation: computeSlot(serverSeed, clientSeed, 3, 12) → 12 HMAC calls → final_slot = 8
Step 2 — Multiplier lookup: scaling_edge[0].multipliers["12"]["medium"][8] = 1.11012046
Step 3 — Payout: 0.01 × 1.11012046 = 0.0111012046
| Step | Process | Output |
|---|---|---|
| 1 | HMAC-SHA256 × 12 rows | slot = 8 ✅ |
| 2 | Multiplier: 12r/medium slot 8 | 1.11012046× |
| 3 | RTP check: P(slot=8) × 1.11012046 | Contributes to 99.9% |
| 4 | Payout: 0.01 × 1.11012046 | 0.0111012046 ✅ |
// 12 rows, medium risk — anti-circularity proof// P(slot=k) = C(12,k) × 0.5^12 for k = 0..12//// Σ binomProb(12, k) × scalingEdgeMultiplier(12, "medium", k)// = 0.999000 (99.9%)//// This is a mathematical identity, not a measurement.// Casino supplies multipliers; probabilities are independently computed.Technical Evidence & Verification5 evidence groups
4.10Evidence Coverage Summary6 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Anti-circularity (Step 17) | 351 probabilities, 27/27 RTP cross-checks | Pass |
| Zero Edge audit (Step 10) | Both edge groups, 0 mismatches | Pass |
| Scaling edge (Step 16) | All 27 configs, bracket 0 confirmed | Pass |
| Simulation Pass 1 (Step 19) | 27M rounds, 0/27 chi-squared, 0/27 serial | Pass |
| Simulation Pass 2 (Step 20) | 162 seeds × 10K nonces, 10 flags (p=0.773) | Pass |
| Bet-size invariance (Step 9) | 200/200 Phase C at $10 | Pass |
4.11Code References6 source files
| File | Purpose |
|---|---|
| tests/steps/anti-circularity.ts | Step 17: Probability independence and RTP cross-check |
| tests/steps/dataset.ts | Steps 10, 16: Zero Edge audit, scaling edge analysis |
| tests/steps/simulation.ts | Steps 19–20: Simulation integrity and cherry-pick detection |
| src/simulate.ts | Monte Carlo simulation (27M rounds, two-pass) |
| src/stats.ts | binomProb, chiSquaredTest, lag1Autocorrelation, runsTest |
| src/config.ts | theoreticalRTP, scalingEdgeMultiplier, payoutTableMultiplier |
4.12Datasets UsedSource artifacts
4.13Verified Invariants10 invariants
| Invariant | Result |
|---|---|
| P(slot) × multiplier = 0.999 for all 27 configs (non-circular) | Pass |
| HOUSE_EDGE = 0.001 confirmed from plinkoConfig.json for all 27 configs | Pass |
| Binomial B(rows, 0.5) probabilities match plinkoConfig.json for all 351 config×slot pairs | Pass |
| Simulated RTP average = 99.917% across 27M rounds | Pass |
| 0/27 configs reject at Bonferroni α/27 | Pass |
| 0 serial independence failures at Bonferroni-corrected threshold | Pass |
| No evidence of seed pre-selection across 162 casino seeds (Pass 2) | Pass |
| Phase C ($10) slot positions match Phase B ($0.01) algorithm | Pass |
| Bet amount absent from RNG input by construction | Pass |
| Zero Edge and standard bets use identical multiplier tables | Pass |
4.14Reproduction InstructionsRun locally
git clone https://github.com/ProvablyFair-org/duel-plinko.gitcd duel-plinko && npm installnpm run simulate # 27M simulation + cherry-pick test (~6 min)npm run verify # Steps 9, 10, 16, 17, 19, 20 cover S4cat outputs/simulation-results.jsonS4-related steps:
[PASS] Step 9 — Phase C Code-Path Equivalence
[PASS] Step 10 — Zero Edge Audit
[PASS] Step 16 — Scaling Edge Analysis
[PASS] Step 17 — Probability Independence (Anti-Circularity)
[PASS] Step 19 — Simulation Results — Pass 1 Integrity
[PASS] Step 20 — Simulation Results — Pass 2 Cherry-Pick TestFairness Integrity Testing
Does the implementation maintain fairness under non-standard conditions?
Sections 1–4 prove the game is mathematically fair. Section 5 proves the implementation maintains integrity under non-standard conditions. We applied 15 standard fairness integrity tests covering nonce integrity, seed commitment, outcome determinism, cross-player isolation, and payout integrity. 14 tests passed and 1 is not applicable to this game type.
Adversarial tests check whether inputs, sequences or payouts can be manipulated.
What we verified
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed injection — can server or client seed fields be overridden via API?
- Outcome replay — can a completed bet be replayed for duplicate payouts?
- Cross-player isolation — can one player's seeds or outcomes affect another's?
- Payout tampering — can multiplier or payout values be injected client-side?
- Parameter limits — can invalid row counts or risk levels be submitted?
What this means for you
- Across the 15 tests we ran, no API path allowed outcomes to be altered, replayed, or injected — by player or casino
- Once a bet is placed, the result cannot be changed or replayed
- Each bet is cryptographically unique and isolated
- Your results are independent of every other player
- The server rejects malformed, out-of-range, and duplicate requests
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed injection — can server or client seed fields be overridden via API?
- Outcome replay — can a completed bet be replayed for duplicate payouts?
- Cross-player isolation — can one player's seeds or outcomes affect another's?
- Payout tampering — can multiplier or payout values be injected client-side?
- Parameter limits — can invalid row counts or risk levels be submitted?
- Across the 15 tests we ran, no API path allowed outcomes to be altered, replayed, or injected — by player or casino
- Once a bet is placed, the result cannot be changed or replayed
- Each bet is cryptographically unique and isolated
- Your results are independent of every other player
- The server rejects malformed, out-of-range, and duplicate requests
Nonce integrityPass
Seed commitment integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
15 standard fairness integrity tests: 14 pass, 1 N/A (single-step game). All API probe tests completed.
Category coverage
14 / 14 applicable tests passed · 1 test not applicable to this single-step game.
14 pass · 1 N/A · no fairness failures
| Check | Result | Evidence |
|---|---|---|
| Nonce integrity | Pass | Sequential, server-controlled, no gaps or duplicates across 162 seed pairs |
| Seed commitment integrity | Pass | Locked at bet acceptance, unique per seed pair — 162/162 verified |
| Outcome determinism | Pass | Identical inputs produce identical outcomes — 8,100/8,100 confirmed |
| Round & player isolation | Pass | Per-user seeds, serial independence confirmed (0/27 fail in simulation) |
| Payout integrity | Pass | Parameter limits (rows 8–16, risk 1–3) enforced; client-injected payout fields ignored — server computes outcome from seeds only |
Nonce integrityPass
Seed commitment integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
15 standard fairness integrity tests: 14 pass, 1 N/A (single-step game). All API probe tests completed.
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. Five categories target specific fairness properties. Scope boundary: S5 tests whether fairness guarantees hold under non-standard API interaction. Platform-level implementation testing falls outside standard certification.
| Category | Tests | What It Catches |
|---|---|---|
| Nonce Integrity | 4 | Sequence gaps, server-side nonce manipulation, session continuity |
| Seed Commitment | 5 | Mid-epoch seed changes, seed reuse, predictable seed generation |
| Outcome Determinism | 2 | Non-deterministic outputs, outcome replay |
| Player Isolation | 2 | Cross-round correlation, cross-user outcome dependence |
| Payout Integrity | 2 | Parameter enforcement, server-side computation verification |
5.2Severity Framework & Hard Fail Criteria
Findings are classified by severity. Hard fail criteria — any one triggers a NOT PROVABLY FAIR verdict.
| Severity | Meaning | Action |
|---|---|---|
| PASS | Test passed — no issue detected | None |
| N/A | Test not applicable to this game type | None |
| FLAG | Anomaly detected, documented for transparency | Disclosed |
| HARD FAIL | Fairness guarantee cannot be confirmed | Certification blocked until remediated |
| Condition | Consequence |
|---|---|
| Nonce gap or duplicate within epoch | Outcome sequence integrity broken |
| Server seed changed mid-epoch | Commit-reveal guarantee broken |
| Slot recomputation mismatch | Undisclosed inputs affecting outcomes |
| Client seed not used in HMAC | Player has no influence on outcomes |
15 tests · 14 pass · 1 N/A
Nonce Integrity4/4
FI-NONCE-001PassEach bet increments the nonce sequentially with no gaps, repeats, or resets — preventing the server from skipping unfavourable outcomes
FI-NONCE-002PassNonce progression is server-controlled — the client cannot inject, skip, or replay a nonce value via the API
S1, Step 4 — server controls nonce assignment. Client bet requests do not include a nonce field.
FI-NONCE-003PassSubmitting an invalid or out-of-sequence nonce does not produce a game outcome — the server rejects the request
API probe — 7 adversarial nonce values (−1, 0, 999999999, 'abc', INT32_MAX, INT32_MAX+1, 9e18) injected in request body. All 7 silently ignored; server assigned valid sequential nonces. Continuity preserved (baseline 18 → post-test 26); no stack traces leaked.
FI-NONCE-004PassNonce sequence continues correctly after disconnect/reconnect — no reset to zero mid-epoch
Seed Commitment5/5
FI-SEED-001PassEmpty, null, or invalid client seeds are handled deterministically — the server enforces input requirements
API probe — 6/7 adversarial seeds rejected with HTTP 422 (empty, null, special chars, emoji, CJK, whitespace-only). Oversized seed (>1000 chars) blocked at HTTP 400 by active-game guard before format check — correct safety behaviour. All Plinko bets after each rejection remained functional; no stack traces leaked.
FI-SEED-002PassOnce a bet is accepted, the seed pair is locked for the epoch — no mid-epoch mutation of server or client seed is possible
FI-SEED-003PassEach epoch uses a unique server seed — no seed is reused across epochs or sessions
S1, Step 1 — 162 distinct revealed server seeds, 0 reuse detected.
FI-SEED-004PassNo shared seed pool across users — each player's seeds are independently generated and isolated
API probe — 10 concurrent seed rotations on Account A and 10 on Account B. Zero hash collisions across 20 rotations; all hashes within each account distinct. Full account isolation confirmed.
FI-SEED-005PassServer seeds show no correlation with timestamps, sequential patterns, or other predictable inputs
API probe — 10 rotations spaced 5 seconds apart over ~52 seconds. All 10 server seed hashes distinct; no two shared 6+ leading hex characters. Time-correlated derivation ruled out.
Outcome Determinism2/2
FI-OUTCOME-001PassGiven identical inputs (serverSeed, clientSeed, nonce, rows), the game always produces the same slot — verified across all 8,100 live bets
S3, Step 5 — 8,100/8,100 bets independently recomputed from seeds, 0 mismatches.
FI-OUTCOME-002N/AA completed bet cannot be replayed via the API to generate a duplicate payout
Plinko auto-resolves — single-step game with no separate cashout action. Nonce increments server-side after each bet. No replay surface exists.
Player Isolation2/2
FI-ISO-001PassRNG state is fully independent across rounds — no carry-over from one bet to the next. Each nonce produces a fresh HMAC-SHA256 computation
FI-ISO-002PassOne player's seeds, nonces, and outcomes cannot be observed or influenced by another player — complete cross-user isolation
Follows from seed uniqueness (FI-SEED-003) — if each user has a unique server seed, outcomes are isolated by construction. Confirmed by FI-SEED-004 — zero hash collisions across 20 cross-account rotations.
Payout Integrity2/2
FI-PAYOUT-001PassGame parameters cannot exceed defined limits — only valid row counts (8–16) and risk levels (low, medium, high) are accepted
API probe — 6/6 invalid parameter combinations rejected with HTTP 422 (rows = −1, 0, 17, 99; risk_level = 'extreme' string, 4). All 3 control bets and recovery bet accepted. No out-of-range values silently coerced.
FI-PAYOUT-002PassMultiplier and payout fields in the API request are ignored — the server computes all values from its own formula, not from client-supplied values
API probe — 5 injected fields (payout_multiplier=1000, slot_result=0, final_slot=0, effective_edge=−99, win_amount='99999') all silently ignored. Server computed its own slot and multiplier on every bet from seeds + nonce only. noInjectionHonoured=true.
Technical Evidence & Verification4 evidence groups
5.3Coverage Summary15 checks
| Test ID | Category | Verification Source | Status |
|---|---|---|---|
| FI-NONCE-001 | Nonce | S1, Step 4 (data-driven) | Pass |
| FI-NONCE-002 | Nonce | S1, Step 4 (data-driven) | Pass |
| FI-NONCE-003 | Nonce | API probe | Pass |
| FI-NONCE-004 | Nonce | S1, Step 4 (data-driven) | Pass |
| FI-SEED-001 | Seed | API probe | Pass |
| FI-SEED-002 | Seed | S1, Steps 3 & 6 (data-driven) | Pass |
| FI-SEED-003 | Seed | S1, Step 1 (data-driven) | Pass |
| FI-SEED-004 | Seed | API probe | Pass |
| FI-SEED-005 | Seed | API probe | Pass |
| FI-OUTCOME-001 | Determinism | S3, Step 5 (data-driven) | Pass |
| FI-OUTCOME-002 | Determinism | Structural — single-step game | N/A |
| FI-ISO-001 | Isolation | S2, Step 19 (simulation) | Pass |
| FI-ISO-002 | Isolation | Structural — seed uniqueness | Pass |
| FI-PAYOUT-001 | Payout | API probe | Pass |
| FI-PAYOUT-002 | Payout | API probe | Pass |
5.4Additional Integrity Evidence (S1–S4)6 records
| Property | Source | Finding |
|---|---|---|
| 162/162 seed hashes verified | S1, Step 1 | Commit-reveal chain intact |
| 166/166 next-seed promotions | S1, Step 2 | Seed rotation chain intact |
| 8,100/8,100 exact parity | S3, Step 5 | No post-RNG conditional logic |
| Anti-circularity proven | S4, Step 17 | 0.1% house edge (bracket 0) via independent probabilities × published table |
| 10 early-window flags, p=0.773 (within expected range) | S4, Step 20 | No seed pre-selection bias |
| 84.2% client seed influence | S1, Step 6 | Player entropy is genuine |
5.5Scope & LimitationsScope note
5.6Reproduction InstructionsRun locally
Data-driven tests (8 of 15): Fully reproducible from the open-source audit repo. These tests run against the captured dataset and produce deterministic results.
API probe tests (6 of 15): Live API probes against duel.com captured at audit time. Per-test evidence (requests, responses, server-assigned values, timing) is retained in our private adversarial-testing archive and summarised in the integrity test review document. The testing harness itself is kept private to avoid handing exploit primitives to players.
git clone https://github.com/ProvablyFair-org/duel-plinko.gitcd duel-plinkonpm installnpm run verifyExpected output (S5-related steps):
[PASS] Step 1 — Seed Hash Integrity → FI-SEED-003
[PASS] Step 3 — Hash Consistency → FI-SEED-002
[PASS] Step 4 — Nonce Audit → FI-NONCE-001, 002, 004
[PASS] Step 5 — Slot Recomputation → FI-OUTCOME-001
[PASS] Step 6 — Client Seed Influence → FI-SEED-002
[PASS] Step 19 — Simulation Pass 1 → FI-ISO-001API Probe Tests (completed):
[PASS] FI-NONCE-003 — Invalid nonce handling → 7/7 invalid nonces ignored[PASS] FI-SEED-001 — Invalid client seed handling → 6/7 rejected (1 active-game blocked)[PASS] FI-SEED-004 — Cross-user seed pool check → 0/20 collisions across 2 accounts[PASS] FI-SEED-005 — Seed timing analysis → 10 distinct hashes, no time correlation[PASS] FI-PAYOUT-001 — Invalid parameter handling → 6/6 boundary violations rejected (HTTP 422)[PASS] FI-PAYOUT-002 — Multiplier field handling → 0 injections honouredPlayer 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 your calculated slot position matches the game result, the bet was provably fair. This section walks you through the process — and provides an independent verification tool built from the same code used in this audit.
Use the revealed server seed, your client seed, nonce and row count to check your bet.
Key principles
- Every Plinko outcome can be independently reproduced
- No hidden variables — no private backend data
- If your computed slot matches the game result, the bet was provably fair
- Most players can verify directly through the Duel.com fairness UI
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)
- Every Plinko outcome can be independently reproduced
- No hidden variables — no private backend data
- If your computed slot matches the game result, the bet was provably fair
- Most players can verify directly through the Duel.com fairness UI
- 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)
Player can access all required inputsPass
Client seed origin transparentPass
Verification process documentedPass
Casino UI exposes verification panelPass
Independent verifier availablePass
Manual recomputation possiblePass
No hidden or proprietary stepsPass
Only disclosed inputs are used. Identical inputs always produce identical output.
- 1
Place a Plinko Bet
Choose your row count, risk level, and bet amount. The game generates a result using the provably fair algorithm.
- 2
Open the Fairness Modal
Open the Provably Fair modal on the game page. You'll see the active client seed, the hashed server seed, and your current nonce. Rotate the seed pair to unlock the revealed plaintext server seed for your previous bets.
- 3
Open Past Bet to Reveal Seed
Open Transactions and click a past Plinko round. The per-bet modal shows the revealed plaintext server seed alongside the bet ID, client seed, and nonce.
- 4
Recompute & Confirm
Click Verify in the per-bet modal to open the Provably Fair page with the seeds pre-populated. The page recomputes the slot position inline — if it matches your live game result, the bet was provably fair.
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
Verify with the audit code
Independent verification using the implementation behind this report.
Open independent verifierBuilt-in verification
The operator’s own verification page. This tool is controlled by Duel.com.
Open Duel verifierVisual Walkthroughstep-by-step
Select your row count and risk level, then place a bet. The ball drops through the peg board and lands in a slot. The server has already committed to the outcome before you clicked.

Duel.com Plinko — ball-drop game with row and risk selector. Slot outcome is locked before the ball drops.
Open the Provably Fair modal on the game page. You'll see the active client seed, the hashed server seed (the casino's pre-commitment), and your current nonce. Below those, you can set the new client seed that will become active after rotation. Click 'Rotate seed' to retire the active seed pair and start a new one — this is what unlocks the revealed plaintext server seed for your previous bets in Step 3.

Provably Fair modal — active client seed, hashed server seed, nonce, customisable new client seed, next pair pre-commitment, and Rotate seed action.
Open Transactions and click a past Plinko round. The per-bet modal shows the revealed plaintext server seed alongside the bet ID, client seed, and nonce. The pre-committed hash from Step 2 must match SHA-256 of this revealed seed:
Server Seed (revealed) — plaintext value the casino committed to before your bet. Hash it to confirm it matches the pre-commitment.
Client Seed — your player-controlled entropy input.
Nonce — the sequential bet counter for this seed pair.

Per-bet transaction modal — revealed plaintext server seed, client seed, and nonce for the round.
Click Verify in the per-bet modal to open the Provably Fair page with the seeds pre-populated. The page recomputes the slot position from the disclosed inputs and renders the Game Result inline. If the recomputed slot matches your live game result, the bet was provably fair — the casino committed to the outcome before you bet, you contributed entropy via your client seed, and the result is mathematically reproducible by anyone.

Provably Fair page — seeds populated from the per-bet modal, recomputed slot rendered inline matching the live game result.
Manual Verification (Advanced)6 sections
6.6Why Manual Verification Matters
True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.
6.7How the Algorithm Works (Plain English)
Before you play, the server locks the slot outcome using three ingredients:
- The server's secret seed — committed by publishing its hash before you bet
- Your client seed — generated by your browser, unknown to the server
- The nonce — a counter that makes each bet unique
These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a bounce direction at each row of the peg board. The ball bounces left or right at each row — the final slot is the count of right-bounces. Because the server committed to its seed before you bet, and your client seed is generated in your browser, neither party can predict or influence the outcome.
6.8Casino Verifier vs ProvablyFair.org Verifier4 records
Two verification tools are available. Both should produce identical results — if they don't, something has changed.
| Duel.com Verifier | ProvablyFair.org Verifier | |
|---|---|---|
| Source | Casino-controlled | Independent (audit codebase) |
| Access | duel.com/fairness/verify | audit.provablyfair.org/casino/duel/tools/verify-bets |
| Trust model | Requires trusting the casino | Open-source, version-controlled |
| Monitoring | No change detection | Mismatches detected if casino changes logic |
6.9JavaScript Verification Script
Copy and run this in Node.js to verify any Plinko bet:
const crypto = require('crypto'); function computeSlot(serverSeed, clientSeed, nonce, rows) { const key = Buffer.from(serverSeed, 'hex'); let slot = 0; for (let cursor = 0; cursor < rows; cursor++) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); slot += parseInt(hmac.substring(0, 8), 16) % 2; } return slot;} function verifyHash(serverSeed, serverSeedHashed) { const hash = crypto .createHash('sha256') .update(Buffer.from(serverSeed, 'hex')) .digest('hex'); return hash === serverSeedHashed;} // Replace with your valuesconst serverSeed = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const clientSeed = 'YOUR_CLIENT_SEED';const nonce = 0;const rows = 16; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Computed slot:', computeSlot(serverSeed, clientSeed, nonce, rows));6.10Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac def compute_slot(server_seed, client_seed, nonce, rows): key = bytes.fromhex(server_seed) slot = 0 for cursor in range(rows): message = f'{client_seed}:{nonce}:{cursor}'.encode() h = hmac.new(key, message, hashlib.sha256).hexdigest() slot += int(h[:8], 16) % 2 return slot def verify_hash(server_seed, server_seed_hashed): computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest() return computed == server_seed_hashed # Replace with your valuesserver_seed = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'client_seed = 'YOUR_CLIENT_SEED'nonce = 0rows = 16 print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Computed slot:', compute_slot(server_seed, client_seed, nonce, rows))6.11Evidence Screenshots4 records
| Evidence | Description |
|---|---|
| S08 | Fairness page overview — "What is Provably Fair?" and "How it works" sections |
| S09 | Fairness verification tool — Plinko selected, showing Rows and Risk Level inputs |
| S10 | Fairness page Plinko source code — full validatePlinkoResult and generateHMAC_SHA256 implementation |
| S11 | Client seed rotation response — server echoes client-submitted seed, does not assign |
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
How it works
- 1
Place a Plinko Bet
Choose your row count, risk level, and bet amount. The game generates a result using the provably fair algorithm.
- 2
Open the Fairness Modal
Open the Provably Fair modal on the game page. You'll see the active client seed, the hashed server seed, and your current nonce. Rotate the seed pair to unlock the revealed plaintext server seed for your previous bets.
- 3
Open Past Bet to Reveal Seed
Open Transactions and click a past Plinko round. The per-bet modal shows the revealed plaintext server seed alongside the bet ID, client seed, and nonce.
- 4
Recompute & Confirm
Click Verify in the per-bet modal to open the Provably Fair page with the seeds pre-populated. The page recomputes the slot position inline — if it matches your live game result, the bet was provably fair.
Technical evidence
6.6Why Manual Verification Matters
True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.
6.7How the Algorithm Works (Plain English)
Before you play, the server locks the slot outcome using three ingredients:
- The server's secret seed — committed by publishing its hash before you bet
- Your client seed — generated by your browser, unknown to the server
- The nonce — a counter that makes each bet unique
These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a bounce direction at each row of the peg board. The ball bounces left or right at each row — the final slot is the count of right-bounces. Because the server committed to its seed before you bet, and your client seed is generated in your browser, neither party can predict or influence the outcome.
6.8Casino Verifier vs ProvablyFair.org Verifier4 records
Two verification tools are available. Both should produce identical results — if they don't, something has changed.
| Duel.com Verifier | ProvablyFair.org Verifier | |
|---|---|---|
| Source | Casino-controlled | Independent (audit codebase) |
| Access | duel.com/fairness/verify | audit.provablyfair.org/casino/duel/tools/verify-bets |
| Trust model | Requires trusting the casino | Open-source, version-controlled |
| Monitoring | No change detection | Mismatches detected if casino changes logic |
6.9JavaScript Verification Script
Copy and run this in Node.js to verify any Plinko bet:
const crypto = require('crypto'); function computeSlot(serverSeed, clientSeed, nonce, rows) { const key = Buffer.from(serverSeed, 'hex'); let slot = 0; for (let cursor = 0; cursor < rows; cursor++) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); slot += parseInt(hmac.substring(0, 8), 16) % 2; } return slot;} function verifyHash(serverSeed, serverSeedHashed) { const hash = crypto .createHash('sha256') .update(Buffer.from(serverSeed, 'hex')) .digest('hex'); return hash === serverSeedHashed;} // Replace with your valuesconst serverSeed = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const clientSeed = 'YOUR_CLIENT_SEED';const nonce = 0;const rows = 16; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Computed slot:', computeSlot(serverSeed, clientSeed, nonce, rows));6.10Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac def compute_slot(server_seed, client_seed, nonce, rows): key = bytes.fromhex(server_seed) slot = 0 for cursor in range(rows): message = f'{client_seed}:{nonce}:{cursor}'.encode() h = hmac.new(key, message, hashlib.sha256).hexdigest() slot += int(h[:8], 16) % 2 return slot def verify_hash(server_seed, server_seed_hashed): computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest() return computed == server_seed_hashed # Replace with your valuesserver_seed = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'client_seed = 'YOUR_CLIENT_SEED'nonce = 0rows = 16 print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Computed slot:', compute_slot(server_seed, client_seed, nonce, rows))6.11Evidence Screenshots4 records
| Evidence | Description |
|---|---|
| S08 | Fairness page overview — "What is Provably Fair?" and "How it works" sections |
| S09 | Fairness verification tool — Plinko selected, showing Rows and Risk Level inputs |
| S10 | Fairness page Plinko source code — full validatePlinkoResult and generateHMAC_SHA256 implementation |
| S11 | Client seed rotation response — server echoes client-submitted seed, does not assign |
Only disclosed inputs are used. Identical inputs always produce identical output.
Reproducibility & Artifacts
Can anyone independently reproduce every finding in this audit?
This section consolidates the open-source repository, datasets, output artifacts, and reproducibility posture of the audit. Every finding, every statistic, every pass/fail result can be independently reproduced by anyone with a computer and an internet connection. The repository is the credential — not this report.
Clone the pinned repository and run the same audit locally. Every dataset and result is published.
Repository details
- GitHub: ProvablyFair-org/duel-plinko
- Commit: 5bb2cd4a0ea159af118c3000b74298a16b2f89ad
- Game: Plinko (duel-plinko)
- Public Verifier: audit.provablyfair.org/casino/duel/tools/verify-bets
Prerequisites
- Node.js 18+
- npm 8+
- Git
- TypeScript (installed via npm)
- GitHub: ProvablyFair-org/duel-plinko
- Commit: 5bb2cd4a0ea159af118c3000b74298a16b2f89ad
- Game: Plinko (duel-plinko)
- Public Verifier: audit.provablyfair.org/casino/duel/tools/verify-bets
- Node.js 18+
- npm 8+
- Git
- TypeScript (installed via npm)
AUDITED COMMIT
5bb2cd4 · Browse this revision ↗
Installs TypeScript, ts-node, and cryptographic dependencies. npm test runs mocha (15 unit tests), then the 27M-round simulation, then the 20-step verification pipeline.
Expected verification output
Simulates 1,000,000 rounds for each of the 27 configurations (27M total). Runtime ~6 minutes. Output written to outputs/simulation-results.json and outputs/rtp-convergence.html.
Audit Reproducibility Pinning
- Git Commit
- 5bb2cd4a0ea159af118c3000b74298a16b2f89ad
- Node Version
- v18+ (tested on v22.x)
- Dataset
- data/plinko-master-8100bets.json (8,100 bets, 162 active seeds + 4 boundary)
- Dataset Hash (SHA-256)
- 3cf9359d88220bc800bb32edfe04399f55b909302262928ee3a0582715635267
- Audit Date
- April 2026
- Audit ID
- PF-2026-DL01
Audit Reproducibility Pinning
- Git Commit
- 5bb2cd4a0ea159af118c3000b74298a16b2f89ad
- Node Version
- v18+ (tested on v22.x)
- Dataset
- data/plinko-master-8100bets.json (8,100 bets, 162 active seeds + 4 boundary)
- Dataset Hash (SHA-256)
- 3cf9359d88220bc800bb32edfe04399f55b909302262928ee3a0582715635267
- Audit Date
- April 2026
- Audit ID
- PF-2026-DL01
Output Artifacts
- verification-results.json · ~39KB
- Steps 1–20 results, per-step pass/fail with evidence
- simulation-results.json · ~162KB
- 27M-round RTP, chi-squared, cherry-pick detection (Pass 1 + Pass 2)
- determinism-log.json · ~1.5MB
- Per-bet (computed_slot vs actual_slot) for all 8,100
- chi-squared-results.json · ~18KB
- Per-config chi-squared statistics and p-values
- rtp-convergence.html · ~5KB
- Interactive RTP convergence chart (self-contained HTML)
Output Artifacts5 files generated
| File | Contents | Size |
|---|---|---|
| verification-results.json | Steps 1–20 results, per-step pass/fail with evidence | ~39KB |
| simulation-results.json | 27M-round RTP, chi-squared, cherry-pick detection (Pass 1 + Pass 2) | ~162KB |
| determinism-log.json | Per-bet (computed_slot vs actual_slot) for all 8,100 | ~1.5MB |
| chi-squared-results.json | Per-config chi-squared statistics and p-values | ~18KB |
| rtp-convergence.html | Interactive RTP convergence chart (self-contained HTML) | ~5KB |
Repository Structure
duel-plinko/ ├── src/ │ ├── rng.ts → HMAC-SHA256 slot computation │ ├── config.ts → PlinkoConfig multiplier lookup + RTP │ ├── simulate.ts → Monte Carlo — 1M rounds/config × 27 │ ├── stats.ts → Chi-squared, autocorrelation, runs test │ ├── loader.ts → Dataset loader + SHA-256 hash guard │ └── types.ts → Type definitions ├── tests/ │ ├── verify.ts → 20-step verification pipeline │ ├── steps/ │ │ ├── commitment.ts → Steps 1–4: Commit-reveal integrity │ │ ├── determinism.ts → Steps 5–6: Slot recomputation + client seed │ │ ├── payouts.ts → Steps 7–9: Payout math + Phase C │ │ ├── dataset.ts → Steps 10–16: Dataset integrity checks │ │ ├── anti-circularity.ts → Step 17: Probability independence │ │ ├── phase-d.ts → Step 18: Client seed variation │ │ ├── simulation.ts → Steps 19–20: Simulation integrity │ │ ├── statistical.ts → Informational: RTP, serial, chi-squared │ │ └── context.ts → Shared context + pass/fail helpers │ └── plinko/ │ └── PlinkoTests.ts → 15 unit tests (Mocha) ├── data/ │ └── plinko-master-8100bets.json → 8,100 live bets (4 phases, 166 seeds) ├── outputs/ → Generated by npm test │ ├── verification-results.json → Steps 1–20 pass/fail │ ├── simulation-results.json → 27M rounds, per-config RTP + cherry-pick │ ├── determinism-log.json → Per-bet slot recomputation log │ ├── chi-squared-results.json → Distribution test results │ └── rtp-convergence.html → Interactive RTP convergence chart ├── evidence/ │ ├── S01–S11 *.png → UI, fairness page, seed rotation evidence │ └── client-seed-origin.png → Client seed origin evidence ├── capture/ │ ├── plinko-capture.reference.js → Browser bet capture script │ └── capture-auto.reference.js → Capture automation ├── results/ → Merged capture working directory ├── plinkoConfig.json → Duel.com game configuration (27 configs) ├── README.md ├── MANIFEST.md ├── package.json ├── tsconfig.json └── .mocharc.yml
Step-to-Section Cross-Reference20 verification steps mapped
| Step | Test Name | Report Section |
|---|---|---|
| 1 | Seed Hash Integrity | S1 |
| 2 | Next-Seed Promotion (Commitment Linkage) | S1 |
| 3 | Hash Consistency Within Epoch | S1 |
| 4 | Nonce Audit | S1, S5 |
| 5 | Slot Recomputation (RNG determinism) | S1, S2, S3 |
| 6 | Client Seed Influence | S1, S2 |
| 7 | Payout Math | S3, S4 |
| 8 | Multiplier Table Provenance | S3, S4 |
| 9 | Phase C Code-Path Equivalence | S3, S4 |
| 10 | Zero Edge Audit | S4 |
| 11 | Config Completeness | S3 |
| 12 | Epoch Size | S1, S3 |
| 13 | Multiplier Table + Symmetry | S3 |
| 14 | Phase Labels | S3 |
| 15 | Dataset Hash | S7 |
| 16 | Scaling Edge Analysis | S4 |
| 17 | Probability Independence (Anti-Circularity) | S4 |
| 18 | Phase D — Client Seed Variation | S1, S2 |
| 19 | Simulation Pass 1 Integrity | S2, S4 |
| 20 | Simulation Pass 2 Cherry-Pick Test | S4 |
reproduce-audit.sh
All audit results can be independently reproduced using the pinned commit, dataset, and commands above. The dataset hash ensures you're running against the same 8,100 bets.