Skip to main content
Audits
Duel.com/Limbo
Duel: Limbo Audit

Limbo

Certified · Active

Independent verification, backed by open-source code.

PF-2026-DL11Audited August 2026 · Updated October 2026Aug 2026 · Updated Oct 2026Framework 1.1Release v1.2.1
AUDIT RESULTPassedPROVABLY FAIR — Full Pass30 / 30 steps passed0 flags · 0 hard fails
PROVABLY FAIR — Full Pass30 / 30 steps passed · 0 flags · 0 hard fails
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
30 / 30Verification steps passed

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.

DUEL LIMBO$0.0125 · Target 79.13×
2.37703×Target 79.13×1×10×100×1,000×Loss · wins from 79.209210×
Bet #4079143Nonce 0 · Phase A
50 real bets · Phase ABet 1 of 50
✓ Exact match
2.37703×

The casino result, reproduced.

Casino outcome
Result 2.37703× · Target 79.13× · Loss
Our reproduction
Result 2.37703× · Loss
Recorded payout
$0.0000
Open source

Run this audit yourself

Open GitHub ↗

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.

npm test
Needs git and Node.js 22View reproduction commands ↓
Limbo Audit Overview

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.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPassPROVABLY 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 RTPPass99.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 ParityPass7,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 SystemPass157 / 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 AuditPass154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7)
Client SeedPassA 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 AnalysisPassOur 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 LogicPass2,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 EdgeInfoEvery 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-CircularityPassThe 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 ChecksPass10 fairness integrity tests — 9 pass, 1 N/A (single-step game)
DeterminismPassEvery result recomputes from (serverSeed, clientSeed, nonce); the target and stake are not inputs to the draw (Step 5)
ReproducibilityPassnpm 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
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 RTPPass
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 ParityPass
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 SystemPass
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 AuditPass
154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7)
Client SeedPass
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 AnalysisPass
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 LogicPass
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 EdgeInfo
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-CircularityPass
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 ChecksPass
10 fairness integrity tests — 9 pass, 1 N/A (single-step game)
DeterminismPass
Every result recomputes from (serverSeed, clientSeed, nonce); the target and stake are not inputs to the draw (Step 5)
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 results
Audit scope & reference

What 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
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 server seed hashing, the forward commitment chain, reveal on rotation
Client Seed OriginPlayer-set client seeds honoured (all 154 set by the capture, including 10 custom seeds); the capture's own seeds disclosed
Seed HandlingClient seed control, nonce lifecycle, seed pair rotation
RNG AnalysisHMAC-SHA256 with the first 32 bits used directly, no modulo or rejection, written from the specification; bias bound 0
Payout LogicWin condition result ≥ target / (1 − e), exact payouts, win chance formula, stake invariance (Phase C)
Live ParityOur recomputation and Duel's Verify-tab code (cross-check) against every live bet
RTP ValidationAnti-circularity (exact count over 2³² draws), RTP claim rule on all 9,999,900 targets, 13M-round simulation, cherry-pick test (Pass 2)
Fairness IntegrityIntegrity matrix — 10 tests across nonce, seed commitment, determinism, and isolation
Game Profile Coverage30 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.

The result is determined cryptographically at the moment the bet is placed. The animation is cosmetic — the outcome is fixed before it displays.
Win Conditions

The win condition in Limbo is a single threshold comparison.

3 columns
OutcomeConditionExample
Winresult ≥ T / (1 − e)target 91.95×, result 92.058888 → threshold 92.042042 → Win, pays 91.95×
Lossresult < T / (1 − e)target 3.60×, result 2.221402 → threshold 3.603604 → Loss
At the base tier e = 0.001. The served win flag follows this rule on all 7,700 captured bets (Step 24).
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
3 columns
ParameterValueNotes
Target1.01×–998.37× in the captureDuel's config allows up to 100,000× (max_target); 2-decimal steps
House Edge0.1% at the base tierRises with potential profit, to 1.0% at most (4.5)
Theoretical RTP99.9% (base tier)Counted exactly for every target 1.01×–100,000×
RNG AlgorithmHMAC-SHA256One HMAC call per bet; key = hex-decoded server seed
DrawFirst 8 hex characters → 32-bit integer vUsed 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.

4 columns
Seed TypeFormatExamplePurpose
Server Seed64-char hex (32 bytes)4dcc44b245c86cdd…Casino-provided randomness
Client SeedAlphanumeric stringmTJorxp6VuvAo0epPlayer-contributed entropy
NonceInteger from 04One per bet within an epoch
The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. Passing the hex string as UTF-8 gives completely different results. All 7,700 captured bets recompute with the decoded key.
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)
4 columns
TargetWin ChanceThresholdRTP
1.01×98.9109%1.01101199.9%
2×49.9500%2.00200299.9%
10×9.9900%10.01001099.9%
100×0.9990%100.10010099.9%
1,000×0.0999%1,001.00100199.9%
100,000×0.000999%100,100.10010099.9%
At the base tier every target returns 99.9% at the claim's precision. The exact count over all 2³² draws puts every target between 99.897673% and 99.900000% (Step 26). Results sit on a 6-decimal grid, so the smallest winning result is the threshold rounded up: 2.002003× at 2×, the explainer's Wins From.
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

2 columns
TestDescription
Server seed commit exists before playSHA-256 hash shown to player before betting
Server seed reveal matches commitSHA-256(hexDecode(revealed)) = committed hash
Client seed controlPlayer can set/change client seed via rotation UI
Nonce increments correctlyStarts at 0, +1 per bet, resets at epoch boundary
Full determinismSame inputs → same result

2. Randomness & Entropy Model

2 columns
TestDescription
RNG depends only on seeds + nonceNo external inputs — self-contained HMAC-SHA256
No mixed entropy sourcesNo timestamps, Math.random, etc.
Unbiased mappingThe 32-bit draw is used directly: no modulo, no rejection, bias exactly 0
No state leakageEach bet uses unique HMAC message (clientSeed:nonce)

3. Verifier ↔︎ Live Parity

2 columns
TestDescription
Live outcomes match verifier7,700 / 7,700 results recomputed with 0 mismatches
Multi-phase verificationPhases 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

2 columns
TestDescription
Anti-circularity (Steps 25, 26)Winning draws counted over all 2³² outcomes; the casino's win_chance compared, never used
House edge auditMeasured 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 correctnessWin amount equals stake × target exactly (no tolerance)
Simulated RTP convergence13M rounds: 99.911% across 13 targets, consistent with 99.9%
Cherry-pick detectionRank-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

2 columns
TestDescription
Player can reproduce results offlineUsing seeds + nonce
Verifier logic matches live logicOur RNG (written from the spec) and Duel's Verify-tab code agree on all 7,700 bets
Verifier publicly accessibleThe ProvablyFair.org verifier, Duel's verify page and the open-source scripts in S6
No reliance on private APIsFully client-side verification
10 fairness integrity testsNonce, 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

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

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
nonce-lifecycle
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:

