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

Limbo
Independent verification, backed by open-source code.
- Live Bets Verified
- 6,700
- Parity Rate
- 100%
- Simulated Rounds
- 38M
- Simulated RTP 99.0128% over the reference run’s 20M rounds at 2.00×
- Proven RTP
- 99.00%
- Exact on every target from 1.01× to 10,000×: 98.990018%–99.000000%
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.
The casino result, reproduced.
- Casino outcome
- 14.77× · WIN
- Our reproduction
- 14.77× · WIN
- Recorded payout
- $0.2070
- Seed Hash IntegrityPass
- Next-Seed ChainPass
- Hash ConsistencyPass
- Nonce AuditPass
- Result RecomputationPass
- Win-Rule ReproductionPass
- Recomputation DeterminismPass
- Paid-Multiplier DerivationPass
- Credit ArithmeticPass
- Loss SettlementPass
- Wallet & Exposure CeilingPass
- Dataset IntegrityPass
- Phase Plan ConformancePass
- Anti-CircularityPass
- Win-Chance IdentityPass
- Effective-Edge CurvePass
- Client-Code Maximum MultiplierPass
- Custom Client-Seed ControlPass
- Artifact IntegrityPass
- Settlement DeterminismPass
- Scope & LimitationsPass
- Live-Sample FitPass
- Simulation Pass 1Pass
- Cherry-Pick ScreenPass
- Supplement & Target CeilingPass
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 with no dependency install: 27 unit tests, the 25 verification steps and 6,700 live bets re-verified — or run the two bundles directly with node test.js && node verify.js.
This audit independently validates the Limbo game operated by LIQD under the ProvablyFair.org Audit Framework 1.1. We placed 6,700 real bets across 134 seed epochs — targets from 1.01× to 500,000×, two stakes, ten custom client seeds — and recomputed every result, every win flag and every credited amount with our own implementation of the algorithm, written from a specification without LIQD’s code. Targets above 10,000× were played on the captured build; LIQD’s server now accepts targets from 1.01× to 10,000×, and the certificate covers every one of them. 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.
Audit Verdict
| Check | Result | Evidence |
|---|---|---|
| Overall Status | Pass | PROVABLY FAIR — Full Pass · Passed 25/25 · Hard fails 0 · Certified for every target from 1.01× to 10,000×, the range LIQD’s server accepts (Step 25). Provisional: captured on LIQD’s pre-launch environment; an anonymous recapture on liqd.com completes certification |
| RTP Verified | Pass | 98.990018%–99.000000% Proven RTP on every target from 1.01× to 10,000× (999,900 targets, exact) · house edge at least 1.00% on all 99,999,900 targets · simulation re-derived on every run: 10 targets × 1,000,000 rounds, each within 5 SD + 1 win of its exact win count, worst 1.56 SD (Step 23) · RTP claim rule: LIQD’s published 99% holds on all 999,900 targets, worst gap −0.009982 pp against the ±0.1 pp band (RTP-CLAIM 1.2.0, Steps 16, 23, 25) |
| Live ↔︎ Verifier Parity | Pass | 100% — 6,700 / 6,700 results and win flags reproduced from the revealed seeds, every win credited exactly stake × target (Steps 5, 6, 9) · the 300 supplement rounds of 26 September recompute the same way (Step 25) · LIQD’s own verifier, as a cross-check, agrees on 4 / 4 Verify-calculator values and 2 / 2 /verify replies |
| Commit-Reveal System | Pass | 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; every epoch holds nonces 0–49 under one commitment and one client seed (Steps 1–4) |
| Client Seed | Pass | Player-controlled and customizable — 500 / 500 Phase D rounds under 10 client seeds we chose recompute exactly; the other 124 epochs ran on fresh random seeds our capture set after LIQD had published the commitment; adding one character to the seed changes the draw on 64 / 64 probed rounds (Step 18) |
| RNG Analysis | Pass | Our own implementation, written from the specification: HMAC-SHA256 keyed by the decoded server seed, the first 4-byte chunk below 4,200,000,000 taken modulo 100,000,000 — modulo bias 0; 126 rounds had their first chunk discarded by that guard, and all 126 reproduce (Step 5) |
| Payout Logic | Pass | All 6,700 settlements verified — 2,822 wins credit exactly stake × target, an integer identity with zero tolerance; 3,878 losses pay 0; 20 rounds landed exactly on their target and were paid, as the rule result ≥ target requires (Steps 6, 8–10) |
| Anti-Circularity | Pass | The win chance comes from the draw grid, never from LIQD: target m wins on exactly floor(9.9 × 10⁹ ÷ 100m) of the 100,000,000 draws — an integer identity on all 1,104 captured targets, confirmed by four literal full-grid tallies — and RTP = win chance × target; the check rejects a result one tick away (Steps 14, 15) |
| Fairness Integrity | Pass | 34 fairness integrity tests across 12 categories — all 34 pass; no test altered, replayed or injected an outcome |
| Determinism | Pass | Full reproducibility confirmed — 6,700 / 6,700 outcomes and credits are determined by (server seed, client seed, nonce, target, stake) alone; two forward passes and one reverse pass agree on every round (Steps 7, 20) |
Overall StatusPass
RTP VerifiedPass
Live ↔︎ Verifier ParityPass
Commit-Reveal SystemPass
Client SeedPass
RNG AnalysisPass
Payout LogicPass
Anti-CircularityPass
Fairness IntegrityPass
DeterminismPass
What Was Audited
✓ Audited
- The result is deterministic: every round 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 result
- Nonces run 0–49 in every seed epoch of the capture, with no gaps or reuse
- The draw is one HMAC-SHA256 value on a 100,000,000-point grid, with bias-free rejection sampling
- The result is floor(9,900,000,000 ÷ (draw + 1)) ÷ 100, never below 1×
- The win rule is result ≥ target — a result exactly on the target wins
- A win pays exactly stake × target; a loss pays 0
- Proven RTP for every target from 1.01× to 10,000× — 98.990018% to 99.000000% — and a house edge of at least 1.00% on every target
- LIQD’s published 99% RTP holds on all 999,900 targets under the RTP claim rule (±0.1 pp)
- LIQD’s server refuses targets above 10,000× and limits a bet’s potential payout to $50,000
- Bet amount does not influence the result 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
| Area | Description |
|---|---|
| Commit-Reveal System | SHA-256 server seed hashing, pre-bet commitment, next-seed pre-commitment, reveal on rotation |
| Client Seed Origin | Player-controlled seed set after the server commitment — our own random seeds in 124 epochs and 10 custom seeds in Phase D |
| Seed Handling | Client seed control, nonce lifecycle, seed epoch rotation |
| RNG Analysis | Our own HMAC-SHA256 implementation, written from the specification; LIQD’s verifier used as a cross-check; rejection sampling, the result map and its 1× floor, boundary semantics |
| Payout Logic | Credit = stake × target as an exact integer identity, loss settlement, wallet-field reconciliation, the $50,000 potential-payout limit, bet-size invariance (Phase C) |
| Live Parity | Independent result and win-flag recomputation vs live game results across all four capture phases and the 300-round supplement |
| RTP Validation | Exact RTP of all 999,900 targets, the 1.00% edge floor over all 99,999,900 targets, literal grid tallies, the RTP claim rule, simulation re-derived on every run (Step 23) and a 38M-round reference run, cherry-pick test on the played-target return |
| Fairness Integrity | Integrity tests on LIQD’s running service — 34 across 12 categories: race conditions, input validation, atomic balance / idempotency, broken authorization, max-win bypass, nonce manipulation, RNG / PF verification, RNG / information leak, entropy / PF verification, precision / payout, event replay and adversarial seeds |
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)
- Draws are consistent with the uniform distribution expected from the rejection-sampled HMAC draw — 2,000,000 simulated draws and the 6,700 captured draws both pass their uniformity screens
- RTP is proven analytically: win chance × target = floor(9.9 × 10⁹ ÷ 100m) × 100m ÷ 10¹⁰ for every target m, in exact integers — never above 99%
- LIQD’s published 99% RTP is met by every one of the 999,900 targets the server accepts — 98.990018% to 99.000000%, all within 0.01 pp of 99%
- Simulation agrees with the exact figures: 10 targets from 1.01× to 100,000×, 1,000,000 rounds each, all within 5 SD + 1 win of their exact win counts (worst 1.56 SD), re-derived on every verify run (Step 23)
- The result is fixed by the seed pair and nonce before the target is applied — which target you choose and how much you stake never change the result
- Client seed is a genuine, player-controlled input that materially influences results (500 / 500 custom-seed rounds recompute; adding one character to the seed changes the draw on 64 / 64 probed rounds)
- The house edge is at least 1.00% on every one of the 99,999,900 targets and never favours the player — at most 1.009982% up to 10,000× — and settlement is exact, so no rounding widens it
- Every fairness integrity test in Section 5 passed — no test altered, replayed or injected an outcome
References
Limbo — Game Rules7 sections▶
How LIQD Limbo plays, exactly as verified by this audit — the interactive explainer below is wired to the audited result map and win chance.
Game Rules
Limbo turns one random draw into a result — a multiplier of at least 1×. Before the round you choose a target — any multiplier from 1.01× to 10,000× in steps of 0.01. If the result reaches your target (result ≥ target), you win and are paid stake × target; otherwise the bet is lost. The result comes from one HMAC-SHA256 draw seeded by (serverSeed, clientSeed, nonce), fixed before your target or stake is applied; your choices set the odds and the price, never the result.
How to Play
1. Choose a target — any multiplier from 1.01× to 10,000×; the higher the target, the lower the chance (2× wins 49.5% of the time, 10,000× 0.0099%).
2. Set the stake — a bet’s potential payout, stake × target, can be up to $50,000.
3. Play — the platform derives the result from the committed server seed, your client seed and the current nonce.
4. Settle — a result at or above the target credits exactly stake × target; a lower result pays 0. The nonce advances by 1.
5. Verify — after rotating your seed pair, recompute any round from the revealed seed (Section 6 walks it).
Win Conditions
One rule — win = result ≥ target. A target’s win chance is the share of the 100,000,000 equally likely draws whose result reaches it, and a win pays the target, not the result:
| Target | Win chance | A win pays, on a $1 stake |
|---|---|---|
| 1.01× | 98.019801% | $1.01 |
| 2× | 49.500000% | $2.00 |
| 10× | 9.900000% | $10.00 |
| 100× | 0.990000% | $100.00 |
| 1,000× | 0.099000% | $1,000.00 |
| 10,000× | 0.009900% | $10,000.00 |
Risk vs Reward
Every target returns 99% to within 0.01 pp — the target changes the shape of the risk, never the fairness of the price:
- Near-certain: 1.01× wins 98.019801% of the time — the highest win chance on offer
- Even money: 2× wins 49.5% of the time
- Long shots: 100× wins 0.99% of the time; 10,000×, the highest target the server accepts, 0.0099%
- The price is exact: win chance × target is never above 99%, and between 98.990018% and 99.000000% on every target up to 10,000×
- Variance grows with the target, the edge does not: the house edge stays between 1.00% and 1.009982% on every target you can bet
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Draw domain | 0 – 99,999,999 | 100,000,000 equally likely draws (RANGE = 100000000) |
| Result | from 1× | Two decimals; the 1× floor lifts 1% of draws. LIQD’s game code names 1,000,000× as the maximum — only draws 0–97 would go higher, and they win every target either way |
| Target | 1.01× – 10,000× | Steps of 0.01 — 999,900 settings, every one held to the RTP claim (S4.2) |
| Win chance | 0.0099% – 98.019801% | floor(9,900,000,000 ÷ (100 × target)) ÷ 10⁸ — LIQD’s server states the same range |
| House edge | 1.00% – 1.009982% | Per target up to 10,000×; LIQD states a flat 1.00%, met within ±0.1 pp on all 999,900 |
| Minimum stake | $0.10 | LIQD’s client configuration; every captured stake is at or above it |
| Potential payout | up to $50,000 | stake × target per bet, enforced when the bet is placed |
| Seed epoch | 50 bets | Nonces 0–49, then the pair rotates — the cadence this capture chose |
| Settlement | exact | stake × target — a 2-decimal stake times a 2-decimal target, reproduced with zero tolerance |
| Stakes captured | $0.10 and $10.00 | 6,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:
| Seed Type | Format | Example | Purpose |
|---|---|---|---|
| Server seed | 32-char hex (16 bytes) | ee4724f00cfc46e28425e9de3239d427 | HMAC key (decoded); committed via SHA-256 of the string before play |
| Client seed | string, set by the player | audita5a8f43bd18d | Set at each seed rotation — mixed into every draw |
| Nonce | integer from 0 | 0 | The bet counter under the active pair — 0–49 in this capture |
Multiplier & Payout
The payout is a formula — no paytable file exists. The draw k becomes the result, the result is compared with your target, and a win pays the target. Every row below is a real captured round, recomputed by this audit:
k = first 4-byte chunk below 4,200,000,000, mod 100,000,000
result = max(1, floor(9,900,000,000 / (k + 1)) / 100)
win chance = floor(9,900,000,000 / (100 × target)) / 10⁸
payout = stake × target when result ≥ target, otherwise 0| Draw k | Result | Target | Outcome |
|---|---|---|---|
| 534 | 185,046.72× | 1.17× | Win — pays the target, $0.10 × 1.17 = $0.117, not the result (bet S4mFs74B…) |
| 924,515 | 107.08× | 100× | Win — $0.10 × 100 = $10.00, the round worked through in Section 4.9 (bet S8UAp699…) |
| 40,058,996 | 2.47× | 4.58× | Loss — pays 0 (bet USzRL9gy…) |
| 84,760,019 | 1.16× | 1.16× | Win — exactly on the target, $0.10 × 1.16 = $0.116 (bet bYS2viIe…) |
| 99,762,668 | 1× | 3.76× | Loss — the raw value 0.99 is lifted to the 1× floor (bet yAVLBL0Q…) |
stake × target — credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴, with zero tolerance (Step 9). The round worked through in Section 4.9 paid $0.10 × 100 = $10.00.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 — an unpredictable client seed of your own, set after the commitment is published
- Anyone can verify the outcome after the fact — the revealed seed plus public code reproduces every round 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
| Check | What it proves |
|---|---|
| Server seed hash shown before play | With the hash shown first, the casino cannot change the result after you bet |
| SHA-256 of the revealed seed matches the commitment | The revealed seed is the one that was committed |
| Next epoch's hash pre-committed at rotation | The casino cannot mint a fresh seed in response to your play |
| One commitment per epoch, no mid-epoch switching | Every bet is pinned to a single verified commitment |
Seed System
| Check | What it proves |
|---|---|
| Client seed is set by the player after the next commitment is published | An unpredictable seed set then cannot be pre-evaluated by the operator |
| Client seed changes the result when substituted | Your seed genuinely enters every derivation |
| Nonces run contiguously 0–49 in every captured epoch | No skipped bet where a discarded outcome could hide |
RNG & Parity
| Check | What it proves |
|---|---|
Result recomputes from (serverSeed, clientSeed, nonce) | No hidden inputs influence the result |
Rejection sampling below maxFair = 4,200,000,000 | Every one of the 100,000,000 draws is exactly equally likely |
| The rule result ≥ target reproduces 6,700 / 6,700 win flags | The game settles a result exactly on the target as a win — 20 captured rounds show it |
Payout & RTP
| Check | What it proves |
|---|---|
Wins credit exactly stake × target | The payout is the target you chose, never more or less |
floor(N / ci) × ci ≤ N on all 99,999,900 targets, as integers | The 1.00% house edge is a floor — an identity, not a sampled estimate |
| Closed-form win count equals a literal 100,000,000-draw tally at four targets | The probability side cannot share an arithmetic slip with the formula |
Integrity
| Check | What it proves |
|---|---|
| Losses pay exactly 0 | No round settles money against a losing result |
| Identical engine at $0.10 and $10.00 | Stake size cannot reach the result |
| Dataset SHA-256 pinned (a mismatch stops the suite); supplement, probe journals and simulations pinned and checked in Steps 19, 23 and 25 | The evidence itself is tamper-evident |
High-Level Flow
One round of LIQD Limbo, from commitment to settlement:
1. Commit — the server generates a 32-hex server seed and publishes SHA-256(utf8(seed)) as activeServerSeedHash. You see only the hash.
2. Seed pair active — the client seed you set at rotation and a nonce starting at 0 pair with the committed seed.
3. Bet — you choose a target and a stake. The result is already fully determined by (serverSeed, clientSeed, nonce).
4. Draw — one HMAC-SHA256 value, reduced to a draw k on the 100,000,000-point grid, becomes the result floor(9,900,000,000 / (k + 1)) / 100, never below 1×.
5. Settle — a result at or above the target credits stake × target; a lower result pays 0. The nonce increments by 1.
6. Rotate & verify — rotation reveals the outgoing seed (and pre-commits the next). Check the hash, recompute any round.
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 result is a pure function of those three inputs — and the target 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
Four phases, each independently verified by this audit:
Commit phase
The server generates a 32-hex seed and publishes SHA-256 of that string (UTF-8) as activeServerSeedHash (recorded as hashedServerSeed). The raw seed stays secret. Verified for all 134 epochs (Step 1).
Bet phase
Bets settle under the committed pair; the nonce counts up from 0 (0–49 in this capture). The result is fixed per (seed, clientSeed, nonce) before the target 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 round it served — LIQD’s own verifier, the ProvablyFair.org verifier or the standalone scripts in S6 all do it.
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:
- Set by the player at every seed rotation — after the next server seed’s hash is already published
- Mixed into every HMAC message (
clientSeed:nonce:cursor) — it mathematically cannot be ignored - Phase D played 500 rounds under 10 client seeds we chose — all recomputed exactly
- Sensitivity check: adding one character to the client seed changed the draw on 64 / 64 probed rounds (Step 18)
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
"epoch": 0,"nonceStart": 0,"nonceEnd": 49,"hashedServerSeed": "394004d9c91573202b92489b53a5e8ef…"Determinism Guarantee
Determinism is what turns a fairness claim into a checkable fact: the same inputs must always produce the same result. LIQD's derivation is a pure function of three arguments — verified by reproducing every captured result exactly, on the two-decimal grid, with an independent implementation.
limboResult(serverSeed, clientSeed, nonce) → 2.47 // one result, at least 1× — nothing else in, nothing else outPhase Map
The capture is engineered — four phases, each isolating one claim, and a supplement that fills the high-target decades:
| Phase | Rounds | What it isolates |
|---|---|---|
| A | 5,000 | Random targets from 1.01× to 49.45× (win chance drawn in 2%–98%), $0.10 — 1,104 distinct targets over the whole capture |
| B | 1,000 | The high-multiplier tail — 250 bets each at 100×, 1,000×, 100,000× and 500,000× |
| C | 200 | Stake independence — 2.00× at $10.00, 100× the $0.10 stake |
| D | 500 | Client-seed influence — 10 client seeds we chose, live |
| Supplement, 26 September | 300 | The high-target decades — 100 rounds each from 100× to 999.99×, from 1,000× to 9,999.99× and at 10,000× (Step 25) |
Technical Glossary7 categories▶
Every term used in this report, defined the way the audit uses it.
Core Concepts
| Term | Definition |
|---|---|
| Provably Fair | A 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 result without detection. |
| Commit-Reveal | The mechanism underlying provably fair. The server publishes a commitment (activeServerSeedHash, recorded here as hashedServerSeed) before any bet, then reveals the plaintext seed (revealedServerSeed) when the seed rotates. The auditor checks SHA-256(utf8(serverSeed)) == hashedServerSeed for every epoch. All 134 revealed seeds in this dataset passed. |
| House Edge | The operator’s cut. LIQD’s Limbo page states “Edge: 1.00%” and RTP 99%. The effective edge is derived independently for every target: because the win chance is rounded down to a whole number of draws, it is at least 1.00% on all 99,999,900 targets and at most 1.009982% on the targets up to 10,000× the server accepts (Step 16). |
| RTP (Return to Player) | 1 − houseEdge. For LIQD Limbo the theoretical RTP of a target is win chance × target, exactly: 98.990018% – 99.000000% across the 999,900 targets from 1.01× to 10,000× (Step 16), anchored by literal tallies of all 100,000,000 draws (Step 15) and by simulation (Step 23). Live-bet RTP (100.4475%) is informational only. |
| Anti-Circularity | The requirement that theoretical RTP and simulation expectations derive the win probability from the draw grid alone — floor(9.9 × 10⁹ ÷ 100m) winning draws out of 100,000,000 — never from an operator-supplied odds or win-chance field, so the RTP claim cannot validate itself (Steps 14, 15). |
| RTP Claim Rule | RTP-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 (exit 5). LIQD’s published 99% holds on all 999,900 Limbo targets the server accepts; the largest gap, −0.009982 pp, is at 9,989.91×. |
Seed System
| Term | Definition |
|---|---|
| Server Seed | A 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 Hash | SHA-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 Seed | A string the player supplies at each seed rotation, mixed into the HMAC message as ${clientSeed}:${nonce}:${cursor}. Changing it changes the draw — adding one character changed it on 64 / 64 probed rounds (Step 18). 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. |
| Nonce | An incrementing integer identifying a bet's position within an epoch — 0–49 in this capture, which rotated the pair after 50 bets. Part of the HMAC message. Every epoch in this dataset has contiguous nonces 0–49 — no gaps. |
| Epoch | The bets placed under one (serverSeed, clientSeed) pair — in this capture exactly 50, nonces 0–49. At epoch end the seed rotates and the previous server seed is revealed. This dataset covers 134 epochs. |
Cryptographic Functions
| Term | Definition |
|---|---|
| HMAC-SHA256 | Hash-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 result: the digest is scanned in 4-byte big-endian uint32 chunks, and the first chunk below the modulo-bias ceiling is taken % 100000000. |
| Cursor | The third component of the HMAC message. Limbo makes one draw per round, so cursor = 0 for every round, and the digest’s eight 4-byte chunks are scanned in order. All eight would be rejected about 5.7 × 10⁻¹⁴ of the time — effectively never; no captured round needed more than its third chunk. |
| Modulo-Bias Rejection | Discarding uint32 values at or above maxFair = floor(2³² / 10⁸) × 10⁸ = 4,200,000,000 before taking % 100000000, so every draw is exactly equally likely. A chunk is discarded 2.2111% of the time: 126 of the 6,700 captured rounds had their first chunk discarded, and all 126 reproduce (Step 5). |
| Draw (k) | The round’s random number: the first accepted chunk taken % 100000000 — one of 100,000,000 equally likely values, 0 to 99,999,999. The round worked through in Section 4.9 drew k = 924,515. |
| Result Map | max(1, floor(9,900,000,000 / (k + 1)) / 100) — the draw k placed on the two-decimal result scale, never below 1×. The served result is compared with the recomputed value exactly (Step 5). |
Verification Terms
| Term | Definition |
|---|---|
| Win Chance | floor(9,900,000,000 / ci) / 10⁸, with ci = 100 × target — the exact share of the 100,000,000 draws whose result reaches the target. An exact integer identity on all 1,104 captured targets, confirmed by literal full-grid tallies at 1.01×, 2×, 100× and 500,000× (Step 15). |
| Inclusive Win Rule | result ≥ target — a result exactly on the target wins; a strict > rule would lose those rounds. Reproduced on all 6,700 win flags (Step 6). |
| Boundary Round | A round whose result equals its target exactly — the case the inclusive rule decides. The capture holds 20, against 18.24 expected, and LIQD paid all 20 — for example 1.16× on a 1.16× target, $0.116 on $0.10 (bet bYS2viIe…). |
| House Edge Floor | Because the win chance is rounded down to a whole number of draws, floor(N / ci) × ci ≤ N for every target — so the edge is never below 1.00%. Checked as integers on all 99,999,900 targets (Step 16); up to 10,000× it is at most 1.009982%. |
| Exact Settlement | stake × target — both two-decimal values, so the product is exact at the 4th decimal: credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ on all 2,822 wins, with zero tolerance (Step 9); every loss pays 0 (Step 10). |
| Result Floor | The result never goes below 1×: 1% of draws have a raw value below 1 and are lifted (78 captured rounds; 147 show 1×). The floor never changes an outcome — every target is at least 1.01×. |
| Result Ceiling | LIQD’s game code names 1,000,000× as the maximum result. Only draws 0–97 (98 of 100,000,000) would go higher, and a result that high wins every target either way, so the maximum never changes an outcome. The highest captured result is 185,046.72×, from k = 534. |
Game Mechanics
| Term | Definition |
|---|---|
Target (params.target) | The multiplier you bet on — any two-decimal value from 1.01× to 10,000×, the range LIQD’s server accepts. The only game input; there are no risk levels or variants. 1,104 distinct targets were captured. |
| Result | The round’s outcome, a multiplier of at least 1×. It decides the win, but the payout is the target — the 185,046.72× round on a 1.17× target paid 1.17×. |
| Potential Payout | stake × target, limited to $50,000 per bet: LIQD’s refusal of a $0.10 bet at 1,000,000× named a $0.05 maximum stake for that target (0.05 × 1,000,000 = 50,000), and 250 captured rounds — 500,000× at $0.10 — were placed at exactly that limit. |
| Target Cap | The highest target LIQD’s server accepts: 10,000×. On 2 October 2026 a control round at 10,000× settled, and four requests between 100,101.12× and 990,000.01× were refused with the server’s own message, “must be a multiple of 0.01 between 1.01 and 10000” (Step 25). |
Audit Terms
| Term | Definition |
|---|---|
| Draw Uniformity | A simulation test: 2,000,000 draws binned into 100 cells against N/100, χ² with df = 99. χ² = 94.393, p = 0.6122 (Step 23); the 6,700 captured draws give p = 0.2315 on the same test. |
| Serial Screen | Lag-1 autocorrelation and the Wald-Wolfowitz runs test on the 2,000,000 simulated draws, each judged at α/3: lag-1 z = 0.937, runs p = 0.8679 (Step 23). |
| Bonferroni Correction | The three Pass 1 screens (uniformity, lag-1, runs) each run at α/3 = 0.00333 so the family has level α = 0.01. |
| Played-Target Return | The Pass 2 cherry-pick statistic (the repo’s “served-target return”): each of the 134 revealed seeds is replayed for 10,000 nonces in 200 windows of 50, every window betting the 50 targets played in that epoch, and the captured window is ranked against the other 199. Rank-sum Z = −0.1711 against the −3.090 threshold (α 0.001) — no cherry-picking indicated (Step 24). |
| 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 an unpredictable client seed set after the commitment — a fresh random seed in 124 of the 134 epochs; the statistical check is the played-target return in Pass 2, which catches, at least half the time, a selection putting 16.1% or more of the epochs’ captured windows in their worst decile. |
| Selective Orphaning | A 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). |
| Hard Fail | Any FAIL on a scored step — a broken commitment or chain link, a result, 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 (exit 1). None triggered: Passed 25/25, Hard fails 0. |
Data Formats
| Term | Definition |
|---|---|
winningAmount | The settle-response field holding the amount credited on a win or 0 on a loss. The audit's payout evidence: verified against betAmount × target at zero tolerance, and reconciled with the wallet-denominated winningAmountWallet on all 6,700 bets. |
result, win | The served result (two decimals) and win flag, stored as LIQD returned them. result is compared exactly on the two-decimal grid; win is re-derived from the result and the target. The dataset’s multiplier column is written by our capture script — the target on a win, 0 on a loss — and checked against the target (Step 8). |
| Phase A / B / C / D | The four capture phases. A: 5,000 bets on random targets from 1.01× to 49.45×. B: 1,000 bets, 250 each at 100×, 1,000×, 100,000× and 500,000×. C: 200 bets on 2.00× at $10.00 (stake independence). D: 500 bets under 10 custom pfaudit- client seeds. A 300-round supplement on 26 September adds Phase A targets from 100× to 10,000× (Step 25). |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Limbo round 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 rounds and in what order, and our capture records show each hash was published before its rounds — so the casino cannot change the result after you bet.
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
- The player supplies the client seed at every seed rotation — our capture set its own in all 134 epochs, and every round recomputes under it
- Nonce increments by 1 per round with no gaps or duplicates across 134 seed epochs — nonces 0–49 in every one
- The result is fully determined by the seed inputs before the target or stake is applied
- Identical inputs always produce the same result — confirmed across all 6,700 rounds, exact on the two-decimal grid
- Your client seed is a genuine input — changing it changes the draw
What this means for you
- With the commitment shown first, the casino cannot change the result after you bet
- An unpredictable client seed of your own adds randomness the server cannot predict or pre-select against
- Every round has its own derivation — the nonce guarantees no two rounds 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 when you set an unpredictable client seed after the next commitment is published — the operator cannot pre-evaluate seeds against it
- 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
- The player supplies the client seed at every seed rotation — our capture set its own in all 134 epochs, and every round recomputes under it
- Nonce increments by 1 per round with no gaps or duplicates across 134 seed epochs — nonces 0–49 in every one
- The result is fully determined by the seed inputs before the target or stake is applied
- Identical inputs always produce the same result — confirmed across all 6,700 rounds, exact on the two-decimal grid
- Your client seed is a genuine input — changing it changes the draw
- With the commitment shown first, the casino cannot change the result after you bet
- An unpredictable client seed of your own adds randomness the server cannot predict or pre-select against
- Every round has its own derivation — the nonce guarantees no two rounds 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 when you set an unpredictable client seed after the next commitment is published — the operator cannot pre-evaluate seeds against it
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
audit… seed in each of the 124 Phase A–C epochs, and 10 seeds we chose in Phase D (Step 18)Client seed controlPass
Client seed influencePass
Deterministic outputPass
Win-rule reproductionPass
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 of this capture with no gaps. Outcomes are fully deterministic — the same server seed, client seed and nonce always produce the same result. With each commitment shown before its rounds, as our capture records show, the casino cannot change your result after you bet.
| Check | Result | Evidence |
|---|---|---|
| Seed hash integrity | Pass | 134 / 134 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1) |
| Next-seed chain | Pass | Consistent on 133 / 133 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2) |
| One commitment per epoch | Pass | All 50 rounds in each of 134 epochs recompute under their epoch’s one revealed seed, and the committed and revealed sets are identical — 134 against 134 (Steps 3, 5) |
| Nonce sequencing | Pass | Nonces 0–49 in every epoch: 0 gaps, 0 duplicates, one client seed per epoch (Step 4) |
| Client seed origin | Pass | Set after the server commitment — a fresh random audit… seed in each of the 124 Phase A–C epochs, and 10 seeds we chose in Phase D (Step 18) |
| Client seed control | Pass | LIQD uses the seed the player sets — 500 / 500 Phase D rounds recompute under the 10 seeds we chose (Step 18) |
| Client seed influence | Pass | Adding one character to the client seed changes the draw on 64 / 64 probed rounds; a wrong or empty client seed changes the draw on all 6,700 (Step 18) |
| Deterministic output | Pass | Same (serverSeed, clientSeed, nonce) always produces the same result — 6,700 / 6,700 reproduced exactly; two forward passes and a reverse pass agree on every round (Steps 5, 7) |
| Win-rule reproduction | Pass | 6,700 / 6,700 served win flags reproduced by result ≥ target, including the 20 rounds that landed exactly on their target (Step 6) |
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
audit… seed in each of the 124 Phase A–C epochs, and 10 seeds we chose in Phase D (Step 18)Client seed controlPass
Client seed influencePass
Deterministic outputPass
Win-rule reproductionPass
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 of this capture with no gaps. Outcomes are fully deterministic — the same server seed, client seed and nonce always produce the same result. With each commitment shown before its rounds, 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 round is placed under a seed pair, LIQD shows the SHA-256 hash of a secret 32-character hex server seed as activeServerSeedHash (recorded with every round as hashedServerSeed). The commitment hashes the seed text as a UTF-8 string, not its 16 decoded bytes: SHA-256(utf8(serverSeed)). The HMAC that draws each result is keyed by the decoded bytes instead, so a verifier must keep the two forms apart. 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.
/** * §1.2: the published commitment is SHA-256 over the ASCII text of the seed * (the hex characters as written, not the bytes they encode), as lowercase hex. */export function commitHash(serverSeedHexString: string): string { return createHash("sha256").update(Buffer.from(serverSeedHexString, "ascii")).digest("hex");}Real Example from Live Data:
{ "epoch": 0, "phase": "A", "clientSeed": "audita5a8f43bd18d", "hashedServerSeed": "394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded", "nextHashedServerSeed": "7d89efbe2fc3a4418eaee5086ea1f0a97931c53ce9bc200d8072bdf09b364af0", "serverSeed": "ee4724f00cfc46e28425e9de3239d427", "nonceStart": 0, "nonceEnd": 49}Verification:
// verify-seed.js — check any revealed LIQD server seed against its commitmentconst { createHash } = require('node:crypto');const serverSeed = 'ee4724f00cfc46e28425e9de3239d427'; // revealed 32-hex seed (epoch 0)const committed = '394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded';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 next hash LIQD pre-commits during an epoch (nextServerSeedHash, recorded as nextHashedServerSeed) must equal the commitment 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 round (1eea2dae…) hashes to its own commitment (6e7ed0a2…), and its pre-committed next hash is epoch 0’s hashedServerSeed (394004d9…). 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 rounds it covers.
// ── Step 2: Next-seed pre-commitment chain ────────────────────────────────── // … const byEpoch = [...seeds].sort((a, b) => a.epoch - b.epoch); // … let promoChecked = 0; let promoFails = 0; for (let i = 0; i + 1 < byEpoch.length; i++) { promoChecked++; if (byEpoch[i].nextHashedServerSeed !== byEpoch[i + 1].hashedServerSeed) promoFails++; } // … const preCommitOk = commitHash(pre.revealedServerSeed) === pre.hashedServerSeed; const preLinkOk = pre.nextHashedServerSeed === epoch0.hashedServerSeed; preOk = preCommitOk && preLinkOk;1.3Hash Consistency Within an Epoch
Each round 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 rounds by epoch, confirms that all 50 rounds in each epoch carry the same commitment and that the epochs present are exactly 0–133, and then compares two sets hash for hash: the commitments the rounds settle against and the revealed seed records — 134 against 134, every one matched on both sides. What shows that no seed changed mid-epoch is the recomputation: every round reproduces under its epoch’s revealed seed (Step 5), and that seed hashes to the epoch’s commitment (Step 1).
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) { if (new Set(epochBets.map((b) => b.hashedServerSeed)).size !== 1) epochsMultipleHashes++; } const missingEpochs: number[] = []; for (let e = 0; e < EXPECTED_SEEDS; e++) if (!betsByEpoch.has(e)) missingEpochs.push(e); const strayEpochs = [...betsByEpoch.keys()].filter((e) => e < 0 || e >= EXPECTED_SEEDS); // The seed set and the set of commitments actually settled against must be the same set, // hash for hash — not merely the same size. const seedHashSet = new Set(seeds.map((s) => s.hashedServerSeed)); const betHashSet = new Set(bets.map((b) => b.hashedServerSeed)); const unbackedBetHashes = [...betHashSet].filter((h) => !seedHashSet.has(h)); const unusedSeedHashes = [...seedHashSet].filter((h) => !betHashSet.has(h)); const setsIdentical = unbackedBetHashes.length === 0 && unusedSeedHashes.length === 0 && seedHashSet.size === betHashSet.size; const s3 = step(3, 'Hash Consistency & Epoch Coverage', epochsMultipleHashes === 0 && missingEpochs.length === 0 && strayEpochs.length === 0 && setsIdentical ? 'PASS' : 'FAIL', // … );1.4Client Seed Origin & Control
The client seed is the player’s input to every draw. LIQD takes it when the seed pair rotates — the rotate request carries it — and at that moment the next server seed’s hash is already published, so the server seed is fixed before the client seed it will be paired with is known. That ordering structurally contains cherry-picking when the client seed is unpredictable: an operator that cannot know the upcoming client seed cannot pre-evaluate candidate server seeds against it. In this capture each of the 124 Phase A–C epochs ran on a fresh random client seed — audit plus 12 hex characters, 6 random bytes from Node’s CSPRNG — submitted at rotation after LIQD had published that epoch’s commitment, and the 10 Phase D epochs ran on seeds we chose. Five of the Phase A–C seeds, one every 30 epochs:
audita5a8f43bd18d— epoch 0audit63e25885f5be— epoch 30audit6e338dd45fcb— epoch 60auditf26306e848f1— epoch 90auditdc525d958d88— epoch 120
1.5Nonce Incrementation
The nonce begins at 0 and increments by 1 for each round under the same seed pair — one nonce per result. It resets to 0 when the player rotates their seed, starting a new epoch. In this audit we rotated after every 50 rounds as our sampling cadence (nonces 0–49 per epoch, 134 epochs). 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. It also checks one client seed per epoch, that each epoch opens at the post-rotation nonce LIQD reported (nonceStart, 134 / 134), and that each epoch holds exactly 50 rounds — bound to the constant in src/config.ts rather than read from the dataset header, so a withheld round cannot be absorbed by shrinking the declared epoch.
for (const [hash, epochBets] of byHash) { const sorted = [...epochBets].sort((a, b) => a.nonce - b.nonce); const nonces = sorted.map((b) => b.nonce); const epochNum = sorted[0].epoch; if (new Set(sorted.map((b) => b.clientSeed)).size !== 1) { hardFailures.push(`epoch ${epochNum}: ${new Set(sorted.map((b) => b.clientSeed)).size} distinct client seeds`); } const nonceSet = new Set(nonces); // Nonce REUSE: interior contiguity alone treats a duplicated nonce as a benign gap, so a // replayed draw would pass. Require distinct nonces == bet count. if (nonceSet.size !== nonces.length) { hardFailures.push(`epoch ${epochNum}: nonce reuse — ${nonces.length} bets, ${nonceSet.size} distinct nonces`); } // … const outOfRange = nonces.filter((n) => !Number.isInteger(n) || n < 0 || n >= EXPECTED_EPOCH_SIZE); if (outOfRange.length) hardFailures.push(`epoch ${epochNum}: ${outOfRange.length} nonce(s) outside [0, ${EXPECTED_EPOCH_SIZE})`); const missing: number[] = []; for (let n = 0; n < EXPECTED_EPOCH_SIZE; n++) if (!nonceSet.has(n)) missing.push(n); // … if (seedEntry.nonceStart != null) { if (Math.min(...nonces) !== seedEntry.nonceStart) { hardFailures.push(`epoch ${epochNum}: first observed nonce ${Math.min(...nonces)} != operator post-rotation nonce ${seedEntry.nonceStart} (leading bets withheld?)`); } leadingOperatorBound++; } // … const declaredHere = DECLARED_CAPTURE_GAPS.filter((g) => g.epoch === epochNum && missing.includes(g.nonce)).length; if (epochBets.length + declaredHere !== EXPECTED_EPOCH_SIZE) { hardFailures.push(`epoch ${epochNum}: ${epochBets.length} bets + ${declaredHere} declared capture gap(s) != audited epoch size ${EXPECTED_EPOCH_SIZE} (src/config.ts)`); } // …1.6Deterministic Mapping
The result is a pure function of three inputs: (serverSeed, clientSeed, nonce) → draw k → result. One HMAC-SHA256 digest gives the draw k, an integer from 0 to 99,999,999 (2.1 shows how), and max(1, floor(9,900,000,000 / (k + 1)) / 100) turns it into the result, never below 1× (2.7). No timing, no stake, no target and no session state enters the derivation. Step 5 recomputes all 6,700 captured results exactly on the two-decimal grid — our draw (limboDraw) through the scored result map (resultMultiplier in src/config.ts), which equals our limboResult on every captured draw (the smallest is 534). Step 7 shows the derivation carries no state: the full 6,700-round recomputation run twice and once in reverse order agrees on every round, and 204 rounds recomputed five times each never change. The record below is the first round of the capture — recompute it yourself with the snippet.
/** §1.1 + §2 Step 2: the limbo draw uses range 10^8 and cursor 0. */export function limboDraw(serverSeed: string, clientSeed: string, nonce: number): number { return generateProvablyFairNumber(serverSeed, clientSeed, nonce, 0, LIMBO_RANGE);} /** * §2 Step 3: raw_cents = floor(99 * 10^8 / (r + 1)) by integer division, * clamped up to 100 cents, then expressed as a 2-decimal multiplier. * No upper cap is applied (O3). */export function limboResult(serverSeed: string, clientSeed: string, nonce: number): number { const r = limboDraw(serverSeed, clientSeed, nonce); // Both operands are integers far below 2^53, so Math.floor of the double quotient // equals true integer division here; BigInt is used anyway to make that explicit. const rawCents = Number(BigInt(LIMBO_NUMERATOR) / BigInt(r + 1)); const resultCents = Math.max(RESULT_FLOOR_CENTS, rawCents); return resultCents / 100;}result exactly (Step 5), and two forward passes and a reverse pass agree on every round (Step 7). Zero mismatches, zero skipped. No external entropy is mixed into the Limbo RNG.Real Round Verified:
{ "id": "USzRL9gyMBxZg8NLCNoYS", "phase": "A", "epoch": 0, "nonce": 0, "clientSeed": "audita5a8f43bd18d", "params": { "target": 4.58 }, "result": 2.47, "win": false, "resultLabel": "lost", "betAmount": 0.1, "winningAmount": 0}Verification:
// verify-limbo.js — recompute a LIQD Limbo result 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(2 ** 32 / 1e8) * 1e8; // 4,200,000,000 for (let o = 0; o + 4 <= digest.length; o += 4) { const chunk = digest.readUInt32BE(o); if (chunk < maxFair) return chunk % 1e8; } throw new Error('all eight chunks rejected (about 5.7e-14 per round)');}const k = draw('ee4724f00cfc46e28425e9de3239d427', 'audita5a8f43bd18d', 0);const result = Math.max(100, Math.floor(9_900_000_000 / (k + 1))) / 100;console.log(k, result); // → 40058996 2.47console.log(result >= 4.58); // → false (target 4.58× — a loss)1.7Win-Rule Reproduction & Client Seed Influence
Determinism covers the number; Step 6 covers what LIQD did with it. For every round the recomputed result is compared with the target under the rule win = result ≥ target, and the outcome must equal the served win flag: 6,700 / 6,700 agree. The rule is inclusive, and the settled data show it: 20 rounds landed exactly on their target — 18.24 were expected from the targets played — and LIQD settled all 20 as wins and paid each one stake × target (Steps 6, 9), where a strict > rule would have lost every one. Bet bYS2viIeJOfIRm1YpweuJ is one of them: a 1.16× result on a 1.16× target, paid $0.10 × 1.16 = $0.116. The 1× floor never wins, because every target is at least 1.01× — all 147 rounds at 1× lost. To confirm the client seed is a genuine input, Step 18 recomputes all 500 Phase D rounds under the seeds we chose (500 / 500 reproduce), adds one character to the seed on 64 probed rounds (the draw changed on 64 / 64), and runs a wrong-seed and an empty-seed control over all 6,700 rounds — the draw changed on every one of them.
for (const b of bets) { const target = Number(b.params.target); // … if (!isTargetLegal(target)) illegalTargets++; const ss = seedMap.get(b.hashedServerSeed); if (!ss) continue; const local = resultMultiplier(limboDraw(ss, b.clientSeed, b.nonce)); if (limboWin(local, target) !== !!b.win) { winFails++; if (!firstWinFail) firstWinFail = `epoch ${b.epoch} nonce ${b.nonce}: R ${local} vs target ${target}, served win=${b.win}`; } if (local === target) { discriminating++; if (b.win) discriminatingWins++; } if (local === MIN_RESULT) { floorRounds++; if (b.win) floorWins++; } if ((local > target) !== !!b.win) flipsUnderStrictGt++; winChecked++; } const s6 = step(6, 'Win-Rule Reproduction', winFails === 0 && illegalTargets === 0 && winChecked === EXPECTED_BETS && floorWins === 0 && MIN_RESULT < MIN_MULTIPLIER ? 'PASS' : 'FAIL', // … );result ≥ target on the recomputed result (Step 6), including all 20 rounds exactly on their target; 500 / 500 custom-seed rounds recompute and a changed client seed changes the draw on 64 / 64 probes (Step 18). The client seed is a genuine, material input.Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Seed hash integrity (Step 1) | 134 / 134 revealed seeds hash-verified | Pass |
| Next-seed pre-commitment chain (Step 2) | 133 / 133 epoch transitions + the pre-capture link into epoch 0 | Pass |
| Hash consistency & epoch coverage (Step 3) | 134 / 134 epochs (50 rounds each); committed and revealed sets identical | Pass |
| Nonce audit (Step 4) | 0 gaps, 0 duplicates across 134 epochs; one client seed per epoch | Pass |
| Result recomputation (Step 5) | 6,700 / 6,700 — exact on the two-decimal grid | Pass |
| Win-rule reproduction (Step 6) | 6,700 / 6,700 served win flags; all 20 rounds exactly on target settled as wins | Pass |
| Recomputation determinism (Step 7) | Two forward passes + one reverse pass agree on 6,700 / 6,700; 204 rounds × 5 repeats stable | Pass |
| Custom client-seed control (Step 18) | 500 / 500 Phase D rounds recompute; 64 / 64 probes change the draw; wrong- and empty-seed controls change the draw on 6,700 / 6,700 | Pass |
1.9Code References8 source files
| File | Purpose |
|---|---|
| tests/verify.ts | 25-step verification pipeline (Steps 1–7 and 18 cover S1) |
| tests/steps/commitment.ts | Steps 1–4: commit-reveal integrity checks and nonce audit |
| tests/steps/determinism.ts | Steps 5–7: result recomputation, win-rule reproduction and recomputation determinism |
| tests/steps/phase-d.ts | Step 18: custom client-seed control, sensitivity probe and the wrong- and empty-seed controls |
| src/rng.ts | HMAC-SHA256 draw and result (commitHash, generateProvablyFairNumber, limboDraw, limboResult) — written from docs/ALGORITHM.md |
| src/config.ts | Game constants, the scored result map (resultMultiplier), the win rule (limboWin) and the audited population (134 epochs of 50) |
| src/loader.ts | Dataset loading + SHA-256 hash guard |
| capture/capture-limbo.reference.mjs | Capture methodology record (Phases A–D), credentials removed |
1.10Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live Limbo game data from LIQD’s pre-launch environment (qa.liqd.com) — 6,700 real rounds across 4 phases, USDC |
| Capture environment | Pre-launch host qa.liqd.com · one authenticated account · one session |
| Total Records | 6,700 rounds across 134 epochs (50 rounds each), plus 134 seed records |
| Captured | 11 September 2026 (13:16–14:12 UTC) |
| SHA-256 | 8e3ce5b5e1494d1fcc3a783965d3ba7b6a7099f120a64d61e21a577098c07334 |
Fields used: serverSeed, hashedServerSeed, nextHashedServerSeed, nonceStart, nonceEnd, clientSeed, nonce, params.target, result, win, resultLabel, betAmount, winningAmount, stakeWallet, winningAmountWallet, and the pre-capture record meta.preCapture
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-limbo.reference.mjs — credentials removed; it documents the capture, it does not replay it). Every computational claim downstream of the dataset is reproducible from the file itself.
1.11Verified Invariants11 invariants
| Invariant | Result |
|---|---|
| SHA-256(utf8(serverSeed)) = hashedServerSeed for all 134 revealed seeds | Pass |
| Next-seed pre-commitment chain intact for all 133 transitions, plus the pre-capture link into epoch 0 | Pass |
| Every round recomputes under its epoch’s revealed seed — no seed changed mid-epoch (Steps 3, 5) | Pass |
| The commitments the rounds settle against and the revealed seed records are the same set — 134 against 134 | Pass |
| Zero nonce gaps within any epoch (contiguous 0–49) | Pass |
| Zero nonce duplicates within any epoch | Pass |
| Every epoch contains exactly 50 rounds, this capture’s cadence (134 / 134) — bound to the code constant, not the dataset header | Pass |
Every epoch opens at the post-rotation nonce LIQD reported (nonceStart = 0) — 134 / 134 | Pass |
| Same inputs produce the same result, exact on the two-decimal grid, for all 6,700 rounds | Pass |
| The recomputed result reproduces the served win flag under result ≥ target for all 6,700 rounds | Pass |
| Custom client seeds honoured: 500 / 500 Phase D rounds recompute; a changed seed changes the draw on 64 / 64 probes | Pass |
1.12Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-limbo.gitcd liqd-limbo && git checkout v1.0.1npm cinpm run verify# Expected output: Steps 1–7 and 18 all PASSSteps 1–7 and 18 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 & Epoch Coverage
[PASS] Step 4 — Nonce Audit
[PASS] Step 5 — Draw & Result Recomputation
[PASS] Step 6 — Win-Rule Reproduction
[PASS] Step 7 — Recomputation Determinism
[PASS] Step 18 — Custom Client-Seed Control (Phase D)RNG & Entropy Model
Is the randomness genuinely random, or could it be rigged?
This section verifies that LIQD’s Limbo random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. Each round makes one HMAC-SHA256 draw — the first 4-byte chunk of the digest below the maxFair ceiling, reduced modulo 100,000,000 to a draw k from 0 to 99,999,999, with bias-free rejection sampling — and max(1, floor(9,900,000,000 ÷ (k + 1)) ÷ 100) turns the draw into the result, never below 1×. The implementation is our own, written from a specification without LIQD’s code; it reproduces all 6,700 live results, and LIQD’s own verifier serves only as a cross-check.
What we verified
- Each round’s draw comes from HMAC-SHA256, a standard keyed hash whose output cannot be predicted without the server seed
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Rejection sampling removes modulo bias exactly — every one of the 100,000,000 draws is equally likely, and 126 captured rounds show LIQD applies the guard
- The RNG is our own implementation, written from the specification — it reproduces all 6,700 captured results
- Draw uniformity and serial independence pass their screens on 2,000,000 simulated draws, and 10 targets × 1,000,000 simulated rounds all land within 5 SD + 1 win of their exact win count (Step 23)
- A changed client seed changes the draw — 64 / 64 sensitivity probes, 500 / 500 custom-seed rounds
What this means for you
- Each result is generated fairly and cannot be skewed
- All 100,000,000 draws are equally likely — no result comes up more often than the published formula gives
- No hidden randomness or server-side tricks influence the result
- Consecutive rounds are not correlated — past results don’t affect future outcomes
- The algorithm depends only on seeds you can verify
- Each round’s draw comes from HMAC-SHA256, a standard keyed hash whose output cannot be predicted without the server seed
- Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
- Rejection sampling removes modulo bias exactly — every one of the 100,000,000 draws is equally likely, and 126 captured rounds show LIQD applies the guard
- The RNG is our own implementation, written from the specification — it reproduces all 6,700 captured results
- Draw uniformity and serial independence pass their screens on 2,000,000 simulated draws, and 10 targets × 1,000,000 simulated rounds all land within 5 SD + 1 win of their exact win count (Step 23)
- A changed client seed changes the draw — 64 / 64 sensitivity probes, 500 / 500 custom-seed rounds
- Each result is generated fairly and cannot be skewed
- All 100,000,000 draws are equally likely — no result comes up more often than the published formula gives
- No hidden randomness or server-side tricks influence the result
- Consecutive rounds are not correlated — past results don’t affect future outcomes
- The algorithm depends only on seeds you can verify
RNG derived only from disclosed inputsPass
Entropy purityPass
Our own implementationPass
docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 72 specification checks pass and it reproduces all 6,700 scored results exactly — 7,004 / 7,004 captured rounds in allLIQD verifier cross-checkPass
tests/limbo/rngTests.ts) and both replies of LIQD’s /verify endpoint at the boundary (Step 19) — a cross-check, not the sourceModulo biasPass
Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Limbo RNG uses only the disclosed inputs, and our own implementation — written from the specification — reproduces every captured result. The simulated draws are consistent with a uniform draw across the 100,000,000-point grid, the serial screens find no sign of dependence, and 10 targets simulated at 1,000,000 rounds each all land within 5 SD + 1 win of their exact win count, the worst at 1.56 SD (Step 23). The rejection guard is applied live — 126 captured rounds depend on it — and the client seed is a genuine input: adding one character to it alters the draw on every probe.
| Check | Result | Evidence |
|---|---|---|
| RNG derived only from disclosed inputs | Pass | HMAC-SHA256(serverSeed, clientSeed:nonce:0) — one draw per round, no hidden entropy |
| Entropy purity | Pass | No timestamps, external APIs, Math.random, or server-side state |
| Our own implementation | Pass | Written from the specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 72 specification checks pass and it reproduces all 6,700 scored results exactly — 7,004 / 7,004 captured rounds in all |
| LIQD verifier cross-check | Pass | Our implementation reproduces the 4 values read from LIQD’s Verify calculator (unit test tests/limbo/rngTests.ts) and both replies of LIQD’s /verify endpoint at the boundary (Step 19) — a cross-check, not the source |
| Modulo bias | Pass | Rejection ceiling 4,200,000,000 = 42 × 100,000,000 — bias 0, computed from the bound the draw itself applies; 126 captured rounds had their first chunk discarded and all 126 reproduce (Step 5) |
| Key encoding verified | Pass | Server seed hex-decoded to bytes for the HMAC key; the commitment hashes the seed text — both confirmed by recomputing every seed and every round (Steps 1, 5) |
| Serial independence | Pass | Lag-1 z = 0.937 against the applied critical 2.936; runs test p = 0.8679 — both clear at α/3 over 2,000,000 simulated draws (Step 23); the captured draws in capture order show no dependence either (lag-1 z −0.348, runs p 0.6423; Step 22, informational) |
| Client seed influence | Pass | A changed client seed changes the draw on 64 / 64 probes; all 500 Phase D custom-seed rounds recompute (Step 18) |
RNG derived only from disclosed inputsPass
Entropy purityPass
Our own implementationPass
docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 72 specification checks pass and it reproduces all 6,700 scored results exactly — 7,004 / 7,004 captured rounds in allLIQD verifier cross-checkPass
tests/limbo/rngTests.ts) and both replies of LIQD’s /verify endpoint at the boundary (Step 19) — a cross-check, not the sourceModulo biasPass
Key encoding verifiedPass
Serial independencePass
Client seed influencePass
The Limbo RNG uses only the disclosed inputs, and our own implementation — written from the specification — reproduces every captured result. The simulated draws are consistent with a uniform draw across the 100,000,000-point grid, the serial screens find no sign of dependence, and 10 targets simulated at 1,000,000 rounds each all land within 5 SD + 1 win of their exact win count, the worst at 1.56 SD (Step 23). The rejection guard is applied live — 126 captured rounds depend on it — and the client seed is a genuine input: adding one character to it alters the draw on every probe.
How It Works — RNG & Entropy Model8 sections
2.1RNG Function Implementation
Each Limbo round 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 of 4,200,000,000 is reduced % 100000000 to give the draw k, an integer in [0, 100000000). The result is max(1, floor(9,900,000,000 / (k + 1)) / 100) — two decimals, never below 1×. All eight chunks would be rejected about 5.7 × 10⁻¹⁴ of the time — effectively never; no captured round needed more than its third chunk. The target and stake never enter this function — they are applied to the result afterwards. The scored steps compute the same result map with resultMultiplier in src/config.ts, which also applies LIQD’s game-code maximum of 1,000,000×; the two agree on every draw except k ≤ 97 (98 in 100,000,000), where either value wins every target.
| Component | Detail |
|---|---|
| Hash function | HMAC-SHA256 (32-byte digest) |
| Key | hex_decode(serverSeed) — 16 raw bytes (distinct from the commitment domain, which hashes the seed text) |
| Message | clientSeed:nonce:cursor — UTF-8, literal colons, no padding; cursor = 0 |
| Extraction | 4-byte big-endian chunks → readUInt32BE → uint32, up to 8 per digest |
| Rejection | chunk < maxFair where maxFair = floor(2³² / 10⁸) × 10⁸ = 4,200,000,000 |
| Reduction | uint32 % 100000000 → draw k in [0, 100,000,000) |
| Result | max(1, floor(9,900,000,000 / (k + 1)) / 100) → two decimals, from 1× |
| Exhaustion path | All 8 chunks rejected → our implementation stops with an error (≈ 5.7 × 10⁻¹⁴ per round; no captured round needed more than its third chunk) |
| Output | One result per round — the target is applied to it afterwards |
function walkChunks(digest: Buffer, range: number): { index: number; r: number } | null { const limit = Math.floor(TWO_POW_32 / range) * range; for (let i = 0; i + CHUNK_BYTES <= DIGEST_BYTES; i += CHUNK_BYTES) { const v = digest.readUInt32BE(i); if (v < limit) { return { index: i / CHUNK_BYTES, r: v % range }; } } return null;} /** * §2 Steps 1-2: the accepted draw r in [0, range). A rejected chunk moves to the * next chunk of the same digest; the cursor is NOT advanced (§2 Step 2). If all * 8 chunks are rejected this throws (O1). */export function generateProvablyFairNumber( serverSeed: string, clientSeed: string, nonce: number, cursor: number, range: number,): number { assertRange(range); const hit = walkChunks(roundDigest(serverSeed, clientSeed, nonce, cursor), range); if (hit === null) { throw new Error( // … ); } return hit.r;}docs/ALGORITHM.md) by an implementer who never opened LIQD’s code, its client bundle or any port of it, and the specification’s 72 checks pass (tests/limbo/RngSpecTests.ts). LIQD’s own verifier is used only as a cross-check — our implementation reproduces the 4 values read from LIQD’s Verify calculator (tests/limbo/rngTests.ts) and both /verify replies (Step 19).2.2Entropy Sources
All randomness derives exclusively from the deterministic HMAC-SHA256 function combining three cleanly separated inputs — each one part of the round record or the seed record:
| Source | Controlled By | Purpose |
|---|---|---|
| Server Seed | Casino | Base randomness — the HMAC key, committed via SHA-256 hash of the seed text before betting |
| Client Seed | Player | Player-contributed entropy — mixed into every HMAC message |
| Nonce | System | Uniqueness per round (0–49 within each epoch in this capture, audited gap-free) |
| Cursor | System | Fixed at 0 — one draw per round. An index, not an entropy source |
How we know: 6,700 / 6,700 rounds 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 % 100000000. Modulo bias occurs because 2^32 is not evenly divisible by 100,000,000 — the 32-bit range holds 42 complete cycles of 100,000,000 plus a remainder of 94,967,296 values, so without a guard every draw below 94,967,296 would be reachable one extra time: 43 ways instead of 42, about 2.38% more often. The algorithm eliminates this bias entirely via rejection sampling: a ceiling maxFair = floor(2^32 / 100000000) × 100000000 ensures only unbiased chunks are used. Any chunk ≥ maxFair is discarded and the next 4-byte chunk of the same digest is tried:
range = 100,000,000
2^32 = 4,294,967,296
floor(2^32 / range) = 42
maxFair = 42 × 100,000,000 = 4,200,000,000
dead values = 2^32 − maxFair = 94,967,296
P(chunk rejected) = 94,967,296 / 2^32 ≈ 2.2111% (about one round in 45)
P(all 8 chunks rejected) = 0.022111^8 ≈ 5.7e-14
draws in this capture = 6,700 (one per round)
expected first-chunk rejections ≈ 6,700 × 2.2111% ≈ 148.15
observed = 126 rounds (128 chunks: 124 rounds took the second chunk, 2 the third)Below maxFair, each of the 100,000,000 draws owns exactly 42 chunk values — perfectly uniform. The 94,967,296 values at the top of the 32-bit space are discarded. Step 5 computes this bound from the same rule the draw applies (src/bias.ts, tied to src/rng.ts by an acceptance probe of 4,000 fixed rounds, 81 of which exercise the discarded range) and fails the audit if the accepted window is not a whole number of ranges. Limbo’s range makes the guard fire on about one round in 45, so the capture tests it live: 126 of the 6,700 rounds had their first chunk discarded, and our guarded draw reproduces the served result on all 126. A draw without the guard would have given a different result on every one of them — 2.8 traces the first.
2.4RNG Isolation
Each round uses a unique nonce in the HMAC message (clientSeed:nonce:0), ensuring per-round outputs are cryptographically independent. HMAC-SHA256 is a pseudorandom function — knowing one result gives no usable information about any other result under the same seed pair. There is no shared state between rounds or between epochs, and the target and stake have nowhere to enter: the draw is produced first, in isolation, and the target is applied to its result afterwards.
Evidence: the limboDraw function in 1.6 takes exactly three arguments (serverSeed, clientSeed, nonce) — no stake, no target, no timestamp, no account identifier — and returns a number. Same inputs always produce the same output; if any hidden input influenced the result, our 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 client seeds we chose — both recompute under the identical three-argument function (Steps 13, 18).
2.5Monte Carlo Simulation — Pass 1 and the 38M Reference Run
Two simulations check the algorithm at scale, both on seed pairs of our own — derived from recorded master seeds, independent of LIQD’s seeds and rotated every 50 draws, like the epochs of this capture — so each one replays byte for byte. Pass 1 (Step 23) is re-derived on every verify run: 2,000,000 draws binned into 100 cells for uniformity and screened for serial structure, and 10 targets from 1.01× to 100,000× at 1,000,000 rounds each, every one compared with its exact win chance. The reference run has three parts — 2,000,000 uniformity draws, a 20,000,000-round convergence run at 2.00× and 8 targets at 2,000,000 rounds each — 38,000,000 simulated rounds, regenerated byte for byte by npm run test:full; S4’s convergence chart is drawn from it.
| Pass 1 (Step 23) | Value |
|---|---|
| Draws and rounds | 2,000,000 uniformity draws + 10 targets × 1,000,000 rounds |
| Seeds | sha256(master | stream | block) from the recorded master seed pf.org/liqd-limbo/pass1/2026-09-26 — replayed byte for byte by npm run pass1:check |
| Draw uniformity (χ², 100 bins, df = 99) | χ² = 94.393, p = 0.6122 — pass at α/3 |
| Serial screen | lag-1 z = 0.937 < 2.936; runs test p = 0.8679 ≥ 0.00333 |
| Targets | 10 / 10 within 5 SD + 1 win of the exact win count; largest deviation 1.56 SD (10×) |
| Target | Wins of 1,000,000 | Expected | z | Simulated RTP | Exact RTP |
|---|---|---|---|---|---|
| 1.01× | 980,236 | 980,198.0 | +0.27 | 99.0038% | 98.999999% |
| 1.50× | 659,544 | 660,000.0 | −0.96 | 98.9316% | 99.000000% |
| 2× | 494,575 | 495,000.0 | −0.85 | 98.9150% | 99.000000% |
| 5× | 197,421 | 198,000.0 | −1.45 | 98.7105% | 99.000000% |
| 10× | 98,534 | 99,000.0 | −1.56 | 98.5340% | 99.000000% |
| 50× | 19,991 | 19,800.0 | +1.37 | 99.9550% | 99.000000% |
| 100× | 9,981 | 9,900.0 | +0.82 | 99.8100% | 99.000000% |
| 1,000× | 1,003 | 990.0 | +0.41 | 100.3000% | 99.000000% |
| 10,000× | 114 | 99.0 | +1.51 | 114.0000% | 99.000000% |
| 100,000× | 8 | 9.9 | −0.60 | 80.0000% | 99.000000% |
| Reference run | Value |
|---|---|
| Rounds | 38,000,000 — 2M uniformity draws + 20M at 2.00× + 8 targets × 2M |
| Seeds | sha256(master | stream | n) from the recorded master seed pf.org/liqd-limbo/simulate/2026-09-26 — regenerated byte for byte by npm run test:full |
| Draw uniformity (χ², 100 bins, df = 99) | χ² = 110.884, p = 0.1949 |
| Serial screen (first 200,000 draws) | lag-1 Z = −0.62 (critical 2.576); runs Z = −0.898, p = 0.3694 |
| RTP convergence (20M at 2.00×) | 99.0128% against 99% — 0.57 SE |
| Reference run target | Simulated RTP | Exact RTP | Distance |
|---|---|---|---|
| 1.1× | 99.0163% | 99.0000% | 0.7 SE |
| 1.5× | 99.0135% | 99.0000% | 0.27 SE |
| 2× | 98.9224% | 99.0000% | 1.1 SE |
| 5× | 98.9073% | 99.0000% | 0.66 SE |
| 10× | 99.2695% | 99.0000% | 1.28 SE |
| 20.14× | 99.2348% | 99.0000% | 0.76 SE |
| 100× | 98.5650% | 99.0000% | 0.62 SE |
| 1,000× | 97.5000% | 99.0000% | 0.67 SE |
/** Deterministic seed pair for (stream, block): 32-hex server seed (operator format), 16-hex client seed. */export function pass1Seeds(stream: string, block: number): { serverSeed: string; clientSeed: string } { const h = createHash('sha256').update(`${PASS1_MASTER_SEED}|${stream}|${block}`).digest('hex'); return { serverSeed: h.slice(0, 32), clientSeed: h.slice(32, 48) };} /** Draw i of a stream: epochs of PASS1_EPOCH nonces, a fresh seed pair per epoch (like the real game). */export function pass1Draw(stream: string, i: number): number { const { serverSeed, clientSeed } = pass1Seeds(stream, Math.floor(i / PASS1_EPOCH)); return generateProvablyFairNumber(serverSeed, clientSeed, i % PASS1_EPOCH, CURSOR, RANGE);}Step 23 decides every statistic itself, from the artifact’s integer rows — the 100-bin histogram, the integer sums behind the lag-1 and runs tests, and the win count of every target — and replays the first 50,000 draws and 20,000 rounds of every stream from the master seed on every run, so the rows are bound to the declared seeds. The three screens (uniformity, lag-1, runs) are each judged at α/3 = 0.00333, so the family has level α = 0.01; each target must land within 5 standard deviations plus one win of its exact win count, floor(9,900,000,000 / (100 × target)) of every 100,000,000 draws.
2.6Serial Independence
Serial independence ensures consecutive results are not correlated — a high result on one round does not make the next more or less likely to be high. Step 23 applies two tests to Pass 1’s 2,000,000 draws in stream order, each judged at the Bonferroni-corrected α/3 = 0.00333 (the third screen in the family is uniformity itself):
Lag-1 autocorrelation: The lag-1 autocorrelation of consecutive draws’ 100-bin index, r₁ = 0.000663, standardised as z = r₁ · √n = 0.937. The critical value is the two-sided normal quantile at α/3, 2.936; z clears it comfortably. Step 23 re-derives r₁ from the integer sums the artifact stores.
Wald-Wolfowitz runs test: Tests whether the sequence of draws above and below the midpoint has the expected number of runs: 1,000,117 runs against 999,999.4 expected, z = 0.166, two-sided p = 0.8679 — clear at α/3.
Reference run: The reference run screens the raw draws of its first 200,000 at α = 0.01: lag-1 Z = −0.62 (critical 2.576), runs Z = −0.898, p = 0.3694.
Captured draws (Step 22, informational): The 6,700 draws recomputed from the revealed seeds, in capture order: lag-1 z = −0.348, runs p = 0.6423; their fit to the uniform domain gives χ² p = 0.2315 and Kolmogorov–Smirnov p = 0.6985. These fits are reported for information rather than scored; every result is recomputed round by round in Step 5.
// 3. serial, re-derived from the integer sums const mean = Number(u.sumB) / n; const varSum = Number(u.sumB2) - n * mean * mean; const lagSum = Number(u.sumLag) - (n - 1) * mean * mean; const r1 = varSum > 0 ? lagSum / varSum : NaN; const lagZ = r1 * Math.sqrt(n); const n1 = Number(u.above), n0 = n - n1; const muR = (2 * n1 * n0) / n + 1; const varR = (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n * n * (n - 1)); const runsZ = (Number(u.runs) - muR) / Math.sqrt(varR); const runsP = twoSidedNormalP(runsZ); const perTest = PASS1_ALPHA / 3; const zCrit = inverseCriticalZ(perTest); const uniformOk = histOk && chi.pValue >= perTest; const serialOk = Number.isFinite(lagZ) && Math.abs(lagZ) < zCrit && Number.isFinite(runsP) && runsP >= perTest;2.7The Result Map & the 1× Floor
The draw k becomes the result through floor(9,900,000,000 / (k + 1)) / 100, lifted to 1× where it falls below: the smaller the draw, the higher the result. At the bottom of the scale, any draw above 98,999,999 — 1,000,000 of the 100,000,000, exactly 1.0000% — gives a raw value of 0.99, and LIQD serves it as 1×: 147 scored rounds show 1×, 78 of them lifted from 0.99, against 132.67 and 67.00 expected. The floor never changes a win or a loss, because every target is at least 1.01×. At the top, the formula reaches 99,000,000× at k = 0; LIQD’s game code names 1,000,000× as the maximum result, which only draws 0–97 would exceed — 98 in 100,000,000 — and a result that high wins every target either way. The highest captured result is 185,046.72×, from k = 534. The transitions:
| Draw k | Result | What it shows |
|---|---|---|
| 0 | 99,000,000× | the top of the scale — LIQD’s game code names 1,000,000× as the maximum; draws 0–97 exceed it and win every target either way |
| 98 | 1,000,000× | exactly 1,000,000.00× |
| 99 | 990,000× | the first draw below 1,000,000× |
| 534 | 185,046.72× | the highest captured result (bet S4mFs74B…) |
| 98,019,800 | 1.01× | the last draw above 1× |
| 98,019,801 | 1× | the first draw giving 1× — raw value 1.00, no lift |
| 98,999,999 | 1× | the last draw with raw value 1.00 |
| 99,000,000 | 1× | the first draw lifted (raw 0.99) |
| 99,999,999 | 1× | the bottom of the domain |
2.8Worked Example — Full RNG Trace
Real round from the dataset — bet krv88h79b6gDGRTDgswi9 (Phase A, epoch 5), nonce 2, target 1.59×: the earliest round in capture order whose first chunk was discarded. Verified from limbo-master-6700bets.json. One HMAC digest, eight candidate chunks — the first is at or above the ceiling, so the scan moves on to the second:
serverSeed = b73582d05dec4dd4a43b4e0b72ec3ebd
clientSeed = audited5df347a318
nonce = 2
cursor = 0
message = audited5df347a318:2:0
digest = fd8e759aaade3ebec782575f93f3c3fe4203937508034c42119a7c299ce951f6
maxFair = 4,200,000,000| chunk | bytes (hex) | uint32 | below 4,200,000,000? | draw k |
|---|---|---|---|---|
| 0 | fd8e759a | 4,253,971,866 | no — discarded | — |
| 1 | aade3ebe | 2,866,691,774 | yes — taken | 66,691,774 |
| 2 | c782575f | 3,347,208,031 | (not needed) | — |
| 7 | 9ce951f6 | 2,632,536,566 | (not needed) | — |
2,866,691,774 % 100,000,000 = 66,691,774, and floor(9,900,000,000 / 66,691,775) = 148, so the result is 1.48×, exactly as the live game recorded (Step 5). Against the 1.59× target that is a loss, and LIQD credited 0 (Steps 6, 10). Without the guard, chunk 0 would have given k = 53,971,866 and a 1.83× result — a win; LIQD served 1.48×, the guarded value.Technical Evidence & Verification5 sections
2.9Evidence Coverage Summary11 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Our implementation | 7,004 / 7,004 captured rounds, the 6,700 scored ones included — written from the specification; its 72 specification checks pass | Pass |
| Result recomputation (Step 5) | 6,700 / 6,700 — exact on the two-decimal grid, recomputed on every run | Pass |
| LIQD verifier cross-check | 4 / 4 Verify-calculator values (unit test) and 2 / 2 /verify replies (Step 19) reproduced | Pass |
| Key encoding (hex vs UTF-8) | Confirmed via recomputation — HMAC keys the decoded bytes, commitment hashes the text | Pass |
| Modulo bias analysis (Step 5) | Rejection ceiling 4,200,000,000; bias 0 from the draw’s own bound; 126 / 126 rounds with a discarded first chunk reproduce | Pass |
| External entropy non-participation (Step 5) | 6,700 / 6,700 reproduced from disclosed inputs alone | Pass |
| Draw uniformity (Step 23) | χ² = 94.393, df = 99, p = 0.6122 over 2,000,000 draws — pass at α/3 | Pass |
| Serial independence (Step 23) | lag-1 z = 0.937 < 2.936; runs p = 0.8679 (2,000,000 draws) | Pass |
| Win counts at 10 targets (Step 23) | 10 / 10 within 5 SD + 1 win of the exact count at 1,000,000 rounds each; worst 1.56 SD | Pass |
| Live-sample fit (Step 22, informational) | The 6,700 captured draws in order: χ² p = 0.2315, KS p = 0.6985, lag-1 z = −0.348, runs p = 0.6423 | Pass |
| Client seed influence (Step 18) | 500 / 500 Phase D + 64 / 64 sensitivity probes | Pass |
2.10Code References11 source files
| File | Purpose |
|---|---|
| src/rng.ts | HMAC-SHA256 single draw and result (commitHash, generateProvablyFairNumber, limboDraw, limboResult) — written from docs/ALGORITHM.md |
| docs/ALGORITHM.md | The specification the RNG was written from — inputs, commitment, derivation, win rule and payout, with the source of each step |
| src/bias.ts | The draw-bias bound and the acceptance probe that ties it to src/rng.ts (Step 5) |
| src/config.ts | Game constants, the scored result map (resultMultiplier) and the simulation constants |
| src/pass1-grid.ts | Pass 1 producer — seeds from the recorded master seed, integer rows only |
| tests/steps/pass1.ts | Step 23: re-derives every Pass 1 statistic from the rows and replays the prefixes |
| tests/steps/live-fit.ts | Step 22: fit of the 6,700 captured draws, in capture order (informational) |
| src/simulate.ts | The reference run (38M rounds) and its convergence chart |
| src/stats.ts | Chi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test, normal quantiles and tails |
| tests/limbo/RngSpecTests.ts | The specification’s 72 checks, run against src/rng.ts |
| tests/limbo/rngTests.ts | The 4 Verify-calculator values, the rejection branch, the commitment convention and client-seed sensitivity |
2.11Verified Invariants11 invariants
| Invariant | Result |
|---|---|
| HMAC-SHA256 output reproduces the live result for all 6,700 rounds | Pass |
| Key is hex-decoded (16 bytes, not UTF-8) — the wrong encoding produces wrong results | Pass |
| Message format is clientSeed:nonce:cursor with cursor 0 — literal colons, no padding | Pass |
| Chunks scanned big-endian, 4 bytes, first accepted wins | Pass |
| Rejection sampling eliminates modulo bias — maxFair = 42 × 100,000,000, so every draw owns exactly 42 chunk values | Pass |
| The guard is applied live — 126 / 126 captured rounds with a discarded first chunk reproduce | Pass |
| No external entropy sources required for result computation | Pass |
| Draw distribution consistent with uniform across the 100,000,000-point grid (χ² p = 0.6122, 2,000,000 draws) | Pass |
| Lag-1 z < 2.936 and runs p ≥ 0.00333 on 2,000,000 consecutive draws | Pass |
| Simulated wins within 5 SD + 1 win of the exact win count at 10 targets from 1.01× to 100,000× (worst 1.56 SD) | Pass |
| Client seed change produces a different draw on 64 / 64 probes; 500 / 500 custom-seed rounds recompute | Pass |
2.12Datasets UsedSource artifacts
2.13Reproduction InstructionsRun locally
Clone the repository, install dependencies, and regenerate both simulations before verifying:
git clone https://github.com/ProvablyFair-org/liqd-limbo.gitcd liqd-limbo && git checkout v1.0.1npm cinpm run test:full # unit tests, both simulations regenerated byte for byte, then the 25 stepscat outputs/pass1-grid.json | head -40S2-related steps:
[PASS] Step 5 — Draw & Result Recomputation
[PASS] Step 18 — Custom Client-Seed Control (Phase D)
[PASS] Step 19 — Artifact Integrity & Evidence Reproduction
[PASS] Step 22 — Live-Sample Fit
[PASS] Step 23 — Simulation Pass 1 — Pinned Master Seed, Re-Derived
The regenerated outputs/pass1-grid.json, outputs/simulation-results.json and outputs/rtp-convergence.html are byte-identical to the committed files (SHA-256 9999f98b…, 2d609344… and efd12115…). On our run of 22 October 2026 (Node 22.22.0) the whole of npm run test:full took 6 minutes 27 seconds.Verifier Parity
Does the live game actually follow its own rules?
This section validates that our recomputation produces the exact same result as the live game for every round — equal on the two-decimal grid, with no tolerance. It also re-derives every win flag from the recomputed result and the player’s target (result ≥ target), and confirms every credited amount is exactly stake × target — a win pays the target, never the result — in exact integer arithmetic, so no rounding rule is needed. Any mismatch would invalidate the fairness guarantee.
What we verified
- Every round independently recomputed from its seeds — the result itself verified exactly, not just the payout
- Payout correctness: all 2,822 wins credit exactly stake × target, checked in exact integers with zero tolerance; all 3,878 losses pay 0
- Win/loss re-derived from the recomputed result and the target for every round — 0 mislabelled settlements; the 20 rounds that landed exactly on their target were all paid as wins
- Bet amount is not an input to the RNG — the result depends only on the seeds and the nonce
- All four capture phases recomputed identically (random targets, the 100×–500,000× tail, a $10.00 stake, client seeds we chose)
What this means for you
- The verifier isn’t a simulation — it produces the exact same result as the live game
- Every round you play can be independently recomputed by anyone
- No hidden logic alters outcomes based on how much you bet or which target you choose
- A win pays exactly your stake × your target — never more, never less
- The game LIQD served in the capture follows the specified algorithm exactly
- Every round independently recomputed from its seeds — the result itself verified exactly, not just the payout
- Payout correctness: all 2,822 wins credit exactly stake × target, checked in exact integers with zero tolerance; all 3,878 losses pay 0
- Win/loss re-derived from the recomputed result and the target for every round — 0 mislabelled settlements; the 20 rounds that landed exactly on their target were all paid as wins
- Bet amount is not an input to the RNG — the result depends only on the seeds and the nonce
- All four capture phases recomputed identically (random targets, the 100×–500,000× tail, a $10.00 stake, client seeds we chose)
- The verifier isn’t a simulation — it produces the exact same result as the live game
- Every round you play can be independently recomputed by anyone
- No hidden logic alters outcomes based on how much you bet or which target you choose
- A win pays exactly your stake × your target — never more, never less
- The game LIQD served in the capture follows the specified algorithm exactly
Result recomputationPass
Win-rule reproductionPass
result ≥ target on the recomputed result; all 20 rounds that landed exactly on their target were settled as wins (18.24 expected) (Step 6)Multiplier derivationPass
multiplier equals the target on all 2,822 wins, bit-exact (Step 8); our capture writes that column, and LIQD’s credited amount confirms every win paid the target, never the result (Step 9)Credit arithmeticPass
credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ with zero tolerance — stake × target to the last unit, no rounding rule needed; 3,878 losses pay exactly 0 (Step 9)Wallet-field reconciliationPass
stakeWallet / winningAmountWallet in LIQD’s settle response equal the game ledger on 6,700 / 6,700 (Step 11)Bet-size independencePass
Mode & phase completenessPass
All 6,700 rounds matched our recomputation exactly — results, win flags and credited amounts, across all four capture phases and both stakes. Every win paid exactly stake × target, the target the player chose and never the result, and every loss paid zero, with the money checked in exact integers.
| Check | Result | Evidence |
|---|---|---|
| Result recomputation | Pass | 6,700 / 6,700 exact — the result recomputed from the revealed seeds equals the served result on the two-decimal grid, strict equality, no tolerance; the 126 rounds decided by a later chunk and the 147 at the 1× floor included (Step 5) |
| Win-rule reproduction | Pass | 6,700 / 6,700 served win flags reproduced by result ≥ target on the recomputed result; all 20 rounds that landed exactly on their target were settled as wins (18.24 expected) (Step 6) |
| Multiplier derivation | Pass | The round record’s multiplier equals the target on all 2,822 wins, bit-exact (Step 8); our capture writes that column, and LIQD’s credited amount confirms every win paid the target, never the result (Step 9) |
| Credit arithmetic | Pass | All 6,700 credits exact: on 2,822 wins credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ with zero tolerance — stake × target to the last unit, no rounding rule needed; 3,878 losses pay exactly 0 (Step 9) |
| Wallet-field reconciliation | Pass | 3,878 losses pay 0 at multiplier 0, each with result < target (Step 10); stakeWallet / winningAmountWallet in LIQD’s settle response equal the game ledger on 6,700 / 6,700 (Step 11) |
| Bet-size independence | Pass | Bet amount is absent from the RNG input — the same draw function at $0.10 and $10.00; 200 / 200 Phase C rounds settle by the same rule (Step 13), and all 6,700 settlements follow from seeds, nonce, target and stake alone (Step 20) |
| Mode & phase completeness | Pass | One bet type, a target; all four planned phases at their planned counts and stakes (A 5,000 · B 1,000 · C 200 · D 500), Phase B in four tranches of 250 (Step 13); with the 26 September supplement, Phase A has at least 50 rounds in every target decade up to 10,000× (Step 25) |
Result recomputationPass
Win-rule reproductionPass
result ≥ target on the recomputed result; all 20 rounds that landed exactly on their target were settled as wins (18.24 expected) (Step 6)Multiplier derivationPass
multiplier equals the target on all 2,822 wins, bit-exact (Step 8); our capture writes that column, and LIQD’s credited amount confirms every win paid the target, never the result (Step 9)Credit arithmeticPass
credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ with zero tolerance — stake × target to the last unit, no rounding rule needed; 3,878 losses pay exactly 0 (Step 9)Wallet-field reconciliationPass
stakeWallet / winningAmountWallet in LIQD’s settle response equal the game ledger on 6,700 / 6,700 (Step 11)Bet-size independencePass
Mode & phase completenessPass
All 6,700 rounds matched our recomputation exactly — results, win flags and credited amounts, across all four capture phases and both stakes. Every win paid exactly stake × target, the target the player chose and never the result, and every loss paid zero, with the money checked in exact integers.
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 draw and the result from raw inputs with our own code, re-derives the win flag from the player’s target, and recomputes the credit as stake × target — 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.
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 the everyday range of targets, the high-multiplier tail, bet-size invariance and client seeds we chose. A supplement captured on 26 September 2026 adds Phase A rounds in the higher target decades.
| Phase | Rounds | Target | Bet | Epochs | Purpose |
|---|---|---|---|---|---|
| A — Target coverage | 5,000 | Random targets 1.01×–49.45× (win chance drawn between 2% and 98%) | $0.10 | 0–99 | Verify the draw, the result map and the win rule across the everyday range of targets |
| B — High-multiplier tail | 1,000 | 250 each at 100×, 1,000×, 100,000× and 500,000× | $0.10 | 100–119 | Maximum variance; the largest potential payouts in the capture, up to the $50,000 limit |
| C — Stake equivalence | 200 | 2.00× | $10.00 | 120–123 | Confirm bet amount is not an RNG input (100× the $0.10 stake) |
| D — Client-seed control | 500 | Random targets 1.01×–48.50× under 10 client seeds we chose (pfaudit-) | $0.10 | 124–133 | Confirm LIQD uses the client seed the player sets |
| Supplement (26 September 2026) | 300 | 100 at 100×–999.99×, 100 at 1,000×–9,999.99×, 100 at exactly 10,000× | $0.10 | 6 of its own | Phase A coverage of every target decade up to 10,000× — scored by Step 25 |
3.3Result Recomputation (Step 5)
For every round, the audit independently recomputes the draw and the result from the epoch’s revealed server seed, the client seed and the nonce — limboDraw(serverSeed, clientSeed, nonce) for the draw k (one HMAC-SHA256 digest keyed with the hex-decoded server seed over clientSeed:nonce:0, its first 4-byte chunk below 4,200,000,000, reduced modulo 100,000,000) and resultMultiplier(k) for the result, which on every captured draw is max(1, floor(9,900,000,000 ÷ (k + 1)) ÷ 100) (the function also holds LIQD’s game-code maximum of 1,000,000×, which only draws 0–97 would exceed, 2.7) — and compares it with the served result. The comparison is strict: the served value must be a two-decimal value inside the result domain and equal to the recomputed value — no tolerance. Every round counts, including the 126 whose first chunk was discarded by the bias guard (2.3) and the 147 at the 1× floor, 78 of them lifted from a raw 0.99 (2.7).
for (const b of bets) { const served = Number(b.result); // … if (!Number.isFinite(served) || served < MIN_RESULT || served > MAX_MULTIPLIER || !isTwoDecimal(served)) rangeViolations++; const ss = seedMap.get(b.hashedServerSeed); if (!ss) { unrevealed++; continue; } const k = limboDraw(ss, b.clientSeed, b.nonce); if (!Number.isInteger(k) || k < 0 || k >= RANGE) rangeViolations++; const rej = rejectedLeadingChunks(ss, b.clientSeed, b.nonce, RANGE); chunksRejected += rej; const local = resultMultiplier(k); if (rej > 0) { roundsFirstChunkRejected++; if (!servedResultMatches(served, local)) rejectedMismatch++; } if (!servedResultMatches(served, local)) { mismatches++; if (!sample) sample = `epoch ${b.epoch} nonce ${b.nonce}: local ${local} vs served ${b.result}`; } if (served === MIN_RESULT) flooredRounds++; if (Math.floor(RESULT_NUMERATOR / (k + 1)) / MULTIPLIER_SCALE < MIN_RESULT) clampedRaw++; // … checked++; }3.4Paid Multiplier & Credit Arithmetic (Steps 8, 9)
A Limbo win pays the target the player chose, never the result: the capture’s largest result, 185,046.72× on a 1.17× target, paid 1.17×. Step 8 checks the round record’s multiplier column — the column our capture script writes, the target on a win and 0 on a loss — against the target on all 2,822 wins, bit-exact; Step 9 checks the payment itself. Step 9 replays the money from LIQD’s credited amount, winningAmount: on every win the credit must be the exact decimal product of stake and target, asserted as the integer identity credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ with zero tolerance, and on every loss it must be exactly 0. A two-decimal stake times a two-decimal target is already exact to four decimals, so no rounding rule is needed. All 6,700 reconcile: 2,822 wins on targets from 1.01× to 100×, the largest credit $20.00 (a Phase C win, $10.00 on 2.00×), and 3,878 losses at 0. The same credits also equal ROUND_8(stake × target) computed in binary64, the form a re-implementer would write, on 2,822 / 2,822 wins; against the raw binary64 product the credit lands exactly on 1,472 of them and differs on the rest by at most 4.44e-16 — a property of binary64 itself, which is why Step 9 compares integers.
| Metric | Count |
|---|---|
Wins where the record’s multiplier equals the target, bit-exact (Step 8) | 2,822 / 2,822 |
Wins where credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ (Step 9) | 2,822 / 2,822 |
| Losses crediting exactly 0 (Step 9) | 3,878 / 3,878 |
Wins where credit == ROUND_8(stake × target) in binary64 | 2,822 / 2,822 |
| Targets won · largest credit | 1.01× to 100× · $20.00 (Phase C, $10.00 × 2.00) |
| Max |credit − binary64 stake × target| · net across the capture | 4.44e-16 · −2.33e-14 USDC (binary64 representation, not the settlement) |
for (const b of bets) { const stake = Number(b.betAmount); const target = Number(b.params.target); const credit = Number(b.winningAmount); cChecked++; if (b.win) { const lhs = Math.round(credit * 1e8); const rhs = hundredths(stake) * hundredths(target) * 1e4; if (lhs !== rhs) { intFails++; if (!firstCFail) firstCFail = `epoch ${b.epoch} nonce ${b.nonce}: credit ${credit} vs exact decimal ${rhs / 1e8}`; } if (credit !== round8(stake * target)) r8Fails++; const raw = credit - stake * target; netResidual += raw; if (Math.abs(raw) > maxRawResidual) maxRawResidual = Math.abs(raw); if (raw === 0) landsOnProduct++; } else { if (credit !== 0) { intFails++; if (!firstCFail) firstCFail = `epoch ${b.epoch} nonce ${b.nonce}: losing bet credited ${credit}`; } } }3.5Loss Settlement & Wallet Fields (Steps 10, 11)
Step 10 checks every losing round in isolation: winningAmount must be exactly 0 and the round record’s multiplier exactly 0 — and, the part that matters, the round’s result must genuinely have missed the target (result < target, on the served result, which Step 5 shows equals the recomputed one), so no round that reached its target could have been labelled a loss. 3,878 / 3,878 conform, and resultLabel agrees with the win flag on every round. Step 11 reads the wallet-denominated fields of LIQD’s settle response: stakeWallet must equal betAmount and winningAmountWallet must equal winningAmount — present on all 6,700 rounds and equal on all 6,700, 0 divergent. It also confirms every stake is at or above LIQD’s $0.10 minimum (its client configuration) and every round’s potential payout, stake × target, sits within LIQD’s $50,000 limit — compared in whole hundredths, because for 427 of the capture’s 1,105 distinct stake × target pairs the binary64 product is not the exact decimal (0.1 × 1.07 = 0.10700000000000001).
| Scope | Coverage | Verified |
|---|---|---|
Losses — winningAmount == 0 and multiplier == 0 | 3,878 rounds | 3,878 / 3,878 |
Losses genuinely missed — result < target (served result = recomputed, Step 5) | 3,878 rounds | 3,878 / 3,878 |
resultLabel agrees with the win flag | 6,700 rounds | 6,700 / 6,700 (3,878 lost · 2,822 won) |
Wallet fields == game fields (stakeWallet = betAmount, winningAmountWallet = winningAmount) | 6,700 rounds | 6,700 / 6,700 (present on every round, 0 divergent) |
| Stake at or above LIQD’s $0.10 minimum | 6,700 rounds | 0 below |
| Potential payout (stake × target) within $50,000, in whole hundredths | 6,700 rounds | 0 over — maximum exactly $50,000, on 250 rounds |
3.6Settlement Determinism (Step 20)
Steps 8 and 9 check the round record and the credit against the target. Step 20 takes the served outcome out of the picture: for every round, the outcome and the credited amount are rebuilt from (serverSeed, clientSeed, nonce, target, stake) alone — the draw and the result from the seeds, the win flag from result ≥ target, the credit as stake × target in exact integers, 0 on a loss — and must match the served settlement with zero tolerance. The win flag, the credit and the record’s multiplier column are rebuilt from the inputs and only then compared with what was served and credited, so a settlement chosen after the draw has nowhere to hide. 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: 2,822 wins and 3,878 losses, both stakes, all four phases, the 147 rounds at the 1× floor and the 20 rounds that landed exactly on their target — the two cases a naive model gets wrong.
// The whole chain, end to end, from inputs a player controls or can check: // (serverSeed, clientSeed, nonce) -> k -> R -> win -> credit // and NOTHING served is read on the way. `b.result`, `b.multiplier`, `b.win` and `b.resultLabel` // are all excluded here; they are only compared at the end. Steps 5/6/8/9 each pin one link // against its served field — this step asserts the whole chain is determined, so there is no // degree of freedom left for an operator to exercise after seeing the draw. // … for (const b of bets) { const ss = seedMap.get(b.hashedServerSeed); if (!ss) { bad++; fails.push(`epoch ${b.epoch} nonce ${b.nonce}: no revealed seed`); continue; } chk++; // … const target = Number(b.params.target); const stake = Number(b.betAmount); const R = resultMultiplier(limboDraw(ss, b.clientSeed, b.nonce)); const win = limboWin(R, target); if (win) wins++; else losses++; // … // determined credit, in the operator's own decimal number system const determinedCreditH8 = win ? hundredths(stake) * hundredths(target) * 1e4 : 0; const creditedH8 = Math.round(Number(b.winningAmount) * 1e8); const determinedMultiplier = win ? target : 0; if (win !== !!b.win || creditedH8 !== determinedCreditH8 || Number(b.multiplier) !== determinedMultiplier) { bad++; if (fails.length < 3) fails.push(`epoch ${b.epoch} nonce ${b.nonce}: determined (win=${win}, credit=${determinedCreditH8 / 1e8}, mult=${determinedMultiplier}) vs served (win=${b.win}, credit=${b.winningAmount}, mult=${b.multiplier})`); } // … }3.7Phase C — Bet-Size Invariance (Steps 13, 20)
Phase C placed 200 rounds at $10.00 on a 2.00× target — 100× the $0.10 stake used in every other phase. All 200 results were recomputed exactly from revealed seeds (0 mismatches), and Step 13 settles each one again from primitives: 0 settled differently from the rule a $0.10 round follows (result, win flag, or a credit of exactly stake × target in integer hundredths). The structural point: limboDraw takes the server seed, the client seed and the nonce — no stake — so the result cannot depend on the bet amount, and the 2.00× target returns exactly 99.000000% at every stake. Beyond the settlement multiplication, the stake enters only LIQD’s limits — the $0.10 minimum and the $50,000 ceiling on stake × target (Step 11) — never the draw, and the only stakes in the dataset are exactly $0.10 and $10.00.
3.8Worked Example — Full Parity Verification
Real round from Phase C — bet pFaN6MRWzGVvE8DpQBMY8 (epoch 120), nonce 2, target 2.00× at $10.00. Verified from limbo-master-6700bets.json — one of the 104 Phase C wins, and at $20.00 the largest credit in the capture:
serverSeed = 141a7e0ab87055e753e0f8d289bbec1a
clientSeed = auditdc525d958d88
nonce = 2
target = 2.00×
betAmount = $10.00
recorded = result 2.38 · win true · resultLabel won · winningAmount 20 · winningAmountWallet 20
commitment = d56deeab14ebfe68…| Step | Process | Output |
|---|---|---|
| 1 | Recompute the commitment — SHA-256(utf8(serverSeed)) | Matches the pre-published hash d56deeab14eb… |
| 2 | Scan the HMAC digest of auditdc525d958d88:2:0 | First chunk 0x6dc1f472 = 1,841,427,570 — below 4,200,000,000, accepted |
| 3 | Reduce it modulo 100,000,000 | k = 41,427,570 |
| 4 | Result max(1, floor(9,900,000,000 ÷ 41,427,571) ÷ 100) | 2.38× — equals the recorded result |
| 5 | Apply result ≥ target against 2.00× | Win — the won label is correct |
| 6 | Credit stake × target, 10.00 × 2.00 | $20.00 — the credited winningAmount and winningAmountWallet (both stored as 20) |
commitHash(serverSeed) → d56deeab14eb… matches the published commitment ✓HMAC-SHA256(seed, "auditdc525d958d88:2:0") → 6dc1f472c6a163b3…chunk 0 = 0x6dc1f472 = 1,841,427,570 < 4,200,000,000 → k = 1,841,427,570 mod 100,000,000 = 41,427,570 ✓floor(9,900,000,000 / 41,427,571) = 238 → result = max(1, 238 / 100) = 2.38× (the recorded result) ✓2.38 ≥ 2.00 → won ✓credit = 10.00 × 2.00 = 20.00 → 1,000 × 200 × 10,000 = 2,000,000,000 = credit × 10⁸ ✓Technical Evidence & Verification5 sections
3.9Evidence Coverage Summary12 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Result recomputation (Step 5) | 6,700 / 6,700 rounds — exact on the two-decimal grid, strict equality | Pass |
| Win-rule reproduction (Step 6) | 6,700 / 6,700 win flags under result ≥ target; 20 / 20 on-target rounds paid as wins | Pass |
| Recomputation determinism (Step 7) | Two forward passes and one reverse pass agree on 6,700 / 6,700; 204 rounds recomputed 5 times each, 0 unstable | Pass |
| Paid multiplier (Step 8) | 2,822 / 2,822 wins — the record’s multiplier equals the target | Pass |
| Credit arithmetic (Step 9) | 6,700 / 6,700 — wins exactly stake × target in integers, losses 0, zero tolerance | Pass |
| Loss settlement (Step 10) | 3,878 / 3,878 losses pay 0, each with result < target | Pass |
| Wallet fields, stake & exposure (Step 11) | 6,700 / 6,700 wallet fields equal the game fields; 0 stakes below $0.10; 0 rounds over $50,000 | Pass |
| Dataset integrity & population (Step 12) | SHA-256 pin match; 6,700 rounds · 134 seeds · 50 per epoch, bound to code constants; 6,700 distinct bet ids | Pass |
| Phase plan conformance (Step 13) | A 5,000 · B 1,000 · C 200 · D 500 at planned stakes; Phase B 250 per tranche; Phase C 200 / 200 settle by the same rule | Pass |
| Phase D client seeds (Step 18) | 500 rounds, 10 client seeds we chose — 500 / 500 match | Pass |
| Settlement determinism (Step 20) | 6,700 / 6,700 rebuilt from (serverSeed, clientSeed, nonce, target, stake) alone | Pass |
| Supplement & target decades (Step 25) | 300 / 300 supplement rounds recompute; Phase A has at least 50 rounds in every decade up to 10,000× | Pass |
3.10Code References8 source files
| File | Purpose |
|---|---|
| tests/steps/determinism.ts | Steps 5–7: Result recomputation, win-rule reproduction and recomputation determinism |
| tests/steps/payouts.ts | Steps 8–11: Paid multiplier, credit arithmetic, loss settlement, wallet fields, stake floor and exposure limit |
| tests/steps/dataset.ts | Steps 12–13: Dataset integrity and phase plan conformance — population bound to src/config.ts |
| tests/steps/phase-d.ts | Step 18: Phase D client seeds and client-seed sensitivity |
| tests/steps/standardization.ts | Step 20: Settlement determinism |
| tests/steps/supplement.ts | Step 25: The 26 September supplement and the server’s target limit |
| src/config.ts | resultMultiplier, limboWin, targetToHundredths, isTargetLegal, round8 — the formulas every round is replayed against |
| src/rng.ts | limboDraw and rejectedLeadingChunks — the draw, written from the specification |
3.11Datasets UsedSource artifacts
3.12Verified Invariants13 invariants
| Invariant | Result |
|---|---|
| Recomputed result equals the served result exactly for all 6,700 rounds | Pass |
result ≥ target on the recomputed result reproduces the served win flag for all 6,700 rounds | Pass |
| The 20 rounds landing exactly on their target are settled as wins (≥, not >) | Pass |
| Every win pays the target, never the result — the credit equals stake × target on all 2,822 wins (Step 9); the record’s multiplier column equals the target on all 2,822 (Step 8) | Pass |
| credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ for all 2,822 wins, zero tolerance | Pass |
| winningAmount == 0 and multiplier == 0 for all 3,878 losses, each with result < target | Pass |
| Wallet fields of the settle response equal the game fields on all 6,700 rounds | Pass |
| Credit is fully determined by (serverSeed, clientSeed, nonce, target, stake) — no served outcome field read — for all 6,700 rounds | Pass |
| Phase C ($10.00) results derive from the identical three-input draw as $0.10 rounds | Pass |
| No hidden inputs beyond (serverSeed, clientSeed, nonce) — stakes present are exactly {$0.10, $10.00} | Pass |
| All four phases at planned counts and stakes; 1,104 distinct targets from 1.01× to 500,000× | Pass |
| Phase D: 500 / 500 results verified across 10 client seeds we chose | Pass |
| Every stake ≥ $0.10 and every potential payout ≤ $50,000 | Pass |
3.13Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-limbo.gitcd liqd-limbo && git checkout v1.0.1npm cinpm run verify# Expected output: Steps 5, 6, 7, 8, 9, 10, 11, 12, 13, 18, 20, 25 all PASSS3-related steps:
[PASS] Step 5 — Draw & Result Recomputation
[PASS] Step 6 — Win-Rule Reproduction
[PASS] Step 7 — Recomputation Determinism
[PASS] Step 8 — Paid-Multiplier Derivation
[PASS] Step 9 — Credit Arithmetic
[PASS] Step 10 — Loss Settlement
[PASS] Step 11 — Wallet Ledger, Stake Floor & Exposure Ceiling
[PASS] Step 12 — Dataset Integrity & Population Identity
[PASS] Step 13 — Phase Plan Conformance
[PASS] Step 18 — Custom Client-Seed Control (Phase D)
[PASS] Step 20 — Settlement Determinism
[PASS] Step 25 — Target-Decade Supplement & Live Target Ceiling …RTP & Payout Logic
Is the house edge what the casino claims?
This section verifies that LIQD’s advertised 1.00% house edge holds at every target a player can bet. The key test is anti-circularity: we prove the RTP from first principles, not from any casino-supplied figure — a target m wins on exactly ⌊9,900,000,000 ÷ (100 · m)⌋ of the 100,000,000 equally likely draws and pays m, so its RTP is that count × 100 · m ÷ 10¹⁰: never above 99%, and up to 10,000× never more than 0.009982 pp below it. We check the identity on all 1,104 captured targets, hold LIQD’s published 99% to every one of the 999,900 targets the server accepts under the RTP claim rule, compare it against 12 million simulated rounds, and test whether the casino pre-selected seeds against the player.
What we verified
- The house edge is at least 1.00% at every target; up to 10,000× it is exactly 1.00% on 282 targets and never more than 1.009982%
- RTP proven from first principles: a target m wins on ⌊9,900,000,000 ÷ 100m⌋ of the 100,000,000 draws and pays m — derived from the result map, checked as an integer identity on all 1,104 captured targets and confirmed by full-grid tallies at four of them
- LIQD’s published 99% holds on all 999,900 targets the server accepts — worst gap −0.009982 pp against the ±0.1 pp band of the RTP claim rule
- Every win pays exactly stake × target — no rounding at settlement, so the return at each target is exactly its win chance × target
- 12,000,000 simulated rounds: uniformity and serial screens pass, and 10 targets from 1.01× to 100,000× all land within 5 SD + 1 win of their exact win count (Step 23)
- Cherry-pick test: across LIQD’s 134 seeds, the captured windows rank among each seed’s other 199 windows as chance predicts (rank-sum Z −0.1711 against a −3.090 threshold) — no sign of seed selection; the test catches a selection putting 16.1% of epochs’ captured windows in their worst decile at least half the time
- Bet amount does not influence the result — confirmed at $0.10 and $10.00
What this means for you
- The house edge on Limbo is 1.00% to 1.01% at every target the server accepts, whatever target or bet size you choose
- The 99% LIQD advertises is what every target returns, within a hundredth of a percentage point
- 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 result
- A win pays exactly your stake × your target — a result above your target doesn’t change the payout
- High targets win rarely and pay large — the 99% is the long-run average at every target
- The house edge is at least 1.00% at every target; up to 10,000× it is exactly 1.00% on 282 targets and never more than 1.009982%
- RTP proven from first principles: a target m wins on ⌊9,900,000,000 ÷ 100m⌋ of the 100,000,000 draws and pays m — derived from the result map, checked as an integer identity on all 1,104 captured targets and confirmed by full-grid tallies at four of them
- LIQD’s published 99% holds on all 999,900 targets the server accepts — worst gap −0.009982 pp against the ±0.1 pp band of the RTP claim rule
- Every win pays exactly stake × target — no rounding at settlement, so the return at each target is exactly its win chance × target
- 12,000,000 simulated rounds: uniformity and serial screens pass, and 10 targets from 1.01× to 100,000× all land within 5 SD + 1 win of their exact win count (Step 23)
- Cherry-pick test: across LIQD’s 134 seeds, the captured windows rank among each seed’s other 199 windows as chance predicts (rank-sum Z −0.1711 against a −3.090 threshold) — no sign of seed selection; the test catches a selection putting 16.1% of epochs’ captured windows in their worst decile at least half the time
- Bet amount does not influence the result — confirmed at $0.10 and $10.00
- The house edge on Limbo is 1.00% to 1.01% at every target the server accepts, whatever target or bet size you choose
- The 99% LIQD advertises is what every target returns, within a hundredth of a percentage point
- 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 result
- A win pays exactly your stake × your target — a result above your target doesn’t change the payout
- High targets win rarely and pay large — the 99% is the long-run average at every target
Anti-circularityPass
House edge auditPass
RTP claim (RTP-CLAIM 1.2.0)Pass
Simulated RTP (Pass 1)Pass
Cherry-pick test (Pass 2)Pass
Bet-size invariancePass
Payout formulaPass
Target range & exposure ceilingPass
Every target from 1.01× to 10,000× returns between 98.990018% and 99.000000%, proven exactly from the draw grid — a target m wins on ⌊9,900,000,000 ÷ 100m⌋ of the 100,000,000 equally likely draws and pays m — so LIQD’s published 99% holds at all 999,900 targets the server accepts, within 0.01 pp. The house edge is at least 1.00% at every target and never favours the player. This is a first-principles proof, not a statistical estimate; 12 million simulated rounds agree with it, the cherry-pick test finds no sign of seed selection, and every win is paid exactly stake × target, with no settlement rounding.
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | Win probability derived from the draw grid, never read from LIQD — a target m wins on exactly ⌊9,900,000,000 ÷ 100m⌋ of the 100,000,000 draws: an integer identity on all 1,104 captured targets, anchored by full-grid tallies at 1.01×, 2×, 100× and 500,000×; the negative control accepts 2.47× and rejects 2.48×, NaN and Infinity on the same round (Steps 14, 15) |
| House edge audit | Pass | Edge ≥ 1.00% asserted in exact integers — ⌊9,900,000,000 ÷ ci⌋ × ci ≤ 9,900,000,000 — at all 99,999,900 targets — 0 violations, 0 player-favourable; up to 10,000× exactly 1.00% on 282 targets and at most 1.009982%, at 9,989.91× (Step 16) |
| RTP claim (RTP-CLAIM 1.2.0) | Pass | LIQD publishes 99% RTP (Edge 1.00%) on the Limbo game page; all 999,900 targets from 1.01× to 10,000× return 98.990018%–99.000000% — worst gap −0.009982 pp against the ±0.1 pp band (Step 16) |
| Simulated RTP (Pass 1) | Pass | 12,000,000 rounds — draw uniformity p = 0.6122, serial screens clear at α/3, 10 / 10 targets from 1.01× to 100,000× within 5 SD + 1 win of their exact win count, worst 1.56 SD (Step 23) |
| Cherry-pick test (Pass 2) | Pass | Played-target return over 134 seeds × 10,000 nonces: the captured windows rank among their seeds’ other 199 windows as chance predicts — rank-sum Z = −0.1711 (one-sided p = 0.4321) against the −3.090 rule, no cherry-picking indicated; a selection putting 16.1% of epochs’ captured windows in their worst decile is caught at least half the time (Step 24) |
| Bet-size invariance | Pass | Bet amount is not an input to the RNG — the same multiplier at every stake; 200 / 200 Phase C rounds at $10.00 settle by the same rule (Step 13), and all 6,700 settlements follow from seeds, nonce, target and stake (Step 20) |
| Payout formula | Pass | A win pays stake × target, never the result: every credit is exactly stake × target in integers, zero tolerance (Step 9), and the record’s multiplier column equals the target on all 2,822 wins (Step 8) |
| Target range & exposure ceiling | Pass | Stake × target within LIQD’s $50,000 limit on all 6,700 rounds (Step 11); the 1,000,000× target in LIQD’s client code could not be bought at any allowed stake (Step 17); the server accepts 1.01×–10,000× and refused all four higher targets probed on 2 October 2026 (Step 25) |
Anti-circularityPass
House edge auditPass
RTP claim (RTP-CLAIM 1.2.0)Pass
Simulated RTP (Pass 1)Pass
Cherry-pick test (Pass 2)Pass
Bet-size invariancePass
Payout formulaPass
Target range & exposure ceilingPass
Every target from 1.01× to 10,000× returns between 98.990018% and 99.000000%, proven exactly from the draw grid — a target m wins on ⌊9,900,000,000 ÷ 100m⌋ of the 100,000,000 equally likely draws and pays m — so LIQD’s published 99% holds at all 999,900 targets the server accepts, within 0.01 pp. The house edge is at least 1.00% at every target and never favours the player. This is a first-principles proof, not a statistical estimate; 12 million simulated rounds agree with it, the cherry-pick test finds no sign of seed selection, and every win is paid exactly stake × target, with no settlement rounding.
How It Works — RTP & Payout Logic9 sections
4.1Anti-Circularity Proof (Steps 14, 15)
The anti-circularity proof establishes each target’s RTP from first principles without using any casino-supplied probability. The win probability never comes from LIQD: it is derived from the result map over the grid of 100,000,000 equally likely draws (2.3). Because the result is max(1, floor(N ÷ (k + 1)) ÷ 100) with N = 9,900,000,000, and every target is at least 1.01×, a result reaches target m exactly when k ≤ ⌊N ÷ ci⌋ − 1, with ci = 100 · m — so target m wins on exactly ⌊N ÷ ci⌋ of the 100,000,000 draws, and its RTP is that count ÷ 10⁸ × m. Step 14 proves the check is falsifiable and anchors that boundary: on the first captured round the very predicate Step 5 uses accepts the recomputed 2.47× and rejects 2.48× — one tick away — NaN and Infinity, and for each of the 1,104 captured targets the draw k = ⌊N ÷ ci⌋ − 1 wins and k = ⌊N ÷ ci⌋ loses under the real result map and rule. Step 15 asserts the count as an integer equality on all 1,104 captured targets — it equals the win chance LIQD’s own pricing formula gives — and anchors it with four literal tallies, each a full sweep of all 100,000,000 draws:
| Component | Formula | Source |
|---|---|---|
| Win probability | ⌊9,900,000,000 ÷ ci⌋ ÷ 100,000,000, ci = 100 · m — the draws whose result reaches m | Grid arithmetic — computed by the audit |
| Counting anchor | Literal tally: run the result map and result ≥ target on every draw 0 … 99,999,999 and count | 4 full sweeps (1.01×, 2×, 100×, 500,000×) — 0 mismatches against the closed form, no tolerance |
| Paid multiplier | The target m — a win pays stake × m | The player’s own input; paid exactly on all 2,822 wins (Step 9) |
| RTP | ⌊9,900,000,000 ÷ ci⌋ × ci ÷ 10¹⁰ — win probability × target | The product; at most 0.99, because the floor can only lower it |
| Integer form | ⌊N ÷ ci⌋ = winChance(m) × 10⁸ | Asserted as an integer equality on all 1,104 captured targets (Step 15) |
| Target | Tallied winning draws | Closed form | Win chance | RTP |
|---|---|---|---|---|
| 1.01× | 98,019,801 | ⌊9,900,000,000 ÷ 101⌋ = 98,019,801 | 98.019801% | 98.999999% |
| 2.00× | 49,500,000 | ⌊9,900,000,000 ÷ 200⌋ = 49,500,000 | 49.500000% | 99.000000% |
| 100.00× | 990,000 | ⌊9,900,000,000 ÷ 10,000⌋ = 990,000 | 0.990000% | 99.000000% |
| 500,000.00× | 198 | ⌊9,900,000,000 ÷ 50,000,000⌋ = 198 | 0.000198% | 99.000000% |
Anti-Circularity Verification:
/** * Literal tally over the ENTIRE draw grid: apply the published result formula and the published win * rule to every one of the 100,000,000 possible draws and count the winners. * * This is the arithmetic-error-immune anchor. It cannot share a flooring, clamping or off-by-one * mistake with the closed form `floor(N / ci(m))`, because it does not do any of that reasoning — * it just runs the game 1e8 times. ~0.35 s per target on the validated runtime. */function tallyWinningDraws(target: number): number { let c = 0; for (let k = 0; k < RANGE; k++) if (limboWin(resultMultiplier(k), target)) c++; return c;} // … const trueResult = resultMultiplier(limboDraw(ss, probe.clientSeed, probe.nonce)); const perturbed = Math.round((trueResult + 0.01) * 100) / 100; const accepts = (r: number) => servedResultMatches(r, trueResult); const negControlRejects = accepts(trueResult) && !accepts(perturbed) && !accepts(NaN) && !accepts(Infinity); // … for (const m of distinctTargets) { const ci = targetToHundredths(m); const count = Math.floor(RESULT_NUMERATOR / ci); if (count !== Math.round(winChance(m) * RANGE) || count / RANGE !== winChance(m)) { identityFails++; if (!firstIdentityFail) firstIdentityFail = `target ${m}: grid count ${count} vs priced ${winChance(m) * RANGE}`; } identityChecked++; }Why this proof is non-circular: the win probability comes from the draw grid and the result map, and the payout is the player’s own target — neither is read from LIQD. The count is ⌊9,900,000,000 ÷ ci⌋ because LIQD’s result map divides 9,900,000,000 by k + 1: the house edge is built into that numerator, 99% of 100 × 10⁸. A target can only win on a whole number of draws, and ⌊N ÷ ci⌋ is never more than N ÷ ci — which is why a target’s RTP can sit a little below 99% but never above it (4.3). LIQD’s page states two of the resulting win chances, 49.5% at 2× and 0.0099% at 10,000×, and both equal the count exactly (Step 15). The identity itself holds by algebra for every target; what ties it to LIQD is Step 5 — the same result map reproduces all 6,700 settled results — together with the tallies, which run the real map and rule over every draw instead of trusting the algebra.
4.2RTP Claim Rule (Step 16)
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 draw grid and the target — 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 and the suite exits with code 5, even when every draw check passes. A setting above the claim never fails. For Limbo a setting is a target, and the settings are every two-decimal target from 1.01× to 10,000× — 999,900 of them, the range LIQD’s server accepts. The rule is scored on Step 16, the step that derives the effective edge.
| Item | Detail |
|---|---|
| Claim | 99% RTP at every target — LIQD’s 1.00% house edge |
| Where LIQD publishes it | The Limbo game page, liqd.com/originals/limbo: header "Released: September 2026 | Edge: 1.00%", stats panel "RTP 99%" and "House Edge 1%", page text "2x lands 49.5% of the time, 10,000x lands 0.0099% of the time, and every target in between returns 99%" — read logged out on 1 October 2026 (evidence/rtp-page-labels-2026-10-01.json); both win chances it quotes equal the exact count (Step 15) |
| Settings tested | 999,900 — every two-decimal target from 1.01× to 10,000×, the range the server accepts (win chance 98.019801% down to 0.009900%) |
| Method | Exact rational: ⌊N ÷ ci⌋ winning draws of 100,000,000, times the target — RTP = ⌊N ÷ ci⌋ × ci ÷ 10¹⁰, with N = 9,900,000,000 and ci = 100 × target |
| Band | ±0.1 percentage points (house side fails, player side never fails) |
| True minimum · maximum | 98.990018% (9,989.91×) · 99.000000% (first at 1.10×) |
| Worst gap | −0.009982 pp (9,989.91×), inside the band |
| Settings beyond the band | 0 on the house side · 0 on the player side |
| Scored on | Step 16 (profile check LIMBO-19), rule enforced |
| Result | Pass — the claim holds at every target the server accepts |
| Targets | Settings | Exact RTP | Lowest gap | Result |
|---|---|---|---|---|
| 1.01×–1.99× | 99 | 98.999998%–99.000000% | −0.000002 pp | Pass — 99 / 99 |
| 2.00×–9.99× | 800 | 98.999990%–99.000000% | −0.000010 pp | Pass — 800 / 800 |
| 10.00×–99.99× | 9,000 | 98.999901%–99.000000% | −0.000099 pp | Pass — 9,000 / 9,000 |
| 100.00×–999.99× | 90,000 | 98.999005%–99.000000% | −0.000995 pp | Pass — 90,000 / 90,000 |
| 1,000.00×–10,000.00× | 900,001 | 98.990018%–99.000000% | −0.009982 pp | Pass — 900,001 / 900,001 |
| All targets 1.01×–10,000× | 999,900 | 98.990018%–99.000000% | −0.009982 pp | Pass — 999,900 / 999,900 |
outputs/rtp-claim-table.json, pinned by SHA-256 and re-derived on every run.4.3House Edge — a 1.00% Floor & Exact Settlement (Steps 9, 16)
Step 16 derives the house edge of every target from first principles, by enumeration: edge = 1 − ⌊N ÷ ci⌋ × ci ÷ 10¹⁰, with N = 9,900,000,000 and ci = 100 × the target, computed in exact integers for all 99,999,900 targets from 1.01× to 1,000,000× (the range in LIQD’s client code). The number of winning draws is 99% of 10⁸ ÷ m rounded down to a whole draw, so ⌊N ÷ ci⌋ × ci ≤ N for every target: the RTP is never above 99% and the edge never below 1.00% — 0 violations, 0 player-favourable targets. Where N divides evenly by ci, nothing is rounded away and the edge is exactly 1.00%; up to 10,000× that holds on 282 targets, and the other 999,618 return less than 99% by at most 0.009982 pp — the most at 9,989.91×, an edge of 1.009982%. On every target the server accepts, the edge is therefore between 1.00% and 1.009982%: never in the player’s favour, never above 1.01%.
Settlement adds nothing on top. A win credits exactly stake × target (Step 9) — a two-decimal stake times a two-decimal target is exact to four decimals — so no rounding moves the payout in either direction, and the return a player receives at a target is exactly its win chance × target.
| Target | Win chance | RTP | Edge |
|---|---|---|---|
| 1.01× | 98.019801% (98,019,801 draws) | 98.999999% | 1.000001% |
| 1.10× | 90.000000% (90,000,000 draws) | 99.000000% | 1.000000% |
| 2.00× | 49.500000% (49,500,000 draws) | 99.000000% | 1.000000% |
| 10.00× | 9.900000% (9,900,000 draws) | 99.000000% | 1.000000% |
| 100.00× | 0.990000% (990,000 draws) | 99.000000% | 1.000000% |
| 9,989.91× | 0.009909% (9,909 draws) | 98.990018% | 1.009982% |
| 10,000× | 0.009900% (9,900 draws) | 99.000000% | 1.000000% |
4.4Simulation Pass 1 — Our Own Seeds (Step 23)
Section 4.1 proves each target’s RTP exactly. But does the game engine actually produce it in practice? To find out, we simulated rounds with our own implementation and our own seeds — every seed derived from a pinned master seed, pf.org/liqd-limbo/pass1/2026-09-26, rotated every 50 rounds like the epochs of this capture, and replayable byte for byte: 2,000,000 uniformity draws and 1,000,000 rounds at each of 10 targets from 1.01× to 100,000× — 12,000,000 simulated rounds, produced by src/pass1-grid.ts. The artifact stores counts, not verdicts; Step 23 re-derives every statistic from them on every verify run and replays the opening draws and rounds of every stream from the master seed. The uniformity, lag-1 and runs screens are each judged at α/3 (0.00333), together a family-wise level of 0.01, and each target’s wins must sit within 5 SD + 1 win of its exact win count:
Draw uniformity: 2,000,000 draws over 100 bins (χ², df = 99) — χ² = 94.393, p = 0.6122.
Serial screen: Lag-1 autocorrelation of the bin index and a runs test above and below the midpoint, over all 2,000,000 draws — lag-1 r = 0.000663 (z = 0.937) against the applied critical 2.936; 1,000,117 runs against 999,999.4 expected (z = 0.166, p = 0.8679).
Win counts at 10 targets: 1,000,000 rounds at each of 1.01×, 1.50×, 2×, 5×, 10×, 50×, 100×, 1,000×, 10,000× and 100,000× — the table below. Every target within 5 SD + 1 win of its exact count ⌊N ÷ ci⌋ ÷ 10⁸ × rounds; largest deviation 1.56 SD, at 10×.
Replay: The first 50,000 draws and the first 20,000 rounds of every target stream are recomputed from the master seed on every verify run; npm run pass1:check regenerates all 12,000,000 byte for byte.
Reference run (the chart above): A separate 38,000,000-round run from its own master seed, pf.org/liqd-limbo/simulate/2026-09-26, regenerated byte for byte by npm run test:full: its 20,000,000 rounds at 2.00× converge to 99.0128%, 0.57 SE from 99%.
| Target | Rounds | Wins | Expected | z | Simulated RTP |
|---|---|---|---|---|---|
| 1.01× | 1,000,000 | 980,236 | 980,198.0 | +0.27 | 99.0038% |
| 1.50× | 1,000,000 | 659,544 | 660,000.0 | −0.96 | 98.9316% |
| 2× | 1,000,000 | 494,575 | 495,000.0 | −0.85 | 98.9150% |
| 5× | 1,000,000 | 197,421 | 198,000.0 | −1.45 | 98.7105% |
| 10× | 1,000,000 | 98,534 | 99,000.0 | −1.56 | 98.5340% |
| 50× | 1,000,000 | 19,991 | 19,800.0 | +1.37 | 99.9550% |
| 100× | 1,000,000 | 9,981 | 9,900.0 | +0.82 | 99.8100% |
| 1,000× | 1,000,000 | 1,003 | 990.0 | +0.41 | 100.3000% |
| 10,000× | 1,000,000 | 114 | 99.0 | +1.51 | 114.0000% |
| 100,000× | 1,000,000 | 8 | 9.9 | −0.60 | 80.0000% |
export function targetWins(target: number, n: number): number { let wins = 0; for (let i = 0; i < n; i++) if (limboWin(resultMultiplier(pass1Draw(`target:${target}`, i)), target)) wins++; return wins;} export function buildPass1Grid(): Record<string, unknown> { const uniformity = uniformityCounts(PASS1_UNIFORMITY_DRAWS); const prefix = uniformityCounts(PASS1_PREFIX_DRAWS); const targets = PASS1_TARGETS.map((t) => ({ target: t, ci: targetToHundredths(t), rounds: PASS1_ROUNDS_PER_TARGET, wins: targetWins(t, PASS1_ROUNDS_PER_TARGET), prefixRounds: PASS1_PREFIX_ROUNDS, prefixWins: targetWins(t, PASS1_PREFIX_ROUNDS), })); // …What is a Monte Carlo simulation? Instead of proving fairness with algebra alone, we simulate millions of game rounds. Each round derives a draw from scratch using the same HMAC-SHA256 pipeline as the live game, maps it to a result, and records whether the round reached its target. We then compare the win counts with the exact counts from 4.1. If they match within tolerance, the math holds in practice — not just on paper.
How the artifact is scored, not believed: Step 23 does not read a pass flag. It re-derives χ² from the stored 100-bin histogram, the lag-1 and runs statistics from the stored integer sums, and each target’s deviation from its integer win count against the exact win count — and it recomputes the stored prefixes from the master seed on every run, which binds the counts to the declared seeds. Because every seed comes from the pinned master seed, npm run pass1:check regenerates the whole 12,000,000-round experiment and reproduces the committed artifact byte for byte.
4.5Cherry-Pick Test — Pass 2 (Step 24)
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: 1,340,000 draws in 200 windows of 50 per seed. Window 0 is the captured window (nonces 0–49) and reproduces all 6,700 captured results. The load-bearing statistic is the played-target return (the repo’s “served-target return”): each window bets the 50 targets played in that epoch’s captured rounds, at unit stake, standardised by each target’s exact mean and variance from its exact win count. If no seed was chosen against the player, the captured window is exchangeable with the other 199, so its rank among them has an exact permutation null. Comparing each captured window with its own seed’s other windows is what tells a chosen seed apart from ordinary variance. The rank-sum Z over the 134 epochs is tested one-sided against a fixed rule: cherry-picking is indicated only at Z < −3.090 (α = 0.001). Step 24 re-derives the whole Pass 2 block from the revealed seeds and compares it with the committed artifact leaf for leaf. A supplementary screen compares each seed’s first 50 draws with its next 50.
Primary defence — structural: LIQD takes the client seed from the player at each seed rotation, after the next server seed’s hash is already published. An operator that cannot predict the player’s seed cannot pre-evaluate candidate server seeds against it — so for a player who sets an unpredictable seed, cherry-picking is contained by design. In this capture 124 of the 134 epochs carry a fresh random client seed set after LIQD published that epoch’s commitment; the other 10 are Phase D’s, seeds we chose to show LIQD uses the seed the player sets.
Statistical check: The played-target return test above — a direct check on the money, backing the structural defence.
| Test | Detail |
|---|---|
| Seeds tested | 134 — every revealed server seed in the capture, with its epoch’s client seed |
| Replay depth | 10,000 nonces per seed — 1,340,000 draws in 200 windows of 50; window 0 replays the captured results with 0 mismatches |
| Statistic | Played-target return of the captured window, ranked among its seed’s 200 windows; rank-sum Z over the 134 epochs, one-sided |
| Result | Z = −0.1711 (one-sided p = 0.4321) against the −3.090 rule — no cherry-picking indicated: the captured windows rank as chance predicts, below their epoch’s median in 63 epochs, above it in 62 and level with it in the rest |
| Detection power | A selection that puts at least 16.1% of epochs’ captured windows in their worst decile is detected at least half the time |
| Early/late screen | Nonces 0–49 against 50–99 over 20 bins, exact multinomial null over 832 attainable values: critical χ² ≥ 30.8, attained size 0.045924; 10 epochs over the threshold against 5.8712 expected, exact binomial P = 0.0708 — consistent with chance (rejected only below 0.05) |
| Pooled captured draws | All 6,700 captured draws, 100 bins: χ² = 108.985, df = 99, p = 0.2315 — informational |
const served = group.map((b) => targetToHundredths(Number(b.params.target))); const targets = group.map((b) => Number(b.params.target)); const cr = served.map((ci) => Math.floor(RESULT_NUMERATOR / ci)); // exact winning draws per target (floor(N/ci)) let mu = 0, v = 0; for (let j = 0; j < PASS2_WINDOW; j++) { const p = cr[j] / RANGE, T = served[j] / 100; mu += T * p; v += T * T * p * (1 - p); } const S: number[] = []; const early: number[] = [], late: number[] = []; for (let w = 0; w < WINDOWS; w++) { let ret = 0; for (let j = 0; j < PASS2_WINDOW; j++) { const n = w * PASS2_WINDOW + j; const k = generateProvablyFairNumber(s.serverSeed, s.clientSeed, n, CURSOR, RANGE); noncesTotal++; const R = resultMultiplier(k); if (w === 0) { early.push(k); if (R !== Number(group[j].result) || group[j].nonce !== n) replayMismatches++; } if (w === 1) late.push(k); if (limboWin(R, targets[j])) ret += served[j] / 100; } S.push((ret - mu) / Math.sqrt(v)); if (w === 0) { capturedReturn += ret; capturedExpected += mu; } } const x0 = S[0]; const rank0 = S.filter((y) => y < x0).length + (S.filter((y) => y === x0).length + 1) / 2; const meanR = (WINDOWS + 1) / 2; rankSum += rank0 - meanR; // … const Z = rankSum / Math.sqrt(rankVar); const oneSidedP = normalCdf(Z);4.6Bet-Size Invariance (Steps 13, 20)
Phase C placed 200 rounds at $10.00 on 2.00× — 100× the $0.10 stake used in every other phase. All 200 results were recomputed exactly from revealed seeds using the same single-draw HMAC-SHA256 path, and every $10.00 win credited exactly stake × 2.00 = $20.00. Bet amount is not an input to the RNG and does not affect the result distribution, the win chance or the RTP — the same multiplier at every stake.
| Metric | Phases A, B, D ($0.10) | Phase C ($10.00) |
|---|---|---|
| Rounds | 6,500 | 200 |
| Result recomputation parity | 6,500 / 6,500 | 200 / 200 |
| Wins · losses | 2,718 · 3,782 | 104 · 96 |
| Paid multiplier | The target, on every win | 2.00×, on all 104 wins |
| Credit per win | Stake × target exactly, in integers (Steps 9, 20) | $20.00 — exactly $10.00 × 2.00 |
| Win chance and RTP at the target | ⌊N ÷ ci⌋ ÷ 10⁸ × target — no stake input | 49.500000% × 2.00 = 99.000000% |
Stake argument in limboDraw | None — the function takes the server seed, the client seed and the nonce | None — same function, same code path |
4.7Target Range & Exposure Ceiling (Steps 11, 17, 25)
LIQD bounds a Limbo bet in three ways, and the audit checks each against the captured rounds and the evidence. The potential payout, stake × target, is capped at $50,000: on 11 September LIQD refused a $0.10 bet at 1,000,000× with errorCode 3138 (MaxExposureExceededError) and named $0.05 as the maximum stake for that target — $0.05 × 1,000,000 = $50,000. Every captured round sits within the limit (Step 11); the 250 rounds at 500,000× × $0.10 were placed at exactly $50,000, and none of them won. The stake has a $0.10 minimum in LIQD’s client configuration, and every captured stake is at or above it. The target starts at 1.01× (played in Phase A), and LIQD’s server accepts targets up to 10,000× in steps of 0.01: on 2 October 2026 a $0.10 control at 10,000× settled, and requests at 100,101.12×, 497,487.44×, 500,000× and 990,000.01× were each refused with HTTP 400, errorCode 3001 — "Too big: expected number to be <=10000" — with no nonce consumed (Step 25). The 1,000,000× target in LIQD’s client code could not be bought at any allowed stake on the captured build: at the $0.10 minimum it is $100,000 of exposure against the $50,000 limit (Step 17). LIQD’s game code also names 1,000,000× as the maximum result — only draws 0–97 would go higher, and they win every target either way (2.7).
| Item | Limit | Evidence |
|---|---|---|
| Maximum potential payout (stake × target) | $50,000 | Derived from LIQD’s own refusal — errorCode 3138 named $0.05 as the maximum stake at 1,000,000×, and $0.05 × 1,000,000 = $50,000; 0 of 6,700 rounds over it, 250 placed at exactly $50,000 (Step 11) |
| Minimum stake | $0.10 | LIQD’s client configuration; every captured stake is at or above it (Step 11) |
| Minimum target | 1.01× (win chance 98.019801%) | Played in Phase A (Step 13) |
| Maximum target the server accepts | 10,000× (win chance 0.009900%) | The server’s own message: "must be a multiple of 0.01 between 1.01 and 10000 (win chance 0.009900%–98.019801%)" — 4 / 4 higher targets refused on 2 October 2026 (Step 25) |
| 1,000,000× target in LIQD’s client code | 1,000,000× | Not purchasable at any allowed stake on the captured build — $100,000 of exposure at $0.10 (Step 17) |
| Highest captured target | 500,000× | 250 rounds on the captured build, which accepted targets up to 500,000× — each recomputed exactly (Steps 5, 13) |
4.8Informational Items (Not Scored)
These items are reported for transparency; none of them is a scored check. They provide context on the empirical return observed during data collection 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 exact proof in 4.1–4.3 is the authoritative evidence.
| Item | Value | Context |
|---|---|---|
| Live capture return | 100.4475% | $2,661.86 returned on $2,650.00 wagered — variance illustration only; Phase C’s 104 wins at $20.00 alone returned $2,080.00 on $2,000.00 |
| Live win rate | 42.12% (2,822 / 6,700) | Against 2,831.0 wins expected from each round’s own win chance — mixed targets, informational |
| Phase returns | A 99.245% · B 40.000% · C 104.000% · D 91.266% | $496.23 on $500.00 · $40.00 on $100.00 (4 wins, all at 100×) · $2,080.00 on $2,000.00 · $45.63 on $50.00 |
| Largest result | 185,046.72× | Bet S4mFs74Bt-nTtdNWQDxk5, draw k = 534, on a 1.17× target — paid 1.17×, the target: a win pays stake × target, never the result |
| Results at the 1× floor | 147 (2.19%) | Against 132.67 expected; 78 of them lifted from a raw 0.99 — every one a loss, as 1× is below the 1.01× minimum target |
| Rounds exactly on target | 20 | Against 18.24 expected — all 20 paid as wins (Step 6) |
| Edge on the captured targets | 1.000000%–1.000041% | Across the 1,104 targets the capture used, 57 of them at exactly 1.00% — the full curve over every target is 4.3 |
| Capture-side self-check fields | localDraw, localResult, localWin, verified, creditedOk, walletOk | Capture annotations, not audit findings — scored verification derives every result from the recorded inputs and ignores them |
4.9Worked Example — Payout Verification
Real round from Phase B — bet S8UAp699y0_Mx7AZDzOp5 (epoch 100), nonce 39, target 100× at $0.10. Verified from limbo-master-6700bets.json — one of the four 100× wins and the highest target won in the capture:
serverSeed = c3e4e4ddf3a2a4074439e34bffbe367a
clientSeed = audit3b6552f5138f
nonce = 39
target = 100.00×
betAmount = $0.10
recorded = result 107.08 · win true · resultLabel won · winningAmount 10
commitment = cdaf3cbded0a18c4…Step 1 — Draw computation: HMAC-SHA256(seed, "audit3b6552f5138f:39:0") → first chunk 0x1ddb8063 = 500,924,515 < 4,200,000,000 → mod 100,000,000 = 924,515 → k = 924,515.
Step 2 — Result: floor(9,900,000,000 / 924,516) = 10,708 → result 107.08× — matches the record.
Step 3 — Win check: 107.08 ≥ 100 → the round is legitimately won. The payout multiple is the target, 100×; the result above it pays nothing extra.
Step 4 — Payout: 0.10 × 100 = 10 — in integers 10 × 10,000 × 10,000 = 1,000,000,000 = credit × 10⁸ → $10.00, the credited winningAmount (stored as 10).
| Step | Process | Output |
|---|---|---|
| 1 | Recompute the draw from seeds | k = 924,515 |
| 2 | Result from the draw | 107.08× — equal to the record |
| 3 | Apply result ≥ target at 100× | Win — won confirmed |
| 4 | Credit stake × target, 0.10 × 100 | $10.00 credited |
| 5 | RTP identity at this target | 990,000 ÷ 100,000,000 × 100 = 0.99 — exactly 99% |
winningDraws(100×) = floor(9,900,000,000 / 10,000) = 990,000 → P(win) = 990,000 / 100,000,000 = 0.0099payout multiplier = the target = 100RTP = P(win) × target = 0.0099 × 100 = 0.99 → 99.000000% (990,000 × 10,000 = 9,900,000,000: nothing rounded away)Technical Evidence & Verification5 sections
4.10Evidence Coverage Summary12 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Anti-circularity (Step 14) | Negative control rejects 2.48×, NaN and Infinity; boundary anchor on 1,104 / 1,104 captured targets; grid extremes checked | Pass |
| Win-chance identity (Step 15) | 1,104 / 1,104 targets — integer equality; 4 full 100,000,000-draw tallies agree; LIQD’s 49.5% and 0.0099% match | Pass |
| House edge audit (Step 16) | Edge ≥ 1.00% at all 99,999,900 targets in exact integers, 0 violations, 0 player-favourable; at most 1.009982% up to 10,000× | Pass |
| RTP claim rule (Step 16) | 999,900 / 999,900 targets within ±0.1 pp of 99%; worst gap −0.009982 pp | Pass |
| Payout & credit (Steps 8, 9) | 2,822 wins paid at the target; 6,700 credits exact in integers | Pass |
| Bet-size invariance (Steps 13, 20) | 200 / 200 Phase C at $10.00; 6,700 / 6,700 settlements determined | Pass |
| Stake & exposure limits (Step 11) | 0 stakes below $0.10; 0 rounds over $50,000 | Pass |
| 1,000,000× purchase check (Step 17) | 1,000,000× not purchasable at any allowed stake on the captured build | Pass |
| Simulation Pass 1 (Step 23) | 12,000,000 simulated rounds — uniformity p = 0.6122, serial clear, 10 / 10 targets within 5 SD + 1 win | Pass |
| Simulation Pass 2 (Step 24) | 134 seeds × 10,000 nonces: rank-sum Z = −0.1711 against −3.090; early/late 10 against 5.8712 (P = 0.0708) | Pass |
| Artifact integrity (Step 19) | Dataset, simulation and chart pins match; 966 numeric values inside their declared domains | Pass |
| Server target limit (Step 25) | 4 / 4 targets above 10,000× refused on 2 October 2026; 300 / 300 supplement rounds recompute | Pass |
4.11Code References14 source files
| File | Purpose |
|---|---|
| tests/steps/anti-circularity.ts | Steps 14–15: Negative control, grid boundary anchor, win-chance identity and the four full-grid tallies |
| tests/steps/edge.ts | Steps 16–17: The effective edge of all 99,999,900 targets and the 1,000,000× purchase check |
| src/rtp-claim.ts | The RTP claim rule, RTP-CLAIM 1.2.0 — a byte-identical copy of the audit framework’s rule |
| src/rtp-settings.ts | The 999,900-target exact RTP table and the claim it is held to (LIQD’s published 99%) |
| tests/steps/payouts.ts | Steps 8–11: Paid multiplier, credit arithmetic, loss settlement, wallet fields, stake floor and exposure limit |
| tests/steps/pass1.ts | Step 23: Re-derives every Pass 1 statistic from the stored counts and replays the prefixes |
| src/pass1-grid.ts | Pass 1 producer — seeds from the pinned master seed, counts only |
| tests/steps/pass2.ts | Step 24: Re-derives the Pass 2 block from the revealed seeds |
| src/pass2.ts | Pass 2 — the played-target return with its exact permutation null, and the early/late screen with its exact multinomial null |
| src/simulate.ts | The reference run (38M rounds) and its convergence chart |
| tests/steps/standardization.ts | Steps 19–20: Artifact integrity and settlement determinism |
| tests/steps/supplement.ts | Step 25: The supplement and the server’s target limit |
| tests/steps/statistical.ts | Informational items — live return, win rate, phase returns, result distribution |
| src/config.ts | winChance, theoreticalRTP, effectiveEdge, resultMultiplier, limboWin — closed-form formulas |
4.12Datasets UsedSource artifacts
4.13Verified Invariants13 invariants
| Invariant | Result |
|---|---|
| Target m wins on exactly ⌊N ÷ ci⌋ of the 100,000,000 draws — derived from the result map, not read from LIQD (non-circular) | Pass |
| ⌊N ÷ ci⌋ = winChance(m) × 10⁸ as integers on all 1,104 captured targets; full-grid tallies agree at 1.01×, 2×, 100× and 500,000× | Pass |
| Negative control: a result one tick away (2.48× for 2.47×), NaN and Infinity are rejected by the Step 5 predicate | Pass |
| ⌊N ÷ ci⌋ × ci ≤ N at all 99,999,900 targets — edge ≥ 1.00%, never player-favourable | Pass |
| LIQD’s published 99% is met by all 999,900 targets up to 10,000× — worst gap −0.009982 pp against ±0.1 pp | Pass |
| LIQD’s page figures equal the exact win chance: 49.5% at 2× and 0.0099% at 10,000× | Pass |
| Every win pays stake × target exactly — credit × 10⁸ = hundredths(stake) × hundredths(target) × 10⁴ on 2,822 / 2,822 | Pass |
| Every captured potential payout within $50,000; every stake ≥ $0.10 | Pass |
| Draw uniformity χ² p = 0.6122 over 2,000,000 simulated draws, at α/3 | Pass |
| Lag-1 z = 0.937 < 2.936 and runs p = 0.8679 ≥ α/3 on 2,000,000 draws | Pass |
| 10 / 10 simulated targets within 5 SD + 1 win of their exact win count (worst 1.56 SD) | Pass |
| No cherry-picking indicated across 134 casino seeds (rank-sum Z = −0.1711 against −3.090; 0 replay mismatches) | Pass |
| Phase C ($10.00) results derive from the same three-input draw as $0.10 rounds; stake absent from the RNG input by construction | Pass |
4.14Reproduction InstructionsRun locally
Clone the repository, install dependencies, and regenerate both simulations before verifying:
git clone https://github.com/ProvablyFair-org/liqd-limbo.gitcd liqd-limbo && git checkout v1.0.1npm cinpm run test:full # regenerates both simulations from their pinned master seeds, then verifieshead -40 outputs/rtp-claim-table.jsoncat outputs/pass1-grid.json | head -40S4-related steps and the RTP claim rule:
[PASS] Step 8 — Paid-Multiplier Derivation
[PASS] Step 9 — Credit Arithmetic
[PASS] Step 11 — Wallet Ledger, Stake Floor & Exposure Ceiling
[PASS] Step 13 — Phase Plan Conformance
[PASS] Step 14 — Anti-Circularity (Negative Control + Grid Boundary Anchor)
[PASS] Step 15 — Win-Chance vs Result-Distribution Identity
[PASS] Step 16 — Effective-Edge Curve
[PASS] Step 17 — Client-Code Maximum Multiplier …
[PASS] Step 20 — Settlement Determinism
[PASS] Step 23 — Simulation Pass 1 — Pinned Master Seed, Re-Derived
[PASS] Step 24 — Simulation Pass 2 — Cherry-Pick Screen, 10,000 Nonces per Revealed Seed
[PASS] Step 25 — Target-Decade Supplement & Live Target Ceiling …
rtp-claim: ENFORCED — PASS
rtp-claim: …
rtp-claim: band ±0.10 pp · 999,900 settings · true min 98.990018% (target=9989.91) · true max 99.000000% (target=1.10)
rtp-claim: house side beyond band: 0 · player-favourable beyond band: 0 · worst target=9989.91 at -0.009982 pp
rtp-claim: …
rtp-claim: outcome PASS · minimal fix: none — the claim holds for every setting it states a figure for
rtp-claim: committed table outputs/rtp-claim-table.json: equals this run; sha256 cdfbf600e0713b82… matches the pin
Final convergence RTP in the reference run’s committed artifact: 0.990128 (exact RTP at 2.00×: 0.99). The reference run is deterministic: npm run test:full regenerates outputs/simulation-results.json and outputs/rtp-convergence.html byte for byte; the simulation the verdict scores is Step 23’s, re-derived from outputs/pass1-grid.json on every run. The chart above plots the reference run’s 2.00× convergence series.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. 34 passed. No test altered, replayed or injected an outcome, and all 2,764 bets captured during the testing recompute exactly.
What we verified
- Race conditions — can parallel bets, a high-stake over-commit, 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 the result, payout, status or multiplier be injected, the win target swapped, or the payout cap bypassed?
- Parameter limits — can invalid targets, stakes, content types 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 result 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
- Every live round checked against LIQD’s verifier agreed with our independent recomputation
- Race conditions — can parallel bets, a high-stake over-commit, 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 the result, payout, status or multiplier be injected, the win target swapped, or the payout cap bypassed?
- Parameter limits — can invalid targets, stakes, content types or currencies be submitted?
- Across the 34 tests, no request path let anyone alter, replay or inject an outcome
- Once a bet is placed, its result 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
- Every live round checked against LIQD’s verifier agreed with our independent recomputation
Nonce integrityPass
Seed integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
Parameter limitsPass
34 integrity tests: 34 pass. No test altered, replayed or injected an outcome, and every live round checked against LIQD’s verifier agreed with our independent recomputation. Test procedures and payloads are withheld by policy — see 5.5.
Category Coverage
| Check | Result | Evidence |
|---|---|---|
| Nonce integrity | Pass | 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 integrity | Pass | 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 determinism | Pass | All 2,764 bets captured during the integrity testing recompute exactly, with LIQD’s verifier agreeing on every live round checked against it (EXTRA-002, PF-001, PARAM-003); 2,000 live bets pass the uniformity test (FAIR-001) |
| Round & player isolation | Pass | 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 integrity | Pass | Injected result, payout, status, multiplier and alias win-target fields all rejected; the 50,000 USDC potential-payout cap refuses over-limit stakes at bet time; 200 consecutive rounds settled with no payout mismatch |
| Parameter limits | Pass | Every out-of-range and wrong-type target tested was rejected, and no content-type variant let an invalid target settle; invalid stakes rejected with no silent conversion to zero; all six currency overrides rejected |
Nonce integrityPass
Seed integrityPass
Outcome determinismPass
Round & player isolationPass
Payout integrityPass
Parameter limitsPass
34 integrity tests: 34 pass. No test altered, replayed or injected an outcome, and every live round checked against LIQD’s verifier agreed with our independent recomputation. 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 round 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.
| Category | Tests | What It Catches |
|---|---|---|
| Race Conditions | 4 | Parallel bets racing placement, settlement or seed rotation |
| Input Validation | 5 | Malformed, wrong-type or out-of-range targets, stakes, content types and currencies |
| Atomic Balance / Idempotency | 3 | Double debits, orphaned debits and unsafe retry behaviour after a timeout or disconnect |
| Broken Authorization | 2 | Acting on another account, or after the session has ended |
| Max-win Bypass | 5 | Client-supplied outcome or win-target fields, and the stake and potential-payout limits |
| Nonce Manipulation | 4 | Reused, skipped, zero, negative or oversized nonces supplied by the client |
| RNG / PF Verification | 4 | Result distribution, commitment and reveal, and independent recomputation of live rounds |
| RNG / Information Leak | 1 | Disclosure of the active server seed, or of hidden game state, before it is due |
| Entropy / PF Verification | 1 | Server-seed rotation behaviour: fresh seeds, a new commitment and the nonce reset |
| Precision / Payout | 1 | Settlement arithmetic and wallet reconciliation over many rounds |
| Event Replay | 1 | Duplicate or replayed requests settling more or less than once |
| Adversarial Seeds | 3 | Unusual 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: all 34 tests pass, so there is nothing to grade.
| Status | Meaning | Action |
|---|---|---|
| Pass | The service handled the scenario correctly: the control held | None — recorded with its result |
| Remediated | A failed control that LIQD has since fixed, re-tested by ProvablyFair.org on the current build | Recorded with the fix and its re-test |
| Warning | A real defect, or a property not yet proven, without a direct effect on funds or fairness | Recorded with its impact; open until a re-test resolves it |
| Fail | A defect affecting funds, fairness or security | Remediation and a re-test required |
| Condition | Consequence |
|---|---|
| A substituted server seed is accepted against a published commitment | Hard fail |
| A revealed seed fails its commitment and play continues | Hard fail |
| Any settled round’s result cannot be reproduced from disclosed inputs | Hard fail |
| A credited payout deviates from stake × target on a win, or from zero on a loss | Hard fail |
| Nonce reuse or gaps that could conceal discarded outcomes | Hard fail |
34 tests · 34 pass
Race Conditions4/4
LIQD-LIMBO-EXTRA-001PassSeed 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-LIMBO-RACE-001PassNonce uniqueness under parallel bets
- Control assessed
- Every accepted bet gets its own server-assigned nonce in sequence, even under parallel load.
- Testing approach
- Bursts of parallel bets from one account; nonces and response order compared.
- Observed result
- All 55 bets were accepted with unique nonces and no server error; ordered by arrival, the nonces ascend without a gap across the run.
- Impact
- None — no nonce was shared or skipped.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-RACE-002PassIdentical 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-LIMBO-RACE-003PassHigh-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 16 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-LIMBO-PARAM-001PassTarget limits and input types
- Control assessed
- Valid targets are accepted; out-of-range and wrong-type targets are refused.
- Testing approach
- The valid boundary targets, out-of-range and wrong-type targets, with a control bet.
- Observed result
- The control and both valid boundaries were accepted, and every out-of-range and wrong-type target was rejected.
- Impact
- None — the control held.
- Limitations
- Targets above 10,000× are checked in Section 4 (Step 25).
LIQD-LIMBO-PARAM-002PassStake edge cases
- Control assessed
- Minimum and free-play stakes are accepted; invalid stakes are refused, with no silent conversion to zero.
- Testing approach
- Minimum-stake and zero-stake controls, then negative, oversized, sub-precision, null and wrong-type stakes.
- Observed result
- Both controls settled; every invalid stake was rejected, with no silent conversion to zero.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-PARAM-003PassLowest target, recomputed
- Control assessed
- The lowest valid target settles, and its outcome is independently reproducible from the revealed seed.
- Testing approach
- A bet at the 1.01× floor, recomputed after the reveal and checked against LIQD’s verify endpoint.
- Observed result
- The 1.01× bet settled, and its result, win flag and multiplier matched our recomputation exactly, with LIQD’s verifier agreeing.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-PARAM-004PassContent-type variations around validation
- Control assessed
- Changing how the request body is encoded cannot let an invalid target settle.
- Testing approach
- Invalid targets sent under each alternative content type, with a control bet.
- Observed result
- The control settled; both invalid targets were rejected under every content type, so no body-parsing path skipped validation.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-TOKEN-001PassCurrency 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-LIMBO-IDEMP-001PassSame-key retry after a timeout
- Control assessed
- A retry with the same key returns the original round, with no second debit.
- Testing approach
- Bets sent with very short client timeouts, then retried with the same key; wallet reconciled over the nonces used.
- Observed result
- All 20 retries returned their original round; all 10 timed-out bets had settled server-side, and the wallet reconciled exactly over the 20 nonces.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-IDEMP-002PassOrphaned-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-LIMBO-IDEMP-003PassDisconnect while a bet is processed
- Control assessed
- Every consumed nonce is explained, and no debit is left unexplained.
- Testing approach
- Bets abandoned by the client at different moments; rounds and balance reconciled.
- Observed result
- The earliest-abandoned bet never reached settlement; the other three settled. The balance moved by exactly the net of the three settled rounds.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Broken Authorization2/2
LIQD-LIMBO-AUTH-001PassForeign 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.
LIQD-LIMBO-AUTH-003PassSession 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 Bypass5/5
LIQD-LIMBO-INJECT-001PassSingle outcome field injected
- Control assessed
- No client-supplied result, payout, status 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 by schema validation; 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-LIMBO-INJECT-002PassWin-target field injection
- Control assessed
- No injected field changes the win condition, and malformed targets are refused.
- Testing approach
- Bets carrying alias target fields, malformed targets and a parameter override.
- Observed result
- The alias fields and malformed targets were rejected; the override bet settled against the target it declared. No win condition or multiplier diverged from the request.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-MAXWIN-001PassStake boundary enforcement
- Control assessed
- Out-of-range stake values are refused without touching the balance.
- Testing approach
- Oversized, negative and malformed stakes alongside a free-play control.
- Observed result
- The control settled; all four invalid stakes were rejected and none consumed balance.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-MAXWIN-002PassSeveral 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.
LIQD-LIMBO-MAXWIN-003PassPotential-payout cap at bet time
- Control assessed
- A bet whose potential payout exceeds the 50,000 USDC cap is refused when it is placed, and the boundary stake settles.
- Testing approach
- Stakes just inside and just outside the cap at a high target, and one far above it.
- Observed result
- The oversized and just-over stakes were refused, the reply naming the largest allowed stake; the boundary stake settled. Only the boundary bet consumed a nonce and moved funds.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Nonce Manipulation4/4
LIQD-LIMBO-NONCE-001PassReused 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
- The replayed nonce was rejected at validation and never echoed; only the control and recovery bets advanced the nonce and moved the balance.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-NONCE-002PassSkipped-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
- The skipped-ahead nonce was rejected at validation and never echoed; the server did not skip ahead in its sequence.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-NONCE-003PassZero 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
- All three zero and negative nonces were rejected at validation and none was honoured; only the two controls advanced the nonce.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-NONCE-004PassOverflow-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
- All three overflow-sized nonces were rejected at validation, with no server error; only the two controls advanced the nonce.
- 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-LIMBO-EXTRA-002PassIndependent result and outcome check
- Control assessed
- Live results can be reproduced independently from the revealed inputs.
- Testing approach
- Rounds recomputed with our own implementation and checked against LIQD’s verify endpoint, then every bet captured during the integrity testing recomputed.
- Observed result
- All five rounds recomputed to the settled outcome, with LIQD’s verifier agreeing; all 2,764 bets captured during the integrity testing recomputed exactly.
- Impact
- None — the control held.
- Limitations
- The recomputation covers the logic version tested; no result in testing came near the top of the result range.
LIQD-LIMBO-FAIR-001PassDraw distribution and recomputation
- Control assessed
- Every bet reproduces exactly from its revealed seed, the draw is uniform, and a winning bet pays exactly its target.
- Testing approach
- Two thousand live bets at 2×, recomputed after the reveal; the draw tested for uniformity and the win rate against its price.
- Observed result
- All 2,000 bets recomputed exactly — result, win flag and multiplier. The draw passed the uniformity test, the win rate matched the priced 0.495, and every win paid exactly 2×.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-PF-001PassProvably-fair data and commit / reveal
- Control assessed
- Each bet carries every provably-fair field, the revealed seed matches its commitment, and each outcome reproduces.
- Testing approach
- Three bets checked field by field, then the seed revealed and the rounds recomputed and checked against LIQD’s verify endpoint.
- Observed result
- All fields were present and the nonce advanced by exactly 3; the revealed seed hashed to its commitment, and all three rounds recomputed to the settled outcome, with LIQD’s verifier agreeing.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-PF-002PassNonce 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; once the seed was revealed, all 10 bets recomputed exactly.
- 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-LIMBO-DISCLOSE-001PassActive 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 one bet and one seed period.
Entropy / PF Verification1/1
LIQD-LIMBO-ENTROPY-004PassNonce 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-LIMBO-PREC-001PassPayout 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
- None specific to this test; the general scope is in 5.5.
Event Replay1/1
LIQD-LIMBO-REPLAY-001PassDuplicate 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 — one won and one lost at 2×, so the balance was unchanged: two real rounds netting to zero.
- Impact
- None — each request settled exactly once.
- Limitations
- None specific to this test; the general scope is in 5.5.
Adversarial Seeds3/3
LIQD-LIMBO-SEED-001PassClient-seed length and character sets
- Control assessed
- Client seeds have a consistent length limit, and the game stays usable after every accepted or refused change.
- 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 longer ones consistently rejected; empty, markup and path-like seeds were rejected, other character sets accepted. Every following bet settled.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-SEED-002PassUnicode 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 and stored as the same composed seed, and both following bets settled.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-LIMBO-SEED-003PassRapid 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: 34 pass.
| Category | Tests | Result |
|---|---|---|
| Race Conditions | 4 | All pass |
| Input Validation | 5 | All pass |
| Atomic Balance / Idempotency | 3 | All pass |
| Broken Authorization | 2 | All pass |
| Max-win Bypass | 5 | All pass |
| Nonce Manipulation | 4 | All pass |
| RNG / PF Verification | 4 | All pass |
| RNG / Information Leak | 1 | All pass |
| Entropy / PF Verification | 1 | All pass |
| Precision / Payout | 1 | All pass |
| Event Replay | 1 | All pass |
| Adversarial Seeds | 3 | All pass |
5.4Findings & Remediation
No findings — all 34 tests in this section pass. Each test’s card in the matrix above gives its control, approach, result, impact and limitations.
5.5Scope & LimitationsScope note
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, run timestamps and build identifiers — available to LIQD and to regulators on request.
Integrity tests ....................... 34Categories ............................ 12Pass .................................. 34Remediated ............................ 0Warning (open) ........................ 0Findings .............................. 0Procedures and payloads ............... withheld (5.5)Player Verification
Can a player verify their own bets without trusting anyone?
Every Limbo round 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 recomputed result matches the game — and gives the same win or loss against your target — the round was provably fair. This section walks you through the process — with LIQD’s own Verify calculator, with the independent ProvablyFair.org verifier, or with a few lines of code you run yourself.
Key principles
- Every Limbo round can be independently reproduced
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saw before betting and your recomputed result matches the game, the round was provably fair
- LIQD’s own Verify calculator recomputes any round’s result from its seeds and nonce
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)
- Target — the multiplier you set, from 1.01× to 10,000×; the result must reach it
- Every Limbo round can be independently reproduced
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saw before betting and your recomputed result matches the game, the round was provably fair
- LIQD’s own Verify calculator recomputes any round’s result from its seeds and nonce
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the bet number in sequence (ensures uniqueness)
- Target — the multiplier you set, from 1.01× to 10,000×; the result must reach it
Player can access all required inputsPass
Client seed origin transparentPass
Verification process documentedPass
MANIFEST.md, Verify a bet) — field names, the commitment encoding, the draw and its guard, the result, the win rule and the credit, with worked real roundsCasino UI exposes verification panelPass
tests/limbo/rngTests.ts) and both replies of LIQD’s /verify endpoint at the boundary — a 7.5× result on a 7.5× target a win, on 7.51× a loss (Step 19); live, the endpoint agreed on every round the integrity tests checked against it (EXTRA-002, PF-001, PARAM-003)Independent verifier availablePass
Manual recomputation possiblePass
No hidden or proprietary stepsPass
Public algorithm documentationInfo
docs/ALGORITHM.md), and we recommend LIQD publish its ownOnly disclosed inputs are used. Identical inputs always produce identical output.
- 1
Place a Limbo Bet
Set a target multiplier from 1.01× to 10,000× and play. The result is drawn from the seeds and the nonce alone — the target and the stake never enter the draw. The target decides the win: a result at or above it pays stake × target; below it, the bet is lost.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the Limbo game page. Its Seeds tab shows the commitment for your seed pair — the hashed server seed, published before you play — and the hash of the next server seed, fixed before you choose your next client seed. Rotating ends the epoch and reveals the outgoing server seed in plaintext; the client seed you submit with the rotation becomes the new active one. 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
Enter the Seeds in the Verify Tab
Open the panel’s Verify tab — a calculator that recomputes a round’s result from a server seed, a client seed and a nonce — and enter the revealed server seed, your client seed, the round’s nonce and your target.
- 4
Recompute & Confirm
Press Verify and compare the calculator’s result with your round — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed and the result matches, with the same win or loss against your target, the round was provably fair. The scripts under Manual Verification also take your stake and check the payout to the last decimal.
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
Verify with the audit code
Independent verification using the implementation behind this report.
Open independent verifierBuilt-in verification
The operator’s own verification page. This tool is controlled by LIQD.com.
Open LIQD verifierVisual Walkthroughstep-by-step
Set your target — any multiplier from 1.01× to 10,000× in steps of 0.01 — and play. The platform computes the result from (serverSeed, clientSeed, nonce) before your target is applied: the target decides whether the result wins and what a win pays, never what is drawn.
Target — the multiplier you set; the round wins when the result reaches it, and a win pays stake × target.
Result — the multiplier the round draws, never below 1×; it is the same whatever target or stake you choose.
Win chance — 99% ÷ target, rounded down to whole draws: 49.5% at 2×, 0.0099% at 10,000× — the figures LIQD’s game page states.

LIQD Limbo on liqd.com — a $1.00 bet at a 2.00× target (win chance 49.50%, payout on win $2.00); this round drew 17.07×, the one before it 1.98×.
The panel has two tabs, Seeds and Verify. Before you play, the Seeds tab shows the commitment for your active seed pair and for the next one, with your client seed and the bets made with the pair; LIQD’s provably-fair API publishes the same values. Rotation ends the epoch: the outgoing server seed is revealed for verification, the pre-committed next seed becomes active, and the client seed you submit with the rotation takes effect. That advance commitment is the chain the audit’s Step 2 checks across all 133 links, plus the pre-capture link into epoch 0.
Active commitment — Active Server Seed (Hashed) on the Seeds tab, activeServerSeedHash in LIQD’s provably-fair API: the SHA-256 of the current epoch’s server seed; the plaintext is revealed when you rotate and must hash to this value.
Next commitment — Next Server Seed under Rotate Seed Pair, nextServerSeedHash in the API: published before you set the next client seed; it becomes the active commitment when you rotate.
Revealed server seed — returned by the rotation as revealedServerSeed, 32 hex characters; its SHA-256, taken over the hex text, must equal the commitment you saw before betting.
Client seed — your input for the new epoch, sent with the rotation. Set after the next commitment is published, an unpredictable client seed means the server seed was fixed before your seed could be known.

The Seeds tab before rotating — Active Server Seed (Hashed) 523d9aa4…c2eeb5 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, 39e1fead…136654, hashes to 523d9aa4…c2eeb5.
The Verify tab recomputes a round on demand from a server seed, client seed and nonce, and with your target it shows whether the round won — the audit read four of its values, and our implementation reproduces all four. Enter the server seed revealed in step 2, your client seed, the round’s nonce and your target. Everything this audit checks cryptographically, the calculator and the scripts below let you check by hand.
Verify calculator — the Verify tab: recomputes a round’s result from a server seed, client seed and nonce; our implementation reproduces the four values the audit read from it.
Nonce — counts your bets under the pair from 0 and resets at rotation; it locates each round within the epoch.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 1 and the 2.00× target.
Press Verify. The calculator recomputes the result from the three seed inputs, and your target decides the round — a result at or above it wins and pays stake × target. A match shows the result is the one the committed seed produces. The scripts in the Manual Verification section below take the target and the stake as well, so they check the draw, the win and the payout too — with nothing but Node.js or Python, pre-loaded with the first round of this capture and the expected output of four more.
Commitment — the SHA-256 of the revealed seed’s hex text; it must equal the hash you saw before betting.
Result — must equal your round’s result to two decimals, as the scripts print it.
Win & payout — a result at or above your target wins stake × target; below it the payout is 0.

The result: 17.07×, a win at the 2.00× target. Our scripts in 6.8–6.9 reproduce it exactly: draw 5,797,579, result 17.07, $2.00 paid 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)
Five moving parts, in the order they act:
- Commit — before you play, LIQD publishes
SHA-256of 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 digest is read as eight 4-byte big-endian chunks, and the first chunk below 4,200,000,000 is taken modulo 100,000,000 — that is the draw k, 0 to 99,999,999. The guard discards the chunks at or above 4,200,000,000 (2.2111% of them), so every draw is exactly as likely as every other. - Result —
max(1, floor(9,900,000,000 ÷ (k + 1)) ÷ 100), a multiplier with two decimals, never below 1×: the smaller the draw, the higher the result. - Win — the round wins if the result is at or above your target; a result exactly on the target wins.
- Pay — a win credits stake × target, never the result; a loss pays 0. Both are two-decimal amounts, so the credit is exact.
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 list the expected output of five rounds from this report in their comments so you can self-check the tool before trusting it. The other classic slips are the guard — taking the first chunk without checking it changes the result of about one round in 45, all 126 such rounds among the 6,700 scored — and the floor: the lowest result is 1×, not 1.01×, and it loses against every target. LIQD’s game code names 1,000,000× as the maximum result — only draws 0–97 would go higher, and they win every target either way.
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:
| LIQD Built-in (Verify calculator) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino | ProvablyFair.org — open source |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | As a cross-check — our implementation reproduces the 4 values recorded from its Verify calculator (a unit test) and both replies of its /verify endpoint at the boundary (Step 19); live, the endpoint agreed on every round the integrity tests checked against it | The scripts in 6.8–6.9 implement the derivation this audit specifies — the HMAC-SHA256 draw and its guard, the result map, the win rule and the payout — and reproduce all 6,700 scored rounds |
| If they disagree | Suspect the casino’s tool or a tampered round | The repo + scripts are the tiebreaker anyone can run |
6.8JavaScript Verification Script
Complete, dependency-free, and pre-loaded with the first round of the capture as its default inputs — run it as-is to see a full match, then swap in your own round. Its comments list the expected output of four more rounds from this report: the win exactly on its target, a result lifted to 1×, a round whose first chunk was discarded, and the capture’s highest result. Run on every one of the 6,700 scored rounds, it reproduces each one: commitment, draw, result, win and payout.
// verify-limbo.js — standalone LIQD Limbo verifier (Node.js, no dependencies)// Usage: node verify-limbo.js <serverSeed> <clientSeed> <nonce> <target> [stake]const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonce, targetArg, stakeArg = '0.10'] = argv.length ? argv : ['ee4724f00cfc46e28425e9de3239d427', 'audita5a8f43bd18d', '0', '4.58'];// Amounts as exact whole hundredths: '4.58' → 458nconst hundredths = s => { const m = /^(\d+)(?:\.(\d{1,2}))?$/.exec(s); if (!m) throw new Error(`not a 2-decimal amount: ${s}`); return BigInt(m[1]) * 100n + BigInt((m[2] || '').padEnd(2, '0'));};const target = hundredths(targetArg), stake = hundredths(stakeArg);const fmt = h => `${h / 100n}.${String(h % 100n).padStart(2, '0')}`; // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — the hash LIQD showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) The draw — HMAC-SHA256 keyed on the DECODED bytes, message clientSeed:nonce:0; the first// 4-byte big-endian chunk below 4,200,000,000 (= 42 × 10^8), taken modulo 10^8const digest = createHmac('sha256', Buffer.from(serverSeed, 'hex')) .update(`${clientSeed}:${nonce}:0`).digest();let chunk = 0;while (chunk < 8 && digest.readUInt32BE(4 * chunk) >= 4200000000) chunk++;if (chunk === 8) throw new Error('all eight chunks rejected (about 5.7e-14 per round)');const k = digest.readUInt32BE(4 * chunk) % 100000000;const skipped = chunk === 0 ? '' : chunk === 1 ? '; chunk 0 discarded' : `; chunks 0-${chunk - 1} discarded`;console.log(`draw = ${k} (chunk ${chunk}${skipped})`); // 3) Result in hundredths: floor(9,900,000,000 / (k + 1)), never below 1.00. (LIQD's game code// names 1,000,000× as the maximum result — only draws 0–97 would go higher, and they win// every target either way.)const raw = 9900000000n / BigInt(k + 1);const result = raw < 100n ? 100n : raw;console.log('result =', fmt(result) + (raw < 100n ? ` (raw ${fmt(raw)}, lifted to 1.00)` : '')); // 4) Win iff result >= target; a win pays stake × target exactly:// credit × 10^8 = stake¢ × target¢ × 10^4const win = result >= target;const credit = win ? stake * target * 10000n : 0n; // in units of 10^-8const payout = `${credit / 100000000n}.${String(credit % 100000000n).padStart(8, '0')}`;const verdict = `${win} (${fmt(result)} ${win ? '>=' : '<'} ${fmt(target)})`;console.log(`win = ${verdict} | payout = ${payout}`);// Expected, defaults (bet USzRL9gyMBxZg8NLCNoYS, epoch 0 nonce 0, 4.58× at $0.10 — a loss):// commitment = 394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded// draw = 40058996 (chunk 0)// result = 2.47// win = false (2.47 < 4.58) | payout = 0.00000000// Expected, bet bYS2viIeJOfIRm1YpweuJ (epoch 17 nonce 32 — exactly on its target, a win):// node verify-limbo.js 441045a40a9fdbea912bb7c77afb1d2e audite7134d0080bc 32 1.16 0.10// commitment = e9fe937d57fa02b85c8a610297ea3caaba8decc6383d5c07e2e303067e822126// draw = 84760019 (chunk 0)// result = 1.16// win = true (1.16 >= 1.16) | payout = 0.11600000// Expected, bet yAVLBL0QWD0RVuewVxG_v (epoch 3 nonce 29 — lifted to the 1× floor):// node verify-limbo.js 56481f25aad5d0b6644fbfff05d4e468 audit8b0f66ee7a38 29 3.76 0.10// commitment = ecf1e700cd3852d869c2e44a5794e854aff5cdf50af7783a25196528d197e653// draw = 99762668 (chunk 0)// result = 1.00 (raw 0.99, lifted to 1.00)// win = false (1.00 < 3.76) | payout = 0.00000000// Expected, bet krv88h79b6gDGRTDgswi9 (epoch 5 nonce 2 — first chunk discarded):// node verify-limbo.js b73582d05dec4dd4a43b4e0b72ec3ebd audited5df347a318 2 1.59 0.10// commitment = 7d57ba291003c8aa316f52b6b94d6b0100d5ff1b830ee1c3c6ba677f7c9a3033// draw = 66691774 (chunk 1; chunk 0 discarded)// result = 1.48// win = false (1.48 < 1.59) | payout = 0.00000000// Expected, bet S4mFs74Bt-nTtdNWQDxk5 (epoch 34 nonce 29 — the capture’s highest result):// node verify-limbo.js 06dd164fe01b803ae5a843b4b3fc4bbb audit02b3dbac08bb 29 1.17 0.10// commitment = 7375ad4e0375ba1eca0bb0a97ce8b31dfcbf25cef3840e9d3594a00967ffa1ae// draw = 534 (chunk 0)// result = 185046.72// win = true (185046.72 >= 1.17) | payout = 0.117000006.9Python Verification Script
The same verifier in Python 3, standard library only, with exact integer arithmetic throughout — so it doubles as an independent check on the JavaScript version. It reproduces all 6,700 scored rounds as well:
# verify-limbo.py — standalone LIQD Limbo verifier (Python 3, standard library only)# Usage: python3 verify-limbo.py <serverSeed> <clientSeed> <nonce> <target> [stake]import hashlib, hmac, struct, sysfrom fractions import Fraction args = sys.argv[1:] or ['ee4724f00cfc46e28425e9de3239d427', 'audita5a8f43bd18d', '0', '4.58']server_seed, client_seed, nonce = args[0], args[1], int(args[2]) def hundredths(s): # amounts as exact whole hundredths: '4.58' -> 458 h = Fraction(s) * 100 if h.denominator != 1: raise ValueError(f'not a 2-decimal amount: {s}') return h.numerator target, stake = hundredths(args[3]), hundredths(args[4] if len(args) > 4 else '0.10')fmt = lambda h: f'{h // 100}.{h % 100:02d}' # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) The draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:0; the first# 4-byte big-endian chunk below 4,200,000,000 (= 42 × 10^8), taken modulo 10^8message = f'{client_seed}:{nonce}:0'.encode()digest = hmac.new(bytes.fromhex(server_seed), message, hashlib.sha256).digest()for chunk, value in enumerate(struct.unpack('>8I', digest)): if value < 4200000000: k = value % 100000000 breakelse: raise ValueError('all eight chunks rejected (about 5.7e-14 per round)')skipped = ('' if chunk == 0 else '; chunk 0 discarded' if chunk == 1 else f'; chunks 0-{chunk - 1} discarded')print(f'draw = {k} (chunk {chunk}{skipped})') # 3) Result in hundredths: floor(9,900,000,000 / (k + 1)), never below 1.00raw = 9900000000 // (k + 1)result = max(100, raw)print('result =', fmt(result) + (f' (raw {fmt(raw)}, lifted to 1.00)' if raw < 100 else '')) # 4) Win iff result >= target; a win pays stake × target exactly:# credit × 10^8 = stake¢ × target¢ × 10^4win = result >= targetcredit = stake * target * 10**4 if win else 0 # in units of 10^-8payout = f'{credit // 10**8}.{credit % 10**8:08d}'verdict = f"{'true' if win else 'false'} ({fmt(result)} {'>=' if win else '<'} {fmt(target)})"print(f'win = {verdict} | payout = {payout}')# Expected, defaults (bet USzRL9gyMBxZg8NLCNoYS, epoch 0 nonce 0, 4.58× at $0.10 — a loss):# commitment = 394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded# draw = 40058996 (chunk 0)# result = 2.47# win = false (2.47 < 4.58) | payout = 0.00000000# Expected, bet bYS2viIeJOfIRm1YpweuJ (epoch 17 nonce 32 — exactly on its target, a win):# python3 verify-limbo.py 441045a40a9fdbea912bb7c77afb1d2e audite7134d0080bc 32 1.16 0.10# commitment = e9fe937d57fa02b85c8a610297ea3caaba8decc6383d5c07e2e303067e822126# draw = 84760019 (chunk 0)# result = 1.16# win = true (1.16 >= 1.16) | payout = 0.11600000# Expected, bet yAVLBL0QWD0RVuewVxG_v (epoch 3 nonce 29 — lifted to the 1× floor):# python3 verify-limbo.py 56481f25aad5d0b6644fbfff05d4e468 audit8b0f66ee7a38 29 3.76 0.10# commitment = ecf1e700cd3852d869c2e44a5794e854aff5cdf50af7783a25196528d197e653# draw = 99762668 (chunk 0)# result = 1.00 (raw 0.99, lifted to 1.00)# win = false (1.00 < 3.76) | payout = 0.00000000# Expected, bet krv88h79b6gDGRTDgswi9 (epoch 5 nonce 2 — first chunk discarded):# python3 verify-limbo.py b73582d05dec4dd4a43b4e0b72ec3ebd audited5df347a318 2 1.59 0.10# commitment = 7d57ba291003c8aa316f52b6b94d6b0100d5ff1b830ee1c3c6ba677f7c9a3033# draw = 66691774 (chunk 1; chunk 0 discarded)# result = 1.48# win = false (1.48 < 1.59) | payout = 0.00000000# Expected, bet S4mFs74Bt-nTtdNWQDxk5 (epoch 34 nonce 29 — the capture’s highest result):# python3 verify-limbo.py 06dd164fe01b803ae5a843b4b3fc4bbb audit02b3dbac08bb 29 1.17 0.10# commitment = 7375ad4e0375ba1eca0bb0a97ce8b31dfcbf25cef3840e9d3594a00967ffa1ae# draw = 534 (chunk 0)# result = 185046.72# win = true (185046.72 >= 1.17) | payout = 0.117000006.10Evidence Records6 records
The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.
| File | Shows |
|---|---|
| E13-limbo-boundary-probes.json | E13 · Two replies of LIQD’s /verify endpoint on an already-revealed seed whose result is 7.5×: on a 7.5× target a win at 7.5×, on 7.51× a loss — the casino-verifier rows in 6.7 and the verdict table (Step 19) |
| E15-limbo-max-exposure.json | E15 · LIQD’s bet-time refusal of $0.10 at 1,000,000× (MaxExposureExceededError, maximum stake 0.05) — the $50,000 exposure ceiling (4.7, Step 17) |
| E20-target-ceiling-probe-2026-09-26/liqd-limbo-capture-v2-probe-2026-09-26T17-43-21-558Z.journal.ndjson | E20 · The 26 September probe: a $0.10 bet at exactly 10,000× settled; the next request, above 10,000×, was not settled (4.7, Step 25) |
| E20-target-ceiling-probe-2026-09-26/liqd-limbo-capture-v2-probe-2026-09-26T17-47-53-359Z.journal.ndjson | E20 · The probe’s nonce check: the active seed read back at nonce 1, so the request above 10,000× consumed no nonce; further bets, one at 10,000×, settled (Step 25) |
| E21-target-cap-probe/liqd-limbo-cap-probe-2026-10-02T18-22-46-633Z.journal.ndjson | E21 · The 2 October cap probe: a $0.10 control at 10,000× settled, and 100,101.12×, 497,487.44×, 500,000× and 990,000.01× were each refused on the target rule with no nonce consumed — the certified range (4.7, Step 25) |
| rtp-page-labels-2026-10-01.json | E23 · The RTP and house-edge figures on LIQD’s Limbo game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2) |
Verify a real bet, step by stepVerify a real bet · Swipe for all 4 steps →
How it works
- 1
Place a Limbo Bet
Set a target multiplier from 1.01× to 10,000× and play. The result is drawn from the seeds and the nonce alone — the target and the stake never enter the draw. The target decides the win: a result at or above it pays stake × target; below it, the bet is lost.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the Limbo game page. Its Seeds tab shows the commitment for your seed pair — the hashed server seed, published before you play — and the hash of the next server seed, fixed before you choose your next client seed. Rotating ends the epoch and reveals the outgoing server seed in plaintext; the client seed you submit with the rotation becomes the new active one. 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
Enter the Seeds in the Verify Tab
Open the panel’s Verify tab — a calculator that recomputes a round’s result from a server seed, a client seed and a nonce — and enter the revealed server seed, your client seed, the round’s nonce and your target.
- 4
Recompute & Confirm
Press Verify and compare the calculator’s result with your round — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed and the result matches, with the same win or loss against your target, the round was provably fair. The scripts under Manual Verification also take your stake and check the payout to the last decimal.
Visual Walkthroughstep-by-step
Set your target — any multiplier from 1.01× to 10,000× in steps of 0.01 — and play. The platform computes the result from (serverSeed, clientSeed, nonce) before your target is applied: the target decides whether the result wins and what a win pays, never what is drawn.
Target — the multiplier you set; the round wins when the result reaches it, and a win pays stake × target.
Result — the multiplier the round draws, never below 1×; it is the same whatever target or stake you choose.
Win chance — 99% ÷ target, rounded down to whole draws: 49.5% at 2×, 0.0099% at 10,000× — the figures LIQD’s game page states.

