
Limbo
Independent verification, backed by open-source code.
- Live Bets Verified
- 7,700
- Parity Rate
- 100%
- Simulated RoundsSimulated RTP 99.911%
- 13M
- Simulated RTP 99.911%
- Proven RTP · base tier
- 99.9%
- Every target · base stake tier
Verification pipeline
Real bets. Independently reproduced.
Placed anonymously at Duel.com. We rebuilt the game logic from scratch and matched all 7,700 recorded outcomes.
The casino result, reproduced.
- Casino outcome
- Result 2.37703× · Target 79.13× · Loss
- Our reproduction
- Result 2.37703× · Loss
- Recorded payout
- $0.0000
- Commit-Reveal IntegrityPass
- Forward CommitmentPass
- Seed Coverage & EpochsPass
- Dataset VerifiabilityPass
- Result DeterminismPass
- Negative ControlPass
- Nonce ContinuityPass
- Payout MathPass
- House Edge MeasuredPass
- Win ConditionPass
- Operator-Reported EdgePass
- Reported Win ChancePass
- Dataset IntegrityPass
- Population & PhasesPass
- Target CoveragePass
- Bet-Size InvariancePass
- Client Seed VariationPass
- Simulation Pass 1Pass
- Cherry-Pick Test (Pass 2)Pass
- Scaling EdgePass
- Artifact IntegrityPass
- Client Seed InfluencePass
- Draw Domain & Bias BoundPass
- Result Range & Win RulePass
- Win Probability per TargetPass
- Edge on Every TargetPass
- Target Bounds & Max PayoutPass
- Phase A Decade CoveragePass
- Wallet EnvelopesPass
- Live-Sample FitPass
The repo is the credential. Every finding ships as code. At release v1.2.1, npm test runs 43 unit tests and all 30 scored steps against the 7,700 captured bets, then compares the fresh results with the committed ones. It prints 30 PASS, 0 flags, 0 hard fails, REPRODUCED, and exits 0.
PROVABLY FAIR — Full Pass. We captured 7,700 real Limbo bets on Duel.com on 31 August 2026 and recomputed every result and payout with our own implementation, written from the specification. All 30 steps pass, with no flags and no hard fails. Certified under ProvablyFair.org Framework 1.1 at release v1.2.1, for every target from 1.01× to 100,000× at the base stake tier.
Audit Verdict
| Check | Result | Evidence |
|---|---|---|
| Overall Status | Pass | PROVABLY FAIR — Full Pass. 30 of 30 steps pass, 0 flags, 0 hard fails (Framework 1.1, release v1.2.1). Covers every target from 1.01× to 100,000× at the base stake tier. |
| Proven RTP | Pass | 99.9% at the base stake tier: the winning draws for each of the 9,999,900 legal targets, 1.01× to 100,000×, are counted exactly over all 2³² outcomes, giving 99.897673% to 99.900000% (Step 26). RTP claim (RTP-CLAIM 1.2.0): Duel's game settings state a 0.1% floor edge; 0 of 9,999,900 targets beyond the ±0.1 pp band: PASS (Step 26). Simulated: 99.911% over 13M rounds |
| Live ↔︎ Verifier Parity | Pass | 7,700 / 7,700 results reproduced (Step 5). Duel's own Verify-tab code, run unmodified as a cross-check, reproduces all 7,700 (Step 5) |
| Commit-Reveal System | Pass | 157 / 157 revealed server seeds open a commitment published earlier, and all 154 betting epochs are revealed; 158 promotions and 157 forward links are consistent (Steps 1–3). That each commitment was shown before its bets rests on the capture's records |
| Nonce Audit | Pass | 154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7) |
| Client Seed | Pass | A genuine HMAC input: under a wrong client seed 0 of 7,700 results stay the same, against a chance bound of 1.255, and an empty seed gives 0 collisions (Step 22). The capture set its own client seed for all 154 epochs, including 10 custom seeds in Phase D (Step 17) |
| RNG Analysis | Pass | Our own HMAC-SHA256 implementation, written from the specification, reproduces every result; the draw is a 32-bit value used directly, with no modulo or rejection, so its bias is exactly 0 (Steps 5, 23) |
| Payout Logic | Pass | 2,965 wins paid exactly stake × target and 4,735 losses exactly 0, compared as integers on the served grid; every served win chance equals (1 − 0.001) / target to within 10⁻¹⁸ (Steps 8, 25) |
| Scaling House Edge | Info | Every captured bet was priced at the 0.1% floor (Step 20). Duel's config allows up to 1.0%; on our reading the edge rises once a bet's potential profit passes 25,000 and reaches 1.0% at 250,000. Explained from the config, not certified |
| Anti-Circularity | Pass | The win probability comes from an exact count of winning draws over all 2³² outcomes for each of the 4,094 served targets, not the casino's win_chance field (Step 25). The edge measured from the win/lose boundary is 0.0957%, and Duel's 0.1% sits inside the bracket (Steps 9 and 11) |
| Integrity Checks | Pass | 10 fairness integrity tests — 9 pass, 1 N/A (single-step game) |
| Determinism | Pass | Every result recomputes from (serverSeed, clientSeed, nonce); the target and stake are not inputs to the draw (Step 5) |
| Reproducibility | Pass | npm test at v1.2.1: 43 unit tests and 30 / 30 steps, REPRODUCED, exit 0. Runs write to outputs/run/ and are compared with the committed results |
Overall StatusPass
v1.2.1). Covers every target from 1.01× to 100,000× at the base stake tier.Proven RTPPass
Live ↔︎ Verifier ParityPass
Commit-Reveal SystemPass
Nonce AuditPass
Client SeedPass
RNG AnalysisPass
Payout LogicPass
Scaling House EdgeInfo
Anti-CircularityPass
win_chance field (Step 25). The edge measured from the win/lose boundary is 0.0957%, and Duel's 0.1% sits inside the bracket (Steps 9 and 11)Integrity ChecksPass
DeterminismPass
ReproducibilityPass
npm test at v1.2.1: 43 unit tests and 30 / 30 steps, REPRODUCED, exit 0. Runs write to outputs/run/ and are compared with the committed resultsWhat Was Audited
✓ Audited
- Every result is set by the server seed, client seed and nonce, and recomputes exactly (7,700 / 7,700)
- Your target and stake are not inputs to the result
- Revealed server seeds match their commitments (157 / 157), and the 158 seed records fit together
- Your client seed is a genuine input: changing it changes the result
- Every payout is exact: stake × target on a win, 0 on a loss
- Theoretical RTP is 99.9% at every target from 1.01× to 100,000× (99.8977%–99.9000% by exact count), matching the 0.1% floor edge Duel's game settings state
- The price is the same at $0.0125 and $10 at target 1.01×
- Players can check every bet
What Audit Excludes
- Scaling house edge above the base stake tier (up to 1.0%) — explained from the config in S4.5
- Max win
- Rakeback, Zero Edge and promotions — 99.9% is the proven RTP; these are operator-side
- Cross-account sampling
- Duel's server code and seed generation — outcomes are verified from the captured bets and their commitments
- Infrastructure or server security
- Wallet, payments, or operational systems outside game logic
What Audit Covers
| Area | Description |
|---|---|
| Commit-Reveal System | SHA-256 server seed hashing, the forward commitment chain, reveal on rotation |
| Client Seed Origin | Player-set client seeds honoured (all 154 set by the capture, including 10 custom seeds); the capture's own seeds disclosed |
| Seed Handling | Client seed control, nonce lifecycle, seed pair rotation |
| RNG Analysis | HMAC-SHA256 with the first 32 bits used directly, no modulo or rejection, written from the specification; bias bound 0 |
| Payout Logic | Win condition result ≥ target / (1 − e), exact payouts, win chance formula, stake invariance (Phase C) |
| Live Parity | Our recomputation and Duel's Verify-tab code (cross-check) against every live bet |
| RTP Validation | Anti-circularity (exact count over 2³² draws), RTP claim rule on all 9,999,900 targets, 13M-round simulation, cherry-pick test (Pass 2) |
| Fairness Integrity | Integrity matrix — 10 tests across nonce, seed commitment, determinism, and isolation |
| Game Profile Coverage | 30 of 31 Limbo profile checks mapped to a step or a package check; the ProvablyFair.org verifier run has none |
What Audit Guarantees
- Outcomes are deterministic and reproducible from the recorded inputs
- Live results match our recomputation for every captured bet (7,700 / 7,700)
- At the base stake tier, theoretical RTP is 99.9% at every target from 1.01× to 100,000×, from an exact count of winning draws
- Duel's stated edge holds under the RTP claim rule: 0 of 9,999,900 targets beyond ±0.1 pp
- The client seed is a genuine input: under a wrong seed, 0 of 7,700 results stay the same
- Your target and stake are not inputs to the result
References
Limbo — Game Rules7 sections▶
Limbo is a multiplier game: pick a target multiplier and win if the result reaches it. Higher targets pay more and win less often.
Game Rules
Limbo is a single-outcome betting game. The player chooses a target multiplier T from 1.01× to 100,000×. One HMAC-SHA256 computation produces a result multiplier of at least 1.00×; the bet wins if the result reaches the target's threshold, T / (1 − e), and pays stake × T. The higher the target, the lower the win chance and the higher the payout. This audit covers every target at the base stake tier, where e = 0.1%.
How to Play
1. Choose a target — Enter a target multiplier from 1.01× to 100,000×, in steps of 0.01. The game shows the matching win chance.
2. Enter bet amount — Choose how much to wager.
3. Place the bet — The platform generates the result from the committed server seed, your client seed and the current nonce.
4. Outcome — If the result reaches the threshold for your target, you win stake × target; otherwise the stake is lost.
5. Rotate to verify — Rotate your seed pair to reveal the server seed, then check any past bet.
Win Conditions
The win condition in Limbo is a single threshold comparison.
| Outcome | Condition | Example |
|---|---|---|
| Win | result ≥ T / (1 − e) | target 91.95×, result 92.058888 → threshold 92.042042 → Win, pays 91.95× |
| Loss | result < T / (1 − e) | target 3.60×, result 2.221402 → threshold 3.603604 → Loss |
Risk vs Reward
The core mechanic of Limbo is the trade-off between target and win chance.
- Target controls probability — the win chance is 0.999 / T: 49.95% at 2×, 9.99% at 10×, 0.000999% at 100,000×
- RTP is the same at the base tier — every target returns 99.9% there, at the claim's precision (0.1% house edge)
- Variance grows with the target — a 100,000× bet wins about once in 100,100 bets; a 1.5× bet wins two times in three
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Target | 1.01×–998.37× in the capture | Duel's config allows up to 100,000× (max_target); 2-decimal steps |
| House Edge | 0.1% at the base tier | Rises with potential profit, to 1.0% at most (4.5) |
| Theoretical RTP | 99.9% (base tier) | Counted exactly for every target 1.01×–100,000× |
| RNG Algorithm | HMAC-SHA256 | One HMAC call per bet; key = hex-decoded server seed |
| Draw | First 8 hex characters → 32-bit integer v | Used directly: no modulo, no rejection |
| Result | ⌊2³² · 10⁶ / (v + 1)⌋ / 10⁶ | 6 decimals, at least 1.000000 |
Seed Formats
Every Limbo bet uses three cryptographic inputs to generate the result.
| Seed Type | Format | Example | Purpose |
|---|---|---|---|
| Server Seed | 64-char hex (32 bytes) | 4dcc44b245c86cdd… | Casino-provided randomness |
| Client Seed | Alphanumeric string | mTJorxp6VuvAo0ep | Player-contributed entropy |
| Nonce | Integer from 0 | 4 | One per bet within an epoch |
Win Chance & Threshold Formulas
The win chance and the win threshold are fixed functions of the target. The 1 − e factor is the source of the 0.1% house edge.
win_chance = (1 − e) / T
threshold = T / (1 − e)
payout = stake × T (e = 0.001 at the base tier)| Target | Win Chance | Threshold | RTP |
|---|---|---|---|
| 1.01× | 98.9109% | 1.011011 | 99.9% |
| 2× | 49.9500% | 2.002002 | 99.9% |
| 10× | 9.9900% | 10.010010 | 99.9% |
| 100× | 0.9990% | 100.100100 | 99.9% |
| 1,000× | 0.0999% | 1,001.001001 | 99.9% |
| 100,000× | 0.000999% | 100,100.100100 | 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 Overview7 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 — self-contained HMAC-SHA256 |
| No mixed entropy sources | No timestamps, Math.random, etc. |
| Unbiased mapping | The 32-bit draw is used directly: no modulo, no rejection, bias exactly 0 |
| No state leakage | Each bet uses unique HMAC message (clientSeed:nonce) |
3. Verifier ↔︎ Live Parity
| Test | Description |
|---|---|
| Live outcomes match verifier | 7,700 / 7,700 results recomputed with 0 mismatches |
| Multi-phase verification | Phases A (targets 1.01×–1,000×), B (deep sample at 1.01×), C (bet-size check at 1.01×), D (client seed) |
| Bet-size invariance | $10 bets carry the same win chance as $0.0125 bets at target 1.01× |
4. Game Logic & RTP Validation
| Test | Description |
|---|---|
| Anti-circularity (Steps 25, 26) | Winning draws counted over all 2³² outcomes; the casino's win_chance compared, never used |
| House edge audit | Measured from the win/lose boundary: 0.0957%, with Duel's 0.1% inside the bracket; higher tiers explained from the config (4.5) |
| Payout rules correctness | Win amount equals stake × target exactly (no tolerance) |
| Simulated RTP convergence | 13M rounds: 99.911% across 13 targets, consistent with 99.9% |
| Cherry-pick detection | Rank-sum Z = 0.453 across 154 casino seeds — no sign of seed pre-selection |
| RTP claim rule (RTP-CLAIM 1.2.0) | Duel's stated 0.1% base-tier edge held at all 9,999,900 targets with a ±0.1 pp band — PASS |
5. Fairness Integrity & Player Verification
| Test | Description |
|---|---|
| Player can reproduce results offline | Using seeds + nonce |
| Verifier logic matches live logic | Our RNG (written from the spec) and Duel's Verify-tab code agree on all 7,700 bets |
| Verifier publicly accessible | The ProvablyFair.org verifier, Duel's verify page and the open-source scripts in S6 |
| No reliance on private APIs | Fully client-side verification |
| 10 fairness integrity tests | Nonce, seed commitment, determinism, isolation |
High-Level Flow
To get an overview of how the process works, here is a high-level breakdown:
1. Player Bets — Chooses a target (1.01×–100,000×) and bet amount
2. Seeds Combined — HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce)
3. RNG Output — the first 8 hex characters give a 32-bit integer v
4. Result Determination — result = ⌊2³²·10⁶ / (v + 1)⌋ / 10⁶, at least 1.000000
5. Win Check — win if result ≥ target / (1 − e); win chance = (1 − e) / target
6. Payout Result — win_amount = bet_amount × target
Provably Fair Model
Provably fair gambling systems use cryptographic primitives so outcomes can be checked after the fact. 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 and sends the player only the SHA-256 hash of the hex-decoded seed. The player can't know the seed beforehand, but can check it once it is revealed.
Bet Phase:
The player places their bet. The server combines the server seed with the player's client seed via HMAC-SHA256 to compute the Limbo result. Each epoch uses the same server seed and client seed pair.
Reveal Phase:
When the player rotates the seed pair, the server reveals the plaintext server seed. The player can now check SHA-256(hexDecode(serverSeed)) against the hash saved before betting.
Verify Phase:
Anyone can recompute every bet's result from the revealed server seed, client seed, and nonce using the published HMAC-SHA256 algorithm.
Player-Controlled Client Seed
The player's client seed is submitted to the server through the seed rotation UI, and players can set their own 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 you rotate the seed pair
Seed Epoch: (serverSeed, clientSeed) Bet 1: nonce = 0 → Result ABet 2: nonce = 1 → Result BBet 3: nonce = 2 → Result C ...[Player rotates seed — epoch complete]Next bet: nonce = 0 → Result X (new seed pair)Determinism Guarantee
Given identical inputs, the output is always identical:
key = hex_decode(serverSeed)message = clientSeed + ":" + noncehash = HMAC-SHA256(key, message)v = uint32(first 8 hex chars of hash)result = floor(2^32 · 10^6 / (v + 1)) / 10^6win = (result ≥ target / (1 − e))Technical Glossary8 categories▶
Framework 1.1
| Term | Definition |
|---|---|
| Framework 1.1 | The ProvablyFair.org audit framework this report is certified under (October 2026). It adds the RTP claim rule, capture flags, the base-stake-tier rule, the standard S5 matrix and game profile coverage. |
| Capture flag | A flag caused by our capture, not by the casino. Disclosed and counted, and verdict-neutral. This audit has none. |
| RTP claim rule | RTP-CLAIM 1.2.0. Holds the audited RTP to the figure the casino publishes (game page, fairness page, help page, bet panel, rules or game settings) within ±0.1 pp. A house-side miss makes the verdict RTP FIGURE OVERSTATED (exit 5). |
| Base stake tier | The 0.1% floor of Duel's scaling edge; on our reading of the config it applies until a bet's potential profit passes 25,000. Every captured bet was priced here, and the audit's guarantees apply here. |
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 Limbo result. |
| 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 value controlled by the player. Players can change it at any time via the seed rotation UI. |
| Nonce | A sequential counter from 0 that rises by 1 per bet within an epoch. Resets to 0 when you rotate the seed pair. |
| Hashed Server Seed | SHA-256(hexDecode(serverSeed)) — the commitment hash shown before betting. After rotation, players verify the revealed seed produces this hash. |
| Epoch | The bets placed under one (serverSeed, clientSeed) pair, until the player rotates it. Our capture rotated every 50 bets. |
Cryptographic Functions
| Term | Definition |
|---|---|
| HMAC-SHA256 | Hash-based Message Authentication Code using SHA-256. Duel Limbo uses HMAC-SHA256 with the hex-decoded server seed as key and clientSeed:nonce as message. One call per bet. |
| 32-bit draw | The first 8 hex characters of the HMAC output, read as an integer from 0 to 2³² − 1. Limbo uses it directly, with no modulo or rejection, so every value is equally likely. |
| SHA-256 | Secure Hash Algorithm 256-bit. Used for server seed commitment: SHA-256(hexDecode(serverSeed)) = serverSeedHashed. |
| Hex Decoding | Converting a 64-character hex string to 32 raw bytes. Critical for the HMAC key — using the hex string as UTF-8 produces wrong outputs. |
Verification Terms
| Term | Definition |
|---|---|
| Verifier | A tool that recomputes the result from the seeds and nonce. Duel's Verify-tab code, run unmodified, reproduces all 7,700 bets (Step 5). |
| Parity | Degree of matching between verifier and live game results. 100% parity = every result matches. This audit: 7,700/7,700 exact match. |
| Anti-Circularity | Proof that RTP derives from first principles, not the casino's own figures. For Limbo: the winning draws are counted over all 2³² outcomes for every served target, and the edge is measured from the win/lose boundary (Steps 9, 25). |
| Cherry-Pick Detection | A test for selective seed use. Pass 2 replays all 154 revealed seeds for 10,000 nonces and ranks the captured window among 200 windows per seed. |
Game Mechanics
| Term | Definition |
|---|---|
| Target | Player-selected multiplier: 1.01× to 100,000× in Duel's config (2-decimal steps), 1.01× to 998.37× in the capture. The bet wins if the result reaches target / (1 − e). |
| Result | The output multiplier, ⌊2³²·10⁶ / (v + 1)⌋ / 10⁶: 6 decimals, at least 1.000000. |
| Win Chance | (1 − e) / target, so 0.999 / target at the base tier. Served on every bet and checked to 10⁻¹⁸. |
| Threshold | target / (1 − e): the result a bet must reach to win. At 2× it is 2.002002. |
| House edge (e) | 0.001 (0.1%) at the base tier. The 1 − e factor in the threshold is where the edge comes from. |
Audit Terms
| Term | Definition |
|---|---|
| Seed Pair | A (serverSeed, clientSeed) combination used for a sequence of bets. Our capture used each pair for 50 bets. |
| Phase | A structured data collection segment. Phase A: targets spread over 1.01×–1,000× at $0.0125. Phase B: 1.01× for edge precision. Phase C: $10 bet-size check. Phase D: 10 custom client seeds. |
| Scored Step | One of the 30 checks behind the verdict: PASS, FLAG or FAIL. Here all 30 pass. |
| Informational Context | Statistical observations reported for transparency: the live-sample fit is Step 30, which can only read PASS or INFO, and the realised return is not scored. Both are underpowered at 7,700 bets. |
Data Formats
| Term | Definition |
|---|---|
| limbo-master.json | The 7,700-bet dataset used throughout this audit: all bets, 158 seed records and 163 sampled wallet envelopes across four phases. SHA-256: b2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc. |
| verification-results.json | Output of the 30-step verification suite: every step result, profile coverage and the verdict. |
| simulation-results.json | Pass 1 on pinned seeds: the 2M-draw census and 13 targets × 1M rounds. Pass 2 (154 casino seeds × 10,000 nonces) runs in verify. |
| rtp-claim-table.json | Every target from 1.01× to 100,000× scored against the 99.9% claim (RTP-CLAIM 1.2.0). |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Limbo bet on Duel.com comes from three inputs: server seed, client seed and nonce. Duel commits to the server seed by showing its SHA-256 hash before you bet, and reveals the seed when you rotate. Your target and stake are not inputs, so the result is fixed before the animation plays. A hash match shows the records are consistent; the proof of timing is a copy of the hash you saved before betting.
What we verified
- Every revealed server seed opens a commitment published earlier (157 / 157), and all 154 betting epochs are revealed
- The 158 seed records fit together: 158 promotions and 157 forward links, plus the pre-capture anchor
- That each commitment was shown before its bets rests on the capture's records
- Nonces: 154 / 154 epochs carry exactly nonces 0–49, with no gaps or repeats
- Identical inputs always produce the same result (7,700 / 7,700)
- Your client seed is a genuine input: under a wrong seed, 0 of 7,700 results stay the same
What this means for you
- Once a server seed is committed, the casino can't swap it for another
- The result is fixed by the seeds and nonce before the animation plays
- Save the hashed server seed before you bet: your saved copy is what proves the timing
- Setting your own client seed adds your input to every result
- Any bet can be recomputed after you rotate your seed
- Every revealed server seed opens a commitment published earlier (157 / 157), and all 154 betting epochs are revealed
- The 158 seed records fit together: 158 promotions and 157 forward links, plus the pre-capture anchor
- That each commitment was shown before its bets rests on the capture's records
- Nonces: 154 / 154 epochs carry exactly nonces 0–49, with no gaps or repeats
- Identical inputs always produce the same result (7,700 / 7,700)
- Your client seed is a genuine input: under a wrong seed, 0 of 7,700 results stay the same
- Once a server seed is committed, the casino can't swap it for another
- The result is fixed by the seeds and nonce before the animation plays
- Save the hashed server seed before you bet: your saved copy is what proves the timing
- Setting your own client seed adds your input to every result
- Any bet can be recomputed after you rotate your seed
Seed hash integrityPass
Commitment linkagePass
Hash consistency within epochPass
Nonce sequencingPass
Deterministic outputPass
Client seed participationPass
Client seed originPass
All 157 revealed seeds open commitments published earlier, every betting epoch is revealed and the 158 seed records fit together. Every result recomputes from (serverSeed, clientSeed, nonce), with no gap in any nonce sequence. When each commitment was shown rests on the capture's records.
| Check | Result | Evidence |
|---|---|---|
| Seed hash integrity | Pass | 157 / 157 revealed seeds open a commitment published at an earlier record; 154 / 154 betting epochs revealed; the 4 commitments never opened carry no bets (Step 1) |
| Commitment linkage | Pass | 158 / 158 promotions and 157 / 157 forward links recomputed, plus the pre-capture anchor (Step 2). A consistency check: the timing rests on the capture's records |
| Hash consistency within epoch | Pass | Every bet sits in the rotation window of the record whose commitment and client seed it carries: 7,700 / 7,700, in 154 epochs of 50 (Step 3) |
| Nonce sequencing | Pass | 154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7) |
| Deterministic output | Pass | 7,700 / 7,700 results recomputed from (serverSeed, clientSeed, nonce) as exact 6-decimal values (Step 5) |
| Client seed participation | Pass | Under a wrong client seed 0 of 7,700 results stay the same (bound 1.255); an empty seed gives 0 collisions (Step 22). The 500 Phase D bets recompute under 10 custom seeds (Steps 5 and 17) |
| Client seed origin | Pass | The client seed is sent from the player's side at each rotation and carried back by Duel; the capture set all 154, including 10 custom seeds in Phase D (E05). Its seed generators are disclosed in 1.4 |
Seed hash integrityPass
Commitment linkagePass
Hash consistency within epochPass
Nonce sequencingPass
Deterministic outputPass
Client seed participationPass
Client seed originPass
All 157 revealed seeds open commitments published earlier, every betting epoch is revealed and the 158 seed records fit together. Every result recomputes from (serverSeed, clientSeed, nonce), with no gap in any nonce sequence. When each commitment was shown rests on the capture's records.
How It Works — Seed, Nonce & Determinism7 sections
1.1Server Seed Commitment
Before any bet, Duel commits to a secret server seed by showing its SHA-256 hash. The server seed is a 64-character hex string (32 bytes), and the commitment is SHA-256(hexDecode(serverSeed)): the hash of the 32 raw bytes, not of the 64 characters. Hashing the text form opens none of the 154 betting commitments. When you rotate, Duel reveals the seed, and anyone can check that it hashes to the commitment. Step 1 also checks the order: each revealed seed must open a commitment published at an earlier seed record. A match shows the two are consistent. The proof that the seed was fixed before you bet comes from a copy of the hash you saved before betting.
export function verifyHash(serverSeed: string, serverSeedHashed: string): boolean { if (typeof serverSeedHashed !== "string" || !/^[0-9a-fA-F]{64}$/.test(serverSeedHashed)) return false; let key: Buffer; try { key = decodeServerSeed(serverSeed); } catch { return false; } const commitment = createHash("sha256").update(key).digest("hex"); return commitment === serverSeedHashed.toLowerCase();}Real Example from Live Data:
// data/limbo-master.json// bets[0] — the seed fields bet 4077531 carries (nonce 0):"server_seed_hashed": "6da4c8020d776be303c9e727017b8023e5a6c27a58b43174a2556eaf27197eed","client_seed": "6MAdd35Ffi5AfC7A","nonce": 0,// seeds[1] — the next rotation reveals the server seed:"serverSeed": "7a3b7d8e935810ccff93a711a217fb041231e15cc4d9d84ad83dde6982c23205","revealedFrom": {"transactionId": 1197235085}Verification:
const crypto = require('crypto'); const serverSeed = "7a3b7d8e935810ccff93a711a217fb041231e15cc4d9d84ad83dde6982c23205"; const hashedServerSeed = crypto .createHash("sha256") .update(Buffer.from(serverSeed, 'hex')) .digest("hex"); console.log(hashedServerSeed);// Output: 6da4c8020d776be303c9e727017b8023e5a6c27a58b43174a2556eaf27197eed ✅1.2Commitment Linkage (Next-Seed Promotion)
At every rotation Duel shows the hash of the seed it will activate next (next_server_seed_hash). Step 2 recomputes all 158 promotions from the raw operator fields: the next-seed hash outstanding before a rotation must become the active hash, and that must be the record's own commitment. It then walks the chain: each record's nextServerSeedHash must equal the next record's serverSeedHashed, 157 links in all. Record 0 is the rotation made when the capture opened. The next-seed hash the capture read just before it, 6da4c8020d77…, is the commitment epoch 0 settles against: the pre-capture anchor. The links show the 158 records fit one order, not when each hash was shown. That rests on the capture's records, each made before its epoch's first bet (Step 3).
const promotions = seeds.filter(s => s.nextSeedPromotion);const promoOk = promotions.filter( s => s.nextSeedPromotion!.previousNextHash === s.nextSeedPromotion!.newActiveHash && s.nextSeedPromotion!.newActiveHash === s.seed.serverSeedHashed,).length;let chainOk = 0, chainChecked = 0;for (let i = 0; i < seeds.length - 1; i++) { chainChecked++; if (seeds[i].seed.nextServerSeedHash === seeds[i + 1].seed.serverSeedHashed) chainOk++;}1.3Hash Consistency Within Epoch
The capture holds 158 seed records, one for every rotation. 154 carry bets, 50 each (Step 14). The other 4 carry none. At each of the three phase changes the capture rotated twice in a row, leaving one empty epoch (records 100, 141 and 146) whose seed is never revealed, and the final record opens a fresh epoch after the last bet.
Step 3 doesn't group bets by the hash under test, which would pass by construction. It places each bet in the rotation window of the record active when it was placed, from that record's time to the next one's, and requires the bet to carry that record's commitment and client seed. A mid-epoch seed change would put a bet under the wrong record. Every betting epoch must also hold exactly 50 bets and be revealed.
const windows = seeds.map((s, i) => ({ s, from: Date.parse(s.at), to: i + 1 < seeds.length ? Date.parse(seeds[i + 1].at) : Infinity }));let bound = 0, wrongHash = 0, wrongClient = 0, noWindow = 0;const w3: string[] = [];for (const b of bets) { const t = Date.parse(b.at); const w = windows.find(x => t >= x.from && t < x.to); if (!w) { noWindow++; continue; } if (w.s.seed.serverSeedHashed !== b.response.server_seed_hashed) { wrongHash++; if (w3.length < 2) w3.push(`bet ${b.response.id} hash ≠ window's`); continue; } if (w.s.seed.clientSeed !== b.response.client_seed) { wrongClient++; if (w3.length < 2) w3.push(`bet ${b.response.id} client seed ≠ window's`); continue; } bound++;}1.4Client Seed Origin & Control
The client seed is yours. At every rotation it is sent from the player's side with the rotate request, Duel's reply carries it back, and every bet in the epoch carries it (Step 3). You can replace it with your own at any time. The client seed is in every HMAC message, so changing it changes the result.
The capture used its own seeds (E05). Phases A–C took 144 seeds from the capture script's Math.random(): 16 characters from A–Z, a–z and 0–9. Phase D used 10 auditor seeds such as pfauditmthn4ofx, built from a timestamp. Each was set at a rotation after the capture had recorded that epoch's server-seed commitment as the outstanding next-seed hash. These are choices on our side; they don't change what Duel's server does with a seed.
6MAdd35Ffi5AfC7ApaYStrspwMufQlJWS06wBI051NtT24Q9pfauditmthn4ofxpfauditmthn5uvp
1.5Nonce Incrementation
The nonce starts at 0 for each server seed and rises by 1 per bet. Duel reveals a server seed only when you rotate. Our capture rotated every 50 bets; that 50-bet epoch is our schedule, not Duel's.
Step 7 checks every epoch against the plan written in code (src/capture-plan.ts: first nonce 0, 50 bets), not against the dataset: exactly nonces 0–49, with no gap, repeat or out-of-range value. The capture kept no retry log and declares no retry gaps, so a gap would count against the operator. There are none (Step 7). In capture order every epoch also runs 0 to 49 (docs/ALGORITHM.md §7).
for (const [hash, group] of ctx.byHash) { epochs++; const nonces = group.map(b => b.response.nonce).sort((a, b) => a - b); const seen = new Set(nonces); const missing: number[] = []; for (let n = NONCE_BASE; n < NONCE_BASE + EPOCH_SIZE; n++) if (!seen.has(n)) missing.push(n); const outOfRange = nonces.filter(n => n < NONCE_BASE || n >= NONCE_BASE + EPOCH_SIZE).length; const repeats = nonces.length - seen.size; if (!missing.length && !outOfRange && !repeats) { clean++; continue; } const allDeclared = !outOfRange && !repeats && missing.every(n => declared.has(`${hash}:${n}`)); if (allDeclared) captureGaps++; else operatorBad++;1.6Deterministic Mapping
The result is fully deterministic: the same server seed, client seed and nonce always give the same result. Our src/rng.ts was written from the specification (docs/ALGORITHM.md) by an implementer who never opened Duel's code. The server seed is hex-decoded to 32 bytes for the HMAC key, and the message is clientSeed:nonce. The first 4 bytes of the digest, read big-endian, give a number u from 0 to 4,294,967,295, and the result is floor(2³² × 10⁶ / (u + 1)) / 10⁶: a multiplier of at least 1.000000×, truncated to 6 decimals and computed in exact integers. Every value of u is used; nothing is skipped or reduced.
// §2 Step 1: digest = HMAC-SHA256(key = 32 decoded seed bytes, message = UTF-8(clientSeed ":" nonce)).// §2 Step 2: u = digest bytes 0..3 as an unsigned big-endian 32-bit integer. One HMAC per round, no cursor.function drawFromKey(keyBuffer: Buffer, clientSeed: string, nonce: number): number { if (typeof clientSeed !== "string") throw new Error("clientSeed must be a string"); const message = Buffer.from(`${clientSeed}:${nonceText(nonce)}`, "utf8"); const digest = createHmac("sha256", keyBuffer).update(message).digest(); return digest.readUInt32BE(0);} export function resultUnitsFromU(u: number): number { if (!Number.isInteger(u) || u < 0 || u >= MAX_U32) { throw new Error("u must be an integer in [0, 2^32)"); } const units = (MAX_U32_BIG * RESULT_SCALE_BIG) / (BigInt(u) + BigInt(1)); // BigInt division truncates = floor for positives return Number(units);}(serverSeed, clientSeed, nonce).Real Bet Verified:
// data/limbo-master.json · bet 4077534 (Phase A, target 2.69, nonce 3)"request": {"amount": "0.0125", "target": 2.69, "instant": true}"response": {"id": 4077534, "result": "7.979831", "is_win": true, "target": "2.69", "multiplier": "2.690000000000000000", "win_chance": "0.371375464684014870", "amount_won": "0.033625", "nonce": 3, "client_seed": "6MAdd35Ffi5AfC7A", "server_seed_hashed": "6da4c8020d776be303c9e727017b8023e5a6c27a58b43174a2556eaf27197eed"}// revealed server seed (seeds[1]): 7a3b7d8e935810ccff93a711a217fb041231e15cc4d9d84ad83dde6982c23205Verification:
// computeResultUnitsFromBuffer(hexDecode("7a3b7d8e935810cc..."), "6MAdd35Ffi5AfC7A", 3) // message = "6MAdd35Ffi5AfC7A:3"// digest = HMAC-SHA256(hexDecode("7a3b7d..."), message)// = 2014b4558cf0cf96a9e4733f987531cb327110ed8bc7ea7f7d7d250b7c035e2d // First 4 bytes: 0x2014b455 = 538,227,797// units = floor(2^32 × 10^6 / 538,227,798) = 7,979,831// result = 7.979831 ✅ // Threshold: 2.69 / (1 − 0.001) = 2.692692…// Win: 7.979831 ≥ 2.692692… = true ✅// Payout: 0.0125 × 2.69 = 0.033625 ✅1.7Client Seed Influence
To confirm the client seed is a genuine HMAC input, Step 22 recomputes all 7,700 bets with a deliberately wrong client seed (wrong-client-seed-test) and again with an empty one. A different seed can land on the same 6-decimal result by chance. That chance is exact for each bet: the share of the 2³² draws that give its result. Over the 7,700 bets it adds up to 0.0025 expected matches, so the bound, expectation + 5σ + 1, is 1.255. A result that ignored the client seed would match every time. Separately, the 500 Phase D bets used 10 auditor-set seeds and all recompute (Steps 5 and 17).
for (const b of bets) { if (!seedMap.has(b.response.server_seed_hashed)) continue; const k = key(b.response.server_seed_hashed); const r = computeResultUnitsFromBuffer(k, b.response.client_seed, b.response.nonce); const p = drawsWithUnits(r) / TWO32; tested++; E += p; V += p * (1 - p); if (computeResultUnitsFromBuffer(k, 'wrong-client-seed-test', b.response.nonce) === r) collisions++; eTested++; eE += p; eV += p * (1 - p); if (computeResultUnitsFromBuffer(k, '', b.response.nonce) === r) eColl++;}const bound = E + 5 * Math.sqrt(V) + 1, eBound = eE + 5 * Math.sqrt(eV) + 1;Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Commit-reveal integrity (Step 1) | 157 / 157 revealed seeds open an earlier commitment; 3 phase-change records repeat the reveal before them; 4 commitments never opened, none with bets | Pass |
| Forward commitment (Step 2) | 158 / 158 promotions, 157 / 157 forward links + the pre-capture anchor | Pass |
| Epoch binding (Step 3) | 7,700 / 7,700 bets in their record's window; 154 epochs of 50, all revealed | Pass |
| Dataset verifiability (Step 4) | 7,700 / 7,700 bets under a revealed server seed | Pass |
| Result recomputation (Step 5) | 7,700 / 7,700 | Pass |
| Nonce continuity (Step 7) | 154 / 154 epochs carry exactly nonces 0–49 | Pass |
| Client seed variation (Step 17) | 500 / 500 Phase D bets under 10 auditor seeds | Pass |
| Client seed influence (Step 22) | 0 of 7,700 unchanged (wrong seed); 0 of 7,700 (empty seed); bound 1.255 | Pass |
1.9Code References10 source files
| File | Purpose |
|---|---|
| tests/verify.ts | 30 scored steps (S1 draws on Steps 1–5, 7, 17 and 22) and the reproduction gate |
| tests/steps/commitment.ts | Steps 1–4: commit-reveal, forward commitment, epoch binding, verifiability |
| tests/steps/determinism.ts | Steps 5–7: result recomputation, Duel's code cross-check, negative control, nonce audit |
| tests/steps/dataset.ts | Steps 13–17, including Step 17: player-set client seeds |
| tests/steps/exact.ts | Steps 22–30, including Step 22: client seed influence |
| src/rng.ts | verifyHash, drawValue and resultUnitsFromU, written from docs/ALGORITHM.md |
| docs/ALGORITHM.md | The written specification the result code was built from |
| src/capture-plan.ts | The capture plan as code: 7,700 bets, 158 records, 154 epochs of 50 |
| src/loader.ts | Dataset loading, the pinned hash and seed pairing by hash |
| capture/capture-auto.reference.js | Reference record of the capture script (E05) |
1.10Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live Limbo bets on Duel.com, 31 August 2026 (17:29–19:47 UTC) |
| Total Records | 7,700 bets across 154 epochs (158 seed records) |
| SHA-256 | b2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc |
Fields used: serverSeed, serverSeedHashed, nextServerSeedHash, previousNextHash, newActiveHash, clientSeed, at, server_seed_hashed, client_seed, nonce, target, result, is_win. Bets are paired with seeds by hash; each plaintext comes from the next rotation record and is kept only if its SHA-256 matches.
1.11Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| SHA-256(hexDecode(serverSeed)) opens an earlier record's commitment for all 157 revealed seeds | Pass |
| Forward commitment chain consistent over all 157 links, plus the pre-capture anchor | Pass |
| Every bet carries the commitment and client seed of the record active when it was placed | Pass |
| Every betting epoch is revealed: 7,700 / 7,700 bets verifiable | Pass |
| One client seed per epoch; nonces exactly 0–49, no gap or repeat | Pass |
| Same inputs produce the same result for all 7,700 bets | Pass |
| A wrong or empty client seed changes every result | Pass |
| Player-set client seeds are honoured: 500 / 500 Phase D bets | Pass |
1.12Reproduction InstructionsRun locally
Clone the repository at the release tag, install, and run the verifier:
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verifyExpected output, as run at v1.2.1 (lines for this section):
[PASS] Step 1 — Commit-Reveal Integrity (SHA-256 over hex bytes)
[PASS] Step 2 — Forward Commitment (next server seed hash honoured)
[PASS] Step 3 — Seed Coverage & Epoch Binding (every epoch revealed, one commitment and one client seed per rotation window)
[PASS] Step 4 — Dataset Verifiability (100% of bets)
[PASS] Step 5 — Result Determinism (exact equality, no tolerance)
[PASS] Step 7 — Nonce Continuity (no gaps or repeats within an epoch)
[PASS] Step 17 — Client-Seed Variation (player-supplied entropy)
[PASS] Step 22 — Client-Seed Influence (model-rate gate + empty-seed control)RNG & Entropy Model
Is the randomness genuinely random, or could it be rigged?
Our RNG is our own implementation, written from the specification without opening Duel's code. It reproduces every one of the 7,700 captured results, and Duel's own Verify-tab code, run unmodified, agrees as a cross-check. Each result takes the first 32 bits of one HMAC-SHA256 value and uses them directly, with no modulo and no rejection, so every one of the 2³² draws is exactly equally likely.
What we verified
- Our RNG is written from the specification, without opening Duel's code, and reproduces all 7,700 results
- Duel's own Verify-tab code, run unmodified as a cross-check, gives the same result on all 7,700
- Only disclosed inputs: server seed, client seed and nonce. No target, stake, timestamps or outside randomness
- The 32-bit draw is used directly, with no modulo or rejection: bias exactly 0
- 2M simulated draws fit a uniform spread and show no sign of serial dependence
- Under a wrong client seed, 0 of 7,700 results stay the same
What this means for you
- Each result is generated fairly and can't be skewed toward any value
- Every one of the 2³² possible draws is equally likely
- No hidden randomness or server-side state decides the result
- Past results don't affect future results
- The result depends only on seeds you can check
- Our RNG is written from the specification, without opening Duel's code, and reproduces all 7,700 results
- Duel's own Verify-tab code, run unmodified as a cross-check, gives the same result on all 7,700
- Only disclosed inputs: server seed, client seed and nonce. No target, stake, timestamps or outside randomness
- The 32-bit draw is used directly, with no modulo or rejection: bias exactly 0
- 2M simulated draws fit a uniform spread and show no sign of serial dependence
- Under a wrong client seed, 0 of 7,700 results stay the same
- Each result is generated fairly and can't be skewed toward any value
- Every one of the 2³² possible draws is equally likely
- No hidden randomness or server-side state decides the result
- Past results don't affect future results
- The result depends only on seeds you can check
RNG derived only from disclosed inputsPass
HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce); the result takes (key, clientSeed, nonce) only (Step 5)Written from the specificationPass
src/rng.ts rebuilt from docs/ALGORITHM.md by an implementer who never opened Duel's code; 118 / 118 spec checks (unit test)Operator code cross-checkPass
Draw biasPass
Key encodingPass
Uniformity & serial independencePass
Client seed influencePass
Our own implementation, written from the specification, reproduces every live result, and Duel's Verify-tab code agrees as a cross-check. Every 32-bit draw is exactly equally likely. 2M simulated draws find no sign of bias or serial dependence, and the client seed changes the result.
| Check | Result | Evidence |
|---|---|---|
| RNG derived only from disclosed inputs | Pass | HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce); the result takes (key, clientSeed, nonce) only (Step 5) |
| Written from the specification | Pass | src/rng.ts rebuilt from docs/ALGORITHM.md by an implementer who never opened Duel's code; 118 / 118 spec checks (unit test) |
| Operator code cross-check | Pass | Duel's Verify-tab code (E10), run unmodified: same result on 7,700 / 7,700 (Step 5) |
| Draw bias | Pass | The first 8 hex characters give a 32-bit value used directly: no modulo, no rejection, bias exactly 0 (Step 23) |
| Key encoding | Pass | Server seed hex-decoded to 32 bytes; the text form gives different results on all 5 engine vectors (unit test) |
| Uniformity & serial independence | Pass | 2,000,000 simulated draws in 1,000 bins: chi² 986.69 (df 999, p 0.6031); lag-1 z −0.844 (Step 18). The live results fit P(result ≥ x) = 1/x (KS p 0.2172, Step 30) |
| Client seed influence | Pass | Under a wrong client seed 0 of 7,700 results stay the same; an empty seed stays within its chance bound (Step 22) |
RNG derived only from disclosed inputsPass
HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce); the result takes (key, clientSeed, nonce) only (Step 5)Written from the specificationPass
src/rng.ts rebuilt from docs/ALGORITHM.md by an implementer who never opened Duel's code; 118 / 118 spec checks (unit test)Operator code cross-checkPass
Draw biasPass
Key encodingPass
Uniformity & serial independencePass
Client seed influencePass
Our own implementation, written from the specification, reproduces every live result, and Duel's Verify-tab code agrees as a cross-check. Every 32-bit draw is exactly equally likely. 2M simulated draws find no sign of bias or serial dependence, and the client seed changes the result.
How It Works — RNG & Entropy Model7 sections
2.1RNG Function Implementation
Each Limbo bet takes one HMAC-SHA256: the key is the server seed hex-decoded to 32 bytes, the message is clientSeed:nonce. The first 4 bytes of the digest, its first 8 hex characters, are read big-endian as one unsigned 32-bit number u, from 0 to 4,294,967,295. The result is floor(2³² × 10⁶ / (u + 1)) / 10⁶: the multiplier 2³² / (u + 1), truncated to 6 decimals. Small values of u give large results, so high multipliers are rare: the chance of reaching x is about 1 in x. There is no modulo, no rejection and no second draw.
| Component | Detail |
|---|---|
| Hash function | HMAC-SHA256 |
| Key | hexDecode(serverSeed): 32 bytes |
| Message | clientSeed:nonce, nonce in plain decimal |
| Extraction | First 4 bytes (8 hex characters), big-endian: u from 0 to 2³² − 1 |
| Reduction | None: no modulo, no rejection |
| Result | floor(2³² × 10⁶ / (u + 1)) / 10⁶, in exact integer arithmetic |
| Output range | 1.000000× to 4,294,967,296×, served with trailing zeros removed |
/** 2^32: the number of distinct 32-bit draws (ALGORITHM.md §2 Step 2). */export const MAX_U32: number = 4294967296; /** The result is truncated to 6 decimals (ALGORITHM.md §2 Step 2: R = floor(raw x 10^6) / 10^6). */export const RESULT_DECIMALS: number = 6; /** The draw reads the first 4 digest bytes = 8 hex characters (ALGORITHM.md §2 Step 2: digest[0..3]). */export const DRAW_HEX_CHARS: number = 8; /** Raw 32-bit draw u for a round, from the server seed as published (64 hex chars). §2 Steps 1-2. */export function drawValue(serverSeed: string, clientSeed: string, nonce: number): number { return drawFromKey(decodeServerSeed(serverSeed), clientSeed, nonce);} /** §2 Steps 1-2 from already-decoded key bytes. */export function computeResultUnitsFromBuffer(keyBuffer: Buffer, clientSeed: string, nonce: number): number { return resultUnitsFromU(drawFromKey(keyBuffer, clientSeed, nonce));}docs/ALGORITHM.md by an implementer who never opened Duel's code, and its self-test runs the specification's 118 vector and property checks (118 / 118).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 input |
| Nonce | System | One per bet; rises by 1 within each epoch |
How we know: all 7,700 bets were recomputed from these inputs alone. A hidden input would make recomputation fail; it does not. The result function takes only (key, clientSeed, nonce), so your target and stake never reach it. The bet response carries no beacon or other random value: as recorded live before the capture (E03), its fields are the seed inputs, the result, the settlement, the two edge fields, the balance type and record identifiers.
No timestamps, external APIs or server-side state: only HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce).
2.3Draw Domain & Bias Bound
Many games turn a random number into a result with a modulo step, value % N, and that favours some results unless the range divides evenly. Limbo has no modulo step. The draw is the first 8 hex characters of the HMAC, 32 bits, and the result is computed from the whole value. Every one of the 2³² values is used and maps to exactly one result, so each draw has probability exactly 2⁻³². There is nothing to reject and no bias to bound: the bound is 0 by construction (Step 23).
draw u = HMAC bytes 0–3, big-endian (8 hex characters)
domain = 0 … 4,294,967,295 (2³² values, all used)
reduction = none: no modulo, no rejection
P(each u) = 1 / 4,294,967,296 = 2⁻³², exactly
result units = floor(2³² × 10⁶ / (u + 1))
lowest = 1.000000× for u ≥ 4,294,963,001 (4,295 draws, about 1 in a million)
highest = 4,294,967,296× for u = 0 (1 draw)
The map falls as u rises, so P(result ≥ x) is a whole number of draws over 2³²,
close to 1/x. S4 counts them exactly for every legal target.src/rng.ts: 32 bits over 2³², no modulo, no rejection, bias bound 0. 7,700 / 7,700 recomputed draws lie in [0, 2³²), and the result map is non-increasing at all 103 probe points.No draw gives less than 1.000000×: before truncation the multiplier 2³² / (u + 1) is never below 1, and it equals exactly 1 only at u = 2³² − 1. So the Math.max(1.0, …) floor in Duel's Verify-tab code never changes a result. 4,295 draws, about one in a million, truncate to exactly 1.000000×. Duel's code computes in doubles and ours in exact integers; compared over all 4,294,967,296 values of u, the two differ on none. The 7,700 captured draws run from 712,552 to 4,294,861,939, all inside the domain.
2.4RNG Isolation
Each bet uses its own nonce in the HMAC message (clientSeed:nonce), so every bet gets its own HMAC output. HMAC-SHA256 is a pseudorandom function: one output tells you nothing practical about another. No state is shared between bets or epochs. Your target and stake never enter the draw: they only decide what the result is compared with and what a win pays.
Evidence: each of the 7,700 served results recomputes from its own (serverSeed, clientSeed, nonce) alone (Step 5). A result that depended on earlier bets or on server state would not. Our functions in 1.6 are pure as well: no module state, no external calls, no memory between bets.
2.5Monte Carlo Simulation (13M Rounds + 2M-Draw Census)
The simulation checks the draw at scale, on pinned seeds independent of any casino data: every seed is derived by SHA-256 from one master label, so a rerun on the same runtime reproduces the file byte for byte. A census of 2,000,000 draws from one seed pair tests uniformity and serial independence. Then 13 targets from 1.1× to 100,000×, covering every decade of the legal range, each play 1,000,000 rounds on their own seed pair under the declared 0.1% edge: 13 million rounds. S4.6 has the per-target results.
| Metric | Value |
|---|---|
| Simulated RTP (13 targets, precision-weighted) | 99.911% (± 0.024 pp; method in 4.6) |
| Exact RTP at the declared edge (13 targets) | 99.898% to 99.900% |
| Targets inside the family-wise screen | 13 / 13 (largest |z| 1.56, at 100×; limit 3.403) |
| Census uniformity (1,000 bins) | chi² 986.69, df 999, p 0.6031 |
| Census bins populated | 1,000 / 1,000 (1,876 to 2,137 draws each; 2,000 expected) |
| Serial independence (lag-1) | z −0.844, inside 3.403 |
export const SIM_MASTER_SEED = 'duel-limbo-pass1-2026-09-26';export const ROUNDS_PER_TARGET = 1_000_000;/** Target hundredths: 1.10x … 100,000x (max_target) — thirteen targets spanning every decade of the legal range. */export const TARGETS_CENTS = [110, 125, 150, 200, 300, 500, 1000, 2500, 5000, 10000, 100000, 1000000, 10000000];/** Operator-declared floor edge (limboConfig.json min_edge = house_edge_percentage 0.1). */export const SIM_EDGE: { num: number; den: number } = { num: 1, den: 1000 };export const UNIFORMITY_DRAWS = 2_000_000;export const UNIFORMITY_BINS = 1000;Methodology: chi-squared on 1,000 equal bins of the 2,000,000 census draws, and the lag-1 correlation of consecutive draws. Each target's win count is compared with its exact win probability, counted over all 2³² draws. All 15 tests (13 targets, uniformity, serial) share a family-wise level of 0.01 (Bonferroni): each target and the serial check must stay inside |z| 3.403, and the uniformity p-value above 0.01 / 15. Step 18 re-derives every statistic from the stored counts and sums; nothing the simulator wrote about itself is trusted.
2.6Serial Independence
Serial independence means consecutive results are not correlated: one result does not shape the next. The census checks the draw itself, and the live results are checked against the fair law as a cross-reference:
Lag-1 autocorrelation (census): Correlation between consecutive draws, u / 2³², over the 2,000,000 census draws. Expected r ≈ 0 for an independent sequence; z = r × √n must stay below 3.403 in size. Measured z = −0.844.
Live-sample fit (Step 30, informational): The 7,700 captured results against the fair law P(result ≥ x) = 1/x: Kolmogorov–Smirnov D = 0.01199, p = 0.2172. Lag-1 correlation of 1/result in capture order: r = −0.00262, z = −0.230 (p = 0.8182). A deviation would be reported, never counted against the verdict.
const s = uni.serial;const n = s.n, mean = s.sumX / n;const varSum = s.sumXX - n * mean * mean;const cov = s.sumLag1Products - mean * (2 * s.sumX - s.first - s.last) + (n - 1) * mean * mean;const r1 = varSum > 0 ? cov / varSum : NaN;serialZ = r1 * Math.sqrt(n);2.7Worked Example — Full RNG Trace
Real bet from the dataset: bet 4085383, Phase A, target 91.95×, nonce 4. From limbo-master.json, computed with the package's own src/rng.ts:
serverSeed = 4dcc44b245c86cdd0151bf76943f24d0c999ba1cea7839bfedcd5032b0c29d3b
SHA-256 = 3abeb052ab32192a22394d91ea58b17700dfefeae70efb85c0e073d2ef4432aa (the bet's commitment)
clientSeed = mTJorxp6VuvAo0ep
nonce = 4
target = 91.95| Step | Operation | Value |
|---|---|---|
| 1 | Key | hex_decode("4dcc44b245c86cdd...") → 32 bytes |
| 2 | Message | "mTJorxp6VuvAo0ep:4" |
| 3 | HMAC-SHA256 | 02c7e463c230f3ce14e4d974b8b4c9b866596c59ec1d52c829c5bb172c2280f8 |
| 4 | First 8 hex characters | 02c7e463 → u = 46,654,563 |
| 5 | Result units | floor(2³² × 10⁶ / 46,654,564) = 92,058,888 |
| 6 | Result | 92,058,888 / 10⁶ = 92.058888× |
| 7 | Threshold | 91.95 / (1 − 0.001) = 92.042042…× |
| 8 | Win check | 92.058888 ≥ 92.042042… = true |
| 9 | Payout | 0.0125 × 91.95 = 1.149375 |
result = 92.058888 matches the recomputation exactly, and the revealed seed hashes to the bet's commitment. Win: 92.058888 ≥ 92.042042…, as served. Payout: 0.0125 × 91.95 = 1.149375, the served amount. Of all 2,965 wins, this result is the closest to its target in relative terms, so it sets the upper end of the measured edge bracket: 1 − 91.95 / 92.058888 = 0.1183% (Step 9).Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary7 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Result recomputation (Step 5) | 7,700 / 7,700 bets | Pass |
| Spec self-test (unit test) | 118 / 118 vector and property checks | Pass |
| Operator code cross-check (Step 5) | 7,700 / 7,700, Duel's Verify-tab code run unmodified (E10) | Pass |
| Key encoding (unit test) | Hex-decoded key reproduces the 5 engine vectors; the text form does not | Pass |
| Draw domain & bias bound (Step 23) | 32-bit draw used whole: bias 0 | Pass |
| Census uniformity and serial check (Step 18) | chi² p 0.6031; lag-1 z −0.844 | Pass |
| Live-sample fit (Step 30) | KS p 0.2172; lag-1 z −0.230 (informational) | Pass |
2.9Code References9 source files
| File | Purpose |
|---|---|
| src/rng.ts | HMAC-SHA256 draw and 6-decimal result, written from docs/ALGORITHM.md |
| tests/limbo/RngSpecTests.ts | The specification's 118 vector and property checks |
| src/operator-verifier.ts | Runs Duel's Verify-tab code (E10) unmodified as the second check |
| src/simulate.ts | Monte Carlo Pass 1: 2M-draw census, 13 targets × 1M rounds |
| src/exact.ts | Exact winning-draw counts over all 2³² draws |
| src/stats.ts | Chi-squared, Kolmogorov–Smirnov, lag-1 correlation, normal tails |
| tests/steps/determinism.ts | Step 5: result recomputation and Duel's code cross-check |
| tests/steps/exact.ts | Steps 22, 23 and 30: client seed influence, draw domain, live-sample fit |
| tests/steps/simulation.ts | Steps 18–19: simulation re-derived and cherry-pick screen |
2.10Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| Recomputed result matches the live result for all 7,700 bets | Pass |
| Key is hex-decoded (not the text form); the text form gives wrong results | Pass |
| The 32-bit draw is used whole: no modulo, no rejection, bias 0 | Pass |
| Outcomes use only server seed, client seed and nonce | Pass |
| 2M census draws fill all 1,000 bins and pass the uniformity screen | Pass |
| Lag-1 check passes on the census | Pass |
| All 13 simulated targets inside the family-wise screen of their exact RTP | Pass |
| A wrong client seed changes every one of the 7,700 results | Pass |
2.11Datasets UsedSource artifacts
2.12Reproduction InstructionsRun locally
Clone the repository at the release tag, install, and run the unit tests and the verifier:
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm testExpected output, as run at v1.2.1 (lines for this section):
✔ HMAC key must be hex-decoded bytes (UTF-8 key would differ)
✔ all 118 spec vector and property checks pass
✔ every captured bet: the operator roll equals the served result and src/rng.ts (7,700/7,700, integer 6-dp units)
43 passing
[PASS] Step 5 — Result Determinism (exact equality, no tolerance)
[PASS] Step 18 — Simulation Pass-1 (pinned seeds: draw uniformity + exact-RTP convergence)
[PASS] Step 23 — Draw Domain & Bias Bound (uint32, no range reduction)
[PASS] Step 30 — Live-Sample Fit (informational)Verifier Parity
Does the live game actually follow its own rules?
The verifier must produce the exact same result as the live game for every bet, and the payout must follow from it exactly. Two independent implementations check every bet: ours, written from the specification, and Duel's own Verify-tab code.
What we verified
- Every bet recomputed from its seeds: the result itself, not just the payout
- Every payout exact: stake × target on a win, 0 on a loss, with no tolerance
- Win condition and win chance checked against their formulas on all 7,700 bets
- Two implementations agree on every result: ours and Duel's Verify-tab code
- All four capture phases recompute the same way, at $0.0125 and at $10
What this means for you
- The verifier isn't a simulation: it produces the exact result the live game served
- Every bet you play can be recomputed by anyone
- Your stake doesn't change the result
- The live game follows the published rules on every captured bet
- Every bet recomputed from its seeds: the result itself, not just the payout
- Every payout exact: stake × target on a win, 0 on a loss, with no tolerance
- Win condition and win chance checked against their formulas on all 7,700 bets
- Two implementations agree on every result: ours and Duel's Verify-tab code
- All four capture phases recompute the same way, at $0.0125 and at $10
- The verifier isn't a simulation: it produces the exact result the live game served
- Every bet you play can be recomputed by anyone
- Your stake doesn't change the result
- The live game follows the published rules on every captured bet
Result recomputationPass
Verifier parityPass
Negative controlPass
Payout correctnessPass
Win condition integrityPass
is_win = result ≥ target / (1 − e) on all 7,700 bets, at the measured edge (Step 10) and, exactly, at the declared edge (Step 24)Win chance formulaPass
win_chance = (1 − e) / target within 10⁻¹⁸ on all 7,700 bets (Step 25)Bet-size independencePass
Target coveragePass
All 7,700 bets match the recomputation exactly, across all four capture phases. Both implementations agree on every result. Payouts, the win condition and the win chance all follow the published formulas, compared exactly on the served grid.
| Check | Result | Evidence |
|---|---|---|
| Result recomputation | Pass | 7,700 / 7,700 results recomputed exactly, as 6-decimal integer units (Step 5) |
| Verifier parity | Pass | Duel's Verify-tab code (cross-check), pinned by SHA-256, reproduces all 7,700 results (Step 5) |
| Negative control | Pass | A tampered nonce changes the result, and a served result one unit off is rejected (Step 6) |
| Payout correctness | Pass | 2,965 wins equal stake × target exactly; 4,735 losses credit exactly 0; no tolerance (Step 8) |
| Win condition integrity | Pass | is_win = result ≥ target / (1 − e) on all 7,700 bets, at the measured edge (Step 10) and, exactly, at the declared edge (Step 24) |
| Win chance formula | Pass | Served win_chance = (1 − e) / target within 10⁻¹⁸ on all 7,700 bets (Step 25) |
| Bet-size independence | Pass | The 200 $10 bets carry exactly the win chance served to base-stake bets at the same target (Step 16), recompute like them (Step 5) and pay exactly stake × target (Step 8) |
| Target coverage | Pass | 4,094 distinct targets, 1.01× to 998.37×; Phase A fills each decade: 1,662 / 1,693 / 1,645 bets (Steps 15, 28) |
Result recomputationPass
Verifier parityPass
Negative controlPass
Payout correctnessPass
Win condition integrityPass
is_win = result ≥ target / (1 − e) on all 7,700 bets, at the measured edge (Step 10) and, exactly, at the declared edge (Step 24)Win chance formulaPass
win_chance = (1 − e) / target within 10⁻¹⁸ on all 7,700 bets (Step 25)Bet-size independencePass
Target coveragePass
All 7,700 bets match the recomputation exactly, across all four capture phases. Both implementations agree on every result. Payouts, the win condition and the win chance all follow the published formulas, compared exactly on the served grid.
How It Works — Verifier Parity9 sections
3.1Why Parity Matters
If the verifier gives a different result from the live game, players can't rely on verification at all. Parity has to be 100%: a single mismatch would mean a bug in the verifier, a change in the live game, or an RNG that doesn't match its description. A player must be able to take the revealed seeds, run them through an independent verifier, and get the exact result they saw. Limbo's result is a multiplier with 6 decimals, so parity means equality to the last decimal, not closeness.
3.2Four-Phase Collection Design
Data was collected on 31 August 2026 across four structured phases, each designed to test a specific property. Limbo's edge sits in the win threshold, so it is measured where wins turn into losses. That boundary is pinned fastest at a low target, which is why Phase B bets 1.01×.
| Phase | Bets | Config | Bet Amount | Purpose |
|---|---|---|---|---|
| A — Target coverage | 5,000 | Targets log-uniform from 1.01× to 1,000×, 3,787 distinct | $0.0125 | Cover the target range and the tail |
| B — Edge precision | 2,000 | Target 1.01× (fixed) | $0.0125 | Pin the house edge at the win/lose boundary |
| C — Elevated stake | 200 | Target 1.01× (fixed) | $10.00 | Check the stake changes neither the result nor the price |
| D — Client seed variation | 500 | Log-uniform targets, 474 distinct; 10 auditor-set client seeds | $0.0125 | Check player-set client seeds are used |
| Phase B target | Captures where the edge bracket closes to 0.1 pp | Median width | 95th percentile width |
|---|---|---|---|
| 1.01× (chosen) | 78.3% | 0.0573 pp | 0.1645 pp |
| 10× | 31.7% | 0.1430 pp | 0.3795 pp |
| 100× | 25.5% | 0.1713 pp | 0.4614 pp |
outputs/capture-power.json: 1,500 simulated captures of this plan for each Phase B target. The executed capture closed the bracket to 0.0452 pp (Step 9). Step 14 checks the population: A 5,000, B 2,000, C 200, D 500, 158 seed records, 154 epochs of 50.3.3Result Recomputation (Steps 5–6)
For every bet, the verifier recomputes the result with computeResultUnitsFromBuffer(key, clientSeed, nonce) and compares it with the served result as a whole number of millionths, parsed from the served string; the floating-point path must land on the same unit. In the same step, Duel's own Verify-tab code (E10), pinned by SHA-256 and run with only its four input values filled in, must give the same result on every bet. A bet that can't be recomputed fails the step rather than being skipped.
Step 6 shows the comparison can fail: one nonce higher changes the result.
const keyCache = new Map<string, Buffer>();let mismatches = 0, skipped = 0, checked = 0, offGrid = 0;const examples: string[] = [];for (const b of bets) { const seed = seedMap.get(b.response.server_seed_hashed); if (!seed) { skipped++; continue; } let key = keyCache.get(seed); if (!key) { key = Buffer.from(seed, 'hex'); keyCache.set(seed, key); } const served = unitsFromDecimal(b.response.result, RESULT_DECIMALS); if (served === null) { offGrid++; continue; } const recomputed = BigInt(computeResultUnitsFromBuffer(key, b.response.client_seed, b.response.nonce)); // the float path (multiplier = units / 1e6, printed with toFixed(6)) must land on the same lattice point const viaFloat = unitsFromDecimal(computeMultiplierFromBuffer(key, b.response.client_seed, b.response.nonce).toFixed(RESULT_DECIMALS), RESULT_DECIMALS); checked++; if (recomputed !== served || viaFloat !== served) { mismatches++; if (examples.length < 3) examples.push(`bet ${b.response.id}: served ${b.response.result}, recomputed ${recomputed} units (float path ${viaFloat})`); }}src/rng.ts on every npm test (0 mismatches), and the 18 August 2026 transcription of Duel's verifier (E04) still reproduces all 7,700 results.3.4Payout Math (Step 8)
Stake, target, payout and the multiplier field are served as decimal strings, so Step 8 compares them as whole numbers on a common grid of 10⁻²⁰ (18-decimal amounts × 2-decimal targets), with no tolerance. A win must pay exactly stake × target, and a loss exactly 0. The 18-decimal multiplier field must equal the target, and the served target must equal the requested one.
for (const b of bets) { const stake = unitsFromDecimal(b.request.amount, AMT_DP); const won = unitsFromDecimal(b.response.amount_won, AMT_DP); const tgt = unitsFromDecimal(b.response.target, TGT_DP); const echo = unitsFromDecimal(b.response.multiplier, AMT_DP); const reqT = unitsFromDecimal(String(b.request.target), TGT_DP); if (stake === null || won === null || tgt === null || echo === null || reqT === null) { unparsable++; continue; } if (reqT !== tgt) targetDisagree++; // the multiplier echo is the target on an 18-dp grid if (echo !== tgt * 10n ** BigInt(AMT_DP - TGT_DP)) echoErrors++; const expected = b.response.is_win ? stake * tgt : 0n; // units of 1e-20 const paid = won * 10n ** BigInt(TGT_DP); // units of 1e-20 if (b.response.is_win) wins++; if (expected !== paid) { payoutErrors++; if (payoutExamples.length < 3) payoutExamples.push(`bet ${b.response.id}: expected ${b.request.amount} × ${b.response.target}, paid ${b.response.amount_won}`); }}3.5Win Condition (Steps 10, 24)
A bet wins when its result reaches the target with the edge taken out: result ≥ target / (1 − e). Step 10 checks every settlement at the edge measured from the data (4.2). Step 24 computes the rule again at the declared edge, e = 0.001, in exact integers, without the served flag or any served edge field, then compares it with every served flag.
The capture shows where the edge sits. Four bets landed at or above their target but below the threshold, and all four lost; a rule without the edge would have paid them. Every served result lies on the 6-decimal grid between 1.000000 and 4,294,967,296. No captured result landed exactly on a threshold, so the tie itself is not exercised; Duel's published comment gives ≥.
| Bet | Phase | Target | Result | Threshold T / (1 − e) | Served |
|---|---|---|---|---|---|
| 4085747 | A | 82.65 | 82.685069 | 82.732732… | Loss |
| 4100195 | A | 2.90 | 2.902121 | 2.902902… | Loss |
| 4106072 | B | 1.01 | 1.010017 | 1.011011… | Loss |
| 4116046 | D | 1.04 | 1.040218 | 1.041041… | Loss |
for (const b of bets) { const k = unitsFromDecimal(b.response.result, RESULT_DECIMALS); const t = targetCents(b.response.target); if (k === null || t === null) { if (bad.length < 2) bad.push(`bet ${b.response.id} unparsable`); continue; } n++; const kk = Number(k); if (kk >= UNIT && kk <= kMax) inRange++; const kMin = winMinUnits(t, A, B); const win = kk >= kMin; if (win === b.response.is_win) ruleOk++; else if (bad.length < 3) bad.push(`bet ${b.response.id}: result ${b.response.result} target ${b.response.target} rule says ${win}, served ${b.response.is_win}`); if (kk * (B - A) === t * B * 10_000) ties++; // k/1e6 == (t/100)·b/(b−a) exactly if (kk === UNIT) floors++; if (win) tightWin = Math.min(tightWin, kk - kMin); else tightLoss = Math.min(tightLoss, kMin - kk);}result ≥ target / (1 − 0.001), computed exactly (Step 24), and the rule at the measured edge (Step 10). The tightest win sits 1,079 millionths above its threshold, the tightest loss 782 below.3.6Win Chance & Threshold Formulas (Steps 11, 12, 25)
Limbo has no multiplier table: the payout multiplier is the target itself. What Duel prices is the win chance, served on every bet as win_chance = (1 − e) / target to 18 decimals, and the threshold the result must reach, target / (1 − e). Step 25 compares every served win chance with the formula exactly, to one unit in the 18th decimal, and counts the winning draws for each of the 4,094 served targets over all 2³² draws (4.1). Step 11 checks that the edge Duel reports, house_edge_percentage 0.1, sits inside the bracket measured from the data, and Step 12 that every served win chance does. Duel's house_edge_percentage and win_chance are compared, never used to derive the win probability or the RTP.
threshold = target / (1 − e) e = 0.001 on every captured bet
win chance = (1 − e) / target served as win_chance, 18 decimals
multiplier = target a win pays stake × target
target 2.69: threshold 2.692692… win_chance 0.371375464684014870
target 1.01: threshold 1.011011… win_chance 0.989108910891089109for (const wc of wcs) { wcN++; const u = unitsFromDecimal(wc, 18); if (u === null) continue; const lhs = u * BigInt(B) * BigInt(t); // wc · B · t · 1e18 const rhs = BigInt(B - A) * 100n * 10n ** 18n; // (B−A)·100 · 1e18 const diff = lhs > rhs ? lhs - rhs : rhs - lhs; if (diff < BigInt(B) * BigInt(t)) wcOk++; // |wc − q| < 1e-18}3.7Phase C — Bet-Size Equivalence (Step 16)
Phase C placed 200 bets at $10 on target 1.01×, the target Phase B played 2,000 times at $0.0125. Every Phase C bet carries exactly the win chance served to base-stake bets at that target, 0.989108910891089109, and recomputes from the revealed seeds; the stake is not an input to the result function. Each of the 198 Phase C wins paid $10 × 1.01 = $10.10, by the same exact rule as every $0.0125 win at 1.01× ($0.012625).
3.8Target Coverage (Steps 15, 28)
The edge is measured from bets that land near their thresholds, so the capture needed a wide spread of targets. Phase A drew its targets log-uniformly from 1.01× to 1,000×, so each decade gets about the same number of bets. Step 15 counts the distinct targets across the capture. Step 28 checks Phase A against the plan written in code: at least 50 Phase A bets in each decade, and the 1.01× minimum placed somewhere in the capture. Targets above 1,000×, up to the 100,000× maximum, were not played; their pricing is counted exactly for every one of them (Step 26, 4.1).
| Decade | Phase A bets | Floor |
|---|---|---|
| 1× – 10× | 1,662 | 50 |
| 10× – 100× | 1,693 | 50 |
| 100× – 1,000× | 1,645 | 50 |
const phaseA = bets.filter(b => b.phase === 'A');const counts = PHASE_A_DECADES.map(([lo, hi]) => phaseA.filter(b => { const T = (targetCents(b.response.target) ?? 0) / 100; return T >= lo && T < (hi === 1000 ? 1000.0001 : hi);}).length);const minTargetPlaced = bets.some(b => targetCents(b.response.target) === MIN_TARGET_CENTS);3.9Worked Example — Full Parity Verification
Real bet from Phase C: bet 4113793, target 1.01×, $10 stake, nonce 0. From limbo-master.json:
serverSeed = b44b9829f864ee16e20838027bd033b5455af11c34c98e8fe0572918a12a04d9
clientSeed = S06wBI051NtT24Q9
nonce = 0
target = 1.01
bet amount = $10.00| Step | Process | Output |
|---|---|---|
| 1 | HMAC-SHA256(hexDecode(serverSeed), "S06wBI051NtT24Q9:0") | 32f2b1d304145aeb... |
| 2 | First 8 hex characters: 0x32f2b1d3 | 854,766,035 |
| 3 | units = floor(2³² × 10⁶ / 854,766,036) | 5,024,728 |
| 4 | result = 5,024,728 / 10⁶ | 5.024728 — matches live game ✅ |
| 5 | Win check: 5.024728 ≥ 1.01 / 0.999 | true ✅ |
| 6 | Win chance: 0.999 / 1.01 | 0.989108910891089109 ✅ |
| 7 | Payout: $10 × 1.01 | $10.10 ✅ |
// computeResultUnitsFromBuffer(hexDecode("b44b9829f8..."), "S06wBI051NtT24Q9", 0)//// message = "S06wBI051NtT24Q9:0"// digest = HMAC-SHA256(hexDecode("b44b9829..."), message)// = 32f2b1d304145aeb81b9f23ab1518fb523725cf7857dae3ae5541cc35f3e6687//// First 8 hex characters: 0x32f2b1d3 = 854,766,035// units = floor(2^32 × 10^6 / 854,766,036) = 5,024,728// result = 5.024728 ✅//// Threshold: 1.01 / (1 − 0.001) = 1.011011…// Win: 5.024728 ≥ 1.011011… = true ✅// Win chance: 0.999 / 1.01 = 0.989108910891089109 (served) ✅// Payout: 10 × 1.01 = 10.1 ✅Technical Evidence & Verification5 sections
3.10Evidence Coverage Summary10 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Result recomputation (Step 5) | 7,700 / 7,700 bets, exact 6-decimal units | Pass |
| Operator code cross-check (Step 5) | Duel's Verify-tab code: 7,700 / 7,700 | Pass |
| Negative control (Step 6) | Nonce + 1 changes the result; ± 1 unit rejected | Pass |
| Payout math (Step 8) | 7,700 / 7,700, exact on the 10⁻²⁰ grid | Pass |
| Win condition (Steps 10, 24) | 7,700 / 7,700 at the measured and the declared edge | Pass |
| Win chance formula (Step 25) | 7,700 / 7,700 within one unit in the 18th decimal | Pass |
| Reported edge and win chance (Steps 11, 12) | 0.1000% inside the measured bracket; 7,700 / 7,700 in band | Pass |
| Phase C ($10) (Steps 5, 8, 16) | 200 / 200 priced as base stake; 198 wins paid exactly | Pass |
| Target coverage (Steps 15, 28) | 4,094 distinct targets; each Phase A decade above its floor | Pass |
| Population (Step 14) | 7,700 bets, 158 seed records, phases A–D as planned | Pass |
3.11Code References8 source files
| File | Purpose |
|---|---|
| tests/steps/determinism.ts | Steps 5–6: result recomputation, Duel's code cross-check, negative control |
| tests/steps/payouts.ts | Steps 8–12: payout math, measured edge, win condition, reported edge and win chance |
| tests/steps/exact.ts | Steps 24, 25 and 28: win rule at the declared edge, win chance, Phase A decades |
| tests/steps/dataset.ts | Steps 13–17: dataset hash, population, target coverage, bet-size invariance, Phase D |
| src/rng.ts | HMAC-SHA256 draw and 6-decimal result |
| src/exact.ts | Exact integer comparisons and winning-draw counts |
| src/operator-verifier.ts | Runs Duel's Verify-tab code unmodified (second check) |
| verifier-parity/operator/run-operator-verifier.cjs | The runner that fills in the file's four input values |
3.12Datasets UsedSource artifacts
3.13Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| Computed result matches the live result for all 7,700 bets | Pass |
| Duel's Verify-tab code agrees on all 7,700 results | Pass |
| Every win pays exactly stake × target; every loss pays 0 | Pass |
| is_win = (result ≥ target / (1 − e)) for all 7,700 bets | Pass |
| win_chance = (1 − e) / target to 18 decimals for all 7,700 bets | Pass |
| The multiplier field equals the target on all 7,700 bets | Pass |
| Phase C ($10) bets priced and paid like base-stake bets: 200 / 200 | Pass |
| Phase D: 500 / 500 results recompute under 10 player-set client seeds | Pass |
3.14Reproduction InstructionsRun locally
Clone the repository at the release tag, install, and run the verifier:
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verifyExpected output, as run at v1.2.1 (lines for this section):
[PASS] Step 5 — Result Determinism (exact equality, no tolerance)
[PASS] Step 6 — Negative Control (tampered nonce must break reproduction)
[PASS] Step 8 — Payout Math (stake x target, exact on the money grid)
[PASS] Step 10 — Win Condition (result >= target / (1 - measured edge))
[PASS] Step 11 — Operator-Reported Edge Cross-Check (distribution edge)
[PASS] Step 12 — Reported Win Chance Consistent with Measured Edge Bracket
[PASS] Step 14 — Population & Phase Structure (capture plan honoured)
[PASS] Step 15 — Target Coverage (spread of chosen multipliers)
[PASS] Step 16 — Bet-Size Invariance (edge independent of stake)
[PASS] Step 24 — Result Range & Win Rule at the Declared Edge (exact, with boundary census)
[PASS] Step 25 — Win Probability per Served Target (exact count over 2^32 draws)
[PASS] Step 28 — Phase A Target Coverage by Decade (against the plan in code)RTP & Payout Logic
Is the house edge what the casino claims?
The theoretical RTP is derived from first principles: the winning draws for every legal target are counted exactly over all 2³² outcomes, giving 99.9% at every target from 1.01× to 100,000× at the base stake tier (99.8977%–99.9000%), with no casino probability data. Duel's game settings state a 0.1% floor edge, and the RTP claim rule finds all 9,999,900 targets on it: PASS. Above the floor, the config allows an edge of up to 1%; on our reading it scales with potential profit. That is explained from the config, not certified.
What we verified
- Theoretical RTP is 99.9% at every target from 1.01× to 100,000× at the base stake tier (99.8977%–99.9000%)
- Derived from first principles: the winning draws are counted over all 2³² outcomes, not taken from the casino
- RTP claim rule: Duel's game settings state a 0.1% floor edge; 0 of 9,999,900 targets beyond ±0.1 pp — PASS
- 13M simulated rounds: precision-weighted 99.911%, consistent with 99.9%; all 13 targets agree with their exact RTP
- Pass 2 finds no sign of server-seed cherry-picking, at its stated power
What this means for you
- At the base tier the game returns 99.9% over the long run, whatever target you pick. Duel's Zero Edge rakeback, 0.1% of stake on every sampled bet, is credited separately (4.9)
- On our reading of Duel's config, the edge rises once a bet's potential profit passes 25,000, to 1.0% at most. Explained from the config, not certified
- The RTP figure is derived independently, not taken from the casino
- The 0.1% edge comes from the 1 − e factor in the win threshold
- Theoretical RTP is 99.9% at every target from 1.01× to 100,000× at the base stake tier (99.8977%–99.9000%)
- Derived from first principles: the winning draws are counted over all 2³² outcomes, not taken from the casino
- RTP claim rule: Duel's game settings state a 0.1% floor edge; 0 of 9,999,900 targets beyond ±0.1 pp — PASS
- 13M simulated rounds: precision-weighted 99.911%, consistent with 99.9%; all 13 targets agree with their exact RTP
- Pass 2 finds no sign of server-seed cherry-picking, at its stated power
- At the base tier the game returns 99.9% over the long run, whatever target you pick. Duel's Zero Edge rakeback, 0.1% of stake on every sampled bet, is credited separately (4.9)
- On our reading of Duel's config, the edge rises once a bet's potential profit passes 25,000, to 1.0% at most. Explained from the config, not certified
- The RTP figure is derived independently, not taken from the casino
- The 0.1% edge comes from the 1 − e factor in the win threshold
Anti-circularityPass
win_chance is compared, never used (Step 25)Theoretical RTPPass
RTP claim (RTP-CLAIM 1.2.0)Pass
min_edge 0.001). 9,999,900 targets: 0 beyond ±0.1 pp (Step 26). A miss would read RTP FIGURE OVERSTATED (exit 5)Edge measuredPass
Payout exactnessPass
Simulation (Pass 1)Pass
Cherry-pick test (Pass 2)Pass
Stake tiersInfo
Counting the winning draws over all 2³² outcomes gives 99.9% at every target from 1.01× to 100,000×, computed exactly, not estimated. Duel's stated edge holds under the RTP claim rule. Payouts are exact on every captured bet, and the simulation and cherry-pick test find no sign of bias. Tiers above the base tier are explained from the config, not certified.
RTP claim (RTP-CLAIM 1.2.0): Duel's config states a 0.1% floor edge; 0 of 9,999,900 targets beyond ±0.1 pp, PASS. Lowest of any target: 99.897673% (99,945.78×).
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | Winning draws counted over all 2³² outcomes for every served target; the casino's win_chance is compared, never used (Step 25) |
| Theoretical RTP | Pass | 99.897673%–99.900000% across all 9,999,900 targets 1.01×–100,000×, by exact count (Step 26) |
| RTP claim (RTP-CLAIM 1.2.0) | Pass | Claim: flat 99.9%, from Duel's game settings (min_edge 0.001). 9,999,900 targets: 0 beyond ±0.1 pp (Step 26). A miss would read RTP FIGURE OVERSTATED (exit 5) |
| Edge measured | Pass | From the win/lose boundary of 7,700 bets: 0.0957%, bracket (0.0731%, 0.1183%] (Step 9); Duel's reported 0.1% sits inside it (Step 11) |
| Payout exactness | Pass | 2,965 wins and 4,735 losses exact on the served grid (Step 8) |
| Simulation (Pass 1) | Pass | 13 targets × 1M rounds: all 13 agree with their exact RTP (worst |z| 1.56, limit 3.403; Step 18); precision-weighted 99.911% (4.6) |
| Cherry-pick test (Pass 2) | Pass | 154 casino seeds × 10,000 nonces: rank-sum Z 0.453, one-sided p 0.6748; the rule fires below −3.090 (Step 19) |
| Stake tiers | Info | Every captured bet priced at the 0.1% floor (Step 20). Duel's config allows up to 1.0%; on our reading the edge rises once a bet's potential profit passes 25,000 and reaches 1.0% at 250,000. Explained from the config, not certified |
Anti-circularityPass
win_chance is compared, never used (Step 25)Theoretical RTPPass
RTP claim (RTP-CLAIM 1.2.0)Pass
min_edge 0.001). 9,999,900 targets: 0 beyond ±0.1 pp (Step 26). A miss would read RTP FIGURE OVERSTATED (exit 5)Edge measuredPass
Payout exactnessPass
Simulation (Pass 1)Pass
Cherry-pick test (Pass 2)Pass
Stake tiersInfo
Counting the winning draws over all 2³² outcomes gives 99.9% at every target from 1.01× to 100,000×, computed exactly, not estimated. Duel's stated edge holds under the RTP claim rule. Payouts are exact on every captured bet, and the simulation and cherry-pick test find no sign of bias. Tiers above the base tier are explained from the config, not certified.
How It Works — RTP & Payout Logic10 sections
4.1Anti-Circularity (Steps 25–26)
The anti-circularity check derives the RTP without using the casino's probability data. A bet at target T wins when its draw gives a result of at least T / (1 − e). The result falls as the draw rises, so the winning draws are exactly the first W(T) values, and W(T) can be counted exactly, by binary search over the result formula. Step 25 counts W for every served target, and Step 26 for every legal target, 1.01× to 100,000×. A win pays stake × T (Step 8), so the RTP at T is T × W(T) / 2³². The proof has three parts:
| Component | Formula | Source |
|---|---|---|
| Win probability | W(T) / 2³², W(T) = draws whose result reaches T / (1 − e) | Counted over all 2³² draws by binary search (src/exact.ts): counting, not casino data |
| Payout | stake × T | Published rule; equals every served payout (Step 8) |
| RTP | T × W(T) / 2³² | Independent count × verified payout |
| Result | 99.897673% – 99.900000% for all 9,999,900 targets | Computed exactly, not estimated |
| Target | Winning draws W | P(win) | Nominal (1 − e) / T | RTP |
|---|---|---|---|---|
| 1.01× | 4,248,186,268 | 98.910794% | 98.910891% | 99.899902% |
| 2× | 2,145,335,094 | 49.949975% | 49.950000% | 99.899950% |
| 3.6× | 1,191,853,293 | 27.749997% | 27.750000% | 99.899989% |
| 10× | 429,067,190 | 9.989999% | 9.990000% | 99.899990% |
| 100× | 42,906,722 | 0.999000% | 0.999000% | 99.899997% |
| 1,000× | 4,290,672 | 0.099900% | 0.099900% | 99.899992% |
| 10,000× | 429,067 | 0.009990% | 0.009990% | 99.899946% |
| 36,099.45× | 118,857 | 0.002767% | 0.002767% | 99.900000% |
| 99,945.78× | 42,929 | 0.001000% | 0.001000% | 99.897673% |
| 100,000× | 42,906 | 0.000999% | 0.000999% | 99.898316% |
/** Smallest winning result units k at target t (hundredths) and edge a/b, exact rational rule. */export function winMinUnits(t: number, a = 1, b = 1000): number { const num = BigInt(t) * BigInt(b) * 10_000n; const den = BigInt(b - a); return Number((num + den - 1n) / den);} /** * Number of draws u ∈ [1, 2^32] whose result units reach kMin — exact, by binary search over the * result formula of src/rng.ts (K non-increasing in u). */export function winningDraws(kMin: number): number { if (resultUnitsAtU(1) < kMin) return 0; let lo = 1, hi = MAX_U32; // invariant: K(lo) >= kMin if (resultUnitsAtU(hi) >= kMin) return hi; while (hi - lo > 1) { const mid = Math.floor((lo + hi) / 2); if (resultUnitsAtU(mid) >= kMin) lo = mid; else hi = mid; } return lo;}Why this is non-circular: the win probability comes from counting draws under the published win rule, a property of the result formula, not of Duel's data. The casino's win_chance field is compared (Step 25), never used. The payout multiplier is the target itself, matched on every captured bet. The count is a whole number of draws, and the result is truncated to 6 decimals, so each target returns a hair under 99.9%.
4.2House Edge Mechanism (Step 9)
Limbo's result carries no edge: it is the fair multiplier 2³² / (u + 1). The 0.1% edge sits in the win threshold, target / (1 − e), so a bet needs a result 0.1% above its target to win, and a win pays the target itself. Duel's verifier code agrees on the outcome, in two forms. The 18 August 2026 transcription (E04) says the backend applies the edge to the win threshold and never changes the result. The current Verify-tab code (E10) keeps the roll edge-free, equal to the served result, and scores the bet on the roll × (1 − e), which it prints as the "Game result". At the 0.1% floor both forms give the served win flag on all 7,700 bets (reported in Step 5).
Because the edge is a backend setting, Step 9 measures it from the data instead of assuming it. A win at target T with result R proves e ≤ 1 − T/R; a loss proves e > 1 − T/R. The closest win and the closest loss bracket the edge.
win at (T, R) → e ≤ 1 − T/R upper bound
loss at (T, R) → e > 1 − T/R lower bound
tightest loss bet 4100195 T 2.90 R 2.902121 → e > 0.073084%
tightest win bet 4085383 T 91.95 R 92.058888 → e ≤ 0.118281%
edge bracket (0.073084%, 0.118281%] point 0.0957% width 0.045 pp
declared edge 0.1% (limbo config min_edge 0.001; house_edge_percentage 0.1)let upper = Infinity; // min over wins of 1 - T/Rlet lower = -Infinity; // max over losses of 1 - T/Rlet upperWitness = '', lowerWitness = '', witnesses = 0; for (const b of bets) { const T = num(b.request.target); const R = num(b.response.result); if (!(T > 0) || !(R > 0)) continue; const bound = 1 - T / R; witnesses++; if (b.response.is_win) { if (bound < upper) { upper = bound; upperWitness = `bet ${b.response.id} (T=${T}, R=${R})`; } } else if (bound > lower) { lower = bound; lowerWitness = `bet ${b.response.id} (T=${T}, R=${R})`; }}house_edge_percentage, 0.1000%, is consistent with it (Step 11). The bracket is 0.045 pp wide, inside the 0.1 pp the step requires.4.3Full RTP Table — All Targets
Limbo's RTP is counted exactly for every legal target, 1.01× to 100,000× in steps of 0.01: 9,999,900 targets. The table gives the lowest and highest RTP in each decade, from outputs/rtp-claim-table.json.
| Targets | Settings | Lowest RTP | Highest RTP | Beyond ±0.1 pp |
|---|---|---|---|---|
| 1.01× – 1.99× | 99 | 99.899902% | 99.900000% | 0 |
| 2.00× – 9.99× | 800 | 99.899950% | 99.900000% | 0 |
| 10.00× – 99.99× | 9,000 | 99.899990% | 99.900000% | 0 |
| 100.00× – 999.99× | 90,000 | 99.899977% | 99.900000% | 0 |
| 1,000.00× – 9,999.99× | 900,000 | 99.899768% | 99.900000% | 0 |
| 10,000.00× – 100,000.00× | 9,000,001 | 99.897673% | 99.900000% | 0 |
4.4RTP Claim Rule (RTP-CLAIM 1.2.0)
Framework 1.1 holds the audited RTP to the figure the casino publishes. The rule looks for a claim in six places: the game page, fairness page, help page, bet panel, rules or game settings. For Limbo, Duel's game settings (the limbo config, recorded on 21 August 2026 and fetched again on 5 October 2026) give the scaling edge a floor, min_edge 0.001, and every captured bet was served house_edge_percentage 0.1. So the claim is a flat 99.9% at the base tier. Duel's game and fairness pages state no figure.
The claim is scored on Step 26 against a ±0.1 pp band, for every legal target from 1.01× to 100,000×. A figure more than 0.1 pp below the claim, on the house side, makes the verdict RTP FIGURE OVERSTATED (exit 5). It never makes it NOT PROVABLY FAIR, because the results themselves pass.
The game page's "Zero Edge" badge is noted; the rakeback behind it is reported in 4.9. Edges above the floor are not scored.
| Rows | Settings | Exact RTP | Beyond the band |
|---|---|---|---|
| Targets 1.01×–100,000×, floor edge | 9,999,900 | 99.897673% – 99.900000% | 0 |
rtp-claim: ENFORCED — PASSrtp-claim: claim flat 99.900000% — source: game settings: scaling_edge min_edge 0.001 in Duel's limbo config (limboConfig.json; E12, as fetched 2026-10-05), served on every captured bet as house_edge_percentage 0.1 …rtp-claim: band ±0.10 pp · 9,999,900 settings · true min 99.897673% (target=99945.78) · true max 99.900000% (target=36099.45)rtp-claim: house side beyond band: 0 · player-favourable beyond band: 0 · worst target=99945.78 at -0.002327 pprtp-claim: outcome PASS · minimal fix: none — the claim holds for every setting it states a figure forrtp-claim: committed table outputs/rtp-claim-table.json: equals this run; sha256 e26e7b2bfbf41985… matches the pinoutputs/rtp-claim-table.json, pinned by SHA-256.4.5House Edge & Stake Tiers (Steps 20, 27)
Duel's limbo config (limboConfig.json, recorded 21 August 2026; the 5 October 2026 fetch, E12, carries the same values) declares a maximum target of 100,000× and a scaling_edge block: floor min_edge 0.1%, ceiling max_edge 1%, bankroll 25,000,000, max_loss_fraction 0.12. It holds values, not a formula, and Duel's Verify-tab code notes only that the round multiplier changes with bet size and the scaling edge.
Read as a cap on potential profit (bankroll × edge), the values imply edge = stake × (target − 1) / 25,000,000, clamped to 0.1%–1%. The floor holds while a bet's potential profit is at most 25,000; the 1% ceiling is reached at 250,000, which is also the largest profit a bet can carry. bankroll × max_loss_fraction (3,000,000) never binds. This reading is ours, from the config.
This audit certifies the 0.1% floor, where every captured bet was priced. Step 20 recomputes each bet's edge exactly from target × win chance: all 7,700 read 0.100000%. The largest potential profit in the capture, 12.47, is 2,005 times below where scaling would start.
| Target | 0.1% holds up to (stake, rounded) | 1% at the maximum profit (stake, rounded) |
|---|---|---|
| 2× | 25,000 | 250,000 |
| 10× | 2,778 | 27,778 |
| 100× | 253 | 2,525 |
| 1,000× | 25 | 250 |
| 100,000× | 0.25 | 2.50 |
| Bound (Step 27) | Value |
|---|---|
| Legal targets | 1.01× to 100,000× on the 2-decimal grid; all 7,700 captured targets inside |
| Result cap | Not stated by Duel; the formula allows up to 4,294,967,296× |
| Largest potential profit captured | 12.47 (stake × (target − 1)) |
| Maximum profit (implied by the config) | 250,000 |
| Maximum target at the smallest captured stake | 0.0125 × (100,000 − 1) = 1,249.99, within the 250,000 maximum profit |
| Minimum legal target | Not declared; 1.01× is the smallest the capture placed |
4.6Simulation Pass 1 — Pinned Seeds (Step 18)
The count in 4.1 gives each target's RTP exactly. The simulation checks that the result code behaves that way in practice: 13 targets from 1.1× to 100,000×, 1,000,000 rounds each on its own pinned seed pair at the declared 0.1% edge, plus the 2,000,000-draw census in 2.5. None of it uses casino data. Each target's win count is compared with its exact win probability; the 13 targets and the two census tests share a family-wise level of 0.01, so each z must stay inside 3.403.
Precision-weighted RTP: 99.911% ± 0.024 pp across the 13 targets. Each target's RTP is weighted by its precision, 1 / variance, with the variance T² × p(1 − p) / n at the theoretical p = 0.999 / T. High targets are noisy: at 100,000× a single win moves the RTP by 10 points, so an unweighted average would mostly measure those few wins. The chart above plots this weighted RTP at 13K, 130K, 1.3M, 6.5M and 13M rounds.
Screen: Worst target 100× at z = 1.56; 0 of 13 beyond 3.403. The census: chi² 986.69 (df 999, p 0.6031), lag-1 z −0.844 (2.5, 2.6).
| Target | Wins (of 1M) | Simulated RTP | Exact RTP | SE | z |
|---|---|---|---|---|---|
| 1.1× | 908,370 | 99.921% | 99.8999% | 0.032 pp | +0.65 |
| 1.25× | 798,799 | 99.850% | 99.8999% | 0.050 pp | −1.00 |
| 1.5× | 666,439 | 99.966% | 99.9000% | 0.071 pp | +0.93 |
| 2× | 499,502 | 99.900% | 99.9000% | 0.100 pp | 0.00 |
| 3× | 333,074 | 99.922% | 99.9000% | 0.141 pp | +0.16 |
| 5× | 199,932 | 99.966% | 99.9000% | 0.200 pp | +0.33 |
| 10× | 100,064 | 100.064% | 99.9000% | 0.300 pp | +0.55 |
| 25× | 39,894 | 99.735% | 99.9000% | 0.490 pp | −0.34 |
| 50× | 20,103 | 100.515% | 99.9000% | 0.700 pp | +0.88 |
| 100× | 10,145 | 101.450% | 99.9000% | 0.994 pp | +1.56 |
| 1,000× | 975 | 97.500% | 99.9000% | 3.16 pp | −0.76 |
| 10,000× | 102 | 102.000% | 99.8999% | 9.99 pp | +0.21 |
| 100,000× | 11 | 110.000% | 99.8983% | 31.61 pp | +0.32 |
for (const t of TARGETS_CENTS) { const label = `target:${t}`; const k = derive(`${label}:server`); const c = derive(`${label}:client`).toString('hex').slice(0, 16); const kMin = winMinUnits(t, SIM_EDGE.num, SIM_EDGE.den); let wins = 0; const snaps: { n: number; wins: number }[] = []; for (let n = 0; n < ROUNDS_PER_TARGET; n++) { if (computeResultUnitsFromBuffer(k, c, n) >= kMin) wins++; if (SNAPSHOTS.includes(n + 1)) snaps.push({ n: n + 1, wins }); } targets.push({ targetCents: t, rounds: ROUNDS_PER_TARGET, wins, serverSeedLabel: label, snapshots: snaps }); // eslint-disable-next-line no-console console.log(`target ${t / 100}: ${wins} wins / ${ROUNDS_PER_TARGET}`);}What the simulation adds. Each round computes a result from scratch with the same HMAC-SHA256 code used to check the live bets, then settles it by the published win rule. A return consistent with the exact RTP over millions of rounds shows the code and the count agree. npm test re-derives every statistic from the committed counts; npm run test:full reruns the simulation and compares it with the committed file.
4.7Cherry-Pick Detection — Pass 2 (Step 19)
Could the casino have chosen server seeds that start badly for players? Pass 2 takes every server seed Duel actually used, 154 of them, and plays each for 10,000 nonces: 200 windows of 50, each betting the targets of the captured epoch in the same order. Window 0 is the captured one. With no cherry-picking, it is just one of 200 interchangeable windows, so its rank among them is uniform. Pass 2 ranks it in every epoch and sums the ranks.
Why the rank: Under no cherry-picking the captured window's rank is uniform by symmetry, so the test needs no model of the return's spread. The raw return is printed beside it.
Why the early window: A casino that picked seeds against players would make the rounds actually played return less than the rest. The test rejects only on that side.
| Measure | Value |
|---|---|
| Seeds tested | 154 |
| Nonces per seed | 10,000 (200 windows of 50; window 0 holds the captured bets) |
| Captured results replayed | 7,700 / 7,700 (window 0 of every epoch) |
| Statistic | Rank of the captured window's standardized return among its epoch's 200 windows, summed over epochs (exact permutation null) |
| Rank-sum Z | 0.453 (one-sided p 0.6748) |
| Rule | Cherry-picking indicated at Z < −3.090 (one-sided α 0.001, pre-registered) |
| Direction split | Captured window below its epoch median in 71 epochs, above in 83 |
| Captured-window return | 7,324.67 against 7,692.30 expected (unit stakes) |
| Detection power | ≥ 50% for a cherry-pick that puts 15.5% or more of epochs' captured windows in their worst decile |
// mid-rank of window 0 among the 200, and its exact permutation variance const ranks = S.map(x => S.filter(y => y < x).length + (S.filter(y => y === x).length + 1) / 2); const meanR = (WINDOWS + 1) / 2; rankSum += ranks[0] - meanR; rankVar += ranks.reduce((a, r) => a + (r - meanR) ** 2, 0) / WINDOWS; if (ranks[0] < meanR) below++; else if (ranks[0] > meanR) above++;}const Z = rankSum / Math.sqrt(rankVar);4.8Bet-Size Equivalence (Phase C)
Phase C placed 200 bets at $10 on target 1.01×, the target Phase B played at $0.0125. All 200 results recompute from the revealed seeds through the same HMAC-SHA256 path; the stake is not an input to the result. Both stakes were served the same win chance, and each win paid stake × 1.01 exactly. On our reading of Duel's config, a larger potential profit can raise the edge (4.5), which would change the price, never the result; no captured bet came near that point.
| Metric | Phase B ($0.0125) | Phase C ($10) |
|---|---|---|
| Target | 1.01× | 1.01× |
| Bets | 2,000 | 200 |
| Results verified | 2,000 / 2,000 | 200 / 200 |
| Served win chance | 0.989108910891089109 | 0.989108910891089109 |
| Wins | 1,981 | 198 |
| Payout per win | 0.012625 | 10.1 |
| Applied edge (Step 20) | 0.100000% | 0.100000% |
4.9Informational Items & Wallet Envelopes
The return rows are reported for transparency, not scored: a sample this size can't resolve a 0.1% edge in a game whose captured targets reach 998×, and the count in 4.1 is the RTP evidence. The last two rows are Step 29, a scored check of the 163 sampled wallet envelopes; the rakeback it measures is a wallet credit outside the certified game RTP.
| Item | Value | Context |
|---|---|---|
| Phase A observed return | 95.5272% | 5,000 bets at $0.0125, targets 1.01×–998.37× |
| Phase B observed return | 100.0405% | 2,000 bets at $0.0125, target 1.01× |
| Phase C observed return | 99.9900% | 200 bets at $10, target 1.01× |
| Phase D observed return | 69.5040% | 500 bets at $0.0125, targets 1.02×–987.58× |
| Overall observed return | 99.7664% | 7,700 bets, $2,093.75 wagered; Phase C is most of the money |
| Win count | 2,965 against 2,943.46 expected | At the measured edge: chi² 0.2551 (df 1), p 0.6135 |
| Fair-multiplier tail | ≥ 2×: 3,916 · ≥ 10×: 811 · ≥ 100×: 76 · ≥ 1,000×: 6 | Against 3,850.0 · 770.0 · 77.0 · 7.7 expected under P(R ≥ x) = 1/x |
| Edge by target band | Every band's bracket contains the measured edge | 5 bands, from 1.01×–1.02× (2,205 bets) to 50×–1,000× (2,368 bets) |
| Zero Edge rakeback (Step 29) | Exactly 0.001 × stake on 163 / 163 sampled wallet envelopes | A wallet credit separate from settlement; not part of the game's RTP |
| Wallet ledger (Step 29) | 227 / 227 legs move the coin balance by exactly their coin delta | Debit = −stake and credit = payout (wins only) on 163 / 163 |
4.10Worked Example — Payout Verification
Real bet from Phase A: bet 4084905, target 769.02×, nonce 47, the highest target won in the capture. From limbo-master.json:
serverSeed = ec2d7f44c2f1828885be647a35192cdbcff8d8cb1f83531675e08bb83bbba0ea
clientSeed = T4kZsQsziQWrrS69
nonce = 47
target = 769.02Step 1 — Result: computeResultUnitsFromBuffer(key, clientSeed, 47) → HMAC-SHA256 → first 8 hex characters 0x00510a20 = 5,311,008 → 808.691398
Step 2 — Win check: 808.691398 ≥ 769.02 / 0.999 = 769.789789… → win
Step 3 — Payout: 0.0125 × 769.02 = 9.61275
Step 4 — RTP at this target: 769.02 × 5,579,402 / 2³² = 99.899986%
| Step | Process | Output |
|---|---|---|
| 1 | HMAC-SHA256 → first 8 hex characters | result = 808.691398 ✅ |
| 2 | Win check: 808.691398 ≥ 769.789789… | true ✅ |
| 3 | Payout: 0.0125 × 769.02 | 9.61275 ✅ |
| 4 | RTP: T × W(T) / 2³² | 769.02 × 5,579,402 / 2³² = 99.899986% |
// Target 769.02 — RTP by exact count// threshold = 769.02 / (1 − 0.001) = 769.789789… → 769,789,790 units// winning draws W = floor(2^32 × 10^6 / 769,789,790) = 5,579,402//// RTP = 769.02 × 5,579,402 / 2^32 = 99.899986%// Nominal: 769.02 × (0.999 / 769.02) = 99.9%; the count sits a hair below it.Technical Evidence & Verification5 sections
4.11Evidence Coverage Summary10 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Anti-circularity (Steps 25–26) | 4,094 served targets and all 9,999,900 legal targets counted | Pass |
| RTP claim (Step 26) | 9,999,900 targets, 0 beyond ±0.1 pp | Pass |
| House edge measured (Step 9) | (0.073084%, 0.118281%], width 0.045 pp | Pass |
| Reported edge (Step 11) | 0.1000% inside the measured bracket | Pass |
| Applied edge (Step 20) | 7,700 / 7,700 at the 0.1% floor | Pass |
| Edge above the floor | Explained from the config, up to 1.0% | Info — see 4.5 |
| Target bounds (Step 27) | 7,700 / 7,700 within 1.01×–100,000×; maximum profit 250,000 | Pass |
| Simulation Pass 1 (Step 18) | 13M rounds; 13 / 13 within the screen; census passes | Pass |
| Simulation Pass 2 (Step 19) | 154 seeds × 10,000 nonces; rank-sum Z 0.453 | Pass |
| Wallet envelopes (Step 29) | 163 / 163; rakeback 0.001 × stake | Pass |
4.12Code References9 source files
| File | Purpose |
|---|---|
| src/exact.ts | Winning-draw counts by binary search; the exact win rule |
| tests/steps/exact.ts | Steps 24–29: win rule, win probability, every legal target, bounds, decades, wallet |
| tests/steps/payouts.ts | Steps 8–12: payout math and the edge measured from the boundary |
| tests/steps/scalingedge.ts | Step 20: the applied edge, exactly, on every bet |
| src/scaling-edge.ts | Config parameters, scaling onset and maximum profit |
| src/rtp-claim.ts | Framework rule RTP-CLAIM 1.2.0 |
| src/rtp-settings.ts | The flat claim, its source, and the RTP claim table |
| tests/steps/simulation.ts | Steps 18–19: simulation re-derived and cherry-pick screen |
| src/simulate.ts | Monte Carlo Pass 1: census, 13 targets × 1M rounds, convergence snapshots |
4.13Datasets UsedSource artifacts
4.14Verified Invariants8 invariants
| Invariant | Result |
|---|---|
| T × W(T) / 2³² ≤ 0.999 for all 9,999,900 targets (non-circular) | Pass |
| Counted win probability within the truncation bound of (1 − e) / T on all 4,094 served targets | Pass |
| Every RTP-claim setting within ±0.1 pp of 99.9% | Pass |
| Every captured bet priced at the 0.1% floor (edge from target × win chance) | Pass |
| The measured edge bracket contains the declared 0.1% | Pass |
| 13 / 13 simulated targets within the family-wise screen | Pass |
| Pass 2 rank-sum Z above −3.090 across 154 casino seeds | Pass |
| Phase C ($10) bets follow the same result and price as $0.0125 | Pass |
4.15Reproduction InstructionsRun locally
Clone the repository at the release tag, install, and run the verifier:
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verifyExpected output, as run at v1.2.1 (lines for this section):
[PASS] Step 8 — Payout Math (stake x target, exact on the money grid)
[PASS] Step 9 — House Edge Measured from Win/Lose Boundary
[PASS] Step 10 — Win Condition (result >= target / (1 - measured edge))
[PASS] Step 11 — Operator-Reported Edge Cross-Check (distribution edge)
[PASS] Step 12 — Reported Win Chance Consistent with Measured Edge Bracket
[PASS] Step 18 — Simulation Pass-1 (pinned seeds: draw uniformity + exact-RTP convergence)
[PASS] Step 19 — Simulation Pass-2 (cherry-pick screen, 10,000 nonces per revealed seed)
[PASS] Step 20 — Scaling Edge (bet-size dependence)
[PASS] Step 24 — Result Range & Win Rule at the Declared Edge (exact, with boundary census)
[PASS] Step 25 — Win Probability per Served Target (exact count over 2^32 draws)
[PASS] Step 26 — Effective Edge on Every Legal Target vs Claim (exact enumeration)
[PASS] Step 27 — Target Bounds & Maximum Payout
[PASS] Step 29 — Wallet Envelopes (ledger legs and rakeback, exact)
rtp-claim: ENFORCED — PASS
rtp-claim: outcome PASS · minimal fix: none — the claim holds for every setting it states a figure forFairness Integrity Testing
Does the implementation maintain fairness under non-standard conditions?
Sections 1–4 prove the game is mathematically fair. Section 5 shows the implementation keeps those guarantees on every nonce, seed and result in the capture. We applied 10 fairness integrity tests covering nonce integrity, seed commitment, outcome determinism, and cross-player isolation. 9 tests passed and 1 is not applicable to this game type.
What we verified
- Nonce sequence — was any nonce skipped, repeated, or reset mid-epoch?
- Seed locking — did the seed pair ever change in the middle of an epoch?
- Seed reuse — is any server seed used for more than one epoch?
- Seed predictability — do server seeds follow the clock or each other?
- Result determinism — do the same seeds and nonce always give the same result?
- Cross-player isolation — can one player's seeds or outcomes affect another's?
What this means for you
- Across the 9 tests that apply, nothing in 7,700 live bets or 158 seed rotations shows an outcome altered, skipped, or repeated — by player or casino
- Every result follows from its seeds and nonce alone, and recomputes exactly
- Each bet is cryptographically unique and isolated
- No server seed was reused, and the seeds show no detectable link to the clock or to each other
- Your results are independent of every other player
- Nonce sequence — was any nonce skipped, repeated, or reset mid-epoch?
- Seed locking — did the seed pair ever change in the middle of an epoch?
- Seed reuse — is any server seed used for more than one epoch?
- Seed predictability — do server seeds follow the clock or each other?
- Result determinism — do the same seeds and nonce always give the same result?
- Cross-player isolation — can one player's seeds or outcomes affect another's?
- Across the 9 tests that apply, nothing in 7,700 live bets or 158 seed rotations shows an outcome altered, skipped, or repeated — by player or casino
- Every result follows from its seeds and nonce alone, and recomputes exactly
- Each bet is cryptographically unique and isolated
- No server seed was reused, and the seeds show no detectable link to the clock or to each other
- Your results are independent of every other player
Nonce integrityPass
Seed commitment integrityPass
Outcome determinismPass
Round & player isolationPass
10 fairness integrity tests: 9 pass, 1 N/A (single-step game).
Category Coverage
| Check | Result | Evidence |
|---|---|---|
| Nonce integrity | Pass | Server-assigned: bet requests never carry a nonce; 154 / 154 epochs hold exactly 0–49, no gaps, no repeats; in capture order, each runs 0 to 49 |
| Seed commitment integrity | Pass | 157 / 157 reveals (154 distinct seeds) match their commitments; 158 distinct commitments; seed pair constant within every epoch |
| Outcome determinism | Pass | Identical inputs produce identical results: 7,700 / 7,700 |
| Round & player isolation | Pass | Each result depends only on its own seed pair and nonce; the 2M-draw census finds no sign of serial dependence (lag-1 z −0.844) |
Nonce integrityPass
Seed commitment integrityPass
Outcome determinismPass
Round & player isolationPass
10 fairness integrity tests: 9 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. Four categories target specific fairness properties. Scope boundary: S5 tests whether the fairness guarantees hold across everything Duel served in the capture — every nonce, seed rotation and result. Platform-level implementation testing falls outside standard certification.
| Category | Tests | What It Catches |
|---|---|---|
| Nonce Integrity | 3 | Sequence gaps, server-side nonce manipulation, session continuity |
| Seed Commitment | 3 | 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 |
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 |
| CAPTURE FLAG | A gap in our own capture, not the operator | Disclosed; verdict-neutral |
| 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 |
| Result recomputation mismatch | Undisclosed inputs affecting outcomes |
| Client seed not used in HMAC | Player has no influence on outcomes |
10 tests · 9 pass · 1 N/A
Nonce Integrity3/3
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
FI-NONCE-004PassNonce sequence continues correctly after disconnect/reconnect — no reset to zero mid-epoch
Seed Commitment3/3
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
FI-SEED-005PassServer seeds show no correlation with timestamps, sequential patterns, or other predictable inputs
Statistical quality check on the seed records: 158 rotations over 2.3 hours, 1.9–60 seconds apart. All 158 commitments distinct; no two share more than 3 leading hex characters, the typical maximum for 158 random hashes. The 154 revealed seeds: consecutive seeds differ in 127.6 of 256 bits on average (128 for independent seeds); byte-value χ² 246.2 on 255 df (z −0.39); reveal order × first 32 bits Spearman ρ = 0.0437 (z 0.54). No detectable time correlation or sequential pattern.
Outcome Determinism2/2
FI-OUTCOME-001PassGiven identical inputs (serverSeed, clientSeed, nonce), the game always produces the same result — verified across all 7,700 live bets
S3, Step 5 — 7,700 / 7,700 bets recomputed from seeds, 0 mismatches.
FI-OUTCOME-002N/AA completed bet cannot be replayed via the API to generate a duplicate payout
Limbo settles in one request — single-step game with no separate cashout or settlement step to replay. The nonce increments server-side after each bet.
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 outcomes cannot be influenced by another player — each result depends only on that player's own seed pair and nonce
Structural — each result is a function of its own server seed, client seed and nonce alone (7,700 / 7,700 recompute with no other input, S3, Step 5), with a distinct server seed for every epoch (FI-SEED-003), so another player's bets have no input into it. No other bet took a nonce between ours: every epoch runs 0–49 with no gaps (S1, Step 7).
Technical Evidence & Verification4 sections
5.3Coverage Summary10 checks
| Test ID | Category | Verification Source | Status |
|---|---|---|---|
| FI-NONCE-001 | Nonce | S1, Step 7 (data-driven) | Pass |
| FI-NONCE-002 | Nonce | Captured requests + S1, Step 7 (data-driven) | Pass |
| FI-NONCE-004 | Nonce | S1, Steps 3 & 7 (data-driven) | Pass |
| FI-SEED-002 | Seed | S1, Steps 3 & 22 (data-driven) | Pass |
| FI-SEED-003 | Seed | S1, Steps 1 & 3 (data-driven) | Pass |
| FI-SEED-005 | Seed | Statistical analysis (data-driven) | 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 18 (simulation census) | Pass |
| FI-ISO-002 | Isolation | Structural — seed uniqueness | Pass |
5.4Additional Integrity Evidence (S1–S4)7 records
| Property | Source | Finding |
|---|---|---|
| 157 / 157 reveals match | S1, Step 1 | Revealed seeds (154 distinct) consistent with their commitments |
| 157 / 157 forward links | S1, Step 2 | Commitment chain consistent, plus the pre-capture anchor |
| 7,700 / 7,700 exact parity | S3, Step 5 | Every served result is the RNG output, unaltered |
| 7,700 / 7,700 exact payouts | S3, Step 8 | Every win paid stake × target, every loss 0 |
| Anti-circularity derived | S4, Steps 25–26 | 99.9% from an exact count of winning draws, not the casino's win_chance field |
| Pass 2 cherry-pick test | S4, Step 19 | Rank-sum Z = 0.453; no sign of seed pre-selection |
| 7,700 / 7,700 client seed influence | S1, Step 22 | Player input is genuine |
5.5Scope
This certification covers the 10 fairness integrity tests listed above, which verify that the provably fair implementation holds its guarantees across every captured bet and seed record. This is not a penetration test. It focuses specifically on the provably fair implementation — not the operator's broader platform security.
No infrastructure testing: Server security, network-level protections and operational controls fall under platform-level security, not game-level fairness, and sit outside this certification.
Point-in-time: Results apply to the game configuration and codebase at the time of audit. Post-audit changes require re-certification.
5.6Reproduction InstructionsRun locally
Data-driven tests (8 of 10): Fully reproducible from the open-source repo. Seven run against the captured dataset and seed records, and FI-ISO-001 against the pinned simulation census; all produce deterministic results.
Structural test (FI-ISO-002): Follows from the result formula, recomputed on every bet in S3: each result takes only its own seed pair and nonce.
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verifyExpected output, as run at v1.2.1 (S5-related steps):
[PASS] Step 1 — Commit-Reveal Integrity (SHA-256 over hex bytes)
[PASS] Step 2 — Forward Commitment (next server seed hash honoured)
[PASS] Step 3 — Seed Coverage & Epoch Binding (every epoch revealed, one commitment and one client seed per rotation window)
[PASS] Step 5 — Result Determinism (exact equality, no tolerance)
[PASS] Step 7 — Nonce Continuity (no gaps or repeats within an epoch)
[PASS] Step 8 — Payout Math (stake x target, exact on the money grid)
[PASS] Step 13 — Dataset Integrity (pinned SHA-256)
[PASS] Step 18 — Simulation Pass-1 (pinned seeds: draw uniformity + exact-RTP convergence)
[PASS] Step 19 — Simulation Pass-2 (cherry-pick screen, 10,000 nonces per revealed seed)
[PASS] Step 22 — Client-Seed Influence (model-rate gate + empty-seed control)
[PASS] Step 25 — Win Probability per Served Target (exact count over 2^32 draws)
[PASS] Step 26 — Effective Edge on Every Legal Target vs Claim (exact enumeration)
Tests decided:
FI-NONCE-001 Step 7
FI-NONCE-002 Step 7 + the captured requests
FI-NONCE-004 Steps 3 + 7
FI-SEED-002 Steps 3 + 22
FI-SEED-003 Steps 1 + 3
FI-SEED-005 fi-seed-quality.mjs, below
FI-OUTCOME-001 Step 5
FI-ISO-001 Step 18Statistical Quality Test (FI-SEED-005):
// FI-SEED-005: do the captured server seeds show any link to the clock or to each other?// Save in the repo root as fi-seed-quality.mjs, then run: node fi-seed-quality.mjsimport { readFileSync } from 'node:fs';const { seeds } = JSON.parse(readFileSync('data/limbo-master.json', 'utf8')); // 1. Timing: how far apart the rotations wereconst at = seeds.map(s => Date.parse(s.at));const gaps = at.slice(1).map((t, i) => (t - at[i]) / 1000).sort((a, b) => a - b); // 2. Commitments: all distinct, and the longest leading run any two shareconst hashes = seeds.map(s => s.seed.serverSeedHashed);const sorted = [...new Set(hashes)].sort();const shared = (a, b) => { let n = 0; while (a[n] === b[n]) n++; return n; };const prefix = Math.max(...sorted.slice(1).map((h, i) => shared(sorted[i], h))); // 3. Revealed seeds (a reveal repeated at a phase boundary is counted once)const revealed = seeds.map(s => s.seed.serverSeed).filter((p, i, all) => p && p !== all[i - 1]);const n = revealed.length;const bits = (a, b) => [...(BigInt('0x' + a) ^ BigInt('0x' + b)).toString(2)].filter(c => c === '1').length;const flips = revealed.slice(1).map((p, i) => bits(p, revealed[i])); // 4. Byte values: chi-squared over the 256 equally likely valuesconst counts = new Array(256).fill(0);for (const p of revealed) for (let i = 0; i < 64; i += 2) counts[parseInt(p.slice(i, i + 2), 16)]++;const e = n * 32 / 256, chi2 = counts.reduce((s, c) => s + (c - e) ** 2 / e, 0); // 5. Order: Spearman correlation of reveal order with each seed's first 32 bitsconst ranked = revealed.map((p, i) => [parseInt(p.slice(0, 8), 16), i]).sort((a, b) => a[0] - b[0]);const rho = 1 - 6 * ranked.reduce((s, [, i], r) => s + (r - i) ** 2, 0) / (n * (n * n - 1)); const hours = (at.at(-1) - at[0]) / 3.6e6, median = gaps[gaps.length >> 1];const average = flips.reduce((a, b) => a + b, 0) / flips.length;console.log(`rotations ${seeds.length} over ${hours.toFixed(1)} h, ${gaps[0].toFixed(1)}–${gaps.at(-1).toFixed(1)} s apart (median ${median.toFixed(0)} s)`);console.log(`commitments ${hashes.length}, distinct ${sorted.length}, longest shared leading hex ${prefix}`);console.log(`revealed seeds ${n}: consecutive seeds differ in ${average.toFixed(1)} of 256 bits on average (${Math.min(...flips)}–${Math.max(...flips)})`);console.log(`byte values ${n * 32}: chi2 ${chi2.toFixed(1)} on 255 df (z ${((chi2 - 255) / Math.sqrt(510)).toFixed(2)})`);console.log(`reveal order vs first 32 bits: Spearman rho ${rho.toFixed(4)} (z ${(rho * Math.sqrt(n - 1)).toFixed(2)})`);Expected output, run from the repo root:
rotations 158 over 2.3 h, 1.9–60.0 s apart (median 55 s)commitments 158, distinct 158, longest shared leading hex 3revealed seeds 154: consecutive seeds differ in 127.6 of 256 bits on average (112–143)byte values 4928: chi2 246.2 on 255 df (z -0.39)reveal order vs first 32 bits: Spearman rho 0.0437 (z 0.54)data/limbo-master.json, the dataset pinned by SHA-256 in Step 13, so every FI-SEED-005 figure reproduces from the repo. It needs Node.js 22 and nothing else.Player Verification
Can a player verify their own bets without trusting anyone?
Every Limbo bet can be checked from public inputs. Save the hashed server seed before you bet. After you rotate, compare the revealed seed's hash with your saved copy, then recompute the result. This section walks through it step by step, with scripts you can run yourself that reproduce all 7,700 captured bets, and links the ProvablyFair.org verifier.
Key principles
- Save the hashed server seed before you bet: your saved copy is what proves the timing
- Every bet can be reproduced from the server seed, client seed and nonce
- If the revealed seed hashes to your saved copy and your recomputed result matches the game's, the result was the committed one
- A win always pays stake × target; the edge sits in the win threshold, and every captured bet was at the 0.1% floor
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)
- Your target and stake — to check the win and the payout
- Save the hashed server seed before you bet: your saved copy is what proves the timing
- Every bet can be reproduced from the server seed, client seed and nonce
- If the revealed seed hashes to your saved copy and your recomputed result matches the game's, the result was the committed one
- A win always pays stake × target; the edge sits in the win threshold, and every captured bet was at the 0.1% floor
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the bet number in sequence (ensures uniqueness)
- Your target and stake — to check the win and the payout
Player can access all required inputsPass
Client seed origin transparentPass
Verification steps documentedPass
SHA-256(hexDecode(serverSeed)), the result, the win rule at the bet's edge, the payout (the package's Verify a bet)Casino UI exposes verification panelPass
Independent verifier availablePass
src/rng.ts reproduces all 7,700 bets, 0 differ (Step 5), as do the scripts below. The ProvablyFair.org verifier is linked aboveManual recomputation possiblePass
No hidden or proprietary stepsPass
Only disclosed inputs are used, and identical inputs always give the same result. Save the hashed seed before you bet, so the timing rests on your own record.
- 1
Save the Commitment
Before your first bet on a seed pair, open duel.com → Provably Fair and copy the active server seed hash. Keep it outside Duel.
- 2
Bet, Then Rotate
Place your bets. When you're done, rotate the seed pair to reveal the server seed.
- 3
Open a Past Bet
Open the bet from Transactions. After the rotation its details show the client seed, the revealed server seed and the nonce.
- 4
Check and Recompute
Check that SHA-256 of the hex-decoded server seed equals the hash you saved, not one shown after the reveal. Then recompute the result and check the win rule and the payout.
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
Verify with ProvablyFair.org
Recompute any Limbo bet from its revealed seeds, independently of the casino.
Open independent verifierBuilt-in verification
The operator’s own verification page. This tool is controlled by Duel.com.
Open Duel verifierVisual Walkthroughstep-by-step
Enter your target in the Multiplier field and choose a bet amount, then place the bet. Duel shows the matching win chance: 49.95% at 2.00×, which is 0.999 / 2. The result is a multiplier from 1× up, fixed by the seeds and nonce before the animation plays. At the 0.1% floor edge you win when the result reaches target / 0.999, and a win pays stake × target: here 2.86× clears the 2.002003× a 2.00× target needs, so a bet of 1 pays 2.

Illustrative: Duel.com Limbo — a 2.00× target in the Multiplier field, its 49.95% win chance, a bet of 1 and the result, 2.86×. The "Zero Edge" label is Duel's rakeback of 0.1% of stake, credited separately (4.9).
Open the Provably Fair panel from the link at the bottom right of the game. You'll see the active client seed, the hashed server seed (the casino's commitment) and your current nonce. Copy the hashed server seed now, before you bet, and keep it outside Duel: it is your proof of when the seed was fixed. You can also set the client seed for the next pair, whose server seed is already committed (its hash is shown). Clicking "Rotate seed" retires the active pair and reveals its server seed for your past bets.

Illustrative: Provably Fair panel — active client seed, hashed server seed, nonce, new client seed, next pair commitment and the Rotate seed action.
Open Transactions and click a past Limbo bet. Its Results tab shows the result, your target (Multi), the wager, the rakeback and the payout: here 2.86× against 2.00×, a win paying 2 on a wager of 1, with 0.001 rakeback. After you rotate the seed pair, its Verify tab shows the revealed server seed with the client seed and nonce. SHA-256 of the hex-decoded revealed seed must equal the hash you saved in Step 2:
Server Seed (revealed) — the plaintext value the casino committed to before your bet. Hash it and compare with the commitment you saved before betting.
Client Seed — your player-controlled entropy input.
Nonce — the sequential bet counter for this seed pair.

The bet's Results tab — result 2.86×, target (Multi) 2.00×, wager 1, rakeback 0.001 and payout 2 (account details masked). The Verify tab beside it shows the revealed server seed once the pair is rotated.
Open Duel's verify page (Provably Fair → Verify), choose Limbo and enter the revealed server seed, the client seed, the nonce and the bet's house edge (0.001 at the 0.1% floor). Its "PF roll before edge" must equal your result, and its "Game result", the roll with the edge taken out, wins when it reaches your target. If both match, and the hash matches your saved copy, the result was the one committed before you bet.
Manual Verification (Advanced)7 sections
6.5Why 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.6How the Algorithm Works (Plain English)
Before you bet, the result is fixed by three ingredients:
- The server's secret seed — committed by showing its hash before you bet
- Your client seed — sent from your side when the seed pair rotates; you can set it yourself
- The nonce — a counter that makes each bet unique
These go into HMAC-SHA256. The first 4 bytes of the HMAC output, 8 hex characters, give a number u from 0 to 4,294,967,295, and the result is 2³² / (u + 1), cut to 6 decimals: a multiplier from 1.000000× up to 4,294,967,296×, with no edge in it. You win if the result reaches your target divided by (1 − e); at the 0.1% floor edge that is target / 0.999. A win pays stake × target. Your target and stake decide the win and the payout; they never touch the result. Because the server showed the hash of its seed before you bet, and you saved that hash, the result can't be changed afterwards.
6.7Casino Verifier vs ProvablyFair.org Verifier3 records
Two verification tools are available, and the scripts below need neither. All should produce identical results — if they don't, something has changed.
| Duel.com Verifier | ProvablyFair.org Verifier | |
|---|---|---|
| Source | Casino-controlled | Independent (ProvablyFair.org) |
| Access | duel.com/provably-fair/verify | provablyfair.org/verify-bets/duel |
| Trust model | Requires trusting the casino | Not controlled by the casino |
src/rng.ts reproduces the same 7,700 results (Step 5).6.8JavaScript Verification Script
Run this in Node.js to verify any Limbo bet. It checks the commitment against the hash you saved, recomputes the result as the specification describes, and works out the smallest winning result at your bet's edge and the payout, in exact integers. It reproduces all 7,700 captured results, win flags and payouts.
const crypto = require('crypto'); // Limbo result, as written in the audit's specification (docs/ALGORITHM.md §2).function computeResult(serverSeed, clientSeed, nonce) { const key = Buffer.from(serverSeed, 'hex'); // 32 bytes, not the hex text const digest = crypto.createHmac('sha256', key).update(`${clientSeed}:${nonce}`).digest(); const u = digest.readUInt32BE(0); // first 4 bytes = first 8 hex characters return (2n ** 32n * 1000000n) / BigInt(u + 1); // floor(2^32 × 10^6 / (u + 1)) millionths, never below 1×} function verifyHash(serverSeed, savedHash) { const hash = crypto.createHash('sha256').update(Buffer.from(serverSeed, 'hex')).digest('hex'); return hash === savedHash;} // Decimal strings as exact integers, so nothing is rounded along the way.const units = (s, places) => { const [w, f = ''] = s.split('.'); return BigInt(w) * 10n ** BigInt(places) + BigInt((f + '0'.repeat(places)).slice(0, places)); };const show = (u, places, trim = true) => { const s = String(u).padStart(places + 1, '0'), t = `${s.slice(0, -places)}.${s.slice(-places)}`; return trim ? t.replace(/\.?0+$/, '') : t; }; // Win rule: result ≥ target / (1 − edge). Returns the smallest winning result, in millionths.function threshold(target, edge) { const num = units(target, 2) * 10n ** 16n, den = 10n ** 12n - units(edge, 12); return (num + den - 1n) / den; // target / (1 − edge), rounded up to the 6-decimal grid} // Replace with your values. savedHash is the hashed server seed you saved BEFORE betting.const serverSeed = 'YOUR_REVEALED_SERVER_SEED';const savedHash = 'YOUR_SAVED_SERVER_SEED_HASH';const clientSeed = 'YOUR_CLIENT_SEED';const nonce = 0;const target = '2.00'; // your target multiplierconst stake = '0.0125';const edge = '0.001'; // the 0.1% floor edge; larger bets: see 6.10 console.log('Hash check:', verifyHash(serverSeed, savedHash) ? 'PASS' : 'FAIL');const result = computeResult(serverSeed, clientSeed, nonce);const needed = threshold(target, edge);const win = result >= needed;console.log('Result:', show(result, 6) + '×');console.log('Wins from:', show(needed, 6, false) + '×', '→', win ? 'WIN' : 'LOSS');console.log('Payout:', win ? show(units(stake, 18) * units(target, 2), 20) : '0');6.9Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac # Limbo result, as written in the audit's specification (docs/ALGORITHM.md §2).def compute_result(server_seed, client_seed, nonce): key = bytes.fromhex(server_seed) # 32 bytes, not the hex text digest = hmac.new(key, f'{client_seed}:{nonce}'.encode(), hashlib.sha256).digest() u = int.from_bytes(digest[:4], 'big') # first 4 bytes = first 8 hex characters return 2**32 * 10**6 // (u + 1) # floor(2^32 × 10^6 / (u + 1)) millionths, never below 1× def verify_hash(server_seed, saved_hash): return hashlib.sha256(bytes.fromhex(server_seed)).hexdigest() == saved_hash # Decimal strings as exact integers, so nothing is rounded along the way.def units(s, places): w, _, f = s.partition('.') return int(w) * 10**places + int((f + '0' * places)[:places])def show(u, places, trim=True): s = str(u).rjust(places + 1, '0') t = f'{s[:-places]}.{s[-places:]}' return t.rstrip('0').rstrip('.') if trim else t # Win rule: result ≥ target / (1 − edge). Returns the smallest winning result, in millionths.def threshold(target, edge): num, den = units(target, 2) * 10**16, 10**12 - units(edge, 12) return -(-num // den) # target / (1 − edge), rounded up to the 6-decimal grid # Replace with your values. saved_hash is the hashed server seed you saved BEFORE betting.server_seed = 'YOUR_REVEALED_SERVER_SEED'saved_hash = 'YOUR_SAVED_SERVER_SEED_HASH'client_seed = 'YOUR_CLIENT_SEED'nonce = 0target = '2.00' # your target multiplierstake = '0.0125'edge = '0.001' # the 0.1% floor edge; larger bets: see 6.10 print('Hash check:', 'PASS' if verify_hash(server_seed, saved_hash) else 'FAIL')result = compute_result(server_seed, client_seed, nonce)needed = threshold(target, edge)win = result >= neededprint('Result:', show(result, 6) + '×')print('Wins from:', show(needed, 6, False) + '×', '→', 'WIN' if win else 'LOSS')print('Payout:', show(units(stake, 18) * units(target, 2), 20) if win else '0')6.10Payouts and Stake Tiers
Every captured bet was at the 0.1% floor edge, where a win needs a result of at least target / 0.999 and pays stake × target; the scripts above match Duel to the last decimal on all 7,700 bets. The edge never changes the payout, only the result you need. Your bet's own edge follows from the fields Duel serves with it: e = 1 − target × win_chance, using the 18-decimal win_chance (a rounded on-screen figure won't do), rounded to 12 decimal places; every captured bet gives 0.001. On our reading of Duel's config, a bet's edge rises above the floor once its potential profit, stake × (target − 1), passes 25,000, reaching 1% at 250,000 (4.5); for such a bet, put its own e into the scripts. That pricing is explained from the config, not certified.
6.11Evidence Exhibits11 records
| Evidence | Description |
|---|---|
| E01 | The live capture of 31 August 2026: 7,700 bets, 158 seed records and 163 sampled wallet envelopes (data/limbo-master.json) |
| E02 | Duel's Limbo config as recorded on 21 August 2026: the 100,000× maximum target and the scaling_edge block (limboConfig.json) |
| E03 | Five live pre-flight bets and the request and response shapes, recorded before the capture |
| E04 | Duel's published Limbo verifier, transcribed from its verify page on 18 August 2026 |
| E05 | The capture script that placed the bets, with its client-seed generators and phase plan |
| E06 | Simulation Pass 1 from pinned seeds and its convergence chart |
| E07 | The capture-plan power analysis: why Phase B bets at 1.01× (design rationale) |
| E08 | The verifier's committed results: 30 scored steps and the profile coverage |
| E09 | Run summaries: the win/lose chi-squared and the recomputation census |
| E10 | Duel's Limbo Verify-tab code, copied on 26 September 2026 and run unmodified as a second check |
| E12 | Duel's Limbo config as fetched on 5 October 2026: the same values as E02 |
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
How it works
- 1
Save the Commitment
Before your first bet on a seed pair, open duel.com → Provably Fair and copy the active server seed hash. Keep it outside Duel.
- 2
Bet, Then Rotate
Place your bets. When you're done, rotate the seed pair to reveal the server seed.
- 3
Open a Past Bet
Open the bet from Transactions. After the rotation its details show the client seed, the revealed server seed and the nonce.
- 4
Check and Recompute
Check that SHA-256 of the hex-decoded server seed equals the hash you saved, not one shown after the reveal. Then recompute the result and check the win rule and the payout.
Visual Walkthroughstep-by-step
Enter your target in the Multiplier field and choose a bet amount, then place the bet. Duel shows the matching win chance: 49.95% at 2.00×, which is 0.999 / 2. The result is a multiplier from 1× up, fixed by the seeds and nonce before the animation plays. At the 0.1% floor edge you win when the result reaches target / 0.999, and a win pays stake × target: here 2.86× clears the 2.002003× a 2.00× target needs, so a bet of 1 pays 2.

Illustrative: Duel.com Limbo — a 2.00× target in the Multiplier field, its 49.95% win chance, a bet of 1 and the result, 2.86×. The "Zero Edge" label is Duel's rakeback of 0.1% of stake, credited separately (4.9).
Open the Provably Fair panel from the link at the bottom right of the game. You'll see the active client seed, the hashed server seed (the casino's commitment) and your current nonce. Copy the hashed server seed now, before you bet, and keep it outside Duel: it is your proof of when the seed was fixed. You can also set the client seed for the next pair, whose server seed is already committed (its hash is shown). Clicking "Rotate seed" retires the active pair and reveals its server seed for your past bets.

