Skip to main content
Audits
LIQD.com/Dice
Provisional audit

Not yet certified. Pre-launch tests passed; final certification requires public launch and an independent recheck of live bets using anonymous, self-funded accounts.

LIQD: Dice Audit

Dice

Provisional

Independent verification, backed by open-source code.

PF-2026-LQ04Audited August–September 2026 · Framework 1.1 · Release v1.1.0August–September 2026 · Framework 1.1 · v1.1.0
AUDIT RESULTPassedPassed 22/22 · Hard fails 0 · Flags 0VERDICT: PROVABLY FAIR — Full Pass, as printed by npm test.
Live Bets Verified
6,700
Parity Rate
100%
Simulated Rounds
98M
Mean RTP 99.0203% over the 98 targets × 1M bets of Pass 1
Proven RTP
99.0000%
22 / 22Verification steps passed

Verification pipeline

Real bets. Independently reproduced.

Captured on LIQD’s pre-launch environment. We rebuilt the game logic from scratch and matched all 6,700 recorded outcomes.

LIQD DICE$0.10 · Under 60.78
Bet #v_nPrj0VJIyLTIJJEawyaNonce 0 · Phase A
50 real bets · Phase ABet 1 of 50
✓ Exact match
83.66

The casino result, reproduced.

Casino outcome
83.66 · Loss
Our reproduction
83.66 · Loss
Recorded payout
$0.0000
Open source

Run this audit yourself

Open GitHub ↗

The repo is the credential. You don’t have to trust us — every scored check ships as code. Run npm test to re-run the full audit: 123 unit tests, the 22 verification steps and 6,700 live bets re-verified — or node test.js && node verify.js with no dependency install at all.

npm testCopy includes clone & installView reproduction commands ↓
Dice Audit Overview

This audit independently validates the Dice game operated by LIQD under the ProvablyFair.org Audit Framework 1.1. We placed 6,700 real bets across 134 seed epochs — every mode, two stakes, ten custom client seeds — and recomputed every roll, every win flag and every credited amount with our own implementation of the algorithm, written from a specification without LIQD’s code. The capture ran on LIQD’s pre-launch environment, so the certificate is provisional until an anonymous recapture on liqd.com confirms the same behaviour.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPassPROVABLY FAIR — Full Pass · Passed 22/22 (21 scored + 1 informational) · Hard fails 0 · Flags 0. Provisional: captured on LIQD’s pre-launch environment; an anonymous recapture on liqd.com completes certification
RTP VerifiedPass99.0000% Proven RTP · 99.0203% simulated (mean of 98 targets × 1M bets) · RTP claim rule: LIQD’s published 99% holds on all 9,800 settings, worst gap 0.000000 pp against the ±0.1 pp band (RTP-CLAIM 1.2.0, Step 15)
Live ↔︎ Verifier ParityPass100% — 6,700 / 6,700 rolls reproduced byte-identically and 6,700 / 6,700 win flags reproduced from the revealed seeds (Steps 5, 6)
Commit-Reveal SystemPass134 / 134 revealed seeds hash to their commitments; the next-seed chain is consistent on 133 / 133 rotations plus the pre-capture link into epoch 0 (Steps 1–3)
Client SeedPassPlayer-controlled and customizable — 500 / 500 custom-seed bets recompute, and a different client seed changes the roll on 8 / 8 probes (Step 14)
RNG AnalysisPassOur own implementation, written from the specification: HMAC-SHA256, one 32-bit draw per bet, values at or above 4,294,960,000 rejected — worst-case modulo bias 0 (Step 5)
Payout LogicPassAll 6,700 settlements verified — multiplier 9900 / b bit-exact on all 2,807 wins; every credit reproduced by the two-stage 8-decimal settlement with zero tolerance; 3,893 losses pay 0 (Steps 7–9, 20)
Anti-CircularityPassP(win) × m = (b / 10,000) × (9900 / b) = 0.99 from grid arithmetic alone — the closed form equals a literal 10,000-point tally on all 5,304 captured bands (Step 13) and the integer identity holds on 6,700 / 6,700 bets (Step 15)
Fairness IntegrityPass34 fairness integrity tests across 12 categories — 33 pass, 1 remediated (overload handling under extreme bursts, Low — fixed by LIQD and re-verified); no test altered, replayed or injected an outcome
DeterminismPassFull reproducibility confirmed — 6,700 / 6,700 outcomes and credits are determined by (roll, band, stake) alone (Step 20)
Overall StatusPass
PROVABLY FAIR — Full Pass · Passed 22/22 (21 scored + 1 informational) · Hard fails 0 · Flags 0. Provisional: captured on LIQD’s pre-launch environment; an anonymous recapture on liqd.com completes certification
RTP VerifiedPass
99.0000% Proven RTP · 99.0203% simulated (mean of 98 targets × 1M bets) · RTP claim rule: LIQD’s published 99% holds on all 9,800 settings, worst gap 0.000000 pp against the ±0.1 pp band (RTP-CLAIM 1.2.0, Step 15)
Live ↔︎ Verifier ParityPass
100% — 6,700 / 6,700 rolls reproduced byte-identically and 6,700 / 6,700 win flags reproduced from the revealed seeds (Steps 5, 6)
Commit-Reveal SystemPass
134 / 134 revealed seeds hash to their commitments; the next-seed chain is consistent on 133 / 133 rotations plus the pre-capture link into epoch 0 (Steps 1–3)
Client SeedPass
Player-controlled and customizable — 500 / 500 custom-seed bets recompute, and a different client seed changes the roll on 8 / 8 probes (Step 14)
RNG AnalysisPass
Our own implementation, written from the specification: HMAC-SHA256, one 32-bit draw per bet, values at or above 4,294,960,000 rejected — worst-case modulo bias 0 (Step 5)
Payout LogicPass
All 6,700 settlements verified — multiplier 9900 / b bit-exact on all 2,807 wins; every credit reproduced by the two-stage 8-decimal settlement with zero tolerance; 3,893 losses pay 0 (Steps 7–9, 20)
Anti-CircularityPass
P(win) × m = (b / 10,000) × (9900 / b) = 0.99 from grid arithmetic alone — the closed form equals a literal 10,000-point tally on all 5,304 captured bands (Step 13) and the integer identity holds on 6,700 / 6,700 bets (Step 15)
Fairness IntegrityPass
34 fairness integrity tests across 12 categories — 33 pass, 1 remediated (overload handling under extreme bursts, Low — fixed by LIQD and re-verified); no test altered, replayed or injected an outcome
DeterminismPass
Full reproducibility confirmed — 6,700 / 6,700 outcomes and credits are determined by (roll, band, stake) alone (Step 20)
Audit scope & reference

What Was Audited

✓ Audited

  • The RNG is deterministic: every roll reproduces from server seed, client seed and nonce
  • Server seeds are committed by SHA-256 hash, and each rotation pre-commits the next seed’s hash
  • The client seed is player-controlled and changes the roll
  • Nonces run 0–49 in every seed epoch with no gaps or reuse
  • The roll is one HMAC-SHA256 draw on the 10,000-point grid, with bias-free rejection sampling
  • The win rule is the half-open band [lower, upper) — lower bound in, upper bound out — with outside its exact complement
  • Winning multipliers equal 9900 / b for the band’s basis points — bit-exact on every captured win — and settle through a reproducible two-stage 8-decimal rounding
  • Proven RTP is 99.0000% on every band in every mode (flat 1.00% house edge), asserted in exact integer arithmetic
  • LIQD’s published 99% RTP holds on all 9,800 settings under the RTP claim rule (±0.1 pp)
  • Bet amount does not influence the roll or the edge
  • Players can independently verify every bet

What Audit Excludes

  • Infrastructure or server security
  • Wallet, payments, or operational systems outside game logic
  • Cross-account sampling
What Audit Covers
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 server seed hashing, pre-bet commitment, next-seed pre-commitment, reveal on rotation
Client Seed OriginPlayer-controlled seed set after the server commitment — our own random seeds in 124 epochs and 10 custom seeds in Phase D
Seed HandlingClient seed control, nonce lifecycle, seed epoch rotation
RNG AnalysisOur own HMAC-SHA256 implementation, written from the specification; LIQD’s verifier used as a cross-check; rejection sampling, bias bound, boundary semantics
Payout LogicQuotient multiplier 9900 / b, two-stage 8-decimal settlement, loss settlement, wallet-field reconciliation, bet-size invariance (Phase C)
Live ParityIndependent roll and win-flag recomputation vs live game results across all four capture phases
RTP ValidationInteger edge identity, literal grid enumeration, the RTP claim rule over all 9,800 settings, simulated RTP (98 targets × 1M bets), cherry-pick test on the served-band return
Fairness IntegrityIntegrity tests on LIQD’s running service — 34 across 12 categories, including race conditions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, information disclosure and the provably fair surface
What Audit Guarantees
  • Outcomes are deterministic and reproducible from the recorded inputs
  • Live game results match independent recomputation for the verified sample (6,700 / 6,700)
  • Rolls are consistent with the uniform distribution expected from the rejection-sampled HMAC draw — 2,000,000 simulated draws and the 6,700 captured rolls both pass their uniformity screens
  • RTP is proven analytically: P(win) × multiplier = (b / 10,000) × (9900 / b) = 0.99 for every band, asserted in exact integers on all 6,700 captured bets
  • LIQD’s published 99% RTP is met by every one of the 9,800 settings, each exactly 99.000000%
  • Mean simulated RTP of 99.0203% across 98 targets × 1,000,000 bets (over 2.00 to over 99.00) — 0.98 standard errors from the proven 99.0000%, with every target inside the 5·SE tolerance
  • The roll is fixed by the seed pair and nonce before the band is applied — which band you choose and how much you stake never change the number rolled
  • Client seed is a genuine, player-controlled input that materially influences results (500 / 500 custom-seed bets recompute; 8 / 8 sensitivity probes change the roll)
  • The house edge is a flat 1.00% on every band in every mode — settlement rounding at the 8th decimal is directionless (1,341 wins rounded down, 1,316 up, 150 exact) and keeps the settled RTP between 98.999995% and 99.000005%
  • Every fairness integrity test passed or was remediated and re-verified — no test altered, replayed or injected an outcome, and the one finding never touched the provably fair layer

References

Dice — Game Rules7 sections▶

How LIQD Dice plays, exactly as verified by this audit — the interactive slider below is wired to the audited win rule and multiplier formula.

Game Rules

Dice rolls a single number from 0.00 to 99.99 — 10,000 equally likely two-decimal outcomes. Before you roll, you choose a band on that scale in one of four modes: under (from 0.00 up to a limit), over (from a limit up to 100), inside (between two limits) or outside (everything except an inside band). You win if the roll lands in your winning region. The region is half-open — a roll exactly on the lower bound wins, a roll exactly on the upper bound loses — and outside is the exact complement. The roll itself comes from one HMAC-SHA256 draw seeded by (serverSeed, clientSeed, nonce), fixed before your band or stake is applied; your choices set the odds and the price, never the number.

How to Play

1. Choose a mode — under, over, inside or outside — and set the band limits; the win chance is the width of the winning region in percentage points.
2. Set the stake — the multiplier is quoted immediately as 9900 / b, where b is the win chance in basis points (a 50% band pays 1.98×, a 1% band 99×).
3. Roll — the platform derives the roll from the committed server seed, your client seed and the current nonce, then applies your band to it.
4. Settle — a win credits stake × multiplier, rounded in two stages at the 8th decimal; a loss pays 0. The nonce advances by 1.
5. Verify — after rotating your seed pair, recompute any roll from the revealed seed (Section 6 walks it).

Boundary note: the lower bound is inclusive and the upper bound exclusive on every mode. LIQD’s own verifier confirms it with the roll exactly on each bound (E13, S4.7) — and it is what makes the house edge exactly 1.00% on every band.
Win Conditions

Four modes, one rule — inBand = roll ≥ lower AND roll < upper, inverted for outside. Every example below is a real captured bet from epoch 0, recomputed by this audit:

3 columns
ModeWinning regionExample (epoch 0)
Under0.00 ≤ roll < upperUnder 92.37 · roll 24.01 → win, 1.07177655× (bet kzLjSr11…)
Overlower ≤ roll < 100Over 73.31 · roll 88.28 → win, 3.70925440× (bet M6RZjoxr…)
Insidelower ≤ roll < upperInside 59.31–83.26 · roll 42.76 → loss, payout 0 (bet sbQbiJS7…)
Outsideroll < lower OR roll ≥ upperOutside 16.81–85.72 · roll 11.94 → win, 3.18430363× (bet 9uY-gCHz…)
The win/loss label is not taken on trust: it is recomputed from the seed-derived roll for every one of the 6,700 rounds in the dataset (Step 6), and every credited amount is rebuilt from the band and stake alone (Step 20).
Risk vs Reward

Every band returns the same 99.0000% — the win chance changes the shape of the risk, never the fairness of the price:

  • Even money: a 50% band (under 50, over 50, inside 25–75, outside 25–75) pays 1.98×
  • Long shots: a 10% band pays 9.9×; a 2% band 49.5×; a 1% band (over 99) 99× — the largest multiplier won in this capture
  • The tail: a 0.01% band ([0, 0.01), one grid point) quotes 9900× — the UI’s ceiling, accepted live on a losing bet (E14, S4.7)
  • Variance scales with the multiplier, the edge does not: P(win) × multiplier = 0.99 on every band
Parameters
3 columns
ParameterValueNotes
Roll domain0.00 – 99.9910,000 grid points (RANGE = 10000), two decimals
Modesunder · over · inside · outsideAll four captured live — 1,362 / 2,592 / 1,360 / 1,386 bets
Win chance0.01% – 98%Every basis point from 0.01% to 98.00% — 9,800 settings, all held to the RTP claim (S4.2); settings API reports minOdds 1.0102
House edge1.00% flatApplied once inside the multiplier — identical on every band
Multiplier9900 / bb = win chance in basis points; 1.0105× – 99× captured
Seed epoch50 betsNonces 0–49, then the pair rotates — the audit's cadence, not a platform limit
Settlement8-decimal, two-stageround8(m) then stake × round8(m) rounded half-even at 1e-8 — reproduced with zero tolerance
Stakes captured$0.10 and $10.006,500 bets at $0.10, 200 at $10.00 (Phase C)
Seed Formats

The three verification inputs, exactly as they appear in LIQD's panel and the bet record:

4 columns
Seed TypeFormatExamplePurpose
Server seed32-char hex (16 bytes)015307d5a3f62db476210c3797bac8b7HMAC key (decoded); committed via SHA-256 of the string before play
Client seedfree-form stringauditf2456fc140acPlayer-editable entropy — mixed into every draw
Nonceinteger 0–490The bet counter under the active pair
Two hashing domains: the commitment hashes the hex string (UTF-8); the HMAC keys the decoded bytes. Both conventions are verified in S1–S2.
Multiplier & Payout

The payout is a formula — no paytable file exists. The winning width in percentage points becomes basis points, and 9900 (10,000 less the 1% edge) is divided by it:

W = inverted ? 100 − (upper − lower) : upper − lower
b = round(100 × W)
m = 9900 / b
4 columns
BandWin chancebMultiplier
under 5050.00%50001.98000000
inside 25–75 · outside 25–7550.00%50001.98000000
under 92.37 (featured bet)92.37%92371.07177655
over 9010.00%10009.90000000
over 982.00%20049.50000000
over 991.00%10099.00000000
under 0.010.01%19900.00000000
The served multiplier field equals the binary64 quotient 9900 / b bit-for-bit on all 2,807 captured wins (Step 7). Your balance is credited stake × round8(m), rounded half-even at the 8th decimal (Step 8) — in this capture that rounding went the player’s way 1,316 times, against them 1,341 times and landed exactly 150 times.
Why Provably Fair Matters▶

Traditional online casinos require players to trust that games are fair. Provably fair systems eliminate that requirement — three properties, all verified in this audit, make trust unnecessary:

  • The casino commits to the inputs that determine a result before you play — the SHA-256 commitment locks the server seed while it is still secret
  • A source of randomness exists that the casino cannot predict — your client seed, editable after the commitment is published
  • Anyone can verify the outcome after the fact — the revealed seed plus public code reproduces every roll exactly
High-Level Overview8 sections▶

The whole system in one pass — from commitment to payout, with the capture design that tested it.

Checklist Reference

Commit-Reveal

2 columns
CheckWhat it proves
Server seed hash shown before playWith the hash shown first, the casino cannot change the roll after you bet
SHA-256 of the revealed seed matches the commitmentThe revealed seed is the one that was committed
Next epoch's hash pre-committed at rotationThe casino cannot mint a fresh seed in response to your play
One commitment per epoch, no mid-epoch switchingEvery bet is pinned to a single verified commitment

Seed System

2 columns
CheckWhat it proves
Client seed is player-editable after the commitment existsThe operator cannot pre-evaluate seeds against yours
Client seed changes the roll when substitutedYour seed genuinely enters every derivation
Nonces run contiguously 0–49 in every epochNo skipped bet where a discarded outcome could hide

RNG & Parity

2 columns
CheckWhat it proves
Roll recomputes from (serverSeed, clientSeed, nonce)No hidden inputs influence the number rolled
Rejection sampling below maxFair = 4,294,960,000Every one of the 10,000 grid points is exactly equally likely
Half-open win rule reproduces 6,700 / 6,700 win flagsThe game settles under the half-open rule its own verifier returns at the bounds (E13)

Payout & RTP

2 columns
CheckWhat it proves
Wins credit stake × round8(9900 / b) half-even at 1e-8The advertised 1.00% edge is the real edge
100 · winCount · 9900 == 99 · 10,000 · b on every captured bet, as integersThe RTP is an identity, not a sampled estimate
Closed-form win count equals a literal 10,000-point tallyThe probability side cannot share an arithmetic slip with the formula

Integrity

2 columns
CheckWhat it proves
Losses pay exactly 0 with multiplier 0No round settles money against a losing roll
Identical engine at $0.10 and $10.00Stake size cannot reach the roll
Dataset SHA-256 pinned (a mismatch stops the suite); simulation and chart pins scored in Step 19The evidence itself is tamper-evident
High-Level Flow

One round of LIQD Dice, from commitment to settlement:

1. Commit — the server generates a 32-hex server seed and publishes SHA-256(utf8(seed)) as hashedServerSeed. You see only the hash.
2. Seed pair active — your client seed (platform default or your own) and a nonce starting at 0 pair with the committed seed.
3. Bet — you pick a mode, a band and a stake. The roll is already fully determined by (serverSeed, clientSeed, nonce).
4. Roll — one HMAC-SHA256 draw, reduced to 0.00–99.99, is compared with your band under the half-open rule.
5. Settle — wins credit stake × 9900 / b through the two-stage 8-decimal rounding; losses pay 0. The nonce increments by 1.
6. Rotate & verify — rotation reveals the outgoing seed (and pre-commits the next). Check the hash, recompute any roll.

High-Level Flow — illustrated flow
Provably Fair Model

The model splits control of the randomness. The operator controls the server seed but must commit to it before play; the player controls the client seed and can change it after seeing the commitment; the nonce is a public counter. Because the roll is a pure function of those three inputs — and the band and stake enter only afterwards, as the rule and the price — neither side can steer an outcome without the other side's input, and everything needed to check the result is disclosed by design.

Commit-Reveal Model

Three phases, each independently verified by this audit:

Commit-Reveal Model — illustrated flow

Commit phase
The server generates a 32-hex seed and publishes SHA-256 of that string (UTF-8) as hashedServerSeed. The raw seed stays secret. Verified for all 134 epochs (Step 1).

Bet phase
Bets settle under the committed pair; the nonce increments 0–49. The roll is fixed per (seed, clientSeed, nonce) before the band is applied. Verified by full recomputation (Step 5).

Reveal phase
Rotation reveals the outgoing seed and pre-commits the next epoch’s hash — the chain is consistent on 133 / 133 transitions, plus the pre-capture link into epoch 0 (Step 2).

Verify phase
Anyone re-hashes the reveal against the commitment and recomputes every roll it served — the panel, the ProvablyFair.org verifier, or the standalone scripts in S6 all work.

Player-Controlled Client Seed

The client seed is the mechanism that structurally contains server-seed cherry-picking — when it is unpredictable and set after the commitment — and this audit exercised the control live:

  • Editable at any time in the fairness panel — including immediately after a new commitment is published
  • Mixed into every HMAC message (clientSeed:nonce:cursor) — it mathematically cannot be ignored
  • Phase D played 500 rounds under 10 auditor-chosen pfaudit- seeds — all recomputed exactly
  • Sensitivity probe: appending one character to the client seed changed the roll on 8 / 8 probed Phase D bets (Step 14)
Nonce Lifecycle

The nonce is the bet counter under an active pair. In this capture the audit rotated the pair after every 50 bets: the nonce starts at 0 and increments by 1 per bet. A gap or reuse would be the fingerprint of a discarded outcome — the audit found neither, anywhere.

  • Contiguous 0–49 in every one of the 134 epochs
  • 0 gaps, 0 duplicates, 0 resets across 6,700 bets
  • Each seed record pins nonceStart / nonceEnd — the capture’s own record of the window it served
seed record · nonce window · Verified
"epoch": 0,"nonceStart": 0,"nonceEnd": 49,"hashedServerSeed": "918c2a302c7219c91cf31ce3619acb0a…"
Determinism Guarantee

Determinism is what turns a fairness claim into a checkable fact: the same inputs must always produce the same roll. LIQD's derivation is a pure function of three arguments — verified by reproducing every captured roll with an independent implementation, byte-identical on the two-decimal grid.

the pure function · Verified
diceRoll(serverSeed, clientSeed, nonce)  → 2401   // the roll in hundredths (24.01 on the 0.00–99.99 grid) — nothing else in, nothing else out
Phase Map

The capture is engineered — four phases, each isolating one claim:

3 columns
PhaseRoundsWhat it isolates
A5,000Random bands across all four modes — 5,304 distinct bands over the whole capture, $0.10
B1,000The high-multiplier tail — 500 bets on over 98 (49.5×) and 500 on over 99 (99×)
C200Stake independence — over 50 at $10.00, 100× the $0.10 stake
D500Client-seed influence — 10 auditor-chosen seeds, live
Why phases matter: a random sample proves averages, but a designed capture proves mechanisms. Each phase removes one degree of freedom that an unfair implementation would need — every mode at volume, the rare-win tail thickened, stake varied 100×, and ten auditor-chosen seeds.
Technical Glossary7 categories▶

Every term used in this report, defined the way the audit uses it.