LIQD Limbo on liqd.com — a $1.00 bet at a 2.00× target (win chance 49.50%, payout on win $2.00); this round drew 17.07×, the one before it 1.98×.
The panel has two tabs, Seeds and Verify. Before you play, the Seeds tab shows the commitment for your active seed pair and for the next one, with your client seed and the bets made with the pair; LIQD’s provably-fair API publishes the same values. Rotation ends the epoch: the outgoing server seed is revealed for verification, the pre-committed next seed becomes active, and the client seed you submit with the rotation takes effect. That advance commitment is the chain the audit’s Step 2 checks across all 133 links, plus the pre-capture link into epoch 0.
Active commitment — Active Server Seed (Hashed) on the Seeds tab, activeServerSeedHash in LIQD’s provably-fair API: the SHA-256 of the current epoch’s server seed; the plaintext is revealed when you rotate and must hash to this value.
Next commitment — Next Server Seed under Rotate Seed Pair, nextServerSeedHash in the API: published before you set the next client seed; it becomes the active commitment when you rotate.
Revealed server seed — returned by the rotation as revealedServerSeed, 32 hex characters; its SHA-256, taken over the hex text, must equal the commitment you saw before betting.
Client seed — your input for the new epoch, sent with the rotation. Set after the next commitment is published, an unpredictable client seed means the server seed was fixed before your seed could be known.