Illustrative: Provably Fair panel — active client seed, hashed server seed, nonce, new client seed, next pair commitment and the Rotate seed action.
Open Transactions and click a past Limbo bet. Its Results tab shows the result, your target (Multi), the wager, the rakeback and the payout: here 2.86× against 2.00×, a win paying 2 on a wager of 1, with 0.001 rakeback. After you rotate the seed pair, its Verify tab shows the revealed server seed with the client seed and nonce. SHA-256 of the hex-decoded revealed seed must equal the hash you saved in Step 2:
Server Seed (revealed) — the plaintext value the casino committed to before your bet. Hash it and compare with the commitment you saved before betting.
Client Seed — your player-controlled entropy input.
Nonce — the sequential bet counter for this seed pair.

The bet's Results tab — result 2.86×, target (Multi) 2.00×, wager 1, rakeback 0.001 and payout 2 (account details masked). The Verify tab beside it shows the revealed server seed once the pair is rotated.
Open Duel's verify page (Provably Fair → Verify), choose Limbo and enter the revealed server seed, the client seed, the nonce and the bet's house edge (0.001 at the 0.1% floor). Its "PF roll before edge" must equal your result, and its "Game result", the roll with the edge taken out, wins when it reaches your target. If both match, and the hash matches your saved copy, the result was the one committed before you bet.
Technical evidence
6.5Why 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.6How the Algorithm Works (Plain English)
Before you bet, the result is fixed by three ingredients:
- The server's secret seed — committed by showing its hash before you bet
- Your client seed — sent from your side when the seed pair rotates; you can set it yourself
- The nonce — a counter that makes each bet unique
These go into HMAC-SHA256. The first 4 bytes of the HMAC output, 8 hex characters, give a number u from 0 to 4,294,967,295, and the result is 2³² / (u + 1), cut to 6 decimals: a multiplier from 1.000000× up to 4,294,967,296×, with no edge in it. You win if the result reaches your target divided by (1 − e); at the 0.1% floor edge that is target / 0.999. A win pays stake × target. Your target and stake decide the win and the payout; they never touch the result. Because the server showed the hash of its seed before you bet, and you saved that hash, the result can't be changed afterwards.
6.7Casino Verifier vs ProvablyFair.org Verifier3 records
Two verification tools are available, and the scripts below need neither. All should produce identical results — if they don't, something has changed.
| Duel.com Verifier | ProvablyFair.org Verifier | |
|---|---|---|
| Source | Casino-controlled | Independent (ProvablyFair.org) |
| Access | duel.com/provably-fair/verify | provablyfair.org/verify-bets/duel |
| Trust model | Requires trusting the casino | Not controlled by the casino |
src/rng.ts reproduces the same 7,700 results (Step 5).6.8JavaScript Verification Script
Run this in Node.js to verify any Limbo bet. It checks the commitment against the hash you saved, recomputes the result as the specification describes, and works out the smallest winning result at your bet's edge and the payout, in exact integers. It reproduces all 7,700 captured results, win flags and payouts.
const crypto = require('crypto'); // Limbo result, as written in the audit's specification (docs/ALGORITHM.md §2).function computeResult(serverSeed, clientSeed, nonce) { const key = Buffer.from(serverSeed, 'hex'); // 32 bytes, not the hex text const digest = crypto.createHmac('sha256', key).update(`${clientSeed}:${nonce}`).digest(); const u = digest.readUInt32BE(0); // first 4 bytes = first 8 hex characters return (2n ** 32n * 1000000n) / BigInt(u + 1); // floor(2^32 × 10^6 / (u + 1)) millionths, never below 1×} function verifyHash(serverSeed, savedHash) { const hash = crypto.createHash('sha256').update(Buffer.from(serverSeed, 'hex')).digest('hex'); return hash === savedHash;} // Decimal strings as exact integers, so nothing is rounded along the way.const units = (s, places) => { const [w, f = ''] = s.split('.'); return BigInt(w) * 10n ** BigInt(places) + BigInt((f + '0'.repeat(places)).slice(0, places)); };const show = (u, places, trim = true) => { const s = String(u).padStart(places + 1, '0'), t = `${s.slice(0, -places)}.${s.slice(-places)}`; return trim ? t.replace(/\.?0+$/, '') : t; }; // Win rule: result ≥ target / (1 − edge). Returns the smallest winning result, in millionths.function threshold(target, edge) { const num = units(target, 2) * 10n ** 16n, den = 10n ** 12n - units(edge, 12); return (num + den - 1n) / den; // target / (1 − edge), rounded up to the 6-decimal grid} // Replace with your values. savedHash is the hashed server seed you saved BEFORE betting.const serverSeed = 'YOUR_REVEALED_SERVER_SEED';const savedHash = 'YOUR_SAVED_SERVER_SEED_HASH';const clientSeed = 'YOUR_CLIENT_SEED';const nonce = 0;const target = '2.00'; // your target multiplierconst stake = '0.0125';const edge = '0.001'; // the 0.1% floor edge; larger bets: see 6.10 console.log('Hash check:', verifyHash(serverSeed, savedHash) ? 'PASS' : 'FAIL');const result = computeResult(serverSeed, clientSeed, nonce);const needed = threshold(target, edge);const win = result >= needed;console.log('Result:', show(result, 6) + '×');console.log('Wins from:', show(needed, 6, false) + '×', '→', win ? 'WIN' : 'LOSS');console.log('Payout:', win ? show(units(stake, 18) * units(target, 2), 20) : '0');6.9Python Verification Script
The same verification in Python (standard library only):
import hashlib, hmac # Limbo result, as written in the audit's specification (docs/ALGORITHM.md §2).def compute_result(server_seed, client_seed, nonce): key = bytes.fromhex(server_seed) # 32 bytes, not the hex text digest = hmac.new(key, f'{client_seed}:{nonce}'.encode(), hashlib.sha256).digest() u = int.from_bytes(digest[:4], 'big') # first 4 bytes = first 8 hex characters return 2**32 * 10**6 // (u + 1) # floor(2^32 × 10^6 / (u + 1)) millionths, never below 1× def verify_hash(server_seed, saved_hash): return hashlib.sha256(bytes.fromhex(server_seed)).hexdigest() == saved_hash # Decimal strings as exact integers, so nothing is rounded along the way.def units(s, places): w, _, f = s.partition('.') return int(w) * 10**places + int((f + '0' * places)[:places])def show(u, places, trim=True): s = str(u).rjust(places + 1, '0') t = f'{s[:-places]}.{s[-places:]}' return t.rstrip('0').rstrip('.') if trim else t # Win rule: result ≥ target / (1 − edge). Returns the smallest winning result, in millionths.def threshold(target, edge): num, den = units(target, 2) * 10**16, 10**12 - units(edge, 12) return -(-num // den) # target / (1 − edge), rounded up to the 6-decimal grid # Replace with your values. saved_hash is the hashed server seed you saved BEFORE betting.server_seed = 'YOUR_REVEALED_SERVER_SEED'saved_hash = 'YOUR_SAVED_SERVER_SEED_HASH'client_seed = 'YOUR_CLIENT_SEED'nonce = 0target = '2.00' # your target multiplierstake = '0.0125'edge = '0.001' # the 0.1% floor edge; larger bets: see 6.10 print('Hash check:', 'PASS' if verify_hash(server_seed, saved_hash) else 'FAIL')result = compute_result(server_seed, client_seed, nonce)needed = threshold(target, edge)win = result >= neededprint('Result:', show(result, 6) + '×')print('Wins from:', show(needed, 6, False) + '×', '→', 'WIN' if win else 'LOSS')print('Payout:', show(units(stake, 18) * units(target, 2), 20) if win else '0')6.10Payouts and Stake Tiers
Every captured bet was at the 0.1% floor edge, where a win needs a result of at least target / 0.999 and pays stake × target; the scripts above match Duel to the last decimal on all 7,700 bets. The edge never changes the payout, only the result you need. Your bet's own edge follows from the fields Duel serves with it: e = 1 − target × win_chance, using the 18-decimal win_chance (a rounded on-screen figure won't do), rounded to 12 decimal places; every captured bet gives 0.001. On our reading of Duel's config, a bet's edge rises above the floor once its potential profit, stake × (target − 1), passes 25,000, reaching 1% at 250,000 (4.5); for such a bet, put its own e into the scripts. That pricing is explained from the config, not certified.
6.11Evidence Exhibits11 records
| Evidence | Description |
|---|---|
| E01 | The live capture of 31 August 2026: 7,700 bets, 158 seed records and 163 sampled wallet envelopes (data/limbo-master.json) |
| E02 | Duel's Limbo config as recorded on 21 August 2026: the 100,000× maximum target and the scaling_edge block (limboConfig.json) |
| E03 | Five live pre-flight bets and the request and response shapes, recorded before the capture |
| E04 | Duel's published Limbo verifier, transcribed from its verify page on 18 August 2026 |
| E05 | The capture script that placed the bets, with its client-seed generators and phase plan |
| E06 | Simulation Pass 1 from pinned seeds and its convergence chart |
| E07 | The capture-plan power analysis: why Phase B bets at 1.01× (design rationale) |
| E08 | The verifier's committed results: 30 scored steps and the profile coverage |
| E09 | Run summaries: the win/lose chi-squared and the recomputation census |
| E10 | Duel's Limbo Verify-tab code, copied on 26 September 2026 and run unmodified as a second check |
| E12 | Duel's Limbo config as fetched on 5 October 2026: the same values as E02 |
Only disclosed inputs are used, and identical inputs always give the same result. Save the hashed seed before you bet, so the timing rests on your own record.
Reproducibility & Artifacts
Can anyone independently reproduce every finding in this audit?
This section covers the repository, the release tag, the commands and the reproduction gate. Every finding, statistic and pass/fail result can be reproduced by anyone with a computer. The repository is the credential, not this report.
Repository Details
- GitHub: github.com/ProvablyFair-org/duel-limbo
- Release: v1.2.1 · Framework 1.1
- Public Verifier: provablyfair.org/verify-bets/duel
Prerequisites
- Node.js 22.x (
.nvmrc) - npm
- Git
- GitHub: github.com/ProvablyFair-org/duel-limbo
- Release: v1.2.1 · Framework 1.1
- Public Verifier: provablyfair.org/verify-bets/duel
- Node.js 22.x (
.nvmrc) - npm
- Git
npm test at release v1.2.1 reproduces the committed results: 43 unit tests, 30 PASS, 0 flags, 0 hard fails, REPRODUCED, exit 0. Runs write to outputs/run/ and never overwrite the evidence of record.
Commands to Reproduce
npm test runs the 43 unit tests, then the 30 scored steps against the captured bets, then the reproduction gate. It scores the committed simulation and re-derives its statistics; it doesn't rerun the simulation. npm run test:full does.
The scored steps on their own. Results go to outputs/run/, never over the committed evidence.
Regenerates Pass 1 from its pinned seeds into outputs/run/: the 2,000,000-draw census and 1,000,000 rounds for each of 13 targets, 1.1× to 100,000×. The unit tests and the verifier then run, and the regenerated simulation is compared with the committed one. Pass 2, the 154 revealed seeds × 10,000 nonces, runs inside every verify.
npm run mutate breaks each scored step in a disposable copy and checks that the suite catches it: every mutation must be killed, and every scored step must have one. node scripts/release-manifest.mjs --check compares the release manifest's eleven SHA-256 hashes with the files on disk.
Audit Reproducibility Pinning
- Release
v1.2.1(Framework 1.1)- Release commit
2ea4ab9b408d1a23b7951a50e12bcc359c0f0618(21 October 2026)- Node Version
- 22.x (
.nvmrc,package.jsonengines) - Dataset
- data/limbo-master.json (7,700 bets, 158 seed records)
- Dataset Hash (SHA-256)
- b2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc
- Limbo Config Hash (E02, SHA-256)
- 676beff74f5a3c2e9d634a49e994acfc5adc0aa1a19fdc1ee4be36a916b07379
- Capture
- 31 August 2026
- Signed Tag
- v1.2.1, ProvablyFair.org SSH key SHA256:gEujiJm2pRyq7lQsfYQfQ1r97H/J7GdaKHbOW8GtikY
- Audit ID
- PF-2026-DL11
Output Artifacts7 files of record
| File | Contents | Size |
|---|---|---|
| verification-results.json | All 30 step results, profile coverage, verdict | ~37KB |
| determinism-log.json | 7,700 verified, 0 mismatches, 0 skipped | 57B |
| chi-squared-results.json | Live win count against the measured edge (informational) | 213B |
| rtp-claim-table.json | RTP claim: the 9,999,900 targets 1.01×–100,000× against 99.9%, by decade (PASS) | ~2.7KB |
| simulation-results.json | Pass 1: 2M-draw census + 13 targets × 1M rounds | ~22KB |
| capture-power.json | Capture-plan power analysis (design rationale, not verdict-bearing) | ~6.0KB |
| rtp-convergence.html | Realised RTP per target at 1K to 1M rounds | ~2.0KB |
Repository Structure
duel-limbo/ (release v1.2.1) ├── src/ │ ├── rng.ts → HMAC-SHA256 result, rebuilt from docs/ALGORITHM.md │ ├── exact.ts → Exact winning-draw counts over all 2³² draws │ ├── simulate.ts → Monte Carlo: 2M-draw census + 13 targets × 1M rounds, pinned seeds │ ├── scaling-edge.ts → Duel's scaling_edge config, pinned (E02, E12) │ ├── rtp-claim.ts → Framework rule RTP-CLAIM 1.2.0 │ ├── rtp-settings.ts → The flat 99.9% claim and the RTP claim table │ ├── operator-verifier.ts → Runs Duel's Verify-tab code unmodified (second check) │ ├── capture-plan.ts → The capture plan in code: phases, epochs, audit ID │ ├── artifact-pins.ts → SHA-256 pins for the artifacts of record │ ├── profile-map.ts → Step → Limbo profile check (31 checks) │ └── loader.ts, stats.ts, diff.ts, domains.ts, capture-power.ts, rtp-claim-switch.ts, types.ts ├── tests/ │ ├── verify.ts → 30 scored steps + the reproduction gate │ ├── steps/ → commitment, determinism, payouts, dataset, simulation, │ │ scaling edge, artifacts, exact checks, statistical │ ├── limbo/ → Mocha unit tests (43, incl. the spec self-test) │ └── mutations.json → Mutation battery registry (46 entries) ├── docs/ALGORITHM.md, INTERFACE.md → The written specification and module interface ├── data/limbo-master.json → 7,700 live bets (4 phases, 158 seed records, 163 wallet envelopes) ├── limboConfig.json → Duel's Limbo config, recorded 21 August 2026 (E02) ├── evidence/ → Pre-flight record (E03), transcription (E04), Verify-tab code (E10), config (E12) ├── verifier-parity/operator/ → Runner for Duel's Verify-tab code ├── capture/ → Reference record of the capture script (E05) ├── outputs/ → Committed evidence of record (runs write to outputs/run/) ├── scripts/ → mutate.mjs, release-manifest.mjs ├── RELEASE-MANIFEST.json → Release identity, artifact hashes, verdict, signature ├── MANIFEST.md → Consolidated audit overview ├── README.md └── package.json, package-lock.json, .nvmrc (Node 22), tsconfig.json, LICENSE (MIT)
Step-to-Section Cross-Reference30 verification steps mapped
| Step | Test Name | Report Section |
|---|---|---|
| 1 | Commit-Reveal Integrity (SHA-256 over hex bytes) | S1 |
| 2 | Forward Commitment (next server seed hash honoured) | S1 |
| 3 | Seed Coverage & Epoch Binding (every epoch revealed, one commitment and one client seed per rotation window) | S1 |
| 4 | Dataset Verifiability (100% of bets) | S1 |
| 5 | Result Determinism (exact equality, no tolerance) | S3 |
| 6 | Negative Control (tampered nonce must break reproduction) | S3 |
| 7 | Nonce Continuity (no gaps or repeats within an epoch) | S5 |
| 8 | Payout Math (stake x target, exact on the money grid) | S4 |
| 9 | House Edge Measured from Win/Lose Boundary | S4 |
| 10 | Win Condition (result >= target / (1 - measured edge)) | S4 |
| 11 | Operator-Reported Edge Cross-Check (distribution edge) | S4 |
| 12 | Reported Win Chance Consistent with Measured Edge Bracket | S4 |
| 13 | Dataset Integrity (pinned SHA-256) | S5 |
| 14 | Population & Phase Structure (capture plan honoured) | S3 |
| 15 | Target Coverage (spread of chosen multipliers) | S3 |
| 16 | Bet-Size Invariance (edge independent of stake) | S3 |
| 17 | Client-Seed Variation (player-supplied entropy) | S1, S2 |
| 18 | Simulation Pass-1 (pinned seeds: draw uniformity + exact-RTP convergence) | S4 |
| 19 | Simulation Pass-2 (cherry-pick screen, 10,000 nonces per revealed seed) | S5 |
| 20 | Scaling Edge (bet-size dependence) | S4 |
| 21 | Artifact Integrity (pinned artifacts of record) | S5, S7 |
| 22 | Client-Seed Influence (model-rate gate + empty-seed control) | S2 |
| 23 | Draw Domain & Bias Bound (uint32, no range reduction) | S2 |
| 24 | Result Range & Win Rule at the Declared Edge (exact, with boundary census) | S4 |
| 25 | Win Probability per Served Target (exact count over 2^32 draws) | S4 |
| 26 | Effective Edge on Every Legal Target vs Claim (exact enumeration) | S4 |
| 27 | Target Bounds & Maximum Payout | S4 |
| 28 | Phase A Target Coverage by Decade (against the plan in code) | S3 |
| 29 | Wallet Envelopes (ledger legs and rakeback, exact) | S4 |
| 30 | Live-Sample Fit (informational) | S2 |
reproduce-audit.sh
npm test at release v1.2.1 reproduces the committed results: 43 unit tests, 30 PASS, 0 flags, 0 hard fails, REPRODUCED, exit 0. Runs write to outputs/run/ and never overwrite the evidence of record.
Reproduction Gate & Release5 sections
7.1Exit Codes
The exit code is the verdict:
| Code | Meaning |
|---|---|
0 | PROVABLY FAIR — Full Pass, and this run reproduces the committed artifacts |
2 | PROVABLY FAIR — Conditional Pass: at least one operator-origin FLAG |
5 | RTP FIGURE OVERSTATED: the RTP claim rule (1.2.0) found a published RTP figure above the game's real RTP; the draws pass |
1 | NOT PROVABLY FAIR (a hard fail), or a reproduction difference beyond tolerance |
7.2The Reproduction Gate
npm run verify writes this run's results to outputs/run/ (gitignored) and compares them field by field with the committed results in outputs/: integers and strings exactly, other numbers within 1e-14 absolute + 1e-12 relative. When npm run test:full has regenerated the simulation, that is compared too. A re-run can't replace the evidence it scores: refreshing it takes a deliberate writer (npm run report, npm run simulate or npm run rtp-claim:write), and a pinned file is re-pinned in the same commit.
Committed artifacts NOT modified.Reproduction vs the committed artifacts on Node 22.23.4: REPRODUCED — compared: verification-results.json, determinism-log.json, chi-squared-results.json7.3SHA-256 Pinned Artifacts
The verifier checks the dataset's SHA-256 before any step runs. Step 21 checks the five pinned artifacts of record (the simulation results, the convergence chart, the power analysis, and the chi-squared and determinism logs) and Duel's config against their pins, and the RTP claim table has its own pin; a missing or altered file fails. The release manifest records the SHA-256 of all eleven files below.
| File | SHA-256 |
|---|---|
| data/limbo-master.json | b2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc |
| limboConfig.json | 676beff74f5a3c2e9d634a49e994acfc5adc0aa1a19fdc1ee4be36a916b07379 |
| outputs/simulation-results.json | 1ab73639811451f4a75212f83fde7651fbe9bb3e974473523c2be70ed789d2ae |
| outputs/chi-squared-results.json | e2d83a46b05f3aa6ff0cb2f4b60f9a86b9a269b6387537a511bcab62a89048a7 |
| outputs/determinism-log.json | cc52b48e1bb1bc37a962b7fd90d19e7671f593cf8659637591b9c9579b581c6c |
| outputs/capture-power.json | 4aa3c304278706fb6628442fe5727416431e5457809f359770d53597f6d6a5ef |
| outputs/rtp-convergence.html | 203df3b6da90d6902ab2c9f12a2a9702033beb698555860ee0d9cfb5ed2d1ac9 |
| outputs/verification-results.json | 30a0bf1383a4b09f5aca4774d38d68b63864b17749ac3c991686b9b5de606d5c |
| outputs/rtp-claim-table.json | e26e7b2bfbf41985e18cf8cc21c34db8d220eb1f48892a5f366e79f7c9ab939b |
| evidence/E12-limbo-config-2026-10-05.json | ce36245867f930f5587e4ca71d4e2ace5c082dd660c4c6574c888f8fa162eb18 |
| evidence/E12-limbo-config-2026-10-05.json.meta.json | b27f4a9588b68d8e4d01b2a7b443ed2e23ad77f17df3dad9c149adebb8ad5136 |
node scripts/release-manifest.mjs --check checks that all eleven hashes match the files on disk.
7.4Mutation Battery & Runtime
Mutation battery: 46 / 46 mutations killed and all 30 scored steps have a killing mutation, no declared survivors (npm run mutate, 20 October 2026; re-run at v1.2.1 on 25 October 2026 with the same result).
Runtime: the release manifest records npm test at 32.5 s on an Apple M2 Max shared with other audit runs (28 September 2026), and a fresh clone the day before took 11.4 s for npm test and 28.7 s for npm run test:full, both before v1.2.0. At v1.2.1, on the 2-core cloud machine used for this report (Node 22.23.4), npm test took 79.5 s, npm run verify 66.4 s and npm run test:full 137.6 s. Runtime varies by machine.
7.5Release History
The original audit was completed on 17 September 2026. Its re-certification under Framework 1.1 is the first public release:
| Release | What changed | Verdict |
|---|---|---|
v1.2.0 | Re-certified under Framework 1.1 and published, 21 October 2026: 30 scored steps (21 in the original audit), the RTP claim rule switched on, simulation Pass 1 from pinned seeds, the mutation battery grown to 46 entries; the RNG code rebuilt from a written specification, with Duel's Verify-tab code kept as a second check; dataset unchanged | PROVABLY FAIR — Full Pass |
v1.2.1 | Errata, 21 October 2026: adds .gitattributes so a Windows clone keeps every file byte-identical; no audit content changed | Unchanged |
Game Profile Coverage
Is every check in the Limbo profile covered?
The Limbo profile (version 1.0.2) lists 31 checks, LIMBO-01 to LIMBO-31. Each one is covered by a scored step or a package check, or marked with the package's own reason. Three are partial, where the package says so.
| Check | Covered by | Coverage | What it covers |
|---|---|---|---|
| LIMBO-01 | Steps 1, 3, 4 | Full | Revealed seeds open commitments published earlier; every betting epoch revealed; every bet under a revealed seed |
| LIMBO-02 | Step 2 | Full | Promotions and forward links recomputed from the raw hashes on all 158 records, plus the pre-capture anchor |
| LIMBO-03 | Step 3 | Full | Every bet in the rotation window whose commitment and client seed it carries; epochs bound to the capture plan |
| LIMBO-04 | Step 7 | Full | Nonces exactly 0–49 in every epoch; a gap or repeat would fail |
| LIMBO-05 | Step 14 | Full | Bets, seed records, phases, epoch count and epoch size bound to the capture plan |
| LIMBO-06 | Steps 17, 22 | Partial | Client seed influence and the Phase D custom seeds. Partial: the capture's A–C seeds came from Math.random(), not a CSPRNG |
| LIMBO-07 | Step 5 | Full | Every bet recomputed to its served result as exact 6-decimal units; coverage equals all 7,700 bets |
| LIMBO-08 | Step 23 | Full | 32-bit draw used directly: no modulo or rejection, bias bound exactly 0 |
| LIMBO-09 | Package check | Full | RNG rebuilt from the written specification; Duel's Verify-tab code run unmodified as a cross-check |
| LIMBO-10 | Steps 10, 24 | Full | Win flag equals result ≥ target / (1 − e) on every bet; every result on the 6-decimal grid, at least 1.000000 |
| LIMBO-11 | Step 8 | Full | Payouts exact: stake × target on a win, 0 on a loss, no tolerance |
| LIMBO-12 | Step 16 | Full | Phase C stake invariance: 200 bets at $10 priced exactly like base-stake bets; 198 elevated-stake wins |
| LIMBO-13 | Steps 8, 11, 12, 25 | Full | Served multiplier, house edge and win chance compared with the target and the measured edge, never used |
| LIMBO-14 | Step 27 | Full | Every target on the 2-decimal grid within 1.01×–100,000×; the maximum profit the config implies printed |
| LIMBO-15 | Steps 15, 28 | Full | 4,094 distinct targets; Phase A spread across the planned decades |
| LIMBO-16 | Step 29 | Partial | Wallet ledger legs exact on 163 sampled envelopes. Partial: the balance field appears only on sampled envelopes, so consecutive-bet continuity isn't asserted |
| LIMBO-17 | Step 24 | Partial | Boundary census printed. Partial: no captured result landed exactly on a threshold or at the 1.000000 floor |
| LIMBO-18 | Steps 6, 25 | Full | Negative control; win probability counted over all 2³² draws for every served target |
| LIMBO-19 | Steps 9, 26 | Full | Edge measured from the win/lose boundary; every legal target priced by exact count against the claim |
| LIMBO-20 | Step 18 | Full | 2M-draw census and 13 targets × 1M rounds against each target's exact RTP, re-derived from the stored counts |
| LIMBO-21 | Step 19 | Full | Pass 2 cherry-pick test on all 154 revealed seeds, on the targets each epoch served |
| LIMBO-22 | Step 30 | Full | Live-sample fit and serial checks (informational) |
| LIMBO-23 | Package check | Full | The package’s S5 matrix, each row mapped to the scored steps |
| LIMBO-24 | — | None | Release v1.2.1 runs no copy of the ProvablyFair.org verifier on the captured bets; its own src/rng.ts and Duel's Verify-tab code reproduce all 7,700 (Step 5) |
| LIMBO-25 | Package check | Full | Player verification steps in order, with the commitment encoding |
| LIMBO-26 | Step 13 | Full | Dataset pinned by SHA-256 |
| LIMBO-27 | Step 21 | Full | Pinned artifacts and value domains |
| LIMBO-28 | Package check | Full | Fresh-clone reproduction record (npm test and test:full) |
| LIMBO-29 | Package check | Full | Mutation battery: every scored step has a killing mutation |
| LIMBO-30 | Package check | Full | Limitations register and load-bearing premises |
| LIMBO-31 | Step 20 | Full | Every bet's applied edge recovered from target × win chance, all at the 0.1% floor; the scaling onset stated |
Math.random(), not a CSPRNG (a capture-side choice; the seed's role in every result is checked in full). LIMBO-16 because the balance field appears only on 163 sampled wallet envelopes. LIMBO-17 because no captured result landed exactly on a threshold or at the 1.000000 floor. LIMBO-24 is marked none: release v1.2.1 runs no copy of the ProvablyFair.org verifier on the captured bets, while its own code and Duel's reproduce all 7,700 (Step 5).