Core Concepts
2 columns
TermDefinition
Provably FairA cryptographic scheme in which the game outcome can be independently verified by the player after the fact, using seeds committed to before the round. The player cannot alter the server seed; the server cannot alter it after committing to its hash; neither party can change the roll without detection.
Commit-RevealThe mechanism underlying provably fair. The server publishes hashedServerSeed (a commitment) before any bet, then reveals the plaintext serverSeed after the seed rotates. The auditor checks SHA-256(utf8(serverSeed)) == hashedServerSeed for every epoch. All 134 revealed seeds in this dataset passed.
House EdgeThe operator's cut. Nominal houseEdge = 1 (%) per /system/get-game-settings (an advertised label). The effective edge is derived independently and is exactly 1.00% on every band, because the winning region holds exactly b of the 10,000 grid points and the multiplier is 9900 / b — the product is 0.99 as an integer identity, before the 8-decimal settlement rounding (a ≤ 5.5e-9 residual per win, in both directions).
RTP (Return to Player)1 − houseEdge. For LIQD Dice the theoretical RTP is a flat 99.0000% on every band in every mode — proven analytically (integer identity, Step 15), by literal enumeration (Step 13) and checked against 98M simulated bets across 98 targets. Live-bet RTP (103.56%) is informational only.
Anti-CircularityThe requirement that theoretical RTP and simulation expectations derive the win probability from the grid geometry alone (b / 10,000), never from an operator-supplied odds or win-chance field — so the RTP claim cannot validate itself.
RTP Claim RuleRTP-CLAIM 1.2.0, part of the audit method: the RTP a casino publishes — on the game page, fairness page, help page, bet panel, rules or game settings — must hold at every setting to within ±0.1 percentage points. A house-side miss ends the verdict as RTP FIGURE OVERSTATED. LIQD’s published 99% holds on all 9,800 Dice settings.
Seed System
2 columns
TermDefinition
Server SeedA 32-character (16-byte) hexadecimal string generated by LIQD. Kept secret during betting, revealed after epoch rotation. Used as the HMAC key in its hex-decoded form: Buffer.from(serverSeed, 'hex').
Commitment / Server Seed HashSHA-256(utf8(serverSeed hex string)) — the text-hash convention: LIQD hashes the UTF-8 bytes of the 32-character hex string, not the 16 decoded bytes. Published before any bet under the pair. A revealed seed that fails this check would prove tampering. Distinct from the HMAC key, which uses the decoded bytes.
Client SeedA string controlled by the player (or generated by the platform), mixed into the HMAC message as ${clientSeed}:${nonce}:${cursor}. Changing it changes every roll under the same server seed. When the player sets an unpredictable seed after the server-seed commitment is published, it is the primary structural defence against server-seed cherry-picking; a predictable seed does not deliver that protection.
NonceAn incrementing integer identifying a bet's position within an epoch. Range 0–49 (50 bets per epoch). Part of the HMAC message. Every epoch in this dataset has contiguous nonces 0–49 — no gaps.
EpochA sequence of exactly 50 bets under one (serverSeed, clientSeed) pair, nonces 0–49. At epoch end the seed rotates and the previous server seed is revealed. This dataset covers 134 epochs.
Cryptographic Functions
2 columns
TermDefinition
HMAC-SHA256Hash-based Message Authentication Code using SHA-256. Produces a 256-bit (32-byte) deterministic output from a key and message. Here it generates the single uniform integer behind each roll: the digest is scanned in 4-byte big-endian uint32 chunks, the first chunk below the modulo-bias ceiling is taken % 10000.
CursorThe third component of the HMAC message. Dice makes one draw per bet, so cursor = 0 for every roll. If all eight chunks of a digest were rejected — about 7 × 10⁻⁴⁷ per bet — the specification defines no further draw, and our implementation stops with an error; no captured bet comes close.
Modulo-Bias RejectionDiscarding uint32 values at or above maxFair = floor(2³² / 10000) × 10000 = 4,294,960,000 before taking % 10000, so every grid point is exactly equally likely. Without it the lowest 7,296 residues would be favoured by one part in 429,496.
Rolldraw / 100 — the accepted integer draw in [0, 10000) placed on the two-decimal scale 0.00 – 99.99. The served field is compared with the recomputed value byte-for-byte on that grid (Step 5).
Verification Terms
2 columns
TermDefinition
Basis Points (b)The winning width in hundredths of a percent — round(100 × W) — and, under the half-open rule, exactly the number of grid points that win. An auditor-derived quantity: the field does not appear in the capture.
Quoted Multiplier9900 / b — the value LIQD serves and pays. Reproduced bit-for-bit as a binary64 quotient on all 2,807 wins; the equivalent float form (100 / W) × 0.99 is the same rational but agrees bit-for-bit on only 1,719 of them, which is why the audit pins the quotient.
Two-Stage Settlementround8(m) (half-up at the 8th decimal), then stake × round8(m) rounded half-even at the 8th decimal, in exact integer arithmetic on 1e-8 units. Reproduces all 6,700 credited amounts with zero tolerance; 274 wins sit on an exact tie where the half-even rule decides the last unit.
Half-Open Band[lower, upper) — the lower bound wins, the upper bound loses; outside is the exact complement. Reproduces all 6,700 win flags and is pinned at the boundary by LIQD's verifier endpoint (E13: two discriminating probes).
Game Mechanics
2 columns
TermDefinition
Band (params)The requested {lower, upper, inverted} — two-decimal bounds on the roll scale plus the inversion flag. The only game-specific inputs; there are no risk levels or variants. 5,304 distinct bands were captured.
ModeHow the audit classifies a band for reporting: outside if inverted, under if lower = 0, over if upper = 100, otherwise inside. All four are present at volume (1,362 / 2,592 / 1,360 / 1,386 bets).
Win ChanceThe continuous width of the winning region in percentage points, W = inverted ? 100 − (upper − lower) : upper − lower. Under the half-open rule the discrete probability b / 10,000 equals it exactly.
Audit Terms
2 columns
TermDefinition
Draw UniformityA simulation test: 2,000,000 draws binned into 100 cells against N/100, χ² with df = 99 from the exact regularized incomplete gamma. Two-sided at α/3 — the lower tail rejects a statistic too uniform to be a real 2,000,000-draw sample. χ² = 108.885, p = 0.2335.
Serial ScreenLag-1 autocorrelation (z = r₁ · √n) and the Wald-Wolfowitz runs test on the first 200,000 simulated rolls, each judged at α/3. z = 0.159 against the applied critical 2.9352; runs p = 0.6910.
Effective-Edge BandsNine named bands (under 50, over 50, under 10, over 90, inside 25–75, outside 25–75, over 98, over 99, under 1) simulated at 2,000,000 bets each; each simulated RTP must sit within 5 × SE + 0.0001 of 0.99 and reconstruct bit-for-bit from an integer win count.
98 Over-Mode TargetsThe main RTP simulation: every over-mode band from over 2.00 to over 99.00 (win chance 98% to 1%, 1.0102× to 99×), 1,000,000 bets each on our own seeds. Each target must sit within 5 × SE + 0.0001 of its theoretical RTP and reconstruct bit-for-bit from an integer win count; the mean over the 98 targets is the convergence chart in S4.
Bonferroni CorrectionThe three Pass 1 screens (uniformity, lag-1, runs) each run at α/3 = 3.333e-3 so the family has level α = 0.01.
Served-Band ReturnThe Pass 2 cherry-pick statistic: each of the 134 revealed seeds is replayed for 10,000 nonces with its recorded client seed, and the payout-weighted return of the bands LIQD actually served in the captured window (nonces 0–49) is compared with its exact expectation and variance. A one-sided test at α = 0.01: a seed set chosen against the player would show a return deficit.
Exact Discrete NullThe reference distribution of the supplementary draw-uniformity screen in Pass 2: for a 50-draw window over 20 bins the statistic S = Σ hᵢ² takes only 832 attainable values, enumerated exactly from 181,274 occupancy partitions. No bootstrap and no asymptotic approximation — it replays identically on every run.
Cherry-Picking (Server-Seed)The residual risk in any beacon-free commit-reveal scheme: an operator with foreknowledge of the upcoming client seed could pre-evaluate candidate server seeds and commit to an unfavourable one. The structural defence is a client seed set after the commitment; the statistical check is the served-band return in Pass 2 (z = 0.752, p = 0.7738), which shows no return deficit.
Selective OrphaningA distinct residual channel: an operator observing a round server-side and orphaning or re-issuing an unfavourable result rather than surfacing it. In this capture the nonces run contiguously 0–49 in all 134 epochs (Step 4); a signed server-side audit log closes the channel in general.
Hard FailAny FAIL on a scored step — a broken commitment or chain link, a roll, win-flag, multiplier or credit mismatch, an operator-side nonce gap, a population or artifact-integrity fault. A hard fail ends the verdict as NOT PROVABLY FAIR. None triggered: Passed 22/22, Hard fails 0, Flags 0.
Capture FlagA flag raised when a gap in the record comes from our capture script — for example, a retried request that skipped a nonce — rather than from the casino. It is disclosed with a limitation row and does not change the verdict; an operator-side gap is a hard fail. This audit has 0 capture flags.
Data Formats
2 columns
TermDefinition
winningAmountThe settle-response field holding the amount credited on a win or 0 on a loss. The audit's payout evidence; verified against the two-stage settlement of betAmount × 9900 / b at zero tolerance, and reconciled with the wallet-denominated winningAmountWallet on all 6,700 bets (the capture records per-bet amounts, not running wallet balances).
roll, win, multiplierResponse values stored after numeric conversion. roll is compared byte-for-byte on the two-decimal grid; multiplier is 9900 / b on wins and 0 on losses; win is re-derived from the roll and band.
Phase A / B / C / DThe four capture phases. A: 5,000 bets, random bands across all modes. B: 1,000 bets on over 98 and over 99. C: 200 bets on over 50 at $10.00 (stake independence). D: 500 bets under 10 custom pfaudit- client seeds.
01

Seed, Nonce & Determinism

Can the casino change your outcome after you bet?

Every Dice roll on LIQD is generated from three inputs: server seed, client seed, and nonce. LIQD commits to its server seed by publishing a SHA-256 hash before the seed is used — and pre-commits the next epoch’s hash at every rotation. After you rotate your seed, the server reveals the actual seed, and anyone can check it against the hash. A matching hash shows which seed goes with which bets and in what order, and our capture records show each hash was published before its bets — so the casino cannot change the roll after you bet.

Commit-Reveal Verification

134 / 134seeds verified

What we verified

  • Every revealed server seed hashes to the commitment published for its epoch — 134 / 134
  • The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 133 / 133 links, plus the pre-capture link into epoch 0
  • Client seed is player-controlled and set after the server commitment
  • Players can set or change their client seed at any time via the fairness panel
  • Nonce increments by 1 per bet with no gaps or duplicates across 134 seed epochs — 0 capture flags
  • The roll is fully determined by the seed inputs before the band or stake is applied
  • Identical inputs always produce the same roll — confirmed across all 6,700 bets, byte-identical on the two-decimal grid
  • Your client seed is a genuine input — changing it changes the roll

What this means for you

  • With the commitment shown first, the casino cannot change the roll after you bet
  • You contribute randomness the server cannot predict or pre-select against
  • Every bet has its own derivation — the nonce guarantees no two bets share the same inputs
  • Any result can be independently verified using the public tools and repo
  • Outcomes stay verifiable months later — the revealed seeds and the algorithm are all you need
  • Server-seed cherry-picking is structurally contained — you can set or rotate your client seed after the commitment is published, so the operator cannot pre-evaluate seeds against it
134 / 134seeds verified
  • Every revealed server seed hashes to the commitment published for its epoch — 134 / 134
  • The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 133 / 133 links, plus the pre-capture link into epoch 0
  • Client seed is player-controlled and set after the server commitment
  • Players can set or change their client seed at any time via the fairness panel
  • Nonce increments by 1 per bet with no gaps or duplicates across 134 seed epochs — 0 capture flags
  • The roll is fully determined by the seed inputs before the band or stake is applied
  • Identical inputs always produce the same roll — confirmed across all 6,700 bets, byte-identical on the two-decimal grid
  • Your client seed is a genuine input — changing it changes the roll
HMAC-SHA256 Pipeline — seed inputs through one rejection-sampled draw to the 0.00–99.99 roll — illustrated flow
CheckResultEvidence
Seed hash integrityPass134 / 134 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1)
Next-seed chainPassConsistent on 133 / 133 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPassAll 50 bets in each of 134 epochs carry the same commitment — no mid-epoch switching (Step 3)
Nonce sequencingPassNonces 0–49 in every epoch: 0 gaps, 0 duplicates, 0 capture flags (Step 4)
Client seed originPassSet after the server commitment — our own random audit… seeds in 124 epochs (Phases A–C) and 10 custom pfaudit- seeds in Phase D
Client seed controlPassPlayer can set or change the client seed at any time — 500 / 500 Phase D bets recompute under their custom seeds (Step 14)
Client seed influencePassA one-character change to the client seed changes the roll on 8 / 8 probed bets (Step 14)
Deterministic outputPassSame (serverSeed, clientSeed, nonce) always produces the same roll — 6,700 / 6,700 confirmed, byte-identical (Step 5)
Win-rule reproductionPass6,700 / 6,700 served win flags reproduced from the recomputed roll under the half-open band rule (Step 6)
Seed hash integrityPass
134 / 134 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1)
Next-seed chainPass
Consistent on 133 / 133 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPass
All 50 bets in each of 134 epochs carry the same commitment — no mid-epoch switching (Step 3)
Nonce sequencingPass
Nonces 0–49 in every epoch: 0 gaps, 0 duplicates, 0 capture flags (Step 4)
Client seed originPass
Set after the server commitment — our own random audit… seeds in 124 epochs (Phases A–C) and 10 custom pfaudit- seeds in Phase D
Client seed controlPass
Player can set or change the client seed at any time — 500 / 500 Phase D bets recompute under their custom seeds (Step 14)
Client seed influencePass
A one-character change to the client seed changes the roll on 8 / 8 probed bets (Step 14)
Deterministic outputPass
Same (serverSeed, clientSeed, nonce) always produces the same roll — 6,700 / 6,700 confirmed, byte-identical (Step 5)
Win-rule reproductionPass
6,700 / 6,700 served win flags reproduced from the recomputed roll under the half-open band rule (Step 6)
✓  Commit-reveal verified

All 134 revealed seeds match their commitments, and the next-seed chain is consistent across 133 rotations plus the pre-capture link into epoch 0. Nonces run 0–49 in every epoch with no gaps. Outcomes are fully deterministic — the same server seed, client seed and nonce always produce the same roll. With each commitment shown before its bets, as our capture records show, the casino cannot change your result after you bet.

How It Works — Seed, Nonce & Determinism7 sections
1.1Server Seed Commitment

Before any bet is placed under a seed pair, LIQD shows the SHA-256 hash of a secret 32-character hex server seed as hashedServerSeed. The commitment hashes the seed text as a UTF-8 string, not its 16 decoded bytes: SHA-256(utf8(serverSeed)). When the pair rotates, the server seed is revealed and anyone can recompute the hash. A match shows the revealed seed is the committed one: a substitute would need a second preimage of SHA-256, and would fail every recomputation that follows.

Server Seed Commitment — illustrated flow
src/rng.ts · commitHash · Verified
// §1.2: the published commitment is SHA-256 over the seed's ASCII characters,// NOT over the bytes the hex decodes to.export function commitHash(serverSeedHexString: string): string {  assertServerSeed(serverSeedHexString);  return sha256Buffer(Buffer.from(serverSeedHexString, "ascii"));}
Result: 134 / 134 revealed server seeds hash to their published commitment (Step 1). Zero mismatches.

Real Example from Live Data:

dice-master-6700bets.json · seed record (epoch 0)
{  "epoch": 0,  "phase": "A",  …  "clientSeed": "auditf2456fc140ac",  "hashedServerSeed": "918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8",  "nextHashedServerSeed": "2e788a2ef11d3214cb57a6dbfa0fff43df1c972617325e9b8bf157158e5d08b5",  "serverSeed": "015307d5a3f62db476210c3797bac8b7",  "nonceStart": 0,  "nonceEnd": 49,  …}

Verification:

verify-seed.js · Verified
// verify-seed.js — check any revealed LIQD server seed against its commitmentconst { createHash } = require('node:crypto');const serverSeed = '015307d5a3f62db476210c3797bac8b7';            // revealed 32-hex seed (epoch 0)const committed  = '918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8';const recomputed = createHash('sha256').update(serverSeed, 'utf8').digest('hex');console.log(recomputed === committed ? 'MATCH — seed is the committed one' : 'MISMATCH');
1.2Next-Seed Pre-Commitment Chain

At every seed rotation, the nextHashedServerSeed LIQD pre-commits during an epoch must equal the hashedServerSeed that becomes active after the rotation, so the successor seed is fixed before the previous epoch ends. Step 2 walks all 133 epoch transitions and checks each link, and also checks the pre-capture record: the seed active before the first audited bet (2ca3cfde…) hashes to its own commitment (1bf54b7d…), and its pre-committed next hash is epoch 0’s hashedServerSeed (918c2a30…). LIQD’s own seeds panel shows the pre-committed next hash alongside the active one (E11; the Seeds tab in S6 shows the same panel). A hash chain shows the values are consistent and fixes their order; it does not carry a date. Our capture records show each commitment was published before the bets it covers.

tests/steps/commitment.ts · Step 2 · Verified
  // ── Step 2: Next-seed pre-commitment chain ──────────────────────────────────  const byEpoch = [...seeds].sort((a, b) => a.epoch - b.epoch);  let promoChecked = 0, promoFails = 0;  for (let i = 0; i + 1 < byEpoch.length; i++) {    promoChecked++;    if (byEpoch[i].nextHashedServerSeed !== byEpoch[i + 1].hashedServerSeed) promoFails++;  }  const expectedTransitions = seeds.length - 1;  // …    const preCommitOk = commitHash(pre.revealedServerSeed) === pre.hashedServerSeed;    const preLinkOk = pre.nextHashedServerSeed === epoch0.hashedServerSeed;    preOk = preCommitOk && preLinkOk;  // …  const s2 = step(2, 'Next-Seed Pre-Commitment Chain',    promoFails === 0 && promoChecked === expectedTransitions && preOk ? 'PASS' : 'FAIL',
Result: 133 / 133 next-seed transitions consistent, plus the pre-capture link into epoch 0. The pre-committed next server seed hash matched the promoted active hash at every rotation boundary — the chain has no breaks.
1.3Hash Consistency Within an Epoch

Each bet record carries its epoch’s hashedServerSeed — the commitment recorded when the pair became active, which the capture writes onto every row. Step 3 groups the 6,700 bets by epoch and confirms that all 50 bets in each epoch carry the same commitment, and that the number of epochs seen equals the number of seed records. More than one distinct hash within a single epoch would indicate a mid-epoch seed substitution — a critical commit-reveal violation. What binds each bet to its seed is the recomputation: the bet’s hashedServerSeed matches its seed record, the record’s hash matches the reveal (Step 1), and the revealed seed reproduces every roll in the epoch (Step 5).

tests/steps/commitment.ts · Step 3 · Verified
  // ── Step 3: Hash consistency within epoch ────────────────────────────────────  const betsByEpoch = new Map<number, typeof bets>();  for (const b of bets) {    const arr = betsByEpoch.get(b.epoch) ?? [];    arr.push(b);    betsByEpoch.set(b.epoch, arr);  }  let epochsMultipleHashes = 0;  for (const [, epochBets] of betsByEpoch) {    const hashes = new Set(epochBets.map(b => b.hashedServerSeed));    if (hashes.size !== 1) epochsMultipleHashes++;  }  const s3CoverageOk = betsByEpoch.size === seeds.length;  const s3 = step(3, 'Hash Consistency Within Epoch',    epochsMultipleHashes === 0 && s3CoverageOk ? 'PASS' : 'FAIL',    `${betsByEpoch.size}/${seeds.length} epochs: all bets within each epoch share the same hashedServerSeed; ${epochsMultipleHashes} violations`,  );
Result: 134 / 134 epochs — hashedServerSeed constant across all 50 bets within every epoch. Zero mid-epoch changes detected.
1.4Client Seed Origin & Control

Players have full control over their client seed through the LIQD fairness panel, allowing them to view, modify, or rotate it at any time — including after the server-seed commitment is already published. The client seed is an input to every HMAC-SHA256 computation — changing it produces a completely different roll. LIQD generates a default value, and the server commits to its seed before the client seed for that epoch is set, which structurally contains cherry-picking when that seed is unpredictable: an operator that cannot know the upcoming client seed cannot pre-evaluate candidate server seeds against it. In this capture, 124 epochs (Phases A–C) used our own random audit… seeds — 48 bits of fresh CSPRNG material each, set after the commitment — and the 10 Phase D epochs used custom pfaudit- seeds. During capture the audit exercised this control directly:

  • Player-editable at any time via the fairness panel — no wager required to change it
  • Rotatable after the server-seed commitment is published — the operator commits blind
  • Mixed into every draw as the HMAC message clientSeed:nonce:cursor — never decorative
  • Phase D set 10 auditor-chosen pfaudit- seeds; all 500 rounds recompute under them (Step 14)
  • Sensitivity probe: appending a single character to the client seed changed the roll on 8 / 8 probed bets (Step 14)
Result: Client seeds are player-controlled and vary across all phases and epochs — our own audit… seeds throughout Phases A–C (e.g. auditf2456fc140ac in epoch 0, audit4195522a3970 in epoch 113), plus the 10 custom pfaudit- seeds of Phase D. Evidence: dice-master-6700bets.json
1.5Nonce Incrementation

The nonce begins at 0 and increments by 1 for each bet under the same seed pair — one nonce per roll. The nonce resets to 0 when the player rotates their seed, starting a new epoch. In this audit we rotated every 50 bets as our chosen sampling cadence (nonces 0–49 per epoch, 134 epochs); epoch length is not enforced by the casino — players can rotate at any time. Step 4 audits every epoch for gaps, reuse, and resets: a skipped nonce is where a discarded unfavourable outcome would hide, so the sequence must be contiguous. The epoch size is bound to the constant in src/config.ts rather than read from the dataset header, so a withheld bet cannot be absorbed by shrinking the declared epoch. A gap caused by our own capture script — a retried request whose bet still recomputes — would be a capture flag: disclosed with a limitation row and verdict-neutral. A gap or repeat on the operator side is a hard fail. This capture has no gaps and 0 capture flags.

Nonce Incrementation — illustrated flow
tests/steps/commitment.ts · Step 4 · Verified
    if (missing.length > 0) {      const ss = seedMap.get(hash);      let allVerify = ss !== undefined;      if (ss) {        for (const b of sorted) {          const served = Number(b.roll);          const local = diceRoll(ss, b.clientSeed, b.nonce) / SCALE;          if (!servedRollMatches(served, local)) { allVerify = false; break; }        }      }      if (allVerify) disclosedGaps.push(`epoch ${epochNum} (Phase ${phase}), nonce ${missing.join(',')} orphaned; all recorded bets verify`);      else hardFailures.push(`Epoch ${epochNum}: unverifiable nonce gap at ${missing.join(',')}`);    }  // …  if (hardFailures.length > 0) {    s4status = 'FAIL';    s4detail = `${hardFailures.length} violations: ${hardFailures.slice(0, 3).join('; ')}`;  } else if (disclosedGaps.length > 0) {    s4status = 'FLAG';    s4origin = 'capture';  // …
Result: 0 gaps, 0 duplicates and 0 capture flags across all 134 epochs. Nonces run 0–49 contiguously in every epoch — 6,700 bets in a complete, ordered ledger. Single client seed per epoch.
1.6Deterministic Mapping

The RNG algorithm is fully deterministic: given the same server seed, client seed and nonce, diceRoll always produces the exact same roll. The algorithm is a single HMAC-SHA256 draw at cursor 0 — the first 32-bit big-endian chunk below the rejection ceiling, reduced modulo 10,000 — returning the roll in hundredths (2401 for a roll of 24.01), which the suite divides by 100. No timing, no bet amount, no band, no UI state enters the derivation. Our implementation, written from the specification, reproduced all 6,700 captured roll values with a byte-identical comparison on the two-decimal grid. The bet record below is the featured bet used throughout this report — recompute it yourself with the snippet.

Deterministic Mapping — illustrated flow
src/rng.ts · diceRoll · Verified
// §2 Step 2: the dice roll is the draw over N = 10000 outcomes at cursor 0,// an integer in hundredths (0..9999 shown as 0.00..99.99).export function diceRoll(serverSeed: string, clientSeed: string, nonce: number): number {  return generateProvablyFairNumber(serverSeed, clientSeed, nonce, 0, 10000);}
Result: All 6,700 bets with revealed seeds: the single-draw HMAC recompute matches roll byte-identically (Step 5). Zero mismatches, zero skipped. No external entropy is mixed into the Dice RNG.

Real Bet Verified:

dice-master-6700bets.json · bet kzLjSr11zq… (epoch 0, nonce 0) · Verified
{  …  "epoch": 0,  "phase": "A",  "id": "kzLjSr11zq5GvEOHlj9n6",  "params": { "lower": 0, "upper": 92.37, "inverted": false },  "nonce": 0,  "clientSeed": "auditf2456fc140ac",  …  "roll": 24.01,  "win": true,  "multiplier": 1.071776550828191,  "result": "won",  "betAmount": 0.1,  "winningAmount": 0.10717766,  …}

Verification:

verify-dice.js · Verified
// verify-dice.js — recompute a LIQD Dice roll from its seedsconst { createHmac } = require('node:crypto');function draw(seed, client, nonce) {  const digest = createHmac('sha256', Buffer.from(seed, 'hex'))    .update(`${client}:${nonce}:0`).digest();  const maxFair = Math.floor(0x1_0000_0000 / 10000) * 10000;   // 4,294,960,000  for (let o = 0; o + 4 <= digest.length; o += 4) {    const chunk = digest.readUInt32BE(o);    if (chunk < maxFair) return chunk % 10000;  }  throw new Error('all eight chunks rejected (not specified; about 7e-47 per bet)');}const roll = draw('015307d5a3f62db476210c3797bac8b7', 'auditf2456fc140ac', 0) / 100;console.log(roll);                       // → 24.01console.log(roll >= 0 && roll < 92.37);  // → true  (under 92.37 — the lower bound is in, the upper is out)
1.7Win-Rule Reproduction & Client Seed Influence

Determinism covers the number; Step 6 covers what LIQD did with it. For every bet the recomputed roll is passed through the half-open rule LIQD’s verifier returns at the bounds (E13) — inBand = roll ≥ lower AND roll < upper, inverted for outside — and the result must equal the served win flag: 6,700 / 6,700 agree, across all four modes, including the two captured rolls that landed exactly on a lower bound (a roll of 0.00 under [0, 94.69) and a roll of 99.00 over [99, 100), both served as wins). To confirm the client seed is a genuine input to the HMAC-SHA256 computation, Step 14 recomputes all 500 Phase D bets under their auditor-chosen pfaudit- seeds (500 / 500 reproduce) and then probes sensitivity directly: appending one character to the recorded client seed and re-deriving changed the roll on 8 / 8 probed bets. Because every one of the 6,700 rolls requires its recorded seed to reproduce, and a chance collision of two different seeds on the same roll has probability 1 in 10,000, the seed is demonstrably load-bearing.

tests/steps/determinism.ts · Step 6 · Verified
  // ── Step 6: Win-rule reproduction ────────────────────────────────────────────  // The recomputed roll under the published band rule must reproduce the served win flag.  let winChecked = 0, winFails = 0;  for (const b of bets) {    const ss = seedMap.get(b.hashedServerSeed);    if (!ss) continue;    const local = diceRoll(ss, b.clientSeed, b.nonce) / SCALE;    if (diceWin(local, b.params) !== !!b.win) winFails++;    winChecked++;  }  const s6 = step(6, 'Win-Rule Reproduction',    winFails === 0 && winChecked === bets.length ? 'PASS' : 'FAIL',    winFails === 0      ? `${winChecked}/${bets.length} bets: recomputed roll ∈/∉ band reproduces the served win flag (inverted honoured)`      : `${winFails} win-flag mismatches`,  );
Result: 6,700 / 6,700 served win flags reproduced by the half-open rule on the recomputed roll (Step 6); 500 / 500 custom-seed bets recompute and a changed client seed changes the roll on 8 / 8 probes (Step 14). Client seed is a genuine, material input.
Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Seed hash integrity (Step 1)134 / 134 revealed seeds hash-verifiedPass
Next-seed pre-commitment chain (Step 2)133 / 133 epoch transitions + the pre-capture link into epoch 0Pass
Hash consistency (Step 3)134 / 134 epochs (50 bets each)Pass
Nonce audit (Step 4)0 gaps, 0 duplicates across 134 epochs; 0 capture flagsPass
Roll recomputation (Step 5)6,700 / 6,700 — byte-identical on the two-decimal grid, with our own implementationPass
Win-rule reproduction (Step 6)6,700 / 6,700 served win flags reproduced under the half-open rulePass
Client seed influence (Step 14)500 / 500 Phase D bets recompute; 8 / 8 sensitivity probes change the rollPass
1.9Code References7 source files
2 columns
FilePurpose
tests/verify.ts22-step verification pipeline (Steps 1–6 cover S1)
tests/steps/commitment.tsSteps 1–4: Commit-reveal integrity checks and nonce audit
tests/steps/determinism.tsSteps 5–6: Roll recomputation and win-rule reproduction
tests/steps/phase-d.tsStep 14: Custom client-seed control and sensitivity probe
src/rng.tsHMAC-SHA256 single draw (commitHash, generateProvablyFairNumber, diceRoll) — written from docs/ALGORITHM.md
src/loader.tsDataset loading + SHA-256 hash guard
capture/capture-dice.reference.mjsCapture methodology record (Phases A–D), authentication removed
1.10Datasets UsedSource artifacts
Primary: data/dice-master-6700bets.json
2 columns
PropertyValue
SourceLive Dice game data from LIQD’s pre-launch environment (qa.liqd.com) — 6,700 real bets across 4 phases, USDC
Capture environmentPre-launch host qa.liqd.com · one authenticated account · one session
Total Records6,700 bets across 134 epochs (50 bets each)
Captured27 August 2026 (18:52–20:30 UTC)
SHA-256ca1a181a9b4c89a1823e15fe71603b7a5e2267382c134873d80cba536d3aef34