The Seeds tab before rotating — Active Server Seed (Hashed) 523d9aa4…c2eeb5 after 2 bets, and the next server seed’s hash already published. The seed revealed by the rotation, 39e1fead…136654, hashes to 523d9aa4…c2eeb5.
The Verify tab recomputes a round on demand from a server seed, client seed and nonce, and with your target it shows whether the round won — the audit read four of its values, and our implementation reproduces all four. Enter the server seed revealed in step 2, your client seed, the round’s nonce and your target. Everything this audit checks cryptographically, the calculator and the scripts below let you check by hand.
Verify calculator — the Verify tab: recomputes a round’s result from a server seed, client seed and nonce; our implementation reproduces the four values the audit read from it.
Nonce — counts your bets under the pair from 0 and resets at rotation; it locates each round within the epoch.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 1 and the 2.00× target.
Press Verify. The calculator recomputes the result from the three seed inputs, and your target decides the round — a result at or above it wins and pays stake × target. A match shows the result is the one the committed seed produces. The scripts in the Manual Verification section below take the target and the stake as well, so they check the draw, the win and the payout too — with nothing but Node.js or Python, pre-loaded with the first round of this capture and the expected output of four more.
Commitment — the SHA-256 of the revealed seed’s hex text; it must equal the hash you saw before betting.
Result — must equal your round’s result to two decimals, as the scripts print it.
Win & payout — a result at or above your target wins stake × target; below it the payout is 0.

