
Keno
Independent verification, backed by open-source code.
audit and dataset.
- Live Bets Verified
- 5,100
- Parity Rate
- 100%
- Simulated Rounds
- 40M
- Proven RTP
- 99.9%
Verification pipeline
Real bets. Independently reproduced.
Placed anonymously at Duel.com. We rebuilt the game logic from scratch and matched all 5,100 recorded outcomes.
The casino result, reproduced.
- Casino outcome
- 0 / 10 hits
- Our reproduction
- 0 / 10 hits
- Recorded payout
- $0.0000
- Seed Hash IntegrityPass
- Commitment LinkagePass
- Hash ConsistencyPass
- Nonce AuditPass
- Draw RecomputationPass
- Client Seed InfluencePass
- Payout MathPass
- Multiplier ProvenancePass
- Bet-Size InvariancePass
- Configuration CompletenessPass
- Zero-Edge AuditPass
- Consolation DisclosurePass
- Phase LabelsPass
- Dataset HashPass
- Epoch SizePass
- Hit Symmetry (Hypergeom.)Pass
- Anti-CircularityPass
- Client Seed VariationPass
- Simulation Pass 1Pass
- Cherry-Pick DetectionPass
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, 40M simulated rounds, 5,100 live bets re-verified.
This audit independently validates the Keno 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 5,100 real bets across 102 seed pairs and independently verified every single outcome using our own implementation of the algorithm.
Audit Verdict
| Check | Result | Evidence |
|---|---|---|
| Overall Status | Pass | |
| RTP Verified | Pass | 99.9% proven · 99.970% simulated (40M) · 0.1% base-tier edge |
| Live ↔︎ Verifier Parity | Pass | 100% — 5,100 / 5,100 bets matched |
| Commit-Reveal System | Pass | SHA-256 verified, 102 seeds — commitment intact across all rotations |
| Client Seed | Pass | Browser-generated + player customizable — server commits before client seed is known |
| RNG Analysis | Pass | HMAC-SHA256 backward Fisher-Yates — 10 numbers drawn from 40, bias-free rejection sampling, no hidden inputs |
| Payout Logic | Pass | All 5,100 payouts verified — win_amount = bet × multiplier, exact to 8 decimal places |
| Scaling House Edge | Info | 0.1% at base tier, scaling to 1.0% at larger bet sizes — disclosed; all captured bets fall in the base tier. See 4.6b. |
| Anti-Circularity | Pass | Hypergeometric P(hits) × committed multiplier = 99.9% for all 40 configs — probabilities derived independently, applied to the committed table |
| Fairness Integrity | Pass | 15 standard fairness integrity tests — 14 pass, 1 N/A (single-step game) |
| 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
Fairness IntegrityPass
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
- Drawn numbers are computed via HMAC-SHA256 backward Fisher-Yates with bias-free rejection sampling
- Draw outcomes are reproducible from server seed, client seed, and nonce
- Payout logic matches the published multiplier tables for all 40 configurations
- Theoretical RTP is 99.9% across all configurations at the base bet tier (0.1% house edge; scales to 1.0% on larger bets — see 4.6b)
- Bet amount does not influence the RNG or draw 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
- Cross-account sampling
- Max win cap enforcement — not embedded in game logic
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 backward Fisher-Yates verification, rejection sampling, bias analysis |
| Payout Logic | Multiplier table accuracy, house edge verification, bet-size invariance (Phase D) |
| Live Parity | Independent draw recomputation vs live game results |
| RTP Validation | Anti-circularity proof, simulated RTP (40M 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 (5,100 / 5,100)
- Draw distribution follows the hypergeometric model for all 40 configurations
- RTP is proven analytically: hypergeometric P(hits) × multiplier = 99.9% for all 40 configurations
- Client seed is a genuine, browser-generated input that materially influences results
- The house edge is a flat 0.1% across all 40 configurations at the base bet tier (the tier all captured bets fall in)
- All 15 standard fairness integrity tests addressed at audit time — 14 pass, 1 N/A (single-step game)
References
Keno — Game Rules7 sections▶
Keno is a draw-based lottery game: pick 1–10 numbers on a 40-number grid, choose a risk level, and watch 10 numbers get drawn. The more of your picks that hit, the bigger the payout.
Game Rules
Keno is a draw-based lottery game. Before the round you pick between 1 and 10 numbers from a 40-number grid (your 'picks') and choose a risk level — Classic, Low, Medium, or High — which shapes the payout curve. The platform then draws 10 numbers from the same grid using a backward Fisher-Yates shuffle seeded with HMAC-SHA256. Your payout depends on how many of your picks land in those 10 drawn numbers: more hits means a bigger multiplier, and higher risk levels shift more of the reward toward rare full-hit outcomes.
How to Play
1. Choose picks — Select between 1 and 10 numbers from the 40-number grid.
2. Choose risk level — Select Classic, Low, Medium, or High. Higher risk shifts payout weight toward rare large hits.
3. Enter bet amount — Choose how much to wager.
4. Draw — The platform draws 10 numbers via the backward Fisher-Yates shuffle.
5. Outcome — Payout = bet amount × multiplier for your hit count.
Win Conditions
The win condition in Keno depends on how many of your picks match the 10 drawn numbers.
| Outcome | Condition | Example (HIGH picks=10) |
|---|---|---|
| Jackpot (all hits) | All picks match drawn numbers | 10/10 hits → 1200× — ~1 in 847 million chance |
| Win (partial hits) | Several picks match drawn | 6/10 hits → 13.1× |
| Loss (no hits) | No picks match drawn | 0/10 hits → 0× |
Risk vs Reward
The core mechanic of Keno is the tradeoff between pick count, risk level, and payout shape.
- Pick count controls hit probability — more picks means more chances to match, but multipliers scale accordingly to maintain 99.9% RTP
- Risk level controls payout shape — Classic has moderate variance; High concentrates value at rare full-hit outcomes with 0× on low hit counts
- RTP is constant — all 40 configurations have the same theoretical RTP (99.9%) regardless of picks or risk
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Grid Size | 40 (numbers 1–40) | Fixed; all draws from this pool |
| Numbers Drawn | 10 per round | Fixed; drawn via backward Fisher-Yates |
| Player Picks | 1–10 | Player selects; determines hit probability |
| Risk Levels | Classic, Low, Medium, High | Affects multiplier table, not drawn numbers |
| House Edge | 0.1% flat | Within the base tier, the same 0.1% edge applies to all 40 configs |
| Theoretical RTP | 99.9% | Verified across all 40 configurations |
| Configurations | 40 | 10 pick counts × 4 risk levels |
| RNG Algorithm | HMAC-SHA256 | Backward Fisher-Yates with bias-free rejection sampling; key = hex-decoded server seed |
Seed Formats
Every Keno bet uses three cryptographic inputs to generate the draw result.
| Seed Type | Format | Example | Purpose |
|---|---|---|---|
| Server Seed | 64-char hex (32 bytes) | 94eee14e882d4332… | Casino-provided randomness |
| Client Seed | Alphanumeric string | dAn1wtkuDAXxQOKP | Player-contributed entropy |
| Nonce | Integer (0–49) | 7 | Ensures uniqueness per bet within epoch |
Multiplier Tables & Payout
Payouts in Keno are determined by the hit count, pick count, and risk level. The multiplier is calibrated so that the expected return is exactly 99.9% across all 40 configurations.
win_amount = bet_amount × multiplier_table[risk][picks][hits]| Picks | Risk | Max Multiplier (all hits) | Zero-Hit Multiplier | RTP |
|---|---|---|---|---|
| 1 | Classic | 3.996× | 0× | 99.9% |
| 1 | Low | 1.866× | 0.71× | 99.9% |
| 10 | Classic | 200× | 0× | 99.9% |
| 10 | High | 1200× | 0× | 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 (drand absent from Keno) |
| No mixed entropy sources | No timestamps, Math.random, etc. |
| Unbiased mapping | Bias-free rejection sampling with maxFair ceiling eliminates modulo bias for all ranges (2–40) |
| No state leakage | RNG isolated per round — each shuffle step uses unique cursor |
3. Verifier ↔︎ Live Parity
| Test | Description |
|---|---|
| Live outcomes match verifier | 5,100 / 5,100 draws recomputed with 0 SET mismatches |
| Multi-phase verification | Phases A (32 configs), B (picks=1), C (picks=10), D (stake equivalence) |
| Bet-size invariance | $10 bets produce same draws as $0.01 bets |
4. Game Logic & RTP Validation
| Test | Description |
|---|---|
| Anti-circularity proof | Hypergeometric P(hits) × committed multiplier = 99.9% — independent probabilities, committed table |
| House edge audit | Flat 0.1% confirmed across all 40 configs at the base bet tier (scaling 0.1%→1.0% by bet size — see 4.6b) |
| Payout rules correctness | Win amount matches multiplier × bet within 1e-8 |
| Simulated RTP convergence | 40M rounds converge on theoretical 99.9% |
| Cherry-pick detection | 57/1,666 flags ≤ 80 threshold — no evidence of seed pre-selection |
5. Fairness Integrity & Player Verification
| Test | Description |
|---|---|
| Player can reproduce results offline | Using seeds + nonce + backward Fisher-Yates |
| Verifier logic matches live logic | Same HMAC-SHA256 backward Fisher-Yates algorithm |
| Verifier publicly accessible | ProvablyFair.org verifier — no login required |
| No reliance on private APIs | Fully client-side verification |
| 20 verification steps | 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 pick count (1–10), risk level, and bet amount
2. Seeds Combined — HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:cursor) for each shuffle step
3. RNG Output — Backward Fisher-Yates: 39 iterations from i=39 downto 1, each step swaps positions[i] with a bias-free random index
4. Draw Result — positions[0..9] + 1 = the 10 drawn numbers (1-indexed SET)
5. Hit Count — Count of player's picks appearing in drawn set
6. Multiplier Lookup — hits maps to multiplier via kenoConfig[risk][picks][hits]
7. 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 computes the drawn numbers using HMAC-SHA256 with the server seed (hex-decoded) as the HMAC key and the client seed, nonce, and cursor as the message. The same server seed and client seed pair are used for every bet in the epoch; the player can rotate the seed pair at any time to end the epoch.
Reveal Phase:
After the player rotates to a new seed pair, the server reveals the plaintext server seed from the completed epoch. The player can now independently verify SHA-256(hexDecode(serverSeed)) = committedHash.
Verify Phase:
Anyone can recompute every bet's drawn numbers from the revealed server seed, client seed, and nonce using the published backward Fisher-Yates 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 player rotates to a new server seed
Seed Epoch: (serverSeed, clientSeed) Bet 1: nonce = 0 → Draw ABet 2: nonce = 1 → Draw BBet 3: nonce = 2 → Draw C...Bet N: nonce = N−1 → Draw Z [Player rotates seed — epoch complete]Next bet: nonce = 0 → Draw 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 step → Always same swap index in Fisher-YatesSame swaps across all 39 steps → Always same drawn SETSame drawn SET + same picks → Always same hit countSame hit count + 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, and nonce must always generate the same drawn number set. |
| 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. Starts at 0 and resets to 0 when the player 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 | Index used in the HMAC message during the backward Fisher-Yates shuffle. cursor = gridSize − 1 − i, starting at 0 for the first step (i=39). Each cursor value produces one shuffle step. |
| Epoch | A sequence of consecutive bets sharing the same server seed and client seed pair. Ends when the player rotates to a new seed pair. This audit covered 102 bet-bearing epochs. |
Cryptographic Functions
| Term | Definition |
|---|---|
| HMAC-SHA256 | Hash-based Message Authentication Code using SHA-256. Duel Keno uses HMAC-SHA256 with the hex-decoded server seed as key and clientSeed:nonce:cursor as message. Multiple calls per bet (one per shuffle step). |
| 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. |
| Backward Fisher-Yates Shuffle | A variant of the Fisher-Yates algorithm that iterates from index 39 down to 1, swapping each position with a random lower index. Produces a uniformly random permutation of the 40-number grid. |
Verification Terms
| Term | Definition |
|---|---|
| Verifier | A tool that independently calculates drawn number sets using provided seeds and nonce. The ProvablyFair.org verifier is built from the audit codebase. |
| Parity | Degree of matching between verifier and live game results. 100% parity = every draw SET matches. This audit: 5,100/5,100 exact match. |
| Anti-Circularity | Proof that the win probabilities derive from first principles, not the casino's own claims: independently-derived hypergeometric P(hits) applied to the committed multiplier table = 99.9%. |
| Rejection Sampling | Bias elimination technique used in the shuffle. A maxFair ceiling discards values that would cause modulo bias, ensuring uniform randomness for all ranges. |
Game Mechanics
| Term | Definition |
|---|---|
| Drawn Numbers | The 10 numbers (1–40) selected by the backward Fisher-Yates shuffle. These are positions[0..9] + 1 from the shuffled array. The result is a SET — order is presentation-only. |
| Pick Count | Player-selected parameter (1–10) determining how many numbers the player chooses from the grid. More picks = more chances to match, but multipliers scale down. |
| Risk Level | Player-selected parameter (Classic, Low, Medium, High) that determines the multiplier table. Higher risk = more value at rare full-hit outcomes. |
| Hypergeometric Distribution | The probability distribution for hit counts: P(k hits) = C(picks,k) × C(40−picks, 10−k) / C(40,10). Used in the anti-circularity RTP proof. |
| Consolation Payout | Two configurations pay non-zero multipliers on zero hits: LOW picks=1 (0.71×) and MEDIUM picks=1 (0.41×). Design choice within the 99.9% RTP constraint. |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Keno game on Duel.com is generated from three inputs: server seed, client seed, and nonce. 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.
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 102 epochs
- Drawn numbers are fully determined by the seed inputs before the draw animation plays
- Identical inputs always produce the same drawn set — confirmed across all 5,100 bets
- Your client seed is a genuine input — changing it changes the outcome
What this means for you
- The casino cannot change which numbers are drawn 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 102 epochs
- Drawn numbers are fully determined by the seed inputs before the draw animation plays
- Identical inputs always produce the same drawn set — confirmed across all 5,100 bets
- Your client seed is a genuine input — changing it changes the outcome
- The casino cannot change which numbers are drawn 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 epochPass
Seed hash integrityPass
Deterministic outputPass
Client seed participationPass
All 102 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, and nonce always produce the same drawn number set. 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 epoch, 0 gaps, 0 duplicates across 102 epochs |
| Hash consistency within epoch | Pass | serverSeedHashed constant across all bets within each of 102 epochs |
| Seed hash integrity | Pass | 102 / 102 revealed seeds hash-verified — commitment chain intact |
| Deterministic output | Pass | Same (serverSeed, clientSeed, nonce) always produces same drawn SET — 5,100/5,100 confirmed |
| Client seed participation | Pass | Client seed is a genuine input — changing it changes the drawn numbers |
Server seed committed before betPass
Client seed originPass
Client seed controlPass
Nonce sequencingPass
Hash consistency within epochPass
Seed hash integrityPass
Deterministic outputPass
Client seed participationPass
All 102 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, and nonce always produce the same drawn number set. 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 seedBytes = Buffer.from(serverSeed, 'hex'); const computed = crypto.createHash('sha256').update(seedBytes).digest('hex'); return computed === serverSeedHashed;}Real Example from Live Data:
{ "clientSeed": "dAn1wtkuDAXxQOKP", "serverSeedHashed": "111880213ddf0a3a7b93cf725aeec359113c3986a902fffa0e5a33987600b77a", "serverSeed": "94eee14e882d43325f7948ef9569552f4506feeeb70a2ca35023916b4f9f0fe9", "nonce": 0}Verification:
const crypto = require('crypto'); const serverSeed = "94eee14e882d43325f7948ef9569552f4506feeeb70a2ca35023916b4f9f0fe9"; const hashedServerSeed = crypto .createHash("sha256") .update(Buffer.from(serverSeed, 'hex')) .digest("hex"); console.log(hashedServerSeed);// Output: 111880213ddf0a3a7b93cf725aeec359113c3986a902fffa0e5a33987600b77a ✅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; promoChecked++; // 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 102 epochs, the serverSeedHashed 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 [, epochBets] of byHash) { const distinctHashes = new Set(epochBets.map(b => b.seed.serverSeedHashed)); if (distinctHashes.size !== 1) epochsWithMultipleHashes++;}serverSeedHashed 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 drawn number set. 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.
05c2Ex3iqjnhrWPk0aPrHGFQSzkmvZqv2FyZzGbkRqrxBjW634hzj6BD4gCPaNAM4fUHsJsnDQXEejT6
keno-master-5100bets.json1.5Nonce Incrementation
The nonce begins at 0 and increments by 1 for each bet under the same server seed. In Keno, 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 this audit we rotated every 50 bets as our chosen sampling cadence (nonces 0–49 per epoch); epoch length is not enforced by the casino — players can rotate at any time. Two epochs exhibited a capture-retry nonce pattern (nonce 0–50 instead of 0–49); both were reconstructed from the revealed seed — draw recomputation passes for all captured bets in each affected epoch.
// Step 4: Nonce Auditfor (const [hash, epochBets] of byHash) { const sorted = [...epochBets].sort((a, b) => a.seed.nonce - b.seed.nonce); const nonces = sorted.map(b => b.seed.nonce); const clientSeeds = new Set(sorted.map(b => b.seed.clientSeed)); if (clientSeeds.size !== 1) { hardFailures.push(`Epoch ${shortHash}: ${clientSeeds.size} distinct client seeds`); } if (nonces[0] !== 0) { hardFailures.push(`Epoch ${shortHash}: first nonce is ${nonces[0]} (expected 0)`); } // 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, and nonce, it always produces the exact same drawn number set. The algorithm is a backward Fisher-Yates shuffle with HMAC-SHA256. The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. HMAC message format: clientSeed:nonce:cursor where cursor starts at 0 for the first shuffle step (i=39). Each step produces a uint32 from a 4-byte HMAC chunk; rejection sampling via the maxFair ceiling ensures bias-free index selection. After 39 steps, positions[0..9] + 1 gives the drawn SET of 10 numbers.
export function computeDraw( serverSeed: string, clientSeed: string, nonce: number, gridSize = GRID_SIZE, drawnCount = DRAWN_COUNT,): number[] { const key = Buffer.from(serverSeed, 'hex'); return computeDrawFromBuffer(key, clientSeed, nonce, gridSize, drawnCount);}drawn_numbers. Zero SET mismatches. drand confirmed absent from Keno RNG.Real Bet Verified:
// Source: data/keno-master-5100bets.json// Game ID: 34309702 (Phase A, MEDIUM risk, picks=8, nonce 4)// ✅ VERIFIED — drawn set recomputed from revealed server seed { "serverSeed": "94eee14e882d43325f7948ef9569552f4506feeeb70a2ca35023916b4f9f0fe9", "serverSeedHashed": "111880213ddf0a3a7b93cf725aeec359113c3986a902fffa0e5a33987600b77a", "clientSeed": "dAn1wtkuDAXxQOKP", "nonce": 4, "difficulty": 3, "picks": 8, "selected_positions": [1, 5, 6, 15, 17, 22, 33, 36], "drawn_numbers": [1, 5, 8, 16, 20, 26, 29, 35, 36, 37], "hits": 3, "multiplier": "2.01", "win_amount": "0.0201"}Verification:
// computeDraw("94eee14e...", "dAn1wtkuDAXxQOKP", 4) // Backward Fisher-Yates: 39 steps (i=39 downto 1)// Each step: HMAC-SHA256(hexDecode(serverSeed), "dAn1wtkuDAXxQOKP:4:{cursor}")// cursor = gridSize - 1 - i (starts at 0)// Rejection sampling: chunk < maxFair → j = chunk % range; swap // Drawn SET: {1, 5, 8, 16, 20, 26, 29, 35, 36, 37} ✅// Hits (vs selected [1,5,6,15,17,22,33,36]): 3 (matched: 1, 5, 36) ✅// Multiplier (MEDIUM/picks=8, 3 hits): 2.01× ✅// Payout: 0.01 × 2.01 = 0.0201 ✅1.7Client Seed Influence
To confirm the client seed is a genuine input to the HMAC-SHA256 computation, all 5,100 bets across 102 epochs were recomputed using a deliberately incorrect client seed (WRONG_CLIENT_SEED_FOR_AUDIT_TEST). 5,100 of 5,100 draws (100.0%) changed — every single bet produced a different drawn set with the wrong client seed. No epoch produced identical outcomes with the wrong seed.
// Step 6: Client Seed Influenceconst wrongClientSeed = 'WRONG_CLIENT_SEED_FOR_AUDIT_TEST';for (const [hash, epochBets] of ctx.byHash) { const ss = seedMap.get(hash); if (!ss) continue; const key = Buffer.from(ss, 'hex'); let changed = 0; for (const bet of epochBets) { const correctDraw = new Set(computeDrawFromBuffer(key, bet.seed.clientSeed, bet.seed.nonce)); const wrongDraw = new Set(computeDrawFromBuffer(key, wrongClientSeed, bet.seed.nonce)); if (correctDraw.size !== wrongDraw.size || [...correctDraw].some(n => !wrongDraw.has(n))) changed++; totalChecked++; } totalChanged += changed;}Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary6 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Seed hash integrity (Step 1) | 102/102 revealed seeds hash-verified | Pass |
| Commitment linkage (Step 2) | 106/106 next-seed promotions | Pass |
| Hash consistency (Step 3) | 102/102 epochs | Pass |
| Nonce audit (Step 4) | 0 gaps, 0 duplicates across 102 epochs | Pass |
| Draw recomputation (Step 5) | 5,100/5,100 | Pass |
| Client seed influence (Step 6) | 5,100/5,100 (100.0%) | 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: Draw recomputation and client seed influence |
| src/rng.ts | HMAC-SHA256 backward Fisher-Yates (computeDraw, verifyHash) |
| src/loader.ts | Dataset loading + seed/bet parsing |
| capture/capture-auto.reference.js | Browser-based data collection script |
1.10Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live Keno game data from Duel.com |
| Total Records | 5,100 bets across 102 epochs (106 seed entries) |
| SHA-256 | 841755fdfb268d7796017212f08a18bfea7615c30a4590dd29793b34c5e6bcf9 |
Fields used: serverSeed, serverSeedHashed, clientSeed, nonce, difficulty, selected_positions, drawn_numbers, multiplier, amount_won
1.11Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| SHA-256(hexDecode(serverSeed)) = serverSeedHashed for all 102 revealed seeds | Pass |
| Next-seed promotion chain intact for all 106 transitions | Pass |
| serverSeedHashed constant within epoch for all 102 epochs | Pass |
| Zero nonce gaps within any epoch | Pass |
| Zero nonce duplicates within any epoch | Pass |
| Same inputs produce same drawn SET for all 5,100 bets | Pass |
| Wrong client seed changes drawn SET in 100.0% of tests (5,100/5,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-keno.gitcd duel-keno && 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 — Draw 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 Keno random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. The RNG uses a backward Fisher-Yates shuffle with HMAC-SHA256 — each shuffle step derives a swap index from a 4-byte HMAC chunk with bias-free rejection sampling via the maxFair ceiling. We independently implemented this algorithm, verified it produces the same results as the live game, and confirmed no hidden inputs can influence outcomes.
What we verified
- HMAC-SHA256 produces cryptographically sound, unpredictable output for each shuffle step
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Rejection sampling via
maxFairceiling eliminates modulo bias for all ranges (2–40) - Draw distribution follows hypergeometric expectations for all 40 configurations (confirmed over 40M simulated rounds)
- Consecutive outcomes are statistically independent — no patterns, no streaks
- 100.0% of outcomes change with a different client seed (5,100/5,100 tested bets)
What this means for you
- Each drawn number is generated fairly and cannot be skewed
- All 40 grid positions are equally likely to be drawn — no positional bias
- No hidden randomness or server-side tricks influence which numbers appear
- 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 shuffle step
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Rejection sampling via
maxFairceiling eliminates modulo bias for all ranges (2–40) - Draw distribution follows hypergeometric expectations for all 40 configurations (confirmed over 40M simulated rounds)
- Consecutive outcomes are statistically independent — no patterns, no streaks
- 100.0% of outcomes change with a different client seed (5,100/5,100 tested bets)
- Each drawn number is generated fairly and cannot be skewed
- All 40 grid positions are equally likely to be drawn — no positional bias
- No hidden randomness or server-side tricks influence which numbers appear
- 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
maxFair ceiling eliminates bias for all ranges (2–40)Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Keno RNG uses only the disclosed inputs, produces uniform draw distribution across all 40 configurations, and shows no serial dependence across 40M simulated rounds. The client seed is a genuine input — 100.0% 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 5,100 bets |
| Modulo bias | Pass | Rejection sampling via maxFair ceiling eliminates bias for all ranges (2–40) |
| Key encoding verified | Pass | Server seed hex-decoded to bytes (not UTF-8) — confirmed via 5,100-bet recomputation |
| Serial independence | Pass | Lag-1 autocorrelation near zero and runs tests pass across all 40 configs at 1M rounds each |
| Client seed influence | Pass | 100.0% of outcomes change with an alternate client seed — confirmed across all 5,100 bets |
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
maxFair ceiling eliminates bias for all ranges (2–40)Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Keno RNG uses only the disclosed inputs, produces uniform draw distribution across all 40 configurations, and shows no serial dependence across 40M simulated rounds. The client seed is a genuine input — 100.0% of outcomes change with a different seed.
How It Works — RNG & Entropy Model7 sections
2.1RNG Function Implementation
Each Keno bet draws 10 numbers from a 40-number grid using a backward Fisher-Yates shuffle. The algorithm iterates from index 39 down to 1 (39 steps). At each step, an HMAC-SHA256 hash is computed with the hex-decoded server seed as key and clientSeed:nonce:cursor as the message. The first 4-byte chunk below the maxFair ceiling is used: j = chunk % range determines the swap index. After all 39 steps, positions[0..9] + 1 gives the 10 drawn numbers (1-indexed SET).
| Component | Detail |
|---|---|
| Hash function | HMAC-SHA256 |
| Key | Buffer.from(serverSeed, 'hex') — 32 bytes |
| Message | clientSeed:nonce:cursor |
| Extraction | 4-byte chunks → parseInt(hex, 16) → uint32 |
| Rejection | value < maxFair — bias-free ceiling per range |
| Reduction | uint32 % range → swap index j ∈ [0, range) |
| Shuffle | Backward Fisher-Yates: i=39 downto 1 |
| Output | positions[0..9] + 1 → 10 drawn numbers (1-indexed SET) |
export function computeDrawFromBuffer( keyBuffer: Buffer, clientSeed: string, nonce: number, gridSize = GRID_SIZE, drawnCount = DRAWN_COUNT,): number[] { const positions: number[] = Array.from({ length: gridSize }, (_, i) => i); for (let i = gridSize - 1; i > 0; i--) { const range = i + 1; const maxFair = MAX_UINT32 - (MAX_UINT32 % range); let cursor = gridSize - 1 - i; while (true) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex'); let found = false; for (let off = 0; off + 8 <= hmac.length; off += 8) { const value = parseInt(hmac.substring(off, off + 8), 16); if (value < maxFair) { const j = value % range; [positions[i], positions[j]] = [positions[j], positions[i]]; found = true; break; } } if (found) break; cursor++; } } return positions.slice(0, drawnCount).map(p => p + 1);}2.2Entropy Sources
All randomness derives exclusively from the deterministic HMAC-SHA256 function combining three 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 within each epoch) |
| Cursor | System | Per-step isolation (0 to 38, one per Fisher-Yates step) |
How we know: 5,100/5,100 bets were recomputed using only the three 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 shuffle step's swap index is determined by chunk % range where range varies from 2 to 40 across the 39 Fisher-Yates steps. Modulo bias occurs when 2^32 is not evenly divisible by the range. The algorithm eliminates this bias entirely via rejection sampling: a ceiling maxFair = 0xFFFFFFFF − (0xFFFFFFFF % range) ensures only unbiased chunks are used. Any chunk ≥ maxFair is discarded.
For range = 40 (first step, i=39):
maxFair = 0xFFFFFFFF − (0xFFFFFFFF % 40)
= 4,294,967,295 − 15
= 4,294,967,280 (= 0xFFFFFFF0)
Rejected values: [4,294,967,280 .. 4,294,967,295] = 16 values
Rejection rate: 16 / 4,294,967,296 ≈ 3.7 × 10⁻⁹
Accepted values: exactly divisible by 40
→ each residue 0–39 equally likely (zero bias)For range = 2 (last step, i=1), maxFair = 0xFFFFFFFF − (0xFFFFFFFF % 2) = 0xFFFFFFFE. Only two values (0xFFFFFFFE, 0xFFFFFFFF) are rejected — negligible. For all 39 ranges in the shuffle, the rejection ceiling makes every swap index uniformly distributed. Retries (all 8 chunks rejected in one hash) are astronomically rare — probability (16/2^32)^8 ≈ 10⁻⁷⁶ for range=40.
2.4RNG Isolation
Each shuffle step within a single bet uses a unique cursor value in the HMAC message (clientSeed:nonce:cursor), ensuring per-step outputs are cryptographically independent. Each bet uses a unique nonce, ensuring per-bet outputs are independent. HMAC-SHA256 is a pseudorandom function — knowing one step's output gives zero information about other steps' outputs. There is no shared state between steps, between bets, or between epochs.
Evidence: The computeDrawFromBuffer 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 (keyBuffer, clientSeed, nonce) explicitly and returns an array. Same inputs always produce the same output.
2.5Monte Carlo Simulation (40M Rounds)
A 40,000,000-round Monte Carlo simulation (1,000,000 rounds per config, all 40 configurations) verified that the algorithm produces the expected hit distribution at scale.
| Metric | Value |
|---|---|
| Average simulated RTP | 99.970% |
| Average theoretical RTP | 99.900% |
| Chi-squared (α=0.01) | 40/40 configs pass |
| Bonferroni-corrected (α/40) | 40/40 pass |
| Serial independence | 40/40 pass |
// Simulation parameters// Per-config pinned seeds via crypto.randomBytes// Rounds: 1,000,000 per config × 40 configs = 40,000,000// Hit distribution tested against hypergeometric P(k)All 40 configurations pass individual chi-squared tests. Serial independence: 0 failures across 40M rounds.
Methodology: Per-config pinned seeds for reproducibility. Chi-squared goodness-of-fit on hit-count distribution vs independent hypergeometric P(k) = C(picks,k)×C(40−picks,10−k)/C(40,10). 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 hit-count 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(series: number[]): number { const n = series.length; const mean = series.reduce((a, b) => a + b, 0) / n; let num = 0, den = 0; for (let i = 0; i < n - 1; i++) num += (series[i] - mean) * (series[i + 1] - mean); for (let i = 0; i < n; i++) den += (series[i] - mean) ** 2; return den === 0 ? 0 : num / den;}2.7Worked Example — Full RNG Trace
Real bet from dataset — Game ID 34309702, MEDIUM risk, picks=8, nonce 4. Verified from keno-master-5100bets.json:
serverSeed = 94eee14e882d43325f7948ef9569552f4506feeeb70a2ca35023916b4f9f0fe9
clientSeed = dAn1wtkuDAXxQOKP
nonce = 4
risk = MEDIUM (difficulty=3)
picks = 8
selected = [1, 5, 6, 15, 17, 22, 33, 36]| i | cursor | range | HMAC[:8] | uint32 | j | swap |
|---|---|---|---|---|---|---|
| 39 | 0 | 40 | b8feba71 | 3,103,701,617 | 17 | pos[39]↔︎pos[17] |
| 38 | 1 | 39 | 5a417f90 | 1,514,241,936 | 12 | pos[38]↔︎pos[12] |
| 37 | 2 | 38 | e435da0a | 3,828,734,474 | 14 | pos[37]↔︎pos[14] |
| 36 | 3 | 37 | 34334094 | 875,774,100 | 10 | pos[36]↔︎pos[10] |
| 35 | 4 | 36 | 7ea8251d | 2,124,948,765 | 21 | pos[35]↔︎pos[21] |
2.01×. Payout: 0.01 × 2.01 = 0.0201.Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary6 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Algorithm implementation (Step 5) | 5,100/5,100 bets | Pass |
| Key encoding (hex vs UTF-8) | Confirmed via recomputation | Pass |
| Modulo bias analysis | Rejection sampling: maxFair ceiling for all ranges 2–40 | Pass |
| drand non-participation (Step 5) | 5,100/5,100 reproduced without drand | Pass |
| Simulation chi-squared (Step 19) | 0/40 configs fail at α=0.01 | Pass |
| Serial independence (Step 19) | 0/40 configs fail (lag-1 + runs test) | Pass |
2.9Code References5 source files
| File | Purpose |
|---|---|
| src/rng.ts | HMAC-SHA256 backward Fisher-Yates (computeDraw, computeDrawFromBuffer) |
| src/simulate.ts | Monte Carlo simulation (40M rounds, two-pass) |
| src/stats.ts | Chi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test |
| tests/steps/determinism.ts | Steps 5–6: Draw 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 5,100 bets | Pass |
| Key is hex-decoded (not UTF-8) — wrong encoding produces wrong draws | Pass |
| Rejection sampling eliminates modulo bias for all ranges (2–40) | Pass |
| No external entropy sources required for draw computation | Pass |
| Hit distribution matches hypergeometric for all 40 configs (1M rounds each) | Pass |
| Lag-1 autocorrelation near zero for all 40 configs | Pass |
| Runs test p > 0.01 for all 40 configs | Pass |
| Per-config simulated RTP converges to 99.9% theoretical | Pass |
| Client seed change produces different drawn SET in 100.0% of bets (5,100/5,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-keno.gitcd duel-keno && npm installnpm run simulate # 40M-round simulation (~29 min)npm run verify # Steps 5, 19 cover S2S2-related steps:
[PASS] Step 5 — Draw 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 drawn number set 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.
What we verified
- Every bet independently recomputed from seeds — full drawn number set verified, not just the payout
- Payout correctness: win_amount = bet × multiplier, exact to 8 decimal places for all 5,100 bets
- Multiplier table produces the correct value for all 40 risk/picks configurations
- Bet amount is not an input to the RNG — drawn numbers depend only on seeds and nonce
- All four capture phases recomputed identically (config coverage, picks=1 edge case, picks=10 max variance, elevated stake)
What this means for you
- The verifier isn't a simulation — it produces the exact same drawn set 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 drawn number set verified, not just the payout
- Payout correctness: win_amount = bet × multiplier, exact to 8 decimal places for all 5,100 bets
- Multiplier table produces the correct value for all 40 risk/picks configurations
- Bet amount is not an input to the RNG — drawn numbers depend only on seeds and nonce
- All four capture phases recomputed identically (config coverage, picks=1 edge case, picks=10 max variance, elevated stake)
- The verifier isn't a simulation — it produces the exact same drawn set 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
Draw recomputationPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 5,100 bets matched the independent verifier exactly — drawn number sets verified across all four capture phases. Payout math correct. Multiplier table confirmed across all 40 configurations.
| Check | Result | Evidence |
|---|---|---|
| Draw recomputation | Pass | 5,100/5,100 exact match — drawn number SET verified for every bet |
| Payout correctness | Pass | All 5,100 bets: win_amount = bet × multiplier, exact to 8 decimal places |
| Multiplier table integrity | Pass | All observed multipliers match kenoConfig.json for all 40 configurations |
| Bet-size independence | Pass | Bet amount is absent from the RNG input — drawn numbers depend only on seeds and nonce |
| Config completeness | Pass | All 40 risk/picks configurations (4 risks × 10 pick counts) covered in live data |
| Multi-phase coverage | Pass | 4 structured phases: config coverage (A), picks=1 edge case (B), picks=10 max variance (C), elevated stake (D) |
Draw recomputationPass
Payout correctnessPass
Multiplier table integrityPass
Bet-size independencePass
Config completenessPass
Multi-phase coveragePass
All 5,100 bets matched the independent verifier exactly — drawn number sets verified across all four capture phases. Payout math correct. Multiplier table confirmed across all 40 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 drawn number set 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, edge-case picks, bet-size invariance, and client seed verification.
| Phase | Bets | Config | Bet Amount | Purpose |
|---|---|---|---|---|
| A — Configuration coverage | 3,000 | 32 configs (picks=2–9) | $0.01 | Verify mid-range pick counts across all 4 risk levels |
| B — Picks=1 edge case | 1,000 | 4 configs (picks=1) | $0.01 | Minimum selection; consolation payout verification |
| C — Picks=10 max variance | 1,000 | 4 configs (picks=10) | $0.01 | Maximum selection; highest per-bet variance |
| D — Stake equivalence | 100 | 1 config (MEDIUM picks=10) | $10.00 | Confirm bet amount is not an RNG input |
3.3Draw Recomputation (Step 5)
For every bet belonging to an epoch with a revealed server seed, the verifier independently computed the drawn number set using computeDrawFromBuffer(key, clientSeed, nonce) and compared it (as a SET) to the server-reported drawn_numbers. The computation uses HMAC-SHA256 backward Fisher-Yates with the hex-decoded server seed as key.
// Step 5: Draw Recomputationfor (const b of bets) { const ss = seedMap.get(b.seed.serverSeedHashed); if (!ss) { skipped++; continue; } const key = Buffer.from(ss, 'hex'); const computed = computeDrawFromBuffer(key, b.seed.clientSeed, b.seed.nonce); const apiSet = new Set(b.response.drawn_numbers); const compSet = new Set(computed); if (apiSet.size !== compSet.size || [...apiSet].some(n => !compSet.has(n))) { mismatches++; }}drawn_numbers in the dataset.3.4Payout Math (Step 7)
For each of the 5,100 bets, the verifier computed amount × multiplier and compared the result to the server-reported amount_won. 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 Mathfor (const b of bets) { const mult = parseFloat(b.response.multiplier); const amount = parseFloat(b.request.amount); const won = parseFloat(b.response.amount_won); if (mult > 0) { if (Math.abs(amount * mult - won) > 1e-8) errors++; } else { if (Math.abs(won) > 1e-12) errors++; } checked++;}win_amount = amount × multiplier within tolerance 1e-8. Zero mismatches. Payout math is exact.3.5Multiplier Table Provenance (Step 8)
For each of the 5,100 bets, the observed multiplier was compared against the multiplier table in kenoConfig.json. The lookup uses risk level, pick count, and hit count to retrieve the expected multiplier. All 5,100 bets match exactly — confirming the live game uses the same multiplier table as the published configuration.
| Metric | Count |
|---|---|
| Total bets checked | 5,100 |
| Matching kenoConfig.json | 5,100 |
| Mismatches | 0 |
// Step 8: Multiplier Table Provenancefor (const b of bets) { const risk = DIFFICULTY_TO_RISK[b.request.difficulty]; const picks = b.request.selected_positions.length; const hits = countHits(b); const mult = parseFloat(b.response.multiplier); try { const cfgMult = cfg.multiplier(risk, picks, hits); if (Math.abs(cfgMult - mult) > 1e-8) mismatches++; } catch { mismatches++; }}kenoConfig.json. 40/40 configurations verified. Base-tier 0.1% house edge across all 40 configs (captured config has scaling_edge = []); the live game applies bet-size scaling (0.1%→1.0%) separately — see 4.6b3.6Consolation Payout Verification (Step 12)
Two of the 40 configurations pay non-zero multipliers on zero hits — a design choice within the 99.9% RTP constraint. Step 12 verifies these consolation payouts match the published kenoConfig.json multiplier table exactly.
| Config | Zero-Hit Multiplier | Verified |
|---|---|---|
| LOW picks=1 | 0.71× | ✅ |
| MEDIUM picks=1 | 0.41× | ✅ |
| All 38 other configs | 0× | ✅ |
3.7Phase D — Bet-Size Equivalence (Step 9)
Phase D placed 100 bets at $10 on MEDIUM picks=10 — the same configuration tested at $0.01 in Phase C. All 100 draws were recomputed correctly from revealed seeds. All 100 payout multipliers matched the same kenoConfig.json table as Phase C. Equivalence is proven deterministically — 100/100 exact draw and multiplier matches. Phase D used 2 distinct pfaudit-prefixed client seeds to additionally confirm client seed variation at the elevated stake.
3.8Worked Example — Full Parity Verification
Real bet from Phase C — Game ID 34333136, LOW risk, picks=10, nonce 0. Verified from keno-master-5100bets.json:
serverSeed = d978b0fb5a2b9690a0052c0a232845d6b79b3cfd13976b6b75f14dcfd3716461
clientSeed = DJPPRaQ41Tl9XchI
nonce = 0
risk = LOW (difficulty=2)
picks = 10
selected = [6, 11, 13, 14, 16, 19, 21, 27, 32, 39]| Step | Process | Output |
|---|---|---|
| 1 | Backward Fisher-Yates: 39 HMAC-SHA256 calls with cursor 0–38 | Shuffled positions array |
| 2 | positions[0..9] + 1 → drawn SET | {4, 16, 19, 22, 30, 32, 33, 36, 37, 39} |
| 3 | Count hits: selected ∩ drawn | 4 hits (matched: 16, 19, 32, 39) |
| 4 | Multiplier lookup: LOW/picks=10, 4 hits | 1.31× |
| 5 | Payout: 0.01 × 1.31 | 0.0131 ✅ |
// computeDraw("d978b0fb...", "DJPPRaQ41Tl9XchI", 0)// 39 Fisher-Yates steps → drawn SET//// Multiplier lookup: kenoConfig.multipliers.LOW["10"][4]// = 1.31//// Payout: 0.01 × 1.31 = 0.0131// Live amount_won: 0.0131 ✅Technical Evidence & Verification5 sections
3.9Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Draw recomputation | 5,100 / 5,100 bets — full drawn SET verified | Pass |
| Payout math | 5,100 / 5,100 bets (exact to 8 decimal places) | Pass |
| Multiplier table integrity | All 40 configs — observed multipliers match kenoConfig.json | Pass |
| Consolation payout verification | 2/2 zero-hit configs (LOW/MEDIUM picks=1) confirmed | Pass |
| Phase D draw recomputation | 100 / 100 at $10 | Pass |
| Phase D multiplier match | 100 / 100 bets — identical to $0.01 table | Pass |
| Config completeness | All 40 configs (4 risks × 10 picks) covered in live data | Pass |
| Multi-phase coverage | 4 phases: A (3,000) + B (1,000) + C (1,000) + D (100) | Pass |
3.10Code References5 source files
| File | Purpose |
|---|---|
| tests/steps/determinism.ts | Step 5: Draw recomputation |
| tests/steps/payouts.ts | Steps 7–12: Payout math, multiplier provenance, Phase D equivalence, consolation payouts |
| tests/steps/dataset.ts | Steps 10, 13–18: Config completeness, epoch size, phase labels, anti-circularity |
| src/rng.ts | HMAC-SHA256 backward Fisher-Yates |
| src/config.ts | KenoConfig multiplier lookup and RTP calculation |
3.11Datasets UsedSource artifacts
3.12Verified Invariants9 invariants
| Invariant | Result |
|---|---|
| Computed drawn SET matches live drawn_numbers for all 5,100 bets | Pass |
| win_amount = bet × multiplier, exact to 8 decimal places for all 5,100 bets | Pass |
| Every multiplier matches kenoConfig.json for all 40 configurations | Pass |
| Consolation payouts verified: LOW picks=1 (0.71×), MEDIUM picks=1 (0.41×) | Pass |
| Phase D ($10) produces identical drawn sets to $0.01 bets | Pass |
| Phase D multipliers match $0.01 table (same config, different bet size) | Pass |
| No hidden inputs beyond (serverSeed, clientSeed, nonce) | Pass |
| All 40 risk/picks configurations present in live data | Pass |
| Phase D: 100/100 draws verified across 2 custom client seeds | Pass |
3.13Reproduction InstructionsRun locally
git clone https://github.com/ProvablyFair-org/duel-keno.gitcd duel-keno && npm installnpm run verify# Expected output: Steps 5, 7–12 all PASSS3-related steps:
[PASS] Step 5 — Draw Recomputation (RNG determinism)
[PASS] Step 7 — Payout Math
[PASS] Step 8 — Multiplier Table Provenance
[PASS] Step 9 — Phase C Bet-Size Invariance (Phase D at $10)
[PASS] Step 10 — Configuration Completeness
[PASS] Step 11 — Zero-Edge Audit (scaling_edge absent)
[PASS] Step 12 — Consolation Payout Disclosure (LOW/MEDIUM picks=1 zero-hit)RTP & Payout Logic
Is the house edge what the casino claims?
This section mathematically verifies that the base-tier 0.1% house edge is exactly what's advertised across all 40 configurations. The key test is anti-circularity: we prove the RTP using independently-derived hypergeometric probabilities applied to the committed multiplier table — no casino-supplied probability data enters the computation. We then confirm it against 40 million simulated rounds and test whether the casino pre-selected favourable seeds.
What we verified
- House edge is exactly 0.1% — flat across all 40 configurations at the base bet tier
- RTP proven analytically: independently-derived hypergeometric P(hits) × committed multiplier = 99.9% for every configuration
- 40M-round simulation converges on theoretical RTP (mean 99.970%)
- Cherry-pick detection: 102 casino seeds tested — no evidence of seed pre-selection
- Bet amount does not influence drawn numbers — confirmed at $0.01 and $10
What this means for you
- At the base bet tier, the house edge on Keno is a flat 0.1%, the same across all configurations
- The RTP proof is derived independently — it doesn't rely on trusting the casino
- The casino's seeds show no evidence of being chosen to produce favourable early outcomes
- Your bet amount doesn't affect which numbers are drawn
- Bet-size-scaled house edge — the base-tier margin is 0.1%, scaling up to 1.0% on larger bets (see 4.6b); bet size never affects which numbers are drawn
- House edge is exactly 0.1% — flat across all 40 configurations at the base bet tier
- RTP proven analytically: independently-derived hypergeometric P(hits) × committed multiplier = 99.9% for every configuration
- 40M-round simulation converges on theoretical RTP (mean 99.970%)
- Cherry-pick detection: 102 casino seeds tested — no evidence of seed pre-selection
- Bet amount does not influence drawn numbers — confirmed at $0.01 and $10
- At the base bet tier, the house edge on Keno is a flat 0.1%, the same across all configurations
- The RTP proof is derived independently — it doesn't rely on trusting the casino
- The casino's seeds show no evidence of being chosen to produce favourable early outcomes
- Your bet amount doesn't affect which numbers are drawn
- Bet-size-scaled house edge — the base-tier margin is 0.1%, scaling up to 1.0% on larger bets (see 4.6b); bet size never affects which numbers are drawn
Anti-circularityPass
House edge auditInfo
kenoConfig.json table, verified by anti-circularity Step 17. Scaling to 1.0% on larger bets — see 4.6bSimulated RTP (Pass 1)Pass
Cherry-pick detection (Pass 2)Pass
Bet-size invariancePass
Multiplier formulaPass
Config completenessPass
Consolation payout designPass
The 99.9% RTP is proven mathematically from independently-derived hypergeometric probabilities applied to the committed table — P(hits) × multiplier = 0.999 for all 40 configurations. This is an analytic proof, not a statistical estimate. 40M simulated rounds and cherry-pick detection confirm no anomalies. The house edge is 0.1% at the base bet tier; the live game applies bet-size scaling to the multiplier table (0.1%→1.0%) — see 4.6b.
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | Hypergeometric P(hits) × multiplier = 99.9% for all 40 configs — derived from C(picks,k)×C(40−picks,10−k)/C(40,10), no casino data used |
| House edge audit | Info | 0.1% base-tier house edge across all 40 configs — derived from the committed kenoConfig.json table, verified by anti-circularity Step 17. Scaling to 1.0% on larger bets — see 4.6b |
| Simulated RTP (Pass 1) | Pass | 40M rounds, avg RTP = 99.970%, 0/40 chi-squared failures, 0/40 serial independence failures |
| Cherry-pick detection (Pass 2) | Pass | 102 casino seeds tested across 1,666 combinations — no evidence of seed pre-selection |
| Bet-size invariance | Pass | Bet amount is not an input to the RNG — same draw distribution at $0.01 and $10. Tested in Phase D (100/100) |
| Multiplier formula | Pass | Hypergeometric P(k) × kenoConfig multipliers = 99.9% independently verified for all 40 configs |
| Config completeness | Pass | All 40 risk/picks configurations covered |
| Consolation payout design | Pass | LOW picks=1 (0.71×) and MEDIUM picks=1 (0.41×) on zero hits — within 99.9% RTP constraint |
Anti-circularityPass
House edge auditInfo
kenoConfig.json table, verified by anti-circularity Step 17. Scaling to 1.0% on larger bets — see 4.6bSimulated RTP (Pass 1)Pass
Cherry-pick detection (Pass 2)Pass
Bet-size invariancePass
Multiplier formulaPass
Config completenessPass
Consolation payout designPass
The 99.9% RTP is proven mathematically from independently-derived hypergeometric probabilities applied to the committed table — P(hits) × multiplier = 0.999 for all 40 configurations. This is an analytic proof, not a statistical estimate. 40M simulated rounds and cherry-pick detection confirm no anomalies. The house edge is 0.1% at the base bet tier; the live game applies bet-size scaling to the multiplier table (0.1%→1.0%) — see 4.6b.
How It Works — RTP & Payout Logic9 sections
4.1Anti-Circularity Proof (Step 17)
The anti-circularity proof establishes the 99.9% RTP from independently-derived hypergeometric probabilities applied to the committed multiplier table — without using any casino-supplied probability data. (The multipliers themselves are operator-supplied and hash-pinned; the probabilities are derived independently.) This is what separates a mathematical proof from a statistical estimate. The proof has two components — each derived independently:
| Component | Formula | Source |
|---|---|---|
| Hit probability | P(hits=k) = C(picks,k) × C(40−picks, 10−k) / C(40,10) | Hypergeometric distribution — pure math, not from casino |
| Multiplier | kenoConfig.multipliers[risk][picks][k] | Casino-supplied — observed from kenoConfig.json |
| RTP computation | Σ P(k) × multiplier(k) for k = 0 to picks | Independent probability × observed multiplier |
| Result | = 0.999 for all 40 configurations | Analytic proof (independent probabilities × committed table) — not a statistical estimate |
| Picks | Risk | P(0 hits) | Multiplier(0) | Σ P×M | RTP |
|---|---|---|---|---|---|
| 1 | Classic | 0.75000 | 0× | 0.99900 | 99.9% |
| 1 | Low | 0.75000 | 0.71× | 0.99900 | 99.9% |
| 5 | Classic | 0.21657 | 0× | 0.99900 | 99.9% |
| 10 | High | 0.03544 | 0× | 0.99900 | 99.9% |
Anti-Circularity Verification:
// Step 17: Probability Independence — Anti-Circularityfor (const { risk, picks } of cfg.allConfigs()) { let indepRTP = 0; for (let k = 0; k <= picks; k++) { indepRTP += cfg.hitProbability(picks, k) * cfg.multiplier(risk, picks, k); } const deviation = Math.abs(indepRTP - 0.999); if (deviation > maxDeviation) { maxDeviation = deviation; worstConfig = `${risk}/picks=${picks} (RTP=${(indepRTP * 100).toFixed(6)}%)`; }}Why this proof is non-circular: P(hits) comes from the hypergeometric distribution — a mathematical property of sampling without replacement from a 40-number grid, 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 11)
The base-tier game-engine house edge is 0.1% (RTP = 99.9%), implemented by the multiplier table values themselves. kenoConfig.json defines a multiplier for each (risk, picks, hits) combination, and each base-tier table was constructed to yield 99.9% RTP — verified independently by the anti-circularity proof in Step 17 (hypergeometric P(k) × multiplier sums to 0.999 for all 40 configs). This is the audit's primary RTP finding. The scaling_edge field is an empty array in the captured kenoConfig.json, meaning the audited base-tier configuration carries no bet-size brackets; the live game applies bet-size edge scaling (0.1% up to 1.0%) on top of this table, deployed separately and disclosed in 4.6b. Step 8 verifies the multiplier matches the base-tier kenoConfig.json for every captured bet; Step 11 confirms the captured config has scaling_edge = [].
Every bet response also carries an effective_edge field. Per the operator's published mechanism, this field represents the player's net edge after Zero Edge rakeback is applied: bets are tagged 0.1 at settlement and updated to 0 asynchronously once an operator-side rewards queue processes the rakeback. All 5,100 bets in the captured dataset read 0.1, captured at settlement time. A sample re-fetch of bets initially captured at 0.1 showed the field had since updated to 0 — consistent with the operator's description of the rakeback mechanism as an asynchronous post-settlement queue. The audit does not verify the rewards transaction layer itself; it certifies the game-engine base-tier 0.1% house edge, which comes from the multiplier table values and is independent of the effective_edge field. See the audit scope exclusions for the rakeback layer.
kenoConfig.json for all 40 configurations (Step 8), (ii) confirming the captured config has scaling_edge = [] — the base-tier table carries the 0.1% edge as audited (Step 11), while the live game adds bet-size scaling separately (see 4.6b), and (iii) proving the RTP via the Step 17 anti-circularity proof (independently-derived probabilities applied to the committed table). The effective_edge field is not a game-logic input — it is constant at 0.1 across all 5,100 bets, and the engine's drawn numbers and payouts derive entirely from the seed chain and the kenoConfig.json multiplier tables, with no dependence on the field. The operator-side rakeback layer that updates net edge to 0% is outside this audit's scope.4.3Full RTP Table — All 40 Configurations
All 40 configurations produce the same theoretical RTP of 99.900%. The simulated RTP varies per config due to sampling variance, particularly for high-risk configurations where jackpot multipliers create large per-bet variance.
| Config | Theoretical RTP | Simulated RTP | Deviation |
|---|---|---|---|
| CLASSIC/p1 | 99.900% | 100.059% | +0.159% |
| CLASSIC/p5 | 99.900% | 100.028% | +0.128% |
| CLASSIC/p10 | 99.900% | 99.931% | +0.031% |
| LOW/p1 | 99.900% | 99.912% | +0.012% |
| LOW/p10 | 99.900% | 99.936% | +0.036% |
| MEDIUM/p5 | 99.900% | 101.340% | +1.440% |
| HIGH/p5 | 99.900% | 101.786% | +1.886% |
| HIGH/p10 | 99.900% | 99.774% | −0.126% |
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 40 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 hit-count frequencies to expected hypergeometric frequencies. 0/40 fail at uncorrected α=0.01. 0/40 fail at Bonferroni α/40 = 0.00025.
Serial independence: Lag-1 autocorrelation measures correlation between consecutive hit counts. Runs test checks for non-random run structure. 0/40 configs fail either test.
RTP convergence: Avg simulated RTP = 99.970% vs 99.900% theoretical. Deviation of +0.070% is within expected sampling variance.
// Pass 1 — Fresh random seeds// 1,000,000 rounds × 40 configs = 40,000,000 total// Per-config pinned seeds for reproducibility (S7)const seedPair = SIM_SEEDS[ci];const keyBuffer = Buffer.from(seedPair.server, 'hex');const clientSeed = seedPair.client; for (let nonce = 0; nonce < rounds; nonce++) { const drawn = computeDrawFromBuffer(keyBuffer, clientSeed, nonce, cfg.gridSize, cfg.drawnCount); const drawnSet = new Set(drawn); let hits = 0; for (let p = 1; p <= picks; p++) { if (drawnSet.has(p)) hits++; } hitCounts[hits]++; totalPayout += cfg.multiplier(risk, picks, hits);}What is a Monte Carlo simulation? Instead of proving fairness with algebra alone, we simulate millions of real game rounds. Each round computes a drawn number set from scratch using the same HMAC-SHA256 backward Fisher-Yates 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 distribution: Does any individual seed×config combination produce a hit distribution that deviates from hypergeometric across 10,000 nonces? 13/1,666 fail at α=0.01 — within the expected range.
Test B — Early vs late window: Does any seed produce worse outcomes in the early nonces (0–49, where real players bet) compared to later nonces (50–9,999)? 57/1,666 flags ≤ 80 threshold. 0 broad flags — no cherry-picking signature.
| Test | Result |
|---|---|
| Seeds tested | 102 |
| Seed × config combinations | 1,666 |
| Test A fails (p<0.01) | 13 / 1,666 — within expected ≤17 |
| Cherry-pick flags (Test B) | 57 / 1,666 — within expected ≤80 |
| Broad flags (early+late deviate) | 0 — definitive negative indicator |
// Step 20: Pass 2 Cherry-Pick Testconst N = pass2.seed_config_combinations;const flags = pass2.test_b_cherry_pick_flags;const testA = pass2.test_a_chi2_fails_at_alpha01;const expCP = Math.ceil(N * 0.05 * 0.95);const expTA = Math.ceil(N * 0.01); let broadFlags = 0;let earlyOnly = 0;for (const r of pass2.results) { if (r.cherry_pick_flag) { if (r.early_p < 0.05 && r.late_p < 0.05) broadFlags++; else earlyOnly++; }}4.6Bet-Size Invariance (Step 9)
Phase D placed 100 bets at $10 on MEDIUM picks=10 — the same configuration tested at $0.01 in Phase C. All 100 draws were recomputed correctly from revealed seeds using the same HMAC-SHA256 backward Fisher-Yates path. Bet amount is not an input to the RNG and does not affect the draw distribution — the drawn numbers are determined solely by the seed chain and nonce. Both bet sizes settled against the base-tier kenoConfig.json table (all captured bets, at $0.01 and $10, fall in the base tier). Note that bet size does affect the house-edge tier on the live game — larger bets settle against a higher-edge multiplier table (0.1% up to 1.0%) — but this changes the payout multiplier, never the draw. See 4.6b for the scaling-edge disclosure.
| Metric | Phase C ($0.01) | Phase D ($10) |
|---|---|---|
| Bets | 1,000 | 100 |
| Config | 4 configs (picks=10) | 1 config (MEDIUM picks=10) |
| Draws recomputed | 1,000/1,000 | 100/100 |
| Multiplier table | kenoConfig.json (base tier) | kenoConfig.json (base tier) |
| RNG path | HMAC-SHA256 Fisher-Yates | HMAC-SHA256 Fisher-Yates |
4.6bProgressive House Edge — Bet-Size Scaling (Disclosure)
The live game applies a bet-size-dependent house edge that is not present in the audited base-tier configuration. The edge is indexed to wager size as a fraction of a fixed house bankroll (~$25M): each (picks, difficulty) configuration defines a ladder of contiguous bet-size tiers, each carrying its own multiplier table, with the game-engine edge stepping from a 0.1% floor to a 1.0% ceiling. Higher tiers reduce the multiplier table proportionally; the draw distribution is invariant across tiers (confirmed in 4.6), so only the payout per outcome changes. Table 1 documents the edge progression for a representative configuration (1-pick classic).
Tier depth is bounded by each configuration's per-bet payout exposure, not by uniform design. Low-pick configurations expose the full 91-tier ladder to the 1.0% ceiling; high-jackpot configurations reach their payout limit within a few tiers, so the ladder is largely inert on high-volatility play — 10-pick LOW and MEDIUM remain at the 0.1% floor across their entire bet range. Table 2 documents tier structure by configuration.
Scope of this finding: the audit was conducted against the base-tier (0.1%) kenoConfig.json, the configuration under which the 5,100-bet dataset was captured and on which the 99.9% RTP proof is conditioned. All captured wagers ($0.01 and $10) fall within the base tier, where the multiplier tables are identical to the audited artifact. The scaling structure was deployed independently and is live; the tiers above 0.1% are disclosed here but lie outside the captured dataset and are not certified by this audit.
Tier-selection mechanism: Each tier's bet_range carries a max_bankroll_percentage, and across every tier the identity bet_max ÷ max_bankroll_percentage = $25,000,000 holds exactly — confirming the selector is wager size as a fraction of a fixed $25M bankroll. The tiers are contiguous (each tier's max equals the next tier's min), so a wager maps to exactly one tier, and that tier's edge and multiplier table apply. As the wager grows relative to the bankroll, the edge steps up (0.10 → 0.11 → 0.12 … in the config's internal units, i.e. 0.1% → 1.0%), shaving the multipliers accordingly (e.g. a 1-pick hit: 3.996× at the floor → 3.96× at the ceiling). This is a variance / risk-management lever, not a flat margin.
Game-engine edge vs. net edge: The tier figures represent the game-engine edge, prior to rakeback. Per the operator's disclosed Zero Edge mechanism (4.8), settled wagers are tagged at the tier edge and asynchronously rebated toward 0% net. This audit certifies the multiplier tables only; the rewards-transaction layer is outside scope. Tier figures should therefore be read as game-engine edge, not guaranteed net edge.
| Bet Range | House Edge | RTP | 1-Hit Multiplier |
|---|---|---|---|
| $0 – $8,344 | 0.1% | 99.9% | 3.996× |
| $15,874 – $16,711 | 0.2% | 99.8% | 3.992× |
| $41,102 – $41,946 | 0.5% | 99.5% | 3.980× |
| $83,604 – $84,459 | 1.0% | 99.0% | 3.960× |
| Config | Tiers | Base-Tier Ceiling | Edge Range |
|---|---|---|---|
| 1-pick low | 91 | $99,546 | 0.1% – 1.0% |
| 1-pick classic | 91 | $8,344 | 0.1% – 1.0% |
| 10-pick high | 8 | $1,550 | 0.1% – ~0.2% |
| 10-pick classic | 3 | $12,883 | 0.1% – 0.12% |
| 10-pick low / medium | 1 | $2,502 | 0.1% (flat — no scaling) |
4.7Consolation Payout Design (Step 12)
Two configurations pay non-zero multipliers on zero hits. These are design choices within the 99.9% RTP constraint — they partially offset the worst-case outcome for single-pick bets at low and medium risk. The consolation payouts are funded by slightly lower hit multipliers on the same configurations.
| Config | P(0 hits) | Zero-Hit Multiplier | Weighted Contribution |
|---|---|---|---|
| LOW picks=1 | 0.750000 | 0.71× | 0.532500 |
| MEDIUM picks=1 | 0.750000 | 0.41× | 0.307500 |
| All 38 others | varies | 0× | 0.000000 |
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 | 118.478% | 3,000 bets at $0.01 (picks=2–9) — above theoretical due to variance |
| Phase B empirical RTP | 105.378% | 1,000 bets at $0.01 (picks=1) — within expected range |
| Phase C empirical RTP | 95.821% | 1,000 bets at $0.01 (picks=10) — high variance configs |
| Phase D empirical RTP | 90.388% | 100 bets at $10 (MEDIUM picks=10) — expected at N=100 |
| Overall empirical RTP | 91.385% | 5,100 bets — dominated by $1,000 Phase D wagered amount |
4.9Worked Example — Payout Verification
Real bet from Phase A — Game ID 34309818, CLASSIC risk, picks=5, nonce 32. Verified from keno-master-5100bets.json:
serverSeed = 94eee14e882d43325f7948ef9569552f4506feeeb70a2ca35023916b4f9f0fe9
clientSeed = dAn1wtkuDAXxQOKP
nonce = 32
risk = CLASSIC (difficulty=1)
picks = 5
selected = [3, 4, 22, 30, 40]Step 1 — Draw computation: computeDraw(serverSeed, clientSeed, 32) → 39 Fisher-Yates steps → drawn SET = {2, 3, 4, 13, 18, 21, 23, 26, 31, 32}
Step 2 — Hit count: selected ∩ drawn = {3, 4} → 2 hits
Step 3 — Multiplier lookup: kenoConfig.multipliers.CLASSIC["5"][2] = 1.4
Step 4 — Payout: 0.01 × 1.4 = 0.014
| Step | Process | Output |
|---|---|---|
| 1 | Backward Fisher-Yates: 39 HMAC calls | drawn SET = {2,3,4,13,18,21,23,26,31,32} ✅ |
| 2 | Count hits: selected ∩ drawn | 2 hits (matched: 3, 4) |
| 3 | Multiplier: CLASSIC/picks=5, 2 hits | 1.4× |
| 4 | RTP check: P(2 hits) × 1.4 | P=0.27766, contribution=0.38872 |
| 5 | Payout: 0.01 × 1.4 | 0.014 ✅ |
// CLASSIC picks=5 — anti-circularity proof// P(hits=k) = C(5,k) × C(35, 10-k) / C(40,10)//// k=0: 0.21657 × 0.00 = 0.00000// k=1: 0.41648 × 0.25 = 0.10412// k=2: 0.27766 × 1.40 = 0.38872// k=3: 0.07933 × 4.15 = 0.32923// k=4: 0.00957 × 17.00 = 0.16276// k=5: 0.00038 × 37.00 = 0.01417//// Σ P×M = 0.999000 (99.9%) ✅Technical Evidence & Verification5 sections
4.10Evidence Coverage Summary7 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Anti-circularity (Step 17) | 40/40 configs, max deviation 0.000000% | Pass |
| House edge audit (Step 11) | Multiplier table verified against base-tier kenoConfig.json for all bets (Step 8); the captured config has no scaling brackets (scaling_edge = []); edge derived from table values. Live game adds bet-size scaling separately — see 4.6b. | Pass |
| Bet-size scaling (disclosure) | Base tier (0.1%) audited; tiers 0.1%→1.0% live but outside captured dataset | Info — see 4.6b |
| Consolation payouts (Step 12) | 2/2 zero-hit configs verified | Pass |
| Simulation Pass 1 (Step 19) | 40M rounds, 0/40 chi-squared, 0/40 serial | Pass |
| Simulation Pass 2 (Step 20) | 1,666 combos, 57 flags ≤ 80, 0 broad | Pass |
| Bet-size invariance (Step 9) | 100/100 Phase D at $10 | Pass |
4.11Code References6 source files
| File | Purpose |
|---|---|
| tests/steps/dataset.ts | Step 17: Probability independence and RTP cross-check |
| tests/steps/payouts.ts | Steps 9, 11, 12: Bet-size invariance, zero-edge audit, consolation payouts |
| tests/steps/simulation.ts | Steps 19–20: Simulation integrity and cherry-pick detection |
| src/simulate.ts | Monte Carlo simulation (40M rounds, two-pass) |
| src/stats.ts | chiSquaredTest, lag1Autocorrelation, runsTest |
| src/config.ts | hitProbability, multiplier, theoreticalRTP |
4.12Datasets UsedSource artifacts
4.13Verified Invariants10 invariants
| Invariant | Result |
|---|---|
| P(hits) × multiplier = 0.999 for all 40 configs (non-circular) | Pass |
Base-tier house edge is a flat 0.1% — derived from the committed kenoConfig.json multiplier table (captured config has scaling_edge = []); the live game applies bet-size scaling (0.1%→1.0%) separately, see 4.6b | Pass |
| Hypergeometric P(k) = C(picks,k)×C(40−picks,10−k)/C(40,10) matches kenoConfig probabilities for all configs | Pass |
| Simulated RTP average = 99.970% across 40M rounds | Pass |
| 0/40 configs reject at Bonferroni α/40 | Pass |
| 0 serial independence failures | Pass |
| No evidence of seed pre-selection across 102 casino seeds (Pass 2) | Pass |
| Phase D ($10) draws match Phase C ($0.01) algorithm | Pass |
| Bet amount absent from RNG input by construction | Pass |
| Consolation payouts (LOW/MEDIUM picks=1) within 99.9% RTP constraint | Pass |
4.14Reproduction InstructionsRun locally
git clone https://github.com/ProvablyFair-org/duel-keno.gitcd duel-keno && npm installnpm run simulate # 40M simulation + cherry-pick test (~29 min)npm run verify # Steps 9, 11, 12, 17, 19, 20 cover S4cat outputs/simulation-results.jsonS4-related steps:
[PASS] Step 9 — Phase C Bet-Size Invariance (Phase D at $10)
[PASS] Step 11 — Zero-Edge Audit (scaling_edge absent)
[PASS] Step 12 — Consolation Payout Disclosure (LOW/MEDIUM picks=1 zero-hit)
[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.
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 pick 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 pick 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).
Category Coverage
| Check | Result | Evidence |
|---|---|---|
| Nonce integrity | Pass | Sequential, server-controlled, no gaps or duplicates across 102 epochs |
| Seed commitment integrity | Pass | Locked at bet acceptance, unique per epoch — 102/102 verified |
| Outcome determinism | Pass | Identical inputs produce identical outcomes — 5,100/5,100 confirmed |
| Round & player isolation | Pass | Per-user seeds, serial independence confirmed (0/40 fail in simulation) |
| Payout integrity | Pass | Parameter limits enforced (6/6 invalid selectedPositions rejected); injected payout fields ignored (0 honoured); server-computed multiplier consistent across all probes |
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).
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 |
| Draw 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 into bet requests. All 7 silently ignored; server assigned valid sequential nonces. Continuity preserved (baseline 9 → post-test 17); 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 — 7/7 invalid client seeds rejected (empty, null, oversized >1000 chars, special chars, emoji, CJK, whitespace-only). All Keno 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 — 102 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 ~50.6 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), the game always produces the same drawn number set — verified across all 5,100 live bets
S3, Step 5 — 5,100/5,100 bets independently recomputed from seeds, 0 SET mismatches.
FI-OUTCOME-002N/AA completed bet cannot be replayed via the API to generate a duplicate payout
Keno 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 pick counts (1–10) and risk levels (Classic, Low, Medium, High) are accepted
API probe — 6/6 invalid selectedPositions combinations rejected with HTTP 422 (0 picks, 11 picks, value=0, value=41, duplicate picks, non-integer pick). Both controls (3-pick and 10-pick max) 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, drawn_numbers, matches=10, multiplier='99999', amount_won='999999') all silently ignored. Server computed drawn numbers and multiplier from the seed chain on every request. noInjectionHonoured=true.
Technical Evidence & Verification4 sections
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 |
|---|---|---|
| 102/102 seed hashes verified | S1, Step 1 | Commit-reveal chain intact |
| 106/106 next-seed promotions | S1, Step 2 | Seed rotation chain intact |
| 5,100/5,100 exact parity | S3, Step 5 | No post-RNG conditional logic |
| Anti-circularity proven | S4, Step 17 | 0.1% house edge — independent probabilities, committed table |
| 57 cherry-pick flags (≤80) | S4, Step 20 | No seed pre-selection bias |
| 100.0% 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 repo. These tests run against the captured dataset and produce deterministic results.
API probe tests (6 of 15): Verified by issuing live adversarial requests against the running game. Per-test evidence (HTTP status codes, server-assigned nonces, seed hashes, server-computed multipliers, drawn numbers) 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-keno.gitcd duel-kenonpm 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 — Draw 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 (HTTP 200)[PASS] FI-SEED-001 — Invalid client seed handling → 7/7 invalid seeds rejected[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 — Field injection handling → 0 injections honouredPlayer Verification
Can a player verify their own bets without trusting anyone?
Every Keno outcome can be independently reproduced using publicly disclosed inputs. No hidden variables, no private backend data. If your calculated drawn number set 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.
Key principles
- Every Keno outcome can be independently reproduced
- No hidden variables — no private backend data
- If your computed drawn set 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; increments within each epoch)
- Every Keno outcome can be independently reproduced
- No hidden variables — no private backend data
- If your computed drawn set 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; increments within each epoch)
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 Keno Bet
Choose your numbers from the 40-number grid, select a risk level, and place a bet. The platform draws 10 numbers 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 Keno 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 drawn numbers inline — if they match 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 numbers from the 40-number grid, choose a risk level, and place a bet. The platform draws 10 numbers using the provably fair backward Fisher-Yates shuffle. The server has already committed to the outcome before you clicked.