Fields used: serverSeed, hashedServerSeed, nextHashedServerSeed, clientSeed, nonce, params (lower, upper, inverted), roll, win, multiplier, betAmount, winningAmount, stakeWallet, winningAmountWallet

src/loader.ts recomputes the SHA-256 above and aborts the suite before any step runs on a mismatch. The capture methodology ships as a reference record (capture/capture-dice.reference.mjs — authentication removed; it documents the capture, it does not replay it). Numeric response fields are stored as JavaScript numbers; the original decimal text is not part of the dataset. Every computational claim downstream of the dataset is reproducible from the file itself.

1.11Verified Invariants10 invariants
2 columns
InvariantResult
SHA-256(utf8(serverSeed)) = hashedServerSeed for all 134 revealed seedsPass
Next-seed pre-commitment chain intact for all 133 transitions, plus the pre-capture link into epoch 0Pass
hashedServerSeed constant within epoch for all 134 epochs — no mid-epoch seed switchingPass
Zero nonce gaps within any epoch (contiguous 0–49)Pass
Zero nonce duplicates within any epochPass
Every epoch contains exactly 50 bets (134 / 134), bound to the code constant, not the dataset headerPass
Same inputs produce the same roll, byte-identical on the two-decimal grid, for all 6,700 betsPass
Recomputed roll reproduces the served win flag under the half-open rule for all 6,700 betsPass
Every nonce window starts at the recorded nonceStart (0) and ends at nonceEnd (49) — 134 / 134 epochsPass
Custom client seeds honoured: 500 / 500 Phase D bets recompute; a changed seed changes the roll on 8 / 8 probesPass
1.12Reproduction InstructionsRun locally

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

reproduce-s1.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dice && git checkout v1.1.0npm cinpm run verify# Expected output: Steps 1–6 all PASS
Steps 1–6 cover all Seed, Nonce & Determinism checks. Expected output:

  [PASS] Step 1 — Seed Hash Integrity
  [PASS] Step 2 — Next-Seed Pre-Commitment Chain
  [PASS] Step 3 — Hash Consistency Within Epoch
  [PASS] Step 4 — Nonce Audit
  [PASS] Step 5 — Roll Recomputation & Draw Bias Bound
  [PASS] Step 6 — Win-Rule Reproduction
02

RNG & Entropy Model

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

This section verifies that LIQD’s Dice random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. The RNG makes one HMAC-SHA256 draw per bet — the first 4-byte chunk of the digest below the maxFair ceiling, reduced modulo 10,000 and placed on the two-decimal 0.00–99.99 grid, with bias-free rejection sampling. The implementation is our own, written from a specification without LIQD’s code; it reproduces all 6,700 live rolls, and LIQD’s own verifier serves only as a cross-check.

Cryptographic Randomness Verification

6,700 / 6,700captured rolls reproduced

What we verified

  • HMAC-SHA256 produces cryptographically sound, unpredictable output for each roll
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • Rejection sampling removes modulo bias exactly — every one of the 10,000 grid points is equally likely
  • The RNG is our own implementation, written from the specification — it reproduces all 6,700 captured rolls
  • Draw uniformity, serial independence, nine effective-edge bands and all 98 over-mode targets pass their screens across 138M simulated rolls
  • A changed client seed changes the roll — 8 / 8 sensitivity probes, 500 / 500 custom-seed bets

What this means for you

  • Each roll is generated fairly and cannot be skewed
  • All 10,000 outcomes from 0.00 to 99.99 are equally likely — no number is favoured
  • No hidden randomness or server-side tricks influence the roll
  • Consecutive bets are not correlated — past results don't affect future outcomes
  • The algorithm depends only on seeds you can verify
6,700 / 6,700captured rolls reproduced
  • HMAC-SHA256 produces cryptographically sound, unpredictable output for each roll
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • Rejection sampling removes modulo bias exactly — every one of the 10,000 grid points is equally likely
  • The RNG is our own implementation, written from the specification — it reproduces all 6,700 captured rolls
  • Draw uniformity, serial independence, nine effective-edge bands and all 98 over-mode targets pass their screens across 138M simulated rolls
  • A changed client seed changes the roll — 8 / 8 sensitivity probes, 500 / 500 custom-seed bets
RNG Pipeline — HMAC-SHA256 → uint32 → rejection sampling → mod 10,000 → roll — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(serverSeed, clientSeed:nonce:0) — one draw per bet, no hidden entropy
Entropy purityPassNo timestamps, external APIs, Math.random, or server-side state
Our own implementationPassWritten from the specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 84 specification checks pass and it reproduces all 6,700 rolls byte-identically
LIQD verifier cross-checkPassLIQD’s verify endpoint, queried as-is, agrees on 8 recorded vectors and the 7 boundary probes (E13) — a cross-check, not the source
Modulo biasPassRejection ceiling 4,294,960,000 = 10,000 × 429,496 — worst-case bias 0, computed from the bound the draw itself applies; no captured draw needed a second chunk (Step 5)
Key encoding verifiedPassServer seed hex-decoded to bytes for the HMAC key; the commitment hashes the seed text — both confirmed via 6,700-bet recomputation
Serial independencePassLag-1 z = 0.159 against the applied critical 2.9352; runs test p = 0.6910 — both clear at α/3 over 200,000 simulated rolls (Step 16)
Client seed influencePassA changed client seed changes the roll on 8 / 8 probes; all 500 Phase D custom-seed bets recompute
RNG derived only from disclosed inputsPass
HMAC-SHA256(serverSeed, clientSeed:nonce:0) — one draw per bet, no hidden entropy
Entropy purityPass
No timestamps, external APIs, Math.random, or server-side state
Our own implementationPass
Written from the specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 84 specification checks pass and it reproduces all 6,700 rolls byte-identically
LIQD verifier cross-checkPass
LIQD’s verify endpoint, queried as-is, agrees on 8 recorded vectors and the 7 boundary probes (E13) — a cross-check, not the source
Modulo biasPass
Rejection ceiling 4,294,960,000 = 10,000 × 429,496 — worst-case bias 0, computed from the bound the draw itself applies; no captured draw needed a second chunk (Step 5)
Key encoding verifiedPass
Server seed hex-decoded to bytes for the HMAC key; the commitment hashes the seed text — both confirmed via 6,700-bet recomputation
Serial independencePass
Lag-1 z = 0.159 against the applied critical 2.9352; runs test p = 0.6910 — both clear at α/3 over 200,000 simulated rolls (Step 16)
Client seed influencePass
A changed client seed changes the roll on 8 / 8 probes; all 500 Phase D custom-seed bets recompute
✓  Unbiased and Cryptographically Sound

The Dice RNG uses only the disclosed inputs, and our own implementation — written from the specification — reproduces every captured roll. The simulated rolls are consistent with a uniform draw across the 10,000-point grid, and the serial screens find no sign of dependence across 200,000 simulated rolls. The client seed is a genuine input — a one-character change alters the roll on every probe.

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

Each Dice bet makes exactly one draw. An HMAC-SHA256 digest is computed with the hex-decoded server seed as key and clientSeed:nonce:cursor as the message, where cursor is 0. The 32-byte digest is scanned in up to eight 4-byte big-endian chunks; the first chunk below the maxFair ceiling is reduced % 10000 to give an integer draw in [0, 10000), and dividing by 100 places it on the two-decimal roll scale 0.00 – 99.99. If all eight chunks were rejected — about 7 × 10⁻⁴⁷ per bet — the specification defines no further draw, so our implementation stops with an error; no captured draw needed even a second chunk. The band and stake never enter this function — they are applied to the roll afterwards.

2 columns
ComponentDetail
Hash functionHMAC-SHA256 (32-byte digest)
Keyhex_decode(serverSeed) — 16 raw bytes (distinct from the commitment domain, which hashes the seed text)
MessageclientSeed:nonce:cursor — UTF-8, literal colons, no padding; cursor = 0
Extraction4-byte big-endian chunks → readUInt32BE → uint32, up to 8 per digest
Rejectionchunk < maxFair where maxFair = floor(2³² / 10000) × 10000 = 4,294,960,000
Reductionuint32 % 10000 → integer draw in [0, 10000)
Scalingdraw / 100 → roll on the 0.00 – 99.99 grid
Exhaustion pathAll 8 chunks rejected → no further draw defined; our implementation stops with an error (≈ 7 × 10⁻⁴⁷ per bet, never reached)
OutputOne roll per bet — the band is applied to it afterwards
src/rng.ts · generateProvablyFairNumber · Verified
// §2 Step 2: walk the 32-byte digest in eight 4-byte big-endian chunks; the// first chunk below LIMIT gives the draw as chunk mod range. If all eight are// rejected the spec has no settled continuation (O2), so we throw rather than// invent one.export function generateProvablyFairNumber(  serverSeed: string,  clientSeed: string,  nonce: number,  cursor: number,  range: number,): number {  const limit = maxFairFor(range);  const digest = hmacDigest(serverSeed, clientSeed, nonce, cursor);  for (let i = 0; i + 4 <= digest.length; i += 4) {    const v = digest.readUInt32BE(i);    if (v < limit) return v % range;  }  throw new Error("all 32-bit chunks of the digest were rejected (spec O2: continuation unspecified)");}
Result: Our implementation matches all 6,700 live bets with zero roll mismatches (Step 5). It was written from a specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code, its client bundle or any port of it, and the specification’s 84 vectors and property checks pass (tests/dice/rngSpecTests.ts). LIQD’s own verify endpoint is queried as-is and used only as a cross-check — it agrees on eight recorded vectors and the seven boundary probes (E13).
2.2Entropy Sources

All randomness derives exclusively from the deterministic HMAC-SHA256 function combining three cleanly separated inputs — each visible in the fairness panel or the bet record:

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness — the HMAC key, committed via SHA-256 hash of the seed text before betting
Client SeedPlayerPlayer-contributed entropy — mixed into every HMAC message
NonceSystemUniqueness per bet (0–49 within each epoch, audited gap-free)
CursorSystemFixed at 0 — one draw per bet. An index, not an entropy source
No mixed entropy sources detected. Run the same inputs multiple times — results are always identical. Timing, bet amount, band, balance, and session state are all absent from the derivation. 6,700/6,700 confirmed.

How we know: 6,700/6,700 bets were recomputed using only the three declared inputs. If any hidden entropy source existed, recomputation would fail. It does not.

Verified absent: No timestamps, no Math.random(), no external APIs, no server-side mutable state, and no external beacon (no drand round, block hash, or VRF). Only: HMAC-SHA256(hexDecode(serverSeed), clientSeed:nonce:0). Because no external entropy is used, cherry-pick containment is structural rather than cryptographic — the commitment plus the player-controlled client seed carry the fairness; S4 covers exactly what that means and how it is checked.

2.3Modulo Bias Analysis

The draw is chunk % 10000. Modulo bias occurs because 2^32 is not evenly divisible by 10,000 — the 32-bit range holds 429,496 complete cycles of 10,000 plus a remainder of 7,296 values, so without a guard the residues 0–7,295 would each be reachable one extra time. The algorithm eliminates this bias entirely via rejection sampling: a ceiling maxFair = floor(2^32 / 10000) × 10000 ensures only unbiased chunks are used. Any chunk ≥ maxFair is discarded and the next 4-byte chunk of the same digest is tried:

range                = 10,000
2^32                 = 4,294,967,296
floor(2^32 / 10000)  =       429,496
maxFair = 429,496 × 10,000 = 4,294,960,000
dead values          = 2^32 − maxFair = 7,296
P(chunk rejected)    = 7,296 / 2^32      ≈ 1.70e-6
P(all 8 chunks rejected) = (7,296 / 2^32)^8 ≈ 6.9e-47
draws in this capture    = 6,700 (one per bet)
expected chunk rejections ≈ 6,700 × 1.70e-6 ≈ 0.011
observed                  = 0  (every captured roll came from the first chunk)
Result: Zero modulo bias confirmed. Rejection sampling via the maxFair ceiling makes every grid point exactly equally likely; the rejection branch is unit-tested, and no captured draw reached it.

Below maxFair, each of the 10,000 residues owns exactly 429,496 values — perfectly uniform. The 7,296 values at the top of the 32-bit space are discarded. Step 5 computes this bound from the same maxFairFor(10000) the draw applies (src/bias.ts), so the figures describe the code that produced the rolls, and it fails the audit if the accepted window is not a whole number of ranges. Our implementation’s rejection branch is exercised by a constructed unit vector (tests/dice/rngTests.ts: first chunk ≥ maxFair is skipped, second chunk taken); across the 6,700 captured draws every roll came from the first chunk, matching the ≈ 0.011 expectation.

2.4RNG Isolation

Each bet uses a unique nonce in the HMAC message (clientSeed:nonce:0), ensuring per-bet outputs are cryptographically independent. HMAC-SHA256 is a pseudorandom function — knowing one roll gives no usable information about any other roll under the same seed pair. There is no shared state between bets or between epochs, and there is no second draw inside a bet for the band to interact with: the roll is produced first, in isolation, and the band is applied to it afterwards.

RNG Isolation — illustrated flow
Result: RNG isolation confirmed. No state leakage between bets or epochs. Each HMAC call is independently keyed and messaged.

Evidence: The diceRoll implementation in 1.6 confirms this — it is a pure function with no class state, no external calls, and no cross-bet memory. It takes exactly three arguments (serverSeed, clientSeed, nonce) — no wager, no band, no timestamp, no account identifier — and returns a number. Same inputs always produce the same output; if any hidden input influenced the roll, the reference implementation (which lacks that input) could not reproduce 6,700 / 6,700 rounds. The strongest cross-phase evidence: Phase C repeats the game at $10.00 (100× the $0.10 stake) and Phase D swaps in ten auditor-chosen client seeds — both recompute under the identical three-argument function.

2.5Monte Carlo Simulation (138M Rolls)

A 138,000,000-roll Monte Carlo simulation checks the algorithm’s distribution at scale. Its seed pairs are our own — derived from a pinned master seed as HMAC(SIM_MASTER_SEED, counter), independent of LIQD’s seeds and rotated every 50 rolls like real epochs — so the whole experiment replays byte for byte: npm run test:full regenerates the committed artifact exactly. Pass 1 has four parts: 2,000,000 draws binned into 100 cells for uniformity (with the first 200,000 rolls screened for serial structure), 98 over-mode targets at 1,000,000 bets each (over 2.00 to over 99.00, win chance 98% to 1%), nine named bands at 2,000,000 bets each across all four modes, and a 20,000,000-bet run on under 50 (1.98×) sampled at seven checkpoints. Step 16 scores the committed artifact deterministically — every derived field, standard error and win count is recomputed from the stored statistics and must match exactly.

2 columns
MetricValue
Rolls138,000,000 — 2M uniformity draws + 98 targets × 1M bets + 9 bands × 2M bets + 20M under-50 bets
SeedsOur own, from the pinned master seed — replayable byte for byte
Draw uniformity (χ², 100 bins, df = 99)χ² = 108.885, p = 0.2335 — two-sided pass at α/3 (2,000,000 draws)
Serial screen (first 200,000 rolls)lag-1 z = 0.159 < 2.9352; runs test p = 0.6910 ≥ 3.333e-3
98 over-mode targets (1M each)98 / 98 within 5·SE + 1e-4 of 0.99; mean 99.0203%, 0.98·SE from 99.0000%; largest deviation 3.08·SE (over 56.00)
Effective-edge bands (9 × 2M)9 / 9 within 5·SE + 1e-4 of 0.99; largest deviation 2.12·SE (over 90)
Under-50 run (20M)99.0476% vs the proven 99.0000% — 2.15·SE, inside the 5·SE tolerance
src/config.ts · simulation parameters · Verified
export const SIM_UNIFORMITY_DRAWS = 2_000_000;export const SIM_SERIAL_DRAWS = 200_000;export const SIM_EDGE_BETS = 2_000_000;// …export const SIM_RTP_BETS = 20_000_000;/** The sample sizes the convergence series must contain, in order. */export const SIM_RTP_MARKS: readonly number[] = [1_000, 10_000, 100_000, 1_000_000, 5_000_000, 10_000_000, SIM_RTP_BETS];// …export const SIM_TARGET_BETS = 1_000_000;/** Per-target sample sizes at which the cross-target mean RTP is recorded, in order; the last is SIM_TARGET_BETS. */export const SIM_TARGET_MARKS: readonly number[] = [10_000, 50_000, 100_000, 500_000, SIM_TARGET_BETS];// …export const SIM_MASTER_SEED = 'a11d17e5c0ffee5ea1ed0c5b1b7ea5e5d1ce5eed5a1710add1ce5eed10addba5';// …export const SIM_TARGETS: readonly { label: string; params: DiceParams }[] = Array.from(  { length: 98 },  (_, i) => ({ label: `over ${i + 2}.00`, params: { lower: i + 2, upper: 100, inverted: false } }),);
Result: 138M rolls simulated. Uniformity and serial independence pass at the Bonferroni-corrected level; all 98 targets and all nine effective-edge bands sit inside their 5·SE tolerance, and the 98-target mean lands 0.98 standard errors from the 99.0000% identity. The results are consistent with a uniform draw across the 10,000-point grid (Step 16).

All four families pass. The three statistical screens (uniformity, lag-1, runs) are each judged at α/3 = 3.333e-3 so the family has level α = 0.01; the uniformity screen is two-sided, rejecting a statistic too uniform to be a real 2,000,000-draw sample as well as one that is non-uniform. The 98 targets and the nine effective-edge rows must each sit within 5 × SE + 0.0001 of 0.99, and each row’s simulated RTP must be exactly the return that some integer win count produces under the producer’s own accumulation — a published number that no integer count can reach fails the step.

Methodology: the roll is a single uniform outcome per bet, so a 100-cell chi-squared against N/100 is the correct uniformity test. Every target’s client seed and first server seed are re-derived from the master seed at its fixed position in the seed stream, and the 98-target mean is recomputed at each of its five marks (10K, 50K, 100K, 500K and 1M bets per target). The under-50 series stores the integer win count, RTP and standard error at each checkpoint; Step 16 recomputes every standard error and confirms the win counts (493 / 5,046 / 50,371 / 499,779 / 2,501,269 / 5,002,354 / 10,004,806) are cumulatively consistent and reconstruct bit-for-bit to the published values.

2.6Serial Independence

Serial independence ensures consecutive rolls are not correlated — a high roll on one bet does not make the next more or less likely to be high. Two tests were applied to the first 200,000 rolls of the Pass 1 uniformity stream, each judged at the Bonferroni-corrected α/3 = 3.333e-3 (the third screen in the family is uniformity itself):

Lag-1 autocorrelation: Pearson correlation between consecutive rolls, r₁ = 0.000355, standardised as z = r₁ · √n = 0.159. The critical value is the two-sided normal quantile at α/3, 2.9352; the artifact stores the same value and Step 16 checks they agree. z clears it comfortably.

Wald-Wolfowitz runs test: Tests whether the sequence of above/below-50 outcomes has the expected number of runs. z = −0.398, two-sided p = 0.6910 — re-derived from the stored cell counts (99,912 runs, n₁ = 100,103 of 200,000) rather than read; clear at α/3.

src/stats.ts · lag1Autocorrelation · Verified
export function lag1Autocorrelation(series: number[]): number {  const n = series.length;  let mean = 0;  for (let i = 0; i < n; i++) mean += series[i];  mean /= n;  let num = 0, den = 0;  for (let i = 0; i < n - 1; i++) num += (series[i] - mean) * (series[i + 1] - mean);  for (let i = 0; i < n; i++) den += (series[i] - mean) ** 2;  return den === 0 ? 0 : num / den;}
Result: Both serial screens clear at α/3: lag-1 z = 0.159 against 2.9352, runs p = 0.6910. The screens find no sign of serial dependence (Step 16).
2.7Worked Example — Full RNG Trace

Real bet from dataset — bet kzLjSr11zq5GvEOHlj9n6 (Phase A, epoch 0), nonce 0, under 92.37. Verified from dice-master-6700bets.json. One HMAC digest, eight candidate chunks — the first is below the ceiling, so it is the draw:

serverSeed = 015307d5a3f62db476210c3797bac8b7
clientSeed = auditf2456fc140ac
nonce      = 0
cursor     = 0
message    = auditf2456fc140ac:0:0
digest     = 475461d14638cca3e385e940e763bf808fa6ef6c05f1a83c333dc25e586c5b6e
maxFair    = 4,294,960,000
5 columns
chunkhexuint32< maxFair?draw (% 10000)
0475461d11,196,712,401yes — taken2401 → roll 24.01
14638cca31,178,127,523(not needed)—
2e385e9403,817,204,032(not needed)—
7586c5b6e1,483,496,302(not needed)—
Parity verified: Bet kzLjSr11zq… — the roll matches the single-draw HMAC recomputation exactly. 1,196,712,401 % 10,000 = 2,401, and 2,401 / 100 = 24.01, exactly as the live game recorded. Against the band [0, 92.37) the roll wins — the featured bet's payout is traced in S3.8.
Live Game24.01
Verifier24.01
Technical Evidence & Verification5 sections
2.8Evidence Coverage Summary10 checks
3 columns
Verification AreaCoverageResult
Our implementation (Step 5)6,700 / 6,700 bets — written from the specification; its 84 specification checks passPass
LIQD verifier cross-check8 / 8 recorded vectors and 7 / 7 boundary probes (E13) agreePass
Key encoding (hex vs UTF-8)Confirmed via recomputation — HMAC keys the decoded bytes, commitment hashes the textPass
Modulo bias analysisRejection ceiling 4,294,960,000; bias 0 from the draw’s own bound; 0 rejected chunks in 6,700 draws; branch unit-testedPass
External entropy non-participation (Step 5)6,700 / 6,700 reproduced from disclosed inputs alonePass
Draw uniformity (Step 16)χ² = 108.885, df = 99, p = 0.2335 over 2,000,000 draws — two-sided pass at α/3Pass
Serial independence (Step 16)lag-1 z = 0.159 < 2.9352; runs p = 0.6910 (200,000 rolls)Pass
Effective-edge bands (Step 16)9 / 9 bands within 5·SE + 1e-4 of 0.99 at 2,000,000 bets eachPass
Live-sample serial screen (Step 22, informational)The 6,700 captured rolls in order: lag-1 z = 1.057, runs p = 0.2618 — no serial dependencePass
Client seed influence (Step 14)500 / 500 Phase D + 8 / 8 sensitivity probesPass
2.9Code References10 source files
2 columns
FilePurpose
src/rng.tsHMAC-SHA256 single draw (commitHash, generateProvablyFairNumber, diceRoll) — written from docs/ALGORITHM.md
docs/ALGORITHM.mdThe specification the RNG was written from — inputs, commitment, derivation, win rule, settlement and their sources
src/bias.tsThe draw-bias bound, computed from the same maxFairFor the draw applies (Step 5)
src/simulate.tsMonte Carlo simulation (138M rolls, two-pass), deterministic from the pinned master seed, and chart rendering
src/stats.tsChi-squared (exact regularized incomplete gamma), lag-1 autocorrelation, Wald-Wolfowitz runs test, AS 241 normal quantile, exact binomial tail
src/sim-checks.tsInteger win-count recovery, convergence-series validation, the seven scored statistical screens and their nominal levels
tests/steps/determinism.tsSteps 5–6: Roll recomputation, draw-bias bound and win-rule reproduction
tests/steps/simulation.tsSteps 16–17: artifact binding and statistic reconciliation for Pass 1; Pass 2 re-derivation from the dataset’s own seeds
tests/dice/rngSpecTests.tsThe specification’s 84 vectors and property checks
tests/dice/rngTests.ts18 unit tests — eight recorded verifier vectors, the rejection branch, commitment convention, client-seed sensitivity, the boundary probes (E13), 59,998-band edge identity
2.10Verified Invariants12 invariants
2 columns
InvariantResult
HMAC-SHA256 output matches live game for all 6,700 betsPass
Key is hex-decoded (16 bytes, not UTF-8) — wrong encoding produces wrong rollsPass
Message format is clientSeed:nonce:cursor with cursor 0 — literal colons, no paddingPass
Chunks scanned big-endian, 4 bytes, first accepted winsPass
Rejection sampling eliminates modulo bias — the draw’s own bound is 10,000 × 429,496, so every residue owns exactly 429,496 valuesPass
Zero rejected chunks across all 6,700 captured draws on a direct count (expected ≈ 0.011); rejection branch unit-testedPass
No external entropy sources required for roll computationPass
Draw distribution is consistent with uniform across the 10,000-point grid (χ² p = 0.2335, 2,000,000 draws, two-sided)Pass
Lag-1 z < 2.9352 and runs p ≥ 3.333e-3 on 200,000 consecutive rollsPass
Nine effective-edge bands within 5·SE + 1e-4 of 0.99, each reconstructing bit-for-bit from an integer win countPass
98 / 98 over-mode targets within 5·SE + 1e-4; the 98-target mean lands 0.98·SE from the 99.0000% identity; the 20M under-50 run lands 2.15·SE, inside the 5·SE tolerancePass
Client seed change produces a different roll on 8 / 8 probes; 500 / 500 custom-seed bets recomputePass
2.11Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json — 138M rolls (Pass 1: uniformity, serial, 98 over-mode targets, nine edge bands, the under-50 run) + the 134 casino seeds replayed for 10,000 nonces each (Pass 2)
Primary dataset: data/dice-master-6700bets.json — 6,700 live bets for roll recomputation (Step 5)
Simulation output: outputs/simulation-results.json — Pass 1 (2M-draw uniformity + serial, 98 targets × 1M, 9 bands × 2M, 20M under-50 run) + Pass 2 (served-band return over 134 seeds × 10,000 nonces; 50 + 50-draw windows against an exact discrete null) · SHA-256 55873640715d1f9e…
Verification output: outputs/verification-results.json — the 22 steps, including Step 16’s screen-by-screen reconciliation and the nominal allowance accounting
Convergence chart: outputs/rtp-convergence.html — the 98-target mean series, rendered from the saved series by src/chart.ts · SHA-256 6b7b6bb7c474a49b… (pinned and checked by Step 19)
Reference vectors: tests/dice/rngSpecTests.ts (the specification’s 84 checks) and tests/dice/rngTests.ts — 18 known-input unit tests: eight recorded verifier vectors, the rejection branch, the commitment, client-seed sensitivity, E13 boundary semantics and the exact edge identity over 59,998 bands
2.12Reproduction InstructionsRun locally

