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

Crash

Certified · Active

Independent verification, backed by open-source code.

PF-2026-DL05Audited April 2026 · Last Updated: June 2026 · Revision f97d5b8Apr 2026 · Updated Jun 2026
AUDIT RESULTPassed15 / 15 verification stepsPinned to the published
audit and dataset.
Live Bets Verified
1,100
Parity Rate
100%
Simulated Rounds
5M
Proven RTP
99.9%
15 / 15Verification steps passed

Verification pipeline

Real bets. Independently reproduced.

Placed anonymously at Duel.com. We rebuilt the game logic from scratch and matched all 1,100 recorded outcomes.

DUEL CRASH$0.01 · Auto cashout 1.10×
1.35×Cashout 1.10×1.00×1.35×
Round #830175drand 27808337 · Phase B
50 real rounds · Phase BRound 1 of 50
Exact match
1.35×

The casino result, reproduced.

Casino outcome
Crash at 1.35×
Our reproduction
Crash at 1.35×
Recorded payout
$0.0110
Open source

Run this audit yourself

Open GitHub ↗

The repo is the credential. You don't have to trust us — every finding ships as code. Run npm test to re-run the full audit: 15 scored verification steps, 5M simulated rounds, 1,100 live bets re-verified, all 1,100 drand signatures matched byte-for-byte against the public drand chain.

npm testClone & install firstView reproduction commands ↓
Crash Audit Overview

This audit independently validates the Crash game operated by Duel.com across five domains: deterministic outcome generation, entropy integrity, live-to-verifier parity, RTP mathematical accuracy, and fairness integrity testing. We placed 1,100 real bets across three capture phases and independently verified every single crash point using our own implementation of the algorithm — then re-fetched all 1,100 drand beacon signatures directly from the public drand chain to prove the operator did not fabricate external-entropy values.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPass
RTP VerifiedPass99.9% proven · 99.872% simulated (5M rounds across 5 cashout targets) · flat 0.1% distribution edge (pre-rakeback)
Live ↔︎ Verifier ParityPass100% — 1,100 / 1,100 crash points matched
Commit-Reveal SystemPassSHA-256 verified, 1,100 / 1,100 rounds — commitment chain intact across all rotations
Client SeedN/AReplaced by the drand public randomness beacon — server commits before the beacon round is published, so the entropy cannot be known at bet time
RNG AnalysisPassHMAC-SHA256 over serverSeed and drand randomness — bias-free, no hidden inputs
Payout LogicPassAll 1,100 captured amount_won values verified — bet × auto_cashout × 0.999 on wins, 0 on losses (pre-rakeback display values)
Anti-CircularityPassRTP computed from first principles for all 11 cashout targets — all ≤ 99.9%
Fairness IntegrityPass16 fairness integrity tests — 14 pass, 1 N/A (single-step game), 1 flag disclosed (payout input-validation gap — does not affect fairness)
DeterminismPassFull reproducibility confirmed
Overall StatusPass
RTP VerifiedPass
99.9% proven · 99.872% simulated (5M rounds across 5 cashout targets) · flat 0.1% distribution edge (pre-rakeback)
Live ↔︎ Verifier ParityPass
100% — 1,100 / 1,100 crash points matched
Commit-Reveal SystemPass
SHA-256 verified, 1,100 / 1,100 rounds — commitment chain intact across all rotations
Client SeedN/A
Replaced by the drand public randomness beacon — server commits before the beacon round is published, so the entropy cannot be known at bet time
RNG AnalysisPass
HMAC-SHA256 over serverSeed and drand randomness — bias-free, no hidden inputs
Payout LogicPass
All 1,100 captured amount_won values verified — bet × auto_cashout × 0.999 on wins, 0 on losses (pre-rakeback display values)
Anti-CircularityPass
RTP computed from first principles for all 11 cashout targets — all ≤ 99.9%
Fairness IntegrityPass
16 fairness integrity tests — 14 pass, 1 N/A (single-step game), 1 flag disclosed (payout input-validation gap — does not affect fairness)
DeterminismPass
Full reproducibility confirmed
Audit scope & reference

What Was Audited

✓ Audited

  • The RNG algorithm is deterministic and verifiable
  • Each round's server seed is cryptographically committed via SHA-256 before the round opens
  • Every round uses external entropy from the drand quicknet public randomness beacon as its second input
  • Server seeds are unique per round — no reuse, no epoch chain, no nonce cursor
  • Crash points are computed via HMAC-SHA256 over the server seed and drand randomness
  • Crash points are reproducible from (serverSeed, drandRandomness) — verified on all 1,100 rounds
  • Every bet was placed before its round's drand beacon was published — pre-commitment proven for all 1,100 rounds
  • Captured payout records internally consistent — amount_won = amount × auto_cashout × 0.999 on wins, 0 on losses (pre-rakeback display values; settled payout = bet × cashout)
  • Theoretical RTP is 99.9% across every cashout target (flat 0.1% distribution edge pre-rakeback, anti-circularity proven)
  • Bet amount does not influence the RNG or the crash point
  • Players can independently verify every bet using only public inputs

What Audit Excludes

  • Infrastructure or server security
  • Wallet, payments, or operational systems outside game logic
  • Rakeback layer — 99.9% is the certified pre-rakeback distribution RTP; the separate 0.001 × bet rakeback (Zero Edge, net 100.0% within daily cap) is operator-side and out of audit scope
  • Cross-account sampling
  • Max win cap enforcement — not embedded in game logic
What Audit Covers
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 seed hash committed before round, drand beacon published after commitment, server seed revealed after round
External Entropydrand quicknet beacon (chain 52db9ba7…4e971, 3-second period) — second cryptographic input to every round
Pre-Commitment TimingAuthoritative proof: bet placed before drand published, via transactions-API timestamp + drand chain formula
RNG AnalysisHMAC-SHA256 outcome derivation, uint32 threshold mapping, bias analysis
Payout LogicFlat 0.1% edge verification, bet-size invariance, win-condition logic
Live ParityIndependent crash-point recomputation vs. live game results for every captured round
RTP ValidationAnti-circularity formula proof, multi-stream simulated RTP (5M rounds), cherry-pick detection (Pass 2)
Fairness IntegrityStandard 15-test integrity matrix + drand-specific adversarial tests (timing, prediction, round shopping)
What Audit Guarantees
  • Crash points are deterministic and reproducible from (serverSeed, drandRandomness) — verified on all 1,100 live bets
  • Every bet landed on the operator's database before its round's drand beacon was published on the public chain (mean observed margin: 12.196s)
  • All 1,100 drand signatures in the dataset match signatures independently re-fetched from the public drand chain
  • The crash-point distribution follows the uint32 survival-probability formula — verified by 5M simulated rounds and Fisher's combined test
  • The house edge is a flat 0.1% at every cashout target — proven analytically from the uint32 threshold formula
  • drand round IDs are strictly increasing — monotonic, no replays or reuse (no round shopping is established by the bet-before-beacon timing, S1.x)
  • Server seeds are unique across all 1,100 rounds — no reuse, no collision
  • 14 of 16 fairness integrity tests pass; 1 N/A (replay protection — single-step game); 1 FLAG (server-side input validation gap on auto_cashout_multiplier — disclosed to Duel.com, does not affect fairness)

References

Crash — Game Rules6 sections

Crash is a live multiplier game. At the start of each round, the server commits to a server seed and pairs it with a drand public randomness beacon to determine a crash point — the multiplier at which the round will end. During the round, a multiplier ticks upward from 1.00×. Players can set an auto-cashout target before the round starts, or cash out manually at any point during the round. If the player cashes out (manually or automatically) before the crash point, the bet wins; if the crash point is reached first, the bet loses. Every player in a round shares the same server seed and drand beacon pair — the round is a single shared outcome.

How to Play

1. Place your bet — Enter a stake. You can set an auto-cashout target (e.g., 1.5×, 2×, 10×) before the round, cash out manually during the round, or both.
2. Round opens — The operator has already committed to a server seed and drand round ID for this round.
3. Betting closes — No more bets accepted for this round.
4. drand beacon publishes — The public drand chain publishes the beacon signature for the committed drand round. The server seed and drand randomness together determine the crash point.
5. Multiplier ticks — The round's multiplier rises from 1.00× on your screen.
6. Outcome — If you cash out (manually or via auto-cashout) before the crash point, you win: settled payout = `stake × cashout`. The 0.1% edge lives in the crash-point distribution itself; the in-app `amount_won` field shows the pre-rakeback display value `stake × cashout × 0.999`. If the round crashes first, you lose the stake.
7. Verify — After the round ends, take the revealed server seed, the drand signature (which you can independently fetch from api.drand.sh), and the formula and reproduce the crash point yourself.

The crash point is determined cryptographically at the moment the drand beacon publishes — before the multiplier starts ticking on your screen. The ticking animation is cosmetic; the result is fixed from the instant the drand beacon is available. Every player in a round shares the same crash point because every player's bet resolves against the same (server seed, drand beacon) pair.
Win Conditions

A Crash bet wins if the player cashes out (manually or via auto-cashout) before the round's crash point is reached. There are no partial wins — the outcome is binary.

3 columns
OutcomeConditionExample (stake $1, cashout at 2×)
WinPlayer cashed out before the crash pointCashed out at 2×, crash point = 3.5× → captured amount_won = $1 × 2 × 0.999 = $1.998 (pre-rakeback display; settled payout $2.00)
LossRound crashed before the player cashed outCrash point = 1.73× → payout = $0 (stake lost)
Instant crash (loss)Round crashes immediately at 1.00×Any bet loses; happens about 1 in 92 rounds (~1.09%)
An instant crash means the round ends the moment it starts — the multiplier never rises above 1.00×. About 1 in 92 rounds end this way (~1.09%). Every bet placed in an instant-crash round loses, regardless of cashout setting.
Risk vs Reward

Crash's risk curve is controlled by where the player chooses to cash out — either a pre-set auto-cashout target or a manual cashout during the round. Higher cashouts mean rarer wins with larger payouts; lower cashouts mean frequent small wins. The house edge stays flat across all cashout points.

  • Low cashouts (1.1×–1.5×) cash out frequently — The probability of the crash point reaching 1.5× is about 66.6%, so small-cashout strategies win often but pay little per win
  • High cashouts (10×–100×) pay rarely but large — The probability of reaching 100× is about 1%, so high-cashout strategies rely on long tail hits
  • RTP is constant — The house edge is a flat 0.1% at every cashout point. No cashout point is mathematically better or worse than any other in expected value
  • Variance scales inversely with cashout — Higher cashout points produce higher variance in the short run; short-horizon empirical RTP may swing far from 99.9% (see S4)
Parameters
3 columns
ParameterValueNotes
Minimum Crash Point1.00×Raw formula can produce <1; flooring clamps to 1.00
Maximum Crash Point~42,949,672.95×Upper bound from (2³² / 1) × 0.999
Cashout RangePlayer-controlled, above 1.00×Auto-cashout pre-set, manual cashout during round, or both
House Edge0.1% flatNo scaling — same edge at every cashout point
Theoretical RTP99.9%Formal proof from uint32 survival formula
RNG AlgorithmHMAC-SHA256Combines server seed bytes (key) with drand randomness (message)
External Entropy Sourcedrand quicknet3-second publishing period; chain 52db9ba7…4e971
Round StructureMultiplayer shared outcomeAll players in a round share one (serverSeed, drandRoundId) pair
Seed Formats

Every Crash round uses two cryptographic inputs — a server seed committed by the operator and a drand beacon from the public randomness chain. Crash does not use a player-contributed client seed; the drand beacon provides the external entropy in its place.

4 columns
Seed TypeFormatExample (round 829099)Purpose
Server Seed64-char hex (32 bytes)3c9c71aa9ffa2691…fab1d23Operator-provided randomness, revealed after the round ends
Server Seed Hash64-char hex SHA-256706bb90b571c6165…b7f4e151c0Published before round opens — commits the operator to the seed
drand Round IDInteger27798178Identifies the specific drand beacon used by this round
drand Randomness96-char hex (BLS signature)b18fa6dc92304844…1767fd56dPublic randomness, fetched from the drand quicknet chain
The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. The drand randomness is hex-decoded, then converted to a UTF-8 string (a lossy but deterministic operation) before being included in the HMAC message. Both encoding steps are verified by reproducing the crash point from the raw inputs for all 1,100 live bets in the dataset.
Multiplier Formula & Payout

Crash has no multiplier lookup table — the crash point is computed directly from a cryptographic formula, and the payout is a simple function of the player's cashout point.

uint32 value = first 4 bytes of HMAC-SHA256(serverSeed_bytes, drandRandomness:0) as integer
raw        = (2³² / (value + 1)) × 0.999
crashPoint = max(1.00, floor(raw × 100) / 100)

If crashPoint ≥ auto_cashout:   amount_won = amount × auto_cashout × 0.999
Otherwise:                       amount_won = 0
4 columns
Cashout TargetP(crash ≥ target)Payout on Win (per $1 stake)Theoretical RTP
1.5×~66.60%$1.498599.9%
~49.95%$1.99899.9%
~19.98%$4.99599.9%
10×~9.99%$9.9999.9%
50×~1.998%$49.9599.9%
Every cashout target produces the same 99.9% theoretical RTP because P(crash ≥ x) × x × 0.999 = 0.999 for any x ≥ 1 — the house edge is built into the formula, not into a table. The × 0.999 edge factor is the sole source of house advantage; removing it would yield a fair (100%-RTP) game.
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 the inputs that determine a result before the player bets
  • A source of randomness exists that the casino cannot predict or control
  • 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.

2 columns
TestDescription
Server seed commit exists before roundSHA-256 hash of server seed published before the drand beacon for the round is available
Server seed reveal matches commitSHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,100 rounds
External entropy source is publicdrand quicknet beacon — independently fetchable from api.drand.sh
Server seed uniqueness1,100 unique server seeds across 1,100 rounds — no reuse, no chain, no cursor
drand round monotonicitydrand round IDs strictly increasing across all rounds — monotonic, no replays or reordering
Full determinismSame (serverSeed, drandRandomness) → same crash point

2 columns
TestDescription
RNG depends only on server seed + drand randomnessNo client seed, no nonce, no timestamp, no hidden inputs
drand is the external entropy sourcePublic randomness beacon, cryptographically signed, independently verifiable
No mixed entropy sourcesNo Math.random, no system clocks, no other RNG paths
drand signature authenticity1,100 / 1,100 drand signatures in the dataset match signatures fetched directly from api.drand.sh

2 columns
TestDescription
Bet placed before drand publishedFor every round, the operator's signed timestamp_raw from the transactions API is earlier than the drand beacon's publish time (computed from the public chain formula)
Timing proof uses authoritative sourcesBoth endpoints of the margin are independently re-derivable — no trusted auditor clock
Every round satisfies the inequality1,100 / 1,100 rounds with positive pre-commitment margin (minimum: 0.404s)

2 columns
TestDescription
Live outcomes match verifier1,100 / 1,100 crash points recomputed — 0 mismatches
Multi-phase verificationPhase A (baseline 1.5×), Phase B (varied cashout targets), Phase C ($10 stakes)
Bet-size invariance$10 bets produce the same crash points as $0.01 bets

2 columns
TestDescription
Anti-circularity proofRTP computed directly from the uint32 survival formula for 11 cashout targets — all ≤ 99.9%
Distribution edge audit (pre-rakeback)Flat 0.1% confirmed across all cashout targets — no scaling edge
Payout rules correctnessamount_won = amount × auto_cashout × 0.999 on wins, 0 on losses, for all 1,100 bets (pre-rakeback display values)
Simulated RTP convergence5M rounds (10 streams × 500K) converge on theoretical 99.9% via Fisher's combined test (p = 0.5382, 0/10 streams below α = 0.01)
Cherry-pick detectionPass 2 — 14 chi² fails across 1,100 casino seeds (threshold 21 under H₀) — no evidence of seed pre-selection
High-Level Flow

To get an overview of how a single Crash round works, here is a high-level breakdown:

1. Round Opens — Operator commits to a server seed (publishes its SHA-256 hash) and assigns the round a drand round ID from the upcoming quicknet schedule
2. Player Bets — Player enters stake and auto-cashout target; bet is accepted before the committed drand round publishes
3. Betting Closes — No more bets for this round
4. drand Publishes — The drand quicknet chain publishes the beacon for the committed round ID (BLS signature, ~3 seconds after the previous beacon)
5. Crash Point Computed — `HMAC-SHA256(serverSeed_bytes, drand_utf8:0)` → first 4 bytes as uint32 → `(2³² / (value+1)) × 0.999` → floor to 2 decimals
6. Multiplier Ticks — On-screen multiplier rises from 1.00× toward the computed crash point
7. Settlement — If `crashPoint ≥ auto_cashout` the bet wins; captured `amount_won` shows `stake × auto_cashout × 0.999` (pre-rakeback display). Otherwise stake lost.
8. Reveal — Operator reveals the plaintext server seed; player can recompute the crash point from (serverSeed, drandRandomness) and verify

High-Level Flow — illustrated flow
Provably Fair Model

Provably fair gambling systems use cryptographic primitives to guarantee the integrity of outcomes. Crash's model relies on three components: a server seed committed via SHA-256 hash before the round's external-entropy source is available, a public randomness beacon (drand quicknet) whose publication time is independently verifiable, and HMAC-SHA256 as the deterministic function that combines them into a crash point. Because the operator commits to the server seed before the drand beacon for the round is available, and because anyone can independently verify both the SHA-256 commitment and the drand signature, the operator cannot retroactively choose a server seed to produce a favorable outcome.

Commit-Reveal Model

The Commit-Reveal model for Crash spans four phases, with an additional external-entropy step sitting between the commitment and the reveal:

Commit-Reveal Model — illustrated flow

Commit Phase:
Before the round's drand beacon is available, the operator generates a server seed and publishes its SHA-256 hash (`serverSeedHash`) alongside the round's assigned drand round ID. Only the hash is sent to the player — the seed itself stays hidden. Bets are accepted during this phase.

drand Phase:
The drand quicknet chain publishes the beacon for the committed drand round on its fixed 3-second schedule. This step is independent of both the operator and the player — drand's publishing time and content are cryptographically fixed and cannot be manipulated by either party.

Compute Phase:
Once the drand beacon publishes, the operator combines the server seed with the drand randomness via HMAC-SHA256 to produce the crash point. The on-screen multiplier begins ticking.

Reveal Phase:
After the round ends, the operator reveals the plaintext server seed. Any player can now verify `SHA-256(hex_decode(serverSeed)) = serverSeedHash` and recompute the crash point from (serverSeed, drandRandomness). Every subsequent round uses a fresh server seed — there is no seed chain or epoch.

Why Crash Uses a Public Randomness Beacon