Duel.com Keno — 40-number grid with pick and risk selectors. Draw outcome is locked before the animation plays.
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 Keno 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 10 drawn numbers from the disclosed inputs and renders the Game Result inline as the highlighted positions on the 40-number grid. If the recomputed drawn set 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 drawn numbers 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 draw 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 perform a backward Fisher-Yates shuffle of a 40-number grid. The first 10 positions of the shuffled array become the drawn numbers. 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 Keno bet:
const crypto = require('crypto'); const GRID_SIZE = 40;const DRAWN_COUNT = 10;const MAX_UINT32 = 0xFFFFFFFF; function computeDraw(serverSeed, clientSeed, nonce) { const key = Buffer.from(serverSeed, 'hex'); const positions = Array.from({ length: GRID_SIZE }, (_, i) => i); for (let i = GRID_SIZE - 1; i > 0; i--) { const range = i + 1; const maxFair = MAX_UINT32 - (MAX_UINT32 % range); let cursor = GRID_SIZE - 1 - i; while (true) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); let found = false; for (let off = 0; off + 8 <= hmac.length; off += 8) { const value = parseInt(hmac.substring(off, off + 8), 16); if (value < maxFair) { const j = value % range; [positions[i], positions[j]] = [positions[j], positions[i]]; found = true; break; } } if (found) break; cursor++; } } return positions.slice(0, DRAWN_COUNT).map(p => p + 1).sort((a, b) => a - b);} 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; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Drawn numbers:', computeDraw(serverSeed, clientSeed, nonce));6.10Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac GRID_SIZE = 40DRAWN_COUNT = 10MAX_UINT32 = 0xFFFFFFFF def compute_draw(server_seed, client_seed, nonce): key = bytes.fromhex(server_seed) positions = list(range(GRID_SIZE)) for i in range(GRID_SIZE - 1, 0, -1): range_val = i + 1 max_fair = MAX_UINT32 - (MAX_UINT32 % range_val) cursor = GRID_SIZE - 1 - i while True: message = f'{client_seed}:{nonce}:{cursor}'.encode() h = hmac.new(key, message, hashlib.sha256).hexdigest() found = False for off in range(0, len(h) - 7, 8): value = int(h[off:off+8], 16) if value < max_fair: j = value % range_val positions[i], positions[j] = positions[j], positions[i] found = True break if found: break cursor += 1 return sorted([p + 1 for p in positions[:DRAWN_COUNT]]) 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 = 0 print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Drawn numbers:', compute_draw(server_seed, client_seed, nonce))6.11Evidence Screenshots3 records
| Evidence | Description |
|---|---|
| E02 | Fairness page overview — "What is Provably Fair?" and "How it works" sections |
| E03 | Fairness verification tool — Keno selected, showing game-specific verification inputs |
| E11 | 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 Keno Bet
Choose your numbers from the 40-number grid, select a risk level, and place a bet. The platform draws 10 numbers 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 Keno 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 drawn numbers inline — if they match your live game result, the bet was provably fair.
Visual Walkthroughstep-by-step
Select your numbers from the 40-number grid, choose a risk level, and place a bet. The platform draws 10 numbers using the provably fair backward Fisher-Yates shuffle. The server has already committed to the outcome before you clicked.