Clone the repository, install dependencies, and regenerate the simulation before verifying:

reproduce-s2.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dice && git checkout v1.1.0npm cinpm run test:full   # regenerates the 138M-roll simulation from its pinned master seed, then verifiescat outputs/simulation-results.json
S2-related steps:

  [PASS] Step 5 — Roll Recomputation & Draw Bias Bound
  [PASS] Step 6 — Win-Rule Reproduction
  [PASS] Step 14 — Custom Client-Seed Control
  [PASS] Step 16 — Simulation — Pass 1 (RNG + Edge)
  [PASS] Step 22 — Live-Sample Serial Independence (informational)

The regenerated outputs/simulation-results.json and outputs/rtp-convergence.html are byte-identical to the committed files (SHA-256 55873640… and 6b7b6bb7…).
03

Verifier Parity

Does the live game actually follow its own rules?

This section validates that our implementation produces the exact same roll as the live game for every single bet — byte-identical on the two-decimal grid. It also re-derives every win flag from the recomputed roll and the half-open band rule, and confirms every credited amount matches the quotient multiplier 9900 / b through the two-stage 8-decimal settlement. Any mismatch would invalidate the fairness guarantee.

Live ↔︎ Verifier Parity

6,700 / 6,700bets matched

What we verified

  • Every bet independently recomputed from seeds — the roll verified byte-identically, not just the payout
  • Payout correctness: all 2,807 wins credit the two-stage settlement of bet × 9900 / b with zero tolerance; all 3,893 losses pay 0
  • Win/loss label re-derived from the recomputed roll and band for every round — 0 mislabelled settlements
  • Bet amount is not an input to the RNG — the roll depends only on seeds and nonce
  • All four capture phases recomputed identically (random bands, the 49.5× / 99× tail, elevated stake, client seed variation)

What this means for you

  • The verifier isn’t a simulation — it produces the exact same roll as the live game
  • Every bet you play can be independently recomputed by anyone
  • No hidden logic alters outcomes based on how much you bet or which band you choose
  • The game LIQD served in the capture follows the specified algorithm exactly
6,700 / 6,700bets matched
  • Every bet independently recomputed from seeds — the roll verified byte-identically, not just the payout
  • Payout correctness: all 2,807 wins credit the two-stage settlement of bet × 9900 / b with zero tolerance; all 3,893 losses pay 0
  • Win/loss label re-derived from the recomputed roll and band for every round — 0 mislabelled settlements
  • Bet amount is not an input to the RNG — the roll depends only on seeds and nonce
  • All four capture phases recomputed identically (random bands, the 49.5× / 99× tail, elevated stake, client seed variation)
Parity Verification Flow — seeds → recompute → compare → exact match — illustrated flow
6,700Live Bets Tested
100%Parity Rate
$0.10 & $10Bet Sizes Tested
0Mismatches
CheckResultEvidence
Roll recomputationPass6,700 / 6,700 exact match — the roll verified byte-identically on the two-decimal grid for every bet
Win-rule reproductionPass6,700 / 6,700 served win flags reproduced by the half-open band rule on the recomputed roll, inverted honoured
Multiplier derivationPassAll 2,807 wins: served multiplier == 9900 / basisPoints as a binary64 quotient, bit-exact (max deviation 0)
Credit arithmeticPassAll 6,700 credits: winningAmount == round-half-even₈(stake × round₈(multiplier)) with zero tolerance; losses pay exactly 0
Wallet-field reconciliationPassstakeWallet / winningAmountWallet present and equal to the game ledger on 6,700 / 6,700 bets (Step 10)
Bet-size independencePassBet amount is absent from the RNG input — same roll derivation at $0.10 and $10. Tested in Phase C (200 / 200)
Mode & phase completenessPassAll four modes (under / over / inside / outside) and all four planned phases present at their planned counts
Roll recomputationPass
6,700 / 6,700 exact match — the roll verified byte-identically on the two-decimal grid for every bet
Win-rule reproductionPass
6,700 / 6,700 served win flags reproduced by the half-open band rule on the recomputed roll, inverted honoured
Multiplier derivationPass
All 2,807 wins: served multiplier == 9900 / basisPoints as a binary64 quotient, bit-exact (max deviation 0)
Credit arithmeticPass
All 6,700 credits: winningAmount == round-half-even₈(stake × round₈(multiplier)) with zero tolerance; losses pay exactly 0
Wallet-field reconciliationPass
stakeWallet / winningAmountWallet present and equal to the game ledger on 6,700 / 6,700 bets (Step 10)
Bet-size independencePass
Bet amount is absent from the RNG input — same roll derivation at $0.10 and $10. Tested in Phase C (200 / 200)
Mode & phase completenessPass
All four modes (under / over / inside / outside) and all four planned phases present at their planned counts
✓  Live game and verifier fully aligned

All 6,700 bets matched our recomputation exactly — rolls, win flags, multipliers and credited amounts, across all four capture phases and all four modes. Payout math correct at both stakes, with the quotient multiplier reproduced bit-for-bit on every win.

How It Works — Verifier Parity8 sections
3.1Why Parity Matters

If the verifier produces results that differ from the live game, players cannot trust the verification — the entire provably fair system becomes meaningless. Commit-reveal shows the seed didn’t change; parity shows the game the player experienced is the game the seed dictates. For every captured round the audit rebuilds the roll from raw inputs with our own code, re-derives the win flag from the band, and recomputes the credit from the quotient multiplier and the two-stage settlement — then compares all three against what LIQD actually served and credited. 100% parity is required: a single mismatch anywhere in 6,700 rounds would indicate a bug in the verification logic, manipulation in the live game, or an inconsistent RNG implementation.

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

Data was collected across four structured phases, each designed to test a specific fairness property. The phases are complementary — together they cover band breadth across every mode, the high-multiplier tail, bet-size invariance, and client seed variation.

5 columns
PhaseBetsConfigBet AmountPurpose
A — Band coverage5,000Random bands across all four modes (epochs 0–99)$0.10Verify the roll, win rule and multiplier formula across 4,824 distinct bands
B — High-multiplier tail1,000500 × over 98 (49.5×) + 500 × over 99 (99×) (epochs 100–119)$0.10Maximum variance; the 1% band pays the largest multiplier in the capture
C — Stake equivalence200over 50 (1.98×) (epochs 120–123)$10.00Confirm bet amount is not an RNG input (100× the $0.10 stake)
D — Client-seed variation500Random bands under 10 custom pfaudit- seeds (epochs 124–133)$0.10Confirms seed control is player-side and the input is consumed
Total: 6,700 bets across 134 seed epochs of exactly 50 bets each — phase counts and mode coverage verified by Steps 11 and 12 against constants in code, not the dataset header. Total wagered: $2,650.00 ($500.00 Phase A + $100.00 Phase B + $2,000.00 Phase C + $50.00 Phase D).
3.3Roll Recomputation (Step 5)

For every bet, the audit independently computes the roll as diceRoll(serverSeed, clientSeed, nonce) / 100 with the epoch’s revealed server seed and compares it to the server-reported roll. The comparison is strict: the served value must be on the two-decimal grid, inside the roll domain, and equal to the recomputed value — no tolerance. The computation uses one HMAC-SHA256 draw with the hex-decoded server seed as key, rejection-sampled below 4,294,960,000 and reduced modulo 10,000.