determinism
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
2 columns
TermDefinition
Framework 1.1The 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 flagA flag caused by our capture, not by the casino. Disclosed and counted, and verdict-neutral. This audit has none.
RTP claim ruleRTP-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 tierThe 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
2 columns
TermDefinition
Provably FairA cryptographic system that allows players to mathematically verify that game outcomes were not altered. Relies on commit-reveal schemes and deterministic algorithms.
Commit-Reveal ProtocolA two-phase process in which the casino commits to a result (by showing its hash) before the player bets, then reveals the actual value after the bet.
DeterminismThe property that identical inputs always produce identical outputs. Same server seed, client seed, and nonce must always generate the same Limbo result.
Client Seed OriginThe 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
2 columns
TermDefinition
Server SeedA 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 SeedA value controlled by the player. Players can change it at any time via the seed rotation UI.
NonceA sequential counter from 0 that rises by 1 per bet within an epoch. Resets to 0 when you rotate the seed pair.
Hashed Server SeedSHA-256(hexDecode(serverSeed)) — the commitment hash shown before betting. After rotation, players verify the revealed seed produces this hash.
EpochThe bets placed under one (serverSeed, clientSeed) pair, until the player rotates it. Our capture rotated every 50 bets.
Cryptographic Functions
2 columns
TermDefinition
HMAC-SHA256Hash-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 drawThe 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-256Secure Hash Algorithm 256-bit. Used for server seed commitment: SHA-256(hexDecode(serverSeed)) = serverSeedHashed.
Hex DecodingConverting 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
2 columns
TermDefinition
VerifierA tool that recomputes the result from the seeds and nonce. Duel's Verify-tab code, run unmodified, reproduces all 7,700 bets (Step 5).
ParityDegree of matching between verifier and live game results. 100% parity = every result matches. This audit: 7,700/7,700 exact match.
Anti-CircularityProof 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 DetectionA 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
2 columns
TermDefinition
TargetPlayer-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).
ResultThe 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⁻¹⁸.
Thresholdtarget / (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
2 columns
TermDefinition
Seed PairA (serverSeed, clientSeed) combination used for a sequence of bets. Our capture used each pair for 50 bets.
PhaseA 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 StepOne of the 30 checks behind the verdict: PASS, FLAG or FAIL. Here all 30 pass.
Informational ContextStatistical 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
2 columns
TermDefinition
limbo-master.jsonThe 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.jsonOutput of the 30-step verification suite: every step result, profile coverage and the verdict.
simulation-results.jsonPass 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.jsonEvery target from 1.01× to 100,000× scored against the 99.9% claim (RTP-CLAIM 1.2.0).
01

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.

Commit-Reveal Consistency

157 / 157revealed seeds match

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
157 / 157revealed seeds match
  • 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
Provably Fair Limbo Generation — server seed, client seed and nonce through HMAC-SHA256 to the result multiplier — illustrated flow
CheckResultEvidence
Seed hash integrityPass157 / 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 linkagePass158 / 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 epochPassEvery 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 sequencingPass154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7)
Deterministic outputPass7,700 / 7,700 results recomputed from (serverSeed, clientSeed, nonce) as exact 6-decimal values (Step 5)
Client seed participationPassUnder 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 originPassThe 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
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 linkagePass
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 epochPass
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 sequencingPass
154 / 154 epochs carry exactly nonces 0–49: no gaps, no repeats, none out of range (Step 7)
Deterministic outputPass
7,700 / 7,700 results recomputed from (serverSeed, clientSeed, nonce) as exact 6-decimal values (Step 5)
Client seed participationPass
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 originPass
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
✓  Commit-reveal consistent

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.

Server Seed Commitment — illustrated flow
src/rng.ts · verifyHash · Verified
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();}
Result: 157 / 157 revealed seeds open a commitment published at an earlier record: 154 the record just before, and 3 repeat the reveal before them. All 154 betting epochs are opened. 0 mismatches.

Real Example from Live Data:

limbo-master.json · bet and reveal
// 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:

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

tests/steps/commitment.ts · Step 2: Forward Commitment · Verified
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++;}
Result: 158 / 158 promotions and 157 / 157 forward links match, and the pre-capture anchor holds.
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.

tests/steps/commitment.ts · Step 3: Seed Coverage & Epoch Binding · Verified
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++;}
Result: 7,700 / 7,700 bets carry the commitment and client seed of the record active when they were placed: 0 wrong hash, 0 wrong client seed, 0 outside every window. 154 epochs of 50, all revealed.
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.

  • 6MAdd35Ffi5AfC7A
  • paYStrspwMufQlJW
  • S06wBI051NtT24Q9
  • pfauditmthn4ofx
  • pfauditmthn5uvp
Result: 154 distinct client seeds across the capture (A 100, B 40, C 4, D 10), one per epoch. All 500 Phase D bets recompute under the 10 auditor seeds (Steps 5 and 17), so the server uses the seed the player sets.
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).

Nonce Incrementation — illustrated flow
tests/steps/determinism.ts · Step 7: Nonce Continuity · Verified
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++;
Result: 154 / 154 epochs carry exactly nonces 0–49: 0 gaps, 0 repeats, 0 out of range. No retry gaps declared.
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.

Deterministic Mapping — illustrated flow
src/rng.ts · drawFromKey and resultUnitsFromU · Verified
// §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);}
Result: 7,700 / 7,700 results recompute as exact 6-decimal values. Duel's own Verify-tab code (E10), run unmodified as a cross-check, gives the same result on all 7,700. Outcomes derive only from (serverSeed, clientSeed, nonce).

Real Bet Verified:

limbo-master.json · Bet #4077534 · 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]): 7a3b7d8e935810ccff93a711a217fb041231e15cc4d9d84ad83dde6982c23205

Verification:

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

tests/steps/exact.ts · Step 22: Client-Seed Influence · Verified
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;
Result: 0 of 7,700 results stay the same under a wrong client seed, and 0 of 7,700 under an empty one (bound 1.255 each). Phase D: 500 / 500 recompute (Step 5).
Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary8 checks
3 columns
Verification AreaCoverageResult
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 betsPass
Forward commitment (Step 2)158 / 158 promotions, 157 / 157 forward links + the pre-capture anchorPass
Epoch binding (Step 3)7,700 / 7,700 bets in their record's window; 154 epochs of 50, all revealedPass
Dataset verifiability (Step 4)7,700 / 7,700 bets under a revealed server seedPass
Result recomputation (Step 5)7,700 / 7,700Pass
Nonce continuity (Step 7)154 / 154 epochs carry exactly nonces 0–49Pass
Client seed variation (Step 17)500 / 500 Phase D bets under 10 auditor seedsPass
Client seed influence (Step 22)0 of 7,700 unchanged (wrong seed); 0 of 7,700 (empty seed); bound 1.255Pass
1.9Code References10 source files
2 columns
FilePurpose
tests/verify.ts30 scored steps (S1 draws on Steps 1–5, 7, 17 and 22) and the reproduction gate
tests/steps/commitment.tsSteps 1–4: commit-reveal, forward commitment, epoch binding, verifiability
tests/steps/determinism.tsSteps 5–7: result recomputation, Duel's code cross-check, negative control, nonce audit
tests/steps/dataset.tsSteps 13–17, including Step 17: player-set client seeds
tests/steps/exact.tsSteps 22–30, including Step 22: client seed influence
src/rng.tsverifyHash, drawValue and resultUnitsFromU, written from docs/ALGORITHM.md
docs/ALGORITHM.mdThe written specification the result code was built from
src/capture-plan.tsThe capture plan as code: 7,700 bets, 158 records, 154 epochs of 50
src/loader.tsDataset loading, the pinned hash and seed pairing by hash
capture/capture-auto.reference.jsReference record of the capture script (E05)
1.10Datasets UsedSource artifacts
Primary: data/limbo-master.json
2 columns
PropertyValue
SourceLive Limbo bets on Duel.com, 31 August 2026 (17:29–19:47 UTC)
Total Records7,700 bets across 154 epochs (158 seed records)
SHA-256b2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc

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
2 columns
InvariantResult
SHA-256(hexDecode(serverSeed)) opens an earlier record's commitment for all 157 revealed seedsPass
Forward commitment chain consistent over all 157 links, plus the pre-capture anchorPass
Every bet carries the commitment and client seed of the record active when it was placedPass
Every betting epoch is revealed: 7,700 / 7,700 bets verifiablePass
One client seed per epoch; nonces exactly 0–49, no gap or repeatPass
Same inputs produce the same result for all 7,700 betsPass
A wrong or empty client seed changes every resultPass
Player-set client seeds are honoured: 500 / 500 Phase D betsPass
1.12Reproduction InstructionsRun locally

Clone the repository at the release tag, install, and run the verifier:

reproduce-s1.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verify
Expected 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)
02

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.

Cryptographic Randomness Verification

7,700 / 7,700results reproduced

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
7,700 / 7,700results reproduced
  • 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