Client-seed games (like Duel's Keno and Mines) require the player to supply an input the casino cannot predict — the client seed. This gives the player direct control over the unpredictable half of the RNG input. Crash takes a different approach: instead of a per-player client seed, it uses a public randomness beacon (drand quicknet) as the second cryptographic input. drand publishes a new randomness value every 3 seconds, signed by a distributed threshold of independent validators, on a fixed schedule. Because the beacon is public, everyone — including the operator — gets the same value, and no party can predict or bias it. This architecture produces the same cryptographic guarantee as a client seed:

  • The operator commits to the server seed before the drand beacon for the round is available — so the operator cannot pick a seed that pairs favorably with the beacon
  • drand is signed by a BLS threshold of validators distributed across multiple organizations — no single party can forge or withhold a beacon
  • Any player can fetch the drand beacon directly from api.drand.sh and verify it matches the signature the operator claimed to use
  • Every player in the same round shares the same (server seed, drand beacon) pair — the round has one shared outcome, not per-player outcomes
Why There's No Nonce or Epoch Chain

In client-seed games, the casino typically uses a single server seed for many rounds, incrementing a nonce counter per round to produce different outcomes. The seed is only rotated when the player requests it — ending the epoch and revealing the old seed. Crash takes a simpler approach: every round gets a brand-new server seed, and there is no nonce counter.

  • Each round has exactly one server seed — no reuse, no chain, no cursor
  • The nonce is effectively always 0 (fixed in the HMAC message) because every seed is already unique
  • There is no epoch to rotate — the "reveal" happens after each round rather than after N rounds
  • The 1,100-round dataset contains 1,100 unique server seeds and 1,100 unique server seed hashes — verified by Step 4
Why There's No Nonce or Epoch Chain
Round N:      serverSeed_N (unique), drandRoundId_N → crashPoint_N              [seed revealed after round ends] Round N+1:    serverSeed_(N+1) (new, unrelated), drandRoundId_(N+1) → crashPoint_(N+1)              [seed revealed after round ends] ... Every round is a fresh commitment. There is no N-th nonce — every HMACmessage embeds the fixed suffix ":0".
Determinism Guarantee

Given identical inputs, the output is always identical:

Determinism Guarantee
HMAC-SHA256(hexDecode(serverSeed), drandRandomness_utf8 + ":0")  → Always same hashFirst 4 bytes of hash as uint32                                   → Always same value(2³² / (value + 1)) × 0.999                                       → Always same rawmax(1.00, floor(raw × 100) / 100)                                 → Always same crashPointcrashPoint ≥ auto_cashout                                         → Always same win/lossstake × auto_cashout × 0.999                                      → Always same payout
Technical Glossary5 categories
Core Concepts
2 columns
TermDefinition
Provably FairA gambling model in which the inputs to every outcome are cryptographically committed before the outcome is known, and any player can independently verify the outcome after the fact without trusting the operator.
Commit-Reveal ProtocolA protocol in which one party publishes the hash of a secret before any dependent action, then reveals the secret afterward. In Crash, the operator commits to the server seed (via SHA-256) before the round's drand beacon is available, and reveals the seed after the round ends.
External Entropy SourceA source of randomness outside the operator's control that contributes to outcome generation. Crash uses the drand quicknet public randomness beacon as its external entropy source.
DeterminismThe property that identical inputs always produce identical outputs. In Crash, any (serverSeed, drandRandomness) pair deterministically produces exactly one crash point.
Seed System
2 columns
TermDefinition
Server SeedA 64-character hex string (32 bytes) generated by the operator. Hex-decoded to raw bytes before use as the HMAC key. Each Crash round uses a unique server seed.
Server Seed HashThe SHA-256 hash of the hex-decoded server seed bytes. Published by the operator before the round's drand beacon is available — commits the operator to the seed.
drand Round IDAn integer identifying a specific beacon in the drand quicknet chain. Committed by the operator at round start; the beacon for that round publishes on drand's fixed schedule.
drand RandomnessThe 96-character hex BLS signature published by the drand quicknet chain for a given round ID. Used as the HMAC message (after hex→bytes→UTF-8 conversion) together with the :0 suffix.
Single-Use Seed ModelCrash's seed-rotation pattern: every round gets a fresh server seed (no reuse, no chain, no nonce counter). The effective nonce is always 0 — embedded as a fixed :0 suffix in the HMAC message.
Cryptographic Functions
2 columns
TermDefinition
HMAC-SHA256Keyed hash function that combines the server seed bytes (key) with the drand randomness and :0 nonce (message) to produce a 32-byte hash. The first 4 bytes are interpreted as a uint32 to derive the crash point.
SHA-256Cryptographic hash function used for the commit-reveal proof. SHA-256(hex_decode(serverSeed)) = serverSeedHash is checked on every round.
BLS Threshold SignatureThe signature scheme used by the drand quicknet chain. A drand beacon is a BLS signature produced by a threshold-quorum of independent validators — no single validator can forge or withhold a beacon.
Hex DecodingConverting a hex-encoded string to raw bytes. The server seed is hex-decoded before being used as the HMAC key; the drand randomness is hex-decoded before being converted to a UTF-8 string for the HMAC message.
uint32 Threshold MappingThe method by which the HMAC output is transformed into a crash point. The first 4 hash bytes are interpreted as an unsigned 32-bit integer value; the crash point is max(1.00, floor((2³² / (value+1)) × 0.999 × 100) / 100). This mapping is bias-free — every uint32 value has exactly one corresponding crash point.
Verification Terms
2 columns
TermDefinition
VerifierAn independent implementation of the RNG and payout logic used to recompute live-game outcomes from captured inputs. A verifier must produce identical results to the live game on every round.
ParityThe property that the verifier's computed crash point equals the live game's reported crash point for every round. 1,100 / 1,100 parity means zero mismatches across the entire dataset.
Anti-CircularityVerification that the RTP is not derived from the same table used to compute payouts. For Crash this is formula-based — the uint32 survival-probability formula P(crash ≥ x) = floor(2³² × 0.999 / x) / 2³² is independently evaluated for 11 cashout targets, all yielding ≤ 99.9% RTP.
Survival Probability FormulaThe analytical formula P(crash ≥ x) = floor(2³² × 0.999 / x) / 2³² which gives the probability that a Crash round's crash point reaches at least x. Multiplied by x, it yields the theoretical RTP at cashout target x — always ≤ 99.9%. This is the proof technique Crash uses for anti-circularity (analogous to the Hypergeometric Distribution used by Keno or the Combinatorial Identity used by Mines).
Game Mechanics
2 columns
TermDefinition
Crash PointThe multiplier at which a Crash round ends. Minimum 1.00× (instant crash), theoretical maximum ~42,949,672.95×. Computed from the uint32 value derived from HMAC-SHA256.
Auto-CashoutThe multiplier target at which a player's bet automatically settles. If the crash point reaches or exceeds this target, the bet wins stake × auto_cashout × 0.999; otherwise the bet loses.
Instant CrashA round with crashPoint = 1.00 — the minimum possible value. Theoretical probability 1.0891%. Any auto-cashout target above 1.00× loses on an instant-crash round.
Shared RoundA single round's (server seed, drand beacon) pair is shared across every player who bets into it. Every player in the round sees the same crash point. The operator commits to the pair at round-open time, before any individual bet is placed.
Multi-Stream Chi-SquaredThe statistical methodology used in S4 Pass 1. Because Crash produces a continuous distribution with no natural bins, a single long chi² test is vulnerable to Monte Carlo variance in individual tail bins. Running 10 independent 500K-round streams and combining their p-values via Fisher's method (R.A. Fisher, 1925) restores the variance-dilution that per-config chi² gives to discrete games like Keno or Plinko.
01

Commit-Reveal & Pre-Commitment Timing

Can the casino change your outcome after you bet?

Every Crash round on Duel.com is generated from two cryptographic inputs: a server seed committed by the operator and a drand beacon published by the public randomness chain. The operator commits to its server seed by publishing a SHA-256 hash before the round's drand beacon is available — and crucially, before any bet is placed. After the round ends, the server reveals the actual seed, and anyone can verify that the hash matches. This cryptographic commitment, paired with the external drand beacon, makes it impossible for the operator to secretly change your outcome after you bet.

Dual-Entropy Commit-Reveal Guarantee

1,100 / 1,100rounds verified

What we verified

  • Casino commits to the server seed hash before the round's drand beacon is available
  • Every bet was placed before its round's drand beacon was published on the public chain (mean margin 12.196s)
  • drand round IDs are strictly increasing across all 1,100 rounds — monotonic, no replays or reordering
  • Server seeds are unique per round — no reuse, no chain, no nonce cursor
  • Crash points are fully determined by (serverSeed, drandRandomness) before the multiplier animation plays
  • Identical inputs always produce the same crash point — confirmed across all 1,100 bets

What this means for you

  • The casino cannot change the crash point after you bet
  • The external entropy source (drand) publishes on a fixed public schedule outside the operator's control
  • Every bet is unique — fresh server seed per round, no reuse, no cursor
  • Any result can be independently verified using only public inputs
  • Outcomes are tamper-proof and verifiable even months later
  • Cherry-picking favourable (seed, beacon) pairs is structurally impossible
1,100 / 1,100rounds verified
  • Casino commits to the server seed hash before the round's drand beacon is available
  • Every bet was placed before its round's drand beacon was published on the public chain (mean margin 12.196s)
  • drand round IDs are strictly increasing across all 1,100 rounds — monotonic, no replays or reordering
  • Server seeds are unique per round — no reuse, no chain, no nonce cursor
  • Crash points are fully determined by (serverSeed, drandRandomness) before the multiplier animation plays
  • Identical inputs always produce the same crash point — confirmed across all 1,100 bets
Commit-Reveal Pipeline — server seed hash committed before drand beacon publishes, both combine via HMAC-SHA256 to produce the crash point — illustrated flow
CheckResultEvidence
Server seed committed before roundPassSHA-256 hash of server seed published before the round's drand beacon is available — casino cannot change randomness after betting
drand pre-commitment timingPass1,100 / 1,100 bets placed before drand beacon publication (min margin 0.404s, mean 12.196s; authoritative transactions-API timestamp + drand chain formula)
drand round monotonicityPassdrand round IDs strictly increasing across all 1,100 rounds — 0 reuse, 0 replays (monotonic)
Server seed uniquenessPass1,100 unique server seeds and 1,100 unique hashes across 1,100 rounds — no reuse, no chain, no cursor
Seed hash integrityPassSHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,100 revealed seeds — commitment intact
Deterministic outputPassSame (serverSeed, drandRandomness) always produces same crash point — 1,100 / 1,100 confirmed
Bet-size invariancePassPhase C verifies under the identical RNG code path — crash point is independent of bet amount
Server seed committed before roundPass
SHA-256 hash of server seed published before the round's drand beacon is available — casino cannot change randomness after betting
drand pre-commitment timingPass
1,100 / 1,100 bets placed before drand beacon publication (min margin 0.404s, mean 12.196s; authoritative transactions-API timestamp + drand chain formula)
drand round monotonicityPass
drand round IDs strictly increasing across all 1,100 rounds — 0 reuse, 0 replays (monotonic)
Server seed uniquenessPass
1,100 unique server seeds and 1,100 unique hashes across 1,100 rounds — no reuse, no chain, no cursor
Seed hash integrityPass
SHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,100 revealed seeds — commitment intact
Deterministic outputPass
Same (serverSeed, drandRandomness) always produces same crash point — 1,100 / 1,100 confirmed
Bet-size invariancePass
Phase C verifies under the identical RNG code path — crash point is independent of bet amount
✓  Commit-reveal and pre-commitment timing verified

All 1,100 revealed server seeds hash-verified. Every bet landed on the operator's database before its round's drand beacon was published on the public chain, with a minimum margin of 0.404 seconds — proven from the operator's signed transactions-API timestamp and the public drand chain formula. Outcomes are fully deterministic — the same server seed and drand randomness always produce the same crash point. The casino cannot change your result after you bet.

How It Works — Commit-Reveal & Pre-Commitment Timing6 sections
1.1Server Seed Commitment

Before any player places a bet on a round, the operator generates a secret server seed and publicly commits to it by displaying its SHA-256 hash. The server seed is a 64-character hex string (32 bytes), and the commitment hash is computed as SHA-256(hex_decode(serverSeed)). This cryptographic commitment locks the operator's half of the RNG input before the round's drand beacon is available. After the round ends, the actual server seed is revealed — and anyone can verify that the hash matches.

Server Seed Commitment — illustrated flow
src/rng.ts · verifyHash · Verified
export function verifyHash(serverSeed: string, serverSeedHash: string): boolean {  const seedBytes = Buffer.from(serverSeed, 'hex');  const computed  = crypto.createHash('sha256').update(seedBytes).digest('hex');  return computed === serverSeedHash;}
Result: 1,100 / 1,100 revealed seeds hash-verified. Zero mismatches.

Real Example from Live Data:

crash-master-1100rounds.json · round 829099
{  "roundId": 829099,  "serverSeed": "3c9c71aa9ffa2691d29fd754cb0caf8ce1fa87d57804196967de81088fab1d23",  "serverSeedHash": "706bb90b571c6165c96a484580efd2f834b4f3237e7cf0f2680b26b7f4e151c0",  "drandRoundId": 27798178,  "drandRandomness": "b18fa6dc92304844fc4e868d708953e6b628b3f88d4381249d32264df1bfe5bc20aa0905e57fe54063010881767fd56d",  "crashPoint": 2.81}

Verification:

verify-seed.js · Verified
const crypto = require('crypto'); const serverSeed = "3c9c71aa9ffa2691d29fd754cb0caf8ce1fa87d57804196967de81088fab1d23"; const serverSeedHash = crypto  .createHash("sha256")  .update(Buffer.from(serverSeed, 'hex'))  .digest("hex"); console.log(serverSeedHash);// Output: 706bb90b571c6165c96a484580efd2f834b4f3237e7cf0f2680b26b7f4e151c0 ✅
1.2drand Pre-Commitment Timing Proof

For every round, ProvablyFair.org proves the operator accepted the player's bet before the drand beacon for that round was published on the public chain. This is the foundational guarantee of the dual-entropy model — without it, the operator could wait to see the drand output and then craft a favourable server seed to pair with it. The proof uses two authoritative, independently-verifiable timestamps: the operator's signed transactions-API record of when the bet landed in their database, and the drand chain formula that gives the exact UTC publishing time of any drand round. Both sides are re-derivable by any third party from public sources — no trusted auditor clock is involved.

drand Pre-Commitment Timing Proof — illustrated flow
tests/steps/commitment.ts · Step 2 · Verified
// Step 2: drand Commitment Timing (pre-commitment)// Recomputes the margin from raw authoritative fields — drandPublishedAt// (seconds, from drand chain formula) and betPlacedAt (milliseconds, from// transactions API). Does NOT rely on the precomputed commitmentBeforeDrand field.let positive = 0;let nonPositive = 0;for (const r of rounds) {  const cbd = r.timing.drandPublishedAt * 1000 - r.timing.betPlacedAt;  if (cbd > 0) positive++;  else nonPositive++;}
Result: 1,100 / 1,100 rounds passed. Minimum margin: 0.404 seconds. Mean margin: 12.196 seconds. Maximum margin: 13.779 seconds. Zero pre-commitment violations across the full dataset.

Real Example — round 829099 timestamps:

crash-master-1100rounds.json · round 829099 · timing
{  "roundId": 829099,  "drandRoundId": 27798178,  "betPlacedAt":          1776197885172,  "betPlacedAtISO":       "2026-04-14T20:18:05.000000Z",  "drandPublishedAt":     1776197898,  "drandPublishedAtISO":  "2026-04-14T20:18:18.000Z",  "commitmentBeforeDrand": 12828,  "source":               "transactions-api"}

Verification:

verify-timing.js · Verified
// Both endpoints are authoritative://   betPlacedAt      — operator's signed transactions-API record//   drandPublishedAt — computed locally from drand chain formula://                        genesis + (drandRoundId - 1) × 3//                        genesis = 1692803367  (2023-08-23T12:02:47Z)//                        period  = 3 seconds   (quicknet) const QUICKNET_GENESIS = 1692803367;const QUICKNET_PERIOD  = 3; const drandPublishedAt = QUICKNET_GENESIS + (27798178 - 1) * QUICKNET_PERIOD;// → 1776197898 (2026-04-14T20:18:18.000Z) ✅ const margin_ms = drandPublishedAt * 1000 - 1776197885172;// → 12828 ms (12.828 seconds before drand published) ✅

Why bet-level timing, not round-commit timing? Duel's operator API does not expose a server-signed timestamp for the exact moment a round is committed — when the server seed hash and drand round ID are announced. The strongest authoritative timestamp it does expose is timestamp_raw on each individual bet — the millisecond-precision, operator-database-signed record of when the wager was accepted. A weaker alternative would be using the browser-captured WebSocket message time, but that depends on the auditor's local clock being trustworthy, which this methodology is explicitly designed to avoid. Using the bet timestamp is the strongest proof available without introducing a trusted-clock dependency. Because a single round is shared across every player who bets into it — and the operator commits to the (seed, drand round) pair at round-open time, before any bet lands — the bet-level margin reported here is a lower bound on the true round-level commitment margin. The actual commitment happens earlier; we prove pre-commitment at the bet level because that's what we can authoritatively measure.

1.3drand Round Offset Distribution

When the operator opens a new Crash round, they commit to a drand round ID that will publish a few seconds in the future. The typical pattern observed across the 1,100-round dataset is current drand round + 4 or + 5 — giving the round roughly 12–15 seconds between commitment and drand publication. This is the window during which players place bets. Every single round in the dataset has a strictly positive pre-commitment margin; the distribution of drand-round offsets shows the operator consistently targeted a 12–15 second window, with one minor exception.

4 columns
drand Round OffsetRound CountShareTypical Margin
+ 564458.5%~13–15 s
+ 445541.4%~9–13 s
+ 110.1%0.404 s (single outlier — see below)
Result: 1,099 / 1,100 rounds fall into the normal + 4 / + 5 offset pattern. 1 round (0.1%) has a tighter + 1 offset. Every round — including the outlier — still has a strictly positive margin, meaning every bet beat its drand beacon to publication. Scored Step 2 evaluates the margin sign (pre-commit vs. post-commit), not the offset size; all 1,100 rounds pass.

The single tight-margin round. One round (829188) had a commitment margin of 0.404 seconds — tight, but positive: the bet landed before its drand beacon was published, so the pre-commitment guarantee held.

1.4Server Seed Uniqueness & Single-Use Model

Crash does not use seed epochs or a nonce cursor. Every round gets a brand-new server seed, committed before the round opens and revealed after the round ends. There is no seed promotion chain, no nonce increment, and no "end the epoch" action. This single-use seed model means every bet has its own fresh cryptographic commitment — the operator cannot carry a seed across rounds to influence future outcomes. Across the 1,100-round dataset, all 1,100 server seeds and all 1,100 server seed hashes are unique.

tests/steps/commitment.ts · Step 4 · Verified
// Step 4: Server Seed Uniquenessconst seeds  = new Set(rounds.map(r => r.result.serverSeed));const hashes = new Set(rounds.map(r => r.result.serverSeedHash));const allUnique = seeds.size === rounds.length && hashes.size === rounds.length;
Result: 1,100 unique server seeds and 1,100 unique server seed hashes across 1,100 rounds. Zero reuse, zero collision.
1.5drand Round Monotonicity

drand round IDs are strictly increasing across all 1,100 rounds (monotonic — no replays or reordering). The stronger no-round-shopping property is established by the bet-before-beacon timing: the operator cannot reuse an already-published drand beacon to craft a favourable outcome for a subsequent round, and cannot skip backward to cherry-pick beacons from past rounds. Combined with the pre-commitment timing proof (subsection 1.2), monotonicity closes the loop: every round uses a fresh, future drand beacon that neither side has seen.

tests/steps/commitment.ts · Step 3 · Verified
// Step 3: drand Round Monotonicityfor (let i = 1; i < rounds.length; i++) {  if (rounds[i].result.drandRoundId <= rounds[i - 1].result.drandRoundId) {    violations++;  }}
Result: 0 violations across 1,100 rounds. drand round range: 27,798,178 → 27,819,827 (monotonically increasing across the capture window).
1.6Deterministic Mapping

The RNG algorithm is fully deterministic: given the same server seed and drand randomness, it always produces the exact same crash point. The algorithm is a single HMAC-SHA256 computation followed by a uint32 threshold mapping. The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. The drand randomness is hex-decoded to bytes, then converted to a UTF-8 string (a lossy but deterministic operation that matches the operator's server-side encoding). The HMAC message is {drand_utf8}:0 — the :0 suffix is a fixed nonce because every seed is already single-use. The first 4 bytes of the resulting hash are interpreted as a uint32, which maps to the crash point via (2³² / (value + 1)) × 0.999, floored to two decimal places and clamped at 1.00.

Deterministic Mapping — illustrated flow
src/rng.ts · computeCrashPoint · Verified
export function computeCrashPointRaw(serverSeed: string, drandSeed: string): number {  const keyBuffer = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandSeed, 'hex');  const randomness = drandBytes.toString('utf-8');  const message = `${randomness}:0`;   const hmac = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);   const result = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE);  return Math.max(1.0, result);} export function computeCrashPoint(serverSeed: string, drandSeed: string): number {  const raw = computeCrashPointRaw(serverSeed, drandSeed);  return Math.floor(raw * 100) / 100;}
Result: All 1,100 bets with revealed seeds: HMAC-SHA256 recompute matches crashPoint. Zero mismatches. Bet-size invariance confirmed — all 100 Phase C rounds at $10 stake verified using the identical RNG code path as the $0.01 rounds in other phases.