Roll Recomputation (Step 5) — illustrated flow
tests/steps/determinism.ts · Step 5 · Verified
  let checked = 0, mismatches = 0;  let sample = '';  for (const b of bets) {    const ss = seedMap.get(b.hashedServerSeed);    if (!ss) continue;    const local = diceRoll(ss, b.clientSeed, b.nonce) / SCALE;    const served = Number(b.roll);    // Finiteness guard: a non-numeric served roll makes    // `Math.abs(local - NaN) > 1e-9` false, silently passing; reject it explicitly.    if (!servedRollMatches(served, local)) { mismatches++; if (!sample) sample = `epoch ${b.epoch} nonce ${b.nonce}: local ${local} vs served ${b.roll}`; }    checked++;  }  const rollsOk = mismatches === 0 && checked === bets.length;
Result: 6,700/6,700 bets verified, 0 skipped. Zero roll mismatches — including every Phase B tail round and every Phase D custom-seed round. Every computed roll matches roll in the dataset.
3.4Multiplier Derivation + Credit Arithmetic (Steps 7–8)

For each of the 2,807 winning bets, Step 7 rebuilds the multiplier from the band alone: the winning width in basis points, b = round(100 × W), and the quotient 9900 / b. The served multiplier must equal that quotient bit-for-bit as a binary64 — not within a tolerance. It does on all 2,807 wins; the algebraically identical float form (100 / W) × 0.99 agrees bit-for-bit on only 1,719 of them, which is why the audit pins the integer-basis-point quotient as the served formula. b is our derivation — the field does not appear in the capture; the served value matching the quotient on every win is the evidence for the form. Step 8 then replays the money: winningAmount must equal ROUND_HALF_EVEN₈(stake × ROUND₈(multiplier)) — the multiplier rounded to 8 decimals, the product rounded half-even to 8 decimals in exact integer arithmetic on 1e-8 units — with zero tolerance, and a loss must credit exactly 0. All 6,700 reconcile. Against the raw un-rounded product (decimal stake × the exact rational 9900 / b, the operator’s own number system rather than binary64) the settlement rounds the player down on 1,341 wins and up on 1,316, lands exactly on the product on 150, with a maximum absolute adjustment of 5.50e-9 USDC and a net of +4.91e-8 USDC across the capture — no directional bias. 274 of the 2,807 wins settle on an exact 8-decimal tie, where the half-even rule decides the last unit.

Multiplier Derivation + Credit Arithmetic (Steps 7–8) — illustrated flow
2 columns
MetricCount
Wins where multiplier === 9900 / basisPoints bit-for-bit2,807 / 2,807
Wins where the float closed form (100 / W) × 0.99 also matches bit-for-bit1,719 / 2,807 (same rational, different binary64)
Bets where winningAmount equals the two-stage settlement exactly6,700 / 6,700
Wins rounded down / up / exactly on the raw product1,341 / 1,316 / 150
Max |credit − stake × mult| · net across the capture5.50e-9 USDC · +4.91e-8 USDC
Wins settling on an exact 8-decimal tie (half-even decides)274
tests/steps/payouts.ts · Step 7 + src/config.ts · settledCreditUnits · Verified
  const numeratorIntegral = Number.isInteger(PAYOUT_NUMERATOR);  let mChecked = 0, mFails = 0, maxDev = 0, closedFormAgrees = 0;  for (const b of bets) {    if (!b.win) continue;    const bp = payoutBasisPoints(b.params);                           // integer basis points (auditor-derived)    const served = PAYOUT_NUMERATOR / bp;                             // operator's exact multiplier    const dev = Math.abs(Number(b.multiplier) - served);    if (dev > maxDev) maxDev = dev;    if (Number(b.multiplier) !== served) mFails++;                    // bit-exact pin, not tolerance    // The float closed form is the SAME rational and a DIFFERENT binary64 on many bands. Counted, so    // the report does not call the two forms "bit-exact" equal.    if (Number(b.multiplier) === quotedMultiplier(b.params)) closedFormAgrees++;    mChecked++;  } /** The credit in exact integer 1e-8 units — the form with no rounding ambiguity at all. */export function settledCreditUnits(stake: number, multiplier: number): bigint {  const stakeUnits = BigInt(Math.round(stake * 1e8));  const multUnits = BigInt(Math.round(multiplier * 1e8));   // stage 1: ROUND_8(multiplier)  const product = stakeUnits * multUnits;                   // in 1e-16 units  let q = product / UNITS_PER_WHOLE;  const r = product % UNITS_PER_WHOLE;  if (r > HALF_UNIT) q += 1n;  else if (r === HALF_UNIT && (q % 2n) === 1n) q += 1n;     // exact tie → round to even  return q;}
Result: All 2,807 wins: served multiplier is the binary64 quotient 9900 / b, bit-exact (Step 7). All 6,700 bets: credited amount reproduced by the two-stage 8-decimal settlement with zero tolerance, losses exactly 0 (Step 8). Zero payout errors.
3.5Loss Settlement & Wallet Fields (Steps 9–10)

Step 9 checks every losing bet in isolation: winningAmount must be exactly 0 and multiplier exactly 0 — 3,893 / 3,893 conform, with no rounding band to hide a stray credit in. Step 10 reads the wallet-denominated settle-response fields: winningAmountWallet must equal the same two-stage settlement of stakeWallet × multiplier, and both wallet fields must equal the game ledger (winningAmount, betAmount) — 6,700 / 6,700, 0 missing, 0 divergent. Every quoted multiplier also sits within the audit odds bounds [1.0102, 9900].

3 columns
ScopeCoverageVerified
Losses — winningAmount == 0 and multiplier == 03,893 rounds3,893 / 3,893
Wallet credit — winningAmountWallet == settle(stakeWallet × multiplier)6,700 rounds6,700 / 6,700 (0 missing)
Wallet ledger == game ledger (stake and credit fields agree)6,700 rounds6,700 / 6,700 (0 divergent)
Quoted multipliers within [1.0102, 9900]6,700 rounds0 out of range
Result: 3,893 losses pay exactly zero with a zero multiplier (Step 9); the wallet-denominated settle fields reconcile with the formula and the game ledger on all 6,700 bets (Step 10).
3.6Settlement Determinism (Step 20)

Steps 7 and 8 check the served multiplier and the credit against it. Step 20 removes the served multiplier from the picture entirely: for every bet, the outcome and the credited amount are rebuilt from (roll, band, stake) alone — win flag from the half-open rule, multiplier as the integer-basis-point quotient 9900 / b, credit through the two-stage settlement — and must match the served settlement with zero tolerance. In a legitimate game nothing else can move the money: no hidden fee, no per-account adjustment, no stake-dependent formula. All 6,700 rounds conform, across all four modes, 2,807 wins and 3,893 losses.

tests/steps/standardization.ts · Step 20 · Verified
    /** The expected winning multiplier: the integer-basis-point quotient, not the float closed form. */    const expectedMultiplier = (p: typeof bets[number]['params']) => {      const bps = payoutBasisPoints(p);      return bps > 0 ? PAYOUT_NUMERATOR / bps : 0;    };    for (const b of bets) {      chk++;      modesSeen.add(bandMode(b.params));      const win = diceWin(Number(b.roll), b.params);      if (win) wins++; else losses++;      const expected = win ? settledCredit(Number(b.betAmount), expectedMultiplier(b.params)) : 0;      const credited = Number(b.winningAmount);      // win flag must match the roll/band rule AND the credit must be the determined amount      if (win !== !!b.win || credited !== expected) {        bad++; if (fails.length < 3) fails.push(`epoch ${b.epoch} nonce ${b.nonce}: credited ${credited} vs determined ${expected}`);      }    }    const coverOk = modesSeen.size === 4 && wins > 0 && losses > 0;
Result: 6,700 / 6,700 settlements are fully determined by (roll, band, stake) — 0 mismatches; coverage: all four modes, 2,807 wins and 3,893 losses (Step 20).
3.7Phase C — Bet-Size Invariance (Step 18)

Phase C placed 200 bets at $10.00 on over 50 (1.98×) — 100× the $0.10 stake used in every other phase. All 200 rolls were recomputed byte-identically from revealed seeds (0 mismatches). The structural point: diceRoll takes no wager argument, so the roll cannot depend on stake, and the effective edge for the band is 1.0000% at every stake. The only place betAmount appears is the settlement multiplication, and the only stakes in the dataset are exactly $0.10 and $10.00.

Result: Phase C: 200/200 rolls recomputed correctly at $10.00; every win credits exactly stake × 1.98 = $19.80 (Step 8 covers the money). Bet amount is not an input to the RNG.
Variance context: Phase C's 200-bet sample at over 50 produced 105 wins against the 100 expected — $2,079.00 returned on $2,000.00 staked. At N=200 this is small-sample luck and structurally irrelevant: the claim verified here is roll invariance, not RTP. The 99.0000% RTP is proven analytically in S4.
3.8Worked Example — Full Parity Verification

Real bet from Phase A — bet kzLjSr11zq5GvEOHlj9n6 (epoch 0), nonce 0, under 92.37 at $0.10. Verified from dice-master-6700bets.json — the same bet LIQD's own verifier endpoint reproduced in the E13 settled-bet cross-check:

serverSeed = 015307d5a3f62db476210c3797bac8b7
clientSeed = auditf2456fc140ac
nonce      = 0
band       = [0, 92.37), not inverted  (under 92.37)
betAmount  = $0.10
recorded   = roll 24.01 · win true · multiplier 1.071776550828191 · winningAmount 0.10717766
commitment = 918c2a302c7219c91cf31ce3619acb0a…
3 columns
StepProcessOutput
1Recompute the commitment — SHA-256(utf8(serverSeed))Matches the pre-published hash
2Run diceRoll(serverSeed, clientSeed, 0) / 10024.01 — equals the recorded roll
3Apply the half-open band [0, 92.37)In band — the won label is correct
4Derive the multiplier 9900 / 92371.071776550828191 — the served field, bit-for-bit
5Two-stage settlement of 0.10 × round8(m)0.107177655 is an exact tie → half-even → 0.10717766, the credited winningAmount, verbatim
HMAC-SHA256 recomputation + formula · Verified
commitHash(serverSeed)                    → matches the published commitment ✓diceRoll(seed, client, 0)                 → 2401 → roll 24.01  (chunk 0x475461d1 = 1,196,712,401 % 10,000 = 2,401) ✓0 ≤ 24.01 < 92.37                         → in band → won ✓b = round(100 × 92.37) = 9237             → multiplier = 9900 / 9237 = 1.071776550828191 (bit-exact) ✓round8(m) = 1.07177655 → 0.10 × 1.07177655 = 0.107177655 → half-even at the tie → 0.10717766 ✓
Parity verified: Bet kzLjSr11zq… — commitment, roll, win label, quotient multiplier and credited payout all match exactly between the live game and our recomputation. This example exercises Steps 1, 5, 6, 7, 8 and 20 end-to-end, is one of the 274 exact settlement ties, and is the same bet used for the RNG trace in S2.
Live Game$0.10717766
Our recomputation$0.10717766
Technical Evidence & Verification5 sections
3.9Evidence Coverage Summary12 checks
3 columns
Verification AreaCoverageResult
Roll recomputation (Step 5)6,700 / 6,700 rounds — byte-identical on the two-decimal gridPass
Win-rule reproduction (Step 6)6,700 / 6,700 served win flags under the half-open rulePass
Multiplier derivation (Step 7)2,807 / 2,807 wins — 9900 / b bit-exactPass
Credit arithmetic (Step 8)6,700 / 6,700 — two-stage 8-decimal settlement at zero tolerancePass
Loss settlement (Step 9)3,893 / 3,893 losses pay exactly zeroPass
Wallet fields & odds bounds (Step 10)6,700 / 6,700 wallet fields reconcile; 0 multipliers out of [1.0102, 9900]Pass
Dataset integrity & population (Step 11)SHA-256 pin match; 6,700 bets · 134 seeds · 50 per epoch, bound to code constantsPass
Phase & mode coverage (Step 12)A 5,000 · B 1,000 · C 200 · D 500; all four modes; both stakesPass
Phase D client-seed variation (Step 14)500 bets, 10 custom client seeds — 500 / 500 matchPass
Phase C bet-size invariance (Step 18)200 / 200 at $10.00Pass
Settlement determinism (Step 20)6,700 / 6,700 rebuilt from (roll, band, stake) alonePass
Multiplier monotonicity (Step 21)4,176 distinct win-chance points, 0 inversions, span 1.0105×–99×Pass
3.10Code References6 source files
2 columns
FilePurpose
tests/steps/determinism.tsSteps 5–6: Roll recomputation and win-rule reproduction
tests/steps/payouts.tsSteps 7–10: Multiplier derivation, credit arithmetic, loss settlement, wallet fields and odds bounds
tests/steps/dataset.tsSteps 11–12: Dataset integrity and phase / mode coverage — population bound to src/config.ts
tests/steps/phase-d.tsStep 14: Phase D custom client-seed variation and sensitivity
tests/steps/standardization.tsSteps 18, 20, 21: Bet-size invariance, settlement determinism, multiplier monotonicity
src/config.tspayoutBasisPoints, quotedMultiplier, diceWin, settledCreditUnits, creditResidualSign — the formulas every payout is replayed against
3.11Datasets UsedSource artifacts
Primary dataset: data/dice-master-6700bets.json — 6,700 live bets across Phases A–D (134 epochs), one hash-pinned capture
Phase B slice: Epochs 100–119 — 500 bets on over 98 and 500 on over 99 inside the primary dataset (no separate file)
Game config: Quotient formula 9900 / round(100 × W) and the half-open win rule in src/config.ts — algebraic, not a lookup table
Derived figures: outputs/report-figures.json — the settlement residual counts, settled-RTP span, boundary hit counts and Phase D exposure quoted in this report, reconciled field-for-field by Step 19
Verification output: outputs/verification-results.json — Steps 5–12, 14, 18, 20–21
3.12Verified Invariants12 invariants
2 columns
InvariantResult
Computed roll matches the served roll byte-identically for all 6,700 betsPass
Half-open rule on the recomputed roll reproduces the served win flag for all 6,700 betsPass
multiplier === 9900 / basisPoints (binary64 quotient) for all 2,807 winsPass
winningAmount == ROUND_HALF_EVEN₈(stake × ROUND₈(multiplier)) for all 6,700 bets, zero tolerancePass
winningAmount == 0 and multiplier == 0 for all 3,893 lossesPass
Wallet-denominated settle fields equal the game ledger on all 6,700 betsPass
Credit is fully determined by (roll, band, stake) — no served multiplier needed — for all 6,700 betsPass
Phase C ($10.00) rolls derive from the identical three-argument function as $0.10 betsPass
No hidden inputs beyond (serverSeed, clientSeed, nonce) — stakes present are exactly {$0.10, $10.00}Pass
All four modes present at volume; 5,304 distinct bands; 4,176 distinct win-chance valuesPass
Phase D: 500/500 rolls verified across 10 custom pfaudit client seedsPass
Multiplier strictly decreasing in win chance across 4,176 points, spanning 1.0105×–99×Pass
3.13Reproduction InstructionsRun locally

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

reproduce-s3.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dice && git checkout v1.1.0npm cinpm run verify# Expected output: Steps 5, 6, 7, 8, 9, 10, 11, 12, 14, 18, 20, 21 all PASS
S3-related steps:

  [PASS] Step 5 — Roll Recomputation & Draw Bias Bound
  [PASS] Step 6 — Win-Rule Reproduction
  [PASS] Step 7 — Multiplier Derivation
  [PASS] Step 8 — Credit Arithmetic
  [PASS] Step 9 — Loss Settlement
  [PASS] Step 10 — Wallet Credit & Odds Bounds
  [PASS] Step 11 — Dataset Integrity
  [PASS] Step 12 — Phase & Mode Coverage
  [PASS] Step 14 — Custom Client-Seed Control
  [PASS] Step 18 — Bet-Size Invariance
  [PASS] Step 20 — Settlement Determinism
  [PASS] Step 21 — Multiplier Monotonicity & Range
04

RTP & Payout Logic

Is the house edge what the casino claims?

This section verifies that the flat 1.00% house edge is exactly what’s advertised on every band in every mode. The key test is anti-circularity: we prove the RTP from first principles, not from any casino-supplied figure — a band of b basis points wins on exactly b of the 10,000 equally likely grid points and pays 9900 / b, so P(win) × multiplier = 0.99 cancels exactly. We check that identity on all 6,700 captured bets, hold LIQD’s published 99% to every one of the 9,800 settings under the RTP claim rule, compare it against 98 million simulated bets across 98 targets, and test whether the casino pre-selected seeds against the player.

Return to Player Verification

99.0000%theoretical RTP

What we verified

  • House edge is exactly 1.00% — flat across every band, every mode and every bet size
  • RTP proven from first principles: (b / 10,000) × (9900 / b) = 0.99 — as an integer identity on all 6,700 captured bets, not a float approximation
  • LIQD’s published 99% RTP holds on all 9,800 settings — worst gap 0.000000 pp against the ±0.1 pp band of the RTP claim rule
  • The quotient multiplier is served bit-exact on every one of the 2,807 captured wins, and the 8-decimal settlement rounding is directionless (1,341 down, 1,316 up, 150 exact)
  • 98 targets × 1M simulated bets: all 98 within 5·SE, mean 99.0203% — 0.98·SE from 99.0000%; uniformity, serial and nine effective-edge bands also pass
  • Cherry-pick test: the bands LIQD served on its 134 seeds show no return deficit (z = 0.752, p = 0.7738)
  • Bet amount does not influence the roll — confirmed at $0.10 and $10

What this means for you

  • The house edge on Dice is a flat 1.00%, the same whichever mode, band or bet size you choose
  • The 99% LIQD advertises is what every setting actually returns
  • The RTP proof is derived independently — it doesn’t rely on trusting the casino
  • The casino’s seeds show no sign of being chosen against the player
  • Your bet amount doesn’t affect the number rolled
  • The multiplier on screen is exactly what you are paid — settlement rounds at the 8th decimal in both directions, never systematically against you
99.0000%theoretical RTP
  • House edge is exactly 1.00% — flat across every band, every mode and every bet size
  • RTP proven from first principles: (b / 10,000) × (9900 / b) = 0.99 — as an integer identity on all 6,700 captured bets, not a float approximation
  • LIQD’s published 99% RTP holds on all 9,800 settings — worst gap 0.000000 pp against the ±0.1 pp band of the RTP claim rule
  • The quotient multiplier is served bit-exact on every one of the 2,807 captured wins, and the 8-decimal settlement rounding is directionless (1,341 down, 1,316 up, 150 exact)
  • 98 targets × 1M simulated bets: all 98 within 5·SE, mean 99.0203% — 0.98·SE from 99.0000%; uniformity, serial and nine effective-edge bands also pass
  • Cherry-pick test: the bands LIQD served on its 134 seeds show no return deficit (z = 0.752, p = 0.7738)
  • Bet amount does not influence the roll — confirmed at $0.10 and $10
INDEPENDENT DERIVATION
(b / 10,000) × (9900 / b)= 0.99
Half-open band of b basis points · Exact integer identity 100 · winCount · 9900 = 99 · 10,000 · b on all 6,700 captured bets · All 9,800 settings (win chance 0.01%–98.00%) return exactly 99%
99.0000%Theoretical RTP (every band, every mode — exact integer identity)
99.0203%Simulated (98 targets × 1M bets)
1.00%House Edge (flat)
9,800 / 9,800Settings meet LIQD’s published 99%
CheckResultEvidence
Anti-circularityPassP(win) × multiplier = (b / 10,000) × (9900 / b) = 0.99 for every band — proven by cancellation of the grid count against the quotient formula, no casino data used; closed-form win counts equal a literal 10,000-point tally on all 5,304 captured bands (Step 13)
House edge auditPassFlat 1.00% house edge asserted in exact integers (100 · winCount · 9900 === 99 · 10,000 · b, no tolerance) on 6,700 / 6,700 bets, across under / over / inside / outside; the float form of the same quantity strays from 0.01 by at most 6.32e-15 (Step 15)
RTP claim (RTP-CLAIM 1.2.0)PassLIQD publishes 99% RTP (Edge 1.00%) on the Dice game page; all 9,800 settings return exactly 99.000000% — worst gap 0.000000 pp against the ±0.1 pp band (Step 15)
Simulated RTP (Pass 1)Pass98 / 98 targets (over 2.00 to over 99.00, 1M bets each) within 5·SE + 1e-4, mean 99.0203% at 0.98·SE from 99.0000%; draw uniformity p = 0.2335 (two-sided), serial screens clear, 9 / 9 effective-edge bands within 5·SE + 1e-4 (Step 16)
Cherry-pick test (Pass 2)PassServed-band return over 134 seeds × 10,000 nonces: z = 0.752, one-sided p = 0.7738 against α = 0.01 — no return deficit (Step 17)
Bet-size invariancePassBet amount is not an input to the RNG — same roll derivation at $0.10 and $10. Tested in Phase C (200 / 200, Step 18)
Multiplier formulaPass9900 / b served bit-exact on all 2,807 wins; strictly decreasing in win chance across 4,176 distinct points; span 1.0105×–99× within [1.0102, 9900] (Steps 7, 21)
Anti-circularityPass
P(win) × multiplier = (b / 10,000) × (9900 / b) = 0.99 for every band — proven by cancellation of the grid count against the quotient formula, no casino data used; closed-form win counts equal a literal 10,000-point tally on all 5,304 captured bands (Step 13)
House edge auditPass
Flat 1.00% house edge asserted in exact integers (100 · winCount · 9900 === 99 · 10,000 · b, no tolerance) on 6,700 / 6,700 bets, across under / over / inside / outside; the float form of the same quantity strays from 0.01 by at most 6.32e-15 (Step 15)
RTP claim (RTP-CLAIM 1.2.0)Pass
LIQD publishes 99% RTP (Edge 1.00%) on the Dice game page; all 9,800 settings return exactly 99.000000% — worst gap 0.000000 pp against the ±0.1 pp band (Step 15)
Simulated RTP (Pass 1)Pass
98 / 98 targets (over 2.00 to over 99.00, 1M bets each) within 5·SE + 1e-4, mean 99.0203% at 0.98·SE from 99.0000%; draw uniformity p = 0.2335 (two-sided), serial screens clear, 9 / 9 effective-edge bands within 5·SE + 1e-4 (Step 16)
Cherry-pick test (Pass 2)Pass
Served-band return over 134 seeds × 10,000 nonces: z = 0.752, one-sided p = 0.7738 against α = 0.01 — no return deficit (Step 17)
Bet-size invariancePass
Bet amount is not an input to the RNG — same roll derivation at $0.10 and $10. Tested in Phase C (200 / 200, Step 18)
Multiplier formulaPass
9900 / b served bit-exact on all 2,807 wins; strictly decreasing in win chance across 4,176 distinct points; span 1.0105×–99× within [1.0102, 9900] (Steps 7, 21)
✓  RTP Behaves as Advertised

The 99.0000% RTP is proven mathematically from grid arithmetic — under the half-open rule every band of b basis points wins on exactly b of 10,000 grid points and pays 9900 / b, so the product is 0.99 for all 5,304 captured bands and for every one of the 9,800 settings the game offers. LIQD’s published 99% therefore holds at every setting, with zero gap. This is a first-principles proof, not a statistical estimate; 98 million simulated bets across 98 targets are consistent with it, and the cherry-pick test finds no sign of seed selection. The house edge is flat at 1.00% — no mode or band carries a hidden penalty, and the only money-side approximation, the 8-decimal settlement rounding, is directionless.

How It Works — RTP & Payout Logic8 sections
4.1Anti-Circularity Proof (Steps 13, 15)

The anti-circularity proof establishes the 99.0000% RTP from first principles without using any casino-supplied probability data. Dice's proof is an algebraic identity rather than a summation — the win probability and the payout multiplier both contain the band's basis-point count b, which cancels exactly. Step 13 derives the win probability from grid arithmetic only, proves the check is falsifiable with a negative control (a deliberately corrupted roll must be rejected by the same predicate Step 5 uses), and anchors the closed-form win count with a counting leg — every one of the 10,000 grid points is passed through the win rule and tallied for each of the 5,304 distinct captured bands, a method in which a scaling or rounding slip is inexpressible. Step 15 then asserts the identity itself in exact integers on every captured bet:

Anti-Circularity Proof (Steps 13, 15) — illustrated flow
3 columns
ComponentFormulaSource
Win probabilityb / 10,000 — b winning grid points under the half-open ruleGrid arithmetic — computed by the audit
Counting anchorLiteral tally: apply diceWin to every draw 0…9999 and count5,304 distinct captured bands — 0 mismatches against the closed form, no tolerance
Quoted multiplier9900 / b — (1 − 0.01) × 10,000 over the basis pointsClosed form — validated bit-exact against every captured win (Step 7)
RTP(b / 10,000) × (9900 / b) = 9900 / 10,000 = 0.99The product — b cancels
Integer form100 · winCount · 9900 === 99 · 10,000 · b (winCount = the literal tally, b = the priced basis points)Asserted with === on 6,700 / 6,700 bets (Step 15) and over 59,998 bands in the unit suite
5 columns
BandWin chance (b / 10,000)Multiplier (9900 / b)P(win) × MultiplierRTP
under 505000 / 10000 = 0.5000001.980000000.99000099.0000%
over 901000 / 10000 = 0.1000009.900000000.99000099.0000%
inside 25–755000 / 10000 = 0.5000001.980000000.99000099.0000%
outside 25–755000 / 10000 = 0.5000001.980000000.99000099.0000%
under 92.37 (featured bet)9237 / 10000 = 0.9237001.071776550.99000099.0000%
over 99100 / 10000 = 0.01000099.000000000.99000099.0000%
under 0.011 / 10000 = 0.0001009900.000000000.99000099.0000%
Result: Every band: RTP = 99.0000% exactly, by cancellation — no summation, no convergence. The counting anchor matches the closed-form win count with integer equality on all 5,304 captured bands, the negative control rejects a corrupted roll (Step 13), and the integer identity holds with === on 6,700 / 6,700 bets (Step 15). Theoretical RTP proof is non-circular and closed-form.

Anti-Circularity Verification:

tests/steps/anti-circularity.ts · Step 13 · Verified
/** * Step 13: Anti-Circularity — the verifier's independence from operator data. * The theoretical quantities (win probability, multiplier, edge) are pure arithmetic over * the roll grid and band geometry — they never read an operator payout/odds field. To prove * the check is falsifiable (not a vacuous PASS), we run a NEGATIVE CONTROL: corrupt one * recomputed roll and confirm the determinism logic would reject it. */ /** * Literal tally over the 10,000-point grid: apply the win rule to every draw and count. * This is the genuinely arithmetic-error-immune anchor — it cannot share a scaling or * rounding mistake with the closed form, because it never scales a bound at all. */function enumCount(p: DiceParams): number {  let c = 0;  for (let d = 0; d < RANGE; d++) if (diceWin(d / SCALE, p)) c++;  return c;}

Why this proof is non-circular: P(win) = b / 10,000 comes from counting grid points, and 9900 / b is the quoted multiplier — LIQD’s 99% return divided by the win chance. They are reciprocals up to 0.99 — they cancel exactly, no summation, no approximation, no "converges to". The identity holds by inspection for every band with b ≥ 1, which is why the 99.0000% RTP is preserved on every mode and every width, from a 98% band to the single-grid-point 9900× tail. What the identity depends on is the half-open rule — with an inclusive upper bound a band would win on b + 1 points and the edge would shrink as the band narrows, reaching a player-favourable 198% at the 0.01% extreme. That rule is what LIQD’s own verifier returns at the bound (E13, S4.7) and what all 6,700 settled win flags reproduce. What the audit adds is evidence that LIQD’s implementation honours the algebra on every captured bet — the quotient really is what gets served and paid (Steps 7, 8, 20) — and that the probability side is derived without touching a single operator-supplied number.

4.2RTP Claim Rule (Step 15)

The RTP claim rule (RTP-CLAIM 1.2.0) is part of the audit method: the RTP a casino publishes must hold at every setting the game offers. A published figure counts wherever a player can find it — on the game page, the fairness page, the help page, the bet panel, the rules, or in the game settings the site loads. Each setting’s RTP is computed exactly, as a rational number, from the grid and the multiplier formula — never from the casino’s figure — and must sit within ±0.1 percentage points of the claim. A setting below that band on the house side is a miss: the verdict then reads RTP FIGURE OVERSTATED, even when every draw check passes. A setting above the claim never fails. The rule is scored on Step 15, the step that derives the effective edge.

2 columns
ItemDetail
Claim99% RTP — a flat 1.00% house edge
Where LIQD publishes itThe Dice game page, liqd.com/originals/dice: header "Edge: 1.00%", stats panel "RTP 99%" and "House Edge 1%", page text "every setting in Dice returns 99%" — read logged out on 1 October 2026 (evidence/rtp-page-labels-2026-10-01.json)
Settings tested9,800 — every legal band width, win chance 0.01% to 98.00% in basis points (multiplier 9900 / b from 1.0102× to 9900×)
MethodExact rational: b / 10,000 winning draws × the quoted multiplier 9900 / b
Band±0.1 percentage points (house side fails, player side never fails)
True minimum · maximum99.000000% · 99.000000%
Worst gap0.000000 pp
Settings beyond the band0 on the house side · 0 on the player side
Scored onStep 15 (profile check DICE-19)
ResultPass — the claim holds at every setting
6 columns
Setting (win chance)MultiplierExact RTPRTPGapResult
0.01% (b = 1)9900×99/10099.000000%0.000000 ppPass
1.00% (b = 100)99×99/10099.000000%0.000000 ppPass
2.00% (b = 200)49.5×99/10099.000000%0.000000 ppPass
10.00% (b = 1,000)9.9×99/10099.000000%0.000000 ppPass
50.00% (b = 5,000)1.98×99/10099.000000%0.000000 ppPass
92.37% (b = 9,237)1.0718×99/10099.000000%0.000000 ppPass
98.00% (b = 9,800)1.0102×99/10099.000000%0.000000 ppPass
All 9,800 settings9900 / b99/10099.000000%0.000000 ppPass — 9,800 / 9,800
Result: Pass — LIQD’s published 99% is met exactly by all 9,800 settings: the true minimum and maximum are both 99.000000%, so the worst gap is 0.000000 pp against the ±0.1 pp band (Step 15). The full per-setting table ships as outputs/rtp-claim-table.json, pinned by SHA-256 and re-derived on every run.
4.3House Edge & Settlement Rounding (Step 15)

Step 15 scores the effective edge on every one of the 6,700 captured bets — not a sample of configurations — computing 1 − P(win) × quotedMultiplier from first principles and asserting the underlying identity in exact integers: 100 · winCount · 9900 === 99 · 10,000 · basisPoints, with no tolerance. It holds on 6,700 / 6,700 bets, with 0 player-favourable bands, and by mode the edge is 1.000% flat (under 1,362 bets · outside 1,386 · inside 1,360 · over 2,592 — exactly 1.000% in each). The float form of the same quantity strays from 0.01 by at most 6.32e-15 across the capture and 5.06e-13 at the un-sampled 9900× extreme (upper 100 − lower 99.99 in binary64), which is exactly why the assertion is integral rather than a float comparison. The step also confirms the roll ceiling (max captured roll 99.99), that over 99 pays exactly 99× at a 1.0000% edge on all 500 Phase B bets, and that every captured multiplier sits within the audit bound of 9900×.

The only money-side approximation anywhere in the pipeline is settlement rounding at the 8th decimal. Across the 6,700 captured bets the RTP a player actually receives after that rounding spans 98.999994912% – 99.000005204% — below the nominal 99% on 2,742 bets, above it on 2,687, exactly on it on 1,271, and never at or above 100%. Across the 2,807 winning payments the rounding went against the player 1,341 times, for the player 1,316 times and landed exactly on the product 150 times (maximum 5.50e-9 USDC, net +4.91e-8 USDC) — directionless, and immaterial. These figures are computed by committed code (src/report-figures.ts) into outputs/report-figures.json and reconciled field-for-field by Step 19 on every run.

Result: Effective edge = 1.00% flat — the integer identity holds on every captured bet and every mode (Step 15, 6,700 / 6,700; Steps 7, 8 and 20 prove the served multiplier and credit honour it on every win).
Settlement rounding: Two stages at 1e-8 — round8(m), then stake × round8(m) half-even. Player-visible in the 8th decimal, in both directions, maximum 5.50e-9 USDC per win; 274 wins sat on an exact tie where the half-even rule decided the last unit.
4.4Simulation Pass 1 — Our Own Seeds (Step 16)

Section 4.1 proves RTP = 99.0000% mathematically. Does the algorithm deliver that at scale? To find out, we simulated 98 target bands at 1,000,000 bets each — every over-mode band from over 2.00 (98% win chance, 1.0102×) to over 99.00 (1%, 99×) — with our own implementation and our own seeds, derived from a pinned master seed, rotated every 50 rolls like real epochs, and replayable byte for byte. If the simulated payouts sit within tolerance of 99.0000%, the implementation matches the math. Pass 1 is scored as follows. The three statistical screens — uniformity, lag-1 and runs — are each judged at α/3, so together they hold a family-wise level of α = 0.01; the 98 targets, the nine effective-edge bands, the 98-target mean and the under-50 run’s final point must each sit within 5·SE + 1e-4 of 99.0000%:

Simulation Pass 1 — Our Own Seeds (Step 16) — illustrated flow

98 over-mode targets: over 2.00 to over 99.00 at 1,000,000 bets each — 98,000,000 bets. 98 / 98 within 5·SE + 1e-4 of theory (68 within 1·SE, 95 within 2·SE, 97 within 3·SE; largest over 56.00 at 3.08·SE); 51 above theory, 46 below, 1 exactly on it. Their mean is 99.0203% at 1M bets per target, 0.98·SE from 99.0000% — the chart above. Every target’s seeds are re-derived from the master seed and every simulated RTP reconstructed bit-for-bit from an integer win count.

Draw uniformity: 2,000,000 draws over 100 bins (χ², df = 99, exact incomplete-gamma p). Two-sided at α/3 — χ² = 108.885, p = 0.2335; the lower tail would reject a statistic too uniform to be a real sample of that size.

Serial screen: Lag-1 autocorrelation and the Wald-Wolfowitz runs test on the first 200,000 rolls. z = 0.159 against the applied critical 2.9352; runs p = 0.6910 — both clear at α/3.

Effective-edge bands: Nine canonical bands at 2,000,000 bets each — the table below. 9 / 9 within 5·SE + 1e-4 of 0.99, band set identical to SIM_EDGE_BANDS, every derived field re-derived and every simulated RTP reconstructed bit-for-bit from an integer win count.

Under-50 run: 20,000,000 bets on under 50 (1.98×) sampled at seven pinned marks; final 99.0476% vs 99.00%, Δ = 2.15·SE. All seven standard errors recomputed exactly; the seven win counts (493 … 10,004,806) cumulatively consistent and reconstructing bit-for-bit to the published RTPs.

4 columns
BandTheoretical RTPSimulated RTP (2M)Deviation
under 50 (1.98×)99.0000%98.998%0.03·SE
over 50 (1.98×)99.0000%98.930%1.00·SE
under 10 (9.9×)99.0000%98.896%0.50·SE
over 90 (9.9×)99.0000%99.446%2.12·SE
inside 25–75 (1.98×)99.0000%99.068%0.97·SE
outside 25–75 (1.98×)99.0000%99.113%1.61·SE
over 98 (49.5×)99.0000%98.597%0.82·SE
over 99 (99×)99.0000%98.951%0.07·SE
under 1 (99×)99.0000%99.480%0.69·SE
src/simulate.ts · Pass 1 seeds and the 98-target loop · Verified
const SIM_MASTER_KEY = Buffer.from(SIM_MASTER_SEED, 'hex');let simSeedCounter = 0;const randSeed = (): string =>  createHmac('sha256', SIM_MASTER_KEY).update(String(simSeedCounter++)).digest().subarray(0, 16).toString('hex');// …  const targetRows = SIM_TARGETS.map(({ label, params: p }) => {    const m = quotedMultiplier(p);    const seedCounterFirst = simSeedCounter;    // Exactly the band loop above: s = randSeed(), c = randSeed(), a fresh server seed every 50 bets.    let wn = 0, wi = 0, k = 0;    let s = randSeed(); const c = randSeed();    let firstServerSeed = '';    const marks: { n: number; wins: number; rtp: number; se: number }[] = [];    for (let i = 0; i < SIM_TARGET_BETS; i++) {      if (i % 50 === 0) { s = randSeed(); if (i === 0) firstServerSeed = s; }      const roll = drawFrom(s, c, i % 50) / SCALE;      if (diceWin(roll, p)) { wn += m; wi++; }      if (k < SIM_TARGET_MARKS.length && i + 1 === SIM_TARGET_MARKS[k]) {        const at = i + 1, r = wn / at;        marks.push({ n: at, wins: wi, rtp: r, se: convergenceSE(at, r, wi, m) });        k++;      }    }  // …  const targetPoints = SIM_TARGET_MARKS.map((nAt, j) => ({ n: nAt, ...crossTargetPoint(targetRows.map((r) => r.marks[j])) }));
Result: 138M rolls. 98 / 98 targets and 9 / 9 bands within 5·SE + 1e-4 of the 99.0000% identity; the 98-target mean lands at 99.0203%, 0.98·SE from theory, and the under-50 run at 99.0476%, 2.15·SE. Uniformity p = 0.2335 two-sided and both serial screens clear — 0 failures at the Bonferroni-corrected level. The simulated payouts are consistent with the 99.0000% identity (Step 16).

What is a Monte Carlo simulation? Instead of proving fairness with algebra alone, we simulate millions of game rounds. Each round derives a roll from scratch using the same HMAC-SHA256 pipeline as the live game, applies a band, and records whether the bet won and what it paid. We then compare the aggregate return to the theoretical 99.0000%. If they match within tolerance, the math holds in practice — not just on paper.

How the artifact is scored, not believed: Step 16 does not read the simulation’s own pass flags. It recomputes the χ² p-value from the stored statistic, re-derives the lag-1 z from the correlation and the runs z from the stored cell counts, re-derives every target’s and effective-edge row’s multiplier, win probability, theoretical RTP and standard error from the band and the pinned depth, and checks the integer win count behind every published RTP — a published number that no integer win count could produce fails the step (a mutation that sets over 98 to 0.99000495, implying 40,000.2 wins, is caught exactly this way). Because the seeds come from a pinned master seed, npm run test:full regenerates the whole 138M-roll experiment and reproduces the committed artifact byte for byte.

Reading the deviations: Each band is an independent 2,000,000-bet experiment at that band’s own variance, which grows with the multiplier (a 99× band’s return moves by 0.005 pp per win). The largest gap in standard errors is over 90 at 2.12σ; the largest in percentage points is under 1 at +0.48 pp, only 0.69σ for that band. With nine independent bands one beyond 2σ is expected behaviour, not signal — the scored gate is the 5·SE + 1e-4 tolerance and the integer-count reconstruction. On the captured bets the deterministic edge is 1.000% in every mode (Step 15).
4.5Cherry-Pick Test — Pass 2 (Step 17)

Could the casino have chosen server seeds that pay players less? Pass 2 takes every server seed LIQD actually used — all 134 revealed seeds — and replays each one for 10,000 nonces with the epoch’s recorded client seed. The load-bearing statistic is the served-band return: the payout-weighted return of the bands LIQD actually served in the captured window (nonces 0–49), standardised by its exact expectation and variance — each band’s exact lattice win probability times its multiplier. A seed set chosen against the player would show up as a return deficit in the window the player saw, so the test is one-sided at α = 0.01. Step 17 re-derives every per-epoch row from the revealed seeds and compares it with the artifact exactly. A supplementary screen checks each seed’s draws for unusual non-uniformity against an exact discrete null.

Primary defence — structural: The client seed is player-editable and rotatable after the server-seed commitment is published. An operator that cannot predict your seed cannot pre-evaluate candidates against it — the foreknowledge channel is removed by design, and Phase D exercised the rotation live.

Statistical check: The served-band return test above — a direct check on the money, backing the structural defence.

2 columns
TestDetail
Seeds tested134 — every revealed server seed in the capture, keyed by its own epoch
Replay depth10,000 nonces per seed — the captured window is nonces 0–49, the rest is the reference
StatisticPayout-weighted return of the served bands, standardised by its exact per-bet mean and variance; one-sided normal tail
Resultz = 0.752, one-sided p = 0.7738 against α = 0.01 — no return deficit (73 epochs below, 2 equal, 59 above expectation)
Detection powerA per-bet return deficit of 14.377 pp or more is detected at 80% power
Supplementary screen50-draw windows over 20 bins against an exact discrete null (832 values from 181,274 partitions): 10 flags vs 5.87 expected, P(X ≥ 10) = 0.0708 — consistent with chance; rejection would need 13 flags
Pooled real-roll uniformityAll 6,700 captured rolls, 100 bins: χ² = 92.21, df = 99, p = 0.6724 — recomputed from the dataset, informational
tests/steps/simulation.ts · Step 17 (served-band return) · Verified
      for (let nn = 0; nn < SIM_PASS2_NONCES; nn++) {        const band = served[nn % W2];        const mult = quotedMultiplier(band);        const p = discreteWinProbability(band);                       // EXACT lattice win-probability        const roll = diceRoll(serverSeed, clientSeed, nn) / SCALE;        const won = diceWin(roll, band);        const ret = won ? mult : 0;        const eRet = p * mult, vRet = p * (1 - p) * mult * mult;        if (nn < W2) { eR += ret; eE += eRet; eV += vRet; earlyCells.push({ band, won }); }        else         { rR += ret; rE += eRet; rV += vRet; }      }  // …    const sbAggZ = sbTotalEarlyVar > 0 ? (sbTotalEarlyReturn - sbTotalEarlyExpected) / Math.sqrt(sbTotalEarlyVar) : 0;    const sbAggP = normalLowerTailP(sbAggZ);  // …    const sbCherryClean = sbAggP >= SIM_PASS2_ALPHA;                   // HARD_FAIL on a cherry-pick signal
Result: PASS. The bands LIQD served on its 134 seeds returned what their exact odds predict: z = 0.752, p = 0.7738 against α = 0.01, every per-epoch row re-derived from the revealed seeds with 0 mismatches. The supplementary screen finds 10 flags against 5.87 expected (P = 0.0708), and the 6,700 captured rolls pool to a uniform χ² (p = 0.6724). The test finds no sign of seed selection (Step 17).
Why is this included? Cherry-pick testing is a standard part of our audit methodology. Commit-reveal constrains what happens after a commitment, not which seed gets committed. LIQD lets players set or rotate the client seed after the server-seed commitment is published, so cherry-picking is structurally contained for a player who sets an unpredictable seed. Pass 2 adds a statistical check on the money itself, and it replays identically on every run (validated on Node 22).
4.6Bet-Size Invariance (Step 18)

Phase C placed 200 bets at $10.00 on over 50 — the 1.98× band, 100× the $0.10 stake used in every other phase. All 200 rolls were recomputed byte-identically from revealed seeds using the same single-draw HMAC-SHA256 path, and every $10.00 win credited exactly stake × 1.98. Bet amount is not an input to the RNG and does not affect the roll distribution or the edge.

3 columns
MetricPhases A, B, D ($0.10)Phase C ($10.00)
Rounds6,500200
Roll recomputation parity6,500 / 6,500200 / 200
Served multiplier = 9900 / b2,702 / 2,702 wins105 / 105 wins (1.98× on over 50)
Credit per winstake × round8(9900 / b), half-even at 8 dp — every win reproduced (Steps 8, 20)$19.80000000 — exactly $10.00 × 1.98
Effective edge1.0000% on every captured bet1.0000%
Wager argument in diceRollnone — the function has no stake parameternone — same function, same code path
Result: Equivalence proven deterministically. Same RNG, same multiplier formula, same roll distribution regardless of bet amount — 200 / 200 Phase C rounds recompute (Step 18).
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 data collection, on the evidence behind the boundary rule, and on fields outside the scored surface. At the per-phase sample sizes in this audit, empirical RTP is not a meaningful measure of fairness — the identity in 4.1 is the authoritative evidence.

3 columns
ItemValueContext
Live capture RTP103.56%$2,744.36 returned on $2,650.00 wagered — variance illustration only; Phase B’s 23 tail wins alone returned $133.65 on $100 staked
Live win rate41.90% (2,807 / 6,700)Mixed win chances across 5,304 bands — informational
Roll range0.00 – 99.99, mean 49.796Ceiling 99.99 confirmed on every bet; both extremes observed
Live-sample serial screen (Step 22)lag-1 z = 1.057 · runs p = 0.2618The 6,700 captured rolls in capture order: 3,305 runs vs 3,350.9 expected — no serial dependence; reported, never scored
Boundary evidence (E13)7 verifier-endpoint probes, 31 August 2026Roll 3.61 at an unused nonce of the epoch 0 seed: roll == upper loses (non-inverted) and wins (inverted) — half-open confirmed; probes 1 and 5 are the two that discriminate between the candidate rules
Boundary hits in the capture2 lower-bound hits, 0 upper-bound hits4,106 bets had a reachable upper bound (expected hits 0.41); none landed on one, so the E13 verifier checks settle the upper-bound rule
Odds settings and recorded acceptanceminOdds 1.0102 · maxOdds 99 · maxBet 9,898 USDCThe settings field reads maxOdds 99, yet E14 records a $0.10 bet on [0, 0.01) accepted at a 9900× quote and settled on 1 September 2026 (a loss, roll 84.00); the audit uses the live-witnessed 9900× bound
Phase D exposure10 epochs, $50.00 staked, $48.93 paidExpected $49.50; z = −0.14
Settlement residualmax 5.50e-9 USDC · net +4.91e-8 USDCDirectionless at the 8th decimal — 1,341 wins down, 1,316 up, 150 exact; 274 exact ties
Capture-side self-check fieldslocalRoll, localWin, verified, modelMultiplier, multOk, walletOk, meta.edgeScanCapture annotations, not audit findings — scored verification derives every result from the recorded inputs and ignores them
4.8Worked Example — Payout Verification

Real bet from Phase B — bet RbGgV6toJS-1CgkkJI0vC (epoch 113), nonce 47, over 99 at $0.10. Verified from dice-master-6700bets.json — one of the two captured rolls that landed exactly on a band's lower bound, and one of the four 99× wins in the capture:

serverSeed = 36a8d90f5e7cd710ae29bfc89eb709c7
clientSeed = audit4195522a3970
nonce      = 47
band       = [99, 100), not inverted  (over 99)
betAmount  = $0.10
recorded   = roll 99.00 · win true · multiplier 99 · winningAmount 9.9

Step 1 — Roll computation: HMAC-SHA256(seed, "audit4195522a3970:47:0") → first chunk 0xbd93f34c = 3,180,589,900 < maxFair → % 10000 = 9900 → roll 99.00 — matches the record.

Step 2 — Win check: 99.00 ≥ 99 and 99.00 < 100 → in band → the round is legitimately won. The roll sits exactly on the lower bound, which the half-open rule includes — the served flag agrees.

Step 3 — Multiplier derivation: W = 100 − 99 = 1 → b = 100 → 9900 / 100 = 99 exactly; the served multiplier is 99.

Step 4 — Payout: round8(99) = 99 → 0.10 × 99 = 9.9 — no tie, no rounding → $9.90000000, the credited winningAmount, verbatim from the record.

3 columns
StepProcessOutput
1Recompute the roll from seeds99.00 — byte-identical
2Apply the half-open band [99, 100)Lower bound is in — won confirmed
3Quotient multiplier 9900 / 10099
4Two-stage settlement of 0.10 × 99$9.90 credited
5RTP identity at this band(100 / 10,000) × 99 = 0.9900 — the flat edge, exactly
RTP proof for over 99 · Verified
winCount(over 99)        = grid points 9900 … 9999 = 100   →  P(win) = 100 / 10,000 = 0.01quotedMultiplier(over 99) = 9900 / 100                        =  99RTP = P(win) × multiplier    = 0.01 × 99    = 0.9900   →  99.0000%   (b cancels; only the edge remains)
Parity verified: Bet RbGgV6toJS… — roll, win condition on the lower bound, quotient multiplier and payout all match. RTP for over 99 = 99.0000% proven from grid arithmetic, independent of any casino data. Phase B's 500 over-99 bets produced 4 such wins against 5 expected.
Live Game$9.90
Our recomputation$9.90
Technical Evidence & Verification5 sections
4.9Evidence Coverage Summary9 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Step 13)5,304 distinct bands — closed-form win count == literal 10,000-point tally, 0 mismatches; negative control rejects a corrupted rollPass
House edge audit (Step 15)6,700 / 6,700 bets — integer identity 100 · winCount · 9900 === 99 · 10,000 · b; 0 player-favourable bands; all four modes at 1.000%Pass
RTP claim rule (Step 15)9,800 / 9,800 settings at exactly 99.000000%; worst gap 0.000000 pp against ±0.1 ppPass
Multiplier & credit reconciliation (Steps 7, 8)2,807 wins bit-exact 9900 / b; 6,700 credits at zero tolerancePass
Simulation Pass 1 (Step 16)138M rolls — 98 / 98 targets, mean 99.0203% at 0.98·SE; uniformity p = 0.2335, serial clear, 9 / 9 bandsPass
Simulation Pass 2 (Step 17)Served-band return over 134 seeds × 10,000 nonces: z = 0.752, p = 0.7738; supplementary screen 10 flags vs 5.87 (P = 0.0708)Pass
Bet-size invariance (Step 18)200 / 200 Phase C at $10.00Pass
Artifact & figure integrity (Step 19)Dataset, simulation and chart pins match; report figures reconcile field-for-field; 5,609 numeric values inside domains derived from src/config.tsPass
Multiplier monotonicity (Step 21)4,176 distinct win-chance points, 0 inversions, 1.0105×–99× within [1.0102, 9900]Pass
4.10Code References13 source files
2 columns
FilePurpose
tests/steps/anti-circularity.tsStep 13: Negative control + literal grid enumeration anchor
tests/steps/boundary.tsStep 15: Roll ceiling, over-99 multiplier, odds bounds and the exact integer edge identity on every bet
src/rtp-claim.tsThe RTP claim rule, RTP-CLAIM 1.2.0 — every package carries the same file byte for byte
src/rtp-settings.tsThe 9,800-setting exact RTP table and the claim it is held to (LIQD’s published 99%)
tests/steps/payouts.tsSteps 7–10: Multiplier derivation, credit arithmetic, loss settlement, wallet fields
tests/steps/simulation.tsSteps 16–17: Simulation integrity and the cherry-pick test (served-band return, exact-null screen)
tests/steps/standardization.tsSteps 18–21: Bet-size invariance, artifact and value-domain integrity, settlement determinism, multiplier monotonicity
tests/steps/statistical.tsInformational items — live RTP, per-mode edge, roll range, win rate, odds settings
src/simulate.tsMonte Carlo simulation (138M rolls, two-pass), deterministic from the pinned master seed — Pass 1 screens, 98 targets, edge bands and the under-50 run, Pass 2 served-band return and exact-null screen
src/exact-chi2.tsExact discrete null for the Pass 2 supplementary screen — 181,274 partitions, 832 support values, achieved α and the derived flag rate
src/stats.tschiSquaredTest, lag1Autocorrelation, runsTest, inverseCriticalZ (AS 241), binomialTailP
src/config.tspayoutBasisPoints, quotedMultiplier, discreteWinCount, diceWin, effectiveEdgeIsExact, theoreticalRTP, settledCreditUnits — closed-form formulas
src/report-figures.tsDerived-figure artifact (outputs/report-figures.json) — the producing artifact for the settled-RTP span, residual counts, boundary hit counts, cherry-pick test figures and Phase D exposure, reconciled by Step 19
4.11Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json — Pass 1 (2M-draw uniformity + serial, 98 targets × 1M, 9 bands × 2M, 20M under-50 run) + Pass 2 (served-band return over 134 seeds × 10,000 nonces; exact-null screen) · SHA-256 55873640715d1f9e…
RTP claim table: outputs/rtp-claim-table.json — all 9,800 settings with exact RTP, gap and outcome · SHA-256 075811c734cfbf4b…, pinned in src/rtp-settings.ts
Derived figures: outputs/report-figures.json — settled-RTP span, settlement residual, boundary hit counts, cherry-pick test figures, Phase D exposure and live aggregates, re-derived field-for-field by Step 19 on every run
Game config: Quotient formula 9900 / round(100 × W), the half-open win rule and the two-stage settlement in src/config.ts — algebraic, not a lookup table
Primary dataset: data/dice-master-6700bets.json — payout reconciliation, Phase C bet-size invariance, Pass 2 seeds and empirical-RTP context
Convergence chart: outputs/rtp-convergence.html — the mean simulated RTP of the 98 targets at five marks (10K to 1M bets per target) with its ±5·SE band, rendered by src/chart.ts and pinned by SHA-256 6b7b6bb7c474a49b…; the final point, 99.0203%, sits 0.98·SE from 99.0000%, inside the band
Session evidence: evidence/E13-verify-endpoint-boundary-probes.json (7 probes + settled cross-check, 31 August 2026), evidence/E14-maxodds-live-record.json (settings pull, UI clamp, one losing 9900× bet, 1 September 2026) and evidence/rtp-page-labels-2026-10-01.json (the RTP figures on LIQD’s game page, 1 October 2026)
4.12Verified Invariants14 invariants
2 columns
InvariantResult
(b / 10,000) × (9900 / b) = 0.9900 for every band (non-circular)Pass
100 · winCount · 9900 === 99 · 10,000 · basisPoints as integers on all 6,700 bets and at the 9900× extremePass
LIQD’s published 99% is met by all 9,800 settings — worst gap 0.000000 pp against ±0.1 ppPass
Closed-form win count equals a literal 10,000-point tally on all 5,304 distinct captured bandsPass
Negative control: a corrupted roll is rejected by the Step 5 predicate (the check is falsifiable)Pass
Flat 1.00% house edge — the quotient multiplier is served bit-exact on all 2,807 wins; losses pay exactly 0 (3,893 / 3,893)Pass
Settled RTP after 8-decimal rounding spans 98.999994912%–99.000005204% across all 6,700 captured bets — never at or above 100%Pass
Draw uniformity χ² p = 0.2335 over 2,000,000 draws, two-sided at α/3Pass
Lag-1 z = 0.159 < 2.9352 and runs p = 0.6910 ≥ α/3 on 200,000 rollsPass
9 / 9 effective-edge bands within 5·SE + 1e-4 of 0.99, each reconstructing from an integer win countPass
98 / 98 targets within 5·SE + 1e-4; 98-target mean 99.0203%, 0.98·SE from 99.0000% at 1,000,000 bets per target; 490 / 490 target marks and 5 / 5 mean points reconciledPass
Under-50 run 99.0476% within 2.15·SE of 99.0000% at 20,000,000 bets; 7 / 7 standard errors and win counts reconciledPass
No return deficit on the bands served across 134 casino seeds (Pass 2 — z = 0.752, p = 0.7738; 0 row mismatches)Pass
Phase C ($10.00) rolls derive from the same three-argument function as $0.10 bets; bet amount absent from RNG input by constructionPass
4.13Reproduction InstructionsRun locally

Clone the repository, install dependencies, and regenerate the simulation before verifying:

reproduce-s4.sh · 6 lines · Verified
git clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dice && git checkout v1.1.0npm cinpm run test:full   # regenerates the simulation from its pinned master seed, then verifieshead -40 outputs/rtp-claim-table.jsoncat outputs/simulation-results.json outputs/report-figures.json
S4-related steps and the RTP claim rule, as printed:

  [PASS] Step 7 — Multiplier Derivation
  [PASS] Step 8 — Credit Arithmetic
  [PASS] Step 13 — Anti-Circularity (Negative Control + Enumeration Anchor)
  [PASS] Step 15 — Odds Boundary & Effective Edge
  [PASS] Step 16 — Simulation — Pass 1 (RNG + Edge)
  [PASS] Step 17 — Simulation — Pass 2 (Cherry-Pick)
  [PASS] Step 18 — Bet-Size Invariance
  [PASS] Step 19 — Artifact Hash + Value-Domain Integrity
  [PASS] Step 21 — Multiplier Monotonicity & Range

  rtp-claim: ENFORCED — PASS
  rtp-claim:   band ±0.10 pp · 9,800 settings · true min 99.000000% (win chance 0.01%) · true max 99.000000% (win chance 0.01%)
  rtp-claim:   house side beyond band: 0 · player-favourable beyond band: 0 · worst win chance 0.01% at 0.000000 pp
  rtp-claim:   outcome PASS · minimal fix: none — the claim holds for every setting it states a figure for

Final 98-target mean RTP in the committed artifact: 0.9902026676413088 against 0.9899999999999987 (the binary64 mean of the 98 theoretical RTPs; 0.99 exactly in rationals). The simulation is deterministic: npm run test:full regenerates outputs/simulation-results.json and outputs/rtp-convergence.html byte for byte, and the verdict is scored against the committed, SHA-256-pinned artifact. The chart above plots the committed 98-target mean series.
05

Fairness Integrity Testing

Does the implementation maintain fairness under non-standard conditions?

Sections 1–4 establish that the game is mathematically fair; Section 5 tests whether the live implementation keeps its integrity under non-standard conditions. It covers 34 integrity tests run against LIQD’s running service across 12 categories: race conditions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, information disclosure, and the provably fair surface itself. 33 passed. One was remediated: under extreme bursts the server answered some requests with generic errors instead of structured overload responses (Low, robustness); LIQD fixed it and we re-verified it. No test altered, replayed or injected an outcome.

Fairness Integrity Testing

33pass1remediated

What we verified

  • Race conditions — can parallel bets, a high-stake over-commit, extreme bursts, or a seed rotation racing a live bet change an outcome or double-settle?
  • Nonce tampering — can the sequence be forced, replayed, or skipped?
  • Seed handling — can unusual client seeds, or the active server seed, be exposed or overridden?
  • Outcome replay — can an identical bet be settled twice, or a timed-out bet settle twice?
  • Cross-player isolation — can one player act on another’s account, or after logging out?
  • Payout tampering — can roll, multiplier, payout or win-region values be injected, or the payout cap bypassed?
  • Parameter limits — can invalid bands, win chances, stakes or currencies be submitted?

What this means for you

  • Across the 34 tests, no request path let anyone alter, replay or inject an outcome
  • Once a bet is placed, its roll cannot be changed or replayed
  • Each bet gets its own server-assigned nonce under a committed seed
  • Your bets are bound to your session — no other account can act on them
  • The one gap found — generic errors under extreme load instead of structured overload responses — was fixed by LIQD and re-verified; accepted nonces stayed unique throughout
33pass1remediated
  • Race conditions — can parallel bets, a high-stake over-commit, extreme bursts, or a seed rotation racing a live bet change an outcome or double-settle?
  • Nonce tampering — can the sequence be forced, replayed, or skipped?
  • Seed handling — can unusual client seeds, or the active server seed, be exposed or overridden?
  • Outcome replay — can an identical bet be settled twice, or a timed-out bet settle twice?
  • Cross-player isolation — can one player act on another’s account, or after logging out?
  • Payout tampering — can roll, multiplier, payout or win-region values be injected, or the payout cap bypassed?
  • Parameter limits — can invalid bands, win chances, stakes or currencies be submitted?
5 / 5Race Conditions
5 / 5Input Validation
3 / 3Atomic Balance / Idempotency
2 / 2Broken Authorization
4 / 4Max-win Bypass
4 / 4Nonce Manipulation
4 / 4RNG / PF Verification
1 / 1RNG / Information Leak
1 / 1Entropy / PF Verification
1 / 1Precision / Payout
1 / 1Event Replay
3 / 3Adversarial Seeds

Category Coverage

Race Conditions5/5
Input Validation5/5
Atomic Balance / Idempotency3/3
Broken Authorization2/2
Max-win Bypass4/4
Nonce Manipulation4/4
RNG / PF Verification4/4
RNG / Information Leak1/1
Entropy / PF Verification1/1
Precision / Payout1/1
Event Replay1/1
Adversarial Seeds3/3
CheckResultEvidence
Nonce integrityPassServer-assigned and sequential: injected reused, skipped, zero or negative and overflow-sized nonces are all refused (NONCE-001 … NONCE-004); the nonce advances by one per bet (PF-002) and restarts at 0 on rotation (ENTROPY-004)
Seed integrityPassThe active seed is never disclosed before rotation (DISCLOSE-001); 70 / 70 rotations racing live bets revealed the committed seed and never reused one (EXTRA-001); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003)
Outcome determinismPassLive rounds recompute independently to the same roll and band result (EXTRA-002, PARAM-003); a 2,000-roll live sample passes the uniformity test (FAIR-001)
Round & player isolationPass13 / 13 foreign and malformed identifiers rejected; post-logout reuse rejected; parallel identical and high-stake bets settled once each and reconciled to the wallet
Payout integrityPassInjected roll, payout, result, multiplier and win-region fields rejected; the 50,000 USDC potential-payout cap refuses over-limit bets; 200 consecutive rounds reconciled exactly
Overload handlingRemediatedExtreme bursts drew some generic errors instead of structured overload responses (Low); after LIQD’s fix, overload is answered with structured busy and rate-limit responses, and no refused request consumes a nonce (RACE-004)
Nonce integrityPass
Server-assigned and sequential: injected reused, skipped, zero or negative and overflow-sized nonces are all refused (NONCE-001 … NONCE-004); the nonce advances by one per bet (PF-002) and restarts at 0 on rotation (ENTROPY-004)
Seed integrityPass
The active seed is never disclosed before rotation (DISCLOSE-001); 70 / 70 rotations racing live bets revealed the committed seed and never reused one (EXTRA-001); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003)
Outcome determinismPass
Live rounds recompute independently to the same roll and band result (EXTRA-002, PARAM-003); a 2,000-roll live sample passes the uniformity test (FAIR-001)
Round & player isolationPass
13 / 13 foreign and malformed identifiers rejected; post-logout reuse rejected; parallel identical and high-stake bets settled once each and reconciled to the wallet
Payout integrityPass
Injected roll, payout, result, multiplier and win-region fields rejected; the 50,000 USDC potential-payout cap refuses over-limit bets; 200 consecutive rounds reconciled exactly
Overload handlingRemediated
Extreme bursts drew some generic errors instead of structured overload responses (Low); after LIQD’s fix, overload is answered with structured busy and rate-limit responses, and no refused request consumes a nonce (RACE-004)
✓  34 of 34 Pass or Remediated

34 integrity tests: 33 pass and 1 remediated (overload handling — fixed by LIQD and re-verified; accepted nonces stayed unique throughout). No test altered, replayed or injected an outcome. Test procedures and payloads are withheld by policy — see 5.5.

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. Cryptographic verification (S1–S3) shows the game computed every captured roll from its disclosed inputs; fairness-integrity testing asks whether the surface around that computation holds up under non-standard use — seeds swapped, rounds replayed, parameters injected, settlements raced. These tests ran against LIQD’s running service, not against a dataset. This section covers 34 tests across 12 categories. Each card below gives the control assessed, the broad testing approach, the observed result, its impact and any limitation.

3 columns
CategoryTestsWhat It Catches
Race Conditions5Parallel bets racing placement, settlement or seed rotation, and how the service handles extreme bursts
Input Validation5Malformed, wrong-type or out-of-range bands, win chances, stakes, content types and currencies
Atomic Balance / Idempotency3Double debits, orphaned debits and unsafe retry behaviour after a timeout or disconnect
Broken Authorization2Acting on another account, or after the session has ended
Max-win Bypass4Client-supplied outcome, payout or win-region fields, and the stake and potential-payout limits
Nonce Manipulation4Reused, skipped, zero, negative or oversized nonces supplied by the client
RNG / PF Verification4Roll distribution and the multiplier curve, commitment and reveal, and independent recomputation of live rounds
RNG / Information Leak1Disclosure of the active server seed, or of hidden game state, before it is due
Entropy / PF Verification1Server-seed rotation behaviour: fresh seeds, a new commitment and the nonce reset
Precision / Payout1Settlement arithmetic and wallet reconciliation over many rounds
Event Replay1Duplicate or replayed requests settling more or less than once
Adversarial Seeds3Unusual client seeds: length, character sets, Unicode forms and rapid rotation
5.2Severity Framework & Hard Fail Criteria

Two scales apply in this section. The audit framework decides whether a result can pass at all — any test that lets money settle against a tampered or unverifiable outcome is an automatic hard fail. The integrity matrix records each test’s status — Pass, Remediated, Warning or Fail — and each finding is graded Critical, High, Medium or Low. On this page: one Remediated test, graded Low — overload handling under extreme bursts, fixed by LIQD and re-verified.

3 columns
StatusMeaningAction
PassThe service handled the scenario correctly: the control heldNone — recorded with its result
RemediatedA failed control that LIQD has since fixed, re-tested by ProvablyFair.org on the current buildRecorded with the fix and its re-test
WarningA real defect, or a property not yet proven, without a direct effect on funds or fairnessRecorded with its impact; open until a re-test resolves it
FailA defect affecting funds, fairness or securityRemediation and a re-test required
2 columns
ConditionConsequence
A substituted server seed is accepted against a published commitmentHard fail
A revealed seed fails its commitment and play continuesHard fail
Any settled round's roll cannot be reproduced from disclosed inputsHard fail
A credited payout deviates from the quotient multiplier and settlement ruleHard fail
Nonce reuse or gaps that could conceal discarded outcomesHard fail
Hard fail criteria: These conditions are absolute — a single occurrence fails the game regardless of every other result. None of them occurred in testing: no test altered, replayed or injected an outcome, and the one finding never touched the provably fair layer.

34 tests · 33 pass · 1 remediated

Race Conditions5/5
LIQD-DICE-RACE-004LowRemediated

Overload handling under extreme bursts

Control assessed
Overload is answered with structured busy or rate-limit responses — never a generic server error — with no duplicate nonce and no internal detail in error bodies.
Testing approach
Bursts of parallel bets from one account at increasing volume; status codes, error bodies and nonces checked.
Observed result
Before the fix, the largest burst drew a handful of generic server errors, though accepted nonces stayed unique and no error body disclosed internal detail. After LIQD’s fix, re-tested on 23 September 2026: overload is answered with structured busy and rate-limit responses carrying retry guidance; accepted nonces are unique and consecutive, so no refused request consumed one.
Impact
Low · robustness — accepted nonces stayed unique and no error disclosed internal detail. Fixed by LIQD and re-verified.
Limitations
The test checked status codes, error bodies and nonces.
LIQD-DICE-EXTRA-001Pass

Seed rotation racing live bets

Control assessed
A rotation racing bets cannot reveal a seed early or inconsistently, or bring a revealed seed back into use.
Testing approach
Seventy rotations fired against bets in flight; each revealed seed checked against its pre-race commitment.
Observed result
All 70 rotations succeeded; each revealed seed matched its pre-race commitment, and none reappeared as a later active seed.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-RACE-001Pass

Nonce uniqueness under parallel bets

Control assessed
Every accepted bet gets its own server-assigned nonce in sequence, and each roll is bound to that nonce, even under parallel load.
Testing approach
Bursts of parallel bets from one account; nonces and response order, and each roll’s binding to its nonce, compared.
Observed result
All 55 bets received unique nonces with no server error; in a follow-up burst of ten, nonces ascended in arrival order and every roll matched its nonce.
Impact
None — no nonce was shared or skipped.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-RACE-002Pass

Identical bets in parallel, at scale

Control assessed
Each accepted bet settles exactly once, and the balance equals the sum of all settled rounds.
Testing approach
Fifteen identical bets sent in parallel; round IDs and balance reconciled.
Observed result
All 15 settled once, as distinct rounds; the balance change matched their summed outcome exactly.
Impact
None — no lost or duplicated settlement.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-RACE-003Pass

High-stake parallel over-commit

Control assessed
Parallel high-stake bets cannot overdraw the balance, settle twice or leave a phantom round.
Testing approach
Parallel bets whose combined stake exceeds the balance, with a control bet; wallet and bet history reconciled.
Observed result
All 12 settled rounds, the control included, reconciled to the wallet and the bet history: no over-commit, no negative balance, no payout mismatch, no lost or phantom round.
Impact
None — settlement stayed consistent under parallel placement.
Limitations
None specific to this test; the general scope is in 5.5.
Input Validation5/5
LIQD-DICE-PARAM-001Pass

Band limits and strict input types

Control assessed
Win chance stays within the offered 0.01%–98% envelope; invalid bands and wrong-type values are refused.
Testing approach
Valid bands at the limits, out-of-range bands and wrong-type values, each with a control.
Observed result
Valid bands settled; all five out-of-range cases and all three wrong-type cases were rejected. Boundary checks confirmed the 0.01%–98% envelope.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-PARAM-002Pass

Stake precision, zero and invalid types

Control assessed
Invalid stakes are refused, and zero is accepted only as the explicit free-play value.
Testing approach
Minimum-stake and free-play controls, then sub-precision, null, negative, oversized and wrong-type stakes.
Observed result
Both controls settled; every invalid stake was rejected, and no non-zero stake was silently turned into zero.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-PARAM-003Pass

Extreme win chance and band edges

Control assessed
Out-of-range bounds are refused, and every band is half-open: the lower bound wins, the upper bound loses, and inverted bands take the complement.
Testing approach
Bounds just inside and outside the range, and boundary checks of the win rule.
Observed result
The out-of-range bound was rejected and the in-range one accepted; the accepted round verified. Boundary checks confirmed the half-open rule: a 50.00 roll loses on 0–50 and wins on 50–100.
Impact
None — the control held.
Limitations
The upper-bound rule is confirmed by LIQD’s verifier (Section 4); no captured live roll landed exactly on an upper bound.
LIQD-DICE-PARAM-004Pass

Content-type variations around validation

Control assessed
Changing how the request body is encoded cannot let an invalid band settle.
Testing approach
Invalid bands sent under each alternative content type.
Observed result
Both invalid bands were rejected under every content type; no malformed request produced a settled round.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-TOKEN-001Pass

Currency identifier manipulation

Control assessed
A client-supplied currency identifier cannot redirect or alter the stake.
Testing approach
Bets carrying six altered currency identifiers, with a control bet.
Observed result
All six were rejected without a server error; only the control settled, in the expected wallet.
Impact
None — the stake stayed in the account’s own currency.
Limitations
None specific to this test; the general scope is in 5.5.
Atomic Balance / Idempotency3/3
LIQD-DICE-IDEMP-001Pass

Same-key retry after a timeout

Control assessed
A retry with the same key returns the original round — no second debit or credit, and no change of parameters.
Testing approach
Bets sent with very short client timeouts, then retried with the same key; one retry also changed the bet’s parameters.
Observed result
All 20 retries returned their original round; all 11 timed-out bets had settled once, server-side, and the wallet reconciled. The retry with changed parameters still returned the original round, unchanged.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-IDEMP-002Pass

Orphaned-debit check at scale

Control assessed
Every debit belongs to a settled round.
Testing approach
Twenty consecutive bets; the balance reconciled against their outcomes.
Observed result
All 20 settled, and the balance change matched their summed outcome exactly.
Impact
None — no unexplained debit.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-IDEMP-003Pass

Disconnect while a bet is processed

Control assessed
A client disconnect mid-bet leaves no missing or duplicated debit.
Testing approach
Bets abandoned by the client at different moments; rounds and balance reconciled.
Observed result
The abandoned bet still settled once, server-side; all four rounds were accounted for and the balance matched their net outcome.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
Broken Authorization2/2
LIQD-DICE-AUTH-001Pass

Foreign and malformed user identifiers

Control assessed
A user identifier supplied in the request is refused; bets stay bound to the caller’s own session.
Testing approach
Bets sent with foreign and malformed user identifiers in the request, alongside control bets; the caller’s balance reconciled.
Observed result
All 13 injected identifiers were rejected without a server error; the two control bets account for the caller’s entire balance change.
Impact
None — no injected identifier was accepted.
Limitations
Covers the caller’s balance, from one authenticated account.
LIQD-DICE-AUTH-003Pass

Session reuse after logout

Control assessed
A session ends at logout: its cookie cannot place another bet.
Testing approach
A control bet, a logout, then a bet attempted with the old session.
Observed result
The control bet settled and the logout completed; the reused session was rejected and no bet was placed.
Impact
None — the session was invalidated server-side.
Limitations
None specific to this test; the general scope is in 5.5.
Max-win Bypass4/4
LIQD-DICE-INJECT-001Pass

Single outcome field injected

Control assessed
No client-supplied roll, payout, result or multiplier value can control settlement.
Testing approach
Bets each carrying one injected outcome field, with two control bets.
Observed result
All four injected fields were rejected; the two controls account for the entire nonce advance and balance change.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-INJECT-002Pass

Win-region and nested-parameter injection

Control assessed
No injected field changes the winning region or a bet’s effective parameters.
Testing approach
Variants carrying alias and nested parameter fields, with two control bets.
Observed result
All six altered variants were rejected; both controls settled against their declared bands, with matching recorded parameters.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-MAXWIN-001Pass

Potential-payout cap at bet time

Control assessed
A bet whose potential payout exceeds the 50,000 USDC cap is refused when it is placed.
Testing approach
Stakes just inside and just outside the cap at the top multiplier, plus invalid stake values.
Observed result
The stake inside the cap settled and the one just over it was refused; invalid stakes were rejected.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-MAXWIN-002Pass

Several outcome fields injected at once

Control assessed
Outcome fields supplied by the client cannot steer settlement or consume a nonce.
Testing approach
One bet carrying several injected outcome fields together, with a control bet.
Observed result
The combined injection was rejected at validation; only the control consumed a nonce and moved the balance.
Impact
None — settlement stayed server-computed.
Limitations
None specific to this test; the general scope is in 5.5.
Nonce Manipulation4/4
LIQD-DICE-NONCE-001Pass

Reused nonce

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying a reused nonce.
Observed result
Every injected nonce was rejected without a server error; only the two controls consumed nonces or changed the balance.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-NONCE-002Pass

Skipped-ahead nonce

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying a skipped-ahead nonce.
Observed result
Every injected nonce was rejected without a server error; only the two controls consumed nonces or changed the balance.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-NONCE-003Pass

Zero and negative nonces

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying zero and negative nonces.
Observed result
Every injected nonce was rejected without a server error; only the two controls consumed nonces or changed the balance.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-NONCE-004Pass

Overflow-sized nonces

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying overflow-sized nonces.
Observed result
Every injected nonce was rejected without a server error; only the two controls consumed nonces or changed the balance.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
RNG / PF Verification4/4
LIQD-DICE-EXTRA-002Pass

Independent roll and outcome check

Control assessed
Live results can be reproduced independently from the revealed inputs.
Testing approach
Five live rounds recomputed with our own implementation after the reveal.
Observed result
All five rounds matched: the same roll and the same band result. The same derivation reproduces every captured roll and win flag in Sections 1–4.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-FAIR-001Pass

Roll distribution and multiplier curve

Control assessed
Rolls are uniform with no band asymmetry, and the multiplier is 99 / win chance, for 99% RTP before settlement rounding.
Testing approach
A 2,000-roll live sample tested for uniformity and band symmetry; multipliers compared with the formula.
Observed result
The sample passed the uniformity test and showed no band asymmetry; both 50% bands paid 1.98×.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-PF-001Pass

Provably-fair data and commit / reveal

Control assessed
Each bet carries every provably-fair field a player needs, and a revealed seed matches its earlier commitment.
Testing approach
Bets checked field by field, then the seed rotated and revealed.
Observed result
All required fields were present, and the revealed seed matched its commitment.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-PF-002Pass

Nonce sequence within a seed period

Control assessed
The nonce advances by exactly one per bet while the commitment stays unchanged.
Testing approach
Ten consecutive bets on one server seed, then a reveal.
Observed result
The nonce advanced from 0 to 10 with no gap while the commitment stayed unchanged.
Impact
None — the sequence was complete and the commitment fixed.
Limitations
None specific to this test; the general scope is in 5.5.
RNG / Information Leak1/1
LIQD-DICE-DISCLOSE-001Pass

Active server-seed disclosure

Control assessed
The active server seed stays secret until rotation; only then is it revealed.
Testing approach
The responses around a bet checked for the active seed before rotation; a rotation then served as the positive control.
Observed result
No response checked exposed the active seed; after rotation, the revealed seed matched its commitment.
Impact
None — the seed stayed secret until it was due.
Limitations
Covers the seed period tested.
Entropy / PF Verification1/1
LIQD-DICE-ENTROPY-004Pass

Nonce reset after seed rotation

Control assessed
Rotating the seed changes the commitment and restarts the nonce at 0.
Testing approach
Bets, a rotation, then further bets.
Observed result
The nonce reset from 2 to 0, the commitment changed, and both follow-up bets settled normally.
Impact
None — rotation behaved as specified.
Limitations
None specific to this test; the general scope is in 5.5.
Precision / Payout1/1
LIQD-DICE-PREC-001Pass

Payout consistency at scale

Control assessed
Every payout follows the game’s settlement rule, and the wallet reconciles over the run.
Testing approach
Two hundred consecutive rounds; each payout and the aggregate balance change checked.
Observed result
All 200 rounds settled with no payout mismatch; the balance change matched the summed outcome exactly.
Impact
None — settlement arithmetic held.
Limitations
Rounding versus flooring is settled by the captured credits checked in Sections 3–4, which match the rounding rule.
Event Replay1/1
LIQD-DICE-REPLAY-001Pass

Duplicate identical bets sent together

Control assessed
Two identical requests sent at the same moment settle as two distinct rounds — none lost, none settled twice.
Testing approach
A pair of identical bet requests sent in parallel; rounds and balance reconciled.
Observed result
Both requests settled as distinct rounds, and the balance moved by exactly their net outcome.
Impact
None — each request settled exactly once.
Limitations
None specific to this test; the general scope is in 5.5.
Adversarial Seeds3/3
LIQD-DICE-SEED-001Pass

Client-seed length and character sets

Control assessed
Client seeds have a consistent length limit, and no accepted seed disrupts the game.
Testing approach
Client seeds of increasing length and unusual character sets, each followed by a bet.
Observed result
Seeds up to 255 characters were accepted and every longer seed rejected; betting worked after every accepted change.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-SEED-002Pass

Unicode forms of one seed

Control assessed
Composed and decomposed Unicode forms of the same seed are both usable and handled consistently.
Testing approach
The same visible seed submitted in both forms, each followed by a bet.
Observed result
Both forms were accepted, each kept as entered, and the following bets settled.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-DICE-SEED-003Pass

Rapid seed rotation

Control assessed
Each bet uses the seed set by the rotation immediately before it.
Testing approach
Back-to-back rotations, each followed by a bet.
Observed result
Every bet used the seed set just before it; no stale or mixed-up seed appeared.
Impact
None — each bet bound to the current seed.
Limitations
None specific to this test; the general scope is in 5.5.
Technical Evidence & Verification4 sections
5.3Coverage Summary12 categories

Coverage by category — 34 tests: 33 pass, 1 remediated.

3 columns
CategoryTestsResult
Race Conditions54 pass · 1 remediated (RACE-004)
Input Validation5All pass
Atomic Balance / Idempotency3All pass
Broken Authorization2All pass
Max-win Bypass4All pass
Nonce Manipulation4All pass
RNG / PF Verification4All pass
RNG / Information Leak1All pass
Entropy / PF Verification1All pass
Precision / Payout1All pass
Event Replay1All pass
Adversarial Seeds3All pass
Method: Every test ran against LIQD’s running service on its pre-launch environment; remediations were re-tested on LIQD’s current build. Sections 1–4 verify the captured rounds offline — independent derivation, commitment and chain checks, nonce continuity and credit arithmetic — while Section 5 tests the live service around them. Per-test procedures, payloads and captures are retained under the engagement record and are not published — see 5.5.
5.4Findings & Remediation1 record

1 finding from 1 test, remediated and re-verified. Each test’s card in the matrix above gives its control, approach, result, impact and limitations.

3 columns
TestsFindingStatus
RACE-004Extreme bursts drew some generic server errors instead of structured overload responses. Fixed by LIQD and re-verified.Remediated · Low
5.5Scope & LimitationsScope note
What is published here is coverage, not method. Each test in this section appears with its identifier and category, the control it assessed, the broad testing approach, the observed result, its impact and any limitation; a finding also carries its severity, where graded, and its remediation state. Request payloads, preconditions, step sequences, endpoints, timings and response captures are withheld: they show how to repeat a test, and add nothing a reader needs to judge its result. The full record is retained and is available to LIQD and to regulators on request.
Coverage published, method withheld: The classes of issue tested are standard and publicly documented. What would help someone repeat a test — volumes, timings, settings and request sequences — stays in the engagement record.
Point-in-time, per build: Each result is scoped to the build tested; the overload fix was re-tested on 23 September 2026.
Scope: The tests target the provably fair surface and the money path around it — seeds, rounds, placement and settlement. Wallet custody, account management and infrastructure hardening are outside their scope.
Test environment: Every test ran against LIQD’s pre-launch environment (qa.liqd.com), not the public site.
5.6Evidence & Disclosure

Live-surface record: Section 5 tests LIQD’s running service, so unlike Sections 1–4 it does not reproduce from the audit repository. The published record is the test cards above.

Retained under the engagement record: Per-test procedures, request and response captures, re-test captures, run timestamps and build identifiers — available to LIQD and to regulators on request.

Section 5 coverage · published on this page
Integrity tests ....................... 34Categories ............................ 12Pass .................................. 33Remediated ............................ 1Warning (open) ........................ 0Findings .............................. 1 · 1 remediated, 0 openProcedures and payloads ............... withheld (5.5)
Why the runner is not published: The tests run against LIQD’s real-money service, here its pre-launch environment. A published runner, with its payloads, would let anyone repeat them against LIQD, or against another operator built the same way; coverage and results are published instead.
06

Player Verification

Can a player verify their own bets without trusting anyone?

Every Dice outcome can be independently reproduced using publicly disclosed inputs. No hidden variables, no private backend data. If the revealed seed matches the hash you saw before betting and your computed roll matches the game result, the bet was provably fair. This section walks you through the process — with LIQD’s own panel, with the independent ProvablyFair.org verifier, or with a few lines of code you run yourself.

Independent Result Verification

4 Stepsto verify any bet

Key principles

  • Every Dice outcome can be independently reproduced
  • No hidden variables — no private backend data
  • If the revealed seed matches the hash you saw before betting and your computed roll matches the game, the bet was provably fair
  • Most players can verify directly through LIQD's Provably Fair panel

What you need

  • Server Seed — revealed after seed rotation (casino entropy)
  • Client Seed — your player-controlled seed
  • Nonce — the bet number in sequence (ensures uniqueness)
  • Band — the mode and limits you played (from / to, under · over · inside · outside)
4 Stepsto verify any bet
  • Every Dice outcome can be independently reproduced
  • No hidden variables — no private backend data
  • If the revealed seed matches the hash you saw before betting and your computed roll matches the game, the bet was provably fair
  • Most players can verify directly through LIQD's Provably Fair panel
From Bet to Independent Verification — 4-step flow — illustrated flow
Server SeedRevealed after rotation
Client SeedPlayer-controlled input
NonceRecorded bet sequence
Bandthe mode and limits you played (from / to, under · over · inside · outside)
  1. 1

    Place a Dice Bet

    Pick a mode and set your band on the slider, then roll a number from 0.00 to 99.99. The number is computed from the seeds before your band is applied — the mode, the limits and the stake decide the odds and the price, never what is rolled.

  2. 2

    Open the Provably Fair Panel and Rotate the Seed

    Open the Provably Fair panel on the game page. The Seeds tab shows the active client seed, the hashed server seed (the commitment) and your bet count under the pair. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saw before betting — across this audit’s 134 epochs it always did.

  3. 3

    Enter the Seeds in the Verify Tab

    The Verify tab recomputes any roll from a server seed, client seed, nonce and band. Enter the revealed server seed, your client seed, the bet’s nonce and the band you played — the mode, From and To.

  4. 4

    Recompute & Confirm

    Press Verify — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed, the recomputed roll matches your bet and the half-open band gives the same win or loss, the bet was provably fair. The ProvablyFair.org verifier checks the commitment, roll, win and multiplier; it takes no stake, so check the credit with the scripts under Manual Verification.

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

Pick a mode with the game-mode switch, set your band on the slider and roll. The platform computes the roll from (serverSeed, clientSeed, nonce) before the band is applied — the bet below rolls under 49.50, a 49.50% win chance at 2.00×.

LIQD Dice on liqd.com — a $1.00 roll under 49.50 (2.00×, 49.50% win chance) that came up 13.17, a win paying $2.00; the 58.61 before it was the pair’s first roll (nonce 0).

LIQD Dice on liqd.com — a $1.00 roll under 49.50 (2.00×, 49.50% win chance) that came up 13.17, a win paying $2.00; the 58.61 before it was the pair’s first roll (nonce 0).

6.2Step 2: Open the Provably Fair Panel and Rotate the Seed

The panel has two tabs, Seeds and Verify. The Seeds tab holds your active pair and its commitment, with the next server-seed hash already published underneath it. Rotation ends the epoch: the outgoing server seed is revealed for verification, and the pre-committed next pair is promoted to active. That pre-commitment is the chain the audit’s Step 2 checks across all 133 links, plus the pre-capture link into epoch 0.

Active Server Seed (Hashed) — the commitment for the current epoch; the plaintext is revealed when you rotate and must hash to this value.

Active Client Seed — your player-controlled entropy input, editable via the Next Client Seed field before rotation.

Total bets made with pair — the bet counter under the active pair, which locates each round's nonce within the epoch.

The Seeds tab before rotating — Active Server Seed (Hashed) bf7c353e…14d717 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, c18398a8…8ec7ab, hashes to bf7c353e…14d717.

The Seeds tab before rotating — Active Server Seed (Hashed) bf7c353e…14d717 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, c18398a8…8ec7ab, hashes to bf7c353e…14d717.

6.3Step 3: Enter the Seeds in the Verify Tab

The Verify tab takes the four inputs — client seed, server seed, nonce, and the band (mode plus from / to) — and recomputes a roll on demand. Enter the server seed revealed in step 2, your client seed, the bet’s nonce and the band you played. Everything this audit checks cryptographically, the panel lets you check by hand.

The Verify tab filled in for that roll — client seed, the server seed revealed in step 2, nonce 1 and the Under band from 0.00 to 49.50.

The Verify tab filled in for that roll — client seed, the server seed revealed in step 2, nonce 1 and the Under band from 0.00 to 49.50.

6.4Step 4: Recompute & Confirm

Press Verify. The tab recomputes the roll from the three seed inputs and applies the half-open rule to your band — so you can check both the number rolled and the win or loss it produced. A match shows the roll is the one the committed seed produces. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python, pre-loaded with the featured bet from this report — and they also check the credit.

The result: “Rolled 13.17: win at 2.00x”. Our scripts in 6.8–6.9 reproduce it exactly: roll 13.17, a win under 49.50, $2.00 credited on the $1.00 stake.

The result: “Rolled 13.17: win at 2.00x”. Our scripts in 6.8–6.9 reproduce it exactly: roll 13.17, a win under 49.50, $2.00 credited on the $1.00 stake.

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

Hosted verifiers — including ours — are conveniences. The guarantee only becomes unconditional when you can reproduce a round with tools nobody else controls. The algorithm below is small enough to audit by eye and run anywhere: if the revealed seed matches the hash you saw before betting and your recomputation matches the round you played, no one in the chain — casino, auditor, or tool host — changed it after you bet.

6.6How the Algorithm Works (Plain English)

Four moving parts, in the order they act:

  • Commit — before you play, LIQD publishes SHA-256 of the server seed (hashed as the 32-character hex string). The seed itself stays secret until rotation.
  • Draw — one call to HMAC-SHA256(key = decoded seed, message = clientSeed:nonce:0); the first 4-byte chunk below 4,294,960,000 is taken modulo 10,000 and divided by 100 — that is the roll, 0.00 to 99.99.
  • Judge — the roll wins if it lies in [lower, upper) — the lower bound counts, the upper does not — or, for outside, if it does not.
  • Pay — a win credits stake × 9900 / b where b is the winning width in basis points, rounded in two stages at the 8th decimal (multiplier first, then the product half-even).

Note the two hashing domains: the commitment hashes the hex string as UTF-8 text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game — both scripts below get it right, and both print the featured bet’s expected output in their comments so you can self-check the tool before trusting it. The other classic slip is the boundary: use < on the upper bound, not <=, or a roll exactly on it will be judged a win the game never pays.

6.7Casino Verifier vs ProvablyFair.org Verifier4 records

Both should agree — the point of having an independent one is what happens if they ever don't:

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureIndirectly — its verify endpoint agrees with our implementation on eight recorded vectors and the E13 boundary probesYes — its dice module, run from source, reproduces all 6,700 captured draws (HMAC-SHA256 draw, half-open band, 9900 / b)
If they disagreeSuspect the casino’s tool or a tampered roundThe repo + scripts are the tiebreaker anyone can run
6.8JavaScript Verification Script

Complete, dependency-free, and pre-loaded with the featured bet as its default inputs — run it as-is to see a full match, then swap in your own round:

verify-dice.js · Standalone Node.js
// verify-dice.js — standalone LIQD Dice verifier (Node.js, no dependencies)// Usage: node verify-dice.js <serverSeed> <clientSeed> <nonce> <lower> <upper> <inverted> [stake]const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, lowerArg, upperArg, invArg, stakeArg] = argv.length  ? argv  : ['015307d5a3f62db476210c3797bac8b7', 'auditf2456fc140ac', '0', '0', '92.37', 'false', '0.10'];const nonce = Number(nonceArg), lower = Number(lowerArg), upper = Number(upperArg);const inverted = invArg === 'true', stake = Number(stakeArg ?? '0.10'); // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — what the panel showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One bias-free draw — HMAC keyed on the DECODED bytes, message clientSeed:nonce:0function draw() {  const digest = createHmac('sha256', Buffer.from(serverSeed, 'hex'))    .update(`${clientSeed}:${nonce}:0`).digest();  const maxFair = Math.floor(0x1_0000_0000 / 10000) * 10000;   // 4,294,960,000  for (let o = 0; o + 4 <= digest.length; o += 4) {    const chunk = digest.readUInt32BE(o);    if (chunk < maxFair) return chunk % 10000;  }  throw new Error('all eight chunks rejected (not specified; about 7e-47 per bet)');}const roll = draw() / 100;console.log('roll       =', roll.toFixed(2)); // 3) Half-open win rule: the lower bound is in, the upper bound is out; outside = exact complementconst inBand = roll >= lower && roll < upper;const win = inverted ? !inBand : inBand;console.log('win        =', win); // 4) Quotient multiplier 9900 / b (b = winning width in basis points), then the two-stage 8-dp settlementconst width = inverted ? 100 - (upper - lower) : upper - lower;const b = Math.round(width * 100);const multiplier = 9900 / b;const multUnits = BigInt(Math.round(multiplier * 1e8));            // stage 1: round8(multiplier)const product = BigInt(Math.round(stake * 1e8)) * multUnits;       // 1e-16 unitslet credit = product / 100000000n; const r = product % 100000000n;if (r > 50000000n || (r === 50000000n && credit % 2n === 1n)) credit += 1n;   // stage 2: half-evenconsole.log('multiplier =', multiplier, '| credit =', win ? (Number(credit) / 1e8).toFixed(8) : '0.00000000');// Expected for the defaults (bet kzLjSr11zq5GvEOHlj9n6, epoch 0 nonce 0, under 92.37 at $0.10)://   commitment = 918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8//   roll       = 24.01//   win        = true//   multiplier = 1.071776550828191 | credit = 0.10717766
6.9Python Verification Script

The same verifier in Python 3, standard library only — the settlement is done in exact rational arithmetic, so it doubles as an independent check on the JavaScript version:

verify-dice.py · Standalone Python
# verify-dice.py — standalone LIQD Dice verifier (Python 3, stdlib only)import hashlib, hmac, struct, sysfrom fractions import Fraction args = sys.argv[1:] or ['015307d5a3f62db476210c3797bac8b7', 'auditf2456fc140ac', '0', '0', '92.37', 'false', '0.10']server_seed, client_seed, nonce = args[0], args[1], int(args[2])lower, upper, inverted = Fraction(args[3]), Fraction(args[4]), args[5] == 'true'stake = Fraction(args[6] if len(args) > 6 else '0.10') # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One bias-free draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:0def draw():    digest = hmac.new(bytes.fromhex(server_seed),                      f'{client_seed}:{nonce}:0'.encode(), hashlib.sha256).digest()    for chunk in struct.unpack('>8I', digest):        if chunk < 4294960000:          # maxFair = floor(2**32 / 10000) * 10000            return chunk % 10000    raise ValueError('all eight chunks rejected (not specified; about 7e-47 per bet)') roll = Fraction(draw(), 100)print('roll       = %.2f' % roll) # 3) Half-open win rule — lower bound in, upper bound out; outside = exact complementin_band = lower <= roll < upperwin = (not in_band) if inverted else in_bandprint('win        =', win) # 4) Quotient multiplier 9900 / b, then the two-stage 8-dp settlement in exact rational arithmeticwidth = 100 - (upper - lower) if inverted else upper - lowerb = int(width * 100)                                    # basis points (exact for 2-dp bounds)multiplier = Fraction(9900, b)mult_units = (2 * multiplier * 10**8 + 1) // 2         # stage 1: round half-up to 8 dpproduct = stake * mult_units                            # in 1e-8 units of creditq, r = divmod(product.numerator, product.denominator)if 2 * r > product.denominator or (2 * r == product.denominator and q % 2 == 1):    q += 1                                              # stage 2: round half-evencredit = Fraction(q, 10**8) if win else Fraction(0)print('multiplier = %r | credit = %.8f' % (float(multiplier), float(credit)))# Expected for the defaults (bet kzLjSr11zq5GvEOHlj9n6, epoch 0 nonce 0, under 92.37 at $0.10):#   commitment = 918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8#   roll       = 24.01#   win        = True#   multiplier = 1.071776550828191 | credit = 0.10717766
6.10Evidence Screenshots & Records6 records

The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.

2 columns
FileShows
E10-dice-game-ui.pngThe Dice interface — 0–100 slider, game-mode switches, multiplier and win-chance fields, bet controls
E11-provably-fair-seeds-panel.pngThe Seeds tab — active client seed, hashed server seed, bet counter, and the pre-committed next pair
E12-provably-fair-verify-tab.pngThe Verify tab — client seed / server seed / nonce / band inputs and the VERIFY action (default form values, not a captured bet)
E13-verify-endpoint-boundary-probes.jsonSeven recorded probes of LIQD’s verifier endpoint — five with the roll exactly on a band bound, two just past a bound as controls — plus a settled-bet cross-check — the evidence for the half-open rule (S4.7)
E14-maxodds-live-record.jsonThe settings pull, UI clamp observation and one losing 9900× bet at the odds ceiling (S4.7) — a session record
rtp-page-labels-2026-10-01.jsonThe RTP and house-edge figures on LIQD’s Dice game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2)
From Bet to Independent Verification — 4-step flow — illustrated flow
How it works
  1. 1

    Place a Dice Bet

    Pick a mode and set your band on the slider, then roll a number from 0.00 to 99.99. The number is computed from the seeds before your band is applied — the mode, the limits and the stake decide the odds and the price, never what is rolled.

  2. 2

    Open the Provably Fair Panel and Rotate the Seed

    Open the Provably Fair panel on the game page. The Seeds tab shows the active client seed, the hashed server seed (the commitment) and your bet count under the pair. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saw before betting — across this audit’s 134 epochs it always did.

  3. 3

    Enter the Seeds in the Verify Tab

    The Verify tab recomputes any roll from a server seed, client seed, nonce and band. Enter the revealed server seed, your client seed, the bet’s nonce and the band you played — the mode, From and To.

  4. 4

    Recompute & Confirm

    Press Verify — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed, the recomputed roll matches your bet and the half-open band gives the same win or loss, the bet was provably fair. The ProvablyFair.org verifier checks the commitment, roll, win and multiplier; it takes no stake, so check the credit with the scripts under Manual Verification.

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