Limbo HMAC-SHA256 Pipeline — first 8 hex characters → uint32 u → 2³²·10⁶ / (u + 1), floored to 6 decimals — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce); the result takes (key, clientSeed, nonce) only (Step 5)
Written from the specificationPasssrc/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-checkPassDuel's Verify-tab code (E10), run unmodified: same result on 7,700 / 7,700 (Step 5)
Draw biasPassThe first 8 hex characters give a 32-bit value used directly: no modulo, no rejection, bias exactly 0 (Step 23)
Key encodingPassServer seed hex-decoded to 32 bytes; the text form gives different results on all 5 engine vectors (unit test)
Uniformity & serial independencePass2,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 influencePassUnder 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
Duel's Verify-tab code (E10), run unmodified: same result on 7,700 / 7,700 (Step 5)
Draw biasPass
The first 8 hex characters give a 32-bit value used directly: no modulo, no rejection, bias exactly 0 (Step 23)
Key encodingPass
Server seed hex-decoded to 32 bytes; the text form gives different results on all 5 engine vectors (unit test)
Uniformity & serial independencePass
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 influencePass
Under a wrong client seed 0 of 7,700 results stay the same; an empty seed stays within its chance bound (Step 22)
✓  Unbiased, from disclosed inputs only

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.

2 columns
ComponentDetail
Hash functionHMAC-SHA256
KeyhexDecode(serverSeed): 32 bytes
MessageclientSeed:nonce, nonce in plain decimal
ExtractionFirst 4 bytes (8 hex characters), big-endian: u from 0 to 2³² − 1
ReductionNone: no modulo, no rejection
Resultfloor(2³² × 10⁶ / (u + 1)) / 10⁶, in exact integer arithmetic
Output range1.000000× to 4,294,967,296×, served with trailing zeros removed
src/rng.ts · constants and entry points · Verified
/** 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));}
Result: Our implementation reproduces all 7,700 live results. It was written from 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:

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness (committed via SHA-256 hash before betting)
Client SeedPlayerPlayer-contributed input
NonceSystemOne per bet; rises by 1 within each epoch
No mixed entropy sources found. The same inputs always give the same result: 7,700 / 7,700.

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.
Result: Step 23 reads the draw width and the denominator from 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.

RNG Isolation — illustrated flow
Result: No state carries over between bets or epochs. Each HMAC call is keyed and messaged on its own.

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.

2 columns
MetricValue
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 screen13 / 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 populated1,000 / 1,000 (1,876 to 2,137 draws each; 2,000 expected)
Serial independence (lag-1)z −0.844, inside 3.403
src/simulate.ts · Pass 1 design · Verified
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;
Result: 13M rounds and a 2M-draw census. Every target is within its band, and the draws are consistent with a uniform, independent 32-bit value.

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.

tests/steps/simulation.ts · Step 18 serial check (from the stored sums) · Verified
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);
Result: The census check passes at the family-wise level, and the live results fit the fair law with no sign of serial dependence.
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
3 columns
StepOperationValue
1Keyhex_decode("4dcc44b245c86cdd...") → 32 bytes
2Message"mTJorxp6VuvAo0ep:4"
3HMAC-SHA25602c7e463c230f3ce14e4d974b8b4c9b866596c59ec1d52c829c5bb172c2280f8
4First 8 hex characters02c7e463 → u = 46,654,563
5Result unitsfloor(2³² × 10⁶ / 46,654,564) = 92,058,888
6Result92,058,888 / 10⁶ = 92.058888×
7Threshold91.95 / (1 − 0.001) = 92.042042…×
8Win check92.058888 ≥ 92.042042… = true
9Payout0.0125 × 91.95 = 1.149375
Parity: Bet 4085383: 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).
Live Gameresult = 92.058888
Verifierresult = 92.058888
Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Result recomputation (Step 5)7,700 / 7,700 betsPass
Spec self-test (unit test)118 / 118 vector and property checksPass
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 notPass
Draw domain & bias bound (Step 23)32-bit draw used whole: bias 0Pass
Census uniformity and serial check (Step 18)chi² p 0.6031; lag-1 z −0.844Pass
Live-sample fit (Step 30)KS p 0.2172; lag-1 z −0.230 (informational)Pass
2.9Code References9 source files
2 columns
FilePurpose
src/rng.tsHMAC-SHA256 draw and 6-decimal result, written from docs/ALGORITHM.md
tests/limbo/RngSpecTests.tsThe specification's 118 vector and property checks
src/operator-verifier.tsRuns Duel's Verify-tab code (E10) unmodified as the second check
src/simulate.tsMonte Carlo Pass 1: 2M-draw census, 13 targets × 1M rounds
src/exact.tsExact winning-draw counts over all 2³² draws
src/stats.tsChi-squared, Kolmogorov–Smirnov, lag-1 correlation, normal tails
tests/steps/determinism.tsStep 5: result recomputation and Duel's code cross-check
tests/steps/exact.tsSteps 22, 23 and 30: client seed influence, draw domain, live-sample fit
tests/steps/simulation.tsSteps 18–19: simulation re-derived and cherry-pick screen
2.10Verified Invariants8 invariants
2 columns
InvariantResult
Recomputed result matches the live result for all 7,700 betsPass
Key is hex-decoded (not the text form); the text form gives wrong resultsPass
The 32-bit draw is used whole: no modulo, no rejection, bias 0Pass
Outcomes use only server seed, client seed and noncePass
2M census draws fill all 1,000 bins and pass the uniformity screenPass
Lag-1 check passes on the censusPass
All 13 simulated targets inside the family-wise screen of their exact RTPPass
A wrong client seed changes every one of the 7,700 resultsPass
2.11Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json: the 2M-draw census and 13 targets × 1M rounds
Primary dataset: data/limbo-master.json: 7,700 live bets for result recomputation
Simulation output: outputs/simulation-results.json (E06): Pass 1 (census histogram, serial sums, 13 win counts with convergence snapshots). Pass 2 (154 revealed seeds × 10,000 nonces) is recomputed from the dataset on every run
Determinism log: outputs/determinism-log.json: 7,700 verified, 0 mismatches
2.12Reproduction InstructionsRun locally

Clone the repository at the release tag, install, and run the unit tests and the verifier:

reproduce-s2.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm test
Expected 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)
03

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.

Live ↔︎ Verifier Parity

7,700 / 7,700bets matched

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
7,700 / 7,700bets matched
  • 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
Parity Verification Flow — all 7,700 results across Phases A–D reproduced exactly — illustrated flow
7,700Live Bets Tested
100%Parity Rate
$0.0125 & $10Bet Sizes Tested
0Mismatches
CheckResultEvidence
Result recomputationPass7,700 / 7,700 results recomputed exactly, as 6-decimal integer units (Step 5)
Verifier parityPassDuel's Verify-tab code (cross-check), pinned by SHA-256, reproduces all 7,700 results (Step 5)
Negative controlPassA tampered nonce changes the result, and a served result one unit off is rejected (Step 6)
Payout correctnessPass2,965 wins equal stake × target exactly; 4,735 losses credit exactly 0; no tolerance (Step 8)
Win condition integrityPassis_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 formulaPassServed win_chance = (1 − e) / target within 10⁻¹⁸ on all 7,700 bets (Step 25)
Bet-size independencePassThe 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 coveragePass4,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
7,700 / 7,700 results recomputed exactly, as 6-decimal integer units (Step 5)
Verifier parityPass
Duel's Verify-tab code (cross-check), pinned by SHA-256, reproduces all 7,700 results (Step 5)
Negative controlPass
A tampered nonce changes the result, and a served result one unit off is rejected (Step 6)
Payout correctnessPass
2,965 wins equal stake × target exactly; 4,735 losses credit exactly 0; no tolerance (Step 8)
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
Served win_chance = (1 − e) / target within 10⁻¹⁸ on all 7,700 bets (Step 25)
Bet-size independencePass
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 coveragePass
4,094 distinct targets, 1.01× to 998.37×; Phase A fills each decade: 1,662 / 1,693 / 1,645 bets (Steps 15, 28)
✓  Live game and verifier fully aligned

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.

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

5 columns
PhaseBetsConfigBet AmountPurpose
A — Target coverage5,000Targets log-uniform from 1.01× to 1,000×, 3,787 distinct$0.0125Cover the target range and the tail
B — Edge precision2,000Target 1.01× (fixed)$0.0125Pin the house edge at the win/lose boundary
C — Elevated stake200Target 1.01× (fixed)$10.00Check the stake changes neither the result nor the price
D — Client seed variation500Log-uniform targets, 474 distinct; 10 auditor-set client seeds$0.0125Check player-set client seeds are used
4 columns
Phase B targetCaptures where the edge bracket closes to 0.1 ppMedian width95th percentile width
1.01× (chosen)78.3%0.0573 pp0.1645 pp
10×31.7%0.1430 pp0.3795 pp
100×25.5%0.1713 pp0.4614 pp
Total: 7,700 bets across 154 epochs. Total wagered: $2,093.75 ($62.50 Phase A + $25.00 Phase B + $2,000.00 Phase C + $6.25 Phase D). The second table is the design study, 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.

Result Recomputation (Steps 5–6) — illustrated flow
tests/steps/determinism.ts · Step 5: Result Determinism · Verified
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})`);  }}
Result: 7,700 / 7,700 results recomputed, 0 mismatches, 0 skipped, 0 off the 6-decimal grid. Duel's Verify-tab code: 7,700 / 7,700, equal to the served result and to ours. Step 6, bet 4077531: nonce + 1 changes the result, and 2.221402 ± 0.000001 is rejected.
Further checks: A unit test runs 5,000 fresh random seed sets through a Web Crypto implementation of Duel's formula against 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.

Payout Math (Step 8) — illustrated flow
tests/steps/payouts.ts · Step 8: Payout Math · Verified
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}`);  }}
Result: 7,700 / 7,700 payouts exact: 2,965 wins, 4,735 losses, 0 errors. 0 multiplier fields off the target, 0 target disagreements. Example: bet 4077534, stake 0.0125 at target 2.69, pays 0.033625, as served.
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 ≥.

6 columns
BetPhaseTargetResultThreshold T / (1 − e)Served
4085747A82.6582.68506982.732732…Loss
4100195A2.902.9021212.902902…Loss
4106072B1.011.0100171.011011…Loss
4116046D1.041.0402181.041041…Loss
tests/steps/exact.ts · Step 24: Win Rule at the Declared Edge · Verified
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: 7,700 / 7,700 served win flags equal 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.989108910891089109
tests/steps/exact.ts · Step 25: served win_chance vs (1 − e) / T · Verified
for (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}
Result: 7,700 / 7,700 served win chances equal (1 − e) / target to one unit in the 18th decimal (Step 25). For all 4,094 served targets the counted win probability sits within the truncation bound of that formula (worst 0.997 of the bound, at 2×). Duel's reported edge, 0.1000%, lies inside the measured bracket (0.073084%, 0.118281%] (Step 11), and all 7,700 served win chances inside the band it allows (Step 12).
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).

Result: Phase C: 200 / 200 priced exactly as base-stake bets (Step 16), 200 / 200 results recompute (Step 5), 198 wins each paid exactly stake × target (Step 8). The stake is not an input to the result.
Edge brackets: Measured separately, the Phase A bracket (0.073084%, 0.118281%] and the Phase C bracket (−0.079568%, 0.206602%] overlap, consistent with one edge at both stakes (Step 16).
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).

3 columns
DecadePhase A betsFloor
1× – 10×1,66250
10× – 100×1,69350
100× – 1,000×1,64550
tests/steps/exact.ts · Step 28: Phase A Target Coverage by Decade · Verified
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);
Result: 4,094 distinct targets across the capture, 1.01× to 998.37× (Step 15). Phase A: 3,787 distinct targets and 1,662 / 1,693 / 1,645 bets per decade; the 1.01× minimum was placed (Step 28).
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
3 columns
StepProcessOutput
1HMAC-SHA256(hexDecode(serverSeed), "S06wBI051NtT24Q9:0")32f2b1d304145aeb...
2First 8 hex characters: 0x32f2b1d3854,766,035
3units = floor(2³² × 10⁶ / 854,766,036)5,024,728
4result = 5,024,728 / 10⁶5.024728 — matches live game ✅
5Win check: 5.024728 ≥ 1.01 / 0.999true ✅
6Win chance: 0.999 / 1.010.989108910891089109 ✅
7Payout: $10 × 1.01$10.10 ✅
HMAC-SHA256 recomputation · Verified
// 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 ✅
Parity: Bet 4113793: result, win condition, win chance and payout all match exactly. The $10 bet follows the same path as a $0.0125 bet; the stake is not in the HMAC input.
Live Gameresult = 5.024728, payout = $10.10
Verifierresult = 5.024728, payout = $10.10
Technical Evidence & Verification5 sections
3.10Evidence Coverage Summary10 checks
3 columns
Verification AreaCoverageResult
Result recomputation (Step 5)7,700 / 7,700 bets, exact 6-decimal unitsPass
Operator code cross-check (Step 5)Duel's Verify-tab code: 7,700 / 7,700Pass
Negative control (Step 6)Nonce + 1 changes the result; ± 1 unit rejectedPass
Payout math (Step 8)7,700 / 7,700, exact on the 10⁻²⁰ gridPass
Win condition (Steps 10, 24)7,700 / 7,700 at the measured and the declared edgePass
Win chance formula (Step 25)7,700 / 7,700 within one unit in the 18th decimalPass
Reported edge and win chance (Steps 11, 12)0.1000% inside the measured bracket; 7,700 / 7,700 in bandPass
Phase C ($10) (Steps 5, 8, 16)200 / 200 priced as base stake; 198 wins paid exactlyPass
Target coverage (Steps 15, 28)4,094 distinct targets; each Phase A decade above its floorPass
Population (Step 14)7,700 bets, 158 seed records, phases A–D as plannedPass
3.11Code References8 source files
2 columns
FilePurpose
tests/steps/determinism.tsSteps 5–6: result recomputation, Duel's code cross-check, negative control
tests/steps/payouts.tsSteps 8–12: payout math, measured edge, win condition, reported edge and win chance
tests/steps/exact.tsSteps 24, 25 and 28: win rule at the declared edge, win chance, Phase A decades
tests/steps/dataset.tsSteps 13–17: dataset hash, population, target coverage, bet-size invariance, Phase D
src/rng.tsHMAC-SHA256 draw and 6-decimal result
src/exact.tsExact integer comparisons and winning-draw counts
src/operator-verifier.tsRuns Duel's Verify-tab code unmodified (second check)
verifier-parity/operator/run-operator-verifier.cjsThe runner that fills in the file's four input values
3.12Datasets UsedSource artifacts
Primary dataset: data/limbo-master.json: 7,700 live bets across 154 epochs
Verification output: outputs/verification-results.json: Steps 5, 6, 8, 10–12, 14–16, 24, 25 and 28
Pinned verifier: evidence/verify-tab/duel-verify-tab-limbo-2026-09-26.js (E10) and its runner in verifier-parity/operator/: the exact copies Step 5 runs
Design study: outputs/capture-power.json (E07): why Phase B bets 1.01×
3.13Verified Invariants8 invariants
2 columns
InvariantResult
Computed result matches the live result for all 7,700 betsPass
Duel's Verify-tab code agrees on all 7,700 resultsPass
Every win pays exactly stake × target; every loss pays 0Pass
is_win = (result ≥ target / (1 − e)) for all 7,700 betsPass
win_chance = (1 − e) / target to 18 decimals for all 7,700 betsPass
The multiplier field equals the target on all 7,700 betsPass
Phase C ($10) bets priced and paid like base-stake bets: 200 / 200Pass
Phase D: 500 / 500 results recompute under 10 player-set client seedsPass
3.14Reproduction InstructionsRun locally

Clone the repository at the release tag, install, and run the verifier:

reproduce-s3.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verify
Expected 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)
04

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.

Return to Player Verification

99.9%theoretical RTP · base tier

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
99.9%theoretical RTP · base tier
  • 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
INDEPENDENT DERIVATION
P(result ≥ T/(1 − e)) × T≤ 0.999
Exact count of winning uint32 draws · All 9,999,900 targets 1.01×–100,000× · Base stake tier

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

99.9%Theoretical RTP · base tier, every target
99.911%Simulated, precision-weighted (13M rounds)
0.1%Base-tier edge · RTP claim PASS
13/13Targets consistent with exact RTP
CheckResultEvidence
Anti-circularityPassWinning draws counted over all 2³² outcomes for every served target; the casino's win_chance is compared, never used (Step 25)
Theoretical RTPPass99.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)PassClaim: 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 measuredPassFrom 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 exactnessPass2,965 wins and 4,735 losses exact on the served grid (Step 8)
Simulation (Pass 1)Pass13 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)Pass154 casino seeds × 10,000 nonces: rank-sum Z 0.453, one-sided p 0.6748; the rule fires below −3.090 (Step 19)
Stake tiersInfoEvery 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
Winning draws counted over all 2³² outcomes for every served target; the casino's win_chance is compared, never used (Step 25)
Theoretical RTPPass
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 measuredPass
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 exactnessPass
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 tiersInfo
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
✓  Theoretical RTP 99.9% at the base tier, claim holds

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:

Anti-Circularity (Steps 25–26) — illustrated flow
3 columns
ComponentFormulaSource
Win probabilityW(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
Payoutstake × TPublished rule; equals every served payout (Step 8)
RTPT × W(T) / 2³²Independent count × verified payout
Result99.897673% – 99.900000% for all 9,999,900 targetsComputed exactly, not estimated
5 columns
TargetWinning draws WP(win)Nominal (1 − e) / TRTP
1.01×4,248,186,26898.910794%98.910891%99.899902%
2×2,145,335,09449.949975%49.950000%99.899950%
3.6×1,191,853,29327.749997%27.750000%99.899989%
10×429,067,1909.989999%9.990000%99.899990%
100×42,906,7220.999000%0.999000%99.899997%
1,000×4,290,6720.099900%0.099900%99.899992%
10,000×429,0670.009990%0.009990%99.899946%
36,099.45×118,8570.002767%0.002767%99.900000%
99,945.78×42,9290.001000%0.001000%99.897673%
100,000×42,9060.000999%0.000999%99.898316%
src/exact.ts · winMinUnits and winningDraws · Verified
/** 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;}
Result: 4,094 / 4,094 served targets: the counted probability sits within the truncation bound of (1 − e) / T (Step 25). All 9,999,900 legal targets: RTP between 99.897673% (99,945.78×) and 99.900000% (36,099.45×), none above 99.9% (Step 26).

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)
tests/steps/payouts.ts · Step 9: House Edge from the Win/Lose Boundary · Verified
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})`;  }}
Result: The declared 0.1% lies inside the measured bracket, and Duel's reported 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.
Stake tiers: Duel's config allows a higher edge, up to 1.0%; on our reading it applies to large potential profits. That is explained in 4.5 from the config, and not certified.
Zero Edge: The game page carries a "Zero Edge" badge while the served edge is 0.1% on every captured bet. Duel's Zero Edge rakeback is a separate wallet credit, reported in 4.9; it is not part of the game's RTP.
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.

5 columns
TargetsSettingsLowest RTPHighest RTPBeyond ±0.1 pp
1.01× – 1.99×9999.899902%99.900000%0
2.00× – 9.99×80099.899950%99.900000%0
10.00× – 99.99×9,00099.899990%99.900000%0
100.00× – 999.99×90,00099.899977%99.900000%0
1,000.00× – 9,999.99×900,00099.899768%99.900000%0
10,000.00× – 100,000.00×9,000,00199.897673%99.900000%0
Result: Every decade sits between 99.897673% and 99.900000%: 99.9% at the claim's precision, everywhere. The lowest is 99,945.78× (99.897673%); the highest, 99.900000%, is 36,099.45×.
Why the high targets dip: Near 100,000× only tens of thousands of draws win (42,929 at 99,945.78×), so one draw is worth up to 0.0023 pp of RTP. The simulated RTP by target is in 4.6.
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.

4 columns
RowsSettingsExact RTPBeyond the band
Targets 1.01×–100,000×, floor edge9,999,90099.897673% – 99.900000%0
Verifier output, as run at v1.2.1 (rtp-claim lines; the source sentence shortened) · Verified
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 pin
Result: PASS. Every target is within 0.0024 pp of 99.9%: the true minimum is 99.897673% (99,945.78×) and the true maximum 99.900000% (36,099.45×). The per-decade table is outputs/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.

3 columns
Target0.1% holds up to (stake, rounded)1% at the maximum profit (stake, rounded)
2×25,000250,000
10×2,77827,778
100×2532,525
1,000×25250
100,000×0.252.50
2 columns
Bound (Step 27)Value
Legal targets1.01× to 100,000× on the 2-decimal grid; all 7,700 captured targets inside
Result capNot stated by Duel; the formula allows up to 4,294,967,296×
Largest potential profit captured12.47 (stake × (target − 1))
Maximum profit (implied by the config)250,000
Maximum target at the smallest captured stake0.0125 × (100,000 − 1) = 1,249.99, within the 250,000 maximum profit
Minimum legal targetNot declared; 1.01× is the smallest the capture placed
Finding: A disclosed edge structure that allows a higher edge, up to 1.0%, above the certified 0.1% floor; on our reading it applies to large potential profits. Stake and target can select the edge; they never affect the result.
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.

Simulation Pass 1 — Pinned Seeds (Step 18) — illustrated flow

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

6 columns
TargetWins (of 1M)Simulated RTPExact RTPSEz
1.1×908,37099.921%99.8999%0.032 pp+0.65
1.25×798,79999.850%99.8999%0.050 pp−1.00
1.5×666,43999.966%99.9000%0.071 pp+0.93
2×499,50299.900%99.9000%0.100 pp0.00
3×333,07499.922%99.9000%0.141 pp+0.16
5×199,93299.966%99.9000%0.200 pp+0.33
10×100,064100.064%99.9000%0.300 pp+0.55
25×39,89499.735%99.9000%0.490 pp−0.34
50×20,103100.515%99.9000%0.700 pp+0.88
100×10,145101.450%99.9000%0.994 pp+1.56
1,000×97597.500%99.9000%3.16 pp−0.76
10,000×102102.000%99.8999%9.99 pp+0.21
100,000×11110.000%99.8983%31.61 pp+0.32
src/simulate.ts · Pass 1 target loop · Verified
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}`);}
Result: 13M rounds. Precision-weighted RTP 99.911%. 13 / 13 targets within the family-wise screen (worst |z| 1.56, at 100×).

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.

2 columns
MeasureValue
Seeds tested154
Nonces per seed10,000 (200 windows of 50; window 0 holds the captured bets)
Captured results replayed7,700 / 7,700 (window 0 of every epoch)
StatisticRank of the captured window's standardized return among its epoch's 200 windows, summed over epochs (exact permutation null)
Rank-sum Z0.453 (one-sided p 0.6748)
RuleCherry-picking indicated at Z < −3.090 (one-sided α 0.001, pre-registered)
Direction splitCaptured window below its epoch median in 71 epochs, above in 83
Captured-window return7,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
tests/steps/simulation.ts · Step 19 rank-sum · Verified
  // 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);
Result: The captured windows rank above the middle more often than below (83 against 71), and Z is positive. Pass 2 finds no sign of seed pre-selection.
Why is this included? Players set their own client seed at every rotation, and in the capture each seed was set after that epoch's server-seed commitment had been recorded, which makes pre-selecting server seeds against a player hard. Pass 2 adds an empirical check on the seeds Duel actually used.
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.

3 columns
MetricPhase B ($0.0125)Phase C ($10)
Target1.01×1.01×
Bets2,000200
Results verified2,000 / 2,000200 / 200
Served win chance0.9891089108910891090.989108910891089109
Wins1,981198
Payout per win0.01262510.1
Applied edge (Step 20)0.100000%0.100000%
Result: Same result code and the same price at $0.0125 and $10. On our reading of the config, stake would move the edge only above a potential profit of 25,000 (4.5); it never moves the result.
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.

3 columns
ItemValueContext
Phase A observed return95.5272%5,000 bets at $0.0125, targets 1.01×–998.37×
Phase B observed return100.0405%2,000 bets at $0.0125, target 1.01×
Phase C observed return99.9900%200 bets at $10, target 1.01×
Phase D observed return69.5040%500 bets at $0.0125, targets 1.02×–987.58×
Overall observed return99.7664%7,700 bets, $2,093.75 wagered; Phase C is most of the money
Win count2,965 against 2,943.46 expectedAt the measured edge: chi² 0.2551 (df 1), p 0.6135
Fair-multiplier tail≥ 2×: 3,916 · ≥ 10×: 811 · ≥ 100×: 76 · ≥ 1,000×: 6Against 3,850.0 · 770.0 · 77.0 · 7.7 expected under P(R ≥ x) = 1/x
Edge by target bandEvery band's bracket contains the measured edge5 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 envelopesA 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 deltaDebit = −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.02

Step 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%

3 columns
StepProcessOutput
1HMAC-SHA256 → first 8 hex charactersresult = 808.691398 ✅
2Win check: 808.691398 ≥ 769.789789…true ✅
3Payout: 0.0125 × 769.029.61275 ✅
4RTP: T × W(T) / 2³²769.02 × 5,579,402 / 2³² = 99.899986%
RTP for this bet's target · Verified
// 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.
Parity: Bet 4084905: result, win check and payout all match, the payout to the last decimal served. RTP for target 769.02 = 99.899986%, from an independent count.
Live Gameresult = 808.691398, payout = 9.61275
Verifierresult = 808.691398, payout = 9.61275
Technical Evidence & Verification5 sections
4.11Evidence Coverage Summary10 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Steps 25–26)4,094 served targets and all 9,999,900 legal targets countedPass
RTP claim (Step 26)9,999,900 targets, 0 beyond ±0.1 ppPass
House edge measured (Step 9)(0.073084%, 0.118281%], width 0.045 ppPass
Reported edge (Step 11)0.1000% inside the measured bracketPass
Applied edge (Step 20)7,700 / 7,700 at the 0.1% floorPass
Edge above the floorExplained 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,000Pass
Simulation Pass 1 (Step 18)13M rounds; 13 / 13 within the screen; census passesPass
Simulation Pass 2 (Step 19)154 seeds × 10,000 nonces; rank-sum Z 0.453Pass
Wallet envelopes (Step 29)163 / 163; rakeback 0.001 × stakePass
4.12Code References9 source files
2 columns
FilePurpose
src/exact.tsWinning-draw counts by binary search; the exact win rule
tests/steps/exact.tsSteps 24–29: win rule, win probability, every legal target, bounds, decades, wallet
tests/steps/payouts.tsSteps 8–12: payout math and the edge measured from the boundary
tests/steps/scalingedge.tsStep 20: the applied edge, exactly, on every bet
src/scaling-edge.tsConfig parameters, scaling onset and maximum profit
src/rtp-claim.tsFramework rule RTP-CLAIM 1.2.0
src/rtp-settings.tsThe flat claim, its source, and the RTP claim table
tests/steps/simulation.tsSteps 18–19: simulation re-derived and cherry-pick screen
src/simulate.tsMonte Carlo Pass 1: census, 13 targets × 1M rounds, convergence snapshots
4.13Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json (E06): Pass 1 (census and 13 targets × 1M rounds, with snapshots); Pass 2 is recomputed from the dataset on every run
RTP claim table: outputs/rtp-claim-table.json: the lowest and highest exact RTP in each decade of the 9,999,900 targets, the two extreme targets, and the claim outcome
Operator config: limboConfig.json (21 August 2026, pinned) and evidence/E12-limbo-config-2026-10-05.json (E12)
Wallet envelopes: the 163 sampled envelopes in data/limbo-master.json
RTP convergence chart: the precision-weighted RTP across the 13 targets at each depth, from outputs/simulation-results.json
4.14Verified Invariants8 invariants
2 columns
InvariantResult
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 targetsPass
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 screenPass
Pass 2 rank-sum Z above −3.090 across 154 casino seedsPass
Phase C ($10) bets follow the same result and price as $0.0125Pass
4.15Reproduction InstructionsRun locally

Clone the repository at the release tag, install, and run the verifier:

reproduce-s4.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verify
Expected 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 for
05

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

Fairness Integrity Testing

9pass1N/A

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
9pass1N/A
  • 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?
3 / 3Nonce Integrity
3 / 3Seed Commitment
1 / 1Outcome Determinism
2 / 2Player Isolation

Category Coverage

Nonce Integrity3/3
Seed Commitment3/3
Outcome Determinism1/1
Player Isolation2/2
CheckResultEvidence
Nonce integrityPassServer-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 integrityPass157 / 157 reveals (154 distinct seeds) match their commitments; 158 distinct commitments; seed pair constant within every epoch
Outcome determinismPassIdentical inputs produce identical results: 7,700 / 7,700
Round & player isolationPassEach 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
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 integrityPass
157 / 157 reveals (154 distinct seeds) match their commitments; 158 distinct commitments; seed pair constant within every epoch
Outcome determinismPass
Identical inputs produce identical results: 7,700 / 7,700
Round & player isolationPass
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)
✓  Fairness Integrity Verified

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.

3 columns
CategoryTestsWhat It Catches
Nonce Integrity3Sequence gaps, server-side nonce manipulation, session continuity
Seed Commitment3Mid-epoch seed changes, seed reuse, predictable seed generation
Outcome Determinism2Non-deterministic outputs, outcome replay
Player Isolation2Cross-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.

3 columns
SeverityMeaningAction
PASSTest passed, no issue detectedNone
N/ATest not applicable to this game typeNone
CAPTURE FLAGA gap in our own capture, not the operatorDisclosed; verdict-neutral
FLAGAnomaly detected, documented for transparencyDisclosed
HARD FAILFairness guarantee cannot be confirmedCertification blocked until remediated
2 columns
ConditionConsequence
Nonce gap or duplicate within epochOutcome sequence integrity broken
Server seed changed mid-epochCommit-reveal guarantee broken
Result recomputation mismatchUndisclosed inputs affecting outcomes
Client seed not used in HMACPlayer has no influence on outcomes
Hard fail criteria: Any single hard fail = NOT PROVABLY FAIR. The audit cannot proceed past a hard fail without operator remediation and re-verification. None was triggered in this audit.

10 tests · 9 pass · 1 N/A

Nonce Integrity3/3
Seed Commitment3/3
FI-SEED-005Pass

Server 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-002N/A

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

One 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
4 columns
Test IDCategoryVerification SourceStatus
FI-NONCE-001NonceS1, Step 7 (data-driven)Pass
FI-NONCE-002NonceCaptured requests + S1, Step 7 (data-driven)Pass
FI-NONCE-004NonceS1, Steps 3 & 7 (data-driven)Pass
FI-SEED-002SeedS1, Steps 3 & 22 (data-driven)Pass
FI-SEED-003SeedS1, Steps 1 & 3 (data-driven)Pass
FI-SEED-005SeedStatistical analysis (data-driven)Pass
FI-OUTCOME-001DeterminismS3, Step 5 (data-driven)Pass
FI-OUTCOME-002DeterminismStructural — single-step gameN/A
FI-ISO-001IsolationS2, Step 18 (simulation census)Pass
FI-ISO-002IsolationStructural — seed uniquenessPass
Method breakdown: 9 tests verified via verification suite steps, the captured requests and seed records, and structural analysis, 1 test N/A (single-step game). Limbo is a single-step game with no multi-step state, so no multi-step state checks apply.
5.4Additional Integrity Evidence (S1–S4)7 records
3 columns
PropertySourceFinding
157 / 157 reveals matchS1, Step 1Revealed seeds (154 distinct) consistent with their commitments
157 / 157 forward linksS1, Step 2Commitment chain consistent, plus the pre-capture anchor
7,700 / 7,700 exact parityS3, Step 5Every served result is the RNG output, unaltered
7,700 / 7,700 exact payoutsS3, Step 8Every win paid stake × target, every loss 0
Anti-circularity derivedS4, Steps 25–2699.9% from an exact count of winning draws, not the casino's win_chance field
Pass 2 cherry-pick testS4, Step 19Rank-sum Z = 0.453; no sign of seed pre-selection
7,700 / 7,700 client seed influenceS1, Step 22Player 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.

reproduce-s5-data.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbo && git checkout v1.2.1npm cinpm run verify
Expected 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 18

Statistical Quality Test (FI-SEED-005):

fi-seed-quality.mjs · Verified
// 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:

node fi-seed-quality.mjs
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)
Seed statistics: The script reads only 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.
06

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.

Independent Result Verification

4 Stepsto verify any bet

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
4 Stepsto verify any bet
  • 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
From Bet to Independent Verification — place a bet, rotate the seeds, recompute with the ProvablyFair.org verifier, confirm the match — illustrated flow
Server SeedRevealed after rotation
Client SeedPlayer-controlled input
NonceRecorded bet sequence
Your target and staketo check the win and the payout
  1. 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. 2

    Bet, Then Rotate

    Place your bets. When you're done, rotate the seed pair to reveal the server seed.

  3. 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. 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
6.1Step 1: Place a Limbo Bet

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

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

6.2Step 2: Open the Fairness Modal

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.

Illustrative: Provably Fair panel — active client seed, hashed server seed, nonce, new client seed, next pair commitment and the Rotate seed action.

6.3Step 3: Open Past Bet to Reveal Seed

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.

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.

6.4Step 4: Recompute & Confirm

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.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (ProvablyFair.org)
Accessduel.com/provably-fair/verifyprovablyfair.org/verify-bets/duel
Trust modelRequires trusting the casinoNot controlled by the casino
Checked by this audit: Duel's Verify-tab code (E10), run unmodified on all 7,700 captured bets as a second check: its roll equals the served result on every bet. The audit's own 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.

verify-limbo.js · Standalone Node.js
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):

verify_limbo.py · Standalone Python
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.

Example (from the config, not a recorded bet): a stake of 100 at 1,000× has a potential profit of 99,900, in the units of the config. Under that reading its edge would be 99,900 / 25,000,000 = 0.3996%, so a win would need a result of at least 1,004.012033× and would pay 100,000, as at any edge.
6.11Evidence Exhibits11 records
2 columns
EvidenceDescription
E01The live capture of 31 August 2026: 7,700 bets, 158 seed records and 163 sampled wallet envelopes (data/limbo-master.json)
E02Duel's Limbo config as recorded on 21 August 2026: the 100,000× maximum target and the scaling_edge block (limboConfig.json)
E03Five live pre-flight bets and the request and response shapes, recorded before the capture
E04Duel's published Limbo verifier, transcribed from its verify page on 18 August 2026
E05The capture script that placed the bets, with its client-seed generators and phase plan
E06Simulation Pass 1 from pinned seeds and its convergence chart
E07The capture-plan power analysis: why Phase B bets at 1.01× (design rationale)
E08The verifier's committed results: 30 scored steps and the profile coverage
E09Run summaries: the win/lose chi-squared and the recomputation census
E10Duel's Limbo Verify-tab code, copied on 26 September 2026 and run unmodified as a second check
E12Duel's Limbo config as fetched on 5 October 2026: the same values as E02
From Bet to Independent Verification — place a bet, rotate the seeds, recompute with the ProvablyFair.org verifier, confirm the match — illustrated flow
How it works
  1. 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. 2

    Bet, Then Rotate

    Place your bets. When you're done, rotate the seed pair to reveal the server seed.

  3. 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. 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
6.1Step 1: Place a Limbo Bet

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

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

6.2Step 2: Open the Fairness Modal

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.

Illustrative: Provably Fair panel — active client seed, hashed server seed, nonce, new client seed, next pair commitment and the Rotate seed action.

6.3Step 3: Open Past Bet to Reveal Seed

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.

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.

6.4Step 4: Recompute & Confirm

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.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (ProvablyFair.org)
Accessduel.com/provably-fair/verifyprovablyfair.org/verify-bets/duel
Trust modelRequires trusting the casinoNot controlled by the casino
Checked by this audit: Duel's Verify-tab code (E10), run unmodified on all 7,700 captured bets as a second check: its roll equals the served result on every bet. The audit's own 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.

verify-limbo.js · Standalone Node.js
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):

verify_limbo.py · Standalone Python
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.

Example (from the config, not a recorded bet): a stake of 100 at 1,000× has a potential profit of 99,900, in the units of the config. Under that reading its edge would be 99,900 / 25,000,000 = 0.3996%, so a win would need a result of at least 1,004.012033× and would pay 100,000, as at any edge.
6.11Evidence Exhibits11 records
2 columns
EvidenceDescription
E01The live capture of 31 August 2026: 7,700 bets, 158 seed records and 163 sampled wallet envelopes (data/limbo-master.json)
E02Duel's Limbo config as recorded on 21 August 2026: the 100,000× maximum target and the scaling_edge block (limboConfig.json)
E03Five live pre-flight bets and the request and response shapes, recorded before the capture
E04Duel's published Limbo verifier, transcribed from its verify page on 18 August 2026
E05The capture script that placed the bets, with its client-seed generators and phase plan
E06Simulation Pass 1 from pinned seeds and its convergence chart
E07The capture-plan power analysis: why Phase B bets at 1.01× (design rationale)
E08The verifier's committed results: 30 scored steps and the profile coverage
E09Run summaries: the win/lose chi-squared and the recomputation census
E10Duel's Limbo Verify-tab code, copied on 26 September 2026 and run unmodified as a second check
E12Duel's Limbo config as fetched on 5 October 2026: the same values as E02
✓  Any Player Can Reproduce Limbo Results

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.

07

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.

Open-Source Audit Repository

v1.2.1release tag

Repository Details

Prerequisites

  • Node.js 22.x (.nvmrc)
  • npm
  • Git
v1.2.1release tag
github.com/ProvablyFair-org/duel-limboRelease v1.2.1

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.

sh
git clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbogit checkout v1.2.1npm ci npm test
Expected output
43 passing Passed:     30/30Hard fails: 0Flags:      0 (0 verdict-affecting, 0 capture-origin) VERDICT: PROVABLY FAIR — Full Pass 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.json

Audit Reproducibility Pinning

Release
v1.2.1 (Framework 1.1)
Release commit
2ea4ab9b408d1a23b7951a50e12bcc359c0f0618 (21 October 2026)
Node Version
22.x (.nvmrc, package.json engines)
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
3 columns
FileContentsSize
verification-results.jsonAll 30 step results, profile coverage, verdict~37KB
determinism-log.json7,700 verified, 0 mismatches, 0 skipped57B
chi-squared-results.jsonLive win count against the measured edge (informational)213B
rtp-claim-table.jsonRTP claim: the 9,999,900 targets 1.01×–100,000× against 99.9%, by decade (PASS)~2.7KB
simulation-results.jsonPass 1: 2M-draw census + 13 targets × 1M rounds~22KB
capture-power.jsonCapture-plan power analysis (design rationale, not verdict-bearing)~6.0KB
rtp-convergence.htmlRealised 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
3 columns
StepTest NameReport Section
1Commit-Reveal Integrity (SHA-256 over hex bytes)S1
2Forward Commitment (next server seed hash honoured)S1
3Seed Coverage & Epoch Binding (every epoch revealed, one commitment and one client seed per rotation window)S1
4Dataset Verifiability (100% of bets)S1
5Result Determinism (exact equality, no tolerance)S3
6Negative Control (tampered nonce must break reproduction)S3
7Nonce Continuity (no gaps or repeats within an epoch)S5
8Payout Math (stake x target, exact on the money grid)S4
9House Edge Measured from Win/Lose BoundaryS4
10Win Condition (result >= target / (1 - measured edge))S4
11Operator-Reported Edge Cross-Check (distribution edge)S4
12Reported Win Chance Consistent with Measured Edge BracketS4
13Dataset Integrity (pinned SHA-256)S5
14Population & Phase Structure (capture plan honoured)S3
15Target Coverage (spread of chosen multipliers)S3
16Bet-Size Invariance (edge independent of stake)S3
17Client-Seed Variation (player-supplied entropy)S1, S2
18Simulation Pass-1 (pinned seeds: draw uniformity + exact-RTP convergence)S4
19Simulation Pass-2 (cherry-pick screen, 10,000 nonces per revealed seed)S5
20Scaling Edge (bet-size dependence)S4
21Artifact Integrity (pinned artifacts of record)S5, S7
22Client-Seed Influence (model-rate gate + empty-seed control)S2
23Draw Domain & Bias Bound (uint32, no range reduction)S2
24Result Range & Win Rule at the Declared Edge (exact, with boundary census)S4
25Win Probability per Served Target (exact count over 2^32 draws)S4
26Effective Edge on Every Legal Target vs Claim (exact enumeration)S4
27Target Bounds & Maximum PayoutS4
28Phase A Target Coverage by Decade (against the plan in code)S3
29Wallet Envelopes (ledger legs and rakeback, exact)S4
30Live-Sample Fit (informational)S2
reproduce-audit.sh
reproduce-audit.sh
# Clone the releasegit clone https://github.com/ProvablyFair-org/duel-limbo.gitcd duel-limbogit checkout v1.2.1npm ci   # Node 22.x, as pinned in .nvmrc # 43 unit tests, the 30 scored steps and the reproduction gatenpm test # Optional: also regenerate the 13M-round simulationnpm run test:fullnode scripts/release-manifest.mjs --check # This run's results (the committed evidence is never overwritten)cat outputs/run/verification-results.jsoncat outputs/run/diff.json
provablyfair.org/verify-bets/duel
This is an open-source audit. Every finding, dataset, and line of verification code is publicly accessible. Clone the repo at the release tag, run the tests, and check every claim yourself.
✓  Reproduced at v1.2.1

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:

2 columns
CodeMeaning
0PROVABLY FAIR — Full Pass, and this run reproduces the committed artifacts
2PROVABLY FAIR — Conditional Pass: at least one operator-origin FLAG
5RTP FIGURE OVERSTATED: the RTP claim rule (1.2.0) found a published RTP figure above the game's real RTP; the draws pass
1NOT PROVABLY FAIR (a hard fail), or a reproduction difference beyond tolerance
This run: exit 0 — PROVABLY FAIR — Full Pass, REPRODUCED.
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.

Verifier output, as run at v1.2.1 (Node 22.23.4) · Verified
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.json
7.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.

2 columns
FileSHA-256
data/limbo-master.jsonb2db8685eeafde731152bd9770bf4df9872bda626b65b3412e29b067871365cc
limboConfig.json676beff74f5a3c2e9d634a49e994acfc5adc0aa1a19fdc1ee4be36a916b07379
outputs/simulation-results.json1ab73639811451f4a75212f83fde7651fbe9bb3e974473523c2be70ed789d2ae
outputs/chi-squared-results.jsone2d83a46b05f3aa6ff0cb2f4b60f9a86b9a269b6387537a511bcab62a89048a7
outputs/determinism-log.jsoncc52b48e1bb1bc37a962b7fd90d19e7671f593cf8659637591b9c9579b581c6c
outputs/capture-power.json4aa3c304278706fb6628442fe5727416431e5457809f359770d53597f6d6a5ef
outputs/rtp-convergence.html203df3b6da90d6902ab2c9f12a2a9702033beb698555860ee0d9cfb5ed2d1ac9
outputs/verification-results.json30a0bf1383a4b09f5aca4774d38d68b63864b17749ac3c991686b9b5de606d5c
outputs/rtp-claim-table.jsone26e7b2bfbf41985e18cf8cc21c34db8d220eb1f48892a5f366e79f7c9ab939b
evidence/E12-limbo-config-2026-10-05.jsonce36245867f930f5587e4ca71d4e2ace5c082dd660c4c6574c888f8fa162eb18
evidence/E12-limbo-config-2026-10-05.json.meta.jsonb27f4a9588b68d8e4d01b2a7b443ed2e23ad77f17df3dad9c149adebb8ad5136

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:

3 columns
ReleaseWhat changedVerdict
v1.2.0Re-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 unchangedPROVABLY FAIR — Full Pass
v1.2.1Errata, 21 October 2026: adds .gitattributes so a Windows clone keeps every file byte-identical; no audit content changedUnchanged
Framework 1.1

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.

31Profile checks
24By scored steps
6By package checks
1 none · 3 partialMarked as such
4 columns
CheckCovered byCoverageWhat it covers
LIMBO-01Steps 1, 3, 4FullRevealed seeds open commitments published earlier; every betting epoch revealed; every bet under a revealed seed
LIMBO-02Step 2FullPromotions and forward links recomputed from the raw hashes on all 158 records, plus the pre-capture anchor
LIMBO-03Step 3FullEvery bet in the rotation window whose commitment and client seed it carries; epochs bound to the capture plan
LIMBO-04Step 7FullNonces exactly 0–49 in every epoch; a gap or repeat would fail
LIMBO-05Step 14FullBets, seed records, phases, epoch count and epoch size bound to the capture plan
LIMBO-06Steps 17, 22PartialClient seed influence and the Phase D custom seeds. Partial: the capture's A–C seeds came from Math.random(), not a CSPRNG
LIMBO-07Step 5FullEvery bet recomputed to its served result as exact 6-decimal units; coverage equals all 7,700 bets
LIMBO-08Step 23Full32-bit draw used directly: no modulo or rejection, bias bound exactly 0
LIMBO-09Package checkFullRNG rebuilt from the written specification; Duel's Verify-tab code run unmodified as a cross-check
LIMBO-10Steps 10, 24FullWin flag equals result ≥ target / (1 − e) on every bet; every result on the 6-decimal grid, at least 1.000000
LIMBO-11Step 8FullPayouts exact: stake × target on a win, 0 on a loss, no tolerance
LIMBO-12Step 16FullPhase C stake invariance: 200 bets at $10 priced exactly like base-stake bets; 198 elevated-stake wins
LIMBO-13Steps 8, 11, 12, 25FullServed multiplier, house edge and win chance compared with the target and the measured edge, never used
LIMBO-14Step 27FullEvery target on the 2-decimal grid within 1.01×–100,000×; the maximum profit the config implies printed
LIMBO-15Steps 15, 28Full4,094 distinct targets; Phase A spread across the planned decades
LIMBO-16Step 29PartialWallet ledger legs exact on 163 sampled envelopes. Partial: the balance field appears only on sampled envelopes, so consecutive-bet continuity isn't asserted
LIMBO-17Step 24PartialBoundary census printed. Partial: no captured result landed exactly on a threshold or at the 1.000000 floor
LIMBO-18Steps 6, 25FullNegative control; win probability counted over all 2³² draws for every served target
LIMBO-19Steps 9, 26FullEdge measured from the win/lose boundary; every legal target priced by exact count against the claim
LIMBO-20Step 18Full2M-draw census and 13 targets × 1M rounds against each target's exact RTP, re-derived from the stored counts
LIMBO-21Step 19FullPass 2 cherry-pick test on all 154 revealed seeds, on the targets each epoch served
LIMBO-22Step 30FullLive-sample fit and serial checks (informational)
LIMBO-23Package checkFullThe package’s S5 matrix, each row mapped to the scored steps
LIMBO-24—NoneRelease 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-25Package checkFullPlayer verification steps in order, with the commitment encoding
LIMBO-26Step 13FullDataset pinned by SHA-256
LIMBO-27Step 21FullPinned artifacts and value domains
LIMBO-28Package checkFullFresh-clone reproduction record (npm test and test:full)
LIMBO-29Package checkFullMutation battery: every scored step has a killing mutation
LIMBO-30Package checkFullLimitations register and load-bearing premises
LIMBO-31Step 20FullEvery bet's applied edge recovered from target × win chance, all at the 0.1% floor; the scaling onset stated
Partial coverage: LIMBO-06 is partial because the capture's own client seeds for Phases A–C came from 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).