Real Bet Verified:

crash-master-1100rounds.json · round 829099 · Verified
// Source: data/crash-master-1100rounds.json// Round: 829099 (Phase A, auto_cashout 1.5×, stake $0.01)// ✅ VERIFIED — crash point recomputed from (serverSeed, drandRandomness) {  "roundId": 829099,  "serverSeed": "3c9c71aa9ffa2691d29fd754cb0caf8ce1fa87d57804196967de81088fab1d23",  "serverSeedHash": "706bb90b571c6165c96a484580efd2f834b4f3237e7cf0f2680b26b7f4e151c0",  "drandRoundId": 27798178,  "drandRandomness": "b18fa6dc92304844fc4e868d708953e6b628b3f88d4381249d32264df1bfe5bc20aa0905e57fe54063010881767fd56d",  "crashPoint": 2.81,  "isWin": true,  "amountWon": "0.014985"}

Verification:

verify-crash.js · Verified
// computeCrashPoint("3c9c71aa...", "b18fa6dc...")//// 1. key     = Buffer.from("3c9c71aa...", 'hex')       // 32 bytes// 2. drand   = Buffer.from("b18fa6dc...", 'hex')       // 48 bytes//              .toString('utf-8')                       // lossy but deterministic// 3. message = drand_utf8 + ":0"                        // fixed nonce// 4. hmac    = HMAC-SHA256(key, message).digest('hex')  // 32 bytes = 64 hex// 5. value   = parseInt(hmac.slice(0, 8), 16)           // first 4 bytes → uint32// 6. raw     = (2³² / (value + 1)) × 0.999// 7. crash   = max(1.00, floor(raw × 100) / 100)//// Output: 2.81 ✅// Payout: 0.01 × 1.5 × 0.999 = 0.014985 ✅
Technical Evidence & Verification5 sections
1.7Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Seed hash integrity (Step 1)1,100 / 1,100 revealed seeds hash-verifiedPass
drand commitment timing (Step 2)1,100 / 1,100 rounds pre-commit (min 0.404s)Pass
drand round monotonicity (Step 3)0 violations across 1,100 roundsPass
Server seed uniqueness (Step 4)1,100 unique seeds, 1,100 unique hashesPass
Outcome recomputation (Step 5)1,100 / 1,100 crash pointsPass
Bet-size invariance (Step 6)100 / 100 Phase C ($10) roundsPass
drand chain formula (Step 11)1,100 / 1,100 drandPublishedAt values match formulaPass
1.8Code References8 source files
2 columns
FilePurpose
tests/verify.ts15-step verification pipeline (Steps 1–6 and 11 cover S1)
tests/steps/commitment.tsSteps 1–4: seed hash, pre-commit timing, drand monotonicity, seed uniqueness
tests/steps/determinism.tsSteps 5–6: crash-point recomputation, bet-size invariance
tests/steps/dataset.tsStep 11: drand chain formula verification
src/rng.tsHMAC-SHA256 + uint32 threshold mapping (computeCrashPoint, verifyHash)
src/verify-drand-timing.jsdrand pre-commit timing check + chain formula verification
src/loader.tsDataset loading, SHA-256 pre-flight check, field parsing
capture/crash-capture.reference.jsBrowser + transactions-API capture script
1.9Datasets UsedSource artifacts
Primary: data/crash-master-1100rounds.json
2 columns
PropertyValue
SourceLive Crash round data from Duel.com
Total Records1,100 rounds (Phase A: 800, Phase B: 200, Phase C: 100)
drand Round Range27,798,178 → 27,819,827 (quicknet)
Capture Window2026-04-14T20:17:12.676Z → 2026-04-15T14:21:25.365Z
SHA-256ea62337a8665842ad33341a7d1b435d0c986f9feb3b03609cb0b0c868f3ef58a

Fields used: roundId, phase, request.amount, request.auto_cashout, result.crashPoint, result.serverSeed, result.serverSeedHash, result.drandRoundId, result.drandRandomness, result.amountWon, result.isWin, timing.betPlacedAt, timing.drandPublishedAt, timing.commitmentBeforeDrand, timing.source

1.10Verified Invariants8 invariants
2 columns
InvariantResult
SHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,100 revealed seedsPass
drandPublishedAt × 1000 − betPlacedAt > 0 for all 1,100 rounds (pre-commit proof)Pass
drandPublishedAt = genesis + (drandRoundId − 1) × 3 for all 1,100 rounds (chain formula)Pass
drand round IDs strictly increasing across all 1,100 roundsPass
1,100 unique server seeds and 1,100 unique server seed hashes across 1,100 roundsPass
computeCrashPoint(serverSeed, drandRandomness) = crashPoint for all 1,100 roundsPass
Phase C ($10 stakes) verify under the identical RNG code path as Phase A ($0.01) — bet amount is not an RNG inputPass
timing.source = "transactions-api" for all 1,100 rounds (authoritative timestamp, not browser clock)Pass
1.11Reproduction InstructionsRun locally

Clone the repository, install dependencies, and run the verification suite:

reproduce-s1.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crash && npm installnpm run verify# Expected output: Steps 1–6 and 11 all PASS
S1-related steps (all reproducible offline from pinned inputs):

[PASS] Step 1   — Seed Hash Integrity
[PASS] Step 2   — drand Commitment Timing (Pre-Commitment)
[PASS] Step 3   — drand Round Monotonicity
[PASS] Step 4   — Server Seed Uniqueness
[PASS] Step 5   — Outcome Recomputation
[PASS] Step 6   — Bet-Size Invariance
[PASS] Step 11  — drand Chain Formula
02

RNG & Entropy Model

Is the randomness genuinely random, or could it be rigged?

This section verifies that Duel.com's Crash random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. The RNG uses HMAC-SHA256 keyed by the hex-decoded server seed with the drand randomness as message — the first 4 bytes of the output are interpreted as a uint32 and mapped through the formula floor(2³²/(value+1) × 0.999 × 100) / 100 to produce the final crash point. We independently implemented this algorithm, verified it produces the same results as the live game for all 1,100 captured rounds, and re-fetched all 1,100 drand signatures from api.drand.sh to confirm the external entropy was not forged.

Cryptographic Randomness Verification

1,100 / 1,100drand signatures verified

What we verified

  • HMAC-SHA256 produces cryptographically sound, unpredictable output for every round
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • All 1,100 drand beacon signatures in the dataset match, byte-for-byte, signatures independently re-fetched from the public drand chain
  • The uint32 → crash point mapping is bijective and bias-free — no rejection sampling needed; every uint32 maps to exactly one crash point
  • Crash-point distribution matches the theoretical uint32 survival-probability model — Fisher's combined p = 0.5382 across 10 independent 500K-round streams, 0 / 10 below α = 0.01
  • Consecutive outcomes are statistically independent across 5M simulated rounds — no autocorrelation, no runs-test anomalies

What this means for you

  • Every crash point is generated fairly — no multiplier value is more or less likely than the formula predicts
  • The external entropy comes from a public, cryptographically-signed source the casino cannot forge or withhold
  • No hidden randomness or server-side tricks influence which multiplier you see
  • Consecutive rounds are not correlated — past results don't affect future outcomes
  • The algorithm depends only on inputs you can verify yourself against the public drand chain
1,100 / 1,100drand signatures verified
  • HMAC-SHA256 produces cryptographically sound, unpredictable output for every round
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • All 1,100 drand beacon signatures in the dataset match, byte-for-byte, signatures independently re-fetched from the public drand chain
  • The uint32 → crash point mapping is bijective and bias-free — no rejection sampling needed; every uint32 maps to exactly one crash point
  • Crash-point distribution matches the theoretical uint32 survival-probability model — Fisher's combined p = 0.5382 across 10 independent 500K-round streams, 0 / 10 below α = 0.01
  • Consecutive outcomes are statistically independent across 5M simulated rounds — no autocorrelation, no runs-test anomalies
RNG Pipeline — serverSeed (key) + drandRandomness:0 (message) → HMAC-SHA256 → first 4 bytes → uint32 → (2³²/(value+1))×0.999 → floor → crashPoint — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0) — no hidden entropy
Entropy purityPassNo timestamps, external APIs (other than drand itself), Math.random, or server-side state
External entropy authenticityPass1,100 / 1,100 drand signatures match, byte-for-byte, signatures re-fetched from api.drand.sh (chain 52db9ba7…4e971)
Algorithm independently implementedPassIndependent implementation produces identical crash points for all 1,100 live rounds
Uniform mappingPassuint32 → crash point mapping is bijective and bias-free — no rejection sampling required
Simulation integrityPass5M rounds (10 streams × 500K) — Fisher's combined p = 0.5382, 0 / 10 streams below α = 0.01
Serial independencePasslag1Z = −0.029, runsP = 0.0436 across combined 5M-round sequence — both within acceptance bounds
RNG derived only from disclosed inputsPass
HMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0) — no hidden entropy
Entropy purityPass
No timestamps, external APIs (other than drand itself), Math.random, or server-side state
External entropy authenticityPass
1,100 / 1,100 drand signatures match, byte-for-byte, signatures re-fetched from api.drand.sh (chain 52db9ba7…4e971)
Algorithm independently implementedPass
Independent implementation produces identical crash points for all 1,100 live rounds
Uniform mappingPass
uint32 → crash point mapping is bijective and bias-free — no rejection sampling required
Simulation integrityPass
5M rounds (10 streams × 500K) — Fisher's combined p = 0.5382, 0 / 10 streams below α = 0.01
Serial independencePass
lag1Z = −0.029, runsP = 0.0436 across combined 5M-round sequence — both within acceptance bounds
✓  Unbiased and Cryptographically Sound