Pick a mode with the game-mode switch, set your band on the slider and roll. The platform computes the roll from (serverSeed, clientSeed, nonce) before the band is applied — the bet below rolls under 49.50, a 49.50% win chance at 2.00×.

LIQD Dice on liqd.com — a $1.00 roll under 49.50 (2.00×, 49.50% win chance) that came up 13.17, a win paying $2.00; the 58.61 before it was the pair’s first roll (nonce 0).

LIQD Dice on liqd.com — a $1.00 roll under 49.50 (2.00×, 49.50% win chance) that came up 13.17, a win paying $2.00; the 58.61 before it was the pair’s first roll (nonce 0).

6.2Step 2: Open the Provably Fair Panel and Rotate the Seed

The panel has two tabs, Seeds and Verify. The Seeds tab holds your active pair and its commitment, with the next server-seed hash already published underneath it. Rotation ends the epoch: the outgoing server seed is revealed for verification, and the pre-committed next pair is promoted to active. That pre-commitment is the chain the audit’s Step 2 checks across all 133 links, plus the pre-capture link into epoch 0.

Active Server Seed (Hashed) — the commitment for the current epoch; the plaintext is revealed when you rotate and must hash to this value.

Active Client Seed — your player-controlled entropy input, editable via the Next Client Seed field before rotation.