Duel.com Keno — 40-number grid with pick and risk selectors. Draw outcome is locked before the animation plays.
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 Keno 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 10 drawn numbers from the disclosed inputs and renders the Game Result inline as the highlighted positions on the 40-number grid. If the recomputed drawn set 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 drawn numbers rendered inline matching the live game result.
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 draw 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 perform a backward Fisher-Yates shuffle of a 40-number grid. The first 10 positions of the shuffled array become the drawn numbers. 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 Keno bet:
const crypto = require('crypto'); const GRID_SIZE = 40;const DRAWN_COUNT = 10;const MAX_UINT32 = 0xFFFFFFFF; function computeDraw(serverSeed, clientSeed, nonce) { const key = Buffer.from(serverSeed, 'hex'); const positions = Array.from({ length: GRID_SIZE }, (_, i) => i); for (let i = GRID_SIZE - 1; i > 0; i--) { const range = i + 1; const maxFair = MAX_UINT32 - (MAX_UINT32 % range); let cursor = GRID_SIZE - 1 - i; while (true) { const message = `${clientSeed}:${nonce}:${cursor}`; const hmac = crypto.createHmac('sha256', key).update(message).digest('hex'); let found = false; for (let off = 0; off + 8 <= hmac.length; off += 8) { const value = parseInt(hmac.substring(off, off + 8), 16); if (value < maxFair) { const j = value % range; [positions[i], positions[j]] = [positions[j], positions[i]]; found = true; break; } } if (found) break; cursor++; } } return positions.slice(0, DRAWN_COUNT).map(p => p + 1).sort((a, b) => a - b);} 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; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Drawn numbers:', computeDraw(serverSeed, clientSeed, nonce));6.10Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac GRID_SIZE = 40DRAWN_COUNT = 10MAX_UINT32 = 0xFFFFFFFF def compute_draw(server_seed, client_seed, nonce): key = bytes.fromhex(server_seed) positions = list(range(GRID_SIZE)) for i in range(GRID_SIZE - 1, 0, -1): range_val = i + 1 max_fair = MAX_UINT32 - (MAX_UINT32 % range_val) cursor = GRID_SIZE - 1 - i while True: message = f'{client_seed}:{nonce}:{cursor}'.encode() h = hmac.new(key, message, hashlib.sha256).hexdigest() found = False for off in range(0, len(h) - 7, 8): value = int(h[off:off+8], 16) if value < max_fair: j = value % range_val positions[i], positions[j] = positions[j], positions[i] found = True break if found: break cursor += 1 return sorted([p + 1 for p in positions[:DRAWN_COUNT]]) 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 = 0 print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Drawn numbers:', compute_draw(server_seed, client_seed, nonce))6.11Evidence Screenshots3 records
| Evidence | Description |
|---|---|
| E02 | Fairness page overview — "What is Provably Fair?" and "How it works" sections |
| E03 | Fairness verification tool — Keno selected, showing game-specific verification inputs |
| E11 | 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.
Repository Details
- GitHub: ProvablyFair-org/duel-keno
- Commit: 4fc06ec9b0a29f7e664732fef7295607a58b57c5
- Game: Keno (duel-keno)
- 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-keno
- Commit: 4fc06ec9b0a29f7e664732fef7295607a58b57c5
- Game: Keno (duel-keno)
- Public Verifier: audit.provablyfair.org/casino/duel/tools/verify-bets
- Node.js 18+
- npm 8+
- Git
- TypeScript (installed via npm)
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 5,100 bets.
Commands to Reproduce
Installs TypeScript, ts-node, and cryptographic dependencies. npm test runs mocha (20 unit tests), then the 40M-round simulation, then the 20-step verification pipeline.
Simulates 1,000,000 rounds for each of the 40 configurations (40M total). Runtime ~29 minutes. Output written to outputs/simulation-results.json and outputs/rtp-convergence.html.
Audit Reproducibility Pinning
- Git Commit
- 4fc06ec9b0a29f7e664732fef7295607a58b57c5
- Node Version
- v18+ (tested on v22.x)
- Dataset
- data/keno-master-5100bets.json (5,100 bets, 102 active seeds + 4 boundary)
- Dataset Hash (SHA-256)
- 841755fdfb268d77…c5e6bcf9
- Audit Date
- April 2026
- Audit ID
- PF-2026-DL03
Output Artifacts5 files generated
| File | Contents | Size |
|---|---|---|
| verification-results.json | Steps 1–20 results, per-step pass/fail with evidence | ~12KB |
| simulation-results.json | 40M-round RTP, chi-squared, cherry-pick detection (Pass 1 + Pass 2) | ~900KB |
| determinism-log.json | Per-bet draw verification (5,100 entries, 0 mismatches) | ~200B |
| 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-keno/ ├── src/ │ ├── rng.ts → HMAC-SHA256 backward Fisher-Yates (40-ball, rejection sampling) │ ├── config.ts → KenoConfig multiplier lookup + RTP │ ├── simulate.ts → Monte Carlo — 1M rounds/config × 40 │ ├── stats.ts → Chi-squared, autocorrelation, runs test │ ├── loader.ts → Dataset + config SHA-256 hash guards │ └── types.ts → Type definitions ├── tests/ │ ├── verify.ts → 20-step verification pipeline │ ├── steps/ │ │ ├── commitment.ts → Steps 1–4: Commit-reveal integrity + nonce audit │ │ ├── determinism.ts → Steps 5–6: Draw recomputation + client seed │ │ ├── payouts.ts → Steps 7–12: Payout math + multiplier + Phase D │ │ ├── dataset.ts → Steps 13–18: Dataset integrity + anti-circularity │ │ ├── simulation.ts → Steps 19–20: Simulation integrity + cherry-pick │ │ ├── statistical.ts → Informational: RTP, serial, chi-squared │ │ └── context.ts → Shared context + pass/fail helpers │ └── keno/ │ └── KenoTests.ts → 20 unit tests (Mocha) ├── data/ │ └── keno-master-5100bets.json → 5,100 live bets (4 phases, 102 epochs) ├── outputs/ → Generated by npm test │ ├── verification-results.json → Steps 1–20 pass/fail + dataset & config hashes │ ├── simulation-results.json → 40M rounds, per-config RTP + cherry-pick │ ├── determinism-log.json → Per-bet draw recomputation log │ ├── chi-squared-results.json → Distribution test results │ └── rtp-convergence.html → Interactive RTP convergence chart ├── evidence/ │ ├── E01–E11 *.png → Game UI, fairness page, phase captures, seed rotation │ └── client-seed-origin.png → Client seed origin evidence ├── capture/ │ └── capture-auto.reference.js → Browser bet capture script (reference) ├── results/ → Merged capture working directory ├── kenoConfig.json → Duel.com base-tier game config (40 configs, SHA-256 pinned) ├── 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 | Draw 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 Bet-Size Invariance (Phase D at $10) | S3, S4 |
| 10 | Configuration Completeness | S3 |
| 11 | Zero-Edge Audit (scaling_edge absent) | S4 |
| 12 | Consolation Payout Disclosure | S3, S4 |
| 13 | Phase Labels | S3 |
| 14 | Dataset Hash | S7 |
| 15 | Epoch Size | S1, S3 |
| 16 | Hit Symmetry (hypergeometric mode verification) | S4 |
| 17 | Probability Independence (Anti-Circularity) | S4 |
| 18 | Phase D — Client Seed Variation | S1, S2 |
| 19 | Simulation Results — Pass 1 Integrity | S2, S4 |
| 20 | Simulation Results — 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 5,100 bets.