Plinko Audit
Provably Fair audit of the Duel Plinko game
Plinko Overview
This audit evaluates the Plinko game operated by Duel Casino under the ProvablyFair.org Audit Framework v1.0. Plinko is a prediction game where a ball drops through a board of pegs and lands in a slot. The outcome is determined by the path taken, which maps to a multiplier based on risk level and row configuration.
Purpose
This audit determines whether outcomes are cryptographically reproducible, statistically fair, and resistant to manipulation by either players or the casino.
What This Audit Evaluates
- Deterministic outcome generation
- Entropy integrity
- Live-to-verifier parity
- RTP mathematical accuracy
- Fairness integrity testing
What Was Audited
- RNG algorithm determinism and verifiability
- Server seed commitment timing and reveal
- Client seed control
- Nonce lifecycle and uniqueness
- Path/slot generation and multiplier logic
- Theoretical RTP per risk level
- Provably fair outcome generation
- Independent player verification path
- Commit-reveal cryptographic integrity
What Audit Covers
The following pillars define the scope of the analysis and map directly to the audited Plinko components.
Commit-Reveal System
Server seed hashing, timing, and reveal mechanics
Seed Handling
Client seed control and nonce lifecycle
RNG Analysis
Path/slot generation and bias testing
Payout Logic
Multiplier accuracy and slot-to-payout verification
Live Parity
Verifier versus live-game result matching
RTP Validation
Theoretical and simulated RTP per risk level
✅ What the audit guarantees
- Outcomes are deterministic and reproducible.
- Live game results match independent recomputation (7,600/7,600).
- Slot distribution follows the expected binomial B(n, 0.5).
- Theoretical RTP is analytically validated at 99.900%.
- All fairness integrity checks passed at audit time.
⚠️ What the audit does not cover
- Infrastructure and server security.
- Wallet, payments, or custody systems.
- Operational controls outside game logic.
- Bet-size brackets above bracket 0 (bets above ~$335 face higher house edge).
- Per-player discrimination testing (single-account audit scope).
Audit Verdict
Public links
To facilitate independent reproduction of our findings, the full audit repository and verification resources are public.
For full reproduction instructions, see Section 7: Reproducibility & Artifacts.
References
Plinko - Game Rules
Plinko is a binary-path game where a ball drops through a triangular grid of pegs. At each peg, the ball bounces left or right. The number of rows is player-configurable: 8, 9, 10, 11, 12, 13, 14, 15, or 16. For each row, HMAC-SHA256 produces one bit (left or right). The final slot equals the count of right-turns; slot range is 0 to rows.
How a bet works
- You select rows (8–16), risk level (low, medium, or high), and bet amount.
- System combines server seed, client seed, and nonce.
For each row (cursor 0 to rows-1), the game computes
HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor), takes the first 4 bytes as uint32, then uses mod 2.- Each row outputs left (0) or right (1). Final slot is the total count of right-turns.
- Win amount = bet amount × multiplier[slot] from the selected multiplier table.
Risk versus reward
- High risk: Center slots pay below 1× (e.g., 0.2× for 16 rows), edge slots pay extremely high (e.g., 1,009× for 16 rows). Volatile returns, same expected RTP.
- Low risk: Center slots pay close to 1×, edge slots pay moderately (e.g., 5.6× for 8 rows). Smoother returns, lower variance.
- RTP consistency: Risk level changes payout distribution between center and edge slots, while expected RTP remains ~99.9% in bracket 0.
Multiplier calculation
Each slot has a multiplier from the configured multiplier table. Win amount = bet amount × multiplier for slot. The casino designs each table so theoretical RTP ≈ 99.9% across all 27 configurations.
Game parameters
| Parameter | Value | Audit note |
|---|---|---|
| Row count | 8–16 | Player selects; determines slot count (rows + 1) |
| Risk levels | Low, Medium, High | Affects multiplier table only, not ball path |
| Slot range | 0 to N | N = number of rows; slot = count of right-turns |
| Slot distribution | Binomial B(N, 0.5) | Each row is independent 50/50 |
| House edge | 0.1% (bracket 0) | Progressive: scales to 2% at highest stakes |
| Theoretical RTP | 99.9% (bracket 0) | Verified across all 27 configurations |
| Configurations | 27 | 9 row counts × 3 risk levels |
| Epoch size | 50 bets | Server seed rotates every 50 bets |
Seed formats
| Input | Format | Example | Purpose |
|---|---|---|---|
| Server seed | 64-char hex (32 bytes) | 4f775f81301c7fe8... | Casino-provided randomness |
| Client seed | Alphanumeric string | kJbhRHVAg4lh_OY7 | Player-controlled randomness |
| Nonce | Integer, starts at 0 | 0, 1, 2, ... | Ensures unique result per bet |
Server seed, client seed, and nonce are combined via HMAC-SHA256. For each row (cursor 0 to rows-1), the message is clientSeed:nonce:cursor. The first 4 bytes of the HMAC output, as uint32, mod 2, produce left (0) or right (1). The ball's final slot is the sum of all right-turns.
Example: 16 rows, high risk
| Slot | Probability | Multiplier |
|---|---|---|
| 0 or 16 (edge) | 0.00153% | 1009.33× |
| 1 or 15 | 0.0244% | 130.78× |
| 2 or 14 | 0.183% | 26.24× |
| 3 or 13 | 0.854% | 9.00× |
| 4 or 12 | 2.78% | 4.00× |
| 5 or 11 | 6.67% | 2.00× |
| 6 or 10 | 12.2% | 0.20× |
| 7 or 9 | 17.5% | 0.20× |
| 8 (center) | 19.6% | 0.20× |
Edge slots (0 and 16) have probability ~1 in 65,536, so they are uncommon in captures of a few thousand bets.
Why Provably Fair Matters
Traditional online casinos require you to trust that games are fair. Provably fair systems eliminate this need for trust by allowing you to mathematically verify that outcomes were not manipulated.
In a Provably Fair system:
- The casino commits to a result before you place your bet.
- You contribute randomness that the casino cannot predict.
- Anyone can verify the outcome afterward.
High-Level Overview
Provably fair model
Every Plinko drop uses server seed, client seed, and nonce. The casino commits to its seed before you bet; you control your client seed; the nonce increments automatically. At each row, HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) produces one bit. Final slot = sum of right-turns across all rows.
High-level flow
Player bets
Player submits a Plinko bet request with risk level and rows.
Seeds combined
System combines server seed, client seed, and nonce.
RNG output generated
HMAC-SHA256 produces one bit per row (left or right); final slot = sum of right-turns.
Game logic evaluated
Game logic determines path/slot and applies multiplier.
Payout resolved
Payout resolves based on landing slot and multiplier table.
Commit-reveal model
Commit
Casino publishes only the SHA-256 hash of the server seed.
Bet
Your client seed contributes independent entropy.
Reveal
Casino reveals server seed after resolution.
Verify
Hashing revealed seed must match committed hash.
Technical Glossary
Core concepts
- Provably fair: Cryptographic proof that outcomes are not manipulated.
- Commit-reveal protocol: Hash commitment before bet, value reveal after bet.
- Determinism: Identical inputs always produce identical outputs.
Plinko-specific terms
- Path: Sequence of left/right decisions at each peg row; each row is an independent 50/50.
- Slot: Final landing position (0 to N), equal to the count of right-turns across all rows.
- Risk level: Low, medium, or high; affects multiplier table only, not ball path; same RTP across levels.
- Row count: Number of peg rows (8–16); determines slot count (rows + 1) and binomial distribution.
Seed system
- Server seed: Casino-generated random value, revealed after commitment period.
- Client seed: Player-controlled random value used in outcome generation.
- Nonce: Sequential counter incremented per bet under the same seed pair.
Verification terms
- Verifier: Tool that recomputes outcomes from disclosed inputs.
- Parity: Match rate between live-game and verifier outcomes.
- Reproducibility: Ability to regenerate historical outcomes exactly.
Summary Verdict
1. Seed, Nonce & Determinism
Player question: Can the casino change your outcome after you bet?
Every Plinko drop on Duel is generated from three inputs: server seed, client seed, and nonce. The casino commits to its seed before you bet, you control your own client seed, and the nonce increments automatically. At each row, these inputs are combined via HMAC-SHA256 to produce a single left/right decision. The ball's final slot is the sum of all right-turns across every row.
This section validates whether Duel seed handling meets provably fair standards and whether the implementation is deterministic and tamper-resistant.
Verdict summary
How It Works
This section documents how Duel Plinko handles server seeds, client seeds, and nonces to produce deterministic, tamper-proof outcomes.
1.1 Server 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. When you rotate your client seed, the previous server seed is revealed. The auditor hashes the revealed value and confirms it matches the pre-committed hash.
Result: all 152 revealed seeds pass
SHA-256(hexDecode(serverSeed)) = serverSeedHashed with zero mismatches.
1.2 Commitment linkage (pre-bet verification)
At the start of each phase (A, B, C), the active server_seed_hashed was captured before betting.
After the first bet of each phase, the response hash was compared against the pre-captured commitment.
Result: all 3/3 pre-capture commitments match the first bet response hash.
Known limitation: explicit pre-bet snapshots were captured at three phase boundaries only. For remaining seed pairs, commitment appears first in the first post-rotation bet response. Step 3 verifies hash constancy within each seed pair; Step 1 verifies revealed-seed hash correctness.
1.3 Hash consistency within epoch
For each of the 152 seed pairs, server_seed_hashed is checked across all bets in the
pair. Any intra-pair hash change is a commit-reveal violation.
Result: 152/152 seed pairs show a constant hash across all bets. No mid-session seed switches.
1.4 Player client seed control
You can view, modify, or randomize your client seed in the Duel UI before placing bets. The client seed is a direct
HMAC input. Phase D explicitly tests 10 distinct client seeds to confirm active usage.
1.5 Nonce incrementation
Nonce starts at 0 and increments by +1 per bet under the same server/client seed pair.
Nonce resets to 0 only when the player rotates client seed and a new server seed is issued. There is no
automatic rotation after a fixed number of bets.
Result: 152 seed pairs checked; 147 show clean nonce sequences. 5 show a
capture-retry artifact (informational) with no fairness impact; slot recomputation still passes for captured bets.
1.6 Deterministic mapping
Given identical (serverSeed, clientSeed, nonce, rows), the algorithm always returns the same slot.
All 7,600 bets with revealed seeds recompute to the exact final_slot. drand is not
required for slot computation.
1.7 Client seed influence
Recomputing all bets with WRONG_CLIENT_SEED_FOR_AUDIT_TEST changes 6,409/7,600 slots
(84.3%). This confirms client seed is a genuine HMAC input.
Unchanged outcomes (15.7%) are expected coincidences where row bits happen to match despite different
client seeds.
What this means for players
- The casino cannot change your outcome after you place a bet.
- You contribute your own randomness through the client seed.
- Every bet stays unique because nonce increments per bet.
- Any Plinko result can be independently verified.
- Outcomes stay reproducible over time from disclosed inputs.
Technical Evidence and Verification
1.8 Purpose
This section indexes reproducible technical artifacts for Section 1 checks.
Audit status: Pass.
1.9 Evidence coverage summary
| Verification area | Coverage | Result |
|---|---|---|
| Server seed commit and reveal | 152 seed entries | PASS |
| Commitment linkage | 3 pre-capture commitments | PASS |
| Hash consistency within seed pair | 152 seed pairs | PASS |
| Client seed usage | 152 epochs + Phase D (10 seeds) | PASS |
| Nonce incrementation | 152 epochs, 7,600 transitions | PASS (5 capture-retry flags) |
| Deterministic recomputation | 7,600 / 7,600 bets | PASS |
| Client seed influence | 6,409 / 7,600 slots changed | PASS |
1.10 Code references
| File | Purpose |
|---|---|
tests/verify.ts | Steps |
src/rng.ts |
|
src/loader.ts | Dataset loader for seeds, bets, and config |
capture/plinko-capture.js | Browser capture script with instance guard, auto-save, and rotation checkpoints |
1.11 Datasets used
| Property | Value |
|---|---|
| Primary dataset | results/merged/plinko-master.json |
| Source | https://duel.com/plinko |
| Total records | 7,600 bets across 152 seed epochs |
| SHA-256 | 8382e45f8cdf4d439a8866669d15e6f4be543f4b926fb64c67e09d9da7d6b2db |
| Supplementary dataset | results/plinko-phase-d.json (500 bets, 10 configs, 11 seed entries) |
1.12 Verified invariants (seed / nonce)
| Invariant | Result |
|---|---|
SHA-256(hexDecode(serverSeed)) = serverSeedHashed for all 152 entries | Pass |
Pre-capture commitment hash matches first bet response hash (3/3 phases) | Pass |
server_seed_hashed constant across all bets in each seed pair (152 pairs) | Pass |
Nonce starts at 0 when a new server seed is issued | Pass |
| A new server seed is issued when the player rotates client seed | Pass |
Nonce increments by exactly +1 for each bet | Pass |
| Nonce does not decrement, skip, or reset within the same seed pair | Pass (5 capture-retry flags) |
| Nonce values are not reused under the same server+client seed pair | Pass |
| Single client seed per epoch | Pass |
Client seed is a genuine HMAC input (84.3% slots changed with wrong seed) | Pass |
7,600/7,600 slots recompute correctly from disclosed inputs | Pass |
1.13 Reproduction instructions
git clone https://github.com/ProvablyFair-org/duel-audit
cd duel-audit
npm install
npx ts-node tests/verify.ts
Steps 1-6 cover all Seed, Nonce and Determinism checks.
Expected output
[PASS] Step 1 - Seed Hash Integrity
[PASS] Step 2 - Commitment Linkage
[PASS] Step 3 - Hash Consistency Within Epoch
[PASS] Step 4 - Nonce Audit
[PASS] Step 5 - Slot Recomputation
[PASS] Step 6 - Client Seed Influence
All tested Plinko outcomes are fully deterministic and can be independently reproduced using the disclosed server seed, client seed, and nonce.
2. RNG & Entropy Integrity
Player question: Is the randomness unbiased, or could it be rigged?
The Duel Plinko RNG uses HMAC-SHA256 with deterministic inputs (serverSeed, clientSeed, nonce, cursor). For each row, the algorithm produces one bit, left (0) or right (1), by taking the first 4 bytes of the HMAC output as uint32 and computing int % 2. Slot distribution follows binomial B(n, 0.5); 2³² mod 2 = 0, so there is zero modulo bias.
This section tests whether the RNG produces unbiased results and whether each bet is isolated from all others.
Verdict summary
How Plinko Randomness and Entropy Works
This section explains how Plinko randomness is generated, what entropy sources are used, and how the RNG ensures unbiased and isolated outcomes.
2.1 RNG function implementation
Duel Plinko uses HMAC-SHA256 with deterministic inputs (serverSeed, clientSeed, nonce, cursor). For
each row, it computes one bit by taking the first 4 bytes of the HMAC output as uint32 and applying
int % 2.
Code implementation
// Source: src/rng.ts — computeSlot
export function computeSlot(
serverSeed: string, clientSeed: string, nonce: number, rows: number
): number {
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");
const hex4 = hmac.substring(0, 8);
const int = parseInt(hex4, 16);
slot += int % 2;
}
return slot;
}
Key encoding: server seed is hex-decoded to a 32-byte buffer.
Message format: clientSeed:nonce:cursor with integer nonce and zero-based cursor.
2.2 Entropy sources
| Source | Controlled by | Purpose |
|---|---|---|
| Server seed | Casino | Base randomness committed via SHA-256 before betting |
| Client seed | Player | Player-contributed entropy |
| Nonce | System | Uniqueness per bet |
Verified absent:
- No timestamps
- No
Math.random()or browser entropy - No external APIs or drand inputs
- No server-side mutable state
Recompute succeeds for 7,600/7,600 bets using only declared inputs, so hidden entropy sources are not
needed to produce outcomes.
2.3 Modulo bias analysis
Row direction uses int % 2 where int is uint32 in
[0, 4,294,967,295]. Because 2^32 mod 2 = 0, there is no modulo bias.
2^32 = 4,294,967,296
4,294,967,296 / 2 = 2,147,483,648 (exact)
There are exactly equal counts of even and odd uint32 values, so right-bounce probability is exactly
1/2.
2.4 RNG isolation
computeSlot is stateless and depends only on (serverSeed, clientSeed, nonce, rows). No
class-level memory or global mutable state contributes to output.
2.5 Monte Carlo simulation (27M rounds)
Simulation runs 27,000,000 rounds (1,000,000 per config across all 27 configs) to test
algorithmic slot distribution at scale.
Methodology
| Check | Details |
|---|---|
| Seed generation | Fresh random seeds per config via crypto.randomBytes |
| Expected counts | Independent binomial B(n, 0.5) |
| Goodness-of-fit test | Chi-squared with Cochran pooling and tail merge |
| P-value computation | Regularized incomplete gamma function |
Results
| Metric | Value |
|---|---|
| Average simulated RTP | 99.890% |
| Average theoretical RTP | 99.900% |
| Chi-squared simulation pass rate | 27/27 at α=0.01 |
| Bonferroni threshold result | α/27 ≈ 0.00037; 27/27 pass |
| Minimum simulation p-value | 0.0059 |
2.6 Chi-squared on live bets
Chi-squared is applied to 7,600 live bets grouped by config, with expected counts from independent
binomial probabilities.
Result: 27/27 configurations pass at α=0.01. Weakest live result is
16r/low (n=200): χ²=20.45, df=10, p=0.0252.
2.7 Serial independence (Step 11)
Independence was tested on Phase B (2,000 bets, single config 16r/high) using lag-1
autocorrelation and runs test.
- Lag-1 autocorrelation result:
r = -0.0089(threshold ±0.0671). - Runs test:
z = -0.483,p = 0.629. - Both tests pass; no serial dependence detected.
Phase A mixes configurations and introduces artificial run patterns. Phase C (200 bets) is lower-power,
so Phase B is primary for serial testing.
2.8 Anti-circularity - probability independence (Step 20)
RTP proof is validated using independent binomial probabilities, then cross-checked against config probabilities and observed multipliers.
Code implementation
// Source: tests/verify.ts — Step 20 (EC-33)
for (const { rows, risk } of cfg.allConfigs()) {
for (let k = 0; k < slotCount; k++) {
const independent = binomProb(rows, k);
const fromConfig = configProbs[k];
if (fromConfig !== independent) failures.push(...);
}
}
// Cross-check: independent probs × scaling_edge multipliers = configRTP
Result: all slot probabilities match independent binomial values for all 27 configurations. RTP
cross-check also matches 27/27.
Technical Evidence and Verification
2.9 Purpose
This section indexes technical artifacts used to verify RNG inputs, entropy purity, bias properties, distribution fit, serial independence, and anti-circularity.
Audit status: Pass.
2.10 Evidence coverage summary
| Verification area | Coverage | Result |
|---|---|---|
| RNG input dependency | 7,600 bets recomputed | PASS |
| Entropy source isolation | Code and live-data verification | PASS |
| Modulo bias | Mathematical proof (2^32 mod 2 = 0) | PASS |
| drand absence | 7,600 recomputations without drand | PASS |
| Chi-squared (live, 27 configs) | All configs with n ≥ 100 | PASS |
| Chi-squared (simulation, 27 configs) | 1M rounds per config | PASS |
| Serial independence | Phase B (2,000) plus Phase C (200) | PASS |
| Anti-circularity | All 27 configs, all slots | PASS |
2.11 Code references
| File | Purpose |
|---|---|
src/rng.ts | computeSlot independent HMAC-SHA256 implementation |
src/stats.ts |
|
src/simulate.ts | Monte Carlo simulation (1M rounds/config) with independent expected counts |
tests/verify.ts | Step 11 (serial), Step 12 (chi-squared live), Step 20 (anti-circularity) |
src/config.ts | theoreticalRTP, scalingEdgeMultiplier, probabilities |
2.12 Datasets used
| Dataset | Value |
|---|---|
| Simulation |
|
| Live chi-squared | outputs/chi-squared-results.json - per-config chi-squared on 7,600 live bets |
| Serial independence sample | results/merged/plinko-master.json - Phase B 2,000 consecutive 16r/high bets |
2.13 Worked example
Bet ID 60086398 (Phase B, 16 rows, high risk):
serverSeed=5b98...d4ff, clientSeed=8zZ78btAe6T8gvtH, nonce=0.
| Cursor | HMAC[:8] | uint32 | % 2 | Direction | Running slot |
|---|---|---|---|---|---|
| 0 | 11bcb113 | 297,578,771 | 1 | right | 1 |
| 1 | e78ed6ac | 3,884,897,964 | 0 | left | 1 |
| 2 | df13e4b7 | 3,742,622,903 | 1 | right | 2 |
| ... | ... | ... | ... | ... | ... |
| 15 | 746204c1 | 1,952,580,801 | 1 | right | 12 |
Final slot is 12, matching API response. Multiplier for 16r/high slot 12 from
scaling_edge[0].multipliers is 4.03732442×.
2.14 Verified invariants (RNG & entropy)
| Invariant | Result |
|---|---|
RNG depends only on declared inputs (serverSeed, clientSeed, nonce, cursor) | PASS |
| No hidden or mixed entropy sources | PASS |
| drand is not an input to Plinko RNG | PASS |
Modulo bias is zero (2^32 mod 2 = 0) | PASS |
| RNG is stateless per bet | PASS |
Chi-squared (live): 0/27 configs fail at α=0.01 | PASS |
Chi-squared (simulation): 0/27 fail at Bonferroni α/27 ≈ 0.00037 | PASS |
| Serial independence: lag-1 autocorrelation within threshold | PASS |
| Serial independence: runs test p > 0.01 | PASS |
| Anti-circularity: independent binomial equals config probabilities for all slots/configs | PASS |
| RTP proof is non-circular: independent probabilities × observed multipliers = 99.900% | PASS |
2.15 Reproduction instructions
git clone https://github.com/ProvablyFair-org/duel-audit
cd duel-audit
npm install
npx ts-node tests/verify.ts
npx ts-node src/simulate.ts
Expected output from verify.ts (S2-related steps)
[PASS] Step 11 - Serial Independence
[PASS] Step 12 - Slot Symmetry
[PASS] Step 20 - Probability Independence (Anti-Circularity)
Expected output from simulate.ts
27 configs
all chi-squared checks pass
average simulated RTP ≈ 99.890%
RNG output is statistically uniform, deterministic, and free from hidden entropy or bias. The RTP proof is non-circular: independent binomial probabilities × observed multiplier table = 99.900%.
3. Live ↔ Verifier Parity
Player question: Does what the casino shows you match what really happened?
Verifier parity is the most critical requirement of a provably fair system. If you take revealed seeds after gameplay, input them into an independent verifier, and get the exact same outcome as the live game, the casino could not have altered the result.
This section tests whether Duel live Plinko outcomes match independent verifier recomputation across a full dataset of real production bets.
Parity metrics
Verdict summary
How It Works - Parity Verification
This section validates the most critical requirement of a provably fair system: independent verifier outcomes must exactly match live game outcomes. A single mismatch invalidates the guarantee.
3.1 Why parity matters
If verifier and live game differ, the verification model is not trustworthy. Players must be able to take revealed seeds after gameplay, run independent recomputation, and obtain the exact same slot they saw live.
3.2 Three-phase collection design
| Phase | Bets | Bet amount | Config | Purpose |
|---|---|---|---|---|
| A - Configuration coverage | 5,400 | $0.01 | All 27 configs | Verify every row/risk combination |
| B - High-variance sampling | 2,000 | $0.01 | 16r/high | Deep coverage of highest-variance config |
| C - Code-path equivalence | 200 | $10.00 | 16r/high | Confirm bet amount is not in RNG path |
Phase A provides full config coverage, Phase B supports deeper single-config checks, and Phase C tests potential stake-dependent divergence.
3.3 Slot recomputation (Step 5)
Code implementation
// Source: src/rng.ts — computeSlot
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;
}
Result: 7,600 / 7,600 computed slots match final_slot. Zero mismatches. This confirms
deterministic mapping with no hidden slot inputs beyond disclosed values.
3.4 Payout math (Step 7)
Formula: win_amount = amount_currency × payout_multiplier.
Code implementation
// Source: tests/verify.ts — Step 7 (EC-11, EC-18)
const expected = amount * mult;
const diff = Math.abs(win - expected);
if (diff > 1e-8) failures.push(...);
Result: all 7,600 bets match within 1e-8 tolerance.
3.5 Multiplier table provenance (Step 8)
Two config sources exist per setup: payout_tables and scaling_edge[0].multipliers. The
maximum observed difference is 1.49×10^-6 (rounding only).
| Metric | Count |
|---|---|
| Total bets checked | 7,600 |
Bets matching scaling_edge[0].multipliers | 7,600 |
Bets matching payout_tables | 7,600 |
| Bets matching neither | 0 |
Reference table selected for subsequent checks: scaling_edge[0].multipliers (higher precision).
Average theoretical RTP across all 27 configs: 99.900% (house edge 0.1% at bracket 0).
3.6 Multiplier symmetry and structure (Step 16)
Code implementation
// Source: tests/verify.ts — Step 16 (EC-10, EC-12, EC-13, EC-14)
for (let k = 0; k <= rows; k++) {
const mk = cfg.scalingEdgeMultiplier(rows, risk, k);
const mkMirror = cfg.scalingEdgeMultiplier(rows, risk, rows - k);
if (Math.abs(mk - mkMirror) > 1e-5) failures.push(...);
}
Results
- Symmetry:
table[k] == table[rows-k]for all slots/configs. - Edge slots: slot
0equals slotNfor all configs. - Table match: observed multipliers match
scaling_edge[0]within1e-5. - Center floor:
16r/highcenter slot is0.2×(non-zero).
3.7 Phase C code-path equivalence (Step 9)
Phase C places 200 bets at $10 on 16r/high to test stake-dependent
divergence.
- EC-15: all
200/200Phase C slots recompute correctly. - EC-16: all
200/200Phase C multipliers match the same table as Phase B.
Result: deterministic equivalence is confirmed for tested stake paths. Phase C empirical RTP is
119.0% ($2,380 on $2,000 wagered), consistent with high-variance sampling.
Worked example
Bet ID 60086398 (Phase B, 16 rows, high risk, $0.01): server seed
5b98f89529c39a86b701f4e4ac44feda603e2b5ab0ce6338c1383e8ba9f7d4ff, client seed
8zZ78btAe6T8gvtH, nonce 0.
| Cursor | HMAC[:8] | uint32 | % 2 | Direction | Running slot |
|---|---|---|---|---|---|
| 0 | 11bcb113 | 297,578,771 | 1 | right | 1 |
| 1 | e78ed6ac | 3,884,897,964 | 0 | left | 1 |
| 2 | df13e4b7 | 3,742,622,903 | 1 | right | 2 |
| ... | ... | ... | ... | ... | ... |
| 15 | 746204c1 | 1,952,580,801 | 1 | right | 12 |
Verification: final slot 12 matches API response; multiplier
4.03732442 matches scaling_edge[0].multipliers for 16r/high slot 12; and
win_amount = 0.01 × 4.03732442 = 0.0403732442 matches within 1e-8.
Technical Evidence - Parity
3.8 Evidence coverage summary
| Verification area | Coverage | Result |
|---|---|---|
| Slot recomputation | 7,600 / 7,600 bets | PASS |
| Payout math | 7,600 / 7,600 bets (tol 1e-8) | PASS |
| Multiplier table match | 7,600 / 7,600 bets | PASS |
| Multiplier symmetry | All 27 configs, all slots | PASS |
| Edge slot equality | All 27 configs | PASS |
| Center slot floor | 16r/high: 0.2× | PASS |
| Phase C slot recomputation | 200 / 200 bets at $10 | PASS |
| Phase C multiplier table match | 200 / 200 bets | PASS |
3.9 Code references
| File | Purpose |
|---|---|
tests/verify.ts Step 5 | Slot recomputation (EC-6, EC-7) |
tests/verify.ts Step 7 | Payout math (EC-11, EC-18) |
tests/verify.ts Step 8 | Multiplier provenance and theoretical RTP (EC-28, EC-32) |
tests/verify.ts Step 9 | Phase C code-path equivalence (EC-15, EC-16, EC-17) |
tests/verify.ts Step 16 | Multiplier symmetry and structure (EC-10, EC-12, EC-13, EC-14) |
src/rng.ts | computeSlot independent HMAC-SHA256 implementation |
src/config.ts | scalingEdgeMultiplier, payoutTableMultiplier, and theoreticalRTP |
3.10 Datasets used
| Dataset | Value |
|---|---|
| Primary |
|
| Config |
|
| Per-bet recomputation log |
|
3.11 Verified invariants (verifier parity)
| Invariant | Result |
|---|---|
Independent slot recomputation matches final_slot for all 7,600 bets | Pass |
win_amount = amount_currency × payout_multiplier within 1e-8 for all 7,600 bets | Pass |
Every payout_multiplier matches scaling_edge[0].multipliers within 1e-5 | Pass |
Table symmetry: table[k] == table[rows-k] for all 27 configs | Pass |
Edge slot equality: slot 0 equals slot N for all 27 configs | Pass |
16r/high center slot multiplier is ≥ 0.2× | Pass |
Phase C ($10) slots recompute with the same algorithm as Phase B ($0.01) | Pass |
Phase C multipliers match the same scaling_edge[0] table as Phase B | Pass |
No hidden inputs beyond (serverSeed, clientSeed, nonce, rows) affect slot output | Pass |
3.12 Reproduction instructions
git clone https://github.com/ProvablyFair-org/duel-audit
cd duel-audit
npm install
npx ts-node tests/verify.ts
S3-related expected output from verify.ts
[PASS] Step 5 - Slot Recomputation (RNG + drand absent)
[PASS] Step 7 - Payout Math
[PASS] Step 8 - Multiplier Table Provenance
[PASS] Step 9 - Phase C Code-Path Equivalence
[PASS] Step 16 - Multiplier Table + Symmetry
Per-bet recomputation log: outputs/determinism-log.json, containing computed versus actual slot for
every bet.
All 7,600 live Plinko outcomes matched the independent verifier exactly. Payout math is correct. Multiplier tables are symmetric and consistent across bet sizes.
4. RTP & Payout Validation
Player question: Does the house edge match what is advertised?
This section verifies payout mechanics, multiplier integrity, and RTP from first principles. It confirms that payouts follow documented rules, the advertised RTP is mathematically proven, and bet size does not distort payout logic at tested stakes.
Advertised vs observed RTP
| Metric | Result | Why it matters |
|---|---|---|
| Advertised RTP | 99.9% | Public target value for standard-stake play. |
| Analytical RTP (all 27 configs) | 99.900% | Independently proven from binomial probabilities and multiplier table. |
| Simulated RTP (27M rounds) | 99.890% average | Large-sample simulation corroborates the analytical proof. |
| Observed deviation | Within expected variance | Simulation noise remains within expected bounds. |
| Proof method | Non-circular | Does not rely on casino-supplied probabilities alone. |
Verdict summary
How It Works - RTP Validation
ProvablyFair validates payout formula correctness, multiplier-table provenance, analytical RTP, scaling-edge behavior, and bet-size equivalence.
4.1 Payout formula (Step 7)
Winning payouts follow: win_amount = bet_amount × multiplier.
Code implementation
// Source: tests/verify.ts — Step 7 (EC-11, EC-18)
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;
if (Math.abs(win - expected) > 1e-8) failures.push(...);
Result: all 7,600 bets match within 1e-8. No undisclosed deductions, rounding drifts, or
scaling inconsistencies detected.
4.2 House edge and RTP explained
House edge is the expected retained percentage over time. RTP (Return to Player) is
100% − house edge. At bracket 0, Plinko house edge is 0.1%, so RTP is
99.9%.
In Plinko, each configuration has its own multiplier table. RTP per config is:
RTP = Σ P(slot=k) × multiplier(k), where P(slot=k) follows binomial
B(rows, 0.5).
4.3 Analytical RTP proof (Step 20)
Theoretical RTP is proven through a two-step non-statistical chain:
- Probability independence:
binomProb(rows, k)equals config probabilities across all slots/configs. - RTP cross-check:
Σ binomProb(rows, k) × scalingEdgeMultiplier(rows, risk, k).
Result: all 27 configurations return 99.900% within floating-point precision.
4.4 Full RTP table - all 27 configurations
| Config | Theoretical RTP | Simulated RTP (1M rounds) | Deviation | Std error |
|---|---|---|---|---|
| 8r/low | 99.900% | 99.915% | +0.015% | ±0.056% |
| 8r/medium | 99.900% | 99.671% | -0.229% | ±0.125% |
| 8r/high | 99.900% | 99.638% | -0.262% | ±0.269% |
| 9r/low | 99.900% | 99.906% | +0.006% | ±0.047% |
| 9r/medium | 99.900% | 99.648% | -0.252% | ±0.129% |
| 9r/high | 99.900% | 100.256% | +0.356% | ±0.297% |
| 10r/low | 99.900% | 99.847% | -0.053% | ±0.053% |
| 10r/medium | 99.900% | 99.792% | -0.108% | ±0.122% |
| 10r/high | 99.900% | 100.156% | +0.256% | ±0.368% |
| 11r/low | 99.900% | 99.892% | -0.008% | ±0.045% |
| 11r/medium | 99.900% | 99.912% | +0.012% | ±0.113% |
| 11r/high | 99.900% | 99.981% | +0.081% | ±0.416% |
| 12r/low | 99.900% | 99.983% | +0.083% | ±0.039% |
| 12r/medium | 99.900% | 99.857% | -0.043% | ±0.130% |
| 12r/high | 99.900% | 99.435% | -0.465% | ±0.442% |
| 13r/low | 99.900% | 99.873% | -0.027% | ±0.047% |
| 13r/medium | 99.900% | 99.987% | +0.087% | ±0.137% |
| 13r/high | 99.900% | 100.639% | +0.739% | ±0.489% |
| 14r/low | 99.900% | 99.915% | +0.015% | ±0.032% |
| 14r/medium | 99.900% | 99.827% | -0.073% | ±0.137% |
| 14r/high | 99.900% | 99.683% | -0.217% | ±0.563% |
| 15r/low | 99.900% | 99.859% | -0.041% | ±0.041% |
| 15r/medium | 99.900% | 100.156% | +0.256% | ±0.157% |
| 15r/high | 99.900% | 100.052% | +0.152% | ±0.606% |
| 16r/low | 99.900% | 99.888% | -0.012% | ±0.033% |
| 16r/medium | 99.900% | 99.959% | +0.059% | ±0.148% |
| 16r/high | 99.900% | 99.310% | -0.590% | ±0.663% |
All 27 configurations converge within expected variance. High-variance configs show wider deviations because rare high multipliers amplify sampling noise.
4.5 Empirical RTP from live bets (informational only)
Per-config empirical RTP from live sample sizes is not used as fairness evidence due low statistical power at observed sample sizes and high multiplier variance.
| Phase | Bets | Empirical RTP |
|---|---|---|
| A (all 27 configs, $0.01) | 5,400 | 96.671% |
| B (16r/high only, $0.01) | 2,000 | 84.683% |
| C (16r/high only, $10.00) | 200 | 119.000% |
Live RTP variation here is expected sampling noise; audit reliance remains on deterministic parity, payout formula verification, and analytical RTP proof.
4.6 Scaling edge structure (Step 19)
plinkoConfig.json includes 191 bet-size brackets per config. House edge increases
progressively from 0.1% at low stakes to 2.0% at highest brackets.
| Bracket | House edge | RTP | Coverage |
|---|---|---|---|
| 0 (lowest stakes) | 0.1% | 99.9% | Up to ~$335.95 (16r/high, most restrictive) |
| 190 (highest stakes) | 2.0% | 98.0% | Maximum bet bracket |
Step 19 verifies:
- Bracket
0house edge =0.001for all 27 configs. - Both test amounts (
$0.01,$10) fall within bracket 0. scaling_edge[0].multipliersremain symmetric across slots.
4.7 Zero edge audit (Step 13)
Game mathematics verify 0.1% house edge in multiplier tables. Platform-level rakeback behavior is
outside cryptographic scope. Step 13 checks table consistency across effective_edge groups.
Result: all observed effective_edge groups use the same
scaling_edge[0].multipliers table.
4.8 Multiplier structure by risk level
| Risk | Center mult (16r) | Edge mult (16r) | Variance |
|---|---|---|---|
| Low | 0.50× | 16.15× | Lowest |
| Medium | 0.30× | 111.01× | Medium |
| High | 0.20× | 1,009.33× | Highest |
Risk level changes variance profile only; RTP target remains the same across risk levels in bracket 0.
Technical Evidence - RTP
4.9 Evidence coverage summary
| Verification area | Coverage | Result |
|---|---|---|
| Payout formula | 7,600 / 7,600 bets (tol 1e-8) | PASS |
| Multiplier table match | 7,600 / 7,600 bets | PASS |
| Theoretical RTP (all 27 configs) | Analytical proof via Step 20 | PASS |
| Anti-circularity | All slots, all configs | PASS |
| Simulated RTP (27M rounds) | 27 configs, 1M each | PASS |
| Scaling edge bracket 0 | All 27 configs, both test amounts | PASS |
| Zero edge audit | All effective_edge groups | PASS |
| Phase C payout equivalence | 200 bets at $10 | PASS |
4.10 Code references
| File | Purpose |
|---|---|
tests/verify.ts Step 7 | Payout math (EC-11, EC-18) |
tests/verify.ts Step 8 | Multiplier provenance and theoretical RTP (EC-28, EC-32) |
tests/verify.ts Step 10 | Empirical RTP (informational) (EC-19-22) |
tests/verify.ts Step 13 | Zero edge audit (EC-23) |
tests/verify.ts Step 19 | Scaling edge analysis (EC-17, EC-32) |
tests/verify.ts Step 20 | Anti-circularity (EC-33) |
src/simulate.ts | Monte Carlo simulation (1M rounds/config) |
src/config.ts | theoreticalRTP, scalingEdgeMultiplier, and bracket0 |
src/stats.ts | binomProb (independent from config) |
4.11 Datasets used
| Dataset | Value |
|---|---|
| Simulation | outputs/simulation-results.json - 27M rounds, per-config RTP, chi-squared, slot counts |
| Verification | outputs/verification-results.json - Steps 7, 8, 10, 13, 19, 20 |
| Config | plinkoConfig.json - payout_tables, scaling_edge (191 brackets × 27 configs), probabilities |
4.12 Verified invariants (RTP & payout)
| Invariant | Result |
|---|---|
win_amount = amount_currency × payout_multiplier for all 7,600 bets (tol 1e-8) | PASS |
All live multipliers match scaling_edge[0].multipliers (tol 1e-5) | PASS |
| Independent binomial probabilities equal config probabilities (all slots/configs) | PASS |
| Theoretical RTP = 99.900% for all 27 configs (non-circular proof) | PASS |
| Simulated RTP average = 99.890% across 27M rounds | PASS |
| 0/27 configs reject at Bonferroni α/27 ≈ 0.00037 | PASS |
Bracket 0 house_edge = 0.001 for all 27 configs | PASS |
Both test amounts ($0.01, $10) fall within bracket 0 ceiling | PASS |
| No multiplier table switching by effective_edge group | PASS |
Multiplier symmetry table[k] == table[rows-k] for all configs | PASS |
| No payout distortion by bet amount (Phase C uses Phase B table) | PASS |
4.13 Reproduction instructions
git clone https://github.com/ProvablyFair-org/duel-audit
cd duel-audit
npm install
npx ts-node tests/verify.ts
npx ts-node src/simulate.ts
S4-related expected output from verify.ts
[PASS] Step 7 - Payout Math
[PASS] Step 8 - Multiplier Table Provenance
[PASS] Step 10 - RTP Analysis
[PASS] Step 13 - Zero Edge Audit
[PASS] Step 19 - Scaling Edge Analysis
[PASS] Step 20 - Probability Independence (Anti-Circularity)
The 99.9% RTP is proven mathematically from independently verified binomial probabilities multiplied by the observed multiplier table. The simulation (27M rounds) corroborates the analytical result.
5. Fairness Integrity Testing
Player question: Can anyone cheat the system, players or casino?
This section tests whether any party, player or casino, can predict outcomes, alter results, or gain an unfair advantage through weaknesses in the provably fair implementation under realistic gameplay conditions.
Verdict summary
How Fairness Integrity Testing Works
Every provably fair system makes implicit guarantees to players. This section tests whether those guarantees hold under adversarial conditions by attempting to predict outcomes before betting, alter outcomes after betting, replay or manipulate cryptographic inputs, leverage cross-round or cross-user state, and tamper with parameters or payouts client-side.
Testing follows the ProvablyFair.org Fairness Integrity Framework, derived from historically observed failure patterns in provably fair systems.
| Category | What it protects |
|---|---|
| Nonce Integrity | Each bet is unique, sequential, and non-replayable |
| Seed Commitment Integrity | Commit-reveal is enforced and cannot be bypassed |
| Outcome Determinism | Identical inputs produce identical outcomes; outcomes are final |
| Round & Player Isolation | No state leakage between rounds or users |
| Payout Integrity | Parameters and payouts are computed server-side and not injectable |
Detailed test procedures for active API probes are documented in the audit methodology; reproduction steps are available in the public repository.
Coverage, Pending Tests, and Scope
5.4 Fairness Integrity Matrix
Nonce integrity
| Test ID | Fairness guarantee | Test scenario | Result | Evidence |
|---|---|---|---|---|
| FI-NONCE-001 | Each bet uses a unique, sequential nonce | Attempt nonce reuse | Pass | Step 4: 152 seed pairs, nonces 0–49 sequential with no repeats (see S1 §1.5) |
| FI-NONCE-002 | Nonce progression cannot be manipulated | Skip nonce by +10 | Pass | Step 4: 0 hard failures; 5 capture-retry patterns classified informational (see S1 §1.5) |
| FI-NONCE-004 | Nonce does not reset on reconnect | Reconnect and verify continuity | Pass | Step 4: capture-retry epochs show continuity across interruptions (see S1 §1.5) |
Seed commitment integrity
| Test ID | Fairness guarantee | Test scenario | Result | Evidence |
|---|---|---|---|---|
| FI-SEED-002 | Seed locked at bet acceptance | Change seed after placement | Pass | Step 3: stable server_seed_hashed across 152 pairs; Step 6: single client seed per epoch (S1 §1.3, §1.4) |
| FI-SEED-003 | Server seed unique per session | Check 152 seed pairs for repeats | Pass | Step 1: 152 distinct seeds, 152/152 hash checks pass (S1 §1.1) |
Outcome determinism
| Test ID | Fairness guarantee | Test scenario | Result | Evidence |
|---|---|---|---|---|
| FI-OUTCOME-001 | Identical inputs produce identical outcomes | Replay known seed tuple | Pass | Step 5: 7,600/7,600 independent slot recomputations match (S2 §2.1, S3 §3.3) |
| FI-OUTCOME-002 | Outcomes cannot be replayed for profit | Replay settle/cashout request | N/A | Plinko has no separate cashout step; nonce auto-increments server-side |
Round & player isolation
| Test ID | Fairness guarantee | Test scenario | Result | Evidence |
|---|---|---|---|---|
| FI-ISO-001 | RNG state independent across rounds | Serial dependence analysis | Pass | Step 11: r=-0.009, runs z=-0.483, p=0.629 (S2 §2.7) |
Coverage summary
Verified (data-driven): 7 FI tests
| Test ID | Category | Cross-reference |
|---|---|---|
| FI-NONCE-001 | Nonce Integrity | S1 §1.5, Step 4 |
| FI-NONCE-002 | Nonce Integrity | S1 §1.5, Step 4 |
| FI-NONCE-004 | Nonce Integrity | S1 §1.5, Step 4 |
| FI-SEED-002 | Seed Commitment | S1 §1.3, §1.4, Steps 3, 6 |
| FI-SEED-003 | Seed Commitment | S1 §1.1, Step 1 |
| FI-OUTCOME-001 | Outcome Determinism | S2 §2.1, S3 §3.3, Step 5 |
| FI-ISO-001 | Round Isolation | S2 §2.7, Step 11 |
N/A for Plinko: 1 FI test
| Test ID | Reason |
|---|---|
| FI-OUTCOME-002 | No cashout step; bets resolve instantly and nonce increments server-side |
Additional game integrity evidence (Sections 1–4)
| Property | Section | Step | Finding |
|---|---|---|---|
| Client seed is a genuine HMAC input | S1 §1.7 | Step 6 (EC-27) | Wrong seed changed 84.3% of slots |
| drand is not in Plinko RNG | S2 §2.2 | Step 5 (EC-6) | 7,600/7,600 recomputed without drand |
| Modulo bias is zero | S2 §2.3 | - | 2³² mod 2 = 0 |
| Slot distribution matches binomial | S2 §2.6 | Step 12 (EC-9) | 27/27 pass at α=0.01 |
| Anti-circular RTP proof | S4 §4.3 | Step 20 (EC-33) | Independent binomial = config probabilities |
| Bet amount does not influence RNG | S3 §3.7 | Step 9 (EC-15-17) | 200/200 $10 bets recompute correctly |
| Multiplier table matches config | S3 §3.5 | Step 8 (EC-28, EC-32) | 7,600/7,600 match scaling_edge[0] |
| Multiplier symmetry verified | S3 §3.6 | Step 16 (EC-14) | table[k] == table[rows-k], all 27 configs |
| Config completeness | S4 | Step 14 (EC-30) | All row/risk combinations tested |
Hard fail criteria
Per the audit framework: any one failure means NOT PROVABLY FAIR.
| EC | Test | Result |
|---|---|---|
| EC-1 | Seed hash mismatch | 0 failures |
| EC-4 | Nonce gap or repeat | 0 failures |
| EC-5 | Client seed changed mid-epoch | 0 failures |
| EC-7 | Slot recomputation mismatch | 0 failures |
| EC-26 | Hash changed mid-epoch | 0 failures |
| EC-27 | Client seed not used | 0 failures |
Scope and limitations
This section verifies cryptographic fairness integrity under adversarial conditions: determinism, seed commitment, nonce uniqueness, entropy isolation, and payout integrity. It is not a full platform security audit.
- Point-in-time certification only.
- Primary dataset is single-account (7,600 bets).
- Bracket 0 only (
$0.01and$10). - No infrastructure, wallet, payment, or rakeback testing.
Reproduction instructions
git clone https://github.com/ProvablyFair-org/duel-audit
cd duel-audit
npm install
npx ts-node tests/verify.ts
All 20 verification steps run in one invocation. Steps 1-20 provide the evidence base for verified FI tests. Full
output: outputs/verification-results.json.
6. Player Verification Guide
Section 6 content is pending disclosure.
7. Reproducibility & Artifacts
Player question: Can you clone, run, and reproduce the audit end to end?
Yes. This audit is fully reproducible end to end using the public repository. Clone, install dependencies, run the verification suite and simulation, then inspect the generated outputs.
Verdict summary
GitHub repository
https://github.com/ProvablyFair-org/duel-audit
Repository structure
duel-audit/
├── tests/verify.ts # 20-step verification suite
├── src/rng.ts # computeSlot, verifyHash
├── src/simulate.ts # 27M-round Monte Carlo
├── src/config.ts # multiplier tables, theoretical RTP
├── src/stats.ts # chi-squared, binomProb, runs test
├── results/merged/ # plinko-master.json
├── outputs/ # verification-results.json, simulation-results.json, determinism-log.json
└── plinkoConfig.json # supplied by Duel.com
Commands to reproduce
Prerequisites
- Node.js 18+
- npm 8+
- Git
Clone repository
git clone https://github.com/ProvablyFair-org/duel-audit.git
cd duel-audit
Install dependencies
npm install
This installs required packages, including testing frameworks and cryptographic libraries.
Run verification and simulation
Run verification suite (20 steps):
npx ts-node tests/verify.ts
Run 27M-round simulation:
npx ts-node src/simulate.ts
Expected output:
[PASS] Step 1: Seed Hash Integrity
[PASS] Step 2: Commitment Linkage
... (20 steps, 0 hard fails)
27 configs, chi-squared pass, avg RTP ≈ 99.890%
View generated outputs
Verification and simulation results:
cat outputs/verification-results.json
cat outputs/simulation-results.json
cat outputs/determinism-log.json
Outputs are written to outputs/ (verification-results.json, simulation-results.json, determinism-log.json).
Audit Reproducibility Pinning
8382e45f8cdf4d439a8866669d15e6f4be543f4b926fb64c67e09d9da7d6b2db