Total bets made with pair — the bet counter under the active pair, which locates each round's nonce within the epoch.

The Seeds tab before rotating — Active Server Seed (Hashed) bf7c353e…14d717 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, c18398a8…8ec7ab, hashes to bf7c353e…14d717.

The Seeds tab before rotating — Active Server Seed (Hashed) bf7c353e…14d717 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, c18398a8…8ec7ab, hashes to bf7c353e…14d717.

6.3Step 3: Enter the Seeds in the Verify Tab

The Verify tab takes the four inputs — client seed, server seed, nonce, and the band (mode plus from / to) — and recomputes a roll on demand. Enter the server seed revealed in step 2, your client seed, the bet’s nonce and the band you played. Everything this audit checks cryptographically, the panel lets you check by hand.

The Verify tab filled in for that roll — client seed, the server seed revealed in step 2, nonce 1 and the Under band from 0.00 to 49.50.

The Verify tab filled in for that roll — client seed, the server seed revealed in step 2, nonce 1 and the Under band from 0.00 to 49.50.

6.4Step 4: Recompute & Confirm

Press Verify. The tab recomputes the roll from the three seed inputs and applies the half-open rule to your band — so you can check both the number rolled and the win or loss it produced. A match shows the roll is the one the committed seed produces. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python, pre-loaded with the featured bet from this report — and they also check the credit.