The result: 17.07×, a win at the 2.00× target. Our scripts in 6.8–6.9 reproduce it exactly: draw 5,797,579, result 17.07, $2.00 paid 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)
Five moving parts, in the order they act:
- Commit — before you play, LIQD publishes
SHA-256of 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 digest is read as eight 4-byte big-endian chunks, and the first chunk below 4,200,000,000 is taken modulo 100,000,000 — that is the draw k, 0 to 99,999,999. The guard discards the chunks at or above 4,200,000,000 (2.2111% of them), so every draw is exactly as likely as every other. - Result —
max(1, floor(9,900,000,000 ÷ (k + 1)) ÷ 100), a multiplier with two decimals, never below 1×: the smaller the draw, the higher the result. - Win — the round wins if the result is at or above your target; a result exactly on the target wins.
- Pay — a win credits stake × target, never the result; a loss pays 0. Both are two-decimal amounts, so the credit is exact.
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 list the expected output of five rounds from this report in their comments so you can self-check the tool before trusting it. The other classic slips are the guard — taking the first chunk without checking it changes the result of about one round in 45, all 126 such rounds among the 6,700 scored — and the floor: the lowest result is 1×, not 1.01×, and it loses against every target. LIQD’s game code names 1,000,000× as the maximum result — only draws 0–97 would go higher, and they win every target either way.
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:
| LIQD Built-in (Verify calculator) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino | ProvablyFair.org — open source |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | As a cross-check — our implementation reproduces the 4 values recorded from its Verify calculator (a unit test) and both replies of its /verify endpoint at the boundary (Step 19); live, the endpoint agreed on every round the integrity tests checked against it | The scripts in 6.8–6.9 implement the derivation this audit specifies — the HMAC-SHA256 draw and its guard, the result map, the win rule and the payout — and reproduce all 6,700 scored rounds |
| If they disagree | Suspect the casino’s tool or a tampered round | The repo + scripts are the tiebreaker anyone can run |
6.8JavaScript Verification Script
Complete, dependency-free, and pre-loaded with the first round of the capture as its default inputs — run it as-is to see a full match, then swap in your own round. Its comments list the expected output of four more rounds from this report: the win exactly on its target, a result lifted to 1×, a round whose first chunk was discarded, and the capture’s highest result. Run on every one of the 6,700 scored rounds, it reproduces each one: commitment, draw, result, win and payout.
// verify-limbo.js — standalone LIQD Limbo verifier (Node.js, no dependencies)// Usage: node verify-limbo.js <serverSeed> <clientSeed> <nonce> <target> [stake]const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonce, targetArg, stakeArg = '0.10'] = argv.length ? argv : ['ee4724f00cfc46e28425e9de3239d427', 'audita5a8f43bd18d', '0', '4.58'];// Amounts as exact whole hundredths: '4.58' → 458nconst hundredths = s => { const m = /^(\d+)(?:\.(\d{1,2}))?$/.exec(s); if (!m) throw new Error(`not a 2-decimal amount: ${s}`); return BigInt(m[1]) * 100n + BigInt((m[2] || '').padEnd(2, '0'));};const target = hundredths(targetArg), stake = hundredths(stakeArg);const fmt = h => `${h / 100n}.${String(h % 100n).padStart(2, '0')}`; // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — the hash LIQD showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) The draw — HMAC-SHA256 keyed on the DECODED bytes, message clientSeed:nonce:0; the first// 4-byte big-endian chunk below 4,200,000,000 (= 42 × 10^8), taken modulo 10^8const digest = createHmac('sha256', Buffer.from(serverSeed, 'hex')) .update(`${clientSeed}:${nonce}:0`).digest();let chunk = 0;while (chunk < 8 && digest.readUInt32BE(4 * chunk) >= 4200000000) chunk++;if (chunk === 8) throw new Error('all eight chunks rejected (about 5.7e-14 per round)');const k = digest.readUInt32BE(4 * chunk) % 100000000;const skipped = chunk === 0 ? '' : chunk === 1 ? '; chunk 0 discarded' : `; chunks 0-${chunk - 1} discarded`;console.log(`draw = ${k} (chunk ${chunk}${skipped})`); // 3) Result in hundredths: floor(9,900,000,000 / (k + 1)), never below 1.00. (LIQD's game code// names 1,000,000× as the maximum result — only draws 0–97 would go higher, and they win// every target either way.)const raw = 9900000000n / BigInt(k + 1);const result = raw < 100n ? 100n : raw;console.log('result =', fmt(result) + (raw < 100n ? ` (raw ${fmt(raw)}, lifted to 1.00)` : '')); // 4) Win iff result >= target; a win pays stake × target exactly:// credit × 10^8 = stake¢ × target¢ × 10^4const win = result >= target;const credit = win ? stake * target * 10000n : 0n; // in units of 10^-8const payout = `${credit / 100000000n}.${String(credit % 100000000n).padStart(8, '0')}`;const verdict = `${win} (${fmt(result)} ${win ? '>=' : '<'} ${fmt(target)})`;console.log(`win = ${verdict} | payout = ${payout}`);// Expected, defaults (bet USzRL9gyMBxZg8NLCNoYS, epoch 0 nonce 0, 4.58× at $0.10 — a loss):// commitment = 394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded// draw = 40058996 (chunk 0)// result = 2.47// win = false (2.47 < 4.58) | payout = 0.00000000// Expected, bet bYS2viIeJOfIRm1YpweuJ (epoch 17 nonce 32 — exactly on its target, a win):// node verify-limbo.js 441045a40a9fdbea912bb7c77afb1d2e audite7134d0080bc 32 1.16 0.10// commitment = e9fe937d57fa02b85c8a610297ea3caaba8decc6383d5c07e2e303067e822126// draw = 84760019 (chunk 0)// result = 1.16// win = true (1.16 >= 1.16) | payout = 0.11600000// Expected, bet yAVLBL0QWD0RVuewVxG_v (epoch 3 nonce 29 — lifted to the 1× floor):// node verify-limbo.js 56481f25aad5d0b6644fbfff05d4e468 audit8b0f66ee7a38 29 3.76 0.10// commitment = ecf1e700cd3852d869c2e44a5794e854aff5cdf50af7783a25196528d197e653// draw = 99762668 (chunk 0)// result = 1.00 (raw 0.99, lifted to 1.00)// win = false (1.00 < 3.76) | payout = 0.00000000// Expected, bet krv88h79b6gDGRTDgswi9 (epoch 5 nonce 2 — first chunk discarded):// node verify-limbo.js b73582d05dec4dd4a43b4e0b72ec3ebd audited5df347a318 2 1.59 0.10// commitment = 7d57ba291003c8aa316f52b6b94d6b0100d5ff1b830ee1c3c6ba677f7c9a3033// draw = 66691774 (chunk 1; chunk 0 discarded)// result = 1.48// win = false (1.48 < 1.59) | payout = 0.00000000// Expected, bet S4mFs74Bt-nTtdNWQDxk5 (epoch 34 nonce 29 — the capture’s highest result):// node verify-limbo.js 06dd164fe01b803ae5a843b4b3fc4bbb audit02b3dbac08bb 29 1.17 0.10// commitment = 7375ad4e0375ba1eca0bb0a97ce8b31dfcbf25cef3840e9d3594a00967ffa1ae// draw = 534 (chunk 0)// result = 185046.72// win = true (185046.72 >= 1.17) | payout = 0.117000006.9Python Verification Script
The same verifier in Python 3, standard library only, with exact integer arithmetic throughout — so it doubles as an independent check on the JavaScript version. It reproduces all 6,700 scored rounds as well:
# verify-limbo.py — standalone LIQD Limbo verifier (Python 3, standard library only)# Usage: python3 verify-limbo.py <serverSeed> <clientSeed> <nonce> <target> [stake]import hashlib, hmac, struct, sysfrom fractions import Fraction args = sys.argv[1:] or ['ee4724f00cfc46e28425e9de3239d427', 'audita5a8f43bd18d', '0', '4.58']server_seed, client_seed, nonce = args[0], args[1], int(args[2]) def hundredths(s): # amounts as exact whole hundredths: '4.58' -> 458 h = Fraction(s) * 100 if h.denominator != 1: raise ValueError(f'not a 2-decimal amount: {s}') return h.numerator target, stake = hundredths(args[3]), hundredths(args[4] if len(args) > 4 else '0.10')fmt = lambda h: f'{h // 100}.{h % 100:02d}' # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) The draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:0; the first# 4-byte big-endian chunk below 4,200,000,000 (= 42 × 10^8), taken modulo 10^8message = f'{client_seed}:{nonce}:0'.encode()digest = hmac.new(bytes.fromhex(server_seed), message, hashlib.sha256).digest()for chunk, value in enumerate(struct.unpack('>8I', digest)): if value < 4200000000: k = value % 100000000 breakelse: raise ValueError('all eight chunks rejected (about 5.7e-14 per round)')skipped = ('' if chunk == 0 else '; chunk 0 discarded' if chunk == 1 else f'; chunks 0-{chunk - 1} discarded')print(f'draw = {k} (chunk {chunk}{skipped})') # 3) Result in hundredths: floor(9,900,000,000 / (k + 1)), never below 1.00raw = 9900000000 // (k + 1)result = max(100, raw)print('result =', fmt(result) + (f' (raw {fmt(raw)}, lifted to 1.00)' if raw < 100 else '')) # 4) Win iff result >= target; a win pays stake × target exactly:# credit × 10^8 = stake¢ × target¢ × 10^4win = result >= targetcredit = stake * target * 10**4 if win else 0 # in units of 10^-8payout = f'{credit // 10**8}.{credit % 10**8:08d}'verdict = f"{'true' if win else 'false'} ({fmt(result)} {'>=' if win else '<'} {fmt(target)})"print(f'win = {verdict} | payout = {payout}')# Expected, defaults (bet USzRL9gyMBxZg8NLCNoYS, epoch 0 nonce 0, 4.58× at $0.10 — a loss):# commitment = 394004d9c91573202b92489b53a5e8eff34ac5b3d34dac76c7379cadd0f34ded# draw = 40058996 (chunk 0)# result = 2.47# win = false (2.47 < 4.58) | payout = 0.00000000# Expected, bet bYS2viIeJOfIRm1YpweuJ (epoch 17 nonce 32 — exactly on its target, a win):# python3 verify-limbo.py 441045a40a9fdbea912bb7c77afb1d2e audite7134d0080bc 32 1.16 0.10# commitment = e9fe937d57fa02b85c8a610297ea3caaba8decc6383d5c07e2e303067e822126# draw = 84760019 (chunk 0)# result = 1.16# win = true (1.16 >= 1.16) | payout = 0.11600000# Expected, bet yAVLBL0QWD0RVuewVxG_v (epoch 3 nonce 29 — lifted to the 1× floor):# python3 verify-limbo.py 56481f25aad5d0b6644fbfff05d4e468 audit8b0f66ee7a38 29 3.76 0.10# commitment = ecf1e700cd3852d869c2e44a5794e854aff5cdf50af7783a25196528d197e653# draw = 99762668 (chunk 0)# result = 1.00 (raw 0.99, lifted to 1.00)# win = false (1.00 < 3.76) | payout = 0.00000000# Expected, bet krv88h79b6gDGRTDgswi9 (epoch 5 nonce 2 — first chunk discarded):# python3 verify-limbo.py b73582d05dec4dd4a43b4e0b72ec3ebd audited5df347a318 2 1.59 0.10# commitment = 7d57ba291003c8aa316f52b6b94d6b0100d5ff1b830ee1c3c6ba677f7c9a3033# draw = 66691774 (chunk 1; chunk 0 discarded)# result = 1.48# win = false (1.48 < 1.59) | payout = 0.00000000# Expected, bet S4mFs74Bt-nTtdNWQDxk5 (epoch 34 nonce 29 — the capture’s highest result):# python3 verify-limbo.py 06dd164fe01b803ae5a843b4b3fc4bbb audit02b3dbac08bb 29 1.17 0.10# commitment = 7375ad4e0375ba1eca0bb0a97ce8b31dfcbf25cef3840e9d3594a00967ffa1ae# draw = 534 (chunk 0)# result = 185046.72# win = true (185046.72 >= 1.17) | payout = 0.117000006.10Evidence Records6 records
The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.
| File | Shows |
|---|---|
| E13-limbo-boundary-probes.json | E13 · Two replies of LIQD’s /verify endpoint on an already-revealed seed whose result is 7.5×: on a 7.5× target a win at 7.5×, on 7.51× a loss — the casino-verifier rows in 6.7 and the verdict table (Step 19) |
| E15-limbo-max-exposure.json | E15 · LIQD’s bet-time refusal of $0.10 at 1,000,000× (MaxExposureExceededError, maximum stake 0.05) — the $50,000 exposure ceiling (4.7, Step 17) |
| E20-target-ceiling-probe-2026-09-26/liqd-limbo-capture-v2-probe-2026-09-26T17-43-21-558Z.journal.ndjson | E20 · The 26 September probe: a $0.10 bet at exactly 10,000× settled; the next request, above 10,000×, was not settled (4.7, Step 25) |
| E20-target-ceiling-probe-2026-09-26/liqd-limbo-capture-v2-probe-2026-09-26T17-47-53-359Z.journal.ndjson | E20 · The probe’s nonce check: the active seed read back at nonce 1, so the request above 10,000× consumed no nonce; further bets, one at 10,000×, settled (Step 25) |
| E21-target-cap-probe/liqd-limbo-cap-probe-2026-10-02T18-22-46-633Z.journal.ndjson | E21 · The 2 October cap probe: a $0.10 control at 10,000× settled, and 100,101.12×, 497,487.44×, 500,000× and 990,000.01× were each refused on the target rule with no nonce consumed — the certified range (4.7, Step 25) |
| rtp-page-labels-2026-10-01.json | E23 · The RTP and house-edge figures on LIQD’s Limbo game page, read logged out on 1 October 2026 — the source of the RTP claim (S4.2) |
Only disclosed inputs are used. Identical inputs always produce identical output.
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.
Repository Details
- GitHub: ProvablyFair-org/liqd-limbo
- Release: v1.0.1 · Framework 1.1
- Game: Limbo (liqd-limbo)
- Public Verifier: provablyfair.org/verify-bets/liqd
Prerequisites
- Node.js 22.x (
package.jsonengines and.nvmrc) — no dependency install fornpm testornode test.js && node verify.js - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.8.0) — fornpm run verify,npm run test:fulland the source commands - Python 3 — only for the optional standalone checker in S6
- GitHub: ProvablyFair-org/liqd-limbo
- Release: v1.0.1 · Framework 1.1
- Game: Limbo (liqd-limbo)
- Public Verifier: provablyfair.org/verify-bets/liqd
- Node.js 22.x (
package.jsonengines and.nvmrc) — no dependency install fornpm testornode test.js && node verify.js - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.8.0) — fornpm run verify,npm run test:fulland the source commands - Python 3 — only for the optional standalone checker in S6
All scored results reproduce from release v1.0.1, the pinned captures and the commands above. The 6,700-round capture is hash-checked before any step runs, so you are provably testing the same rounds; the supplement and its journal, the three probe journals Step 25 reads, the simulation artifact, the Pass 1 grid, the convergence chart and the RTP claim table are pinned by SHA-256 — ten pins in all — and every run is compared with the committed results. Step 21 keeps the declared scope honest too: on every run it re-checks four of the package’s declared limitations against the capture and fails if one stops being true. The simulations are deterministic — npm run test:full regenerates the reference run, the Pass 1 grid and the Pass 2 block — from their pinned master seeds and the capture’s revealed seeds — byte for byte.
Commands to Reproduce
The 6,700-round capture, the 26 September supplement and its journal, the evidence files and the committed results ship inside the repo — no separate download, and every pinned file is checked against its SHA-256, the capture before any step runs. npm test is zero-install: it runs the staleness gate, the release-manifest check, the 27 unit tests and the 25-step verification, and exits with the verdict’s code; npm ci adds the TypeScript tools for the source commands. Node.js 22.x is required. Output as run for this report on Node 22.22.0 in 37 s, abridged where marked “…”.
Every run writes only to outputs/run/ and compares its results with the committed record (outputs/run/diff.json) — that comparison is the reproduction gate. Exit codes: 0 Full Pass · 2 Conditional Pass · 5 RTP FIGURE OVERSTATED (the RTP claim rule finds the published figure overstated) · 3 suite-integrity failure (not every step scored) · 1 any FAIL, a reproduction beyond tolerance or an RTP claim table mismatch. Output as run on Node 22.22.0 in 43 s, abridged where marked “…”.
Regenerates the reference run and Step 23’s Pass 1 grid from their pinned master seeds, and the Pass 2 block from the 134 revealed server seeds of the pinned capture, into outputs/run/, and requires each to be byte-identical to the committed file: the reference run (simulate:check — 2,000,000 uniformity draws, 20,000,000 rounds at 2.00× and 8 targets × 2,000,000 rounds, written with its Pass 2 block and the convergence chart; the program labels its lines “Pass 1”, and its “served-target” return is the played-target return of 4.5), the Pass 1 grid (pass1:check — 2,000,000 draws and 10 targets × 1,000,000 rounds) and the Pass 2 block on its own (pass2:check — 134 revealed seeds × 10,000 nonces). The 27 unit tests run first and verification last. Run for this report on Node 22.22.0: all three BYTE-IDENTICAL, 25 / 25, in 6 minutes 27 seconds; output abridged where marked “…”.
Audit Reproducibility Pinning
- Release Tag
- v1.0.1 — github.com/ProvablyFair-org/liqd-limbo/tree/v1.0.1
- Framework
- ProvablyFair.org Audit Framework 1.1 · Limbo 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 inRELEASE-MANIFEST.json - Node Version
- 22.x (
package.jsonengines,.nvmrc22) — release runs on 22.23.1, this report’s runs on 22.22.0 - Primary Dataset
- data/limbo-master-6700bets.json (6,700 bets, 134 seed epochs)
- Primary Dataset Hash (SHA-256)
- 8e3ce5b5e1494d1fcc3a783965d3ba7b6a7099f120a64d61e21a577098c07334
- Supplement Dataset Hash (SHA-256)
- a2dee374cfd48c62736cf5958fd67f42b1cb7c622ab0883c3f84278d8429ab1e
- Supplement Journal Hash (SHA-256)
- bcacab9c674df62a3b2219cba008542f2e798e9c81559c6321489e1a43949c32
- Target-Cap Probe Journal Hash (SHA-256)
- 14f9880eff8294a5a7d8b0ce3fe715093eda5061899713ff87d3c9c098719809
- Simulation Artifact Hash (SHA-256)
- 2d609344650eac723c262449fb8929b05c68b75f149ceee708b89ad4ade670cc
- Convergence Chart Hash (SHA-256)
- efd121156f8255f391eb9c1b3c6f9f06c008d35807cef9a69b1e82be33fd0d69
- Pass-1 Grid Hash (SHA-256)
- 9999f98ba17fe534b5350274da8e423aa6c1c096f4e136d40daec60a0cbdcae7
- RTP Claim Table Hash (SHA-256)
- cdfbf600e0713b822acf00b420c10d12b7af7fca73cce96c395099f49634ba34
- Reproduction Gate
- Every run writes only to outputs/run/ and is compared with the committed results (outputs/run/diff.json) — exact content byte for byte, a derived floating-point statistic within 1e-14 absolute + 1e-12 relative; anything beyond tolerance exits 1
- Exit Codes
- 0 Full Pass · 2 Conditional Pass · 5 RTP FIGURE OVERSTATED · 3 suite-integrity failure · 1 any FAIL, a reproduction beyond tolerance or an RTP claim table mismatch
- Mutation Battery
- 117 entries — 75 single-file mutations, 41 structural forgeries and 1 declared survivor: 116 caught; the survivor is an exported helper nothing calls
- Runtime
npm test≈ 37 s ·npm run verify≈ 43 s ·node test.js≈ 0.7 s ·npm run test:full≈ 6½ min (this report’s runs, Node 22.22.0) ·npm run mutate≈ 27 min (as the release’s README records it)- Capture Environment
- Pre-launch host qa.liqd.com · the 6,700 scored rounds from one authenticated account in one session · USDC
- Audit Date
- September–October 2026 — scored capture 11 September, supplement 26 September, target-cap probe 2 October 2026
- Audit ID
- PF-2026-LQ05
Output Artifacts5 files committed
| File | Contents | Size |
|---|---|---|
| verification-results.json | Steps 1–25 results — per-step status with evidence detail strings and profile ids, profile coverage (LIMBO-01 … LIMBO-31), the pinned hashes, the RTP claim block, the informational items and the verdict | ~66 KB |
| simulation-results.json | The reference run (2,000,000 uniformity draws, serial screens, the 20,000,000-round convergence series at 2.00×, 8 targets × 2,000,000 rounds) + the Pass 2 block (134 revealed seeds × 10,000 nonces, both screens, per-seed ranks) | ~23 KB |
| pass1-grid.json | Pass 1 on its pinned master seed — the 100-bin histogram of 2,000,000 draws, the serial sums and 10 targets × 1,000,000 rounds with their win counts, re-derived by Step 23 | ~4.8 KB |
| rtp-claim-table.json | The RTP claim rule’s table — LIQD’s published 99% against all 999,900 targets from 1.01× to 10,000×, by target decade with the extremes and the outcome | ~3.5 KB |
| rtp-convergence.html | Convergence chart rendered from the reference run’s saved series at 2.00× (self-contained SVG/HTML) | ~2.5 KB |
Repository Structure
liqd-limbo/ ├── src/ │ ├── rng.ts → commitment hash · HMAC draw with its guard (generateProvablyFairNumber) · limboDraw · limboResult — written from docs/ALGORITHM.md │ ├── bias.ts → the draw-bias bound, from the same maxFair rule the draw applies, and its acceptance probe (Step 5) │ ├── config.ts → the result map (resultMultiplier), win chance and exact RTP, the win rule, the limits, population constants and 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 RTP claim table over all 999,900 targets up to 10,000×, LIQD’s published claim and the table’s pin │ ├── profile-map.ts → which Limbo profile check (LIMBO-01 … LIMBO-31) each step implements │ ├── loader.ts → dataset loader + SHA-256 guard (aborts before any step on a mismatch) │ ├── pass1-grid.ts → Pass 1: 2,000,000 draws + 10 targets × 1,000,000 rounds from the pinned master seed → outputs/pass1-grid.json │ ├── pass2.ts · pass2-emit.ts → Pass 2: 134 revealed seeds × 10,000 nonces, deterministic → the Pass 2 block of outputs/simulation-results.json │ ├── simulate.ts → the reference run (2M draws · 20M rounds at 2.00× · 8 targets × 2M) and the Pass 2 block → outputs/simulation-results.json, rtp-convergence.html │ ├── stats.ts → chi-squared (exact incomplete gamma), lag-1, runs test, normal and binomial tails │ ├── 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) │ └── types.ts → capture record types ├── docs/ │ ├── ALGORITHM.md → the specification the RNG is written from │ └── INTERFACE.md → the module interface the RNG implements ├── tests/ │ ├── verify.ts → 25-step verification runner (results under outputs/run/; PF_EMIT=1 to regenerate the committed record) │ ├── limbo/ → 27 unit tests (RngSpecTests 1 [72 checks] · rngTests 14 · resultFloorTests 7 · diffTests 2 · capProbeTests 3) │ ├── mutate.ts · mutations.json · structural-forgeries.json · mutation-survivors.json → 75 mutations + 41 forgeries, 1 declared survivor │ ├── forgeries/ → the 41 structural forgeries, as 11 executable scripts │ └── steps/ │ ├── commitment.ts → Steps 1–4 (seed hashes, chain + pre-capture link, epoch coverage, nonce windows) │ ├── determinism.ts → Steps 5–7 (result recompute + bias bound, win rule, determinism) │ ├── payouts.ts → Steps 8–11 (paid multiplier, credit, losses, wallet fields + limits) │ ├── dataset.ts → Steps 12–13 (dataset identity + population, phase plan) │ ├── anti-circularity.ts → Steps 14–15 (negative control + boundary anchor, win-chance identity) │ ├── edge.ts → Steps 16–17 (effective edge at every target, the 1,000,000× purchase check) │ ├── phase-d.ts → Step 18 (custom client seeds + controls) │ ├── standardization.ts → Steps 19–21 (artifact integrity, settlement determinism, declared limitations) │ ├── live-fit.ts → Step 22 (live-sample fit, informational) │ ├── pass1.ts → Step 23 (Pass 1 re-derived from the grid) │ ├── pass2.ts → Step 24 (Pass 2 re-derived from the revealed seeds) │ ├── supplement.ts → Step 25 (the supplement and the live target ceiling) │ ├── statistical.ts → informational items, not scored (live return, win rate, result range, per-phase returns) │ └── context.ts → the scored-step contract and its guards ├── scripts/ → npm test’s staleness gate, release manifest + release bar, standalone-bundle builder ├── capture/ → the capture runners, their shared engine and offline self-tests ├── data/ │ ├── limbo-master-6700bets.json → the scored capture: 6,700 rounds · 134 epochs · SHA-256 pinned │ └── liqd-limbo-capture-v2-2026-09-26T17-48-02-207Z.json · .journal.ndjson → the 300-round supplement and its journal (Step 25) · SHA-256 pinned ├── outputs/ │ ├── verification-results.json → 25 steps + profile coverage + artifact hashes + RTP claim block + informational items + verdict │ ├── simulation-results.json → the reference run + the Pass 2 block │ ├── pass1-grid.json → Pass 1’s histograms, serial sums and win counts │ ├── rtp-claim-table.json → the RTP claim rule over all 999,900 targets up to 10,000× │ └── rtp-convergence.html → self-contained convergence chart (the reference run at 2.00×) ├── evidence/ │ ├── E13-limbo-boundary-probes.json → LIQD’s /verify endpoint at the boundary (7.5× against 7.51×) │ ├── E15-limbo-max-exposure.json → the $50,000 exposure ceiling │ ├── E20-target-ceiling-probe-2026-09-26/ → two probe journals at the 10,000× ceiling · SHA-256 pinned │ ├── E21-target-cap-probe/ → the 2 October cap probe; Step 25’s journal is SHA-256 pinned │ └── rtp-page-labels-2026-10-01.json → LIQD’s Limbo page figures — the RTP claim ├── .github/workflows/reproduce.yml → second-machine reproduction on a clean runner (ubuntu-latest, Node from .nvmrc, npm ci && npm test) ├── test.js · verify.js → dependency-free bundles (Node 22) ├── README.md · MANIFEST.md → the report (findings, evidence register, worked rounds, limitations register) ├── RELEASE-MANIFEST.json → release identity: tag, framework, artifact hashes, verdict, signing key ├── package.json → npm test = staleness gate, release-manifest check, node test.js, node verify.js · verify · test:full · mutate └── package-lock.json · tsconfig.json · .mocharc.yml · .nvmrc · .gitattributes · .gitignore · standalone-manifest.json · hygiene-allow.json · LICENSE
Step-to-Section Cross-Reference25 verification steps mapped
| Step | Test Name | Report Section |
|---|---|---|
| 1 | Seed Hash Integrity | S1, S2, S3, S6 |
| 2 | Next-Seed Pre-Commitment Chain | S1, S6 |
| 3 | Hash Consistency & Epoch Coverage | S1 |
| 4 | Nonce Audit | S1 |
| 5 | Draw & Result Recomputation | S1, S2, S3, S4, S6 |
| 6 | Win-Rule Reproduction | S1, S2, S3, S4 |
| 7 | Recomputation Determinism | S1, S3 |
| 8 | Paid-Multiplier Derivation | S3, S4 |
| 9 | Credit Arithmetic | S1, S3, S4 |
| 10 | Loss Settlement | S2, S3, S4 |
| 11 | Wallet Ledger, Stake Floor & Exposure Ceiling | S3, S4 |
| 12 | Dataset Integrity & Population Identity | S3, S7 |
| 13 | Phase Plan Conformance | S2, S3, S4 |
| 14 | Anti-Circularity (Negative Control + Grid Boundary Anchor) | S4 |
| 15 | Win-Chance vs Result-Distribution Identity | S4 |
| 16 | Effective-Edge Curve | S4 |
| 17 | Client-Code Maximum Multiplier | S4, S6 |
| 18 | Custom Client-Seed Control (Phase D) | S1, S2, S3, S6 |
| 19 | Artifact Integrity & Evidence Reproduction | S2, S4, S6, S7 |
| 20 | Settlement Determinism | S3, S4 |
| 21 | Scope & Declared Limitations | S7 |
| 22 | Live-Sample Fit | S2 |
| 23 | Simulation Pass 1 — Pinned Master Seed, Re-Derived | S2, S4 |
| 24 | Simulation Pass 2 — Cherry-Pick Screen, 10,000 Nonces per Revealed Seed | S4 |
| 25 | Target-Decade Supplement & Live Target Ceiling | S3, S4, S6, S7 |
Game Profile CoverageLimbo profile 1.0.2 · 31 checks
Every check in the ProvablyFair.org Limbo profile, version 1.0.2, is mapped to a scored step, to a package check, or marked n/a with a reason — 22 to scored steps, 7 to package checks, 2 n/a. Rows the repo marks full read Full; for LIMBO-09 and LIMBO-24 the row states the exact scope this audit covers. The map ships in src/profile-map.ts and is written beside the step results on every run; the committed outputs/verification-results.json carries it.
| Check | What it requires | Covered by | Coverage |
|---|---|---|---|
| LIMBO-01 | Every revealed seed hashes to its commitment | Step 1 | Full |
| LIMBO-02 | Next-seed pre-commitment chain, including the pre-capture link | Step 2 | Full |
| LIMBO-03 | One commitment per epoch; the committed and revealed sets are identical | Step 3 | Full |
| LIMBO-04 | Nonce continuity — every epoch holds its nonces exactly once, with no reuse and no gaps | Step 4 | Full |
| LIMBO-05 | Audited population bound to code constants, not the dataset header | Steps 12, 13 | Full |
| LIMBO-06 | Client-seed control and sensitivity | Step 18 | Full |
| LIMBO-07 | Every served result recomputes exactly; the derivation is deterministic | Steps 5, 7 | Full |
| LIMBO-08 | Draw-bias bound computed from the draw’s own rejection rule | Step 5 | Full |
| LIMBO-09 | Known-input unit vectors reproduce captured rounds | Package check: docs/ALGORITHM.md + tests/limbo/ (RngSpecTests, rngTests, resultFloorTests) | The specification’s self-test (72 checks), LIQD’s 4 Verify-calculator values and the result-floor vector; the RNG is our own implementation, written from the specification |
| LIMBO-10 | Result range and the win rule — result ≥ target wins | Steps 5, 6 | Full |
| LIMBO-11 | Settlement arithmetic — a win credits stake × target exactly, a loss pays 0, the credit set by (seeds, nonce, target, stake) | Steps 9, 10, 20 | Full |
| LIMBO-12 | Bet-size invariance at an elevated stake | Step 13 | Full |
| LIMBO-13 | The multiplier recorded on every win equals the target; LIQD’s priced win chance equals the result distribution | Steps 8, 15 | Full |
| LIMBO-14 | Target and stake limits — the target range, the minimum stake, the exposure ceiling and the live target ceiling | Steps 11, 17, 25 | Full |
| LIMBO-15 | Target coverage — every decade up to the ceiling played at or above the profile floor; the minimum target played | Steps 13, 25 | Full |
| LIMBO-16 | Wallet balance ledger reconciliation | — | N/A — the capture records per-bet wallet fields, not a running balance; LIQD’s settle-response wallet fields (stakeWallet, winningAmountWallet) are reconciled against the game ledger in Step 11 |
| LIMBO-17 | Boundary rules — the 1× result floor, and a result exactly on the target wins | Steps 5, 6 | Full |
| LIMBO-18 | Anti-circularity — negative control, grid boundary anchor and literal grid tallies | Steps 14, 15 | Full |
| LIMBO-19 | Effective edge at every target against the claimed edge; the RTP claim rule | Step 16 | Full |
| LIMBO-20 | Simulation Pass 1 — uniformity, serial screens and convergence at 10 targets; replayable | Step 23 | Full |
| LIMBO-21 | Cherry-pick test — Simulation Pass 2 | Step 24 | Full |
| LIMBO-22 | Live-sample fit (informational) | Step 22 | Full |
| LIMBO-23 | Framework fairness-integrity rows S5-1 … S5-8, kept as a package checklist (Section 5 reports the live integrity tests) | Package check: MANIFEST.md S5 fairness-integrity matrix | Full |
| LIMBO-24 | Verifier parity — LIQD’s verifier on known inputs; an independent verifier matched to the derivation | LIQD’s verifier: tests/limbo/rngTests.ts and Step 19 · independent: this report’s 6.8–6.9 scripts | LIQD’s own verifier agrees on 6 of 6 checks — 4 / 4 Verify-calculator values and 2 / 2 /verify replies; the 6.8–6.9 scripts on this page reproduce all 6,700 scored rounds |
| LIMBO-25 | Player verification path, in LIQD’s field names | Package check: MANIFEST.md Verify a bet | Full |
| LIMBO-26 | Captures pinned by SHA-256; the loader aborts on a mismatch | Steps 12, 19 | Full |
| LIMBO-27 | Artifacts of record — no run metadata, every numeric value in a declared domain | Steps 19, 23 | Full |
| LIMBO-28 | Release gate — zero-install entry points, pinned runtime, audit ID; regenerations byte-identical | Package check: package.json, .nvmrc, verify.js · test.js, src/simulate.ts | Full |
| LIMBO-29 | Mutation battery with declared survivors | Package check: tests/mutations.json and companions | Full |
| LIMBO-30 | Limitations register and numbered evidence | Package check: MANIFEST.md | Full |
| LIMBO-31 | The same multiplier at every stake | — | N/A as a separate check (bet-size invariance is LIMBO-12, Step 13): LIQD’s client constants carry one house edge, 1.00%, and the edge varies with the target alone (Step 16) |
reproduce-audit.sh
All scored results reproduce from release v1.0.1, the pinned captures and the commands above. The 6,700-round capture is hash-checked before any step runs, so you are provably testing the same rounds; the supplement and its journal, the three probe journals Step 25 reads, the simulation artifact, the Pass 1 grid, the convergence chart and the RTP claim table are pinned by SHA-256 — ten pins in all — and every run is compared with the committed results. Step 21 keeps the declared scope honest too: on every run it re-checks four of the package’s declared limitations against the capture and fails if one stops being true. The simulations are deterministic — npm run test:full regenerates the reference run, the Pass 1 grid and the Pass 2 block — from their pinned master seeds and the capture’s revealed seeds — byte for byte.