The Crash RNG uses only the two disclosed inputs. All 1,100 drand signatures in the dataset match the public drand chain byte-for-byte — the external entropy is verifiably real, not fabricated. The uint32 → crash point mapping is bijective and introduces no bias. 5 million simulated rounds produce a crash-point distribution statistically indistinguishable from the theoretical uint32 survival-probability model (Fisher's combined p = 0.5382). Consecutive outcomes are independent.

How It Works — RNG & Entropy Model8 sections
2.1RNG Function Implementation

Each Crash round produces a single crash point from a single HMAC-SHA256 computation. The hex-decoded server seed (32 bytes) is the HMAC key; the drand randomness hex-decoded to bytes and converted to a UTF-8 string (lossy but deterministic) concatenated with the fixed :0 nonce is the HMAC message. The first 4 bytes of the 32-byte HMAC output are interpreted as a uint32. That uint32 is mapped to the raw crash multiplier via (2³² / (value + 1)) × 0.999, which is then floored to two decimal places and clamped at a minimum of 1.00. No shuffle, no multi-step loop — a single HMAC, a single integer parse, a single arithmetic mapping.

2 columns
ComponentDetail
Hash functionHMAC-SHA256
KeyBuffer.from(serverSeed, 'hex') — 32 bytes
Message{drandRandomness_utf8}:0 — drand bytes re-encoded as UTF-8 + fixed nonce :0
ExtractionFirst 4 hex bytes of HMAC → parseInt(hex, 16) → uint32
Mapping(2³² / (value + 1)) × 0.999 → raw multiplier
ClampingMath.max(1.0, raw) → lower bound at 1.00×
FlooringMath.floor(raw × 100) / 100 → 2-decimal crash point
OutputSingle crash point — no shuffle, no per-step loop
src/rng.ts · computeCrashPointRaw · Verified
export function computeCrashPointRaw(serverSeed: string, drandSeed: string): number {  const keyBuffer = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandSeed, 'hex');  const randomness = drandBytes.toString('utf-8'); // lossy hex→bytes→utf8  const message = `${randomness}:0`;   const hmac = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);   const result = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE);  return Math.max(1.0, result);}
Result: Independent implementation matches all 1,100 live bets with zero mismatches. Algorithm coded from the public cryptographic specification, not copied from any casino source.
2.2Entropy Sources

All randomness derives exclusively from the deterministic HMAC-SHA256 function combining two cleanly separated inputs. Unlike client-seed games, Crash does not accept player-contributed entropy; instead, external entropy comes from the drand quicknet public randomness beacon — a signed, distributed source that the operator cannot predict, forge, or withhold.

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness (committed via SHA-256 hash before the round's drand beacon is available)
drand RandomnessPublic drand quicknet chainExternal entropy — BLS threshold signature from a distributed validator network (chain 52db9ba7…4e971)
Nonce SuffixSystem (fixed)Always :0 — each server seed is single-use, so no nonce counter is needed
No mixed entropy sources detected. Run the same (serverSeed, drandRandomness) inputs multiple times — the crash point is always identical. Pure HMAC-SHA256 must always produce identical outputs. 1,100 / 1,100 confirmed.

How we know: 1,100 / 1,100 live bets were recomputed using only these two declared inputs. If any hidden entropy source existed, recomputation would fail. It does not.

Verified absent: No timestamps, no Math.random(), no external APIs other than drand itself, no server-side mutable state. Only: HMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0).

The authenticity of the drand values themselves is verified separately in 2.3.

2.3drand Chain Authenticity (Step 15)

Step 2.2 establishes that only serverSeed and drandRandomness enter the RNG. This subsection answers the next question: are those drand values themselves real? An operator could in principle record any 32-byte string in the dataset and call it the round's drand value. Step 15 closes that gap by independently re-fetching every drand signature from the public drand chain and comparing it byte-for-byte against the dataset.

drand Chain Authenticity (Step 15) — illustrated flow
src/verify-drand-timing.js · drand API signature re-fetch · Verified
// For every round, re-fetch the BLS signature from the public// drand quicknet chain and compare it to the dataset value.const api = await fetchDrandRound(r.result.drandRoundId);const sigMatch = api.signature === r.result.drandRandomness; // fetchDrandRound hits the public chain endpoint directly://   https://api.drand.sh/<quicknet-chain-hash>/public/<roundId>// A pass means the operator relayed a real, network-signed// beacon — not a value they fabricated or substituted.
Result: 1,100 / 1,100 drand signatures in the dataset match api.drand.sh byte-for-byte. The external entropy used by the Crash RNG is verifiably real — it came from the public drand chain, not from the operator.

Why this matters. The dataset value matching the public chain means the operator faithfully relayed a real, distributed-network-signed drand beacon — one they could not predict, forge, or substitute. A mismatch would mean an operator was running their own fabricated drand value through the RNG, which would let them cherry-pick crash points after seeing what the real beacon would have produced. Step 15 rules that out across every round.

Coverage. All 1,100 / 1,100 drand signatures in the dataset were re-fetched from api.drand.sh for the quicknet chain (52db9ba7…4e971) and compared byte-for-byte. Every one matched. This is full-coverage verification — not a spot-check.

Reproducibility. The pinned match evidence lives at outputs/drand-api-verification.json and Step 15 of the verification suite re-reads it on every npm test run. Any reviewer can independently re-fetch from the live drand chain via npm run timing — the test will fail if any dataset signature does not match what api.drand.sh returns for that round ID.

2.4Uniform Mapping Verification

The uint32 output of HMAC-SHA256 is uniformly distributed over [0, 2³² − 1] (a standard property of HMAC with a well-keyed hash function). Crash maps this uint32 directly to a raw crash multiplier via raw = 2³² / (value + 1) × 0.999, then floors to 2 decimal places. Unlike modulo-based mappings, this arithmetic transformation requires no rejection sampling: every uint32 produces exactly one valid crash point, and every crash point bucket is reached by a contiguous range of uint32 values. The only structural question is whether the bucket sizes (the count of uint32 values that map to each discrete crash-point value) match the theoretical P(crash = c) from the survival-probability formula. They do, by construction — flooring after scaling preserves uniformity.

Mapping:
  uint32 value v ∈ [0, 2³² − 1], uniform
  raw   = 2³² / (v + 1) × 0.999
  crash = max(1.00, floor(raw × 100) / 100)

Survival probability at target x:
  P(crash ≥ x) = floor(2³² × 0.999 / x) / 2³²

Example — target = 2.00:
  P(crash ≥ 2.00) = floor(4,294,967,296 × 0.999 / 2) / 4,294,967,296
                  = floor(2,145,336,164.35) / 4,294,967,296
                  = 2,145,336,164 / 4,294,967,296
                  = 49.95000%

  RTP at target = 2.00:
  = 49.95000% × 2.00 = 99.9000%  ✓ (flat edge)
Result: Zero mapping bias confirmed. The uint32 → crash point transformation is bijective (up to flooring) and produces bucket sizes matching the theoretical survival-probability model exactly. Anti-circularity proof with independent RTP evaluation across 11 cashout targets: Step 12 — see S4.

At the lower boundary, raw < 1.01 clamps to crash = 1.00 (an instant crash). The probability of this is P(v > 2³² × 0.999 / 1.01 − 1) = 1.0891% — an exact, closed-form value from the formula, not a statistical estimate. At the upper boundary, v = 0 produces the theoretical maximum raw multiplier of 2³² × 0.999 ≈ 4,290,672,328, which floors to 4,290,672,328.70× (the highest possible crash point). Every intermediate crash point has a bucket size that matches the formula to the integer uint32 count. No bias correction is required because no % operation is involved.

2.5RNG Isolation

Each Crash round uses a fresh server seed paired with a distinct drand round ID. The HMAC call is a single stateless computation — no shared state between rounds, no cross-round cursor, no persistent buffers. The fixed :0 nonce suffix is a constant, not a counter; it exists only to match the operator's server-side HMAC message format. Because every server seed is single-use, the same input pair (serverSeed, drandRandomness) can never recur — the nonce counter pattern from client-seed games is unnecessary here.

RNG Isolation — illustrated flow
Result: RNG isolation confirmed. No state leakage between rounds. Each HMAC call is independently keyed and messaged. Every (serverSeed, drandRandomness) pair in the dataset is unique.

Evidence: The computeCrashPointRaw implementation in 2.1 confirms this — it is a pure function with no class state, no external calls, and no cross-round memory. The function takes (serverSeed, drandSeed) explicitly and returns a single number. Same inputs always produce the same output. All 1,100 server seeds and all 1,100 drand round IDs in the dataset are distinct (verified by Step 3 drand monotonicity and Step 4 server seed uniqueness in S1).

2.6Monte Carlo Simulation (5M Rounds)

A 5,000,000-round Monte Carlo simulation verified that the algorithm produces the expected crash-point distribution at scale. Because Crash produces a continuous distribution with no natural bins (unlike discrete games that have a finite outcome space per configuration), a single long chi-squared test is vulnerable to Monte Carlo variance in the tail bins. The simulation uses 10 independent 500,000-round streams with per-stream chi-squared tests, then combines the 10 resulting p-values via Fisher's combined probability test (R.A. Fisher, 1925). This provides the variance dilution that multi-configuration games get naturally from per-config tests with Bonferroni correction.

2 columns
MetricValue
Total rounds5,000,000 (10 streams × 500,000)
Fisher's combined statisticT = 18.7488 (df = 20)
Fisher's combined p-value0.5382
Streams below α = 0.010 / 10
lag-1 autocorrelation (Z)−0.029
Wald-Wolfowitz runs test (p)0.0436
Mean simulated RTP across 5 targets99.8720%
Theoretical RTP (all targets)99.9000%
3 columns
Streamchi²p-value
015.0060.1318
114.8580.1373
26.1960.7986
38.5790.5724
410.6760.3833
512.0750.2801
610.2950.4150
711.3740.3291
84.2860.9335
96.7470.7491
3 columns
Cashout TargetTheoretical RTPSimulated RTP (5M rounds)
1.5×99.9000%99.8863%
99.9000%99.8643%
99.9000%99.8948%
10×99.9000%99.9334%
50×99.9000%99.7810%
Mean across 5 targets99.9000%99.8720%
src/simulate.ts · Pass 1 parameters · Verified
// Simulation parameters (Pass 1 — Fresh Seeds)const PASS1_STREAMS      = 10;const PASS1_ROUNDS_EACH  = 500_000;const PASS1_ROUNDS_TOTAL = 5_000_000; // Per-stream deterministic seed domains derived via HMAC-SHA256://   pass1-server-stream-0  pass1-server-stream-1  ...  pass1-server-stream-9// Every run produces identical results. // Chi-squared bins (cashout targets)://   [1.00, 1.01)  [1.01, 1.10)  [1.10, 1.50)  [1.50, 2.00)//   [2.00, 3.00)  [3.00, 5.00)  [5.00, 10.00) [10.00, 20.00)//   [20.00, 50.00)  [50.00, 100.00)  [100.00, ∞)
Result: 5M rounds simulated across 10 independent streams. Fisher's combined p = 0.5382 — squarely in the middle of the [0, 1] range, exactly what a fair RNG should produce. 0 streams below α = 0.01. Per-target RTP converges within ±0.1% of the 99.9% theoretical value at every target. Crash-point distribution matches the uint32 survival-probability model.

What is Fisher's combined probability test? It combines independent p-values from multiple test streams into one aggregate statistic, then checks whether that statistic is consistent with a fair RNG. A truly biased RNG produces low p-values across most streams, which drives the combined result into the rejection region. A fair RNG produces a combined p-value distributed evenly across [0, 1] — Crash's p = 0.5382 is squarely mid-range, exactly what a fair RNG should produce.

2.7Serial Independence

Serial independence ensures consecutive round outcomes are not correlated — a high or low crash point on one round does not statistically influence the next. Two tests were applied to the combined 5M-round Pass 1 sequence:

Lag-1 autocorrelation (Z): Measures correlation between consecutive crash-point values. Expected r ≈ 0 for independent sequences. Threshold: |Z| > 3 (where Z = r × √n) would indicate non-random structure. Observed: Z = −0.029.

Wald-Wolfowitz runs test (p): Tests whether the sequence of above/below-median crash-point indicators has the expected number of runs. p < 0.01 would indicate non-random structure. Observed: p = 0.0436 — above the threshold, consistent with independent sequences.

src/stats.ts · lag1Autocorrelation · Verified
export function lag1Autocorrelation(values: number[]): { r: number; z: number } {  const n = values.length;  if (n < 3) return { r: 0, z: 0 };   const mean = values.reduce((s, v) => s + v, 0) / n;  let num = 0;  let den = 0;  for (let i = 0; i < n; i++) {    den += (values[i] - mean) ** 2;    if (i < n - 1) num += (values[i] - mean) * (values[i + 1] - mean);  }  const r = den === 0 ? 0 : num / den;  const z = r * Math.sqrt(n);  return { r, z };}
Result: Both serial-independence tests pass on the combined 5M-round sequence. lag1Z = −0.029 (well within the |Z| < 3 acceptance bound). runsP = 0.0436 (above the p > 0.01 acceptance bound). Consecutive crash points are statistically independent — past outcomes carry no information about future outcomes.
2.8Worked Example — Full RNG Trace

Real bet from dataset — Round 831399, Phase C ($10 stake, auto-cashout 2×). This is the final round in the dataset, picked to demonstrate bet-size invariance: a $10 Phase C stake recomputes via the identical RNG path as the $0.01 Phase A round traced in S1. Verified from data/crash-master-1100rounds.json:

serverSeed      = 83f547b0019de9e1d0b91ddc620a4c729571a1d30f884651e8405cb01b531f0d
drandRoundId    = 27819827
drandRandomness = a1f86919bcef210c0aee36e3840bc1355208f0f9d06b24d1a5d9ba5510c41cc9
                  b7011cff04f1aa5e9f0577713d079015   (96 hex chars, 48 bytes)

Step 1 — Decode key:
  keyBuffer = Buffer.from(serverSeed, 'hex')           (32 bytes)

Step 2 — Decode drand & convert to UTF-8 (lossy):
  drandBytes = Buffer.from(drandRandomness, 'hex')     (48 bytes)
  randomness = drandBytes.toString('utf-8')            (43 chars — lossy)

Step 3 — Build HMAC message with fixed :0 suffix:
  message = randomness + ":0"

Step 4 — HMAC-SHA256:
  hmac = 20b757315b50202aa5ff985986deb28f15801c73e8cc0241854e0e8469a8f256

Step 5 — First 4 bytes → uint32:
  value = parseInt("20b75731", 16) = 548,886,321

Step 6 — Threshold mapping:
  raw = 2³² / (548,886,321 + 1) × 0.999
      = 4,294,967,296 / 548,886,322 × 0.999
      = 7.8248757... × 0.999
      = 7.81705092...

Step 7 — Floor to 2 decimals and clamp:
  crashPoint = max(1.00, floor(7.81705092 × 100) / 100)
             = max(1.00, 7.81)
             = 7.81
3 columns
StepProcessOutput
1Decode serverSeed from hex → byteskeyBuffer (32 bytes)
2Decode drandRandomness from hex → bytes → lossy UTF-8randomness (43-char string)
3Build HMAC messagerandomness + ":0"
4HMAC-SHA256(keyBuffer, message).digest('hex')20b757315b50202a…69a8f256
5parseInt(hmac.slice(0, 8), 16)548,886,321 (uint32)
6(2³² / (value + 1)) × 0.9997.81705092… (raw)
7max(1.00, floor(raw × 100) / 100)7.81 (crash point)
Parity verified: Round 831399 — crash point matches HMAC-SHA256 recomputation exactly. Player auto-cashout = 2×; since crashPoint (7.81) ≥ auto_cashout (2) the bet wins: captured amount_won = 10 × 2 × 0.999 = 19.98 (matches amountWon in the dataset). Bet-size invariance confirmed — the $10 Phase C stake follows the same RNG path as the $0.01 Phase A round in S1, producing different outputs only because the inputs differ.
Live GamecrashPoint = 7.81
VerifiercrashPoint = 7.81
Technical Evidence & Verification5 sections
2.9Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Algorithm implementation (Step 5)1,100 / 1,100 live crash pointsPass
Key encoding (hex → bytes) & drand UTF-8 encodingConfirmed via recomputationPass
Uniform mapping analysisuint32 → crash point bijective; no rejection neededPass
drand API signature verification (Step 15)1,100 / 1,100 signatures match api.drand.shPass
Simulation Pass 1 — chi-squared (Step 13)Fisher's combined p = 0.5382, 0 / 10 streams below α = 0.01Pass
Simulation Pass 2 — cherry-pick detection (Step 14)14 chi² fails of 1,100 seeds (threshold 21 under H₀)Pass
Serial independence (Step 13)lag1Z = −0.029, runsP = 0.0436 on 5M-round sequencePass
2.10Code References6 source files
2 columns
FilePurpose
src/rng.tsHMAC-SHA256 + uint32 threshold mapping (computeCrashPoint, computeCrashPointRaw, survivalProbability, instantCrashProbability)
src/simulate.tsMonte Carlo simulation (5M rounds, 10-stream Pass 1 + 1,100-seed Pass 2)
src/stats.tsChi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test, Fisher's combined
src/verify-drand-timing.jsdrand API signature re-fetch against api.drand.sh (generates pinned artifact)
tests/steps/determinism.tsSteps 5–6: Crash-point recomputation and bet-size invariance
tests/steps/simulation.tsSteps 13–15: Simulation integrity + drand API signature verification
2.11Verified Invariants13 invariants
2 columns
InvariantResult
HMAC-SHA256 output matches live game for all 1,100 roundsPass
Key is hex-decoded (not UTF-8) — wrong encoding produces wrong crash pointsPass
drand randomness is hex-decoded then converted to UTF-8 (lossy but deterministic) — confirmed by recomputationPass
Fixed :0 nonce suffix — constant, not a counterPass
uint32 → crash point mapping is bijective and bias-free (no rejection sampling required)Pass
No external entropy sources beyond the disclosed server seed and drand randomnessPass
All 1,100 drand signatures in dataset match api.drand.sh byte-for-bytePass
Fisher's combined p-value ≥ 0.01 (observed: 0.5382) on 10 × 500K streamsPass
0 / 10 streams produce chi² p-value below α = 0.01Pass
Pass 2 chi² fails within H₀ expectation (observed: 14, threshold: 21)Pass
Mean casino-seed simulated RTP within 0.1% of theoretical (observed: 99.9638%)Pass
lag1Z within |Z| < 3 bound (observed: −0.029) on 5M-round sequencePass
runsP above 0.01 threshold (observed: 0.0436) on 5M-round sequencePass
2.12Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json — 5M rounds across 10 streams + 1,100 casino-seed cherry-pick test
Primary dataset: data/crash-master-1100rounds.json — 1,100 live rounds for crash-point recomputation verification
Simulation output: outputs/simulation-results.json — Pass 1 (10 streams × 500,000 rounds, per-stream chi² + Fisher's combined + serial independence) + Pass 2 (1,100 casino seeds × 1,000 random drand values, cherry-pick detection)
drand API evidence: outputs/drand-api-verification.json — pinned artifact recording the 1,100 signature comparisons against api.drand.sh, generated by npm run timing and re-read by Step 15 on every verification run
2.13Reproduction InstructionsRun locally

Clone the repository, install dependencies, run the simulation, and run the verification suite:

reproduce-s2.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crash && npm installnpm run simulate  # 5M-round multi-stream simulationnpm run verify    # Steps 5, 13, 14, 15 cover S2# Optional: npm run timing  # re-fetch all 1,100 drand signatures (requires internet)
S2-related steps (all reproducible offline from pinned inputs):

[PASS] Step 5   — Outcome Recomputation
[PASS] Step 13  — Simulation Pass 1
[PASS] Step 14  — Simulation Pass 2 (Casino Seeds)
[PASS] Step 15  — drand API Signature Verification
03

Verifier Parity

Does the live game actually follow its own rules?

This section validates that the independent verifier produces the exact same crash point as the live game for every single bet. It also confirms the win condition and that every payout matches Duel's published multiplier formula. Any mismatch would invalidate the fairness guarantee.

Live ↔︎ Verifier Parity

1,100 / 1,100rounds matched

What we verified

  • Every round independently recomputed from (serverSeed, drandRandomness) — full crash point verified, not just the payout
  • Captured-value consistency: amount_won = stake × auto_cashout × 0.999 on wins, 0 on losses — exact for all 1,100 rounds (pre-rakeback display values)
  • Win/loss flags are correct — the round counted as a win exactly when the crash point reached your cashout target, on all 1,100 / 1,100 rounds
  • Bet amount is not an input to the RNG — crash point depends only on the server seed and drand beacon
  • All three capture phases verified under the identical RNG code path

What this means for you

  • The verifier isn't a simulation — it produces the exact same crash point as the live game
  • Every round you play can be independently recomputed by anyone with the revealed seeds
  • No hidden logic alters your payout based on how much you bet or which target you pick
  • The game engine in production matches the published algorithm exactly
1,100 / 1,100rounds matched
  • Every round independently recomputed from (serverSeed, drandRandomness) — full crash point verified, not just the payout
  • Captured-value consistency: amount_won = stake × auto_cashout × 0.999 on wins, 0 on losses — exact for all 1,100 rounds (pre-rakeback display values)
  • Win/loss flags are correct — the round counted as a win exactly when the crash point reached your cashout target, on all 1,100 / 1,100 rounds
  • Bet amount is not an input to the RNG — crash point depends only on the server seed and drand beacon
  • All three capture phases verified under the identical RNG code path
Parity Verification Flow — (serverSeed, drandRandomness) → recompute → compare → exact match — illustrated flow
1,100Live Rounds Tested
100%Parity Rate
$0.01 & $10Bet Sizes Tested
0Mismatches
CheckResultEvidence
Crash-point recomputationPass1,100 / 1,100 exact match — crash point verified for every round from (serverSeed, drandRandomness)
Payout correctnessPassAll 1,100 rounds: captured amount_won = stake × auto_cashout × 0.999 on wins, 0 on losses — exact to floating-point precision (pre-rakeback display values)
Win-condition logicPass1,100 / 1,100 isWin flags correct (crashPoint ≥ auto_cashout)
Flat distribution edgePasseffectiveEdge = 0.1 on every round — no scaling, no per-target variation
Bet-size invariancePass100 / 100 Phase C ($10) rounds verify under the identical RNG code path used at $0.01 stakes — bet amount is not an RNG input
Multi-phase coveragePass3 structured phases: baseline (A, 800), varied targets (B, 200), elevated stake (C, 100)
Crash-point recomputationPass
1,100 / 1,100 exact match — crash point verified for every round from (serverSeed, drandRandomness)
Payout correctnessPass
All 1,100 rounds: captured amount_won = stake × auto_cashout × 0.999 on wins, 0 on losses — exact to floating-point precision (pre-rakeback display values)
Win-condition logicPass
1,100 / 1,100 isWin flags correct (crashPoint ≥ auto_cashout)
Flat distribution edgePass
effectiveEdge = 0.1 on every round — no scaling, no per-target variation
Bet-size invariancePass
100 / 100 Phase C ($10) rounds verify under the identical RNG code path used at $0.01 stakes — bet amount is not an RNG input
Multi-phase coveragePass
3 structured phases: baseline (A, 800), varied targets (B, 200), elevated stake (C, 100)
✓  Live game and verifier fully aligned

All 1,100 crash points matched the independent verifier exactly. Payout math correct to floating-point precision on all 1,100 rounds. Win-condition logic (crashPoint ≥ auto_cashout) is correct for every bet. Flat 0.1% house edge confirmed across all cashout targets.

How It Works — Verifier Parity7 sections
3.1Why Parity Matters

If the verifier produces results that differ from the live game, players cannot trust the verification — the entire provably fair system becomes meaningless. 100% parity is required because even a single discrepancy would indicate either a bug in the verification logic, manipulation in the live game, or an inconsistent RNG implementation. Players must be able to take the revealed server seed after a round ends, fetch the drand beacon from the public chain, input both into the independent verifier, and receive the exact same crash point they experienced during live play — along with an exact-matching payout.

Why Parity Matters — illustrated flow
3.2Three-Phase Collection Design

Data was collected across three structured phases, each designed to test a specific fairness property. The phases are complementary — together they cover cashout-target breadth, edge-case low and high targets, and bet-size invariance.

5 columns
PhaseRoundsCashout TargetStakePurpose
A — Baseline8001.5×$0.01Statistical coverage at a single common target — largest per-target sample for distribution shape
B — Varied targets2001.1× / 2× / 5× / 10×$0.01Multi-target payout validation — 51 rounds @ 1.1×, 49 @ 2×, 50 @ 5×, 50 @ 10×
C — Bet-size invariance100$10.00Confirms the crash point is independent of stake size (100× Phase B's 2× target bet size)
Total: 1,100 rounds across 1,100 unique server seeds (one per round). Total wagered: $1,010.00 ($8.00 Phase A + $2.00 Phase B + $1,000.00 Phase C). drand round range: 27,798,178 → 27,819,827 across a single continuous capture window.
3.3Crash-Point Recomputation (Step 5)

For every one of the 1,100 rounds in the dataset, the verifier independently computed the crash point using computeCrashPoint(serverSeed, drandRandomness) and compared it to the server-reported crashPoint value. The computation uses HMAC-SHA256 with the hex-decoded server seed as key and {drandRandomness_utf8}:0 as the message, then applies the uint32 threshold mapping and 2-decimal flooring.

Crash-Point Recomputation (Step 5) — illustrated flow
tests/steps/determinism.ts · Step 5 · Verified
// Step 5: Outcome Recomputationlet match = 0;let mismatch = 0;for (const r of rounds) {  const computed = computeCrashPoint(r.result.serverSeed, r.result.drandRandomness);  if (computed === r.result.crashPoint) {    match++;  } else {    mismatch++;  }}
Result: 1,100 / 1,100 rounds verified. Zero crash-point mismatches. Every computed crash point equals the server-reported crashPoint in the dataset to the exact 2-decimal value — no rounding drift, no floating-point mismatches.
3.4Win-Condition Logic (Step 7)

Crash's win condition is binary and formulaic: a bet wins if and only if crashPoint ≥ auto_cashout. There is no multiplier lookup, no risk-level branching, no per-configuration table. The isWin flag in the dataset is therefore a direct yes/no output of this inequality, with no room for interpretation. Step 7 independently evaluates the inequality for every one of the 1,100 rounds and compares the result to the dataset's isWin flag.

2 columns
ObservationValue
Total rounds checked1,100
isWin flags matching crashPoint ≥ auto_cashout1,100
Overall win rate (informational)671 / 1,100 (61.00%)
Phase A win rate (cashout 1.5×)543 / 800 (67.88%)
Phase C win rate (cashout 2×)49 / 100 (49.00%)
tests/steps/payouts.ts · Step 7 (isWin consistency) · Verified
// Step 7: isWin consistency checklet isWinCorrect = 0;for (const r of rounds) {  const shouldBeWin = r.result.crashPoint >= r.request.auto_cashout;  if (r.result.isWin === shouldBeWin) isWinCorrect++;}
Result: Every live isWin flag matches the computed inequality. 1,100 / 1,100 correct. No rounds flagged incorrectly; no disputed win/loss classifications.
3.5Captured Payout Consistency (Step 7)

For each of the 1,100 rounds, the verifier recomputes the captured amount_won display value from the formula and compares it to the server-reported value. These are pre-rakeback display values, not the settled money flow: settled payout on a winning bet = full bet × cashout, and a separate 0.001 × bet rakeback posts on every bet under Duel's Zero Edge mechanism — net player RTP within the daily cap = 100.0%. The certified figure in this audit is the 99.9% pre-rakeback distribution RTP; the rakeback layer is operator-side and outside audit scope (see the repo MANIFEST money model). The captured-value formula is: amount_won = stake × auto_cashout × (1 − 0.1/100) on wins, 0 on losses.

Captured Payout Consistency (Step 7) — illustrated flow
tests/steps/payouts.ts · Step 7 (payout math) · Verified
// Step 7: Payout Mathfor (const r of rounds) {  const amount  = parseFloat(r.request.amount);  const cashout = r.request.auto_cashout;  const won     = parseFloat(r.result.amountWon);  const edge    = r.result.effectiveEdge; // percent — observed: 0.1   if (r.result.isWin) {    // Win: amountWon = amount × cashout × (1 - edge/100)    const expected = amount * cashout * (1 - edge / 100);    if (Math.abs(won - expected) < 0.000001) correct++;    else wrong++;  } else {    // Loss: amountWon should be 0    if (won === 0) correct++;    else wrong++;  }}
Result: All 1,100 rounds: captured amount_won matches the display-value formula within tolerance 1e-6. Zero mismatches. effectiveEdge = 0.1 on every round — the flat 0.1% distribution-edge factor is reflected uniformly in the display values, with no scaling, no per-target variation, and no hidden fees.
3.6Phase C — Bet-Size Invariance (Step 6)

Bet amount is not part of the HMAC inputs that produce a crash point — not the key, not the message, not referenced anywhere in the derivation. As an empirical confirmation, Phase C placed 100 rounds at $10 stake (1,000× larger than Phase A and B's $0.01 stakes) and recomputed every crash point from (serverSeed, drandRandomness) using the same RNG code path. If bet amount secretly influenced the RNG, your verifier — which has no bet-amount input — would produce different crash points. All 100 / 100 rounds matched exactly.

Result: Phase C: 100 / 100 crash points recomputed correctly at $10 stake using the identical RNG code path as the $0.01 Phase A and Phase B rounds. Bet amount is not part of the HMAC message, not part of the key, and not referenced anywhere in the crash-point derivation.
Variance context: Phase C empirical RTP was 97.90% across the 100-bet sample (49 wins × $19.98 payout = $979.02 returned on $1,000.00 wagered). This is within expected range — a 2× cashout target has P(win) = 49.95% theoretically, with per-bet binomial variance. At N=100, empirical RTP is not a meaningful measure; the theoretical 99.9% RTP is proven analytically in S4.
3.7Worked Example — Full Parity Verification

Real bet from Phase C — Round 831304, auto-cashout 2×, stake $10.00. Verified from data/crash-master-1100rounds.json:

roundId          = 831304
serverSeed       = 36c90d999b0922bc5af2d8c4612115d1baa44b5f80791298934d3c99c1af1a43
serverSeedHash   = 010e60f2541151dd03387506bdce3d6e9f716761da3b97b7b32729c344405282
drandRoundId     = 27818881
drandRandomness  = a596fd0c7d37bd7b786e0a04357520a7debda464baf75779d0424da0566a3993
                   82b53668aabc8cfde685c0d283793349   (96 hex chars, 48 bytes)
stake            = $10.00
auto_cashout     = 2.00×
effectiveEdge    = 0.1 (percent)
3 columns
StepProcessOutput
1computeCrashPoint(serverSeed, drandRandomness)3.56 (crash point)
2crashPoint >= auto_cashout3.56 >= 2true (isWin)
3stake × auto_cashout × (1 − edge/100)10 × 2 × 0.999 = 19.98 (amount won)
4Compare computed vs liveAll three match
HMAC-SHA256 recomputation + payout derivation · Verified
// Step 1: Recompute crash point from (serverSeed, drandRandomness)////   HMAC-SHA256 output hex: 47b7bc7a54854c634975b03b2f969020a17f988a6c5b72f0bc27a6ed1bb4be08//   First 4 bytes (hex):    47b7bc7a//   uint32 value:           1,203,223,674//   raw = 2³² / (value+1) × 0.999//       = 4,294,967,296 / 1,203,223,675 × 0.999//       = 3.5695501885... × 0.999//       = 3.5659806383...//   crashPoint = max(1.00, floor(3.5659806383 × 100) / 100)//              = 3.56  ✅  (matches dataset) // Step 2: Win-condition check//   crashPoint (3.56) >= auto_cashout (2.00)  →  isWin = true  ✅ // Step 3: Payout math//   amount_won = 10.00 × 2 × (1 − 0.1/100)//              = 10.00 × 2 × 0.999//              = 19.98  ✅  (matches dataset)
Parity verified: Round 831304 — crash point, win/loss flag, and payout amount all match exactly between live game and independent verifier. The $10 stake (100× the Phase A/B stake) produces the same crash point as a $0.01 stake with the same (serverSeed, drandRandomness) pair would — confirming bet amount is not an RNG input.
Live GamecrashPoint = 3.56 · isWin = true · amount_won = $19.98
VerifiercrashPoint = 3.56 · isWin = true · amount_won = $19.98
Technical Evidence & Verification5 sections
3.8Evidence Coverage Summary6 checks
3 columns
Verification AreaCoverageResult
Crash-point recomputation (Step 5)1,100 / 1,100 rounds — full (serverSeed, drandRandomness) → crashPoint verifiedPass
Payout math (Step 7)1,100 / 1,100 rounds (tolerance 1e-6)Pass
Win-condition logic (Step 7)1,100 / 1,100 isWin flags match crashPoint ≥ auto_cashoutPass
Flat distribution edge (Step 8)effectiveEdge = 0.1 confirmed on every round; formula produces RTP ≤ 99.9%Pass
Phase C bet-size invariance (Step 6)100 / 100 $10 rounds verify under the identical RNG code path used at $0.01 stakesPass
Multi-phase coverage3 phases: A (800) + B (200) + C (100) = 1,100Pass
3.9Code References4 source files
2 columns
FilePurpose
tests/steps/determinism.tsSteps 5–6: Crash-point recomputation and Phase C bet-size invariance
tests/steps/payouts.tsSteps 7–9: captured payout consistency, isWin consistency, distribution edge audit
src/rng.tsHMAC-SHA256 + uint32 threshold mapping (computeCrashPoint, survivalProbability, instantCrashProbability)
src/loader.tsDataset loading and field parsing
3.10Datasets UsedSource artifacts
Primary dataset: data/crash-master-1100rounds.json — 1,100 live rounds across 3 phases (A: 800, B: 200, C: 100)
Verification output: outputs/verification-results.json — Steps 5, 6, 7, 8 (S3-relevant)
Payout formula reference: src/rng.ts — constants HOUSE_EDGE = 0.001 and MAX_UINT32 = 2³²; no multiplier configuration file required (Crash has no lookup table)
3.11Verified Invariants9 invariants
2 columns
InvariantResult
Computed crash point matches live crashPoint for all 1,100 roundsPass
isWin flag equals crashPoint ≥ auto_cashout for all 1,100 roundsPass
amount_won = stake × auto_cashout × 0.999 within tolerance 1e-6 on all winning roundsPass
amount_won = 0 on all losing roundsPass
effectiveEdge = 0.1 on every round — no per-target or per-stake variationPass
Phase C ($10 stake) verifies under the identical RNG code path as $0.01 roundsPass
No hidden inputs beyond (serverSeed, drandRandomness) — bet amount absent from RNGPass
All 3 capture phases represented in the dataset (A: 800, B: 200, C: 100)Pass
All 4 cashout targets used in Phase B produce correct payouts (1.1×: 51, 2×: 49, 5×: 50, 10×: 50)Pass
3.12Reproduction InstructionsRun locally
reproduce-s3.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crash && npm installnpm run verify# Expected output: Steps 5, 6, 7, 8 all PASS
S3-related steps:

[PASS] Step 5  — Outcome Recomputation
[PASS] Step 6  — Bet-Size Invariance
[PASS] Step 7  — Captured Payout Consistency (pre-rakeback display values)
[PASS] Step 8  — Distribution Edge Audit (pre-rakeback)
04

RTP & Payout Logic

Is the house edge what the casino claims?

This section mathematically verifies that the flat 0.1% distribution edge (pre-rakeback) is exactly what's advertised across all cashout targets. The key test is anti-circularity: we prove the RTP from first principles using the uint32 survival-probability formula and the payout formula — no casino-supplied probability data is used. We then back up the first-principles proof with 5,000,000 simulated rounds across 10 independent streams, and run a cherry-pick detection pass against all 1,100 casino-chosen server seeds.

Return to Player Verification

99.9%theoretical RTP

What we verified

  • House edge is exactly 0.1% — flat across all cashout targets and all bet sizes
  • RTP proven from first principles: P(crash ≥ x) × x × 0.999 = 0.999 for every cashout target — derived from the formula, not from casino data
  • 5M-round simulation converges on theoretical RTP (mean 99.8720% across 5 targets — within 0.03% of 99.9%)
  • Cherry-pick detection: all 1,100 casino-chosen server seeds tested against 1,000 random drand values each — no evidence of seed pre-selection
  • Bet amount does not influence crash points — confirmed at $0.01 and $10

What this means for you

  • The house edge on Crash is a flat 0.1%, the same regardless of cashout target or bet size
  • The RTP proof is derived independently — it doesn't rely on trusting the casino or any simulated data
  • The casino's seeds show no evidence of being chosen to produce favourable early outcomes (structurally impossible given drand's post-commitment publishing schedule)
  • Your bet amount doesn't affect the crash point
99.9%theoretical RTP
  • House edge is exactly 0.1% — flat across all cashout targets and all bet sizes
  • RTP proven from first principles: P(crash ≥ x) × x × 0.999 = 0.999 for every cashout target — derived from the formula, not from casino data
  • 5M-round simulation converges on theoretical RTP (mean 99.8720% across 5 targets — within 0.03% of 99.9%)
  • Cherry-pick detection: all 1,100 casino-chosen server seeds tested against 1,000 random drand values each — no evidence of seed pre-selection
  • Bet amount does not influence crash points — confirmed at $0.01 and $10
INDEPENDENT DERIVATION
P(crash ≥ x) × x≈ 0.999
Independent uint32 survival probabilities · All 11 tested cashout targets

Maximum deviation: 2.8 × 10⁻⁸ from quantisation.

99.9%Theoretical RTP (all cashout targets)
99.8720%Simulated (5M rounds)
0.1%House Edge (flat)
99.9% on all 11Anti-circularity proven
CheckResultEvidence
Anti-circularityPassP(crash ≥ x) × x × 0.999 = 0.999 for all 11 tested cashout targets — algebraic identity, no casino data used
Distribution edge auditPass0.1% flat across all targets — implemented via the × 0.999 multiplier in the RNG
Simulated RTP (Pass 1)Pass5M rounds, mean RTP = 99.872%, Fisher's combined p = 0.5382 across 10 streams
Cherry-pick detection (Pass 2)Pass1,100 casino seeds tested against random drand pairings — no evidence of seed pre-selection
Bet-size invariancePassBet amount is not an input to the RNG — same crash point at any stake. Tested in Phase C (100/100)
Formula-based payoutPassamount_won = stake × auto_cashout × 0.999 verified for all 1,100 bets — uniform formula, no multiplier table
Anti-circularityPass
P(crash ≥ x) × x × 0.999 = 0.999 for all 11 tested cashout targets — algebraic identity, no casino data used
Distribution edge auditPass
0.1% flat across all targets — implemented via the × 0.999 multiplier in the RNG
Simulated RTP (Pass 1)Pass
5M rounds, mean RTP = 99.872%, Fisher's combined p = 0.5382 across 10 streams
Cherry-pick detection (Pass 2)Pass
1,100 casino seeds tested against random drand pairings — no evidence of seed pre-selection
Bet-size invariancePass
Bet amount is not an input to the RNG — same crash point at any stake. Tested in Phase C (100/100)
Formula-based payoutPass
amount_won = stake × auto_cashout × 0.999 verified for all 1,100 bets — uniform formula, no multiplier table
✓  RTP Behaves as Advertised

The 99.9% RTP is proven algebraically — P(crash ≥ x) × x × 0.999 = 0.999 for every cashout target, derived from the formula not measured from casino data. 5M simulated rounds confirm: mean RTP = 99.872%, well within statistical tolerance. Cherry-pick detection across the casino's 1,100 actual server seeds shows no anomalies. The house edge is a flat 0.1% — no per-target scaling, no hidden variation.

How It Works — RTP & Payout Logic8 sections
4.1Anti-Circularity Proof (Step 12)

The anti-circularity proof establishes the 99.9% RTP from first principles without using any casino-supplied probability data or multiplier table. This is what separates a mathematical proof from a statistical estimate. For Crash, the proof is especially clean: because the payout formula is a pure function of the cashout target (stake × target × 0.999) and the crash-point distribution is a pure function of the uint32 value (raw = 2³² / (value + 1) × 0.999), the RTP at any target reduces to a closed-form arithmetic expression. The proof has two components — each derived independently:

Anti-Circularity Proof (Step 12) — illustrated flow
3 columns
ComponentFormulaSource
Survival probabilityP(crash ≥ x) = floor(2³² × 0.999 / x) / 2³²Derived from the uint32 threshold mapping — pure math, not from casino
Payout on winamount_won = stake × x × (1 − HOUSE_EDGE) with HOUSE_EDGE = 0.001RNG code constant — observed from src/rng.ts
RTP computationP(crash ≥ x) × x — probability of reaching target × payout multipleIndependent probability × observed formula
Result= 0.999 for all tested cashout targets x ≥ 1First-principles proof — not a statistical estimate
4 columns
TargetThreshold (uint32 count)P(crash ≥ target)RTP (P × target)
1.5×2,860,448,21966.6000%99.9000%
2,145,336,16449.9500%99.9000%
858,134,46519.9800%99.9000%
10×429,067,2329.9900%99.9000%
50×85,813,4461.9980%99.9000%
100×42,906,7230.9990%99.9000%
1000×4,290,6720.0999%99.9000%
Result: All 11 tested targets produce RTP = 99.9% via the independent survival-probability formula. Max deviation from 99.9%: 2.8 × 10⁻⁸ (floating-point quantisation only, no real deviation). Theoretical RTP proof is non-circular.

Anti-Circularity Verification:

tests/steps/dataset.ts · Step 12 · Verified
// Step 12: Anti-Circularity — Independent RTP Verificationconst targets = [1.01, 1.1, 1.5, 2, 3, 5, 10, 20, 50, 100, 1000];let allValid = true; for (const t of targets) {  const rtp = expectedRTP(t);                     // P(crash >= t) × t  const p   = survivalProbability(t);             // from uint32 threshold  const expectedP = Math.floor(2 ** 32 * 0.999 / t) / 2 ** 32;   // Verify survival probability matches the uint32 threshold formula  if (Math.abs(p - expectedP) > 1e-10) allValid = false;   // RTP should be ≤ 0.999 for all targets  if (rtp > 0.9991) allValid = false;}

Why this proof is non-circular: P(crash ≥ x) comes from the uint32 survival-probability formula — a mathematical property of the bijective mapping from [0, 2³² − 1] uniform integers to crash points, not from casino data. The × 0.999 edge factor comes from a constant in the RNG code (HOUSE_EDGE = 0.001). When we multiply the independent survival probability by the target and apply the edge, we get exactly 0.999 for every target — the RTP is proven, not estimated. The only casino-sourced input is the edge factor itself — the HOUSE_EDGE = 0.001 constant in src/rng.ts. Section 4.2 confirms this is the sole edge constant in the RNG source (no scaling_edge array, no per-target overrides) and that every one of the 1,100 captured rounds reconciles to the resulting × 0.999 payout. The per-bet effectiveEdge field is a separate, operator-side rakeback signal — also discussed in 4.2 — and is not an input to this proof.

4.2Distribution Edge Audit (Step 8)

The Crash RNG source code hardcodes a single HOUSE_EDGE = 0.001 constant, applied uniformly in the raw crash-point formula as × (1 − HOUSE_EDGE) = × 0.999. There is no scaling_edge array, no per-target override, no risk-level branching. Every one of the 1,100 rounds in the dataset carries effectiveEdge: 0.1 (percent), and every payout follows the same stake × auto_cashout × 0.999 formula. The flat-edge structure is a consequence of the formula, not a claim — if any round used a different edge factor, the crash point would not recompute and Step 5 would fail.

The effectiveEdge field is Duel's Zero Edge rakeback signal. It is the same field the other game audits refer to as effective_edge — the live Crash endpoint and the /transactions endpoint just spell it differently. Per the operator's published mechanism, a bet is tagged 0.1 at settlement, then updated asynchronously to 0 once an operator-side rewards queue applies the rakeback.

Crash's Step 7 payout check reads the effectiveEdge value recorded with each bet: amount_won = stake × auto_cashout × (1 − effectiveEdge/100). Because each round's amountWon and effectiveEdge were captured together at bet time, they form a matched pair — and the formula reconciles exactly across all 1,100 rounds, confirming every payout was settled correctly for the edge in effect at the time of the bet.

effectiveEdge is also the field Duel's Zero Edge rakeback updates: after a rewards queue applies the rebate, the same bet will show effectiveEdge: 0. This means a bet fetched again later will carry the post-rebate edge rather than its original bet-time value — the expected result of rakeback being applied in the player's favour. Step 7 therefore verifies each bet against its bet-time record; the rewards layer that later applies the rebate is operator settlement-side and sits outside this audit's scope (see the exclusions list).

Result: Flat 0.1% distribution edge confirmed (pre-rakeback). HOUSE_EDGE = 0.001 is the sole edge constant in the codebase. 1,100 / 1,100 rounds carry effectiveEdge = 0.1 at capture time, and the Step 7 payout formula reconciles exactly against the bet-time capture. RTP = 99.9% at every cashout target (proven analytically in 4.1). The effectiveEdge field is the operator's Zero Edge rakeback signal and updates to 0 post-settlement; that rewards layer is outside this audit's scope.
Variance note: Crash is a high-variance game at high cashout targets — the 1000× target has P(win) ≈ 0.0999% and the theoretical maximum crash point is ~4,290,672,328.70×. Empirical RTP from small samples will deviate significantly from the 99.9% theoretical value, especially at high targets. The anti-circularity proof in 4.1 is the authoritative RTP evidence; the simulation in 4.4 provides empirical convergence at scale (5M rounds), not the live dataset (1,100 rounds).
4.3Full RTP Table — All Tested Cashout Targets

Unlike games with per-configuration multiplier tables, Crash's RTP is a single closed-form expression across a continuous target range. The table below shows the theoretical RTP at representative targets (from Step 12's 11-target anti-circularity check) alongside the simulated RTP from Pass 1 where available (targets 1.5×, 2×, 5×, 10×, 50× were binned). All theoretical values are 99.9% — the formula produces exactly that RTP at every target by construction.

4 columns
TargetTheoretical RTPSimulated RTP (5M rounds)Deviation
1.01×99.9000%
1.1×99.9000%
1.5×99.9000%99.8863%−0.0137%
99.9000%99.8643%−0.0357%
99.9000%99.8948%−0.0052%
10×99.9000%99.9334%+0.0334%
50×99.9000%99.7810%−0.1190%
100×99.9000%
1000×99.9000%
Mean across 5 simulated targets99.9000%99.8720%−0.0280%
Result: All 11 anti-circularity targets produce theoretical RTP = 99.9%. All 5 simulated targets converge within 0.12% of theoretical. Mean simulated RTP across 5 targets = 99.8720% (only 0.028% below theoretical). No target shows a systematic deviation indicating mapping bias.
Variance note: The 50× target shows the largest per-target deviation (−0.1190%) because at 500K rounds per stream, the expected count of crash points ≥ 50× is only ~10,000 per stream — small absolute counts amplify sampling variance. The mean across 5 targets (−0.028%) is a better estimator; at 25M round-target observations aggregated, it is statistically indistinguishable from 0. The uint32 values that map to each crash-point bucket have a precisely-known count — no approximation is involved in the theoretical calculation.
4.4Simulation Pass 1 — Multi-Stream Fresh Seeds (Step 13)

Section 4.1 proves RTP = 99.9% mathematically. But does the game engine actually produce that in practice? To find out, we simulated 5,000,000 rounds locally using the same RNG algorithm and independent auditor-generated seeds. Because Crash produces a continuous distribution with only one "configuration" (the crash-point survival curve), a single long chi-squared test is vulnerable to Monte Carlo variance in tail bins. Pass 1 therefore uses 10 independent 500,000-round streams and combines their p-values via Fisher's combined probability test — providing the variance dilution that multi-configuration games get naturally.

Simulation Pass 1 — Multi-Stream Fresh Seeds (Step 13) — illustrated flow

Chi-squared test (per stream): Compares observed crash-point bin counts (11 bins from [1.00, 1.01) to [100.00, ∞)) against expected counts from the uint32 survival formula. 0 / 10 streams fail at α = 0.01.

Fisher's combined test: Combines the 10 stream p-values into T = −2 × Σ ln(p_i), which follows χ²(df=20) under H₀. Observed T = 18.7488, combined p = 0.5382 — squarely in the middle of the expected range for a fair RNG.

Serial independence: Lag-1 autocorrelation and Wald-Wolfowitz runs test on the combined 5M-round sequence. Observed lag1Z = −0.029 (within |Z| < 3), runsP = 0.0436 (above 0.01 threshold).

RTP convergence (mean across 5 targets): 99.8720% vs 99.9000% theoretical. Deviation of −0.028% is within expected sampling variance at 25M target-round observations.

src/simulate.ts · Pass 1 core loop · Verified
// Pass 1 — Fresh random seeds// 500,000 rounds × 10 streams = 5,000,000 total// Per-stream pinned seed domains for reproducibility (S7)const PASS1_STREAMS     = 10;const PASS1_ROUNDS_EACH = 500_000; for (let stream = 0; stream < PASS1_STREAMS; stream++) {  const domain  = `pass1-server-stream-${stream}`;  const seeds   = generateDeterministicSeeds(domain, PASS1_ROUNDS_EACH);   for (const { serverSeed, drandSeed } of seeds) {    const crashPoint = computeCrashPoint(serverSeed, drandSeed);    // Update per-target win counters and bin counts    for (const t of TARGETS) {      if (crashPoint >= t) { wins[t]++; totalPayout[t] += t * 0.999; }      bets[t]++;    }    binCounts[binIndex(crashPoint)]++;  }}// After all streams: chi-squared per stream → 10 p-values → Fisher's combined
Result: 5M rounds across 10 independent streams. Fisher's combined p = 0.5382. 0 / 10 streams below α = 0.01. Mean simulated RTP across 5 targets: 99.8720%. 0 serial-independence failures. Crash-point distribution is statistically indistinguishable from the theoretical uint32 survival model.
4.5Cherry-Pick Detection — Pass 2 (Step 14)

Could the casino have chosen server seeds that produce worse outcomes for players? For Crash, cherry-picking is structurally impossible because the crash point depends not only on the server seed but also on the drand beacon — which publishes after the operator commits to the seed and is outside the operator's control. No server seed can be paired with a favourable drand value because the drand value hasn't been published yet at commitment time. However, as a confirmatory check, Pass 2 takes every one of the 1,100 server seeds the casino actually used and simulates 1,000 random drand values per seed, testing whether any individual seed produces a statistically biased distribution.

Test methodology: For each of the 1,100 captured server seeds, generate 1,000 random drand values, compute each crash point, and run a chi-squared test against the expected survival distribution. A seed that produces an unusual distribution when paired with random drand values would suggest the seed itself is biased (e.g., pre-selected to favour the house across a wide range of possible drand pairings).

Expected fails under H₀: With α = 0.01 and 1,100 independent tests, the expected number of chi-squared fails is 11. The threshold ≤ 21 is a 3σ binomial upper bound — observed 14 fails sits within 1σ of the expected 11.

Mean casino-seed RTP: 99.9638% across 1,100 seeds × 1,000 drand values each. Deviation from theoretical (99.9%): +0.0638% — within expected sampling variance at 1,100,000 round-observations.

2 columns
TestResult
Casino seeds tested1,100 (one per round)
drand values per seed1,000 (random)
Total test rounds1,100,000
Chi-squared fails at α = 0.0114 / 1,100
Threshold under H₀ (binomial 3σ upper bound)≤ 21
Mean casino-seed RTP99.9638%
Theoretical RTP99.9000%
tests/steps/simulation.ts · Step 14 · Verified
// Step 14: Pass 2 Cherry-Pick Testconst p2 = sim.pass2_casino_seeds;const expectedFails = p2.seedsTested * 0.01;                  // = 11const threshold     = Math.ceil(  expectedFails + 3 * Math.sqrt(expectedFails * 0.99)         // = 21);const pass = p2.chi2Fails <= threshold;// Observed: 14 fails, threshold 21, pass = true
Result: Pass 2: 14 chi² fails out of 1,100 casino seeds — within expected ≤ 21 under H₀. Mean casino RTP = 99.9638% (0.06% above theoretical, well within variance). No evidence of seed pre-selection.
Why is this included? Cherry-pick detection is a standard part of our audit methodology. For Crash specifically, the attack is structurally impossible because the operator commits to the server seed before the round's drand beacon is available — there's no way to pair a seed with a known-favourable beacon value. Drand's public, distributed, fixed-schedule publishing model is the architectural defence. Pass 2 is included as an empirical confirmation alongside that structural guarantee. A pass here rules out implementation-level anomalies (e.g., systematic bias in how the operator's RNG generates seeds) that the structural argument alone cannot detect.
4.6Bet-Size Invariance (Step 6)

Bet-size invariance is a structural property of the RNG: the bet amount is not part of the HMAC key, not part of the HMAC message, and not referenced anywhere in the crash-point derivation. Step 6 confirms this by recomputing all 100 Phase C rounds — placed at $10 stake, 1,000× the $0.01 stake used elsewhere — and verifying that every crash point matches the verifier's output exactly. The verifier has no bet-amount input. If bet amount secretly affected the RNG, recomputation would fail.

2 columns
PropertyResult
Bet amount in HMAC keyNo — key is serverSeed only
Bet amount in HMAC messageNo — message is drandRandomness:0 only
Phase C recomputation @ $10 stake100 / 100 crash points match the verifier's output
Phase A + B recomputation @ $0.01 stake1,000 / 1,000 crash points match the verifier's output
RNG code path usedcomputeCrashPoint(serverSeed, drandRandomness) — identical across all stakes
Result: Bet amount is not an input to the RNG. The crash point is determined entirely by (serverSeed, drandRandomness), regardless of stake. Confirmed by 1,100 / 1,100 recomputations across a 1,000× stake range — every live crash point matches the verifier's output exactly.
4.7Informational Items (Not Scored)

These items are reported for transparency but are not scored audit steps. They provide context on the empirical RTP observed during live data collection. Empirical RTP at N=50–100 is dominated by variance and should not be interpreted as evidence of RTP drift — the theoretical 99.9% is proven analytically in 4.1 and confirmed at scale (5M rounds) in 4.4.

3 columns
ItemValueContext
Instant crash rate (live)8 / 1,100 (0.73%)Expected ~12 (1.0891%) — within expected variance
Live RTP @ 1.5×101.71%800 rounds at $0.01 (Phase A) — above theoretical due to variance
Live RTP @ 1.1×94.81%51 rounds at $0.01 (Phase B) — within expected range
Live RTP @ 2×97.90%149 rounds (49 Phase B + 100 Phase C) — within expected range
Live RTP @ 5×69.93%50 rounds at $0.01 (Phase B) — high variance at small N
Live RTP @ 10×79.92%50 rounds at $0.01 (Phase B) — high variance at small N
Phase A empirical RTP101.71%800 rounds @ 1.5× — favourable variance
Phase B empirical RTP85.61%200 rounds across 4 targets — dominated by unfavourable 5× / 10× variance
Phase C empirical RTP97.90%100 rounds @ $10 / 2×
4.8Worked Example — RTP & Payout Verification

Real bet from Phase B — Round 831206, auto-cashout 5×, stake $0.01. Verified from data/crash-master-1100rounds.json with RTP proof derived from the uint32 survival formula:

roundId          = 831206
serverSeed       = 33c21ce3cd6b0e38dc6b9625f4aa55e73d12d3c8d5cb4924bd575f8ba74cf5e8
serverSeedHash   = 521d10b7285349bc9912f07a9301a08b6a4fd03b50c09163499af0a5be4f9ab8
drandRoundId     = 27817966
drandRandomness  = b0e4ee0678e28659e9bf4ff162928b8af401d1eced4bd49789d1bcb5ed2acb71
                   36ec52b0a224b387724aded6d078c3cb   (96 hex chars, 48 bytes)
stake            = $0.01
auto_cashout     = 5.00×

Step 1 — Crash-point computation: computeCrashPoint("33c21ce3…", "b0e4ee06…") → HMAC-SHA256 output 2221d4e0…1237e6c7 → first 4 bytes = uint32 572,642,528raw = 2³²/(572,642,528+1) × 0.999 = 7.49275…floor(× 100)/100 = 7.49

Step 2 — Win-condition check: crashPoint (7.49) ≥ auto_cashout (5.00)isWin = true

Step 3 — Payout: amount_won = 0.01 × 5 × (1 − 0.1/100) = 0.01 × 5 × 0.999 = 0.04995 ✓ (matches dataset)

Step 4 — RTP proof for target = 5×: P(crash ≥ 5) = floor(2³² × 0.999 / 5) / 2³² = 858,134,465 / 4,294,967,296 = 19.9800% · RTP = P × 5 = 99.9000%

3 columns
StepProcessOutput
1computeCrashPoint(serverSeed, drandRandomness)7.49 (crash point)
2crashPoint >= auto_cashout7.49 >= 5true (isWin)
3stake × auto_cashout × 0.9990.01 × 5 × 0.999 = 0.04995 (amount won)
4P(crash ≥ 5) × 5 (anti-circularity)19.98% × 5 = 99.9% (proves target's RTP)
RTP proof for this bet's cashout target · Verified
// Target = 5× — anti-circularity proof from first principles//// Survival probability at 5×://   threshold = floor(2^32 × 0.999 / 5)//             = floor(4,294,967,296 × 0.999 / 5)//             = floor(858,134,465.6208)//             = 858,134,465////   P(crash >= 5) = 858,134,465 / 4,294,967,296//                 = 0.19979999982751906//                 ≈ 19.98%//// Expected RTP at 5×://   = P × target//   = 0.19979999982751906 × 5//   = 0.9989999991375953//   ≈ 99.9% ✅//// Same formula works for every target x >= 1://   RTP(x) = floor(2^32 × 0.999 / x) × x / 2^32 = 0.999 - ε//   where ε is floor-quantisation (< 10⁻⁸ for all tested targets)
Parity verified: Round 831206 — crash point, win/loss flag, payout amount, and target RTP all match exactly between live game and independent verifier. RTP for target = 5× proven from the independent survival-probability formula = 99.900%, with floor-quantisation deviation of less than 10⁻⁸.
Live GamecrashPoint = 7.49 · isWin = true · payout = $0.04995
VerifiercrashPoint = 7.49 · isWin = true · payout = $0.04995
Technical Evidence & Verification5 sections
4.9Evidence Coverage Summary6 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Step 12)11 cashout targets (1.01× → 1000×), all RTP ≤ 99.9%Pass
Distribution edge audit (Step 8)effectiveEdge = 0.1 confirmed on every round; HOUSE_EDGE = 0.001 in RNG codePass
Simulation Pass 1 (Step 13)5M rounds (10 streams × 500K), Fisher's p = 0.5382, 0 / 10 streams below α = 0.01Pass
Simulation Pass 2 (Step 14)1,100 casino seeds × 1,000 drand values, 14 chi² fails ≤ 21 thresholdPass
Bet-size invariance (Step 6)100 / 100 Phase C $10 rounds verify under the identical RNG code pathPass
Serial independence (Step 13)lag1Z = −0.029, runsP = 0.0436 on combined 5M-round sequencePass
4.10Code References6 source files
2 columns
FilePurpose
tests/steps/dataset.tsStep 12: Anti-circularity — independent RTP evaluation for 11 cashout targets
tests/steps/payouts.tsStep 8: Distribution edge audit — effectiveEdge consistency + formula RTP check
tests/steps/simulation.tsSteps 13–14: Pass 1 multi-stream chi² + Pass 2 cherry-pick detection
src/simulate.tsMonte Carlo simulation (5M rounds, two-pass)
src/stats.tsChi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test, Fisher's combined
src/rng.tssurvivalProbability, expectedRTP, instantCrashProbability, HOUSE_EDGE constant
4.11Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json — Pass 1 (5M rounds, 10 streams × 500K, per-stream chi² + Fisher's combined + serial independence) + Pass 2 (1,100 casino seeds × 1,000 drand values, cherry-pick detection)
Primary dataset: data/crash-master-1100rounds.json — 1,100 live rounds for bet-size invariance verification (Phase C), house edge consistency audit (effectiveEdge = 0.1 on all 1,100), and empirical RTP informational items
Verification output: outputs/verification-results.json — Steps 6, 8, 12, 13, 14 (S4-relevant)
4.12Verified Invariants12 invariants
2 columns
InvariantResult
P(crash ≥ x) × x = 0.999 for all 11 tested targets (non-circular — survival from uint32 formula)Pass
HOUSE_EDGE = 0.001 hardcoded in RNG code — no scaling array, no per-target overridePass
effectiveEdge = 0.1 on all 1,100 live rounds (flat edge applied uniformly)Pass
Survival formula P(crash ≥ x) = floor(2³² × 0.999 / x) / 2³² matches computeCrashPoint output distributionPass
Mean simulated RTP across 5 targets = 99.8720% (within 0.03% of 99.9% theoretical)Pass
Fisher's combined p ≥ 0.01 — observed 0.5382 across 10 × 500K streamsPass
0 / 10 streams reject at per-stream α = 0.01Pass
0 serial-independence failures on 5M-round combined sequence (lag1Z, runsP both within bounds)Pass
No evidence of seed pre-selection across 1,100 casino seeds (Pass 2) — 14 chi² fails ≤ 21 thresholdPass
Mean Pass 2 casino-seed RTP = 99.9638% (within 0.07% of theoretical)Pass
Phase C ($10) crash points verify deterministically under the same algorithm used at $0.01Pass
Bet amount absent from RNG input by construction (computeCrashPoint signature takes only seeds)Pass
4.13Reproduction InstructionsRun locally
reproduce-s4.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crash && npm installnpm run simulate  # 5M multi-stream simulation + cherry-pick testnpm run verify    # Steps 6, 8, 12, 13, 14 cover S4cat outputs/simulation-results.json
S4-related steps:

[PASS] Step 6   — Bet-Size Invariance
[PASS] Step 8   — Distribution Edge Audit (pre-rakeback)
[PASS] Step 12  — Anti-Circularity (distribution edge, pre-rakeback)
[PASS] Step 13  — Simulation Pass 1
[PASS] Step 14  — Simulation Pass 2 (Casino Seeds)
05

Fairness Integrity Testing

Does the implementation maintain fairness under non-standard conditions?

Sections 1–4 prove the game is mathematically fair. Section 5 proves the implementation maintains integrity under non-standard conditions. We applied 16 standard fairness integrity tests covering seed integrity, commitment timing, outcome determinism, cross-player isolation, payout integrity, and house-edge / distribution integrity. For Crash, the framework is adapted from the pan-game matrix to reflect the dual-entropy (server seed + drand) architecture: there is no client seed to test, no nonce sequence per player, and the commit-reveal chain is anchored against an external public beacon rather than a per-player seed pair.

Fairness Integrity Testing

14pass1N/A1FLAG

What we verified

  • Seed commitment — server seed hash published before drand beacon emits (1,100 / 1,100 verified)
  • drand authenticity — every beacon value matches the public drand chain byte-for-byte (1,100 / 1,100)
  • Pre-commitment timing — bet placed before drand publication for every round (min margin 0.404s)
  • Outcome determinism — identical inputs produce identical crash points (1,100 / 1,100 recomputed)
  • Cross-player isolation — all players see the same crash point per round (global outcome model)
  • Distribution integrity — crash-point distribution matches uint32 survival formula (Fisher's p = 0.5382)

What this means for you

  • No one — not the player, not the casino — can alter the crash point through the API
  • The casino commits to each round's outcome before the external entropy is even known
  • Every round is cryptographically isolated from every other — no state leakage
  • The external entropy comes from a public beacon the casino cannot forge or withhold
  • The server rejects malformed bet parameters and ignores injected outcome fields
14pass1N/A1FLAG
  • Seed commitment — server seed hash published before drand beacon emits (1,100 / 1,100 verified)
  • drand authenticity — every beacon value matches the public drand chain byte-for-byte (1,100 / 1,100)
  • Pre-commitment timing — bet placed before drand publication for every round (min margin 0.404s)
  • Outcome determinism — identical inputs produce identical crash points (1,100 / 1,100 recomputed)
  • Cross-player isolation — all players see the same crash point per round (global outcome model)
  • Distribution integrity — crash-point distribution matches uint32 survival formula (Fisher's p = 0.5382)
5 / 5Seed Integrity
2 / 2Commitment Timing
2 / 2Determinism
2 / 2Isolation
2 / 2Payout Integrity
2 / 2House Edge & Distribution

Category Coverage

Seed Integrity5/5
Commitment Timing2/2
Determinism2/2
Isolation2/2
Payout Integrity2/2
House Edge & Distribution2/2
CheckResultEvidence
Seed integrityPass5 tests — all four SEED-001..004 pass + new SEED-005 statistical quality (χ² p = 0.7413, H = 7.995 bits/byte)
Commitment timingPassBet placed before drand in 1,100 / 1,100 rounds (min margin 0.404s); drand round IDs strictly increasing
Outcome determinismPassIdentical inputs produce identical crash points — 1,100 / 1,100 confirmed. Replay protection: architecturally N/A
Cross-player isolationPassRNG state independent (lag1Z = −0.029, runsP = 0.0436); global outcome model — all players see same crash point
Payout integrityFlagField injection probed (3 betting-phase + 3 running-phase) — server computed crash point and multiplier from seed chain; no injected field reflected. Boundary re-run (FI-PAYOUT-001): the server imposes no upper bound on auto_cashout_multiplier — two values above the theoretical maximum crash point (~4,290,672,328.70×) were accepted, silently turning an auto-cashout bet into a de-facto manual one. Server-side input-validation gap — disclosed to Duel.com (see FI-PAYOUT-001 in matrix).
House edge & distributionPassAnti-circularity proved 99.9% RTP for 11 targets; Fisher's combined p = 0.5382 across 10 × 500K streams
Seed integrityPass
5 tests — all four SEED-001..004 pass + new SEED-005 statistical quality (χ² p = 0.7413, H = 7.995 bits/byte)
Commitment timingPass
Bet placed before drand in 1,100 / 1,100 rounds (min margin 0.404s); drand round IDs strictly increasing
Outcome determinismPass
Identical inputs produce identical crash points — 1,100 / 1,100 confirmed. Replay protection: architecturally N/A
Cross-player isolationPass
RNG state independent (lag1Z = −0.029, runsP = 0.0436); global outcome model — all players see same crash point
Payout integrityFlag
Field injection probed (3 betting-phase + 3 running-phase) — server computed crash point and multiplier from seed chain; no injected field reflected. Boundary re-run (FI-PAYOUT-001): the server imposes no upper bound on auto_cashout_multiplier — two values above the theoretical maximum crash point (~4,290,672,328.70×) were accepted, silently turning an auto-cashout bet into a de-facto manual one. Server-side input-validation gap — disclosed to Duel.com (see FI-PAYOUT-001 in matrix).
House edge & distributionPass
Anti-circularity proved 99.9% RTP for 11 targets; Fisher's combined p = 0.5382 across 10 × 500K streams
✓  Fairness Guarantees Verified · 1 Flag Disclosed

16 fairness integrity tests: 14 pass, 1 N/A (replay protection — single-step game), 1 FLAG (server-side input validation gap on auto_cashout_multiplier — does not break fairness). Disclosed to Duel.com.

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. For Crash, the framework is adapted to the dual-entropy model: seed integrity now includes commitment timing against a public external beacon (drand) rather than just hash commitment, and the "Nonce Integrity" category standard in client-seed games is replaced by "Commitment Timing" because drand round IDs serve as Crash's global sequence. Scope boundary: S5 tests whether fairness guarantees hold under non-standard API interaction. Platform-level implementation testing falls outside standard certification.

3 columns
CategoryTestsWhat It Catches
Seed Integrity5Hash mismatches, drand forgery, seed reuse, non-determinism, non-uniform seed generation
Commitment Timing2Late-commitment attacks, round shopping, post-hoc drand selection
Determinism3Non-deterministic outputs, bet-size influence on outcome, replay-for-duplicate-payout
Isolation2Cross-round state leakage, cross-player outcome dependence
Payout Integrity2Parameter enforcement, server-side computation verification
House Edge & Distribution2Hidden scaling edge, biased crash-point distribution
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
FLAGAnomaly detected, documented for transparencyDisclosed
HARD FAILFairness guarantee cannot be confirmedCertification blocked until remediated
2 columns
ConditionConsequence
Negative commitment margin (bet placed after drand published)Pre-commitment guarantee broken — operator could have seen beacon first
drand signature mismatch against api.drand.shExternal entropy forged — no independent randomness source
drand round ID non-monotonic (round shopping)Operator cherry-picking favourable beacons post-hoc
Crash-point recomputation mismatchUndisclosed inputs affecting outcomes
Server seed reuse across roundsCommit-reveal guarantee broken
Per-player crash-point divergence in the same roundCross-player isolation broken / hidden per-user seed
Hard fail criteria: Any single hard fail = NOT PROVABLY FAIR. The audit cannot proceed past a hard fail without operator remediation and re-verification.

16 tests · 14 pass · 1 flag · 1 N/A

Seed Integrity5/5
FI-CRASH-SEED-001Pass

Server seed is committed (hashed) before the round's drand beacon is known

S1, Step 1 — 1,100/1,100 hashes verified. SHA-256(serverSeed_bytes) === serverSeedHash published pre-drand.

FI-CRASH-SEED-002Pass

drand beacon is authentic and externally verifiable against the public drand chain

S2, Step 15 — 1,100/1,100 drand signatures cross-verified against api.drand.sh (chain 52db9ba7…4e971). 0 forgeries.

FI-CRASH-SEED-003Pass

Server seed is unique per round — no reuse across the captured sample

S1, Step 4 — 1,100 unique seeds and 1,100 unique hashes across 1,100 rounds.

FI-CRASH-SEED-004Pass

Crash point is deterministic from (serverSeed, drandRandomness) — no hidden entropy

S2, Step 5 — 1,100/1,100 crash points independently recomputed via HMAC-SHA256. 0 mismatches. API-probe confirmation: 10 complete pre/post pairs collected via WebSocket — crash_hashed_pf event captured before drand round, server_seed revealed post-round. SHA-256(server_seed) matched the pre-committed hash in 10/10 rounds. matchCount=10, mismatchCount=0.

FI-CRASH-SEED-005Pass

Captured server seeds show no statistical patterns — uniform byte distribution, high entropy, no round-order correlation

Statistical quality check on 35,200 bytes (1,100 seeds × 32 bytes): byte-value χ² p = 0.7413; Shannon entropy H = 7.99506 bits/byte; round-order × first-byte Spearman ρ = −0.0003 (p = 0.9929). All three sub-tests within thresholds.

Commitment Timing2/2
FI-CRASH-TIMING-001Pass

Bet placement precedes drand beacon publication for every round — operator commits to the seed before external entropy is known

S1, Step 2 — 1,100/1,100 rounds with positive commitment margin (min 0.404s, mean 12.196s, max 13.779s). Single negative margin = hard fail.

FI-CRASH-TIMING-002Pass

drand round IDs strictly increase across the capture window — monotonic, no replays or reordering

S1, Step 3 — 1,100 rounds, drand round IDs strictly increasing across range 27,798,178 → 27,819,827. No gaps, no backward jumps.

Determinism3/3
FI-OUTCOME-001Pass

Given identical inputs (serverSeed, drandRandomness), the game always produces the same crash point

S2, Step 5 — 1,100/1,100 crash points independently recomputed from the two inputs, zero mismatches.

FI-OUTCOME-002N/A

A completed round cannot be replayed via the API to generate a duplicate payout

Crash auto-resolves — single-step game with no separate cashout action. `auto_cashout` target is fixed at bet time; crash point and payout are auto-computed from (serverSeed, drandRandomness) alone. No separate cashout-action request exists for a completed round to replay.

FI-CRASH-DETERMINISM-003Pass

Crash point cannot be influenced by player actions or bet size — outcome is invariant under stake changes

S3, Step 6 — all 100 Phase C ($10) rounds verify under the identical RNG code path as the $0.01 rounds. Bet amount is not an input to the HMAC key or message.

Isolation2/2
FI-ISO-001Pass

RNG state is fully independent across rounds — no carry-over from one round to the next

S4, Step 13 — serial independence tested on combined 5M-round sequence: `lag1Z = −0.029` (within |Z|<3), `runsP = 0.0436` (above 0.01 threshold).

FI-ISO-002Pass

All players see the same crash point per round — single global outcome model, no per-user seed, no cross-user dependence

Architectural — `computeCrashPoint(serverSeed, drandRandomness)` takes no per-user input. RNG code path confirmed in S2 (2.1 Implementation).

Payout Integrity1/2
FI-PAYOUT-001Flag

Bet parameters cannot exceed defined limits — invalid amounts and out-of-range auto_cashout values are rejected server-side

API probe (re-run) — the server imposes no upper bound on the auto_cashout_multiplier parameter. The theoretical maximum crash point is 4,290,672,328.70× — the ceiling of the published formula floor(2³² ÷ (value + 1) × 0.999 × 100) ÷ 100 at its smallest input (uint32 value = 0). Four bets were sent over the WebSocket place-bet event to probe the boundary. Two control bets within range — multiplier=2 and multiplier=1,000,000,000 — were correctly accepted, as expected. Two boundary bets above the theoretical maximum were also accepted: multiplier=4,300,000,000 (just above the real max; bet id 1680011, round 971971) and multiplier=1,000,000,000,000,000 (bet id 1680017, round 971972). This is a server-side input-validation gap: an auto-cashout target above 4,290,672,328.70× can never be reached, so storing it silently converts the bet into a de-facto manual-cashout bet regardless of the configured multiplier — the player believes auto-cashout is armed when it is inert. The server must reject any auto_cashout_multiplier that exceeds the theoretical maximum crash point (~4,290,672,328.70×). The fairness guarantees of the commit-reveal model are unaffected: the crash point is still derived deterministically from the server seed and drand beacon and no injected field overrides it (see FI-PAYOUT-002); any payout remains bounded by the platform's max-win cap. Disclosed to Duel.com.

FI-PAYOUT-002Pass

Payout and crash-point fields cannot be injected into the bet request — server computes outcome only from canonical inputs

API probe — 3 betting-phase + 3 running-phase WebSocket bets each carrying an injected field (crash_point=1000, payout_multiplier=999, cashout_at=999). The accepted betting-phase bet (id 1476259, round 883890) returned auto_cashout_multiplier=2 with no injected field reflected in the response. The crash point is computed server-side from server_seed + drand beacon; no client-supplied field could override it. injectedFieldsLeaked=false.

House Edge & Distribution2/2
FI-CRASH-EDGE-001Pass

House edge is 0.1% as per published formula — no scaling edge, no per-target variation, analytically proven

S4, Step 12 — anti-circularity: `P(crash ≥ x) × x × 0.999 = 0.999` for 11 cashout targets (1.01× → 1000×). Independently derived from uint32 survival formula. Max deviation from 99.9%: 2.8 × 10⁻⁸ (floor quantisation only).

FI-CRASH-STAT-001Pass

Crash-point distribution matches theoretical expectations across 5M simulated rounds — no mapping bias, no serial correlation

S4, Steps 13–14 — Pass 1: Fisher's combined `p = 0.5382` across 10 × 500K streams; 0/10 streams below α = 0.01. Pass 2: 14/1,100 casino-seed chi² fails ≤ 21 threshold; mean casino-seed RTP = 99.9638%.

Technical Evidence & Verification4 sections
5.3Coverage Summary16 checks
4 columns
Test IDCategoryVerification SourceStatus
FI-CRASH-SEED-001SeedS1, Step 1 (data-driven)Pass
FI-CRASH-SEED-002SeedS2, Step 15 (data-driven)Pass
FI-CRASH-SEED-003SeedS1, Step 4 (data-driven)Pass
FI-CRASH-SEED-004SeedS2, Step 5 (data-driven)Pass
FI-CRASH-SEED-005SeedStatistical analysis (data-driven)Pass
FI-CRASH-TIMING-001TimingS1, Step 2 (data-driven)Pass
FI-CRASH-TIMING-002TimingS1, Step 3 (data-driven)Pass
FI-OUTCOME-001DeterminismS2, Step 5 (data-driven)Pass
FI-OUTCOME-002DeterminismStructural — single-step gameN/A
FI-CRASH-DETERMINISM-003DeterminismS3, Step 6 (data-driven)Pass
FI-ISO-001IsolationS4, Step 13 (simulation)Pass
FI-ISO-002IsolationStructural — global outcome modelPass
FI-PAYOUT-001PayoutAPI probeFlag
FI-PAYOUT-002PayoutAPI probePass
FI-CRASH-EDGE-001House EdgeS4, Step 12 (analytical)Pass
FI-CRASH-STAT-001DistributionS4, Steps 13–14 (simulation)Pass
Method breakdown: 13 tests verified via verification suite steps and structural analysis (seed integrity, commitment timing, determinism, isolation, edge & distribution), 1 test N/A (replay protection — single-step game), 2 tests verified via direct WebSocket API probes against the running game (FI-PAYOUT-001 flagged a server-side input-validation gap on auto_cashout_multiplier; FI-PAYOUT-002 confirmed injection prevention). The flagged finding does not affect the fairness guarantees of the commit-reveal model and was disclosed to Duel.com.
5.4Additional Integrity Evidence (S1–S4)7 records
3 columns
PropertySourceFinding
1,100 / 1,100 seed hashes verifiedS1, Step 1Commit-reveal chain intact
1,100 / 1,100 positive commitment marginsS1, Step 2Pre-commitment guarantee holds (min 0.404s)
1,100 / 1,100 drand signatures match public chainS2, Step 15External entropy verifiably authentic
1,100 / 1,100 exact crash-point parityS2, Step 5No post-RNG conditional logic, no hidden entropy
Anti-circularity proven for 11 cashout targetsS4, Step 120.1% distribution edge from first principles
14 / 1,100 Pass 2 chi² fails ≤ 21 thresholdS4, Step 14No seed pre-selection bias
Fisher's combined p = 0.5382 across 10 × 500K streamsS4, Step 13Distribution matches theoretical at scale
5.5Scope & LimitationsScope note
This certification covers the 16 standard fairness integrity tests listed above — the minimum required to verify that the provably fair implementation holds up under non-standard conditions, adapted to Crash's dual-entropy architecture. This is not a penetration test. It focuses specifically on the provably fair implementation — not the operator's broader platform security.
Standard scope: The 16 tests above are our standard Crash fairness integrity matrix. The base list is applied to every game we audit, with per-game adaptations where a test does not apply (e.g. replay protection is N/A for single-step games) or where the architecture requires different probes (Crash uses a dual-entropy model — server seed + drand beacon — and a WebSocket transport, so client-seed and REST tests are replaced with WebSocket-event tests and commitment-timing tests against the drand beacon). Additional private deep-matrix testing may be conducted as a paid add-on — results are shared with the operator under responsible disclosure.
Single-account scope: Tests were conducted from a single auditor account. Cross-player consistency (all users seeing the same crash point per round) is verified architecturally via RNG code path, not empirically through multi-account capture. Multi-account capture is available as a v2 audit extension.
No infrastructure testing: Server security, network-level protections, and operational controls are not tested. These fall under platform-level security, not game-level fairness.
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 (13 of 16): Fully reproducible from the open-source repo. These tests run against the captured 1,100-round dataset and produce deterministic results.

API probe tests (2 of 16): Verified by issuing live adversarial bets via the WebSocket transport against the running game. Per-test evidence (WebSocket events, server-assigned bet IDs, round IDs, accepted/dropped status, stored auto_cashout_multiplier, injected-field-leak flags, drand round IDs) is retained in our private adversarial-testing archive and summarised in the integrity test review document. The testing harness itself is kept private to avoid handing exploit primitives to players.

reproduce-s5-data.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crashnpm installnpm run verify
Expected output (S5-related steps):

[PASS] Step 1   — Seed Hash Integrity                  → FI-CRASH-SEED-001
[PASS] Step 2   — drand Commitment Timing              → FI-CRASH-TIMING-001
[PASS] Step 3   — drand Round Monotonicity             → FI-CRASH-TIMING-002
[PASS] Step 4   — Server Seed Uniqueness               → FI-CRASH-SEED-003
[PASS] Step 5   — Outcome Recomputation                → FI-CRASH-SEED-004, FI-OUTCOME-001
[PASS] Step 6   — Bet-Size Invariance                  → FI-CRASH-DETERMINISM-003
[PASS] Step 13  — Simulation Pass 1                    → FI-ISO-001, FI-CRASH-STAT-001
[PASS] Step 14  — Simulation Pass 2 (Casino Seeds)     → FI-CRASH-STAT-001
[PASS] Step 15  — drand API Signature Verification     → FI-CRASH-SEED-002

Statistical Quality Test (FI-CRASH-SEED-005):

fi-seed-quality.js
// FI-CRASH-SEED-005: Server-seed byte distribution quality// Runs on the 1,100 revealed server seeds from data/crash-master-1100rounds.json//// Three sub-checks (all PASS thresholds)://   1. Byte-value chi² across 256 buckets — pass if p ≥ 0.01//   2. Shannon entropy (bits/byte)        — pass if H > 7.9//   3. Round-order × first-byte Spearman  — pass if |ρ| < 0.1 AND p ≥ 0.01//// Observed://   1. χ² = 240.0145, p = 0.7413  ✓//   2. H  = 7.99506 bits/byte     ✓//   3. ρ  = −0.0003, p = 0.9929   ✓//// Overall: PASS

API Probe Tests (completed):

fi-api-probes.sh
[PASS] FI-CRASH-SEED-004 — Commit-reveal hash chain  → 10/10 SHA-256(server_seed) matched committed hash[FLAG] FI-PAYOUT-001     — Auto-cashout upper bound    → 2/2 controls OK; 2/2 above-max (>4.29B×) accepted — no upper bound — disclosed[PASS] FI-PAYOUT-002     — Field injection handling  → 0 injected fields reflected; auto_cashout_multiplier stored correctly
API probes: All API probe tests completed. Per-test evidence — WebSocket events, server-assigned bet IDs and round IDs, accepted/dropped/no-response classifications, drand round IDs, stored multipliers, injected-field-leak flags — is retained in our private adversarial-testing archive and summarised in the integrity test review document. The testing harness itself is kept private to avoid publishing exploit primitives; per-test evidence is available to operators and regulators on request. One FLAG (FI-PAYOUT-001) was disclosed to Duel.com — does not affect fairness guarantees.
06

Player Verification

Can a player verify their own bets without trusting anyone?

Every Crash round can be independently reproduced using three publicly disclosed inputs: the casino's revealed server seed, the drand round ID, and the drand beacon's randomness value. No hidden variables, no private backend data. If your calculated crash point matches the game result — and the drand beacon matches the public chain at api.drand.sh — the round was provably fair. This section walks you through the process and provides an independent verification tool built from the same code used in this audit.

Independent Round Verification

5 Stepsto verify any round

Key principles

  • Every Crash round can be independently reproduced
  • No hidden variables — no private backend data, no server-side state
  • If your computed crash point matches the game result AND the drand beacon matches the public chain, the round was provably fair
  • Most players can verify directly through the Duel.com fairness UI

What you need

  • Server Seed — revealed after the round ends (casino's committed entropy, hash published before the round)
  • drand Round ID — the external beacon round number (committed before the casino's seed is revealed)
  • drand Randomness — the BLS-signed output from drand's public chain, fetchable from api.drand.sh
5 Stepsto verify any round
  • Every Crash round can be independently reproduced
  • No hidden variables — no private backend data, no server-side state
  • If your computed crash point matches the game result AND the drand beacon matches the public chain, the round was provably fair
  • Most players can verify directly through the Duel.com fairness UI
From Bet to Independent Verification — 5-step flow including drand beacon fetch — illustrated flow
Server SeedRevealed after the round
drand Round IDCommitted beacon round
drand RandomnessBLS-signed public output
  1. 1

    Place a Crash Bet

    Set your bet amount and auto-cashout target, then place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and a drand round ID — but the drand randomness for that round hasn't been published yet, so the crash point is locked to inputs the casino can't see or change.

  2. 2

    Open the Fairness Modal

    Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed, the drand round ID, the drand randomness (now published), and the revealed plaintext server seed.

  3. 3

    Recompute & Confirm

    Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the crash point inline — if it matches your round's actual crash multiplier, the round was provably fair.

  4. 4

    Verify drand Independently (Optional)

    Open api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId} in any browser. The signature field must match the drand Randomness shown in the modal — proving the external entropy came from a public beacon the casino can't influence.

Visual Walkthroughstep-by-step
6.1Step 1: Place a Crash Bet

Set your bet amount and auto-cashout target, then place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and locked in a drand round ID — but the drand randomness for that round hasn't been published yet. The crash point is locked to inputs the casino can't see or change.

Duel.com Crash — round in progress. Bets close before the drand beacon for the round publishes.

Duel.com Crash — round in progress. Bets close before the drand beacon for the round publishes.

6.2Step 2: Open the Fairness Modal

Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed (the casino's pre-commitment), the drand round ID used for the round, the drand randomness (now publicly available), and the revealed plaintext server seed. Crash uses drand as the per-round entropy in place of a player-controlled client seed — every player in the round verifies against the same (server seed, drand randomness) pair.

Server Seed Hashed — SHA-256 of the server seed, published before the drand beacon for the round was known.

Server Seed (revealed) — plaintext value the casino committed to. Hash it to confirm it matches the pre-commitment.

drand Round ID — the round number of the public drand beacon used as the external entropy source.

drand Randomness — the 32-byte BLS-signed randomness value published by the drand chain at the scheduled time for that round.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

6.3Step 3: Recompute & Confirm

Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the crash point from the disclosed inputs and renders the Game Result inline. If the recomputed crash multiplier matches your round's actual outcome, the round was provably fair — the casino committed to its seed before the drand beacon was available, drand provided unpredictable external entropy at a schedule the casino can't influence, and the result is mathematically reproducible by anyone. The verifier also confirms the seed-hash commitment chain automatically.

Provably Fair page — drand seed and server seed populated from the modal, recomputed crash multiplier rendered inline matching the live round.

Provably Fair page — drand seed and server seed populated from the modal, recomputed crash multiplier rendered inline matching the live round.

6.4Step 4: Verify drand Independently (Optional)

The previous step proves the casino didn't change its server seed and that the inputs produce the claimed crash point. This final step proves the drand randomness itself was real — not something the casino fabricated. Open `https://api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId}` in any browser, substituting your drand Round ID from the Provably Fair modal. The response is a tiny JSON object containing the `round` and `signature`. The `signature` value must match the drand Randomness shown in the modal exactly. drand is a public distributed beacon run by an independent network of validators; the casino has no way to influence or pre-compute these values.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Manual Verification (Advanced)6 sections
6.6Why Manual Verification Matters

True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.

6.7How the Algorithm Works (Plain English)

Before each round starts, the casino locks the round's outcome using three ingredients:

  • The casino's secret server seed — committed by publishing its hash before the drand beacon for the round is known
  • The drand round ID — a public counter that advances every 3 seconds on the drand quicknet chain
  • The drand randomness — a BLS-signed value published by drand at the scheduled time, not controllable by the casino

These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a single 32-byte output. The first 4 bytes are read as an unsigned integer and mapped through the formula crash = floor(2³² / (value + 1) × 0.999 × 100) / 100 to produce the final crash point. Because the casino committed to its seed before the drand beacon was published, and because drand is a public distributed beacon the casino cannot predict or forge, neither party can influence the outcome.

6.8Casino Verifier vs ProvablyFair.org Verifier4 records

Two verification tools are available. Both should produce identical results — if they don't, something has changed.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (audit codebase)
Accessduel.com/fairness/verifyaudit.provablyfair.org/casino/duel/tools/verify-bets
Trust modelRequires trusting the casinoOpen-source, version-controlled
MonitoringNo change detectionMismatches detected if casino changes logic
6.9JavaScript Verification Script

Copy and run this in Node.js to verify any Crash round:

verify-crash.js · Standalone Node.js
const crypto = require('crypto'); const HOUSE_EDGE = 0.001;const MAX_UINT32 = 0xFFFFFFFF + 1;  // 2^32 function computeCrashPoint(serverSeed, drandRandomness) {  const keyBuffer = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandRandomness, 'hex');  const randomness = drandBytes.toString('utf-8');     // lossy but deterministic  const message    = `${randomness}:0`;   const hmac  = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);  const raw   = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE);  return Math.max(1.0, Math.floor(raw * 100) / 100);} function verifyHash(serverSeed, serverSeedHashed) {  const hash = crypto.createHash('sha256')    .update(Buffer.from(serverSeed, 'hex'))    .digest('hex');  return hash === serverSeedHashed;} // Replace with your round's valuesconst serverSeed       = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const drandRandomness  = 'YOUR_DRAND_RANDOMNESS'; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Crash point:', computeCrashPoint(serverSeed, drandRandomness));
6.10Python Verification Script

The same verification in Python (standard library only):

verify-crash.py · Standalone Python
import hashlib, hmac, math HOUSE_EDGE = 0.001MAX_UINT32 = 2**32 def compute_crash_point(server_seed, drand_randomness):    key         = bytes.fromhex(server_seed)    drand_bytes = bytes.fromhex(drand_randomness)    randomness  = drand_bytes.decode('utf-8', errors='replace')  # lossy but deterministic    message     = f'{randomness}:0'.encode('utf-8')     h     = hmac.new(key, message, hashlib.sha256).hexdigest()    value = int(h[:8], 16)    raw   = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE)    return max(1.0, math.floor(raw * 100) / 100) def verify_hash(server_seed, server_seed_hashed):    computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest()    return computed == server_seed_hashed # Replace with your round's valuesserver_seed        = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'drand_randomness   = 'YOUR_DRAND_RANDOMNESS' print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Crash point:', compute_crash_point(server_seed, drand_randomness))
6.11Evidence Screenshots3 records
2 columns
EvidenceDescription
E02Fairness page overview — "What is Provably Fair?" and "How it works" sections (Crash context)
E03Fairness verification tool — Crash tab selected, showing server seed hash, drand round ID, and drand randomness inputs
E12drand beacon verification — screenshot of api.drand.sh/v2/beacons/quicknet/rounds/{id} response for a captured round
From Bet to Independent Verification — 5-step flow including drand beacon fetch — illustrated flow
How it works
  1. 1

    Place a Crash Bet

    Set your bet amount and auto-cashout target, then place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and a drand round ID — but the drand randomness for that round hasn't been published yet, so the crash point is locked to inputs the casino can't see or change.

  2. 2

    Open the Fairness Modal

    Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed, the drand round ID, the drand randomness (now published), and the revealed plaintext server seed.

  3. 3

    Recompute & Confirm

    Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the crash point inline — if it matches your round's actual crash multiplier, the round was provably fair.

  4. 4

    Verify drand Independently (Optional)

    Open api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId} in any browser. The signature field must match the drand Randomness shown in the modal — proving the external entropy came from a public beacon the casino can't influence.

Visual Walkthroughstep-by-step
6.1Step 1: Place a Crash Bet

Set your bet amount and auto-cashout target, then place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and locked in a drand round ID — but the drand randomness for that round hasn't been published yet. The crash point is locked to inputs the casino can't see or change.

Duel.com Crash — round in progress. Bets close before the drand beacon for the round publishes.

Duel.com Crash — round in progress. Bets close before the drand beacon for the round publishes.

6.2Step 2: Open the Fairness Modal

Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed (the casino's pre-commitment), the drand round ID used for the round, the drand randomness (now publicly available), and the revealed plaintext server seed. Crash uses drand as the per-round entropy in place of a player-controlled client seed — every player in the round verifies against the same (server seed, drand randomness) pair.

Server Seed Hashed — SHA-256 of the server seed, published before the drand beacon for the round was known.

Server Seed (revealed) — plaintext value the casino committed to. Hash it to confirm it matches the pre-commitment.

drand Round ID — the round number of the public drand beacon used as the external entropy source.

drand Randomness — the 32-byte BLS-signed randomness value published by the drand chain at the scheduled time for that round.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

6.3Step 3: Recompute & Confirm

Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the crash point from the disclosed inputs and renders the Game Result inline. If the recomputed crash multiplier matches your round's actual outcome, the round was provably fair — the casino committed to its seed before the drand beacon was available, drand provided unpredictable external entropy at a schedule the casino can't influence, and the result is mathematically reproducible by anyone. The verifier also confirms the seed-hash commitment chain automatically.

Provably Fair page — drand seed and server seed populated from the modal, recomputed crash multiplier rendered inline matching the live round.

Provably Fair page — drand seed and server seed populated from the modal, recomputed crash multiplier rendered inline matching the live round.

6.4Step 4: Verify drand Independently (Optional)

The previous step proves the casino didn't change its server seed and that the inputs produce the claimed crash point. This final step proves the drand randomness itself was real — not something the casino fabricated. Open `https://api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId}` in any browser, substituting your drand Round ID from the Provably Fair modal. The response is a tiny JSON object containing the `round` and `signature`. The `signature` value must match the drand Randomness shown in the modal exactly. drand is a public distributed beacon run by an independent network of validators; the casino has no way to influence or pre-compute these values.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Technical evidence
6.6Why Manual Verification Matters

True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.

6.7How the Algorithm Works (Plain English)

Before each round starts, the casino locks the round's outcome using three ingredients:

  • The casino's secret server seed — committed by publishing its hash before the drand beacon for the round is known
  • The drand round ID — a public counter that advances every 3 seconds on the drand quicknet chain
  • The drand randomness — a BLS-signed value published by drand at the scheduled time, not controllable by the casino

These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a single 32-byte output. The first 4 bytes are read as an unsigned integer and mapped through the formula crash = floor(2³² / (value + 1) × 0.999 × 100) / 100 to produce the final crash point. Because the casino committed to its seed before the drand beacon was published, and because drand is a public distributed beacon the casino cannot predict or forge, neither party can influence the outcome.

6.8Casino Verifier vs ProvablyFair.org Verifier4 records

Two verification tools are available. Both should produce identical results — if they don't, something has changed.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (audit codebase)
Accessduel.com/fairness/verifyaudit.provablyfair.org/casino/duel/tools/verify-bets
Trust modelRequires trusting the casinoOpen-source, version-controlled
MonitoringNo change detectionMismatches detected if casino changes logic
6.9JavaScript Verification Script

Copy and run this in Node.js to verify any Crash round:

verify-crash.js · Standalone Node.js
const crypto = require('crypto'); const HOUSE_EDGE = 0.001;const MAX_UINT32 = 0xFFFFFFFF + 1;  // 2^32 function computeCrashPoint(serverSeed, drandRandomness) {  const keyBuffer = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandRandomness, 'hex');  const randomness = drandBytes.toString('utf-8');     // lossy but deterministic  const message    = `${randomness}:0`;   const hmac  = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);  const raw   = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE);  return Math.max(1.0, Math.floor(raw * 100) / 100);} function verifyHash(serverSeed, serverSeedHashed) {  const hash = crypto.createHash('sha256')    .update(Buffer.from(serverSeed, 'hex'))    .digest('hex');  return hash === serverSeedHashed;} // Replace with your round's valuesconst serverSeed       = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const drandRandomness  = 'YOUR_DRAND_RANDOMNESS'; console.log('Hash check:', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Crash point:', computeCrashPoint(serverSeed, drandRandomness));
6.10Python Verification Script

The same verification in Python (standard library only):

verify-crash.py · Standalone Python
import hashlib, hmac, math HOUSE_EDGE = 0.001MAX_UINT32 = 2**32 def compute_crash_point(server_seed, drand_randomness):    key         = bytes.fromhex(server_seed)    drand_bytes = bytes.fromhex(drand_randomness)    randomness  = drand_bytes.decode('utf-8', errors='replace')  # lossy but deterministic    message     = f'{randomness}:0'.encode('utf-8')     h     = hmac.new(key, message, hashlib.sha256).hexdigest()    value = int(h[:8], 16)    raw   = (MAX_UINT32 / (value + 1)) * (1 - HOUSE_EDGE)    return max(1.0, math.floor(raw * 100) / 100) def verify_hash(server_seed, server_seed_hashed):    computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest()    return computed == server_seed_hashed # Replace with your round's valuesserver_seed        = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'drand_randomness   = 'YOUR_DRAND_RANDOMNESS' print('Hash check:', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Crash point:', compute_crash_point(server_seed, drand_randomness))
6.11Evidence Screenshots3 records
2 columns
EvidenceDescription
E02Fairness page overview — "What is Provably Fair?" and "How it works" sections (Crash context)
E03Fairness verification tool — Crash tab selected, showing server seed hash, drand round ID, and drand randomness inputs
E12drand beacon verification — screenshot of api.drand.sh/v2/beacons/quicknet/rounds/{id} response for a captured round
✓  Any Player Can Reproduce Crash Results

Only disclosed inputs are used. Identical inputs always produce identical output. The external entropy (drand) is independently verifiable against a public chain.

07

Reproducibility & Artifacts

Can anyone independently reproduce every finding in this audit?

This section consolidates the open-source repository, datasets, output artifacts, and reproducibility posture of the audit. Every finding, every statistic, every pass/fail result can be independently reproduced by anyone with a computer and an internet connection. The repository is the credential — not this report.

Open-Source Audit Repository

f97d5b8commit audited

Repository Details

Prerequisites

  • Node.js 18+
  • npm 8+
  • Git
  • TypeScript (installed via npm)
f97d5b8commit audited
github.com/ProvablyFair-org/duel-crash

Commands to Reproduce

Installs TypeScript, ts-node, and cryptographic dependencies. npm test runs mocha (15 unit tests), then the 5M-round simulation, then the 15-step verification pipeline.

sh
git clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crashnpm install # Run entire audit end-to-end (15 unit tests + simulation + verification)npm test

Audit Reproducibility Pinning

Git Commit
f97d5b8baf234a61ca8ca9441cb6220fbf6647c7
Node Version
v18+ (tested on v22.x)
Dataset
data/crash-master-1100rounds.json (1,100 rounds, 3 phases)
Dataset Hash (SHA-256)
ea62337a8665842ad33341a7d1b435d0c986f9feb3b03609cb0b0c868f3ef58a
Audit Date
April 2026
Audit ID
PF-2026-DL05
Output Artifacts4 files generated
3 columns
FileContentsSize
verification-results.jsonSteps 1–15 results, per-step pass/fail with evidence~4KB
simulation-results.json5M-round RTP, Fisher's combined, cherry-pick detection (Pass 1 + Pass 2)~4KB
drand-api-verification.json1,100 drand signature comparisons vs api.drand.sh (pinned artifact for Step 15)~380KB
rtp-convergence.htmlInteractive per-target RTP convergence chart (self-contained HTML)~5KB
Repository Structure
duel-crash/
├── src/
│   ├── rng.ts                  → HMAC-SHA256 + uint32 threshold mapping
│   ├── simulate.ts             → Monte Carlo — 5M rounds (10 streams × 500K)
│   ├── stats.ts                → Chi-squared, Fisher's combined, autocorrelation, runs test
│   ├── loader.ts               → Dataset loader + SHA-256 hash guard
│   ├── types.ts                → Type definitions
│   └── verify-drand-timing.js  → drand API signature re-fetch (pinned artifact)
├── tests/
│   ├── verify.ts               → 15-step verification pipeline
│   ├── steps/
│   │   ├── commitment.ts       → Steps 1–4: Seed hash, timing, drand monotonicity, seed uniqueness
│   │   ├── determinism.ts      → Steps 5–6: Crash-point recomputation + bet-size invariance
│   │   ├── payouts.ts          → Steps 7–9: captured payout consistency + isWin + distribution edge audit
│   │   ├── dataset.ts          → Steps 10–12: Dataset integrity + anti-circularity
│   │   ├── simulation.ts       → Steps 13–15: Simulation Pass 1/2 + drand API auth
│   │   ├── statistical.ts      → Informational: Live per-target RTP + phase summaries
│   │   └── context.ts          → Shared context + pass/fail helpers
│   └── crash/
│       └── crashTests.ts       → 15 unit tests (Mocha)
├── data/
│   └── crash-master-1100rounds.json → 1,100 live rounds (3 phases)
├── outputs/                    → Generated by npm test
│   ├── verification-results.json → Steps 1–15 pass/fail
│   ├── simulation-results.json   → 5M rounds, per-target RTP, Fisher's combined, cherry-pick
│   ├── drand-api-verification.json → 1,100 drand signature comparisons vs api.drand.sh
│   └── rtp-convergence.html      → Interactive per-target RTP convergence chart
├── results/                 → Reserved for run artifacts (.gitkeep)
├── evidence/
│   └── E07–E11 *.png           → Game UI, fairness page, verify page, provably fair panel
├── capture/
│   └── crash-capture.reference.js → Browser round capture script
├── package.json
├── package-lock.json
├── tsconfig.json
├── .mocharc.yml
├── .gitignore
├── MANIFEST.md             → Audit ID, scope, reproduction steps
└── README.md               → Repo overview + offline npm test instructions
Step-to-Section Cross-Reference15 verification steps mapped
3 columns
StepTest NameReport Section
1Seed Hash IntegrityS1, S5
2drand Commitment Timing (Pre-Commitment)S1, S5
3drand Round MonotonicityS1, S5
4Server Seed UniquenessS1, S5
5Outcome RecomputationS1, S2, S3, S5
6Bet-Size InvarianceS3, S4, S5
7Captured Payout Consistency (pre-rakeback display values)S3
8Distribution Edge Audit (pre-rakeback)S3, S4, S5
9Phase CoverageS7
10Dataset HashS7
11drand Chain FormulaS1
12Anti-Circularity (distribution edge, pre-rakeback)S4, S5
13Simulation Pass 1S2, S4, S5
14Simulation Pass 2 (Casino Seeds Cherry-Pick)S2, S4, S5
15drand API Signature VerificationS2, S5
reproduce-audit.sh
reproduce-audit.sh
# Clone and setupgit clone https://github.com/ProvablyFair-org/duel-crash.gitcd duel-crashgit checkout f97d5b8baf234a61ca8ca9441cb6220fbf6647c7npm install # Run full audit (5M simulation + 15 verification steps; offline by default)npm test # Or run individual componentsnpm run simulate  # 5M-round multi-stream simulation (10 streams × 500K)npm run verify    # 15-step verification pipeline (reads pinned artifacts — no network)npm run timing    # optional: re-fetch all 1,100 drand signatures from api.drand.sh                  # (regenerates outputs/drand-api-verification.json — requires internet) # View generated reportscat outputs/verification-results.jsoncat outputs/simulation-results.jsoncat outputs/drand-api-verification.json
audit.provablyfair.org/casino/duel/tools/verify-bets
This is an open-source audit. Every finding, dataset, and line of verification code is publicly accessible. The 15-step verification replays offline from pinned inputs — no network required to reproduce the audit. The drand signatures have already been verified against the public chain and the match evidence is committed to the repo; the timing script is provided for reviewers who want to independently re-fetch. Clone the repo, run the tests, and verify every claim yourself.
✓  Fully Reproducible

All audit results can be independently reproduced using the pinned commit, dataset, and commands above. The dataset hash ensures you're running against the same 1,100 rounds.