The result: “Rolled 13.17: win at 2.00x”. Our scripts in 6.8–6.9 reproduce it exactly: roll 13.17, a win under 49.50, $2.00 credited on the $1.00 stake.

The result: “Rolled 13.17: win at 2.00x”. Our scripts in 6.8–6.9 reproduce it exactly: roll 13.17, a win under 49.50, $2.00 credited on the $1.00 stake.

Technical evidence
6.5Why Manual Verification Matters

Hosted verifiers — including ours — are conveniences. The guarantee only becomes unconditional when you can reproduce a round with tools nobody else controls. The algorithm below is small enough to audit by eye and run anywhere: if the revealed seed matches the hash you saw before betting and your recomputation matches the round you played, no one in the chain — casino, auditor, or tool host — changed it after you bet.

6.6How the Algorithm Works (Plain English)

Four moving parts, in the order they act:

  • Commit — before you play, LIQD publishes SHA-256 of the server seed (hashed as the 32-character hex string). The seed itself stays secret until rotation.
  • Draw — one call to HMAC-SHA256(key = decoded seed, message = clientSeed:nonce:0); the first 4-byte chunk below 4,294,960,000 is taken modulo 10,000 and divided by 100 — that is the roll, 0.00 to 99.99.
  • Judge — the roll wins if it lies in [lower, upper) — the lower bound counts, the upper does not — or, for outside, if it does not.
  • Pay — a win credits stake × 9900 / b where b is the winning width in basis points, rounded in two stages at the 8th decimal (multiplier first, then the product half-even).

Note the two hashing domains: the commitment hashes the hex string as UTF-8 text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game — both scripts below get it right, and both print the featured bet’s expected output in their comments so you can self-check the tool before trusting it. The other classic slip is the boundary: use < on the upper bound, not <=, or a roll exactly on it will be judged a win the game never pays.

6.7Casino Verifier vs ProvablyFair.org Verifier4 records

Both should agree — the point of having an independent one is what happens if they ever don't:

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureIndirectly — its verify endpoint agrees with our implementation on eight recorded vectors and the E13 boundary probesYes — its dice module, run from source, reproduces all 6,700 captured draws (HMAC-SHA256 draw, half-open band, 9900 / b)
If they disagreeSuspect the casino’s tool or a tampered roundThe repo + scripts are the tiebreaker anyone can run
6.8JavaScript Verification Script

Complete, dependency-free, and pre-loaded with the featured bet as its default inputs — run it as-is to see a full match, then swap in your own round:

verify-dice.js · Standalone Node.js
// verify-dice.js — standalone LIQD Dice verifier (Node.js, no dependencies)// Usage: node verify-dice.js <serverSeed> <clientSeed> <nonce> <lower> <upper> <inverted> [stake]const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, lowerArg, upperArg, invArg, stakeArg] = argv.length  ? argv  : ['015307d5a3f62db476210c3797bac8b7', 'auditf2456fc140ac', '0', '0', '92.37', 'false', '0.10'];const nonce = Number(nonceArg), lower = Number(lowerArg), upper = Number(upperArg);const inverted = invArg === 'true', stake = Number(stakeArg ?? '0.10'); // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — what the panel showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One bias-free draw — HMAC keyed on the DECODED bytes, message clientSeed:nonce:0function draw() {  const digest = createHmac('sha256', Buffer.from(serverSeed, 'hex'))    .update(`${clientSeed}:${nonce}:0`).digest();  const maxFair = Math.floor(0x1_0000_0000 / 10000) * 10000;   // 4,294,960,000  for (let o = 0; o + 4 <= digest.length; o += 4) {    const chunk = digest.readUInt32BE(o);    if (chunk < maxFair) return chunk % 10000;  }  throw new Error('all eight chunks rejected (not specified; about 7e-47 per bet)');}const roll = draw() / 100;console.log('roll       =', roll.toFixed(2)); // 3) Half-open win rule: the lower bound is in, the upper bound is out; outside = exact complementconst inBand = roll >= lower && roll < upper;const win = inverted ? !inBand : inBand;console.log('win        =', win); // 4) Quotient multiplier 9900 / b (b = winning width in basis points), then the two-stage 8-dp settlementconst width = inverted ? 100 - (upper - lower) : upper - lower;const b = Math.round(width * 100);const multiplier = 9900 / b;const multUnits = BigInt(Math.round(multiplier * 1e8));            // stage 1: round8(multiplier)const product = BigInt(Math.round(stake * 1e8)) * multUnits;       // 1e-16 unitslet credit = product / 100000000n; const r = product % 100000000n;if (r > 50000000n || (r === 50000000n && credit % 2n === 1n)) credit += 1n;   // stage 2: half-evenconsole.log('multiplier =', multiplier, '| credit =', win ? (Number(credit) / 1e8).toFixed(8) : '0.00000000');// Expected for the defaults (bet kzLjSr11zq5GvEOHlj9n6, epoch 0 nonce 0, under 92.37 at $0.10)://   commitment = 918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8//   roll       = 24.01//   win        = true//   multiplier = 1.071776550828191 | credit = 0.10717766
6.9Python Verification Script

The same verifier in Python 3, standard library only — the settlement is done in exact rational arithmetic, so it doubles as an independent check on the JavaScript version:

verify-dice.py · Standalone Python
# verify-dice.py — standalone LIQD Dice verifier (Python 3, stdlib only)import hashlib, hmac, struct, sysfrom fractions import Fraction args = sys.argv[1:] or ['015307d5a3f62db476210c3797bac8b7', 'auditf2456fc140ac', '0', '0', '92.37', 'false', '0.10']server_seed, client_seed, nonce = args[0], args[1], int(args[2])lower, upper, inverted = Fraction(args[3]), Fraction(args[4]), args[5] == 'true'stake = Fraction(args[6] if len(args) > 6 else '0.10') # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One bias-free draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:0def draw():    digest = hmac.new(bytes.fromhex(server_seed),                      f'{client_seed}:{nonce}:0'.encode(), hashlib.sha256).digest()    for chunk in struct.unpack('>8I', digest):        if chunk < 4294960000:          # maxFair = floor(2**32 / 10000) * 10000            return chunk % 10000    raise ValueError('all eight chunks rejected (not specified; about 7e-47 per bet)') roll = Fraction(draw(), 100)print('roll       = %.2f' % roll) # 3) Half-open win rule — lower bound in, upper bound out; outside = exact complementin_band = lower <= roll < upperwin = (not in_band) if inverted else in_bandprint('win        =', win) # 4) Quotient multiplier 9900 / b, then the two-stage 8-dp settlement in exact rational arithmeticwidth = 100 - (upper - lower) if inverted else upper - lowerb = int(width * 100)                                    # basis points (exact for 2-dp bounds)multiplier = Fraction(9900, b)mult_units = (2 * multiplier * 10**8 + 1) // 2         # stage 1: round half-up to 8 dpproduct = stake * mult_units                            # in 1e-8 units of creditq, r = divmod(product.numerator, product.denominator)if 2 * r > product.denominator or (2 * r == product.denominator and q % 2 == 1):    q += 1                                              # stage 2: round half-evencredit = Fraction(q, 10**8) if win else Fraction(0)print('multiplier = %r | credit = %.8f' % (float(multiplier), float(credit)))# Expected for the defaults (bet kzLjSr11zq5GvEOHlj9n6, epoch 0 nonce 0, under 92.37 at $0.10):#   commitment = 918c2a302c7219c91cf31ce3619acb0ac820b9b7760e8ea3889787bfcb5d14d8#   roll       = 24.01#   win        = True#   multiplier = 1.071776550828191 | credit = 0.10717766
6.10Evidence Screenshots & Records6 records

The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.

2 columns
FileShows
E10-dice-game-ui.pngThe Dice interface — 0–100 slider, game-mode switches, multiplier and win-chance fields, bet controls
E11-provably-fair-seeds-panel.pngThe Seeds tab — active client seed, hashed server seed, bet counter, and the pre-committed next pair
E12-provably-fair-verify-tab.pngThe Verify tab — client seed / server seed / nonce / band inputs and the VERIFY action (default form values, not a captured bet)
E13-verify-endpoint-boundary-probes.jsonSeven recorded probes of LIQD’s verifier endpoint — five with the roll exactly on a band bound, two just past a bound as controls — plus a settled-bet cross-check — the evidence for the half-open rule (S4.7)
E14-maxodds-live-record.jsonThe settings pull, UI clamp observation and one losing 9900× bet at the odds ceiling (S4.7) — a session record
rtp-page-labels-2026-10-01.jsonThe RTP and house-edge figures on LIQD’s Dice game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2)
✓  Any Player Can Reproduce Dice Results

Only disclosed inputs are used. Identical inputs always produce identical output.

07

Reproducibility & Artifacts

Can anyone independently reproduce every scored check in this audit?

This section consolidates the open-source repository, datasets, output artifacts, and reproducibility posture of the audit. Every scored step, every statistic and every pass/fail result of the audit package 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

v1.1.0release audited

Repository Details

Prerequisites

  • Node.js 22.x (validated on 22.23.1; other majors are refused) — no dependency install for node test.js && node verify.js
  • Git
  • TypeScript toolchain via npm ci (TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.7.6) — for npm test and the source commands
  • Python 3 — only for the optional standalone checker in S6
v1.1.0release audited
github.com/ProvablyFair-org/liqd-dice

Commands to Reproduce

The 6,700-bet dataset and the committed results ship inside the repo — no separate download, and the dataset’s SHA-256 is checked before anything runs. npm test runs the 123 unit tests, the 22-step verification and the release-manifest check; node test.js && node verify.js runs the same unit suite and verification with nothing but Node.js 22. Node.js 22.x is required; other majors are refused before any step runs.

sh
git clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dicegit checkout v1.1.0npm ci # Run the entire audit end-to-end (123 unit tests + 22-step verification + release-manifest check)npm test # Or the dependency-free pathnode test.js && node verify.js
Expected output
  123 passing (…)…  Passed:     22/22  Hard fails: 0  Flags:      0   VERDICT: PROVABLY FAIR — Full Pass…  Reproduction vs the committed artifacts on Node 22.22.0: REPRODUCED  [release-manifest] OK: release bar met; 6 artifact hashes match disk, verdict "PROVABLY FAIR — Full Pass", scored 22/22

Audit Reproducibility Pinning

Framework
ProvablyFair.org Audit Framework 1.1 · Dice profile 1.0.2 · RTP-CLAIM 1.2.0
Release Signature
Release commit and tag signed with the ProvablyFair.org release key (SSH, SHA256:gEujiJm2pRyq7lQsfYQfQ1r97H/J7GdaKHbOW8GtikY), recorded in RELEASE-MANIFEST.json
Node Version
22.x required (validated on 22.23.1; other majors refused)
Primary Dataset
data/dice-master-6700bets.json (6,700 bets, 134 seed epochs)
Primary Dataset Hash (SHA-256)
ca1a181a9b4c89a1823e15fe71603b7a5e2267382c134873d80cba536d3aef34
Simulation Artifact Hash (SHA-256)
55873640715d1f9e38d86e10227791337c5664dca094068b2d8d148fdf9ccd97
Convergence Chart Hash (SHA-256)
6b7b6bb7c474a49b48effbd31dd69ca952e4af5ee451902dbaff4a745743e02c
RTP Claim Table Hash (SHA-256)
075811c734cfbf4bdf1b374d8aac719f823caad23a86a3e30b73cb02b056ee06
Derived Figures
outputs/report-figures.json — checked against a fresh recomputation from the pinned dataset on every run (Step 19), not against a fixed hash
Reproduction Gate
Every run writes only to outputs/run/ and is compared field by field with the committed results (outputs/run/diff.json) — integers exactly, p-values within 1e-14 + 1e-12 relative; anything beyond tolerance exits 1
Exit Codes
0 Full Pass · 2 Conditional Pass · 5 RTP FIGURE OVERSTATED · 1 hard fail or reproduction beyond tolerance
Mutation Battery
59 entries — 49 mutations, 8 forgery scripts, 2 declared survivors: 56 killed, 1 informational signal, 2 survivors as declared, 0 not as declared
Runtime
npm test ≈ 11 s · node verify.js ≈ 5 s · npm run test:full ≈ 5 min · npm run mutate ≈ 8 min (Node 22.22.0)
Capture Environment
Pre-launch host qa.liqd.com · one authenticated account
Audit Date
August–September 2026 (dataset captured 27 August 2026; verifier boundary probes 31 August and odds-acceptance probe 1 September 2026)
Audit ID
PF-2026-LQ04
Output Artifacts5 files committed
3 columns
FileContentsSize
verification-results.jsonSteps 1–22 results — per-step pass states with evidence detail strings, profile coverage (DICE-01 … DICE-30), dataset + simulation + chart hashes, info items, verdict~45 KB
report-figures.jsonDerived figures — settled-RTP span, settlement residual counts, boundary hit counts, cherry-pick test figures, Phase D exposure, live aggregates — re-derived field-for-field by Step 19~1.5 KB
simulation-results.jsonPass 1 summaries (uniformity χ², serial screens, nine effective-edge rows, the seven-point under-50 series, 98 target rows with their marks and the 98-target mean series) + Pass 2 (served-band return per epoch, exact-null rows for 134 epochs)~245 KB
rtp-claim-table.jsonThe RTP claim rule’s table — LIQD’s published 99% against all 9,800 settings, each with exact RTP, gap and outcome~1.3 MB
rtp-convergence.htmlConvergence chart rendered from the saved series (self-contained SVG/HTML — the same data as the chart on this page)~2.3 KB
Repository Structure
liqd-dice/
├── src/
│   ├── rng.ts                  → commitHash · HMAC draw (generateProvablyFairNumber) · diceRoll — written from docs/ALGORITHM.md
│   ├── bias.ts                 → the draw-bias bound, from the same maxFairFor the draw applies
│   ├── config.ts               → game model (range 10000 · half-open win rule · 9900/b quotient · two-stage settlement) + population constants + artifact pins
│   ├── rtp-claim.ts · rtp-claim-switch.ts → the RTP claim rule (RTP-CLAIM 1.2.0) and its enforcement switch (on)
│   ├── rtp-settings.ts         → the 9,800-setting exact RTP table and LIQD’s published claim
│   ├── profile-map.ts          → which Dice profile check (DICE-01 … DICE-30) each step implements
│   ├── loader.ts               → dataset loader + SHA-256 guard (aborts on mismatch)
│   ├── simulate.ts             → Pass 1 (uniformity · serial · 9 edge bands · 20M under-50 run · 98 over-mode targets) + Pass 2, from the pinned master seed → outputs/
│   ├── chart.ts                → renders outputs/rtp-convergence.html from the saved series
│   ├── exact-chi2.ts           → exact discrete null for the Pass 2 screen (181,274 partitions · 832 support values)
│   ├── sim-checks.ts           → integer win-count recovery, convergence validation, the seven scored screens and their nominal levels
│   ├── stats.ts                → chi-squared (exact incomplete gamma), lag-1, runs test, AS 241 quantile, exact binomial tail
│   ├── report-figures.ts       → outputs/report-figures.json — every quoted derived figure, reconciled by Step 19
│   ├── domains.ts              → value domains for every numeric field of the committed artifacts (Step 19)
│   ├── diff.ts                 → field-level diff of a run vs the committed artifacts (outputs/run/diff.json)
│   ├── runtime.ts              → Node 22.x guard (validated on 22.23.1)
│   └── types.ts                → dataset record types (liqd-dice-capture-v1)
├── docs/
│   ├── ALGORITHM.md            → the specification the RNG is written from
│   └── INTERFACE.md            → the module interface the RNG implements
├── tests/
│   ├── verify.ts               → 22-step verification runner (results under outputs/run/; PF_EMIT=1 to regenerate the committed record)
│   ├── dice/                   → 123 unit tests (rng 18 · rngSpec 1 [84 checks] · settlement 21 · simChecks 38 · stats 12 · exactNull 9 · clientSeed 8 · artifact 5 · runtime 4 · captureRetry 4 · commitment 3)
│   ├── mutate.ts · mutations.json · structural-forgeries.json · mutation-survivors.json → 49 mutations + 8 forgeries, 2 declared survivors
│   ├── forgeries/              → the eight structural forgeries as executable scripts
│   └── steps/
│       ├── commitment.ts       → Steps 1–4 (seed hashes, chain + pre-capture link, epoch consistency, nonce windows)
│       ├── determinism.ts      → Steps 5–6 (roll recompute + draw-bias bound, win-rule reproduction)
│       ├── payouts.ts          → Steps 7–10 (quotient multiplier, two-stage credit, losses, wallet fields + odds bounds)
│       ├── dataset.ts          → Steps 11–12 (dataset identity + population, phase / mode coverage)
│       ├── anti-circularity.ts → Step 13 (negative control + 10,000-point enumeration anchor)
│       ├── phase-d.ts          → Step 14 (custom client seeds + sensitivity)
│       ├── boundary.ts         → Step 15 (roll ceiling, odds bounds, exact integer edge identity)
│       ├── simulation.ts       → Steps 16–17 (Pass 1 reconciliation, Pass 2 re-derivation from the dataset’s seeds)
│       ├── standardization.ts  → Steps 18–21 (bet size, artifact + value-domain integrity, settlement determinism, monotonicity)
│       ├── live-fit.ts         → Step 22 (live-sample serial independence, informational)
│       ├── statistical.ts      → informational items (live RTP, per-mode edge, roll range, win rate, odds settings)
│       └── context.ts          → shared context + step helper
├── scripts/                    → release manifest + release bar, standalone-bundle builder and its byte-identity check
├── capture/
│   ├── capture-dice.reference.mjs   → capture methodology record, authentication removed (documents, does not replay)
│   ├── client-seed.reference.mjs    → client-seed chooser (commitment first, then ≥128 bits of fresh randomness)
│   └── bet-operation.reference.mjs · rng.reference.mjs → stable request identities for retries · the capture’s RNG module
├── data/
│   └── dice-master-6700bets.json    → 6,700 bets · 134 epochs · SHA-256 pinned
├── outputs/
│   ├── verification-results.json    → 22 steps + profile coverage + info items + dataset/simulation/chart hashes + verdict
│   ├── report-figures.json          → derived figures (settled RTP, residuals, boundary hits, cherry-pick figures, Phase D, live aggregates)
│   ├── simulation-results.json      → Pass 1 summaries + Pass 2 served-band and exact-null rows
│   ├── rtp-claim-table.json         → the RTP claim rule over all 9,800 settings
│   └── rtp-convergence.html         → self-contained convergence chart (same data as the chart on this page)
├── evidence/                   → E10–E12 UI screenshots · E13 verifier boundary probes · E14 maxOdds live record · RTP page labels
├── .github/workflows/reproduce.yml → second-machine reproduction on a clean runner
├── test.js · verify.js         → dependency-free bundles (Node 22 only)
├── README.md · MANIFEST.md     → the report (findings, evidence register, worked examples, limitations L1–L19)
├── RELEASE-MANIFEST.json       → release identity: tag, framework, artifact hashes, verdict, signing key
├── package.json                → npm test = mocha && verify && release-manifest --check · test:standalone = node test.js && node verify.js
└── package-lock.json · tsconfig.json · .mocharc.yml · .nvmrc · .gitattributes · .gitignore · LICENSE
Step-to-Section Cross-Reference22 verification steps mapped
3 columns
StepTest NameReport Section
1Seed Hash IntegrityS1
2Next-Seed Pre-Commitment ChainS1
3Hash Consistency Within EpochS1
4Nonce AuditS1
5Roll Recomputation & Draw Bias BoundS1, S2, S3
6Win-Rule ReproductionS1, S3
7Multiplier DerivationS3, S4
8Credit ArithmeticS3, S4
9Loss SettlementS3
10Wallet Credit & Odds BoundsS3
11Dataset IntegrityS3
12Phase & Mode CoverageS3
13Anti-Circularity (Negative Control + Enumeration Anchor)S4
14Custom Client-Seed ControlS1, S2, S3
15Odds Boundary & Effective EdgeS4 (also scores the RTP claim rule)
16Simulation — Pass 1 (RNG + Edge)S2, S4
17Simulation — Pass 2 (Cherry-Pick)S4
18Bet-Size InvarianceS3, S4
19Artifact Hash + Value-Domain IntegrityS7
20Settlement DeterminismS3
21Multiplier Monotonicity & RangeS3, S4
22Live-Sample Serial Independence (informational)S2, S4
Game Profile CoverageDice profile 1.0.2 · 30 checks

Every check in the ProvablyFair.org Dice profile, version 1.0.2, is covered by a scored step, by a package check, or marked n/a with a reason — 22 by scored steps, 6 by package checks, 2 n/a. No check has partial coverage. The map ships in src/profile-map.ts and is emitted into outputs/verification-results.json on every run.

4 columns
CheckWhat it requiresCovered byCoverage
DICE-01Every revealed seed hashes to its commitmentStep 1Full
DICE-02Next-seed pre-commitment chain, including the pre-capture linkStep 2Full
DICE-03One commitment per epochStep 3Full
DICE-04Nonce contiguity, no reuse, no withholding; capture-retry gaps flagged, operator gaps failedStep 4Full
DICE-05Audited population bound to code constants, not the dataset headerSteps 11, 12Full
DICE-06Client-seed control and sensitivityStep 14Full
DICE-07Every served roll recomputes byte-identicallyStep 5Full
DICE-08Draw-bias bound computed from the draw’s own rejection windowStep 5Full
DICE-09Known-input unit vectors reproduce captured betsPackage check: tests/dice/rngTests.tsFull
DICE-10Win flag under the half-open rule, roll ceiling 99.99Steps 6, 15Full
DICE-11Settlement arithmetic — winning credits, losses at 0, credit determined by (roll, band, stake)Steps 8, 9, 20Full
DICE-12Bet-size invarianceStep 18Full
DICE-13Served multiplier equals 9900 / basisPointsStep 7Full
DICE-14Odds bounds and strict multiplier monotonicitySteps 10, 21Full
DICE-15All four modes, both stakes and the planned phases presentStep 12Full
DICE-16Wallet balance ledger reconciliation—N/A — the capture carries no running balance field, so there is no balance delta to reconcile; the settle-response wallet fields are reconciled in Step 10
DICE-17Boundary rule at the band edgesStep 15Full
DICE-18Anti-circularity: negative control and literal grid tallyStep 13Full
DICE-19Effective edge against the claimed edge; the RTP claim ruleStep 15Full
DICE-20Simulation Pass 1 — uniformity, serial, edge bands, convergence; replayableStep 16Full
DICE-21Cherry-pick test — served-band returnStep 17Full
DICE-22Live-sample uniformity and serial independence (informational)Steps 17, 22Full
DICE-23Framework fairness-integrity rows S5-1 … S5-8, kept as a package checklist (Section 5 reports the live integrity tests)Package check: MANIFEST.md S5 matrixFull
DICE-24Operator verifier source hash-matched to the derivation—N/A — LIQD publishes no verifier source code to hash-match; its verify endpoint is used as a cross-check (E13)
DICE-25Player verification path, in LIQD’s field namesPackage check: MANIFEST.md Verify a bet + E12Full
DICE-26Dataset and simulation artifacts pinned by SHA-256Steps 11, 19Full
DICE-27Artifacts of record: no run metadata, every numeric value inside a declared domainStep 19Full
DICE-28Release gate: fresh clone, pinned runtime, audit IDPackage check: package.json, .nvmrc, src/config.tsFull
DICE-29Mutation battery with declared survivorsPackage check: tests/mutations.json and companionsFull
DICE-30Limitations register and numbered evidencePackage check: MANIFEST.mdFull
reproduce-audit.sh
reproduce-audit.sh
# Clone and setupgit clone https://github.com/ProvablyFair-org/liqd-dice.gitcd liqd-dicegit checkout v1.1.0npm ci # Run the full audit end-to-end (123 unit tests + the 22 verification steps + release-manifest check)npm test # Or the dependency-free path — the standalone bundles need only Node.js 22node test.js && node verify.js # Or run individual componentsnpm run verify           # 22-step verification — writes outputs/run/, never the committed resultsnpm run test:full        # regenerates the simulation from its pinned master seed, then verifiesnpm run mutate           # mutation battery — 59 entries: 56 killed, 1 informational signal, 2 declared survivors # View the committed resultscat outputs/verification-results.jsoncat outputs/report-figures.jsoncat outputs/rtp-claim-table.json | head -40
provablyfair.org/verify-bets/liqd
This is an open-source audit. Every scored check, dataset and line of verification code is public. You don't need to trust us or the casino. Clone the repo, run the tests, and verify every claim yourself.
✓  Fully Reproducible

All audit-package results reproduce from release v1.1.0, the pinned dataset and the commands above. The dataset hash is checked before any step runs, so you are provably testing the same 6,700 bets; the simulation, chart and RTP claim table are pinned by SHA-256, and the derived figures are recomputed and compared on every run. The simulation is deterministic — npm run test:full regenerates it from its pinned master seed byte for byte.