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

Blackjack
Independent verification, backed by open-source code.
- Live Bets Verified
- 6,000
- Parity Rate
- 100%
- Simulated Rounds
- 30M
- Simulated RTP 99.5230% per initial bet
- Proven RTP (per initial bet)
- 99.512325%
Verification pipeline
Real bets. Independently reproduced.
Captured on LIQD’s pre-launch environment. We rebuilt the game logic from scratch and matched all 6,000 recorded outcomes.
stand
Recorded hand · Cards reproduced.
- Casino outcome
- 19 · win vs dealer 22
- Our reproduction
- 5 / 5 cards match
- Recorded payout
- $1.3000
- Commitment RevealPass
- Pre-Commitment ChainPass
- Hash ConsistencyPass
- Nonce ContinuityPass
- Epoch SizePass
- Recomputation ParityPass
- Client Seed InfluencePass
- Per-Hand Payout MathPass
- Blackjack 3:2 PayoutPass
- Insurance 2:1 PayoutPass
- Dealer Stands Soft 17Pass
- Double After SplitPass
- Dealer Peek (OBO)Pass
- No Surrender ObservedPass
- Perfect Pairs PayoutsPass
- 21+3 PayoutsPass
- Basic-Strategy RTPPass
- Simulation Pass 1Pass
- Cherry-Pick DetectionPass
- House-Edge ReconciliationPass
- No FX Haircut ObservedPass
- Bet-Size InvariancePass
- Side-Bet ConfigPass
- Phase CoveragePass
- Artifact Hash IntegrityPass
- Phase D — Client SeedsPass
- Split RulesPass
- Insurance OfferPass
- Side-Bet Deal-TimePass
- Deck-Model ConfirmationPass
- Stake Bracket BoundsPass
The repo is the credential. You don’t have to trust us — every scored check ships as code. Run npm test to re-verify the audit: 117 unit tests and the 31 verification steps — all 6,000 live hands re-verified card for card and the committed simulation scored — with nothing installed but Node.js 22; npm run simulate deals the simulation again.
This audit independently validates the Blackjack game operated by LIQD under the ProvablyFair.org Audit Framework 1.1. We placed 6,000 real bets across 120 seed epochs — 34,734 dealt cards, 401 splits and 11,600 side bets, plus 446 insurance bets, one on every insurance offer (S3.7) — and recomputed every hand with our own implementation, written from a specification without LIQD’s code. The capture ran on LIQD’s pre-production environment, qa.liqd.com, so the certificate is provisional: it extends to liqd.com after an anonymous production capture passes review.
Audit Verdict
| Check | Result | Evidence |
|---|---|---|
| Overall Status | Pass | PROVABLY FAIR — Full Pass · Passed 31/31 · Hard fails 0 · Flags 0. Provisional: captured on LIQD’s pre-production environment, qa.liqd.com; certification extends to liqd.com after an anonymous production capture passes review |
| RTP Verified | Pass | 99.512325% Proven RTP per initial bet under basic strategy, insurance declined (house edge 0.487675%; 99.567565% per total wagered) — an exact 8-deck solve, within one unit of the last digit of the published Wizard of Odds figure (0.48768%) · 99.5230% simulated (30M rounds, 0.51σ) · RTP claim rule: LIQD’s published 99.52% holds — gap −0.007675 pp, inside the ±0.1 pp band (RTP-CLAIM 1.2.0, Step 20) |
| Side-Bet RTP | Pass | Perfect Pairs 97.8313% RTP (edge 9/415 = 2.1687%) · 21+3 96.2961% RTP (edge 4596/124085 = 3.7039%) — exact enumeration over the 416-card shoe with the verified paytables (25 / 13 / 6 and 100 / 40 / 30 / 10 / 5); all 11,600 live side bets settle exactly (Steps 15, 16, 20) |
| Live ↔︎ Verifier Parity | Pass | 100% — 6,000 / 6,000 hands, 34,734 dealt cards matched in strict shoe order, all 401 split rounds included; the ProvablyFair.org verifier reproduces all 401 split rounds and the first round of every epoch (515 rounds, Step 6) |
| Commit-Reveal System | Pass | 120 / 120 revealed seeds hash to their commitments; the next-seed chain is consistent on 119 / 119 rotations plus the pre-capture link into epoch 0 (Steps 1–3) |
| Client Seed | Pass | Player-controlled and customizable — a wrong or an empty client seed deals a different first card on 974 and 982 of 1,000 sampled hands, the rate a 1-in-52 chance match predicts; 490 / 500 Phase D hands change their first card under 10 custom seeds (Steps 7, 26) |
| RNG Analysis | Pass | Our own implementation, written from the specification: HMAC-SHA256 draws drive a backward Fisher-Yates over the 416-card shoe, each draw exactly uniform; all 2,490,000 captured draws accepted their first chunk, as the 0.0569 expected rejections predict (Step 6) |
| Payout Logic | Pass | All 6,401 hands, 446 insurance bets and 11,600 side bets reconcile exactly in 1e-8 units — 243 naturals at 3:2, insurance paid 2:1 only on a dealer natural (145 wins), both side-bet paytables (Steps 8–10, 15, 16) |
| Anti-Circularity | Pass | The RTP is solved from the rules alone — an exact 8-deck solve anchored by an exact-rational oracle (1e-12) and the published Wizard of Odds figure; the capture’s own recomputation fields are deleted before any step runs, and no operator figure enters the derivation (Step 17) |
| Fairness Integrity | Pass | 39 fairness integrity tests across 12 categories — 32 pass, 7 remediated (fixed by LIQD and re-verified) |
| Determinism | Pass | Full reproducibility confirmed — 6,000 / 6,000 hands recompute from the recorded inputs (Step 6) |
Overall StatusPass
RTP VerifiedPass
Side-Bet RTPPass
Live ↔︎ Verifier ParityPass
Commit-Reveal SystemPass
Client SeedPass
RNG AnalysisPass
Payout LogicPass
Anti-CircularityPass
Fairness IntegrityPass
DeterminismPass
What Was Audited
✓ Audited
- The RNG is deterministic: every shoe 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 shoe
- Nonces run 0–49 in every seed epoch with no gaps or reuse
- The 416-card, 8-deck shoe is shuffled by a backward Fisher-Yates driven by HMAC-SHA256 draws
- Every dealt card reproduces in strict shoe order, split rounds included
- Nine rules are read off the dealt cards — 8 decks, 3:2, insurance 2:1, stand on soft 17, peek, double on any two, double after split, split on matching value, one card to split aces; the RTP is solved for no re-split, no surrender and insurance declined
- Every hand, insurance bet and side bet pays exactly what the cards and the published paytables give
- Proven RTP is 99.512325% per initial bet under basic strategy — a 0.487675% house edge, solved exactly
- LIQD’s published 99.52% RTP holds under the RTP claim rule (±0.1 pp)
- Bet amount is not an input to the shuffle — the $10 rounds recompute with the same wager-free derivation as the $0.10 rounds
- Players can independently verify every hand
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 by the capture at every rotation — 110 auditor-generated audit… seeds and 10 custom pfaudit- seeds in Phase D |
| Seed Handling | Client seed control, nonce lifecycle, seed epoch rotation |
| RNG Analysis | HMAC-SHA256 Fisher-Yates shoe recomputation, the draw-bias bound, deck-model confirmation, first-card and serial statistics |
| Payout Logic | Hand settlement (3:2 naturals, doubles, splits, pushes), insurance 2:1, side-bet paytables, the $10 stake (Phase E) |
| Live Parity | Independent card-by-card recomputation of all 6,000 hands, including dealer rules and the split draw order, plus the ProvablyFair.org verifier on 515 rounds |
| RTP Validation | Anti-circularity proof, the exact basic-strategy solve with its oracle and Wizard of Odds anchors, the RTP claim rule, the 30M-round simulation, cherry-pick detection (Pass 2), and the house-edge reconciliation |
| Fairness Integrity | Integrity tests on LIQD’s running service — 39 across 12 categories, including race conditions on in-round actions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, hidden-card disclosure and the provably fair surface |
What Audit Guarantees
- Outcomes are deterministic and reproducible from the recorded inputs
- Live game results match independent recomputation for the verified sample (6,000 / 6,000 hands, 34,734 cards)
- The first-card and serial screens find no sign of bias across 1,000,000 rounds simulated through our implementation of the shuffle
- RTP is proven analytically: an exact 8-deck solve gives 99.512325% per initial bet under basic strategy with insurance declined, no re-split and no surrender, anchored by an exact-rational oracle (1e-12) and the published Wizard of Odds figure; 30M simulated rounds are consistent with it (0.51σ)
- The whole 416-card shoe is fixed by the seeds before the first card is dealt — split hands, doubles and the dealer’s draws all come from it, and both side bets settle on the deal
- The client seed is a genuine, player-controlled input — a different seed deals a different first card about 51 times in 52
- The house edge is 0.487675% per initial bet (0.432435% per total wagered); LIQD’s published 99.52% holds under the RTP claim rule, and no FX haircut is observable on any credit
- The 120 revealed casino seeds show no sign of seed selection on the Pass 2 money rule (pooled z = −0.746); the same rule catches a 50-candidate seed grind at z = −23.56, and a fresh client seed set after the commitment is the player’s defence
- Every fairness integrity test in Section 5 passed or was remediated and re-verified — none of its findings changed the shuffle or the order cards are dealt in
- The dataset is SHA-256 pinned — a one-byte change stops the run before any step — and the other artifacts of record are pinned too, so a changed one fails Step 25
References
Blackjack — Game Rules6 sections▶
Blackjack is a single-hand game against the dealer — you try to get closer to 21 without going over. Choose from hit, stand, double, or split. The dealer stands on every 17 and blackjack pays 3:2. LIQD deals from a real 8-deck shoe (416 cards), freshly shuffled for every hand — duplicates can appear, but each card exists at most 8 times. Two optional side bets — Perfect Pairs and 21+3 — resolve on the deal, and insurance is offered when the dealer shows an Ace and you do not hold a natural.
How to Play
1. Place your bet — Choose your stake on the main hand. You may also place independent stakes on Perfect Pairs and 21+3 side bets.
2. Receive the deal — You receive two face-up cards (shoe[0], shoe[2]). The dealer receives one face-up upcard (shoe[1]) and one face-down hole card (shoe[3]). Side bets resolve immediately; if the dealer shows an Ace and you do not hold a natural, insurance is offered before play continues.
3. Decide your action — Choose hit (take the next shoe card), stand (keep your hand), double (take exactly one more card and double your stake), or split (turn a matching-value pair into two hands, each with its own stake).
4. Dealer plays out — After your hands complete, the dealer reveals the hole card and draws from the shoe until reaching 17 or more, standing on every 17 including soft 17.
5. Settlement — Beat the dealer's total without busting to win 1:1; a natural (a two-card 21 on the initial deal, not after a split) pays 3:2; ties push and return your stake.
(serverSeed, clientSeed, nonce) before the deal animation plays. Your decisions don’t change the order of the cards — they set how many are drawn and, after a split, which hand takes each one.Win Conditions
The main bet resolves hand-by-hand against the dealer's final total:
| Outcome | Return | Verified |
|---|---|---|
| Natural blackjack (two-card 21 on the initial deal) | 2.5× stake (3:2) | 243 / 243 winning naturals at exactly 2.5× |
| Win (beat dealer, or dealer busts) | 2× stake (1:1) | Reconciled across all 6,401 player hands |
| Push (tie) | 1× stake returned | Reconciled across all 6,401 player hands |
| Loss / bust | 0 | Reconciled across all 6,401 player hands |
| Doubled hand | 2× the above on a doubled stake | 1,283 doubled rounds (1,448 doubled hands) reconciled |
| Insurance (dealer shows Ace) | 3× the insurance stake if dealer has blackjack | 145 / 145 insurance wins at exactly 3× |
Risk vs Reward
Blackjack is a low-volatility, high-decision game — the proven RTP of 99.512325% per initial bet assumes basic strategy, and deviations cost real percentage points. The side bets are the opposite trade:
- Main bet: 0.487675% house edge per initial bet under basic strategy
- Perfect Pairs: 2.1687% house edge — a pure-luck wager on your first two cards matching
- 21+3: 3.7039% house edge — a pure-luck poker-hand wager on your two cards plus the dealer’s upcard
- Insurance: −EV under basic strategy — taken on every offer, it raises the edge to 0.7612% per initial bet
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Deck Model | 8-deck shoe (416 cards), reshuffled every hand | Duplicate cards can appear — 1,269 rounds contain one, which rules out a single deck (Step 30); every captured card recomputes from the 416-card shoe (Step 6) |
| Dealer Rule | Stand on all 17 (S17) | Including soft 17 — the dealer stood on soft 17 in 106 played-out hands, and every dealer hand replays against S17 with 0 over-draws and 0 under-draws (Step 11) |
| Dealer Peek | US peek / OBO on Ace and ten-value upcards | A dealer natural ends the round before any extra wager — across 152 Ace-up and 131 ten-up dealer naturals, no hand lost a double or split stake (Step 13) |
| Blackjack Payout | 3:2 (return = 2.5× stake) | 243 / 243 winning naturals verified (Step 9) |
| Doubling | Any first two cards; double after split (DAS) allowed | 1,283 doubled rounds, 165 of them after a split (Step 12) |
| Splitting | Matching value (any two ten-value cards); one card on split aces; no re-split observed | 401 splits including 134 mixed ten-value pairs and 35 ace splits; no round exceeded 2 hands; the RTP is solved for a two-hand limit (Step 27) |
| Surrender | None in 6,000 hands | The action vocabulary is hit, stand, double, split plus the capture’s insurance request — no surrender action appears in 6,000 hands (Step 14). The RTP is solved without surrender. |
| Insurance | Offered on a dealer Ace upcard; pays 2:1 | 446 insurance bets, all on Ace upcards; the 16 Ace-up rounds without an offer are player naturals (Steps 10, 28) |
| Stakes Captured | $0.10 (5,800 hands) and $10.00 (200 hands), USDC | The stake is not an input to the shoe derivation (Step 22) |
Seed Formats
Three inputs determine every shoe:
| Input | Format | Who Controls It |
|---|---|---|
| Server seed | 32-character hex string (16 bytes), revealed after epoch rotation | Casino — committed via SHA-256 before any bet |
| Client seed | Free-text string, changeable between hands via the fairness panel | Player |
| Nonce | Integer counter, starts at 0 and increments by 1 per bet within the epoch | Protocol — sequential, 0–49 in every captured epoch |
SHA-256(serverSeed) computed over the seed’s UTF-8 text (the 32 hex characters as a string), and publishes the next epoch’s hash while the current pair is active — consistent across all 120 epochs and 119 rotations.Shoe Map & Payout Math
Every card in a round comes from one shoe, in order. The first four positions are the deal; everything after is consumed one card at a time by hits, doubles, split hands, and the dealer:
| Shoe Position | Card | Notes |
|---|---|---|
shoe[0] | Player card 1 | Also feeds Perfect Pairs and 21+3 |
shoe[1] | Dealer upcard | Third card of the 21+3 poker hand |
shoe[2] | Player card 2 | Completes both side-bet hands — side bets are fixed here, before any action |
shoe[3] | Dealer hole card | Face-down; checked at the peek on an Ace or ten upcard, revealed at settlement |
shoe[4], shoe[5] | Split second cards (split rounds only) | Main hand's second card first, then the split hand's |
shoe[4+] (no split) | All further draws | Your hits and double card in order, then the dealer’s draws |
shoe[6+] (split) | Draws after the two split cards | Split hand plays out first, then the main hand, then the dealer — the pinned order verified card-for-card on all 401 splits |
(sum of hand wins) × m + sum of side-bet wins, where m = 1 when no hand doubled and m = 2 when every hand doubled — reconciled exactly across all 6,000 captured rounds. The round total is the capture’s own sum of LIQD’s credit records; the identity checks the 1,283 doubled rounds, and each hand’s credit is rebuilt from the cards (Step 8).Why Provably Fair Matters▶
Traditional online casinos require players to trust that games are fair. Provably fair systems eliminate this trust requirement by allowing players to mathematically verify that outcomes were not manipulated. In a Provably Fair system:
- The casino commits to the inputs that determine a result before the player bets
- A source of randomness exists that the casino cannot predict or control
- Anyone can verify the outcome after the fact
High-Level Overview8 sections▶
How LIQD Blackjack's provably fair system works, end to end — from the commitment you can check before betting to the full-shoe recomputation you can run afterwards.
Checklist Reference
Commit-Reveal
| Check | What it proves |
|---|---|
| Server seed committed before play | The seed behind every hand is the one whose hash was shown before play, as our capture records show |
| SHA-256 of revealed seed matches committed hash | Revealed seed is the one that was actually committed |
| Hash identical across all bets in an epoch | Each epoch’s records carry one commitment; a seed swapped mid-epoch would fail recomputation (Steps 3, 6) |
Seed System
| Check | What it proves |
|---|---|
| Nonces are sequential within an epoch | No nonce skipping or reuse |
| Single client seed per epoch | Each epoch’s records carry one client seed, and every hand recomputes under it (Steps 3, 6) |
| Next-seed pre-committed before rotation | Every next-seed hash matches the next epoch’s commitment (119 / 119), and our capture records show each in advance |
RNG & Parity
| Check | What it proves |
|---|---|
| Every visible card recomputes from seeds + nonce via the shoe position it was dealt from | Card stream is deterministic and verifiable |
| Independent recomputation matches the live capture for every dealt card | The three disclosed inputs reproduce every card — no hidden input is needed |
| Card sequence invariant across player actions | Cards fixed at deal time, not rewritten per action |
Payout & RTP
| Check | What it proves |
|---|---|
| Main payouts match rule set: win 2×, BJ 2.5×, doubled-win 4×, push 1× | Payout arithmetic matches published rules |
| Side-bet payouts match published multiplier table | Perfect Pairs + 21+3 returns are exact to API precision |
| Proven RTP 99.512325% from the exact 8-deck solve; 30M simulation gated at 3σ | The RTP is computed from the rules, not estimated from play |
| RTP claim rule | LIQD’s published 99.52% holds — the exact RTP sits 0.007675 pp below it, inside the ±0.1 pp band |
Integrity
| Check | What it proves |
|---|---|
| Wrong client seed produces different cards | Client seed genuinely influences outcome |
| Bet size is not a shoe input | Phase E $10.00 rounds reproduce with the same stake-free reference function as the $0.10 phases |
| Cherry-pick screen (Pass 2) | No sign of seed selection on the money rule (pooled z = −0.746); a 50-candidate seed grind is caught at z = −23.56 |
High-Level Flow
From commitment to payout, one deterministic pipeline:
1. Casino commits — publishes SHA-256(serverSeed) before any bet, and pre-commits the next epoch's hash at every rotation
2. You bet — your client seed and the per-bet nonce join the committed server seed
3. Shoe shuffles — HMAC-SHA256 drives 415 Fisher-Yates swaps, fixing all 416 card positions
4. Cards deal — PDPD from shoe[0..3]; every hit, split card, and dealer draw takes the next position in order
5. Seed reveals — after rotation, recompute the whole shoe and confirm every card you were dealt
Provably Fair Model
A provably fair game publishes enough cryptographic material that any outcome can be recomputed by a third party. LIQD Blackjack does this with a commit-reveal server seed, a player-controlled client seed, a per-bet nonce, and a deterministic HMAC-SHA256 Fisher-Yates shuffle — the same construction this audit re-implements from scratch and runs against all 6,000 captured hands.
Commit-Reveal Model
The casino locks its randomness before you act, and reveals it afterwards so anyone can check it. While a pair is active the platform also shows the hash of the NEXT server seed — the baseline requirement of a commit-reveal scheme, not an extra — so each epoch’s seed is committed an epoch ahead. The chain is consistent across all 119 rotations in the capture, and our capture records show each hash in advance.
Commit
Publish SHA-256(serverSeed) while the seed itself stays secret. All 120 epoch commitments match their revealed seeds.
Pre-commit
While an epoch is active, also publish the hash of the next epoch’s seed. Each pre-committed hash matched the next epoch’s commitment — and so its revealed seed — at all 119 transitions.
Reveal
After the epoch ends, publish the plaintext server seed so every hand in the epoch can be recomputed.
Player-Controlled Client Seed
The client seed is yours. LIQD’s fairness panel lets you set or rotate it between hands. The server commits to its seed before the pair becomes active, so a seed you set after seeing the commitment lands on randomness the casino has already locked.
- Set or change it between hands via the fairness panel — rotation returns the outgoing server seed
- Phase D used 10 auditor-chosen seeds: 490 / 500 hands changed their first card versus a baseline seed (the ≈98% you’d expect — 1-in-52 matches are chance, not control)
- Set a fresh client seed after the commitment is published: that is what stops a casino from pre-selecting a server seed against you
Nonce Lifecycle
The nonce is a per-bet counter inside an epoch. It starts at 0 when a seed pair activates and increments by 1 per bet, giving every hand a unique input tuple without any new randomness.
- Starts at 0 on epoch activation; increments by exactly 1 per bet
- All 120 captured epochs run nonce 0–49 — 6,000 hands, zero gaps, zero duplicates
- Changing the nonce reshuffles the entire shoe — consecutive bets share no HMAC message
clientSeed = audita6d2fe11bb1f serverSeed = 6dca5e36… (revealed) nonce 0: HMAC(seed, 'audita6d2fe11bb1f:0:415') = d1b4e0fa… → j = 90nonce 1: HMAC(seed, 'audita6d2fe11bb1f:1:415') = 79f68b14… → j = 180nonce 2: HMAC(seed, 'audita6d2fe11bb1f:2:415') = 06787fba… → j = 122 # One integer changes → all 416 card positions reshuffle.Determinism Guarantee
Determinism is what makes verification possible: the same three inputs always produce the same shoe, so anyone can recompute a past hand from the revealed seed. All 6,000 captured hands — 34,734 dealt cards — reproduce exactly.
shoe = fisherYates416(serverSeed, clientSeed, nonce) player = [shoe[0], shoe[2]] dealer = [shoe[1], shoe[3]]draws = shoe[4:] # hits, splits, dealer — in order # Same inputs → same shoe → same cards. Every time.Phase Map
The 6,000-hand capture was structured into six behavioural phases so that the main mechanics — splits, doubles, side bets, custom client seeds and a second stake — appear in the evidence base. Re-split, surrender, LIQD’s own insurance decline, Face-Down Double and the profit cap were not exercised:
| Phase | Hands | Behaviour |
|---|---|---|
| A | 3,300 | Basic strategy at $0.10 — the statistical baseline |
| B | 1,000 | Aggressive play — hits to 17 and never doubles (pairs are still split) |
| C | 500 | Split-force play — splits, double after split and split aces; re-split never attempted |
| D | 500 | Auditor-chosen client seeds — 10 custom pfaudit- seeds |
| E | 200 | Fixed $10.00 stake, no side bets — the bet-size invariance bracket |
| F | 500 | Second split-forcing block (one split per round) — more split and double coverage |
Technical Glossary2 categories▶
Terms and definitions used throughout this audit report, grouped into blackjack terms and provably-fair / cryptographic terms.
Blackjack terms
| Term | Definition |
|---|---|
| Natural / Blackjack | a two-card hand totalling 21 (an Ace plus a ten-value card) on the initial deal. Pays 3:2 unless the dealer also has a natural, in which case the hand pushes. |
| Soft total | a hand total that counts an Ace as 11 without busting (e.g. Ace + 6 = "soft 17"). The Ace can later be counted as 1 ("hard") if a further card would otherwise bust the hand. |
| Hard total | a hand total with no Ace, or with every Ace counted as 1 (e.g. 10 + 7 = "hard 17"). A hard hand cannot absorb a large card without risk of busting. |
| S17 (stand on soft 17) | the dealer rule under which the dealer stands on a soft 17 rather than drawing. The more player-favourable of the two dealer conventions. LIQD Blackjack is S17. |
| H17 (hit soft 17) | the alternative dealer rule under which the dealer draws on a soft 17. Not used by LIQD Blackjack. |
| Double / Double down | doubling the stake in exchange for exactly one more card. LIQD allows doubling on any first two cards. |
| DAS (double after split) | permission to double a hand that was created by splitting. LIQD allows DAS (165 split rounds carried a double in the capture). |
| Split | playing a pair as two separate hands, each with its own stake. LIQD splits on matching value, not only matching rank: any two ten-valued cards (10/J/Q/K) may be split — 134 mixed ten-value splits occur in the captured data. |
| Split aces one card | the common restriction that each hand from a pair of split aces receives exactly one additional card and is then stood automatically. Confirmed at LIQD across 35 split-ace rounds. |
| Re-split | splitting a hand that is itself the product of a split. No re-split was observed at LIQD (no round exceeded 2 hands across 401 splits); the RTP is solved without it. |
| Insurance | a side wager, offered when the dealer’s upcard is an Ace and the player holds no natural, that the dealer holds a natural. Pays 2:1 and wins if and only if the dealer has a two-card 21. A −EV bet under basic strategy; the headline RTP assumes it is declined. |
| Peek / Hole-card / OBO (Original Bets Only) | the dealer deals a face-down hole card and checks ("peeks") for blackjack on an Ace or ten-value upcard before the player commits additional money, so the player never loses a double or split wager to a dealer natural. LIQD uses US peek / OBO. |
| Surrender | an option (no surrender action was observed in this capture) to forfeit half the stake and end the hand before drawing. |
| Push | a tie between player and dealer; the stake is returned (1× return, no win or loss). |
| Upcard | the dealer's face-up card, dealt at shoe[1] in LIQD's PDPD order. |
| Shoe | the pool of cards dealt from. LIQD uses an 8-deck (416-card) shoe, reshuffled every hand. |
| PDPD | the initial deal order Player-Dealer-Player-Dealer, reading shoe[0] (player), shoe[1] (dealer up), shoe[2] (player), shoe[3] (dealer hole). |
| Basic strategy | the fixed total-dependent table of hit / stand / double / split decisions this audit solves exactly and simulates (src/strategy.ts) — “basic” as opposed to composition-dependent play |
| Perfect Pairs | an optional side bet on the player's first two cards forming a pair (perfect / coloured / mixed). LIQD's paytable is 25 / 13 / 6, verified across 5,800 live bets. |
| 21+3 | an optional side bet evaluating the player's two cards plus the dealer upcard as a three-card poker hand (suited trips / straight flush / trips / straight / flush). LIQD's paytable is 100 / 40 / 30 / 10 / 5. |
Provably-fair and cryptographic terms
| Term | Definition |
|---|---|
| RTP (return to player) | the long-run fraction of wagered money returned to players under a stated strategy; 1 − house edge. LIQD Blackjack’s proven RTP is 99.512325% per initial bet (99.567565% per total amount wagered) under basic strategy with insurance declined — an exact 8-deck solve, consistent with 30M simulated rounds. The observed return of the 6,000 captured hands (98.0209%) is a reconciliation, not an RTP figure. |
| House edge | the operator's long-run advantage; 1 − RTP, stated in a declared convention. The per-initial-bet edge (standard "house edge") and the per-total-wagered edge ("element of risk") differ by the average wager per round. LIQD Blackjack base game 0.487675% per initial bet (0.432435% per total amount wagered), exact. |
| Provably fair | a design in which outcomes are cryptographically committed before play and independently verifiable afterward, so the operator cannot alter results and the player need not trust the operator's word. |
| Commit-reveal | the scheme by which the server publishes a hash (commitment) of its secret seed before play, then reveals the seed afterward; anyone can check the revealed seed hashes to the commitment. |
| Commitment | the published hash binding the operator to a server seed. LIQD uses SHA-256(utf8(serverSeed hex string)) (the seed text is hashed, not its decoded bytes). |
| Server seed | the operator-generated secret that keys the shuffle. Committed (hashed) before play, revealed on seed rotation. |
| Client seed | the player-controlled input to every shuffle, part of the HMAC message. Set after the commitment is published, it stops the operator from choosing a server seed against a known client seed. |
| Nonce | a per-hand counter (0,1,2,… within an epoch) that makes each hand a distinct, independent shuffle under the same seed pair. |
| Cursor | the per-swap counter inside the Fisher-Yates shuffle; part of the HMAC message clientSeed:nonce:cursor, running from 415 down to 1. |
| Epoch | a run of hands under one (serverSeed, clientSeed) pair. Its length is the player’s choice (rotate between hands); this capture rotated every 50 hands, giving 120 epochs. |
| Seed rotation | replacing the active seed pair, at which point the previous server seed is revealed so its hands become verifiable. |
| Pre-commitment (next-seed hash) | a hash of the next epoch's server seed, published in advance to chain epochs and prevent mid-stream seed substitution. |
| HMAC (HMAC-SHA256) | a keyed hash function; here keyed on the hex-decoded server seed with message clientSeed:nonce:cursor, producing the uniform draws that shuffle the shoe. |
| SHA-256 | the hash function used for the seed commitment and the dataset integrity guard. |
| Fisher-Yates shuffle | the algorithm that permutes the shoe by swapping each position with a uniformly chosen position at or below it. LIQD uses a backward in-place variant over the 416-card shoe. |
| Modulo-bias rejection | discarding HMAC output values at or above floor(2^32 / range) × range before reducing mod range, so the resulting integer is exactly uniform over [0, range). Our implementation does this on every draw; no captured draw needed it, as expected. |
| Cherry-picking (seed selection) | an operator pre-generating server seeds and committing to one that is unfavourable to a known client seed. A player who sets a fresh client seed after the commitment is protected; Step 19 screens all 120 revealed seeds for it. |
| Standard error (SE) | the sampling uncertainty of an estimate, σ/√n; used to bound the Monte-Carlo RTP and to show why the live money-path reconciliation is not an RTP measurement. |
| Capture flag | a flag raised when a gap in the record comes from our capture script — for example, a retried request that skipped a nonce — rather than from the casino. It is disclosed with a limitation and does not change the verdict; an operator-side gap is a hard fail. This audit has 0 capture flags: every epoch’s nonces run 0–49 with no gaps (Step 4). |
| 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 setting above the figure favours the player and never fails. A house-side miss ends the verdict as RTP FIGURE OVERSTATED. LIQD’s published 99.52% holds for Blackjack (Step 20). |
Seed, Nonce & Determinism
Can the casino change your outcome after you bet?
Every Blackjack hand 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 seed’s hash at every rotation. After you rotate, the retiring seed is revealed in plaintext, and anyone can check it against both commitments. A matching hash shows which seed goes with which hands and in what order, and our capture records show each hash was published before its hands — so the casino cannot change your cards after you bet.
What we verified
- Every revealed server seed hashes to the commitment published for its epoch — 120 / 120
- The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 119 / 119 links, plus the pre-capture link into epoch 0
- Client seed is player-controlled — set after the commitment in 110 of the 120 capture epochs
- Players can set or change their client seed between hands via the fairness panel
- Nonce increments by 1 per hand with no gaps or duplicates across 120 seed epochs — 0 capture flags
- The entire 416-card shoe is fully determined by the seed inputs before the first card is dealt
- Identical inputs always produce the same shoe — confirmed across all 6,000 hands and 34,734 dealt cards
- Your client seed is a genuine input — changing it reshuffles the shoe
What this means for you
- With the commitment shown first, the casino cannot change which cards you’re dealt after you bet
- Set your own unpredictable client seed after the commitment is shown, and the server cannot pre-select against it
- Every hand has its own shoe — the nonce guarantees no two hands share the same inputs
- Any result can be independently verified using the public tools and repo
- Outcomes stay verifiable long after the round — the seeds and the code are all you need
- Cherry-picking risk is low when you set an unpredictable client seed after the commitment is published — the operator cannot pre-evaluate server seeds against a seed it cannot predict
- Every revealed server seed hashes to the commitment published for its epoch — 120 / 120
- The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 119 / 119 links, plus the pre-capture link into epoch 0
- Client seed is player-controlled — set after the commitment in 110 of the 120 capture epochs
- Players can set or change their client seed between hands via the fairness panel
- Nonce increments by 1 per hand with no gaps or duplicates across 120 seed epochs — 0 capture flags
- The entire 416-card shoe is fully determined by the seed inputs before the first card is dealt
- Identical inputs always produce the same shoe — confirmed across all 6,000 hands and 34,734 dealt cards
- Your client seed is a genuine input — changing it reshuffles the shoe
- With the commitment shown first, the casino cannot change which cards you’re dealt after you bet
- Set your own unpredictable client seed after the commitment is shown, and the server cannot pre-select against it
- Every hand has its own shoe — the nonce guarantees no two hands share the same inputs
- Any result can be independently verified using the public tools and repo
- Outcomes stay verifiable long after the round — the seeds and the code are all you need
- Cherry-picking risk is low when you set an unpredictable client seed after the commitment is published — the operator cannot pre-evaluate server seeds against a seed it cannot predict
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
audit… seeds and 10 custom pfaudit- seeds in Phase DClient seed controlPass
Client seed influencePass
Deterministic outputPass
All 120 revealed seeds match their commitments, and the next-seed chain is consistent across 119 rotations plus the pre-capture link into epoch 0. Nonces run 0–49 in every epoch with no gaps. Outcomes are fully deterministic — the same server seed, client seed and nonce always produce the same shoe, card for card. With each commitment shown before its hands, as our capture records show, the casino cannot change your result after you bet.
| Check | Result | Evidence |
|---|---|---|
| Seed hash integrity | Pass | 120 / 120 revealed seeds hash to their commitments — SHA-256 of the seed text, 0 mismatches (Step 1) |
| Next-seed chain | Pass | Consistent on 119 / 119 rotations, plus the pre-capture commitment that links into epoch 0 (Step 2) |
| One commitment per epoch | Pass | All 50 rounds in each of 120 epochs carry the same commitment and client seed (Step 3), and every hand recomputes under its epoch’s seeds — no mid-epoch seed change (Step 6) |
| Nonce sequencing | Pass | Nonces 0–49 in every epoch: 0 gaps, 0 duplicates, 0 capture flags, every window closed at nonce 49 (Step 4) |
| Client seed origin | Pass | Set by the capture at every rotation — 110 auditor-generated audit… seeds and 10 custom pfaudit- seeds in Phase D |
| Client seed control | Pass | Player can set or change the client seed between hands — 500 / 500 Phase D hands recompute under their custom seeds (Step 26) |
| Client seed influence | Pass | A wrong or an empty client seed deals a different first card on 974 and 982 of 1,000 sampled hands — the rate a 1-in-52 chance match predicts (Step 7) |
| Deterministic output | Pass | Same (serverSeed, clientSeed, nonce) always produces the same shoe — 6,000 / 6,000 hands, 34,734 dealt cards (Step 6) |
Seed hash integrityPass
Next-seed chainPass
One commitment per epochPass
Nonce sequencingPass
Client seed originPass
audit… seeds and 10 custom pfaudit- seeds in Phase DClient seed controlPass
Client seed influencePass
Deterministic outputPass
All 120 revealed seeds match their commitments, and the next-seed chain is consistent across 119 rotations plus the pre-capture link into epoch 0. Nonces run 0–49 in every epoch with no gaps. Outcomes are fully deterministic — the same server seed, client seed and nonce always produce the same shoe, card for card. With each commitment shown before its hands, as our capture records show, the casino cannot change your result after you bet.
How It Works — Seed, Nonce & Determinism8 sections
1.1Server Seed Commitment
Before any hand is dealt under a seed pair, LIQD shows the SHA-256 hash of a secret 32-character hex server seed as hashedServerSeed. The commitment hashes the seed text as a UTF-8 string, not its 16 decoded bytes: SHA-256(utf8(serverSeed)). When the pair rotates, the server seed is revealed and anyone can recompute the hash. A match shows the revealed seed is the committed one: a substitute would need a second preimage of SHA-256, and would fail every recomputation that follows.
// §1.2: commitment = lowercase hex SHA-256 of the seed's characters as UTF-8 text// (the 32 ASCII characters, NOT the 16 bytes they encode; the key form differs on purpose).export function commitHash(serverSeedHexString: string): string { return createHash('sha256').update(Buffer.from(serverSeedHexString, 'utf8')).digest('hex');}Real epoch verified:
// Source: data/blackjack-6000hands.json (epoch 0, Phase A) — capture-side fields omitted// SHA-256(utf8(serverSeed)) matches hashedServerSeed { "epoch": 0, "phase": "A", "clientSeed": "audita6d2fe11bb1f", "hashedServerSeed": "948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42", "nextHashedServerSeed": "5112cebbec8745b056100e4cb9234a70fab55dc54c3b3b6a0573a74c071e1d43", "serverSeed": "6dca5e36b6b9dc56730e282ab3c2bae6", "nonceStart": 0, "nonceEnd": 49}1.2Next-Seed Pre-Commitment Chain
At every seed rotation, the nextHashedServerSeed LIQD pre-commits during an epoch must equal the hashedServerSeed that becomes active after the rotation, so the successor seed is fixed before the previous epoch ends. Step 2 walks all 119 epoch transitions and checks each link, and also checks the pre-capture record: the seed active when the capture connected (7fcb29b3…) hashes to its own commitment (7cd82eb9…), and its pre-committed next hash is epoch 0’s hashedServerSeed (948eff89…). 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 hands it covers.
// ── Step 2: Pre-commitment chain — each epoch's nextHashedServerSeed == next epoch's hash ──const ordered = [...seeds].sort((a, b) => a.epoch - b.epoch);let links = 0;let linkBad = 0;for (let i = 0; i + 1 < ordered.length; i++) { if (ordered[i].nextHashedServerSeed == null) continue; links++; if (ordered[i].nextHashedServerSeed !== ordered[i + 1].hashedServerSeed) linkBad++;}// Expected link count is EXPECTED_SEEDS − 1 from src/config.ts, not `seeds.length - 1`:// a chain of 119 epochs is a complete chain over the wrong population.const expectedLinks = EXPECTED_SEEDS - 1; // every epoch but the last pre-commits the next// ── Step 2, pre-capture anchor ─────────────────────────────────────────────────// The chain above starts at epoch 0, so nothing in it says that epoch 0's commitment was// published BEFORE the capture began — a chain can be complete and still start wherever the// file starts. The capture rig recorded the seed pair that was active when it connected and// rotated it before the first captured round (`meta.preCapture`): its revealed seed must hash to// its own commitment, and the next-seed hash it published must be epoch 0's commitment. Both// are recomputed here from the raw fields; the record's own `commitVerified` boolean is not// read. An ABSENT anchor is a FAIL, not a skip.const pre = (ctx.meta.preCapture ?? null) as { revealedServerSeed?: unknown; hashedServerSeed?: unknown; nextHashedServerSeed?: unknown } | null;const preRevealOk = !!pre && typeof pre.revealedServerSeed === 'string' && commitHash(pre.revealedServerSeed) === pre.hashedServerSeed;const preLinkOk = !!pre && ordered.length > 0 && pre.nextHashedServerSeed === ordered[0].hashedServerSeed;const anchorOk = preRevealOk && preLinkOk;1.3Hash Consistency Within an Epoch
Every round record carries the hashedServerSeed it was played under and its client seed. The audit grouped all 6,000 rounds by their epoch label and confirmed every round in an epoch carries the one commitment and the one client seed recorded for that epoch — relabelling a round into another epoch, or swapping two rounds’ labels, fails the step. The per-round fields are stamps the capture script wrote, so this checks the bookkeeping against the seed records; what binds each hand to its seed is the recomputation (Steps 1 and 6), where a seed swapped mid-epoch would fail.
const seedByEpoch = new Map(seeds.map((s) => [s.epoch, s]));const byEpoch = new Map<number, typeof bets>();for (const b of bets) { const arr = byEpoch.get(b.epoch) ?? []; arr.push(b); byEpoch.set(b.epoch, arr);}let epochViolations = 0;let clientSeedViolations = 0;const clientFails: string[] = [];for (const [epoch, epochBets] of byEpoch) { const sd = seedByEpoch.get(epoch); if (!sd || epochBets.some((b) => b.hashedServerSeed !== sd.hashedServerSeed)) epochViolations++; // The same-client-seed half: every round of an epoch was dealt under ONE client seed, and it // is the client seed the epoch's seed record carries. const clients = new Set(epochBets.map((b) => b.clientSeed)); if (clients.size !== 1 || !sd || !clients.has(sd.clientSeed)) { clientSeedViolations++; if (clientFails.length < 3) clientFails.push(`epoch ${epoch}: ${clients.size} client seed(s) ${JSON.stringify([...clients].slice(0, 3))} vs seed record ${JSON.stringify(sd?.clientSeed)}`); }}1.4Client Seed Origin & Control
Players set their client seed in LIQD’s fairness panel, and rotating the pair reveals the outgoing server seed. The client seed is the first field of every HMAC message, so changing it reshuffles the whole shoe. The capture set a new client seed at every rotation — 120 distinct seeds. 110 epochs used audit followed by 12 hex characters, drawn with crypto.getRandomValues by the capture script, which ships byte for byte with the package, where Step 7 binds it to the data. Phase D played 500 hands under 10 auditor-chosen pfaudit- seeds to confirm each is genuinely consumed; those follow a predictable pfaudit-<stamp>-<epoch> pattern, so epochs 96–105 are verified by recomputation and screened for seed selection in S4.11.
audita6d2fe11bb1faudita98dd4cd1e6bpfaudit-1785954682036-96pfaudit-1785954682036-105
audit… seeds and 10 predictable pfaudit- seeds (Phase D, epochs 96–105). Evidence: blackjack-6000hands.json1.5Nonce Incrementation
The nonce begins at 0 and increments by 1 for each hand under the active seed pair, and resets to 0 when the pair rotates. Every epoch in this capture is exactly 50 hands (nonces 0–49), with no gaps, no duplicates, and the recorded window closed at nonce 49 with a round present there. The window is declared in code (NONCE_BASE 0, epoch size 50), not read from the dataset: an operator-side gap would be a hard fail, and a declared retry of our capture script a capture flag — this capture has neither. One scope note: 145 rounds were recorded from the partial insurance response, which omits the nonce, so their nonces are the capture’s own count; each still reproduces its cards (Step 6).
const declaredEnd = NONCE_BASE + EXPECTED_EPOCH_SIZE - 1;for (const s of seeds) { const epochBets = byHash.get(s.hashedServerSeed) || []; if (epochBets.length === 0) continue; const nonces = epochBets.map((b) => b.nonce); // Reorder: the capture sequence (dataset order) must be strictly increasing within an epoch. for (let i = 1; i < nonces.length; i++) if (!(nonces[i] > nonces[i - 1])) { reorders++; break; } const present = new Set(nonces); repeats += nonces.length - present.size; for (const n of present) if (n < NONCE_BASE || n > declaredEnd) outOfWindow++; for (let n = NONCE_BASE; n <= declaredEnd; n++) { if (!present.has(n)) { missingNonces++; if (gapDetail.length < 3) gapDetail.push(`epoch ${s.epoch} nonce ${n}`); } } // The record's window must be the declared window, and the round AT nonceEnd must be present. if (s.nonceStart !== NONCE_BASE || s.nonceEnd !== declaredEnd) { windowBad++; if (winDetail.length < 3) winDetail.push(`epoch ${s.epoch}: record window ${String(s.nonceStart)}…${String(s.nonceEnd)} != declared ${NONCE_BASE}…${declaredEnd}`); } else if (!present.has(declaredEnd)) { tailBad++; if (winDetail.length < 3) winDetail.push(`epoch ${s.epoch}: no round at the declared nonceEnd ${declaredEnd}`); }}const hardBad = repeats > 0 || outOfWindow > 0 || reorders > 0 || windowBad > 0 || tailBad > 0;const captureCountNonces = bets.filter((b) => b.serverSeedId === undefined).length;const undeclaredGaps = missingNonces > EXPECTED_CAPTURE_RETRY_GAPS;const declaredCaptureGaps = missingNonces > 0 && missingNonces === EXPECTED_CAPTURE_RETRY_GAPS;const s4Status = hardBad || undeclaredGaps || missingNonces !== EXPECTED_CAPTURE_RETRY_GAPS ? 'FAIL' : declaredCaptureGaps ? 'FLAG' : 'PASS';1.6Deterministic Mapping
Determinism is the property that makes verification possible: (serverSeed, clientSeed, nonce) fully determines the 416-card shoe, so the same inputs always produce the same cards. There is no hidden state — no time-of-day, no bet size, no account identity in the derivation. Our implementation, written from the specification (docs/ALGORITHM.md) without LIQD’s code, recomputed every hand from these three inputs alone.
// §2 Step 3: backward Fisher-Yates over positions a = [0..415]. For i = 415 down to 1 the// cursor is i itself (§2 Step 1) and j = draw over i + 1 outcomes; swap a[i], a[j].// Card k of the round is BASE[a[k]], dealt from the front of the array.export function blackjackShoe(serverSeed: string, clientSeed: string, nonce: number, census?: DrawCensus): string[] { const key = hmacKey(serverSeed); const base = buildShoeTemplate(); const a: number[] = Array.from({ length: SHOE_SIZE }, (_, x) => x); for (let i = SHOE_SIZE - 1; i >= 1; i--) { const j = drawFromDigest(digestFor(key, clientSeed, nonce, i), i + 1, census); const t = a[i] as number; a[i] = a[j] as number; a[j] = t; } return a.map((x) => base[x] as string);} // §2 Step 4: k = 0 player card 1, k = 1 dealer up card, k = 2 player card 2,// k = 3 dealer hole card; everything from k = 4 on is what later play draws from.export function dealtHands(shoe: string[]): { player: string[]; dealer: string[]; rest: string[] } { if (shoe.length < 4) throw new Error('a deal needs at least 4 cards'); return { player: [shoe[0] as string, shoe[2] as string], dealer: [shoe[1] as string, shoe[3] as string], rest: shoe.slice(4), };}Real bet record (the hand S2.8 recomputes) — trimmed:
{ "id": "6hBu_VAq-lMp3UKjYMS1a", "epoch": 0, "nonce": 0, "phase": "A", "clientSeed": "audita6d2fe11bb1f", "hashedServerSeed": "948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42", "actions": ["hit"], "playerHands": [{ "cards": ["HEART:7", "HEART:9", "HEART:5"], "points": 21, "result": "won", "winningAmount": 0.2, "betAmount": "0.10000000" }], "dealerHand": ["CLUB:10", "HEART:11"], "winningAmount": 0.2, // round credit …}Recompute it yourself:
// inside the repo, after npm ci: node -r ts-node/register verify-ep0n0.js (src/rng.ts is TypeScript)const { blackjackShoe } = require('./src/rng');const shoe = blackjackShoe( '6dca5e36b6b9dc56730e282ab3c2bae6', // revealed server seed, epoch 0 'audita6d2fe11bb1f', 0); // client seed, nonceconsole.log(shoe.slice(0, 5).join(' '));// → HEART:7 CLUB:10 HEART:9 HEART:11 HEART:5 (7♥ 10♣ 9♥ J♥ 5♥ — the deal, then the player's hit)1.7Shoe Architecture
LIQD shuffles a real 8-deck shoe rather than drawing each card independently from an idealised infinite deck. The seed inputs drive a backward Fisher-Yates permutation of all 416 cards (S2 covers the mechanism), and the deal reads from the top: shoe[0] player, shoe[1] dealer up, shoe[2] player, shoe[3] dealer hole — PDPD — with every subsequent draw taking shoe[4], shoe[5], … in order. One shoe per hand, fixed by the seeds and the nonce before the first card is dealt.
function dealtSequence(playerHand: string[], dealerHand: string[]): string[] { const seq: string[] = []; if (playerHand?.[0]) seq.push(playerHand[0]); if (dealerHand?.[0]) seq.push(dealerHand[0]); if (playerHand?.[1]) seq.push(playerHand[1]); if (dealerHand?.[1]) seq.push(dealerHand[1]); for (let i = 2; i < (playerHand?.length || 0); i++) seq.push(playerHand[i]); for (let i = 2; i < (dealerHand?.length || 0); i++) seq.push(dealerHand[i]); return seq;}And the split-round pin (verified on all 401 splits):
/** * Exact draw order for a split round. Derived empirically from the capture and * confirmed against every split hand in the dataset: * * shoe[0..3] main[0], dealer[0], split[0], dealer[1] (the pinned initial deal) * shoe[4..5] main[1], split[1] (one auto card to each hand) * then the SPLIT hand plays out fully (split[2..]), * then the MAIN hand plays out fully (main[2..]), * then the dealer draws (dealer[2..]). * * … */function splitDealtSequence(main: string[], split: string[], dealer: string[]): string[] { return [main[0], dealer[0], split[0], dealer[1], main[1], split[1], ...split.slice(2), ...main.slice(2), ...dealer.slice(2)].filter((c) => c !== undefined);}1.8Client Seed Influence
To confirm the client seed is a genuine HMAC input, Step 7 re-derives 1,000 hands (every 6th round) under a deliberately wrong client seed and under an empty one. The first card survives in 26 and 18 of them — what independent shoes give by chance, since a wrong seed matches the recorded first card with probability 1/52 (19.2 expected; the step fails above 40.9). So the deal changes on 974 and 982 of 1,000. This is a property of our implementation; the comparison with the cards LIQD dealt is Step 6, where every one of the 6,000 recorded hands recomputes under its recorded client seed. Phase D then played 500 live hands under 10 auditor-chosen seeds: 490 / 500 deal a different first card than a baseline seed would (Step 26).
/** * Wrong-seed collision model for the first card. Under a client seed other than the one used, the * recomputed shoe is an independent uniform shuffle of the 416-card shoe, so its first card is * uniform over the 52 card identities (8 copies each) and matches the recorded first card with * probability exactly 1/52. Over n tested rounds the expected number of chance survivors is n/52 * with variance n·(1/52)(51/52). A survivor count above E + 5σ means the client seed is not doing * what the model says; 0 % change is the degenerate case of the same failure. */function collisionModel(n: number): { expected: number; sd: number; bound: number } { const p = 1 / 52; const expected = n * p; const sd = Math.sqrt(n * p * (1 - p)); return { expected, sd, bound: expected + 5 * sd };}// …let sampled = 0;let survivedWrong = 0;let survivedEmpty = 0;const bettable = bets.filter((b) => seedMap.get(b.hashedServerSeed) && b.clientSeed);const stride = Math.max(1, Math.floor(bettable.length / 1000));for (let i = 0; i < bettable.length; i += stride) { const b = bettable[i]; const serverSeed = seedMap.get(b.hashedServerSeed) as string; const realFirst = (b.playerHands[0]?.cards || [])[0]; sampled++; if (blackjackShoe(serverSeed, b.clientSeed + '_x', b.nonce)[0] === realFirst) survivedWrong++; if (blackjackShoe(serverSeed, '', b.nonce)[0] === realFirst) survivedEmpty++;}const model = collisionModel(sampled);// …const s7Ok = sampled >= 1000 && survivedWrong < sampled && survivedEmpty < sampled && survivedWrong <= model.bound && survivedEmpty <= model.bound && formOk && rig.ok;Technical Evidence — Seed, Nonce & Determinism5 sections
1.9Evidence Coverage Summary7 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Commitment reveal integrity (Step 1) | 120 / 120 revealed seeds | Pass |
| Pre-commitment chain (Step 2) | 120 / 120 links consistent (119 transitions + the pre-capture link) | Pass |
| Hash consistency within epoch (Step 3) | 120 / 120 epochs — one commitment and one client seed each | Pass |
| Nonce continuity (Step 4) | 120 epochs × nonces 0–49 — 0 gaps, 0 duplicates, 0 capture flags, 120 / 120 windows closed | Pass |
| Epoch size (Step 5) | 50 hands per epoch, all 120 epochs | Pass |
| Deterministic recomputation (Step 6) | 6,000 / 6,000 hands, 34,734 dealt cards | Pass |
| Client seed influence (Step 7) | 974 / 1,000 and 982 / 1,000 first-card changes under a wrong and an empty seed | Pass |
1.10Code References9 source files
| File | Purpose |
|---|---|
| tests/verify.ts | 31-step verification pipeline (Steps 1–7 and 26 cover S1) |
| tests/steps/commitment.ts | Steps 1–5: commitment reveal, pre-commitment chain, hash consistency, nonce continuity, epoch size |
| tests/steps/parity.ts | Steps 6–7: recomputation parity and client-seed dependence |
| tests/steps/context.ts | Shared verification context — seed/bet maps, phase slices, and the step reporter |
| src/rng.ts | commitHash · generateProvablyFairNumber · blackjackShoe · dealtHands — written from docs/ALGORITHM.md |
| src/config.ts | Population pins — EXPECTED_SEEDS = 120, EXPECTED_EPOCH_SIZE = 50, EXPECTED_HANDS = 6,000, NONCE_BASE = 0: every expected count in Steps 1–6 and 24 comes from here, never from the dataset’s own header (Step 5 scores the header against these pins) |
| src/loader.ts | Dataset loading, the pre-flight SHA-256 check against DATASET_SHA256, and removal of the capture’s own recomputation fields before any step runs |
| capture/capture-blackjack.reference.mjs | Sanitized capture record — phases, endpoints, rotation discipline (transport stubbed, no credentials) |
| capture/capture-liqd-blackjack.aug05-rig.mjs | The capture script that generated the client seeds, kept byte for byte — SHA-256 pinned and bound to the data by Step 7 |
1.11Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Source | Live capture from qa.liqd.com — 6,000 real rounds across 6 phases (A=3,300 · B=1,000 · C=500 · D=500 · E=200 · F=500), gameId fast-games-11, USDC |
| Capture environment | LIQD’s pre-production environment, qa.liqd.com · one authenticated session, one account |
| Total Records | 6,000 rounds · 6,401 player hands · 34,734 dealt cards · 120 revealed seed epochs of exactly 50 rounds |
| Captured | 5 August 2026 (18:31–20:18 UTC, single session) |
| SHA-256 | c2a28c5164a546105963ffb4ff8e7c422a68802df47afcdcfa0b4fdbad1fdbf7 |
| Pre-flight check | src/loader.ts aborts before any step runs if the file’s SHA-256 does not match DATASET_SHA256 in src/config.ts; scored Step 25 re-states the match alongside the other pinned artifacts |
Fields used in S1 verification: hashedServerSeed, serverSeed, nextHashedServerSeed, clientSeed, nonce, nonceStart, nonceEnd, epoch, phase, and the pre-capture record meta.preCapture.
1.12Verified Invariants10 invariants
| Invariant | Result |
|---|---|
| SHA-256(utf8(serverSeed)) = hashedServerSeed for all 120 revealed seeds | Pass |
| Next-seed chain consistent on all 119 transitions and the pre-capture link (120 / 120) | Pass |
| One hashedServerSeed and one client seed within each of the 120 epochs | Pass |
| Nonces run 0–49 with zero gaps and zero duplicates in every epoch | Pass |
| Every epoch contains exactly 50 rounds | Pass |
| blackjackShoe(serverSeed, clientSeed, nonce) reproduces the recorded cards for all 6,000 rounds | Pass |
| A wrong client seed changes the first card in 974 / 1,000 sampled hands, an empty one in 982 / 1,000 | Pass |
| Every round resolves to a revealed seed record — a round with no revealed seed fails Step 6 | Pass |
Expected counts come from src/config.ts, so a shortened dataset fails rather than passing on a smaller count | Pass |
| The epoch-0 commitment 948eff89…e12f42 is pinned as a unit test (tests/blackjack/rngTests.ts) | Pass |
1.13Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-blackjack.gitcd liqd-blackjack && git checkout v1.0.1npm cinpm run verify # Steps 1–7 and 26 cover S1Steps 1–7 and 26 cover the Seed, Nonce & Determinism checks. Expected output:
[PASS] Step 1 — Commitment Reveal
[PASS] Step 2 — Pre-Commitment Chain
[PASS] Step 3 — Within-Epoch Hash Consistency
[PASS] Step 4 — Nonce Continuity
[PASS] Step 5 — Epoch Size
[PASS] Step 6 — Recomputation Parity
[PASS] Step 7 — Client-Seed Dependence
[PASS] Step 26 — Phase D Client-Seed VariationRNG & Entropy Model
Is the randomness genuinely random, or could it be rigged?
This section verifies that LIQD’s Blackjack shuffle comes from the disclosed inputs alone, through a construction that is unbiased by design: HMAC-SHA256 drives a backward Fisher-Yates over the full 416-card, 8-deck shoe — for each swap the digest is read as eight big-endian uint32 chunks, and the first chunk below the rejection ceiling, taken modulo the swap range, picks the position. The deal then reads player, dealer, player, dealer from the top of the shoe. The implementation is our own, written from a specification without LIQD’s code; it reproduces all 6,000 live hands, and the ProvablyFair.org verifier serves as a second check. Every captured draw accepted its first chunk, as the 0.0569 expected rejections predict (S2.3).
What we verified
- Our implementation reproduces all 6,000 hands — 2,490,000 HMAC-SHA256 shuffle draws recomputed
- The shoe is computed from the disclosed inputs alone — the captured hands need no timestamp, server-side state or hidden entropy to reproduce
- Rejection sampling in the specification makes every draw exactly uniform; no captured draw needed it (0.0569 expected)
- First-card uniformity (p = 0.8752) and serial-independence screens find no sign of bias over 1,000,000 simulated real-shoe rounds
- The 8-deck shoe is confirmed from the data — duplicate cards within a round rule out a single deck, and every card recomputes from the 416-card shoe
- A changed client seed reshuffles the shoe — 490 / 500 Phase D hands changed their first card
What this means for you
- The shuffle as specified favours no card at any position — even skipping its rejection step would change a draw by at most 2.4 × 10⁻⁸ (S2.3)
- No hidden randomness or server-side input influences what you’re dealt
- Past hands don’t feed into future ones — each shoe is computed from the seeds and its own nonce
- The algorithm depends only on seeds you can verify
- Our implementation reproduces all 6,000 hands — 2,490,000 HMAC-SHA256 shuffle draws recomputed
- The shoe is computed from the disclosed inputs alone — the captured hands need no timestamp, server-side state or hidden entropy to reproduce
- Rejection sampling in the specification makes every draw exactly uniform; no captured draw needed it (0.0569 expected)
- First-card uniformity (p = 0.8752) and serial-independence screens find no sign of bias over 1,000,000 simulated real-shoe rounds
- The 8-deck shoe is confirmed from the data — duplicate cards within a round rule out a single deck, and every card recomputes from the 416-card shoe
- A changed client seed reshuffles the shoe — 490 / 500 Phase D hands changed their first card
- The shuffle as specified favours no card at any position — even skipping its rejection step would change a draw by at most 2.4 × 10⁻⁸ (S2.3)
- No hidden randomness or server-side input influences what you’re dealt
- Past hands don’t feed into future ones — each shoe is computed from the seeds and its own nonce
- The algorithm depends only on seeds you can verify
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
Key encoding verifiedPass
Statistical uniformityPass
Deck modelPass
Client seed influencePass
Every captured hand comes from the disclosed inputs alone, and our own implementation — written from the specification — reproduces all 6,000. Its draw is unbiased by construction, and the first-card and serial screens find no sign of bias over 1,000,000 simulated real-shoe rounds. The client seed is a genuine input — changing it reshuffles the shoe.
| Check | Result | Evidence |
|---|---|---|
| RNG derived only from disclosed inputs | Pass | HMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — no hidden entropy |
| Entropy purity | Pass | The specified derivation reads no timestamp, external API, Math.random or server-side state, and takes no bet size, account or clock input; the captured hands reproduce from the disclosed inputs alone (Step 6) |
| Algorithm independently implemented | Pass | Our implementation, written from the specification without LIQD’s code, reproduces every dealt card of all 6,000 hands — 34,734 cards (Step 6) |
| Modulo bias | Pass | Rejection sampling in our implementation — a chunk at or above floor(2³²/range)×range is discarded, so each draw is exactly uniform (bias 0 on all 415 ranges). All 2,490,000 captured draws accepted their first chunk (0.0569 rejections expected), as predicted — S2.3 |
| Key encoding verified | Pass | Server seed hex-decoded to bytes for the HMAC key, confirmed by the 6,000-hand recomputation (Step 6); the commitment hashes the seed text, confirmed by 120 / 120 hash matches (Step 1) |
| Statistical uniformity | Pass | χ² = 6.73 (df 12, p = 0.8752) on 1,000,000 real-shoe first cards; serial |r₁ z| = 0.57; runs p = 0.6387 (Step 18) |
| Deck model | Pass | 8-deck shoe confirmed — duplicate cards in 1,269 rounds rule out a single deck (Step 30), and every card recomputes from the 416-card shoe (Step 6) |
| Client seed influence | Pass | A changed seed reshuffles the shoe — 490 / 500 first-card changes across Phase D (Step 26) |
RNG derived only from disclosed inputsPass
Entropy purityPass
Algorithm independently implementedPass
Modulo biasPass
Key encoding verifiedPass
Statistical uniformityPass
Deck modelPass
Client seed influencePass
Every captured hand comes from the disclosed inputs alone, and our own implementation — written from the specification — reproduces all 6,000. Its draw is unbiased by construction, and the first-card and serial screens find no sign of bias over 1,000,000 simulated real-shoe rounds. The client seed is a genuine input — changing it reshuffles the shoe.
How It Works — RNG & Entropy Model8 sections
2.1RNG Function Implementation
One function turns the seed inputs into a uniform integer, and one loop turns those integers into a shuffled shoe. The key is the server seed’s decoded bytes; the message binds the client seed, the nonce and the Fisher-Yates index (the cursor). The 32-byte digest is read as eight big-endian uint32 chunks, and the first chunk under the rejection ceiling is reduced modulo the swap range. This is our implementation, built from the written specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code. As a further cross-check, the validation vectors of the RNG specification LIQD supplied to the auditor — its stated outputs for fixed inputs — are run against our implementation (tests/blackjack/rngTests.ts); LIQD’s code itself is not executed.
| Chunk | BE uint32 value | Outcome |
|---|---|---|
| 0 | 3,518,292,218 | < maxFair → accepted · j = 3,518,292,218 % 416 = 90 |
| 1 | 3,418,081,053 | unused — chunk 0 already accepted |
| 2 | 1,645,490,846 | unused |
| 3 | 2,690,787,354 | unused |
| 4 | 1,493,908,673 | unused |
| 5 | 1,244,025,712 | unused |
| 6 | 2,075,334,824 | unused |
| 7 | 1,580,231,983 | unused |
// §2 Step 2: LIMIT = floor(2^32 / r) * r. Chunk values below it are accepted; it is the// largest multiple of r that fits in 32 bits, so the accepted window maps evenly onto [0, r).export function maxFairFor(range: number): number { assertRange(range); return Math.floor(TWO_POW_32 / range) * range;}// …// §2 Step 2: walk the 8 big-endian u32 chunks of one digest, return the first accepted// one reduced mod range. If every chunk is rejected the spec gives no rule (§6 O1), so// this throws rather than invent one.function drawFromDigest(digest: Buffer, range: number, census?: DrawCensus): number { const limit = maxFairFor(range); if (census) census.draws += 1; for (let c = 0; c < CHUNKS_PER_DIGEST; c++) { const v = digest.readUInt32BE(c * 4); if (v < limit) return v % range; if (c === 0 && census) census.firstChunkRejected += 1; } throw new Error(`all ${CHUNKS_PER_DIGEST} chunks rejected for range ${range}; the spec defines no fallback (O1)`);} // One accepted draw in [0, range) for (serverSeed, clientSeed, nonce, cursor): §2 Steps 1-2.export function generateProvablyFairNumber( serverSeed: string, clientSeed: string, nonce: number, cursor: number, range: number,): number { assertRange(range); return drawFromDigest(digestFor(hmacKey(serverSeed), clientSeed, nonce, cursor), range);}The shoe template the shuffle starts from:
// §2 Step 3: BASE[x] for x = 0..415 is deck-major; inside a deck the suit is// SUITS[(x mod 52) div 13] and the rank is (x mod 13) + 1 (1 = Ace ... 13 = King).export function buildShoeTemplate(): string[] { const shoe: string[] = []; for (let x = 0; x < SHOE_SIZE; x++) { const suit = SUITS[Math.floor((x % 52) / 13)]; const rank = (x % 13) + 1; shoe.push(`${suit}:${rank}`); } return shoe;}The table above is the real first digest of the report’s worked example — HMAC-SHA256(6dca5e36…, 'audita6d2fe11bb1f:0:415') = d1b4e0fa… — read as eight big-endian uint32 chunks. The first chunk under the ceiling wins; here chunk 0 is accepted immediately, as it was in every one of the 2,490,000 captured draws.
clientSeed:nonce:i), giving eight acceptance chances. If all eight were rejected the written specification gives no rule: our implementation stops with an error, while LIQD’s code draws again at cursor + 1,000,000 — S2.3 shows how far out of reach that case is.2.2Entropy Sources
The system's unpredictability rests on two independent parties: the casino's secret 16-byte server seed (committed before play) and the player's free-choice client seed (which the casino cannot know at commitment time when the player sets it afterwards). The nonce adds no entropy — it's a public counter — but it guarantees every bet a unique HMAC message. There is no external randomness beacon in LIQD's design; the security model is the commit-reveal pairing plus the next-seed pre-commitment verified in S1.
| Input | Controlled by | Fixed when | Role in the HMAC |
|---|---|---|---|
serverSeed (16 bytes) | Casino | At commitment — before any bet | The key (hexDecoded bytes) |
clientSeed | Player | Changeable between hands via the fairness panel | Message component |
nonce | Protocol | Increments by 1 per bet | Message component — makes each bet's shoe unique |
swap index i | Algorithm | Walks 415 → 1 inside one shoe | Message component + the modulo range (i + 1) |
That is the complete entropy budget of the specified derivation: the casino’s secret server seed, the player’s chosen client seed and two public counters. No clock, account identity or bet size is an input to it — S2.4 shows this structurally, and behaviourally on the captured sample (one account; $0.10 and $10 stakes).
2.3Modulo Bias Analysis
Reducing a uniform 32-bit integer modulo range is only uniform when 2³² divides evenly by range — otherwise low results are slightly favoured. The specification removes this exactly: for each swap index i, chunks are accepted only below maxFair = floor(2³² / (i+1)) × (i+1), so every residue class is equally likely. Step 6 prints maxFair for all 415 ranges and finds each a whole multiple of its range — bias exactly 0 in our implementation; the worst per-draw chance of rejecting a chunk is 9.45 × 10⁻⁸ (range 410). With eight chunks per digest, rejecting all eight is far rarer still — about 1.6 × 10⁻⁵⁸ per digest at range 416.
range = 416 (the first swap, i = 415):
maxFair = floor(2³² / 416) × 416 = 4,294,967,040
rejected values = 4,294,967,296 − 4,294,967,040 = 256
P(one chunk rejected) = 256 / 2³² ≈ 5.96 × 10⁻⁸
P(all 8 chunks reject) ≈ 1.6 × 10⁻⁵⁸ → the specification gives no rule
The other ranges reject at most 406 values (range 410) — and every accepted
chunk is exactly uniform over its range, by construction.Across the capture’s 2,490,000 draws, a draw needing a later chunk was expected 0.0569 times and none occurred; the chance that all eight chunks of some digest would be rejected anywhere in the capture is 2.0 × 10⁻⁵². So the capture never exercised the rejection branch: a server doing a plain chunk % range with no rejection would have produced byte-identical data with probability 94.47%. If it were omitted, the exposure is at most 2.3 × 10⁻¹⁰ maximum point bias (range 257) and 2.4 × 10⁻⁸ total variation per draw (range 413) — immaterial to any figure quoted here (outputs/rng-branch-coverage.json, npm run branches). Step 6 shows the branch working on a fixed vector instead: nonce 5,591,308, cursor 409, range 410 rejects its first chunk and draws 116, where deleting the branch would give 312.
2.4RNG Isolation
blackjackShoe(serverSeed, clientSeed, nonce) is a pure function: no account identity, no bet size, no wall-clock, no game state. The audit demonstrates this two ways — structurally (our implementation needs only those three inputs to reproduce all 6,000 hands) and behaviourally (Phase E's 200 hands at a 100× larger stake reproduce card-for-card with the same function; Step 22).
2.5Monte Carlo Simulation (30M Rounds)
Three statistical passes back the construction-level argument, every random input derived from one pinned 32-byte master seed so each pass replays exactly. Pass 0 is the sanity harness for the headline: 30,000,000 rounds played under basic strategy by our engine over a plain fair shuffle (the rule set is the object under test, not the shoe algorithm), checked against the exact solve in S4. Pass 1 runs 1,000,000 shoes through the real HMAC-SHA256 blackjackShoe under a seed pair derived from the master seed and screens the output like a lab sample: first-card uniformity χ² = 6.73 (df 12, p = 0.8752), lag-1 serial correlation |z| = 0.57, runs test p = 0.6387 — no sign of bias. A calibration harness (npm run calibrate, 300 replicates × 5,000 rounds by default) re-runs the first-card statistic on fresh-seed input; it writes no committed artifact.
| Metric | Value |
|---|---|
| Total rounds (Pass 0, fair shuffle) | 30,000,000 — plain fair shuffle, no operator input |
| Exact basic-strategy RTP (per initial bet) | 99.512325% (edge 0.487675%) — the 30M simulation reads 99.5230%, 0.51σ from it |
| RTP per total wagered | 99.567565% · average wager 1.127741 units |
| Pass 1 — first-card uniformity (1M real shoes) | χ² = 6.73, df 12, p = 0.8752 |
| Pass 1 — lag-1 serial correlation | r₁ = 0.000573, |z| = 0.57 |
| Pass 1 — runs test (first-card rank sequence, high/low split at 7) | z = −0.47, p = 0.6387 |
| Pass 1 — RTP through the real algorithm (1M rounds) | 99.4977% ± 0.1150% — −0.13σ from the exact figure |
| Rounds | Cumulative simulated RTP (per initial bet) |
|---|---|
| 1,000 | 100.6500% |
| 3,000 | 98.1500% |
| 10,000 | 99.1950% |
| 30,000 | 99.3200% |
| 100,000 | 99.4440% |
| 300,000 | 99.5498% |
| 1,000,000 | 99.6640% |
| 3,000,000 | 99.5584% |
| 10,000,000 | 99.5318% |
| 30,000,000 | 99.5230% (±0.0210%, 1σ) |
/** Play one non-split hand; canDouble gates the first-move double. Returns [finalTotal, betUnits]. */function playHand(cards: number[], up: number, canDouble: boolean, draw: () => number): [number, number] { const bet = 1; for (;;) { const [t, soft] = totalOf(cards); if (t > 21) return [t, bet]; const a = soft ? softAction(t, up) : hardAction(t, up); if (a === 'D' || a === 'Ds') { if (cards.length === 2 && canDouble) { cards.push(draw()); return [totalOf(cards)[0], 2]; } if (a === 'Ds') return [t, bet]; cards.push(draw()); continue; // D with no double → hit } if (a === 'S') return [t, bet]; cards.push(draw()); // hit }} function dealerPlay(cards: number[], draw: () => number): number { // The draw rule is src/config.ts dealerDrawsOn (S17 from RULES.s17), shared with Step 11. for (;;) { const [t, soft] = totalOf(cards); if (dealerDrawsOn(t, soft)) cards.push(draw()); else return t; }}Three passes, three jobs: Pass 0 measures the game’s value under fair conditions (the 3σ sanity check on the exact solve), Pass 1 screens the real shuffle algorithm, and Pass 2 (S4.11) screens the captured epochs for cherry-picking. The convergence table below shows the simulation settling toward the exact figure as rounds accumulate — the early swings are what a ±3.6% standard error at 1,000 rounds predicts.
2.6Serial Independence
Serial independence means one shoe tells you nothing about the next. Consecutive bets share their seeds but differ by one nonce — and one nonce changes every HMAC message, so consecutive shoes share no HMAC message. Pass 1’s serial screens, computed on the first card of each shoe, agree: lag-1 correlation across 1,000,000 consecutive derivations is r₁ = 0.000573 (|z| = 0.57), and the runs test (p = 0.6387) finds no streak structure. The screens read the first card only, so they cannot see suit bias or bias at later positions.
Lag-1 autocorrelation (z) Measures whether one shoe’s first card predicts the next. The step fails at |z| ≥ 3; this run sits at 0.57.
Runs test Counts high/low streaks in the first-card rank sequence (rank above 7 vs 7 and under) against what independent trials produce. A low p-value would mean streaks are too clumpy or too regular; 0.64 is neither.
Calibration harness npm run calibrate re-runs the first-card uniformity test on 300 fresh-seed replicates × 5,000 rounds by default, so its false-alarm rate can be checked; it writes no committed artifact, so its figures are illustrative. The serial and runs statistics are not calibrated by it.
"pass1_fresh_seeds": { … "rounds": 1000000, "serverSeed": "c3b8b8d6b50b6796b56cd6a7f4484f35", "clientSeed": "df0a6fb592b2813da9d8c1cad1c98dfe", … "firstCardChi2": 6.725978000000001, "firstCardDf": 12, "firstCardPValue": 0.8751781565082382, "serialR1": 0.0005731791118897716, "serialR1Z": 0.5731791118897716, "serialRunsZ": -0.46947298217649175, "serialRunsPValue": 0.6387316225088242, "firstCardUniformFail": false, "serialFail": false, …}2.78-Deck Shoe Confirmation
The data itself rules out a single deck: if LIQD dealt from one deck, no round could contain the same card twice — but 1,269 captured rounds do (like the round holding three 10♠). No round shows more than 3 copies of one card, within the 8-copy limit (Step 30) — a check an infinite deck would also pass, so the deck count itself is pinned by the strict-order recomputation of every card from a 416-card shoe (Step 6).
| Evidence | Observed | Verdict |
|---|---|---|
| Rounds containing an exact-duplicate card | 1,269 / 6,000 | Impossible under a single deck — finite multi-deck confirmed |
| Maximum copies of one exact card in a round | 3 | Comfortably inside the 8-copy physical bound |
| Duplicate-round count vs the deck models | 1,269 — a single deck gives 0 | Consistent with a finite multi-deck shoe (the exact rate is not a scored test; the deck count is pinned by Step 6) |
| First-card rank fit over the 6,000 live rounds | χ² = 13.401, df 12, p = 0.3406 | Informational — no sign of misfit (Step 30) |
| Naturals in the live rounds | 253 player and 283 dealer, against 284.7 expected | Informational — z = −1.93 and −0.10, inside |z| ≤ 3 (Step 30) |
// ── Step 30: Deck-Model Confirmation (finite 8-deck) ──// The shoe is a finite 8-deck: exact-duplicate cards appear within single rounds// (refuting a single-deck model) and no exact card exceeds the 8-copy bound.let dupRounds = 0, maxCopies = 0;for (const b of bets) { const seq = [...b.playerHands.flatMap((h) => h.cards), ...(b.dealerHand || [])]; const m = new Map<string, number>(); for (const c of seq) m.set(c, (m.get(c) || 0) + 1); const mx = Math.max(0, ...m.values()); if (mx > 1) dupRounds++; if (mx > maxCopies) maxCopies = mx;}const bound = DECKS; // ≤ 8 copies of any exact card exist in the shoeconst deckOk = bets.length > 0 && dupRounds > 0 && maxCopies <= bound;Step 30 rules out a single deck from the captured hands alone, not from documentation; the 8-deck count is pinned by Step 6, since every captured card is read from the 416-card shoe at its position.
2.8Worked Example — Full RNG Trace
Real hand from dataset — epoch 0, nonce 0 (the first captured hand), verified from blackjack-6000hands.json. client seed audita6d2fe11bb1f, revealed server seed 6dca5e36b6b9dc56730e282ab3c2bae6 (commitment 948eff89…e12f42 — verified in S1.1). The shuffle starts at the highest index and walks down:
message = 'audita6d2fe11bb1f:0:415'
HMAC-SHA256 digest = d1b4e0facbbbc71d…
first uint32 chunk = 3,518,292,218 < maxFair(416) → j = 3518292218 % 416 = 90
swap(shoe[415], shoe[90])
message = 'audita6d2fe11bb1f:0:414'
digest = 2b43ce94c51668d0… → chunk 725,864,084 → j = 34 → swap(shoe[414], shoe[34])
message = 'audita6d2fe11bb1f:0:413'
digest = 438a8d5ac49ff072… → chunk 1,133,153,626 → j = 22 → swap(shoe[413], shoe[22])
… 411 more swaps …
message = 'audita6d2fe11bb1f:0:1'
digest = 68fd04938dd0bd07… → chunk 1,761,412,243 → j = 1 → swap(shoe[1], shoe[1])| Shoe Position | Card | Role |
|---|---|---|
shoe[0] | 7♥ | Player card 1 |
shoe[1] | 10♣ | Dealer upcard |
shoe[2] | 9♥ | Player card 2 |
shoe[3] | J♥ | Dealer hole card |
shoe[4] | 5♥ | Player hit → 7+9+5 = 21 |
Recompute this exact trace:
// inside the repo, after npm ci: node -r ts-node/register verify-ep0n0.js (src/rng.ts is TypeScript)const { blackjackShoe } = require('./src/rng');const shoe = blackjackShoe( '6dca5e36b6b9dc56730e282ab3c2bae6', // revealed server seed, epoch 0 'audita6d2fe11bb1f', 0); // client seed, nonceconsole.log(shoe.slice(0, 5).join(' '));// → HEART:7 CLUB:10 HEART:9 HEART:11 HEART:5 (7♥ 10♣ 9♥ J♥ 5♥ — the deal, then the player's hit)The player hit 16 into exactly 21 and beat the dealer’s 20 — a 0.10 stake returned 0.20. Every card above matches the capture record, and the deal and the hit card (shoe[0..4]) are pinned as unit tests inside the audit repo (tests/blackjack/rngTests.ts and vector V1 of rngSpecTests.ts), so the report, the dataset and the code tell one story.
Technical Evidence — RNG & Entropy Model5 sections
2.9Evidence Coverage Summary8 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Shoe recomputation (Step 6) | 6,000 shoes · 2,490,000 Fisher-Yates draws | Pass |
| Simulation Pass 1 — uniformity & independence (Step 18) | 1,000,000 real-shoe rounds, re-derived from 100 census rows | Pass |
| Deck-model confirmation (Step 30) | 1,269 duplicate-card rounds · 8-copy bound | Pass |
| Client-seed dependence (Step 7) | 974 / 1,000 and 982 / 1,000 first-card changes under a wrong and an empty seed | Pass |
| Draw-bias bound (Step 6) | maxFair a whole multiple of all 415 ranges — bias 0; the branch shown on a fixed vector | Pass |
First-card χ² calibration (npm run calibrate) | Harness included — 300 replicates × 5,000 rounds by default; writes no committed artifact | Available |
Rejection-branch coverage (npm run branches) | 2,490,000 captured draws: 0 later-chunk accepts, 0 cursor fallbacks (expected 0.0569 and 2.0 × 10⁻⁵²) — outputs/rng-branch-coverage.json | Accept path confirmed; rejection never needed |
| Real-shoe RTP consistency (Step 18) | 1,000,000 rounds · 99.4977%, −0.13σ from the exact figure | Pass |
2.10Code References11 source files
| File | Purpose |
|---|---|
| src/rng.ts | generateProvablyFairNumber (rejection sampling), blackjackShoe (backward Fisher-Yates), dealtHands, commitHash — written from docs/ALGORITHM.md |
| docs/ALGORITHM.md | The written specification the RNG is built from — it names the source of every step, five worked rounds and the open points |
| src/bias.ts | The draw-bias bound Step 6 prints, from maxFairFor |
| src/replay.ts · src/round-engine.ts | The three simulation passes as pure functions of the master seed, and the one round engine they play |
| src/simulate.ts | Runs the passes — into outputs/run/ with a field-by-field diff, or the committed artifact only under PF_EMIT=1 |
| src/calibration.ts | Calibration harness for the first-card statistic — 300 fresh-seed replicates by default |
| src/rng-branch-audit.ts | Replays every captured draw through an instrumented draw primitive and states the closed-form rejection expectations → outputs/rng-branch-coverage.json (npm run branches) |
| src/stats.ts | χ², serial-correlation, and runs-test implementations |
| tests/blackjack/rngTests.ts · rngSpecTests.ts | The captured epoch-0 shoe and its deal, the commitment hash, LIQD’s validation vectors as a cross-check, depth uniformity; the specification’s five worked rounds and 142 checks |
| tests/steps/standardization.ts | Step 30: deck-model confirmation (duplicates + 8-copy bound) and the live-sample fit |
| tests/steps/simulation.ts | Steps 17–19: scores the simulation artifact — round counts, df and α are pinned in src/config.ts, never read from the artifact; every Pass 1 statistic is re-derived from its census rows |
2.11Verified Invariants12 invariants
| Invariant | Result |
|---|---|
| Every dealt card equals the recomputed shoe position for all 6,000 rounds (34,734 cards) | Pass |
| maxFair is a whole multiple of all 415 ranges — per-draw bias exactly 0 in our implementation; the capture exercised only the accept path | Pass |
| First-card distribution over 1,000,000 real shoes: χ² = 6.73, df 12, p = 0.8752 | Pass |
| Lag-1 serial correlation across consecutive derivations: |z| = 0.57 | Pass |
| Runs test on the first-card rank (high/low) sequence: p = 0.6387 | Pass |
| No round contains more than 3 copies of one card (8-copy physical bound) | Pass |
| Duplicate-card rounds occur (1,269 / 6,000) — impossible under a single deck | Pass |
| Rejection census: 0 first-chunk rejections in 2,490,000 captured draws, against 0.0569 expected | Pass |
| The rejection branch works on a fixed vector (nonce 5,591,308, cursor 409, range 410 → 116) | Pass |
| Every Pass 1 statistic re-derived from the artifact’s 100 census rows; rows 0 and 99 re-dealt from the seed pair | Pass |
| 30M-round fair-shuffle simulation 0.51σ from the exact figure — inside the 3σ sanity-harness gate | Pass |
| Simulation artifact and convergence chart SHA-256 match their pins in scored Step 25 | Pass |
2.12Datasets UsedSource artifacts
2.13Reproduction InstructionsRun locally
Clone the repository, install dependencies, and replay the simulation and the verification:
git clone https://github.com/ProvablyFair-org/liqd-blackjack.gitcd liqd-blackjack && git checkout v1.0.1npm cinpm run simulate # replays all three passes from the pinned master seed into outputs/run/npm run verify # Steps 6, 7, 18, 26 and 30 cover S2cat outputs/simulation-results.jsonS2-related steps:
[PASS] Step 6 — Recomputation Parity
[PASS] Step 7 — Client-Seed Dependence
[PASS] Step 18 — Simulation Pass 1 — first-card uniformity, serial independence, RTP vs the exact solve
[PASS] Step 26 — Phase D Client-Seed Variation
[PASS] Step 30 — Deck-Model ConfirmationVerifier Parity
Does the live game actually follow its own rules?
This section validates that our implementation produces the exact same cards as the live game for every position in every hand, and that the ProvablyFair.org verifier agrees on a declared sample — 515 rounds, every split among them, and 121 full shoes. Any mismatch would break the fairness claim. We verified all 34,734 dealt cards across 6,000 hands in strict shoe order — including the pinned post-split draw order on all 401 split rounds. The section also confirms that every hand is resolved by LIQD’s rule set and that every payout — the 3:2 natural, insurance, and both side bets — matches the posted paytables.
What we verified
- Every hand independently recomputed from seeds — all 34,734 dealt cards verified in strict shoe order, not just the payout
- Dealer rules hold with zero deviations — stands on soft 17 (106 hands played out on it), peeks for blackjack (283 dealer naturals), never draws on 17 or more
- Split draw order pinned and reproduced for all 401 split rounds; one card to split aces, double after split, no surrender action observed
- Insurance appears only on a dealer Ace — 446 / 446 records; the 16 Ace-up rounds without an offer are player naturals
- Side bets (Perfect Pairs, 21+3) fixed by the first three shoe positions — 11,600 / 11,600 re-derived
- Payouts exact: 6,401 hands, 446 insurance bets and 11,600 side bets reconcile in exact 1e-8 units
- Bet amount is not an input to the shoe derivation — Phase E’s 200 hands at $10 reproduce card-for-card with the same stake-free function as the $0.10 phases (Step 22)
What this means for you
- Our verifier isn’t a simulation — it produces the exact same cards as the live game
- Every hand you play can be independently recomputed by anyone
- The dealer plays by the published rules every time — and the $10 rounds reproduce from their own revealed seeds with the same stake-free function as the $0.10 rounds
- On every captured round, the audited build dealt exactly the cards the specification gives
- Every hand independently recomputed from seeds — all 34,734 dealt cards verified in strict shoe order, not just the payout
- Dealer rules hold with zero deviations — stands on soft 17 (106 hands played out on it), peeks for blackjack (283 dealer naturals), never draws on 17 or more
- Split draw order pinned and reproduced for all 401 split rounds; one card to split aces, double after split, no surrender action observed
- Insurance appears only on a dealer Ace — 446 / 446 records; the 16 Ace-up rounds without an offer are player naturals
- Side bets (Perfect Pairs, 21+3) fixed by the first three shoe positions — 11,600 / 11,600 re-derived
- Payouts exact: 6,401 hands, 446 insurance bets and 11,600 side bets reconcile in exact 1e-8 units
- Bet amount is not an input to the shoe derivation — Phase E’s 200 hands at $10 reproduce card-for-card with the same stake-free function as the $0.10 phases (Step 22)
- Our verifier isn’t a simulation — it produces the exact same cards as the live game
- Every hand you play can be independently recomputed by anyone
- The dealer plays by the published rules every time — and the $10 rounds reproduce from their own revealed seeds with the same stake-free function as the $0.10 rounds
- On every captured round, the audited build dealt exactly the cards the specification gives
Card recomputation (strict order)Pass
Split draw orderPass
Dealer rules (S17 / peek)Pass
Player mechanicsPass
Insurance offer conditionPass
Side bets fixed at the dealPass
Payout correctnessPass
Bet-size independencePass
ProvablyFair.org verifier parityPass
All 6,000 hands match our independent implementation exactly — 34,734 cards in strict shoe order, across all six capture phases — and the ProvablyFair.org verifier agrees on every round it was run on. The dealer stands on soft 17 and peeks for blackjack, both side bets pay their posted paytables, insurance pays exactly 2:1, and every split round follows the post-split order pinned from the data. Payout math is exact at both stakes.
| Check | Result | Evidence |
|---|---|---|
| Card recomputation (strict order) | Pass | 6,000 / 6,000 hands · 34,734 dealt cards · 0 mismatches (Step 6) |
| Split draw order | Pass | 401 / 401 split rounds — pinned draw order reproduced position-exact; the split hand plays out first (Step 6) |
| Dealer rules (S17 / peek) | Pass | 0 over-draws and 0 under-draws — 106 stands on soft 17 (108 hands finished on it); 283 dealer naturals settled before any extra wager (Steps 11, 13) |
| Player mechanics | Pass | Value-based splits, one card to split aces, double after split, no surrender action observed — the action log matches the cards drawn on all 6,000 rounds (Steps 6, 12, 14, 27) |
| Insurance offer condition | Pass | 446 / 446 insurance records sit on a dealer Ace, one per insurance request of the capture (Step 28) |
| Side bets fixed at the deal | Pass | 11,600 / 11,600 side-bet card snapshots equal the initial deal, and every result re-derives from the deal (Steps 15, 16, 29) |
| Payout correctness | Pass | 6,401 hands (3:2 naturals, doubles, splits, pushes), 446 insurance bets at 2:1, and 11,600 side bets reconciled exactly (Steps 8–10, 15, 16) |
| Bet-size independence | Pass | Bet amount is absent from the shoe derivation — Phase E’s 200 hands at $10 recompute card-for-card (Step 22) |
| ProvablyFair.org verifier parity | Pass | The vendored site verifier reproduces all 401 split rounds and the first round of every epoch — 515 rounds, 916 hands, 0 disagreements — and its full 416-card shoe equals ours on 121 shoes (Step 6) |
Card recomputation (strict order)Pass
Split draw orderPass
Dealer rules (S17 / peek)Pass
Player mechanicsPass
Insurance offer conditionPass
Side bets fixed at the dealPass
Payout correctnessPass
Bet-size independencePass
ProvablyFair.org verifier parityPass
All 6,000 hands match our independent implementation exactly — 34,734 cards in strict shoe order, across all six capture phases — and the ProvablyFair.org verifier agrees on every round it was run on. The dealer stands on soft 17 and peeks for blackjack, both side bets pay their posted paytables, insurance pays exactly 2:1, and every split round follows the post-split order pinned from the data. Payout math is exact at both stakes.
How It Works — Verifier Parity11 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 casino fixed its randomness before play; parity shows the game actually used it. A platform could publish honest seeds and still deal from somewhere else entirely; only recomputing every card from the seeds and matching it against what was really dealt closes that gap. This section is the audit’s core: our own implementation, written from the specification without LIQD’s code, run against all 6,000 captured hands — with the ProvablyFair.org verifier as a second check.
3.2Six-Phase Collection Design
Data was collected across six structured phases, each designed to exercise a specific mechanic at verifiable volume. The phases are complementary — together they cover baseline volume, deep draw paths, split settlement, auditor-controlled client seeds and stake independence. Between them: 401 splits, 1,283 doubled rounds, 446 insurance bets, 253 naturals and 11,600 side bets.
| Phase | Bets | Bet Amount | Client Seed | Purpose |
|---|---|---|---|---|
| A — Basic strategy | 3,300 | $0.10 main + $0.10 PP + $0.10 21+3 | 66 distinct (rotated per epoch) | Baseline RNG sampling under basic strategy (S17) across 66 seed pairs |
| B — Aggressive play | 1,000 | $0.10 main + $0.10 PP + $0.10 21+3 | 20 distinct | Hits to 17 and never doubles (pairs are still split) — multi-card hands and dealer-draw lines |
| C — Split-forcing | 500 | $0.10 main + $0.10 PP + $0.10 21+3 | 10 distinct | Split-force play — splits, double after split and split aces; re-split never attempted |
| D — Auditor client seeds | 500 | $0.10 main + $0.10 PP + $0.10 21+3 | 10 (pfaudit-…, auditor-controlled) | Verify the server honours arbitrary player-supplied client seeds |
| E — Stake invariance | 200 | $10.00 (no side bets) | 4 distinct | Confirm bet amount is not an RNG input — 100× the standard $0.10 stake |
| F — Split-forcing (second block) | 500 | $0.10 main + $0.10 PP + $0.10 21+3 | 10 distinct | Second split-forcing block (one split per round) — more split and double coverage |
src/config.ts — no phase drifted (Steps 24, 31).3.3Strict-Order Card Recomputation (Step 6)
For each bet, the engine rebuilds the full 416-card shoe from (serverSeed, clientSeed, nonce) and walks the recorded hand against it position by position: shoe[0] player, shoe[1] dealer up, shoe[2] player, shoe[3] dealer hole, then every hit, double card, split card, and dealer draw in sequence from shoe[4] onward. A hand passes only if every recorded card equals its recomputed position. Step 6 also binds the action log to the cards: hit + double tokens must equal the cards drawn, and the token stream must obey a six-rule grammar — no token after a hand’s last terminal; no double after a hit; split and no-ins only at the head; a hand ends without standing only on a bust, 21, a split ace or a dealer natural; no stand on a busted hand, nor on a single hand that reached 21; and the capture script’s doubled flag must agree with the double tokens — 6,000 / 6,000 rounds consistent. The tokens are the requests the capture sent; the check binds them to the cards, not to a strategy.
3.4Pinned Split Order (Steps 6, 27)
Split rounds consume two extra deal positions and interleave two hands, so their draw order must be pinned, not guessed. The order LIQD deals, pinned from the data: shoe[4] completes the main hand, shoe[5] completes the split hand — then the split hand plays out fully first, then the main hand, then the dealer. Step 6 rebuilds each split round’s dealt order from the recorded hands (splitDealtSequence), and all 401 split rounds match the recomputed shoe card-for-card in this order. The verification treats a reordering — same cards, opposite hand assignment — as a hard failure. The order matters: feeding shoe[4…] to the hands in the obvious order reproduces every unsplit round but fails 255 of the 401 split rounds.
3.5Dealer-Side Rules (Steps 11, 13)
Three dealer behaviours were verified on every hand. S17: the dealer stands on every 17, including the 106 played-out hands that finished on soft 17 (2 more ended there when a player natural settled the round first), never draws on 17 or more, and never stands below 17 while a player hand is live. Peek / OBO: on Ace and ten-value upcards the dealer checks the hole card before play continues; all 283 dealer naturals (152 Ace-up, 131 ten-up) settled before any play action (hit, stand, double or split), so no double or split stake was ever lost to one. No improvisation: replaying every dealer hand against S17 finds 0 over-draws and 0 under-draws — the check that closes the gap Step 6 leaves, since Step 6 compares dealt cards with a prefix of the shoe and cannot know how many cards a round should consume.
3.6Player-Side Mechanics (Steps 12, 14, 27)
The action vocabulary is closed: hit, stand, double, split, plus no-ins, the capture’s insurance request. No surrender action appears in 6,000 hands — the capture script has no surrender call, and the RTP is solved without surrender (Step 14). Splits are value-based (any two ten-value cards may split, not just same-rank pairs), split aces receive exactly one card each, no round reached a third hand (re-split was never attempted), and doubling is available on any first two cards — on non-split hands every two-card total from 5 to 20 was doubled at least once — including after a split (165 DAS rounds). Every mechanic was checked on every hand where it could apply.
3.7Insurance Mechanics (Steps 10, 28)
Insurance is offered when the dealer’s upcard is an Ace and the player holds no natural; it stakes half the main bet and pays 2:1 on a dealer natural. It is a separate wager and uses no shoe card. One capture detail matters: the capture answered every offer with its own zero-amount insurance request (place-insurance-bet {betAmount: 0}, logged as no-ins), which is not the decline LIQD’s interface sends ({acceptInsurance: false}), and the casino booked a half-stake insurance bet on all 446 offers. So every insurance offer in the data carries a booked bet — 446 of the 462 Ace-up rounds (the other 16 are player naturals, which get no offer): 145 won at exactly 3× gross, 301 lost. One offer declined with LIQD’s own NO button on qa on 13 October 2026 booked no insurance bet (E18, outside the scored dataset). The headline RTP assumes insurance is declined, as basic strategy does; with insurance on every offer the edge is 0.7612% per initial bet (Step 28).
3.8Side-Bet Provenance & Deal-Time Invariance (Steps 15, 16, 29)
Perfect Pairs reads your two cards (shoe[0], shoe[2]); 21+3 adds the dealer's upcard (shoe[1]). Both are therefore fixed the instant the shoe is committed — before you act, before the dealer draws. The audit re-derived every category from those positions and confirmed that no later card, action or dealer draw moved a side-bet result: 11,600 / 11,600 classifications and payouts match, including the capture's single 100:1 suited-trips hit.
3.9Phases D & E — Seed & Stake Sensitivity (Steps 22, 26)
Two phases exist purely to falsify hidden couplings. Phase D (500 hands, 10 auditor-chosen client seeds): 490 / 500 hands deal a different first card than a baseline seed would — the ≈98% a 1-in-52 chance-match rate predicts — showing the player’s seed genuinely drives the shuffle (Step 26). Those ten seeds followed a predictable pattern, so epochs 96–105 are verified by recomputation and screened for seed selection in S4.11. Phase E (200 hands at $10.00, a 100× stake jump, no side bets): every round reproduces card-for-card with the same shoe function as the $0.10 rounds — a function that takes no stake argument (Step 22).
3.10Per-Hand Payout Replay (Step 8)
Beyond the cards, the engine re-derives each hand’s outcome and each round’s credit from the cards and the rules. One capture detail matters for readers of the raw data: the platform’s per-hand result field is binary — it labels any hand that returned money as won, every push at exactly 1× included. The audit therefore derives outcomes from the cards and the rules, reconciles the money, and then checks the label against the derivation: it agrees on 6,401 / 6,401 hands. Full RTP-side treatment lives in S4.
3.11Worked Example — Full Parity Verification
Real bet from Phase B — epoch 72, nonce 23, a round that exercises the split, side-bet, and dealer paths together. Verified from blackjack-6000hands.json. Client seed audit677f0cfcf94a, revealed server seed 7a18a3b3a6f54c0b3b981934ac31bdd9, commitment b5da66c6…f9b81 verified. The recomputed shoe opens:
shoe[0] = 10♠ (Player card 1)
shoe[1] = 10♠ (Dealer upcard — a second 10♠: the 8-deck shoe at work)
shoe[2] = 10♠ (Player card 2 — a THIRD 10♠)
shoe[3] = 7♦ (Dealer hole)
shoe[4] = 10♥ (Main hand's second card after the split)
shoe[5] = 5♠ (Split hand's second card)| Check | Recorded | Recomputed |
|---|---|---|
| Split of 10♠ + 10♠ | split → two hands | identical |
Pinned order: main₂ = shoe[4], split₂ = shoe[5] | 10♠+10♥ (20) · 10♠+5♠ (15) | identical, position-exact |
| Perfect Pairs on 10♠ 10♠ | PERFECT_PAIR · pays 2.60 | perfect pair · 26× × 0.10 = 2.60 |
| 21+3 on 10♠ 10♠ + 10♠ | SUITED_THREE_OF_A_KIND · pays 10.10 | suited trips · 101× × 0.10 = 10.10 |
| Dealer play-out on hard 17 (10♠ 7♦) | stands | stands |
| Round credit | 12.90 | 0.20 (hands) + 2.60 + 10.10 = 12.90 |
Three copies of the 10♠ in one round is itself deck-model evidence (S2.7), and the 21+3 hit is the only 100:1 payout in the entire capture — verified at exactly 101× gross. One bet, checked by Steps 1, 6, 8, 11, 15, 16, 27 and 29 — all exact.
Technical Evidence — Verifier Parity5 sections
3.12Evidence Coverage Summary9 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Strict-order recomputation (Step 6) | 6,000 hands · 34,734 dealt cards | Pass |
| Split order & rules (Steps 6, 27) | 401 splits · 134 mixed ten-value · 35 ace splits | Pass |
| Dealer rules (Steps 11, 13) | Every dealer hand replayed against S17 — 106 soft-17 stands; 283 dealer naturals | Pass |
| Action vocabulary / no surrender (Step 14) | All 6,000 rounds’ action streams | Pass |
| Insurance offer & payout (Steps 10, 28) | 446 insurance bets | Pass |
| Side-bet provenance (Steps 15, 16, 29) | 11,600 side bets · deal-time invariance | Pass |
| Phase coverage & labels (Steps 24, 31) | 6 phases · counts and stakes match the plan | Pass |
| Seed sensitivity / $10 reconstruction (Steps 26, 22) | Phase D 490 / 500 first-card changes · Phase E 200 / 200 rounds at $10.00 recompute | Pass |
| ProvablyFair.org verifier (Step 6) | 515 rounds · 916 hands · 121 full shoes | Pass |
3.13Code References7 source files
| File | Purpose |
|---|---|
| tests/steps/parity.ts | Step 6: strict-order shoe recomputation incl. the pinned split order, the action grammar, the draw-bias bound and the site-verifier arm; Step 7: client-seed dependence |
| tests/steps/rules.ts | Steps 11–14: dealer and player rules — S17, peek, DAS, surrender absence |
| tests/steps/sidebets.ts | Steps 15–16: side-bet classification against the published paytables |
| tests/steps/standardization.ts | Steps 22–31: bet-size invariance, side-bet config, phase coverage, artifact hash integrity (25), Phase D seeds, split & insurance rules, deal-time invariance, deck model (30), stake brackets |
| tests/steps/hands.ts | Split-aware initialTwo() shared by Steps 15, 16, 23, 29 and 30 (the initial pair on a split round is card 1 of each hand, not playerHands[0].cards) |
| src/types.ts | Card / hand / bet record types — states each field the capture script wrote and why the steps may read it |
| verifier-parity/ · src/site-verifier.ts | The ProvablyFair.org site verifier, vendored byte for byte, pinned by SHA-256 and run by Step 6 |
3.14Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Card record | playerHands[].cards and dealerHand — every dealt card per hand, in table order. The capture’s own recomputation fields (localSequence, verified, commitVerified, chainLinkOk) are deleted by src/loader.ts before any step runs; antiCircularityTests.ts asserts they are gone. |
| Split pin | splitDealtSequence() in tests/steps/parity.ts — the pinned draw-order reconstruction split rounds are checked against (a verifier function, not a dataset field) |
| Actions | actions[] — the requests the capture sent, in order: hit / stand / double / split plus no-ins, the capture’s insurance request, on which the casino booked an insurance bet |
| Money | winningAmount per hand and per round, betAmount, side-bet records with their own stakes and settlements |
The per-hand result field is binary (won = any money returned, pushes included; lost = zero), and dealerPoints reads 0 in 381 of the 526 rounds that ended at the deal on a natural — epoch 0 nonce 12 reports 0 while the dealer holds 17. The audit derives both from the cards and the rules rather than trusting either field.
3.15Verified Invariants10 invariants
| Invariant | Result |
|---|---|
| Every dealt card equals its recomputed shoe position (strict order) for all 6,000 rounds | Pass |
| Split rounds follow main₂ = shoe[4], split₂ = shoe[5], split-plays-first — 401 / 401 | Pass |
| The dealer never draws on 17 or more and never stands below 17 with a live hand; 106 soft-17 stands | Pass |
| Dealer naturals end the round before any extra wager (peek / OBO) — 283 / 283 | Pass |
| No surrender token exists in any action stream | Pass |
| Insurance records sit only on a dealer Ace — 446 bets, half-stake, 3× gross on wins | Pass |
| Side-bet categories re-derive from shoe[0], shoe[2], shoe[1] on all 11,600 bets | Pass |
| Phase E’s 200 $10.00 rounds recompute card-for-card with the same shoe function as the $0.10 rounds | Pass |
| Hit + double tokens equal the cards drawn, and the action grammar holds, on 6,000 / 6,000 rounds | Pass |
| The ProvablyFair.org verifier reproduces 515 / 515 sampled rounds and 121 / 121 full shoes | Pass |
3.16Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-blackjack.gitcd liqd-blackjack && git checkout v1.0.1npm cinpm run verify # Steps 6, 8–16, 22–24 and 26–31 cover S3S3-related steps:
[PASS] Step 6 — Recomputation Parity
[PASS] Step 8 — Per-Hand Payout (reconstructed from cards)
[PASS] Step 9 — Blackjack Pays 3:2
[PASS] Step 10 — Insurance Pays 2:1 iff Dealer Blackjack
[PASS] Step 11 — Dealer Stands on Soft 17 (S17)
[PASS] Step 12 — Double-After-Split Allowed (DAS)
[PASS] Step 13 — Dealer Peeks for Blackjack
[PASS] Step 14 — No Surrender Action Observed
[PASS] Step 15 — Perfect Pairs Side Bet
[PASS] Step 16 — 21+3 Side Bet
[PASS] Step 22 — Bet-Size Invariance
[PASS] Step 23 — Side-Bet Config Completeness
[PASS] Step 24 — Phase Coverage & Labels
[PASS] Step 26 — Phase D Client-Seed Variation
[PASS] Step 27 — Split Rules Verification
[PASS] Step 28 — Insurance Offer Condition
[PASS] Step 29 — Side-Bet Deal-Time Invariance
[PASS] Step 30 — Deck-Model Confirmation
[PASS] Step 31 — Stake Bracket BoundsRTP & Payout Logic
Is the house edge what the casino claims?
This section verifies the house edge of LIQD Blackjack against what LIQD publishes. The key test is anti-circularity: the RTP is derived from the rules alone — an exact 8-deck solve of the basic-strategy table, anchored by an exact-rational oracle and the published Wizard of Odds figure, with a 30-million-round simulation as a sanity harness — and no operator figure enters the derivation. We hold LIQD’s published 99.52% to the solve under the RTP claim rule, check every payout, and screen the revealed seeds for return-based selection.
What we verified
- Exact basic-strategy RTP is 99.512325% per initial bet — a deterministic 8-deck solve, anchored by an exact-rational oracle (1e-12) and within one unit of the last digit of the published Wizard of Odds figure; the 30M-round simulation is consistent at 0.51σ
- LIQD’s published 99.52% RTP holds under the RTP claim rule — the exact figure sits 0.007675 pp below it, inside the ±0.1 pp band
- No FX haircut observed — every credit reconciles at face value from the cards (exchange-rate field 1.00 on all 12,046 records carrying it)
- Side bets priced from exact combinatorics against the verified paytables: Perfect Pairs 2.1687% edge, 21+3 3.7039% edge
- Cherry-pick screen: the 120 casino seeds show no sign of selection on the money rule (pooled z = −0.746)
What this means for you
- Played with basic strategy, the house edge is about half a percent of your initial bet — LIQD’s published 0.48% equals the exact edge truncated to two decimals
- The RTP is derived from the rules, not from the casino’s own claim — it doesn’t rely on trusting LIQD
- Side bets carry a higher edge than the main game — that’s standard, and the exact figures are stated here
- Insurance costs you: taken on every offer it raises the edge to 0.7612%
- The shuffle function takes no bet input — the $10 rounds reproduce just like the $0.10 rounds
- Setting your own client seed after the commitment is what protects you from seed selection
- Exact basic-strategy RTP is 99.512325% per initial bet — a deterministic 8-deck solve, anchored by an exact-rational oracle (1e-12) and within one unit of the last digit of the published Wizard of Odds figure; the 30M-round simulation is consistent at 0.51σ
- LIQD’s published 99.52% RTP holds under the RTP claim rule — the exact figure sits 0.007675 pp below it, inside the ±0.1 pp band
- No FX haircut observed — every credit reconciles at face value from the cards (exchange-rate field 1.00 on all 12,046 records carrying it)
- Side bets priced from exact combinatorics against the verified paytables: Perfect Pairs 2.1687% edge, 21+3 3.7039% edge
- Cherry-pick screen: the 120 casino seeds show no sign of selection on the money rule (pooled z = −0.746)
- Played with basic strategy, the house edge is about half a percent of your initial bet — LIQD’s published 0.48% equals the exact edge truncated to two decimals
- The RTP is derived from the rules, not from the casino’s own claim — it doesn’t rely on trusting LIQD
- Side bets carry a higher edge than the main game — that’s standard, and the exact figures are stated here
- Insurance costs you: taken on every offer it raises the edge to 0.7612%
- The shuffle function takes no bet input — the $10 rounds reproduce just like the $0.10 rounds
- Setting your own client seed after the commitment is what protects you from seed selection
Anti-circularityPass
Theoretical RTP (per initial bet)Pass
RTP claim (RTP-CLAIM 1.2.0)Pass
Convention disclosurePass
House-edge reconciliationPass
No FX haircut observedPass
Real-shoe consistencyPass
Side-bet economicsPass
Cherry-pick screen (Pass 2)Pass
LIQD Blackjack returns 99.512325% per initial bet under basic strategy with insurance declined (0.487675% house edge; 99.567565% / 0.432435% per total amount wagered, average wager 1.127741) — solved, not sampled, from the rules alone. The figure is anchored three ways: an exact-rational tiny-shoe oracle reproduced at 1e-12, the published Wizard of Odds figure for this rule set (0.48768%) matched within one unit of its last digit, and a 30-million-round simulation consistent at 0.51σ. LIQD’s published 99.52% holds under the RTP claim rule. Side bets carry their own exactly computed edges (Perfect Pairs 2.17%, 21+3 3.70%), and every payout reconciles exactly.
Anchored by an exact-rational tiny-shoe oracle (1e-12) and the published Wizard of Odds figure for these rules (0.48768% edge), matched within one unit of its last digit. Per total amount wagered: 99.567565%.
| Check | Result | Evidence |
|---|---|---|
| Anti-circularity | Pass | RTP derived from the rules under basic strategy — an exact 8-deck solve anchored by an exact-rational oracle (1e-12) and the published Wizard of Odds figure, with a 30M-round simulation harness; no operator figure enters the derivation (Step 17) |
| Theoretical RTP (per initial bet) | Pass | 99.512325% (edge 0.487675%) — deterministic 8-deck solve; Wizard of Odds 0.48768% matched within one unit of its last digit; 30M simulation 99.5230%, 0.51σ (Step 17) |
| RTP claim (RTP-CLAIM 1.2.0) | Pass | LIQD publishes 99.52% (Blackjack game page: header “Edge: 0.48%”, structured data 99.52%); exact 99.512325%, gap −0.007675 pp — inside the ±0.1 pp band (Step 20) |
| Convention disclosure | Pass | 99.567565% per total wagered also stated — an average wager of 1.127741 units per round separates the two conventions |
| House-edge reconciliation | Pass | Game-page header 0.48% (equal to the exact per-initial-bet edge truncated to two decimals) reconciled against the solve (Step 20); the stats panel’s 0.49% equals the same edge rounded |
| No FX haircut observed | Pass | exchangeRate 1.00 on all 12,046 side-bet and insurance records; no main-game hand carries it; the credits reconcile at face value (Steps 8, 10, 15, 16 and 21) |
| Real-shoe consistency | Pass | 1,000,000 rounds through the real HMAC-SHA256 shoe: 99.4977% ± 0.1150% — −0.13σ from the exact figure (Step 18) |
| Side-bet economics | Pass | Perfect Pairs 2.1687% edge (the coloured pair pays a player-favourable 13:1), 21+3 3.7039% — exact combinatorics from the verified paytables (Step 20) |
| Cherry-pick screen (Pass 2) | Pass | 120 casino seeds: money rule 1 / 120 flagged (gate 6), pooled z = −0.746 (gate −3.09); rank screen 7 vs 5.7 expected (P = 0.3444); a 50-candidate grind is flagged at z = −23.56 (Step 19) |
Anti-circularityPass
Theoretical RTP (per initial bet)Pass
RTP claim (RTP-CLAIM 1.2.0)Pass
Convention disclosurePass
House-edge reconciliationPass
No FX haircut observedPass
Real-shoe consistencyPass
Side-bet economicsPass
Cherry-pick screen (Pass 2)Pass
LIQD Blackjack returns 99.512325% per initial bet under basic strategy with insurance declined (0.487675% house edge; 99.567565% / 0.432435% per total amount wagered, average wager 1.127741) — solved, not sampled, from the rules alone. The figure is anchored three ways: an exact-rational tiny-shoe oracle reproduced at 1e-12, the published Wizard of Odds figure for this rule set (0.48768%) matched within one unit of its last digit, and a 30-million-round simulation consistent at 0.51σ. LIQD’s published 99.52% holds under the RTP claim rule. Side bets carry their own exactly computed edges (Perfect Pairs 2.17%, 21+3 3.70%), and every payout reconciles exactly.
How It Works — RTP & Payout Logic15 sections
4.1RTP & House Edge — Published vs Derived (Step 20)
LIQD publishes the edge directly. Its Blackjack game page, read on 1 October 2026, shows Edge: 0.48% in the header — RTP 99.52%, which the page’s structured data also gives — and RTP (optimal play) 99.51% and House Edge 0.49% in its stats panel. The audit’s exact solve gives 0.487675% per initial bet and 0.432435% per total amount wagered (average wager 1.127741). Against the 0.48% header the per-initial-bet edge sits 0.0077 pp higher: it rounds to 0.49% and truncates to 0.48%, so the header equals the exact edge truncated to two decimals (how LIQD arrives at it is not visible from the client) — within one display unit, which Step 20 records as a note, not a flag.
| Source | Figure | What it is |
|---|---|---|
| LIQD game page — header | 0.48% (RTP 99.52%) | the figure shown to players, and the claim held under the RTP claim rule (S4.2) |
| LIQD game page — stats panel | RTP 99.51% · House Edge 0.49% | the same edge, rounded |
| This audit — per initial bet | 0.487675% (exact) | exact 8-deck basic-strategy edge, the standard convention |
| This audit — per total amount wagered | 0.432435% (exact) | element of risk, average wager 1.127741 |
4.2RTP Claim Rule (Step 20)
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 — here from the 8-deck solve, never from the casino’s figure — and must sit within ±0.1 percentage points of the claim. A setting below that band on the house side is a miss: the verdict then reads RTP FIGURE OVERSTATED, even when every draw check passes. A setting above the claim favours the player and never fails. Blackjack has one setting — the main game under its single rule set, played by the basic-strategy table; the side bets are separate wagers. The rule is scored on Step 20, the house-edge step.
| Item | Detail |
|---|---|
| Claim | 99.52% RTP — a flat 0.48% house edge |
| Where LIQD publishes it | The Blackjack game page, liqd.com/originals/blackjack: the header “Edge: 0.48%” and the page’s structured-data RTP of 99.52% — read logged out on 1 October 2026 (evidence/rtp-page-labels-2026-10-01.json). The same page shows “RTP (optimal play) 99.51%”; holding the higher 99.52% covers both |
| Settings tested | 1 — the main game, 8 decks, total-dependent basic strategy, per initial bet |
| Method | Exact: the finite 8-deck solve in outputs/exact-rtp.json, taken as the exact value of its double (1120409176472375 / 1125899906842624) |
| Band | ±0.1 percentage points (house side fails, player side never fails) |
| True RTP | 99.512325% |
| Gap to the claim | −0.007675 pp |
| Settings beyond the band | 0 on the house side · 0 on the player side |
| Scored on | Step 20 (profile check BLACKJACK-19), rule enforced |
| Result | Pass — the claim holds for every setting it states a figure for; no fix needed |
| Setting | Exact RTP | RTP | Gap to 99.52% | Side of the band | Result |
|---|---|---|---|---|---|
| Main game — 8 decks, basic strategy, per initial bet | 1120409176472375 / 1125899906842624 | 99.512325% | −0.007675 pp | within ±0.1 pp | Pass |
export const RTP_CLAIM: RtpClaim = { kind: 'flat', rtp: HELD_RTP, // …};// …/** The scored step that carries BLACKJACK-19 (src/profile-map.ts). */export const RTP_CLAIM_STEP = 20;// …export function computeRtpClaim(): RtpClaimResult { const buf = fs.readFileSync(path.join(__dirname, '..', 'outputs', 'exact-rtp.json')); if (createHash('sha256').update(buf).digest('hex') !== EXACT_RTP_SHA256) throw new Error('outputs/exact-rtp.json does not match its pin'); const x = JSON.parse(buf.toString('utf8')).finite8TD; const r = rationalFromDouble(x.rtpPerInitialBet); const acc = new RtpClaimAccumulator(RTP_CLAIM); acc.add('main game, 8-deck basic strategy (TD), per initial bet', r.n, r.d); return acc.result();}outputs/rtp-claim-table.json, pinned by SHA-256 (9e447e3b…) and re-derived on every run.4.3RTP Conventions — Per Initial Bet vs Per Total Wagered
"RTP" hides a choice: divide returns by the initial bet, or by all money wagered including doubles and split stakes? Both are legitimate; they answer different questions. Per initial bet: 99.512325% (edge 0.487675%) — what a unit placed at the start returns on average. Per total wagered: 99.567565% (edge 0.432435%) — the element-of-risk view. The gap exists because basic strategy adds money mid-hand (average 1.127741 units wagered per round), and that added money is placed in advantageous spots. Both figures come from the same exact solve; this report states both and labels every figure with its convention.
// In infinite mode the enumeration is already normalised; in finite mode the// three-card probabilities sum to 1 by construction. Guard anyway.const norm = pSum > 0 ? pSum : 1;const evPerInitial = evSum / norm;const avgWager = wagerSum / norm; return { model: this.model, strategy: this.strategy, rules: this.rules, evPerInitialBet: evPerInitial, rtpPerInitialBet: 1 + evPerInitial, edgePerInitialBet: -evPerInitial, avgWager, evPerTotalWagered: evPerInitial / avgWager, rtpPerTotalWagered: 1 + evPerInitial / avgWager, edgePerTotalWagered: -evPerInitial / avgWager,4.4Exact 8-Deck RTP Solver (Step 17)
The edge is solved, not sampled. src/exact-play.ts enumerates every initial deal from the 8-deck, 416-card composition without replacement — the deck model the captured hands confirm (Steps 6 and 30) — and computes the exact expected value of playing it out under the total-dependent basic-strategy table (src/strategy.ts), including the hole-card conditioning after a negative peek. Result: 99.512325% / 0.487675% per initial bet, deterministic, emitted to outputs/exact-rtp.json (npm run rtp regenerates it byte-identically). The headline is solved for no surrender, no re-split and insurance declined.
Two anchors sit outside the solver. An exact-rational tiny-shoe oracle (anchor/oracle.py, Python fractions) is a second, physical enumerator — the hole is a concrete dealt card, splits play sequentially against a genuinely depleted shoe, no conditional probability anywhere — whose exact fractions the solver must reproduce at 1e-12 on three constructed shoes (peek-heavy, split-heavy, and a peek-free control). It shares the strategy table, the rules reading and the implementer with the solver, so it anchors arithmetic, not the table’s optimality. The organisationally independent anchor is the Wizard of Odds figure for this rule set (the cross-validation below). A declared mutation shifts the natural payout from 3:2 to 29:20, and the Wizard of Odds gate must then fail (tests/mutations.json).
} else if ((up === A_IDX || up === T_IDX) && this.rules.peek && this.strategy === 'TD') { // ── Hole-explicit peek branch ──────────────────────────────────── // Conditioning happens ONCE, here, and never again below. // … // Here the hole is a CONCRETE card, drawn and removed from the shoe // before the player acts. Inside each branch the dealer's hand is known // and every player draw is the plain physical law on the depleted shoe. // … const natRank = up === A_IDX ? T_IDX : A_IDX; const N = sum(c); const pNat = pDraw(c, N, natRank, this.model); const denomP = 1 - pNat; let evPlay = 0, wagerPlay = 0; for (let h = 0; h < 10; h++) { if (h === natRank || c[h] === 0) continue; const w = denomP > 0 ? pDraw(c, N, h, this.model) / denomP : 0; if (w <= 0) continue; if (this.model.kind === 'finite') c[h]--; // the hole is a real card const r = this.initialEV(c, p1, p2, up, h); if (this.model.kind === 'finite') c[h]++; evPlay += w * r.ev; wagerPlay += w * r.wager; } // OBO: a dealer natural takes the initial bet only. ev = pNat * -1 + (1 - pNat) * evPlay; wager = pNat * 1 + (1 - pNat) * wagerPlay;} else if (up === A_IDX || up === T_IDX) { // …The oracle gate — three exact fractions, asserted at 1e-12:
/** Rank order is [2..9, T, A]; index 8 = Ten, index 9 = Ace. */const SHOES: Array<{ name: string; counts: number[]; exact: [number, number]; note: string }> = [ { name: 'control — peek path unreachable (no Ten, no Ace)', counts: [3, 0, 0, 0, 0, 3, 3, 3, 0, 0], exact: [437, 13200], note: 'proves the core engine (recursion, splits, DAS, dealer play, settlement) is sound', }, { name: 'peekA — Ten/Ace heavy', counts: [0, 0, 0, 0, 0, 2, 2, 2, 5, 3], exact: [17081, 194040],// …},{ name: 'peekB — split-heavy peek shoe', counts: [2, 0, 0, 0, 0, 0, 2, 2, 4, 3], exact: [78863, 900900],// … },];// …describe('Exact solver — tiny-shoe rational oracle (independent anchor)', () => { for (const { name, counts, exact, note } of SHOES) { const [num, den] = exact; it(`reproduces ${num}/${den} exactly — ${name}`, () => { const got = solveShoe(counts); const want = num / den; assert.ok( Math.abs(got - want) < 1e-12, `${name}\n expected ${want.toFixed(15)} (= ${num}/${den})\n ` + `got ${got.toFixed(15)}\n delta ${(got - want).toExponential(3)}\n ${note}`, ); }); }4.5Simulation Pass 0 — 30M Rounds (Step 17 Sanity Harness)
Our engine dealt 30,000,000 complete rounds from fairly shuffled 8-deck shoes (a fair shuffle driven by a seeded xoshiro128** stream rather than the HMAC shoe — here the rule set is the object under test — and zero operator input), playing the same basic-strategy table, every draw taken from a stream derived from the pinned master seed. The committed run reads 99.5230% per initial bet (0.4770% edge, standard error ±0.0210%) and 99.5771% per total wagered — consistent with the exact figure at 0.51σ in both conventions.
The simulation is a 3σ sanity harness, not the precision instrument: at σ = 0.021 pp it cannot resolve the fourth decimal. Because every input derives from the pinned master seed, npm run simulate replays this exact run; a different seed would land elsewhere, typically within a few multiples of the ±0.0210% standard error — which is why the published figure is the exact solve and the simulation is the check. Step 17 re-draws the first 1,000 rounds from the master seed and requires them to reproduce the first convergence checkpoint exactly; the full trace is in outputs/simulation-results.json (baseGame.convergence) and is plotted on this page’s chart against the exact figure.
const exactTD = solveExact({ kind: 'finite', decks: 8 }, 'TD');const exactEdge = exactTD.edgePerInitialBet;const exactPerTotal = exactTD.edgePerTotalWagered; const rtpInitial = sim.baseGame?.rtpInitial ?? sim.baseGame?.rtp ?? sim.rtp;const rtpPerTotal = sim.baseGame?.rtpPerTotalWagered;const rounds = sim.baseGame?.rounds ?? sim.rounds;// …const roundsPinned = Number(rounds) === SIM_BASE_ROUNDS;const se = SIM_SD_PER_ROUND / Math.sqrt(SIM_BASE_ROUNDS); // pinned; the committed run's measured SD is 1.14662130892236 (baseGame.sdPerRound), rounded upconst TOL = 3 * se; // Monte-Carlo error only const simEdge = rtpInitial != null ? 1 - rtpInitial : NaN;const dev = Math.abs(simEdge - exactEdge);// …const sePerTotal = se / exactTD.avgWager;const TOL_PER_TOTAL = 3 * sePerTotal;const devPerTotal = rtpPerTotal != null ? Math.abs((1 - rtpPerTotal) - exactPerTotal) : NaN;const consistent = roundsPinned && rtpInitial != null && dev <= TOL && (rtpPerTotal == null || devPerTotal <= TOL_PER_TOTAL);// …const liftRecomputed = (exactTD.rtpPerInitialBet - computeOptimalRTP('stand', true).rtp) * 100;const liftArtifact = Number(sim.cardRemovalLiftPP);const liftOk = Number.isFinite(liftArtifact) && Math.abs(liftArtifact - liftRecomputed) < 1e-9;// …const status17 = (!liftOk || !roundsPinned || !prefixOk) ? 'FAIL' : (consistent ? 'PASS' : 'FLAG');4.6Real-Shoe Consistency Pass (1M Rounds, Step 18)
A second pass runs 1,000,000 rounds through the actual blackjackShoe algorithm — HMAC-SHA256 Fisher-Yates over all 416 cards — under a seed pair derived from the master seed, and scores the RTP under the same strategy: 99.4977% ± 0.1150% — −0.13σ from the exact figure. It is a consistency check with wide Monte Carlo error, not an RTP method: it asks whether the real shuffle algorithm returns what a fair shuffle returns, and finds no sign that it does not. Step 18 re-derives every Pass 1 statistic from the artifact’s 100 census rows and re-deals the first and last row from the seed pair. Whether the casino’s own seed stream looks selected is a separate question, answered by Pass 2 (S4.11).
// RTP through the real RNG against the engine, at a stated σ from the PINNED round count.const rtpSE1 = SIM_SD_PER_ROUND / Math.sqrt(SIM_PASS1_ROUNDS);const rtpZ = st ? (st.rtp - exactTD.rtpPerInitialBet) / rtpSE1 : NaN;const rtpOk = Number.isFinite(rtpZ) && Math.abs(rtpZ) <= 3;const pRe = st ? st.firstCardPValue : NaN;const uniformOk = st !== null && st.firstCardDf === FIRST_CARD_DF && pRe >= 0.01;const serialOk = st !== null && Math.abs(st.serialR1Z) < 3 && st.serialRunsPValue >= 0.01; // A statistic outside its threshold is a FLAG (a finding about the RNG); an artifact that// contradicts a pin, its own rows or the re-dealt rows is a FAIL (a finding about the artifact).const contradiction18 = !dfOk || !roundsOk || !rowNoncesOk || !!rowsProblem || !seedPairDerived || scalarBad.length > 0 || replayBad.length > 0 || replayed !== PASS1_REPLAYED_ROWS.length || st === null;const ok18 = uniformOk && serialOk && rtpOk && !contradiction18;4.7Wizard of Odds Cross-Validation
The Wizard of Odds reference is an external anchor. Its house-edge calculator was driven at the rule set our solve uses — 8 decks, S17, DAS, double on any first two cards, split to 2 hands, no hit split aces, peek/OBO, blackjack 3:2, no re-split and no surrender (captured 2026-08-23) — and its “basic strategy with continuous shuffler” result, the regime that matches LIQD’s reshuffle-every-hand deal under a total-dependent table, is 0.48768% per initial bet. The audit’s exact 0.4876748% sits within one unit of that figure’s last published digit (delta −5.2 × 10⁻⁶ pp). That checks the engine lands on the published figure; it does not certify the published fifth decimal, since Wizard of Odds’ continuous-shuffler figures resolve to about 10⁻⁴ pp — at six decks the engine reads 0.459936% against a published 0.45999%. The comparison is a test that can fail (tests/blackjack/wooAnchorTests.ts) and is recorded in outputs/exact-rtp.json under wooAnchor.
const WOO_BASIC_CSM_EDGE = 0.0048768; // 0.48768% per initial bet, published at 5 dpconst ONE_ULP = 1e-7; // resolution of the published figure, in fraction describe('blackjack: published external anchor (Wizard of Odds, 8-deck LIQD rule set)', () => { it('exact 8-deck TD edge matches the published 0.48768% at published precision', function () { this.timeout(300_000); const td = solveExact({ kind: 'finite', decks: 8 }, 'TD'); const delta = td.edgePerInitialBet - WOO_BASIC_CSM_EDGE; assert.ok( Math.abs(delta) <= ONE_ULP, `exact 8-deck TD edge ${(td.edgePerInitialBet * 100).toFixed(6)}% vs published ` + `0.48768% — delta ${(delta * 100).toExponential(3)} pp exceeds one ulp of the ` + 'published figure. Either the engine regressed or the anchor is stale.', ); });4.8Deck-Count Sensitivity
Run on a single deck (same rules, same 8-deck table), the exact solve gives a player-favourable edge of −0.083726% per initial bet (finite1TD) — a 0.571 pp swing. The Wizard of Odds calculator's own 8-deck → 1-deck delta at this rule set is 0.602 pp, leaving a ≈0.031 pp difference against the published single-deck figure. The 8-deck headline does not depend on it. LIQD deals 8 decks; the 1-deck figure is a sensitivity reference only, not an RTP of any offered game.
"finite1TD": { "model": { "kind": "finite", "decks": 1 }, "strategy": "TD", … "evPerInitialBet": 0.0008372608547358003, "rtpPerInitialBet": 1.0008372608547358, "edgePerInitialBet": -0.0008372608547358003, "avgWager": 1.1200529442546212, …}4.9Anti-Circularity Proof — Exact RTP
Anti-circularity means no scored figure is checked against a copy of itself or against the operator’s own number. The exact solver takes only the rules and the strategy table — it reads no dataset and no operator figure. The simulation passes play our own round engine: Passes 0 and 1 on seeds derived from the pinned master seed, Pass 2 on the revealed casino seeds. And the loader (src/loader.ts) deletes the four capture-side recomputation fields (localSequence, verified, commitVerified, chainLinkOk) before any step runs; tests/blackjack/antiCircularityTests.ts asserts against the real dataset that they are gone, and a source scan — a lint, not a proof — fails the build early if a verification source names one. The simulation artifact, the convergence chart, the exact-RTP artifact and the experiment artifacts are hash-pinned in src/config.ts and reconciled in scored Step 25 — so a silently swapped artifact cannot pass as the published one.
/** Fields written by the capture rig from its own recomputation — never evidence. */const CAPTURE_SIDE_FIELDS = ['localSequence', 'verified', 'commitVerified', 'chainLinkOk'];// …describe('blackjack: anti-circularity — no capture-side field feeds a scored step', () => { const sources = verificationSources();// …for (const field of CAPTURE_SIDE_FIELDS) { it(`no verification source reads \`.${field}\``, () => { const offenders: string[] = []; for (const file of sources) { const raw = fs.readFileSync(file, 'utf8'); const code = codeOnly(raw); // Indexed access `b['localSequence']` must be matched on the comment-stripped-but- // string-KEPT source: codeOnly() blanks string literals, which would turn // `b['verified']` into `b['']` and hide the very access this guard exists to catch. const withStrings = stripComments(raw); // property access (`b.localSequence`) and destructuring (`const { localSequence } = bet`) // run on the fully stripped source; indexed access runs on withStrings. const patterns: Array<[RegExp, string]> = [ [new RegExp(`\\.\\s*${field}\\b`), code], [new RegExp(`\\[\\s*['"\`]${field}['"\`]\\s*\\]`), withStrings], [new RegExp(`\\{[^}]*\\b${field}\\b[^}]*\\}\\s*=`), code], ]; if (patterns.some(([re, hay]) => re.test(hay))) offenders.push(path.relative(ROOT, file)); } assert.deepEqual(offenders, [], `capture-side field \`${field}\` read by: ${offenders.join(', ')}`); });}// …// ── The structural layer. This is the one that makes the guarantee true. ────────────────it('loader.ts declares exactly the four capture-side fields as strippable', () => { assert.deepEqual([...STRIPPED].sort(), [...CAPTURE_SIDE_FIELDS].sort(), 'src/loader.ts CAPTURE_SIDE_FIELDS has drifted from the list this guard scans for');}); it('loadDataset() returns bets and seeds with NO capture-side field present at all', () => { // Not "no source reads them" — literally not there. A computed key, a bracket access, a // destructure, JSON.stringify: none of them can recover a deleted property. const ds = loadDataset(); assert.ok(ds.bets.length > 0 && ds.seeds.length > 0, 'dataset loaded non-empty'); const offenders: string[] = []; for (const field of CAPTURE_SIDE_FIELDS) { const nBets = ds.bets.filter((b) => Object.prototype.hasOwnProperty.call(b, field)).length; const nSeeds = ds.seeds.filter((s) => Object.prototype.hasOwnProperty.call(s, field)).length; if (nBets || nSeeds) offenders.push(`${field}: ${nBets} bet(s), ${nSeeds} seed(s)`); } assert.deepEqual(offenders, [], `loadDataset() left capture-side fields in place — ${offenders.join('; ')}`);});// …});4.10Exchange-Rate Field — No FX Haircut Observed (Step 21)
The capture's side-bet and insurance records carry an exchange-rate field that could, in a dishonest implementation, quietly haircut every credit. Step 21 checks all 12,046 records carrying it: exactly 1.00 everywhere, no main-game hand carries the field, and every fiat amount is zero; the credits themselves reconcile at face value from the cards (Steps 8, 10, 15 and 16), with no exchange-rate factor in the rebuild. At the capture’s rate of exactly 1.00, every credit is paid at full value; other rates fall outside this USDC capture.
const fxDist = new Map<string, number>();let fxRecords = 0, nonzeroFiat = 0, mainHandsWithFx = 0;for (const b of ctx.bets) { for (const h of b.playerHands) if ('exchangeRate' in h) mainHandsWithFx++; for (const key of ['perfectPair', 'twentyOnePlusThree', 'insurance'] as const) { const sb = b.sideBets[key] as Record<string, unknown> | null; if (sb && sb.exchangeRate != null) { fxRecords++; const r = String(sb.exchangeRate); fxDist.set(r, (fxDist.get(r) ?? 0) + 1); if (Number(sb.fiatBetAmount ?? 0) !== 0 || Number(sb.fiatWinningAmount ?? 0) !== 0) nonzeroFiat++; } }}// …const distStr = [...fxDist.entries()].sort((a, b) => b[1] - a[1]).map(([r, n]) => `${n}×${r}`).join(', ');const fxCoverageOk = fxRecords > 0;const fxIsDisplayOnly = fxCoverageOk && mainHandsWithFx === 0 && nonzeroFiat === 0;// …out.push(step(21, 'No FX haircut observed in this capture', fxIsDisplayOnly ? 'PASS' : 'FLAG', `exchangeRate distribution over ${fxRecords} side-bet records: ${distStr || '(none)'}; ${mainHandsWithFx} main-game hands carry it (expected 0); ${nonzeroFiat} nonzero fiat amounts (expected 0). ` +4.11Cherry-Pick Screen — Pass 2 (Step 19)
Could the casino have chosen server seeds that produce worse outcomes for players? Pass 2 takes each of the 120 revealed casino seeds, extends it to 10,000 nonces under its epoch’s client seed, and plays the declared basic strategy through 200 windows of 50 rounds. The primary statistic is money, fixed in src/config.ts before it was first computed: each seed’s served window (nonces 0–49, the hands that were dealt, replayed under the declared strategy) is ranked against its own 199 later windows, and a seed is flagged at p ≤ 0.01; the screen raises a flag when more than 6 seeds are flagged (the binomial upper tail at 0.001), or when the pooled served-window net falls below z = −3.09. Result: 1 of 120 seeds flagged, and a pooled net of −95.5 initial bets over 6,000 served rounds against an exact expectation of −29.26 — z = −0.746. Pass 2 plays split hands main-first; replayed split-first, as the casino deals (S3.4), the same shoes give −112.5 and z = −0.94 (replay/replay-served-split-order.py) — neither near the gate. A first-card rank screen rides along as a secondary check: 7 flags against 5.7 expected by chance (P = 0.3444).
Measured power: against a 1% / 2% / 5% / 10% shortfall in served-window RTP, the pooled rule detects selection with probability 0.8% / 4.1% / 61.3% / 100%. npm run attack measures it against a real grind: keeping the most house-favourable of 50 candidate server seeds per epoch takes the served windows from 98.5950% to 68.5331% RTP per total wagered, and the money rule flags 34 of 120 seeds with pooled z = −23.56 — detected. 110 of the 120 capture epochs used a fresh client seed set after the commitment, which rules selection out for those epochs. The working defence is the player’s: set a fresh client seed after the commitment is published.
const familyGate = binomialTailThreshold(EXPECTED_SEEDS, PASS2_MONEY_ALPHA, PASS2_FAMILY_ALPHA);// …const moments = solveMoments({ kind: 'finite', decks: 8 });const exactSd = moments.sd;// …// The money statistic, RE-DERIVED from the row's windows.const mp = moneyP(windowNet2);const mf = mp <= PASS2_MONEY_ALPHA;if (mf) moneyFlagsRe++;// …// Pooled served-window net against the EXACT 8-deck mean (exactTD, recomputed in Step 17 above).const servedRounds = EXPECTED_SEEDS * PASS2_EARLY_NONCES;const pooledNet = pooledNet2 / 2;const pooledExpected = -exactTD.edgePerInitialBet * servedRounds;const pooledSD = exactSd * Math.sqrt(servedRounds);const pooledZ = (pooledNet - pooledExpected) / pooledSD;// …const power = PASS2_POWER_SHORTFALLS.map((d) => ({ d, pw: normalCDF((d * servedRounds) / pooledSD - PASS2_POOLED_Z) }));// …const moneyFlagged = moneyFlagsRe > familyGate || pooledZ < -PASS2_POOLED_Z;// …const ok19 = !contradiction19 && !moneyFlagged && survivalRecomputed >= 0.01;4.12Side-Bet EV — Perfect Pairs & 21+3
The side bets carry their own house edges, computed exactly from the verified paytables and 8-deck combinatorics. Perfect Pairs (25 / 13 / 6 to 1) carries a 2.1687% edge (9/415) — LIQD’s coloured pair pays 13:1, one pip above the common 12:1, a player-favourable deviation confirmed on 123 live coloured-pair wins; at 12:1 the edge would be 4.096%. 21+3 (100 / 40 / 30 / 10 / 5 to 1) carries a 3.7039% edge (4596/124085). Both are pure-luck wagers resolved at the deal; the capture’s 11,600 settlements match these paytables exactly (S3.8). The package records LIQD’s game page, read on 1 October 2026, showing the Perfect Pairs edge as 2.17% and the 21+3 edge as 3.63% (that panel is not in the archived page read). 3.63% is not the exact edge of the 100 / 40 / 30 / 10 / 5 paytable the page states — no whole number of decks gives it — so the audit keeps 3.7039%, which the 5,800 captured 21+3 settlements follow (Step 16). The side bets are separate wagers outside the RTP claim table (S4.2).
| Perfect Pairs — 2 cards, C(416, 2) = 86,320 deals | Combinations | Pays |
|---|---|---|
| Perfect pair | 1,456 | 25:1 |
| Coloured pair | 1,664 | 13:1 |
| Mixed pair | 3,328 | 6:1 |
| No pair | 79,872 | lose |
| 21+3 — 3 cards, C(416, 3) = 11,912,160 deals | Combinations | Pays |
|---|---|---|
| Suited three of a kind | 2,912 | 100:1 |
| Straight flush | 24,576 | 40:1 |
| Three of a kind | 61,568 | 30:1 |
| Straight | 368,640 | 10:1 |
| Flush | 700,928 | 5:1 |
| No combination | 10,753,536 | lose |
/** Perfect Pairs on two cards. */export function evaluatePerfectPairs(c1: string, c2: string): SideResult { // LIQD labels a losing Perfect Pairs bet `NO_PAIR`, but a losing 21+3 bet `NO_MATCH`. // The two side bets genuinely use different vocabulary; the category comparison in // Step 15 is against LIQD's own `gameResult`, so this string must match exactly. if (rankOf(c1) !== rankOf(c2)) return { category: 'NO_PAIR', payout: 0 }; if (suitOf(c1) === suitOf(c2)) return { category: 'PERFECT_PAIR', payout: PERFECT_PAIRS.PERFECT_PAIR }; if (isRed(c1) === isRed(c2)) return { category: 'COLORED_PAIR', payout: PERFECT_PAIRS.COLORED_PAIR }; return { category: 'MIXED_PAIR', payout: PERFECT_PAIRS.MIXED_PAIR };} function isThreeStraight(a: number, b: number, c: number): boolean { const consec = (rs: number[]): boolean => { const s = [...rs].sort((x, y) => x - y); return s[0] + 1 === s[1] && s[1] + 1 === s[2]; }; if (consec([a, b, c])) return true; // Ace-high: A(1) counts as 14 for Q-K-A return consec([a, b, c].map((r) => (r === 1 ? 14 : r)));} /** 21+3 on the player's two cards + the dealer upcard. */export function evaluate21Plus3(c1: string, c2: string, dealerUp: string): SideResult { const cards = [c1, c2, dealerUp]; const ranks = cards.map(rankOf); const suits = cards.map(suitOf); const flush = suits[0] === suits[1] && suits[1] === suits[2]; const trips = ranks[0] === ranks[1] && ranks[1] === ranks[2]; const straight = isThreeStraight(ranks[0], ranks[1], ranks[2]); if (trips && flush) return { category: 'SUITED_THREE_OF_A_KIND', payout: TWENTY_ONE_PLUS_THREE.SUITED_THREE_OF_A_KIND }; if (straight && flush) return { category: 'STRAIGHT_FLUSH', payout: TWENTY_ONE_PLUS_THREE.STRAIGHT_FLUSH }; if (trips) return { category: 'THREE_OF_A_KIND', payout: TWENTY_ONE_PLUS_THREE.THREE_OF_A_KIND }; if (straight) return { category: 'STRAIGHT', payout: TWENTY_ONE_PLUS_THREE.STRAIGHT }; if (flush) return { category: 'FLUSH', payout: TWENTY_ONE_PLUS_THREE.FLUSH }; return { category: 'NO_MATCH', payout: 0 };}The edges follow by arithmetic on those tables. Perfect Pairs: (1,456 × 25 + 1,664 × 13 + 3,328 × 6 − 79,872) / 86,320 = −1,872 / 86,320 = −2.1687%. 21+3: (2,912 × 100 + 24,576 × 40 + 61,568 × 30 + 368,640 × 10 + 700,928 × 5 − 10,753,536) / 11,912,160 = −441,216 / 11,912,160 = −3.7039%. Both are per unit wagered on the side bet itself; neither touches the main-bet figure.
4.13Payout Conventions — 3:2, Doubles, Insurance (Steps 8, 9, 10)
Every settlement path was reconciled. Naturals pay 3:2 — all 243 winning naturals returned exactly 2.5× (Step 9); a natural against a dealer natural pushes (10 such hands). Doubles scale the hand’s stake and return 2× — the capture’s per-hand records store the base portion, and the doubled stake settles as a separate bet with the same outcome; every one of the 1,283 doubled rounds reconciles under the round formula credit = (Σ hand wins) × m + Σ side wins with m = 2 (Step 8b). Insurance pays 2:1 (3× gross) — 145 / 145 winning insurance bets exact (Step 10). The round total is the capture script’s own sum of the casino’s credit records, so this identity shows each double bet was credited like its hand, and the per-hand reconstruction from the cards tests every credit; every doubled split round in the capture doubled both hands. Money is compared as exact integer equality in the capture’s 1e-8 accounting units — a one-unit discrepancy fails, and all 42,894 recorded amounts sit on that grid (Step 8).
/** * Expected per-hand return (incl. stake) in integer settlement units, derived from cards + rules. * Returns null when the rule-derived amount is NOT representable on the grid — only reachable for * the 3:2 natural on an odd unit stake, which would itself be a finding, so it is surfaced rather * than rounded away. */function expectedReturnUnits(cards: string[], dealer: string[], stakeUnits: number, isSplitHand: boolean): number | null { const pt = handValue(cards).total; if (pt > 21) return 0; // player bust const dt = handValue(dealer).total; const pBJ = (!isSplitHand || RULES.splitTwentyOneIsNatural) && isBlackjack(cards); const dBJ = dealerHasBJ(dealer); if (pBJ && dBJ) return stakeUnits; // push if (pBJ) return ((BJ_NUM + BJ_DEN) * stakeUnits) % BJ_DEN === 0 ? ((BJ_NUM + BJ_DEN) * stakeUnits) / BJ_DEN : null; // 3:2 if (dBJ) return 0; // dealer natural blackjack beats a non-natural player if (dt > 21) return 2 * stakeUnits; // dealer bust if (pt > dt) return 2 * stakeUnits; if (pt === dt) return stakeUnits; // push return 0;}And the round-credit formula every one of the 6,000 rounds satisfies:
// ── Step 8b: Money-path reconciliation (top-level credit == Σ components) ────// The per-hand `winningAmount` records only the BASE portion of a doubled hand;// the doubled stake settles as a separate bet with the same outcome. So the total// credited on a bet must satisfy:// winningAmount == (Σ per-hand winningAmount) × m + Σ side-bet winningAmount// where m = 2 when every hand was doubled, 1 when none was. A split in which only// one of the two hands doubled cannot be attributed to a specific hand from the flat// `actions` list, so those are bounded rather than solved, and reported — silence on// an unreconciled money path is exactly what this check exists to prevent.//// WHAT THIS IDENTITY CAN SEE. The round's top-level `winningAmount` is not a field LIQD returned:// the capture script computed it as its own sum of the casino's credit records (hands, side bets,// insurance and the double-bet records mainDoubleBet / splitDoubleBet, which it did not store).// On a round with no double the identity therefore compares that sum with its own parts and// carries no information. It carries information on the doubled rounds only (1,283 here): there it// shows that each double bet was credited like its hand. It cannot detect a skim by the casino —// a short credit on any record would be summed into the total as well. The per-hand reconstruction// above (each credit derived from the cards) is what tests the casino's credits.let recOk = 0;let recBad = 0;let recBounded = 0;let recBoundBad = 0;const recFails: string[] = [];for (const b of bets) { const nHands = b.playerHands.length; const nDoubles = b.actions.filter((a) => a === 'double').length; // All four quantities are exact integers of 1e-8, so the identity is checked as integer // equality — no epsilon on either side of it. const base = b.playerHands.reduce((s, h) => s + unitsOr0(h.winningAmount), 0); const side = sideBetReturnUnits(b); const top = unitsOf(b.winningAmount); const m = nDoubles === 0 ? 1 : nDoubles === nHands ? 2 : null;// … if (m === null) { // Partial double across a split: credit must lie between the undoubled and // fully-doubled totals. Integer bounds — inclusive, and not widened by any epsilon. recBounded++; if (top < base + side || top > 2 * base + side) { recBoundBad++; if (recFails.length < 5) recFails.push(`nonce ${b.nonce} (epoch ${b.epoch}): ${asMoney(top)} outside [${asMoney(base + side)}, ${asMoney(2 * base + side)}]`); } continue; } if (top === base * m + side) recOk++; else { recBad++; if (recFails.length < 5) recFails.push(`nonce ${b.nonce} (epoch ${b.epoch}): credited ${asMoney(top)} != ${asMoney(base * m + side)} (base ${asMoney(base)} ×${m} + side ${asMoney(side)}) — off by ${top - (base * m + side)} settlement unit(s) of 1e-8`); }}4.14Informational Items (Not Scored)
Three observations ride along without gating the verdict. Money-path reconciliation: across all 6,000 rounds, 4,152.60 wagered (+86.65 insurance) returned 4,155.35 — 98.02% including insurance, 98.47% excluding it. This is bookkeeping, not an RTP estimate: blackjack’s per-hand SD is about 1.14, so over 6,000 hands the error is roughly ±1.5 pp equal-weighted and ±6 pp money-weighted, and phases B, C and F play off basic strategy on purpose — which is why the headline is the exact solve. Insurance: the casino booked insurance on every dealer-Ace offer the capture made (446 bets, 86.65 wagered, 66.30 returned) — it lost money, as a −EV wager should. Natural frequency: 253 / 6,000 = 4.22% against the ≈4.75% 8-deck expectation — about 1.9σ low, within normal range for one capture and orthogonal to RTP (naturals were paid correctly wherever they occurred).
4.15Worked Example — RTP & Payout Reconciliation
Real bet from Phase A — epoch 2, nonce 25, the busiest money path in one round. Verified from blackjack-6000hands.json: a 6♣/6♥ split, both hands doubled after the split, plus both side bets landing. Stake 0.10 per hand line:
| Component | Detail | Return |
|---|---|---|
| Main hand | 6♣ → double → 18 vs dealer bust 23 | 0.20 unscaled |
| Split hand | 6♥ → double → 17 vs dealer bust 23 | 0.20 unscaled |
| Double multiplier | round-level m = 2 (both hands doubled) | ×2 |
| Perfect Pairs | 6♣ 6♥ — mixed pair, 7× gross × 0.10 | 0.70 |
| 21+3 | 6,6 + dealer 6 — three of a kind, 31× gross × 0.10 | 3.10 |
| Round credit | (0.20 + 0.20) × 2 + 0.70 + 3.10 | 4.60 ✓ recorded |
npm run verify re-derives this round under Step 8; the S6 scripts reproduce its shoe from the seeds.Two supporting one-liners from the same capture: epoch 0 / nonce 12 — A♥ K♣ natural, 0.10 → 0.25, exactly 3:2; epoch 2 / nonce 19 — dealer A♥ 10♦ natural, the 0.05 insurance bet returns 0.15, exactly 2:1. Every number recomputed, every number matches.
Technical Evidence — RTP & Payout Logic5 sections
4.16Evidence Coverage Summary11 checks
| Verification Area | Coverage | Result |
|---|---|---|
| Exact basic-strategy solve + anchors (Step 17) | Exact 8-deck RTP 99.512325% · rational oracle at 1e-12 · Wizard of Odds 0.48768% within one unit of its last digit | Pass |
| RTP claim rule (Step 20) | LIQD’s 99.52% vs exact 99.512325% — gap −0.007675 pp, inside ±0.1 pp | Pass |
| 30M fair-shuffle simulation (Step 17) | 30,000,000 rounds · SE ±0.0210% (σ from the pinned round count) · 0.51σ (3σ gate) | Pass |
| Real-shoe pass (Step 18) | 1,000,000 rounds · 99.4977%, −0.13σ | Pass |
| Per-hand payout math (Steps 8, 9, 10) | 6,401 hands · 243 naturals · 446 insurance | Pass |
| House-edge reconciliation (Step 20) | Game-page header vs the exact solve, both conventions; dataset swept for a served edge or RTP field (none) | Pass |
| No FX haircut observed (Step 21) | Exchange-rate field = 1.00 on all 12,046 records carrying it | Pass |
| Cherry-pick screen (Step 19) | 120 seeds × 10,000 nonces — money rule 1 / 120 (gate 6), pooled z −0.746; rank screen 7 vs 5.7 | Pass |
Seed-grind power experiment (npm run attack) | 50 candidate seeds × 120 epochs against the Step 19 money rule — 34 / 120 flagged, pooled z −23.56 | Simulated grind detected |
| Side-bet EV (Steps 15, 16, 20) | Exact combinatorics vs verified paytables | Pass |
| Stake brackets (Steps 22, 31) | $0.10 × 5,800 · $10.00 × 200 | Pass |
4.17Code References12 source files
| File | Purpose |
|---|---|
| src/exact-play.ts | Exact finite-shoe EV solver (TD basic strategy) — the 99.512325% figure, emitted to outputs/exact-rtp.json (npm run rtp, byte-identical every run) |
| anchor/oracle.py + tests/blackjack/exactOracleTests.ts / wooAnchorTests.ts | The two anchors: the exact-rational tiny-shoe oracle (three shoes, 1e-12) and the Wizard of Odds gate at published precision |
| src/rtp-claim.ts · src/rtp-settings.ts | The RTP claim rule (RTP-CLAIM 1.2.0, enforced) and the claim it holds — 99.52%, the RTP implied by the 0.48% game-page header |
| src/replay.ts · src/simulate.ts | Pass 0 (30M fair-shuffle rounds), Pass 1 (1M real-shoe rounds) and Pass 2 (the 120 revealed seeds × 10,000 nonces); Passes 0 and 1 and the Pass-2 bootstrap derive every input from the pinned master seed |
| src/sidebets.ts | Independent evaluators for both side bets against the paytable constants in src/config.ts (Steps 15–16); the exact edges are enumerated in src/sidebet-edges.ts, tested as exact rationals (9/415 and 4596/124085) in tests/blackjack/sideBetEdgeTests.ts, and recorded in outputs/report-figures.json |
| tests/steps/payouts.ts | Steps 8–10: per-hand payout math from cards + rules, 3:2 naturals, insurance 2:1, round-credit reconciliation — compared as exact integers in 1e-8 settlement units (src/money.ts) |
| tests/steps/houseedge.ts | Steps 20–21: house-edge reconciliation against the exact solve; no FX haircut observed |
| tests/blackjack/antiCircularityTests.ts | src/loader.ts deletes the capture-side recomputation fields before any step runs; the test asserts they are gone and fails the build if a verification source references one |
| src/config.ts | The ten run-time artifact pins (dataset, simulation, convergence chart, exact RTP, simulation provenance, bootstrap nulls, seed-grind experiment, branch coverage, both replay reports) plus the population and simulation-shape pins (120 epochs × 50, 30M / 1M rounds, σ = 1.15, df 12, α) |
| src/round-engine.ts + tests/blackjack/roundEngineTests.ts | The one round engine every simulation pass plays, bound to the exact solve within 5σ (0.4065864 pp over 2,000,000 rounds); mutation R37 turns it red |
| src/cherry-pick-attack.ts | The seed-grind experiment behind S4.11 (npm run attack); deterministic, byte-for-byte reproducible |
| src/report-figures.ts | Regenerates outputs/report-figures.json — the accounting, insurance, natural-frequency and side-bet figures quoted on this page; Step 25 re-derives it field for field |
4.18Datasets UsedSource artifacts
| Property | Value |
|---|---|
| Simulation artifact | outputs/simulation-results.json (≈506 KB) — all three passes, their retained replay inputs and the convergence trace behind this page’s chart. The exact figures are not embedded here: exactFiguresSource points at outputs/exact-rtp.json |
| Simulation SHA-256 | 26ab1574ced2a1e59ca653a582951dac304ca62ea621e96b2b6de1e08a193567 |
| Exact-RTP artifact | outputs/exact-rtp.json — the exact figures of record: the finite-8 TD figure, the finite-1 deck sensitivity, the Wizard of Odds anchor with its delta, and the per-round moments Step 19 uses; npm run rtp regenerates it byte-identically |
| Exact-RTP SHA-256 | f89b1920086bf0af17dbf5f30350bfef259a6fa0f934631a7ffa41e34cec4611 |
| RTP claim table | outputs/rtp-claim-table.json — LIQD’s 99.52% against the one setting, with exact RTP, gap and outcome · SHA-256 9e447e3b… |
| Convergence chart | outputs/rtp-convergence.html (≈6 KB) — a static inline-SVG rendering of the same baseGame.convergence trace; this page re-renders that trace in the site's house chart style |
| Seed-selection experiment | outputs/cherry-pick-attack.json (≈76 KB) — the S4.11 power experiment; deterministic (npm run attack reproduces it byte for byte), SHA-256 44d45d84a8cd7d54dee3fca146be7e146f4e1fa65385cd34d33fa611ee9d1e78 |
| Report figures | outputs/report-figures.json (≈3 KB) — the derived accounting and side-bet figures quoted in S4, re-derived field for field by Step 25 rather than hash-pinned |
Money fields used: betAmount, winningAmount (hand and round), side-bet records, the insurance channel, and the side-bet-record exchange-rate field. All figures settle in USDC.
4.19Verified Invariants12 invariants
| Invariant | Result |
|---|---|
| Winning naturals return exactly 2.5× — 243 / 243 | Pass |
| Pushes return exactly 1× (recorded under the platform’s binary won label) | Pass |
| Round credit = (Σ hand wins) × m + Σ side wins on all 6,000 rounds | Pass |
| Insurance wins return exactly 3× the insurance stake — 145 / 145 | Pass |
| Exchange-rate field = 1.00 on all 12,046 records carrying it; no FX haircut observable | Pass |
| LIQD’s published 99.52% holds under the RTP claim rule — gap −0.007675 pp, inside ±0.1 pp | Pass |
| 30M simulation consistent with the exact figure at 0.51σ, per initial bet and per total wagered (3σ gate) | Pass |
| Pass 2 money rule: 1 / 120 seeds flagged (gate 6), pooled z = −0.746 (gate −3.09) | Pass |
| Side-bet paytables imply 2.1687% (PP) and 3.7039% (21+3) edges — exact combinatorics | Pass |
| Solver reproduces the exact-rational oracle on all three tiny shoes at 1e-12; Wizard of Odds anchor matched within one unit of its last digit (both gates can fail) | Pass |
npm run rtp regenerates outputs/exact-rtp.json byte-identically | Pass |
| Round engine reproduces the exact solve within 5σ over 2,000,000 rounds (0.4065864 pp window) | Pass |
4.20Reproduction InstructionsRun locally
Clone the repository, install dependencies, and run the exact solve and the verification suite:
git clone https://github.com/ProvablyFair-org/liqd-blackjack.gitcd liqd-blackjack && git checkout v1.0.1npm cinpm run rtp # exact 8-deck basic-strategy solve → outputs/exact-rtp.json (byte-identical)npm run verify # Steps 8–10, 15–22 and 31 cover S4, with the RTP claim rulecat outputs/rtp-claim-table.jsonS4-related steps:
[PASS] Step 8 — Per-Hand Payout (reconstructed from cards)
[PASS] Step 9 — Blackjack Pays 3:2
[PASS] Step 10 — Insurance Pays 2:1 iff Dealer Blackjack
[PASS] Step 15 — Perfect Pairs Side Bet
[PASS] Step 16 — 21+3 Side Bet
[PASS] Step 17 — Basic-strategy RTP vs exact 8-deck solver
[PASS] Step 18 — Simulation Pass 1 — first-card uniformity, serial independence, RTP vs the exact solve
[PASS] Step 19 — Simulation Pass 2 — cherry-pick detection (casino seeds)
[PASS] Step 20 — House-edge reconciliation (nominal config vs actual)
[PASS] Step 21 — No FX haircut observed in this capture
[PASS] Step 22 — Bet-Size Invariance
[PASS] Step 31 — Stake Bracket Bounds
rtp-claim: ENFORCED — PASS
rtp-claim: claim flat 99.520000% — source: LIQD Blackjack: the game page liqd.com/originals/blackjack shows "Released: June 2026 | Edge: 0.48%" = RTP 99.52 %, the figure held here, …
rtp-claim: band ±0.10 pp · 1 settings · true min 99.512325% (main game, 8-deck basic strategy (TD), per initial bet) · true max 99.512325% (main game, 8-deck basic strategy (TD), per initial bet)
rtp-claim: house side beyond band: 0 · player-favourable beyond band: 0 · worst main game, 8-deck basic strategy (TD), per initial bet at -0.007675 pp
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 9e447e3bfb9514d6… matches the pinFairness Integrity Testing
Does the implementation maintain fairness under non-standard conditions?
Sections 1–4 check the game’s maths against its seeds, its rules and its published figures; Section 5 tests whether the live implementation keeps its integrity under non-standard conditions. It covers 39 integrity tests run against LIQD’s running service across 12 categories: race conditions on in-round actions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, hidden-card disclosure, and the provably fair surface itself. 32 passed. Seven were remediated: a concurrency defect in the per-round action handler that let parallel doubles or splits attach extra stakes to one hand (Critical and High), retry and delayed-request handling for in-round actions (Medium), and two input-hygiene issues (Low); LIQD fixed each and we re-verified them on the current build. None of these findings changed the shuffle or the order cards are dealt in.
What we verified
- Race conditions — can concurrent actions attach more than one stake to a hand, a delayed request change the next round, or a seed rotation racing a live hand reveal its seed?
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed handling — can unusual client seeds or server seed fields be overridden, or a live round’s seed changed?
- Outcome replay — can a completed hand be replayed, or a retried action apply twice?
- Cross-player isolation — can one player act on another’s round or session?
- Payout tampering — can payout, outcome, double or side-bet amounts be injected?
- Parameter limits — can invalid bet amounts or currency identifiers be submitted?
What this means for you
- None of these tests changed the shuffle or the order cards are dealt in
- Only you can act on your hand — other accounts’ requests were rejected
- Each hand is dealt from the committed seed pair and its own nonce, and its seed is locked while the hand is in play
- These defects — extra stakes on one hand under concurrent requests, repeated or delayed in-round actions, and loose input handling — were fixed by LIQD and re-verified
- The dealer’s hole card stays hidden: in every hand state, responses show only your cards and the dealer’s up-card
- Race conditions — can concurrent actions attach more than one stake to a hand, a delayed request change the next round, or a seed rotation racing a live hand reveal its seed?
- Nonce tampering — can the sequence be forced, replayed, or skipped?
- Seed handling — can unusual client seeds or server seed fields be overridden, or a live round’s seed changed?
- Outcome replay — can a completed hand be replayed, or a retried action apply twice?
- Cross-player isolation — can one player act on another’s round or session?
- Payout tampering — can payout, outcome, double or side-bet amounts be injected?
- Parameter limits — can invalid bet amounts or currency identifiers be submitted?
- None of these tests changed the shuffle or the order cards are dealt in
- Only you can act on your hand — other accounts’ requests were rejected
- Each hand is dealt from the committed seed pair and its own nonce, and its seed is locked while the hand is in play
- These defects — extra stakes on one hand under concurrent requests, repeated or delayed in-round actions, and loose input handling — were fixed by LIQD and re-verified
- The dealer’s hole card stays hidden: in every hand state, responses show only your cards and the dealer’s up-card
Nonce integrityPass
Seed integrityPass
Hidden-card disclosurePass
Round & player isolationPass
Payout integrityPass
Stacked stakes, retries and delayed actionsRemediated
Parameter limitsRemediated
39 integrity tests: 32 pass and 7 remediated (the action-handler concurrency defect, in-round retries and delayed requests, and two input-hygiene issues — all fixed by LIQD and re-verified on the current build). None of these findings changed the shuffle or the order cards are dealt in. 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 and invalid nonces are never honoured (NONCE-001 … NONCE-003); every step is exactly +1 (PF-002) |
| Seed integrity | Pass | Revealed seeds match their commitments and never repeat (ENTROPY-001, ENTROPY-002); a live round’s seed is locked (SEED-004), and rotation is refused while a round is in play or insurance is offered (RACE-006, RACE-008); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003) |
| Hidden-card disclosure | Pass | In every hand state — a double after a split included — responses show only the player’s cards and the dealer’s up-card, with no insurance timing signal (EXTRA-001); LIQD’s verifier reproduces the dealt cards (PF-003) |
| Round & player isolation | Pass | Another account’s actions on a live round rejected; post-logout reuse rejected; one unsettled round per account; settled rounds accept no further actions |
| Payout integrity | Pass | Injected payout, outcome, double and side-bet amounts never changed a settlement; payout arithmetic exact over consecutive rounds |
| Stacked stakes, retries and delayed actions | Remediated | Parallel doubles or splits could attach extra stakes to one hand (Critical, High); retried or delayed in-round actions could apply twice or reach the next round (Medium) — fixed by LIQD and re-verified on the current build (RACE-010, RACE-011, IDEMP-003, IDEMP-004, RACE-014) |
| Parameter limits | Remediated | Clearly invalid amounts were always refused; grey-area stakes and malformed currency identifiers were accepted or defaulted instead of refused (Low) — fixed by LIQD and re-verified (BET-001, TOKEN-001) |
Nonce integrityPass
Seed integrityPass
Hidden-card disclosurePass
Round & player isolationPass
Payout integrityPass
Stacked stakes, retries and delayed actionsRemediated
Parameter limitsRemediated
39 integrity tests: 32 pass and 7 remediated (the action-handler concurrency defect, in-round retries and delayed requests, and two input-hygiene issues — all fixed by LIQD and re-verified on the current build). None of these findings changed the shuffle or the order cards are dealt in. 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 hand 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 39 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 | 13 | Parallel round starts and in-round actions, delayed requests, and seed rotation racing a live hand |
| Input Validation | 2 | Grey-area and malformed stakes, and invalid currency identifiers |
| 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 | 3 | Client-supplied payout, outcome, double-down or side-bet amounts |
| Nonce Manipulation | 3 | Reused, skipped, zero, negative or oversized nonces supplied by the client |
| RNG / PF Verification | 4 | Provably fair fields, the nonce sequence, seed commitments across rotations, and LIQD’s verifier against the cards actually dealt |
| RNG / Information Leak | 1 | Disclosure of the dealer’s hole card, or of any hidden hand state, before settlement |
| Entropy / PF Verification | 1 | Server-seed randomness and uniqueness across rotations |
| Precision / Payout | 1 | Settlement arithmetic and wallet reconciliation over many rounds |
| Event Replay | 2 | Final actions replayed after settlement, and actions sent to a settled round |
| Adversarial Seeds | 4 | Unusual client seeds — length, character sets, Unicode forms, rapid changes — and rotation mid-round |
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: seven Remediated tests — the action-handler concurrency defect (graded Critical and High), two input-hygiene issues (Low), and in-round retry and delayed-request handling (Medium). All seven are re-verified on LIQD’s current build.
| 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 hand’s cards cannot be reproduced from disclosed inputs | Hard fail |
| A credited payout deviates from the rule set and the hand’s outcome | Hard fail |
| Nonce reuse or gaps that could conceal discarded outcomes | Hard fail |
39 tests · 32 pass · 7 remediated
Race Conditions13/13
LIQD-BJ-RACE-010CriticalRemediatedConcurrent double-down on one hand
- Control assessed
- A hand can be doubled once: one extra stake, and any further double sent at the same moment is refused.
- Testing approach
- Double-down requests sent in parallel on a single eligible hand; the settled hand and the wallet ledger inspected.
- Observed result
- Before the fix, several concurrent doubles could be accepted on the same hand. LIQD made per-round bet placement atomic; re-tested on the current build, parallel doubles no longer stack on one hand.
- Impact
- Critical · funds — before the fix, a hand could carry more stake than the rules allow, and winning stacked hands paid out in full. No committed outcome was affected. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-011HighRemediatedConcurrent splits on one hand
- Control assessed
- A pair can be split once, every debited stake is settled or refunded, and the round always reaches a final state.
- Testing approach
- Split requests sent in parallel on a single pair, then the round played to settlement; the ledger, settlement and seed rotation inspected.
- Observed result
- Before the fix, concurrent splits could debit extra split stakes that were neither settled nor refunded, leaving the round open and holding up seed rotation and verification on the account. After LIQD’s fix (atomic placement, with split and double settlement recorded), re-tested on the current build: no orphaned split stake, and every round reaches a final settle or refund.
- Impact
- High · funds and verification — no player profit was possible, but extra split stakes were debited and never settled or refunded, and the stuck round blocked seed rotation and verification for the whole account. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-014MediumRemediatedA delayed action landing on the next round
- Control assessed
- A request that belongs to a finished round cannot change the next one: no extra card, no added stake, no settlement, and no reply naming the new round.
- Testing approach
- Hit, stand, double and split requests held back until the next round had started, with and without the original round ID, plus a control naming the current round. Re-tested on 23 October 2026.
- Observed result
- Before the fix, delayed actions from one round altered the next in every case tried. On the re-test, stale round IDs were rejected in 8 of 8 cases and requests without a round ID in 4 of 4; the new round was unchanged in all 12, while the control on the current round succeeded.
- Impact
- Medium · action integrity — before the fix, a delayed request could change the following round. Fixed by LIQD and re-verified.
- Limitations
- The re-test covered hit, stand, double and split requests within a refreshed session; other request types and session flows were outside its scope.
LIQD-BJ-RACE-001PassParallel round starts
- Control assessed
- Only one round can start at a time, and only one stake is taken.
- Testing approach
- Round starts sent in parallel.
- Observed result
- Exactly one was accepted, and the balance fell by exactly one stake.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-002PassA second bet while a round is live
- Control assessed
- A second bet is refused while a round is still unresolved.
- Testing approach
- A second bet sent immediately after the first.
- Observed result
- The second bet was correctly rejected.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-003PassParallel hits on one round
- Control assessed
- Parallel hits resolve to one consistent hand and one round’s settlement.
- Testing approach
- Hit requests sent in parallel on one round.
- Observed result
- The hits resolved to one consistent hand that grew card by card, and the balance matched exactly one round.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-004PassParallel stands
- Control assessed
- A round settles once, however many stands arrive together.
- Testing approach
- Stand requests sent in parallel on one round.
- Observed result
- Only one was accepted; the balance matched one settlement, with no double credit.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-005PassStand and double at the same moment
- Control assessed
- Exactly one of two competing final actions is accepted.
- Testing approach
- A stand and a double sent together on a double-eligible hand.
- Observed result
- Exactly one action was accepted every time.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-006PassSeed reveal racing an unfinished round
- Control assessed
- A rotation cannot reveal the seed of a round still in play.
- Testing approach
- Seventy attempts to rotate the seed while a round was in play.
- Observed result
- The rotation never succeeded while a round was in play, so no live round’s seed was revealed.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-007PassParallel hits after a split
- Control assessed
- After a split, cards cannot cross between hands, and finished hands stay frozen.
- Testing approach
- Hit requests sent in parallel on split hands.
- Observed result
- No card was attributed to both hands, and finished hands stayed frozen.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-008PassSeed reveal while insurance is offered
- Control assessed
- No seed is revealed while an insurance offer is open.
- Testing approach
- Sixty-six attempts to rotate the seed during an open insurance offer.
- Observed result
- Every attempt was cleanly refused, so no seed was revealed while the offer was open.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-009PassA busting hit racing a stand
- Control assessed
- A busted hand never pays through a stand sent at the same moment.
- Testing approach
- A hit and a stand sent together, where the hit busts the hand.
- Observed result
- The stand never paid a win or a push on the busted hand.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-RACE-013PassStarting rounds from several sessions
- Control assessed
- An account holds at most one unsettled round; a new round starts only after the previous one settles; sessions and accounts stay isolated, and the wallet reconciles.
- Testing approach
- Seventeen iterations starting rounds from two sessions, at varied offsets around settlement.
- Observed result
- Every iteration held at most one unsettled round per account; at all 6 offsets the new round was created after settlement; the wallet and the round history matched in all 17 iterations.
- Impact
- None — the control held.
- Limitations
- The test covered funded play in one currency under the sessions it used; other stake, currency, balance and session conditions were outside it.
Input Validation2/2
LIQD-BJ-BET-001LowRemediatedGrey-area stake values
- Control assessed
- Clearly invalid stakes are refused, and zero or sub-precision amounts are either refused or handled as explicit free play.
- Testing approach
- Zero, null, sub-precision, negative, over-maximum and non-numeric stakes.
- Observed result
- Before the fix, zero, null and sub-precision amounts were accepted as a small or zero stake, while every clearly invalid amount was refused and no out-of-range stake was possible. After LIQD tightened validation, malformed and sub-precision amounts are rejected.
- Impact
- Low · input hygiene, with no effect on funds or fairness. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-TOKEN-001LowRemediatedInvalid currency identifiers
- Control assessed
- Invalid currency identifiers are refused; a missing one is refused or resolved to a documented default.
- Testing approach
- Bets carrying malformed currency identifiers, and one with none.
- Observed result
- Before the fix, two malformed values were silently booked to the account’s default currency; every other invalid value was refused, and no other currency could ever be chosen. After LIQD’s fix, invalid values are rejected; a bet with no currency resolves to the account’s own currency.
- Impact
- Low · input hygiene, with no cross-currency path at any point. Fixed by LIQD and re-verified.
- Limitations
- None specific to this test; the general scope is in 5.5.
Atomic Balance / Idempotency3/3
LIQD-BJ-IDEMP-003MediumRemediatedRetried hit after a client timeout
- Control assessed
- One intended hit applies once, however often it is retried — no extra card, and no unrecovered server error.
- Testing approach
- Hit requests retried with the same key after client timeouts of different lengths; the hand inspected after each. Re-tested on 23 October 2026.
- Observed result
- Before the fix, one intended hit could grow the hand by two cards. LIQD added round and idempotency keys; on the re-test, same-key retries added exactly one card, with three of the four timing variants exercising a genuine replay while the round stayed open.
- Impact
- Medium · action integrity — before the fix, a retried hit could deal a card the player had not asked for. Fixed by LIQD and re-verified.
- Limitations
- The shortest timeout never reached the server; this test closes together with IDEMP-004, which covers the same path.
LIQD-BJ-IDEMP-004MediumRemediatedLost response, then a retry of the same action
- Control assessed
- An action takes effect once across retries: one card for a hit, one added stake for a double or split, one round for a bet. Debits, credits and settlements reconcile.
- Testing approach
- Hit, double, split and bet requests whose responses were dropped after the server processed them, retried with the same key. Re-tested on 23 October 2026.
- Observed result
- Before the fix, a retried hit could add a second card and a completed bet could repeat. On the re-test, none of 88 cases applied an action twice; in 36 of them the drop was shown to follow processing, and the retry returned the original result — the settled bet returned its original round and consumed one nonce.
- Impact
- Medium · action integrity — before the fix, a lost response followed by a retry could repeat an action. Fixed by LIQD and re-verified.
- Limitations
- Of the 88 cases, 36 are confirmed processed-then-dropped replays; the other 52 exercise the same path.
LIQD-BJ-IDEMP-001PassRetry after a timeout on bet placement
- Control assessed
- Retrying a bet after a dropped connection debits the stake once.
- Testing approach
- A bet sent with a very short client timeout, then retried.
- Observed result
- Exactly one stake was debited — no orphaned or doubled bet.
- Impact
- None — no double debit.
- Limitations
- None specific to this test; the general scope is in 5.5.
Broken Authorization2/2
LIQD-BJ-AUTH-001PassAnother player acting on a live round
- Control assessed
- Only a round’s owner can act on it.
- Testing approach
- A second account sent hit, stand and double requests against the first account’s live round; the round compared before and after.
- Observed result
- All three were rejected and the round’s cards were unchanged: ownership is bound to the session, not to a field in the request.
- Impact
- None — isolation held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-AUTH-002PassSession reuse after logout
- Control assessed
- A session ends at logout: its cookie cannot place another bet.
- Testing approach
- A new bet attempted with a session cookie after logout.
- Observed result
- The logged-out session was rejected: logout is enforced server-side, not just in the browser.
- Impact
- None — the session was invalidated server-side.
- Limitations
- None specific to this test; the general scope is in 5.5.
Max-win Bypass3/3
LIQD-BJ-BET-002PassPayout and outcome fields in the request
- Control assessed
- Client-supplied outcome or payout fields never change the server-computed result.
- Testing approach
- Ten variants carrying injected payout, result, hand and legacy payout fields.
- Observed result
- None changed the server-computed outcome.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-BET-003PassInjected double-down amount
- Control assessed
- A double always stakes exactly the original bet.
- Testing approach
- Seven double requests carrying injected amounts.
- Observed result
- The charged amount never changed: the server always doubles the original bet.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-MAXWIN-001PassSide-bet amount tampering
- Control assessed
- Tampered side-bet amounts cannot produce an outsized payout.
- Testing approach
- Oversized, negative and wrong-type Perfect Pairs and 21+3 amounts, across seven cases.
- Observed result
- No case produced an abnormal payout ratio.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Nonce Manipulation3/3
LIQD-BJ-NONCE-001PassReused nonce
- Control assessed
- The client cannot choose the nonce: an injected value never causes reuse.
- Testing approach
- A bet carrying a previously used nonce.
- Observed result
- The nonce was never reused; the server assigned its own.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-NONCE-002PassSkipped nonces, forward and back
- Control assessed
- Injected nonces cannot move the server’s counter in either direction.
- Testing approach
- Bets carrying a nonce ten ahead and one five behind.
- Observed result
- Neither was honoured; the server kept its own sequential counter.
- Impact
- None — the server kept its own sequence.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-NONCE-003PassInvalid and boundary nonces
- Control assessed
- Invalid nonce values are ignored, the sequence stays intact, and no internal detail is returned.
- Testing approach
- Bets carrying seven invalid and boundary nonce values.
- Observed result
- All seven were ignored; the counter progressed correctly, and no internal detail was returned.
- 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-BJ-ENTROPY-001PassSeed uniqueness and commitment across rotations
- Control assessed
- Each revealed seed matches its earlier commitment, and no seed repeats.
- Testing approach
- Three rotation cycles, each revealed seed hashed and compared.
- Observed result
- Each revealed seed hashed to its prior commitment, and none repeated.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-PF-001PassProvably-fair data over five rounds
- Control assessed
- Every provably-fair field a player needs is present and non-empty, and no nonce repeats.
- Testing approach
- Five rounds checked field by field.
- Observed result
- Every required field was present and non-empty in all five rounds, and no nonce repeated.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-PF-002PassNonce sequence over ten rounds
- Control assessed
- The nonce advances by exactly one per round.
- Testing approach
- Ten consecutive rounds.
- Observed result
- All ten steps were exactly +1.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-PF-003PassDealt cards against LIQD’s verifier
- Control assessed
- LIQD’s own verifier reproduces the cards actually dealt.
- Testing approach
- Two rounds checked through LIQD’s verify endpoint after the reveal.
- Observed result
- Both reproduced the dealt cards exactly — checked against LIQD’s authoritative implementation, not a re-implemented shuffle.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
RNG / Information Leak1/1
LIQD-BJ-EXTRA-001PassDealer hole card in every hand state
- Control assessed
- At every point in a hand, a response shows only what the player may see — their own cards and the dealer’s up-card; the hole card appears only at settlement.
- Testing approach
- Responses inspected at every state — deal, hits, a double, a split, and a double after a split — and response timing compared between dealer-blackjack and other rounds for an insurance signal.
- Observed result
- No hole-card disclosure and no timing signal in any state; in the deepest state, a double after a split, only the dealer’s up-card appeared.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Entropy / PF Verification1/1
LIQD-BJ-ENTROPY-002PassServer-seed randomness across rotations
- Control assessed
- Server seeds are unique, well-formed and consistent with a uniform random draw.
- Testing approach
- Six seeds revealed through rotation, checked for format, repetition and per-symbol entropy.
- Observed result
- All six were unique, well-formed 32-character hex strings; per-seed entropy of 3.35–3.80 bits per symbol is the range expected of random hex.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Precision / Payout1/1
LIQD-BJ-PREC-001PassPayout accuracy over fifteen rounds
- Control assessed
- The balance change equals the sum of each round’s settlement.
- Testing approach
- Fifteen consecutive rounds, balance read before and after.
- Observed result
- The balance change matched the summed settlements exactly.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Event Replay2/2
LIQD-BJ-REPLAY-001PassStand replayed after settlement
- Control assessed
- Replaying a final action after settlement changes nothing.
- Testing approach
- The identical stand request sent again after the round settled.
- Observed result
- No additional balance change.
- Impact
- None — the round settled once.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-REPLAY-002PassAn action on a settled round
- Control assessed
- A settled round accepts no further actions.
- Testing approach
- A hit sent to a round that had already settled.
- Observed result
- The hit was rejected, and a follow-up check confirmed no round was left active.
- Impact
- None — the round settled once.
- Limitations
- None specific to this test; the general scope is in 5.5.
Adversarial Seeds4/4
LIQD-BJ-SEED-001PassClient-seed length and characters
- Control assessed
- No client seed, however long or unusual, breaks betting or returns internal detail.
- Testing approach
- Empty, very long (1,000+ characters), special-character, emoji, CJK and whitespace-only seeds, each followed by a bet.
- Observed result
- None broke the following bets or returned internal detail.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-SEED-002PassUnicode forms of one seed
- Control assessed
- Composed and decomposed Unicode forms of the same seed are handled consistently.
- Testing approach
- The same visible seed submitted in both forms, in two rotations.
- Observed result
- Both were handled consistently, with nothing carried between the two rotations; byte-exact handling of both forms is a consistent design choice.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-SEED-003PassRapid seed rotation
- Control assessed
- Bets placed after a rotation land under the new seed.
- Testing approach
- Rotate-then-bet sequences repeated back to back.
- Observed result
- Each sequence stayed on a single seed ID, distinct from the others: bets always land under the freshly rotated seed.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
LIQD-BJ-SEED-004PassRotation requested mid-round
- Control assessed
- A live round’s seed cannot change.
- Testing approach
- Seed rotation requested while a round was active.
- Observed result
- The round’s seed ID never changed: the seed is locked for the round.
- Impact
- None — the control held.
- Limitations
- None specific to this test; the general scope is in 5.5.
Technical Evidence & Verification4 sections
5.3Coverage Summary12 categories
Coverage by category — 39 tests: 32 pass, 7 remediated.
| Category | Tests | Result |
|---|---|---|
| Race Conditions | 13 | 10 pass · 3 remediated (RACE-010, RACE-011, RACE-014) |
| Input Validation | 2 | 2 remediated (BET-001, TOKEN-001) |
| Atomic Balance / Idempotency | 3 | 1 pass · 2 remediated (IDEMP-003, IDEMP-004) |
| Broken Authorization | 2 | All pass |
| Max-win Bypass | 3 | All pass |
| Nonce Manipulation | 3 | 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 | 2 | All pass |
| Adversarial Seeds | 4 | All pass |
5.4Findings & Remediation4 records
4 findings from 7 tests, all four remediated and re-verified. Each test’s card in the matrix above gives its control, approach, result, impact and limitations.
| Tests | Finding | Status |
|---|---|---|
| RACE-010 · RACE-011 | Parallel doubles or splits could attach extra stakes to one hand. LIQD made per-round placement atomic; fixed and re-verified. | Remediated · Critical, High |
| IDEMP-003 · IDEMP-004 | A retried in-round action could apply twice, adding a card or repeating a bet. LIQD added round and idempotency keys; fixed and re-verified. | Remediated · Medium |
| RACE-014 | An action delayed past the end of a round could change the next one. Stale and round-less requests are now refused; fixed and re-verified. | Remediated · Medium |
| BET-001 · TOKEN-001 | Grey-area stakes and malformed currency identifiers were accepted or defaulted instead of refused. Fixed by LIQD and re-verified. | Remediated · Low |
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, re-test captures, run timestamps and build identifiers — available to LIQD and to regulators on request.
Integrity tests ....................... 39Categories ............................ 12Pass .................................. 32Remediated ............................ 7Warning (open) ........................ 0Findings .............................. 4 · 4 remediated, 0 openProcedures and payloads ............... withheld (5.5)Player Verification
Can a player verify their own bets without trusting anyone?
Every Blackjack hand can be independently reproduced using publicly disclosed inputs. No hidden variables, no private backend data. If the revealed seed matches the hash you saved before betting and your computed shoe deals the cards you were dealt, the hand came from the seeds LIQD committed to. This section walks you through the process — with LIQD’s own panel, with the independent ProvablyFair.org verifier, or with a few lines of code you run yourself.
Key principles
- Every Blackjack hand can be independently reproduced — card for card
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saved and your computed shoe deals your cards, the hand came from the committed seeds
- Most players can verify directly through LIQD’s Provably Fair panel
What you need
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the hand’s number under the current seed pair (LIQD’s panel shows the count as “Total bets made with pair”); the pair is shared across LIQD’s Mines, Blackjack and Plinko, so bets in those games advance it too
- Your actions — the hit / stand / double / split decisions you made (the verifier replays them against the card sequence)
- Every Blackjack hand can be independently reproduced — card for card
- No hidden variables — no private backend data
- If the revealed seed matches the hash you saved and your computed shoe deals your cards, the hand came from the committed seeds
- Most players can verify directly through LIQD’s Provably Fair panel
- Server Seed — revealed after seed rotation (casino entropy)
- Client Seed — your player-controlled seed
- Nonce — the hand’s number under the current seed pair (LIQD’s panel shows the count as “Total bets made with pair”); the pair is shared across LIQD’s Mines, Blackjack and Plinko, so bets in those games advance it too
- Your actions — the hit / stand / double / split decisions you made (the verifier replays them against the card sequence)
Player can access all required inputsPass
Client seed origin transparentPass
Verification process documentedPass
MANIFEST.md, Verify a bet)Casino UI exposes verification panelPass
Independent verifier availablePass
Manual recomputation possiblePass
No hidden or proprietary stepsPass
Public algorithm documentationInfo
docs/ALGORITHM.md, written without LIQD’s code and reproduced on every captured card. Publishing it, or shipping a client-side verifier, is our recommendation to the operatorOnly disclosed inputs are used. Identical inputs always produce identical output.
- 1
Play a Blackjack Hand
Place a bet and play the hand. The entire 416-card shoe is shuffled from the seeds before the first card is dealt — your decisions don’t change the order of the cards, only how many are drawn and, after a split, which hand takes each one; the dealer plays by the published rules.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the game page. The Seeds tab shows the active client seed, the hashed server seed (the commitment) and the number of bets made with the pair — your next nonce. Under Rotate Seed Pair it also shows the hash of the next server seed. Save both hashes before you bet, and set your own next client seed there. Rotating retires the pair and reveals its server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saved before betting — across this audit’s 120 epochs it always did.
- 3
Enter the Seeds in the Verify Tab
In the panel’s Verify tab, choose Blackjack and enter the revealed server seed, your client seed and the hand’s nonce.
- 4
Recompute & Confirm
Press Verify — or enter the same inputs in the ProvablyFair.org verifier, or run the scripts under Manual Verification. Replay your actions against the recomputed shoe: if the commitment check in step 2 passed and the cards match your hand, the hand was dealt from the seeds LIQD committed to.
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
Choose your stake on the Blackjack table and place a bet — optionally adding Perfect Pairs and 21+3. The platform computes the entire 416-card shoe from (serverSeed, clientSeed, nonce) before any card is shown: the first four positions are the PDPD deal, and every hit, double, split card, and dealer draw takes the next position in order. Your decisions don’t change the order of the cards — they set how many are drawn and, after a split, which hand takes each one.

LIQD Blackjack on liqd.com — a $1.00 hand dealt Q♣ 10♠ (20) against the dealer’s Q♥, with the hole card face down. The whole shoe is fixed by the seeds before the deal.
The Provably Fair panel’s Seeds tab shows your active client seed, the hashed server seed — the commitment SHA-256(serverSeed) shown before you play — and “Total bets made with pair”, the nonce your next hand will use. Under Rotate Seed Pair it shows the next client seed (editable) and the hash of the next server seed. Save both hashes: after rotation the revealed seed must match the first, and the next pair’s seed is already fixed by the second. Rotating the seed pair retires it and reveals the outgoing server seed in plaintext. First check: SHA-256 of the revealed 32-hex string (hashed as UTF-8 text) must equal the commitment you saved — across the audit’s 120 captured epochs it always did.

The Seeds tab before rotating — Active Server Seed (Hashed) 58fa7f71…53920e after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 8c091faa…c8ddd1, hashes to 58fa7f71…53920e.
The panel’s Verify tab rebuilds a hand from the game, the client seed, the server seed and the nonce. Choose Blackjack and enter the server seed revealed in step 2, your client seed and the hand’s nonce — its number under the pair, counting from 0. Every hand played under that seed can be recomputed this way.

The Verify tab filled in for that hand — Blackjack, client seed, the server seed revealed in step 2 and nonce 0.
Press Verify. The tab rebuilds the full shoe and shows the deal plus the next cards in shoe order — read across to confirm your hits and the dealer’s draws line up with what happened at the table. A match shows the cards you were dealt are the ones the committed seeds produce.

The result: the deal — Q♣ 10♠ (20) for the player, Q♥ 8♦ (18) for the dealer — and the next cards 3♣ K♠ 3♦ J♥ 2♣ 9♥. Our scripts in 6.8–6.9 reproduce it card for card: the shoe opens Q♣ Q♥ 10♠ 8♦ 3♣ K♠ 3♦ J♥ 2♣ 9♥.
Manual Verification (Advanced)6 sections
6.5Why Manual Verification Matters
True provably fair verification means you don’t have to trust any casino-provided tool — or any hosted verifier, including ours. Manual verification runs the calculation on your own machine, so a tampered verifier cannot mislead you. The scripts below do exactly that: complete implementations of the written specification of LIQD’s shoe derivation in about 50 lines each, dependency-free and runnable offline (the one case the specification leaves open — all eight numbers of a digest rejected, about 2 × 10⁻⁵² over the capture — stops them with an error). If they reproduce your hand, no party on the internet was involved in convincing you.
6.6How the Algorithm Works (Plain English)3 records
Start with 416 cards in a fixed factory order — 8 decks, each laid out clubs, hearts, spades, diamonds, Ace through King. Then shuffle backwards through the whole shoe: at position 415, ask HMAC-SHA256 for a number between 0 and 415 and swap; at 414, a number between 0 and 414; down to position 1. Each answer comes from hashing clientSeed:nonce:position with the server seed as the key — so all three inputs shape every swap.
- The 32-byte digest is read as eight 32-bit numbers; the first one under the fairness ceiling is kept (rejection sampling — every position equally likely). No captured draw ever needed a second number, as expected (S2.3)
- 415 swaps later the shoe is fully shuffled — and fully determined. Nothing that happens at the table can change it
- The deal reads from the top:
shoe[0]you,shoe[1]dealer up,shoe[2]you,shoe[3]dealer hole — then every draw takesshoe[4],shoe[5], … in order
| Input | Where You Get It | What It Does |
|---|---|---|
| Server seed (32-char hex) | Returned when you rotate the seed pair (the capture took all 120 from the rotate response) | The HMAC key — the casino's committed entropy |
| Client seed | You set it in the fairness panel | Message component — your influence on every swap |
| Nonce | Your bet number within the epoch (starts at 0) | Message component — makes each bet's shoe unique |
SHA-256 of the 32 hex characters as a UTF-8 string. The scripts below do this correctly; if you roll your own and the commitment won't match, this is almost always why.On split rounds the order is pinned: shoe[4] completes your first (main) hand, shoe[5] completes the split hand — then the split hand plays out first, then the main hand, then the dealer. The audit verified this order card-for-card on all 401 captured splits.
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 (in-game panel) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino — it computes on LIQD’s server | ProvablyFair.org — its derivation code is vendored, byte for byte, in the public audit package |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | Cross-checked live during the capture session and in the integrity tests (LIQD-BJ-PF-003, S5); those pairs are not part of the audit package | Directly — Step 6 runs its pinned code (only the module syntax stripped) on 515 captured rounds (every split and the first round of every epoch), 916 hands and 121 full shoes, 0 disagreements |
| If they disagree | Suspect the casino’s tool or a tampered round | Trust neither blindly: run the standalone scripts below and let the math referee |
6.8JavaScript Verification Script
Complete and dependency-free — save as verify-blackjack.js and run node verify-blackjack.js <serverSeed> <clientSeed> <nonce>. Run as-is, it checks the dataset’s first hand (epoch 0, nonce 0); run on every captured round, it reproduces all 6,000 hands — 120 commitments and 34,734 cards:
// verify-blackjack.js — standalone LIQD Blackjack verifier (Node.js, no dependencies)// Usage: node verify-blackjack.js <serverSeed> <clientSeed> <nonce>const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg] = argv.length ? argv : ['6dca5e36b6b9dc56730e282ab3c2bae6', 'audita6d2fe11bb1f', '0'];const nonce = Number(nonceArg); // 1) Commitment: SHA-256 of the seed's hex STRING (UTF-8) — compare it with the hash you saved before bettingconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One draw over `range` values: HMAC-SHA256 keyed on the 16 DECODED seed bytes, message clientSeed:nonce:cursor;// read the digest as eight big-endian 32-bit numbers and keep the first one below maxFair, mod rangeconst key = Buffer.from(serverSeed, 'hex');function draw(cursor, range) { const digest = createHmac('sha256', key).update(`${clientSeed}:${nonce}:${cursor}`).digest(); const maxFair = Math.floor(2 ** 32 / range) * range; for (let offset = 0; offset < 32; offset += 4) { const value = digest.readUInt32BE(offset); if (value < maxFair) return value % range; } throw new Error('all eight chunks rejected: the specification gives no rule for this case (no captured draw rejected even its first chunk)');} // 3) The shoe: 8 decks in the order CLUB, HEART, SPADE, DIAMOND × ranks 1 (ace) … 13 (king), 416 cards;// then a backward Fisher-Yates: for i = 415 down to 1, swap position i with position draw(i, i + 1)const SUITS = ['CLUB', 'HEART', 'SPADE', 'DIAMOND'];const shoe = [];for (let x = 0; x < 416; x++) shoe.push(`${SUITS[Math.floor((x % 52) / 13)]}:${(x % 13) + 1}`);for (let i = 415; i >= 1; i--) { const j = draw(i, i + 1); [shoe[i], shoe[j]] = [shoe[j], shoe[i]];} // 4) Read the deal off the top of the shoeconst pretty = (card) => { const [suit, rank] = card.split(':'); return ({ 1: 'A', 11: 'J', 12: 'Q', 13: 'K' }[rank] || rank) + { CLUB: '♣', HEART: '♥', SPADE: '♠', DIAMOND: '♦' }[suit];};console.log('shoe[0..15] =', shoe.slice(0, 16).join(' '));console.log('as cards =', shoe.slice(0, 16).map(pretty).join(' '));console.log(`deal = you ${pretty(shoe[0])} ${pretty(shoe[2])} · dealer ${pretty(shoe[1])} (hole card ${pretty(shoe[3])})`);// No split: your hits (or your double card) take shoe[4], shoe[5], … in order, then the dealer draws.// Split: shoe[4] completes your first hand and shoe[5] the split hand; the split hand then plays out,// then your first hand, then the dealer.// Expected for the defaults (bet 6hBu_VAq-lMp3UKjYMS1a, epoch 0 nonce 0, $0.10):// commitment = 948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42// deal = you 7♥ 9♥ · dealer 10♣ (hole card J♥); you hit and draw shoe[4] = 5♥ for 21 against the// dealer's 20 — a win, paid 0.20 on the 0.10 stake6.9Python Verification Script
The same verifier in Python 3, standard library only — save as verify-blackjack.py and run python3 verify-blackjack.py <serverSeed> <clientSeed> <nonce>:
# verify-blackjack.py — standalone LIQD Blackjack verifier (Python 3, stdlib only)# Usage: python3 verify-blackjack.py <serverSeed> <clientSeed> <nonce>import hashlib, hmac, sys args = sys.argv[1:] or ['6dca5e36b6b9dc56730e282ab3c2bae6', 'audita6d2fe11bb1f', '0']server_seed, client_seed, nonce = args[0], args[1], int(args[2]) # 1) Commitment — SHA-256 of the seed's hex string as UTF-8 text; compare it with the hash you saved before bettingprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One draw over `rng` values — HMAC-SHA256 keyed on the 16 decoded seed bytes, message client_seed:nonce:cursor;# read the digest as eight big-endian 32-bit numbers and keep the first one below max_fair, mod rngkey = bytes.fromhex(server_seed)def draw(cursor, rng): digest = hmac.new(key, f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest() max_fair = (2 ** 32 // rng) * rng for offset in range(0, 32, 4): value = int.from_bytes(digest[offset:offset + 4], 'big') if value < max_fair: return value % rng raise ValueError('all eight chunks rejected: the specification gives no rule for this case (no captured draw rejected even its first chunk)') # 3) The shoe — 8 decks in the order CLUB, HEART, SPADE, DIAMOND × ranks 1 (ace) … 13 (king), 416 cards;# then a backward Fisher-Yates: for i = 415 down to 1, swap position i with position draw(i, i + 1)SUITS = ['CLUB', 'HEART', 'SPADE', 'DIAMOND']shoe = [f'{SUITS[(x % 52) // 13]}:{x % 13 + 1}' for x in range(416)]for i in range(415, 0, -1): j = draw(i, i + 1) shoe[i], shoe[j] = shoe[j], shoe[i] # 4) Read the deal off the top of the shoedef pretty(card): suit, rank = card.split(':') return {'1': 'A', '11': 'J', '12': 'Q', '13': 'K'}.get(rank, rank) + {'CLUB': '♣', 'HEART': '♥', 'SPADE': '♠', 'DIAMOND': '♦'}[suit]print('shoe[0..15] =', ' '.join(shoe[:16]))print('as cards =', ' '.join(pretty(c) for c in shoe[:16]))print(f'deal = you {pretty(shoe[0])} {pretty(shoe[2])} · dealer {pretty(shoe[1])} (hole card {pretty(shoe[3])})')# No split: your hits (or your double card) take shoe[4], shoe[5], … in order, then the dealer draws.# Split: shoe[4] completes your first hand and shoe[5] the split hand; the split hand then plays out,# then your first hand, then the dealer.# Expected for the defaults (bet 6hBu_VAq-lMp3UKjYMS1a, epoch 0 nonce 0, $0.10):# commitment = 948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42# deal = you 7♥ 9♥ · dealer 10♣ (hole card J♥); you hit and draw shoe[4] = 5♥ for 21 against the# dealer's 20 — a win, paid 0.20 on the 0.10 stake948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42, shoe head HEART:7 CLUB:10 HEART:9 HEART:11 HEART:5 and the deal “you 7♥ 9♥ · dealer 10♣ (hole card J♥)” — the dataset’s epoch 0, nonce 0 hand, where the player hits the 5♥ for 21 against the dealer’s 20. Both scripts were run on all 6,000 captured hands and reproduce every commitment and every card.6.10Evidence Screenshots & Records7 records
E12, E13, E15 and E16 are live screenshots that illustrate the player path and E14 is a card rendered by the auditor; none of them is an epoch of the audited dataset. The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.
| File | Shows |
|---|---|
| E12-blackjack-table-sidebets.png | The table on the pre-production environment in side-bet mode, with the felt labels |
| E13-provably-fair-panel.png | The Seeds tab — active client seed, hashed server seed, bets made with the pair, next client seed and next server-seed hash |
| E14-seed-rotation-reveal.png | An auditor-rendered reveal card, not a live screenshot — its seed and commitment recompute exactly; the seed is outside the 120-epoch dataset |
| E15-settled-hand.png | A settled losing round (player 9, dealer 17) — documents the flow, not a payout |
| E16-storefront-edge-0.48.png | The game page header showing “Edge: 0.48%”, captured 5 August 2026 |
| E18-insurance-decline-2026-10-13.md | One insurance offer on qa declined with LIQD’s own NO button, 13 October 2026 — the settled hand carries no insurance bet (recording and two pictures alongside) |
| rtp-page-labels-2026-10-01.json | The RTP and house-edge figures on LIQD’s Blackjack 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
Play a Blackjack Hand
Place a bet and play the hand. The entire 416-card shoe is shuffled from the seeds before the first card is dealt — your decisions don’t change the order of the cards, only how many are drawn and, after a split, which hand takes each one; the dealer plays by the published rules.
- 2
Open the Provably Fair Panel and Rotate the Seed
Open the Provably Fair panel on the game page. The Seeds tab shows the active client seed, the hashed server seed (the commitment) and the number of bets made with the pair — your next nonce. Under Rotate Seed Pair it also shows the hash of the next server seed. Save both hashes before you bet, and set your own next client seed there. Rotating retires the pair and reveals its server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saved before betting — across this audit’s 120 epochs it always did.
- 3
Enter the Seeds in the Verify Tab
In the panel’s Verify tab, choose Blackjack and enter the revealed server seed, your client seed and the hand’s nonce.
- 4
Recompute & Confirm
Press Verify — or enter the same inputs in the ProvablyFair.org verifier, or run the scripts under Manual Verification. Replay your actions against the recomputed shoe: if the commitment check in step 2 passed and the cards match your hand, the hand was dealt from the seeds LIQD committed to.
Visual Walkthroughstep-by-step
Choose your stake on the Blackjack table and place a bet — optionally adding Perfect Pairs and 21+3. The platform computes the entire 416-card shoe from (serverSeed, clientSeed, nonce) before any card is shown: the first four positions are the PDPD deal, and every hit, double, split card, and dealer draw takes the next position in order. Your decisions don’t change the order of the cards — they set how many are drawn and, after a split, which hand takes each one.

LIQD Blackjack on liqd.com — a $1.00 hand dealt Q♣ 10♠ (20) against the dealer’s Q♥, with the hole card face down. The whole shoe is fixed by the seeds before the deal.
The Provably Fair panel’s Seeds tab shows your active client seed, the hashed server seed — the commitment SHA-256(serverSeed) shown before you play — and “Total bets made with pair”, the nonce your next hand will use. Under Rotate Seed Pair it shows the next client seed (editable) and the hash of the next server seed. Save both hashes: after rotation the revealed seed must match the first, and the next pair’s seed is already fixed by the second. Rotating the seed pair retires it and reveals the outgoing server seed in plaintext. First check: SHA-256 of the revealed 32-hex string (hashed as UTF-8 text) must equal the commitment you saved — across the audit’s 120 captured epochs it always did.

The Seeds tab before rotating — Active Server Seed (Hashed) 58fa7f71…53920e after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 8c091faa…c8ddd1, hashes to 58fa7f71…53920e.
The panel’s Verify tab rebuilds a hand from the game, the client seed, the server seed and the nonce. Choose Blackjack and enter the server seed revealed in step 2, your client seed and the hand’s nonce — its number under the pair, counting from 0. Every hand played under that seed can be recomputed this way.

The Verify tab filled in for that hand — Blackjack, client seed, the server seed revealed in step 2 and nonce 0.
Press Verify. The tab rebuilds the full shoe and shows the deal plus the next cards in shoe order — read across to confirm your hits and the dealer’s draws line up with what happened at the table. A match shows the cards you were dealt are the ones the committed seeds produce.

The result: the deal — Q♣ 10♠ (20) for the player, Q♥ 8♦ (18) for the dealer — and the next cards 3♣ K♠ 3♦ J♥ 2♣ 9♥. Our scripts in 6.8–6.9 reproduce it card for card: the shoe opens Q♣ Q♥ 10♠ 8♦ 3♣ K♠ 3♦ J♥ 2♣ 9♥.
Technical evidence
6.5Why Manual Verification Matters
True provably fair verification means you don’t have to trust any casino-provided tool — or any hosted verifier, including ours. Manual verification runs the calculation on your own machine, so a tampered verifier cannot mislead you. The scripts below do exactly that: complete implementations of the written specification of LIQD’s shoe derivation in about 50 lines each, dependency-free and runnable offline (the one case the specification leaves open — all eight numbers of a digest rejected, about 2 × 10⁻⁵² over the capture — stops them with an error). If they reproduce your hand, no party on the internet was involved in convincing you.
6.6How the Algorithm Works (Plain English)3 records
Start with 416 cards in a fixed factory order — 8 decks, each laid out clubs, hearts, spades, diamonds, Ace through King. Then shuffle backwards through the whole shoe: at position 415, ask HMAC-SHA256 for a number between 0 and 415 and swap; at 414, a number between 0 and 414; down to position 1. Each answer comes from hashing clientSeed:nonce:position with the server seed as the key — so all three inputs shape every swap.
- The 32-byte digest is read as eight 32-bit numbers; the first one under the fairness ceiling is kept (rejection sampling — every position equally likely). No captured draw ever needed a second number, as expected (S2.3)
- 415 swaps later the shoe is fully shuffled — and fully determined. Nothing that happens at the table can change it
- The deal reads from the top:
shoe[0]you,shoe[1]dealer up,shoe[2]you,shoe[3]dealer hole — then every draw takesshoe[4],shoe[5], … in order
| Input | Where You Get It | What It Does |
|---|---|---|
| Server seed (32-char hex) | Returned when you rotate the seed pair (the capture took all 120 from the rotate response) | The HMAC key — the casino's committed entropy |
| Client seed | You set it in the fairness panel | Message component — your influence on every swap |
| Nonce | Your bet number within the epoch (starts at 0) | Message component — makes each bet's shoe unique |
SHA-256 of the 32 hex characters as a UTF-8 string. The scripts below do this correctly; if you roll your own and the commitment won't match, this is almost always why.On split rounds the order is pinned: shoe[4] completes your first (main) hand, shoe[5] completes the split hand — then the split hand plays out first, then the main hand, then the dealer. The audit verified this order card-for-card on all 401 captured splits.
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 (in-game panel) | ProvablyFair.org | |
|---|---|---|
| Who controls the code | The casino — it computes on LIQD’s server | ProvablyFair.org — its derivation code is vendored, byte for byte, in the public audit package |
| What it proves alone | Internal consistency of LIQD’s own math | Independent recomputation from raw inputs |
| Checked against this capture | Cross-checked live during the capture session and in the integrity tests (LIQD-BJ-PF-003, S5); those pairs are not part of the audit package | Directly — Step 6 runs its pinned code (only the module syntax stripped) on 515 captured rounds (every split and the first round of every epoch), 916 hands and 121 full shoes, 0 disagreements |
| If they disagree | Suspect the casino’s tool or a tampered round | Trust neither blindly: run the standalone scripts below and let the math referee |
6.8JavaScript Verification Script
Complete and dependency-free — save as verify-blackjack.js and run node verify-blackjack.js <serverSeed> <clientSeed> <nonce>. Run as-is, it checks the dataset’s first hand (epoch 0, nonce 0); run on every captured round, it reproduces all 6,000 hands — 120 commitments and 34,734 cards:
// verify-blackjack.js — standalone LIQD Blackjack verifier (Node.js, no dependencies)// Usage: node verify-blackjack.js <serverSeed> <clientSeed> <nonce>const { createHash, createHmac } = require('node:crypto'); const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg] = argv.length ? argv : ['6dca5e36b6b9dc56730e282ab3c2bae6', 'audita6d2fe11bb1f', '0'];const nonce = Number(nonceArg); // 1) Commitment: SHA-256 of the seed's hex STRING (UTF-8) — compare it with the hash you saved before bettingconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) One draw over `range` values: HMAC-SHA256 keyed on the 16 DECODED seed bytes, message clientSeed:nonce:cursor;// read the digest as eight big-endian 32-bit numbers and keep the first one below maxFair, mod rangeconst key = Buffer.from(serverSeed, 'hex');function draw(cursor, range) { const digest = createHmac('sha256', key).update(`${clientSeed}:${nonce}:${cursor}`).digest(); const maxFair = Math.floor(2 ** 32 / range) * range; for (let offset = 0; offset < 32; offset += 4) { const value = digest.readUInt32BE(offset); if (value < maxFair) return value % range; } throw new Error('all eight chunks rejected: the specification gives no rule for this case (no captured draw rejected even its first chunk)');} // 3) The shoe: 8 decks in the order CLUB, HEART, SPADE, DIAMOND × ranks 1 (ace) … 13 (king), 416 cards;// then a backward Fisher-Yates: for i = 415 down to 1, swap position i with position draw(i, i + 1)const SUITS = ['CLUB', 'HEART', 'SPADE', 'DIAMOND'];const shoe = [];for (let x = 0; x < 416; x++) shoe.push(`${SUITS[Math.floor((x % 52) / 13)]}:${(x % 13) + 1}`);for (let i = 415; i >= 1; i--) { const j = draw(i, i + 1); [shoe[i], shoe[j]] = [shoe[j], shoe[i]];} // 4) Read the deal off the top of the shoeconst pretty = (card) => { const [suit, rank] = card.split(':'); return ({ 1: 'A', 11: 'J', 12: 'Q', 13: 'K' }[rank] || rank) + { CLUB: '♣', HEART: '♥', SPADE: '♠', DIAMOND: '♦' }[suit];};console.log('shoe[0..15] =', shoe.slice(0, 16).join(' '));console.log('as cards =', shoe.slice(0, 16).map(pretty).join(' '));console.log(`deal = you ${pretty(shoe[0])} ${pretty(shoe[2])} · dealer ${pretty(shoe[1])} (hole card ${pretty(shoe[3])})`);// No split: your hits (or your double card) take shoe[4], shoe[5], … in order, then the dealer draws.// Split: shoe[4] completes your first hand and shoe[5] the split hand; the split hand then plays out,// then your first hand, then the dealer.// Expected for the defaults (bet 6hBu_VAq-lMp3UKjYMS1a, epoch 0 nonce 0, $0.10):// commitment = 948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42// deal = you 7♥ 9♥ · dealer 10♣ (hole card J♥); you hit and draw shoe[4] = 5♥ for 21 against the// dealer's 20 — a win, paid 0.20 on the 0.10 stake6.9Python Verification Script
The same verifier in Python 3, standard library only — save as verify-blackjack.py and run python3 verify-blackjack.py <serverSeed> <clientSeed> <nonce>:
# verify-blackjack.py — standalone LIQD Blackjack verifier (Python 3, stdlib only)# Usage: python3 verify-blackjack.py <serverSeed> <clientSeed> <nonce>import hashlib, hmac, sys args = sys.argv[1:] or ['6dca5e36b6b9dc56730e282ab3c2bae6', 'audita6d2fe11bb1f', '0']server_seed, client_seed, nonce = args[0], args[1], int(args[2]) # 1) Commitment — SHA-256 of the seed's hex string as UTF-8 text; compare it with the hash you saved before bettingprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) One draw over `rng` values — HMAC-SHA256 keyed on the 16 decoded seed bytes, message client_seed:nonce:cursor;# read the digest as eight big-endian 32-bit numbers and keep the first one below max_fair, mod rngkey = bytes.fromhex(server_seed)def draw(cursor, rng): digest = hmac.new(key, f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest() max_fair = (2 ** 32 // rng) * rng for offset in range(0, 32, 4): value = int.from_bytes(digest[offset:offset + 4], 'big') if value < max_fair: return value % rng raise ValueError('all eight chunks rejected: the specification gives no rule for this case (no captured draw rejected even its first chunk)') # 3) The shoe — 8 decks in the order CLUB, HEART, SPADE, DIAMOND × ranks 1 (ace) … 13 (king), 416 cards;# then a backward Fisher-Yates: for i = 415 down to 1, swap position i with position draw(i, i + 1)SUITS = ['CLUB', 'HEART', 'SPADE', 'DIAMOND']shoe = [f'{SUITS[(x % 52) // 13]}:{x % 13 + 1}' for x in range(416)]for i in range(415, 0, -1): j = draw(i, i + 1) shoe[i], shoe[j] = shoe[j], shoe[i] # 4) Read the deal off the top of the shoedef pretty(card): suit, rank = card.split(':') return {'1': 'A', '11': 'J', '12': 'Q', '13': 'K'}.get(rank, rank) + {'CLUB': '♣', 'HEART': '♥', 'SPADE': '♠', 'DIAMOND': '♦'}[suit]print('shoe[0..15] =', ' '.join(shoe[:16]))print('as cards =', ' '.join(pretty(c) for c in shoe[:16]))print(f'deal = you {pretty(shoe[0])} {pretty(shoe[2])} · dealer {pretty(shoe[1])} (hole card {pretty(shoe[3])})')# No split: your hits (or your double card) take shoe[4], shoe[5], … in order, then the dealer draws.# Split: shoe[4] completes your first hand and shoe[5] the split hand; the split hand then plays out,# then your first hand, then the dealer.# Expected for the defaults (bet 6hBu_VAq-lMp3UKjYMS1a, epoch 0 nonce 0, $0.10):# commitment = 948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42# deal = you 7♥ 9♥ · dealer 10♣ (hole card J♥); you hit and draw shoe[4] = 5♥ for 21 against the# dealer's 20 — a win, paid 0.20 on the 0.10 stake948eff896b5521c1064402dcd5557342210e03e0c4e864d0b83418bfb5e12f42, shoe head HEART:7 CLUB:10 HEART:9 HEART:11 HEART:5 and the deal “you 7♥ 9♥ · dealer 10♣ (hole card J♥)” — the dataset’s epoch 0, nonce 0 hand, where the player hits the 5♥ for 21 against the dealer’s 20. Both scripts were run on all 6,000 captured hands and reproduce every commitment and every card.6.10Evidence Screenshots & Records7 records
E12, E13, E15 and E16 are live screenshots that illustrate the player path and E14 is a card rendered by the auditor; none of them is an epoch of the audited dataset. The walkthrough’s screenshots are page captures from liqd.com, outside the package’s evidence set.
| File | Shows |
|---|---|
| E12-blackjack-table-sidebets.png | The table on the pre-production environment in side-bet mode, with the felt labels |
| E13-provably-fair-panel.png | The Seeds tab — active client seed, hashed server seed, bets made with the pair, next client seed and next server-seed hash |
| E14-seed-rotation-reveal.png | An auditor-rendered reveal card, not a live screenshot — its seed and commitment recompute exactly; the seed is outside the 120-epoch dataset |
| E15-settled-hand.png | A settled losing round (player 9, dealer 17) — documents the flow, not a payout |
| E16-storefront-edge-0.48.png | The game page header showing “Edge: 0.48%”, captured 5 August 2026 |
| E18-insurance-decline-2026-10-13.md | One insurance offer on qa declined with LIQD’s own NO button, 13 October 2026 — the settled hand carries no insurance bet (recording and two pictures alongside) |
| rtp-page-labels-2026-10-01.json | The RTP and house-edge figures on LIQD’s Blackjack 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 one of the 31 scored steps, the statistics behind them and the committed simulation can be independently reproduced by anyone with a computer and an internet connection. The live integrity tests in S5 are the exception: their procedures, payloads and captures are held under the engagement record (S5.5–S5.6). The repository is the credential — not this report.
Repository Details
- GitHub: ProvablyFair-org/liqd-blackjack
- Release: v1.0.1 · Framework 1.1
- Game: Blackjack (liqd-blackjack)
- Public Verifier: provablyfair.org/verify-bets/liqd
Prerequisites
- Node.js 22.x (
package.jsonengines and.nvmrc) —npm testneeds no dependency install - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.7.6) — for the source commands (npm run verify,rtp,simulate,mutate) - Python 3 — only for the optional replays in
replay/and the oracle inanchor/
- GitHub: ProvablyFair-org/liqd-blackjack
- Release: v1.0.1 · Framework 1.1
- Game: Blackjack (liqd-blackjack)
- Public Verifier: provablyfair.org/verify-bets/liqd
- Node.js 22.x (
package.jsonengines and.nvmrc) —npm testneeds no dependency install - Git
- TypeScript toolchain via
npm ci(TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.7.6) — for the source commands (npm run verify,rtp,simulate,mutate) - Python 3 — only for the optional replays in
replay/and the oracle inanchor/
All 31 scored steps and the committed simulation reproduce from release v1.0.1, the pinned artifacts and the commands above. The dataset hash is checked before any step runs, so you are testing the same 6,000 hands; the ten artifacts of record are pinned by SHA-256 and reconciled in Step 25 on every run, and npm run simulate deals the committed simulation again from its pinned master seed.
Commands to Reproduce
The 6,000-hand dataset and the committed results ship inside the repo — no separate download, and the dataset’s SHA-256 is checked before any step runs. npm test needs only Node.js 22: it checks that the bundles match the sources, runs the 117 unit tests (the exact 8-deck solves among them) and the 31-step verification, then checks the release manifest. Output as run on Node 22.22.0 with two CPU cores (10 min 22 s, about 7 minutes of it the unit tests).
Every run writes only to outputs/run/ and compares its results field by field with the committed results (outputs/run/diff.json) — that comparison is the reproduction gate. Exit codes: 0 Full Pass · 2 a flag with no fail (Conditional Pass) · 5 RTP FIGURE OVERSTATED (the RTP claim rule finds a published figure overstated) · 1 any hard fail, or a reproduction beyond tolerance. Output of node verify.js as run on Node 22.22.0; npm run verify runs the same steps from source. The RTP claim’s source note is shortened here (…).
npm test replays every captured hand and scores the committed simulation, but does not rerun it. npm run simulate does: every random input derives from the pinned master seed, so the three passes are dealt again — 30,000,000 Pass 0 rounds, the 1,000,000-round real-shoe pass and the 120 revealed seeds × 10,000 nonces of Pass 2 — written to outputs/run/ and compared field by field with the committed simulation (outputs/run/simulation-diff.json), never over it. Output as run on Node 22.22.0 with two CPU cores (52 minutes), with the progress bars and an internal solver diagnostic left out (…).
Audit Reproducibility Pinning
- Release Tag
- v1.0.1 — github.com/ProvablyFair-org/liqd-blackjack/tree/v1.0.1
- Framework
- ProvablyFair.org Audit Framework 1.1 · Blackjack profile 1.0.2 · RTP-CLAIM 1.2.0
- Release Signature
- Signed with the ProvablyFair.org release key (
SHA256:gEujiJm2pRyq7lQsfYQfQ1r97H/J7GdaKHbOW8GtikY), recorded inRELEASE-MANIFEST.json - Node Version
- 22.x (
package.jsonengines and.nvmrc); run here on 22.22.0 - Primary Dataset
- data/blackjack-6000hands.json (6,000 rounds, 6,401 player hands, 34,734 cards, 120 seed epochs of exactly 50)
- Primary Dataset Hash (SHA-256)
- c2a28c5164a546105963ffb4ff8e7c422a68802df47afcdcfa0b4fdbad1fdbf7
- Rule Set
- Single-sourced in
src/config.tsRULES (11 rules); the solver’s rules andanchor/rules.jsonmust equal it field for field (Step 25) - Exact-RTP Artifact Hash (SHA-256)
- f89b1920086bf0af17dbf5f30350bfef259a6fa0f934631a7ffa41e34cec4611
- RTP Claim Table Hash (SHA-256)
- 9e447e3bfb9514d6f01adf90af4e366c17bb1b5431ad632ea7bd753ad990cb27
- Simulation Artifact Hash (SHA-256)
- 26ab1574ced2a1e59ca653a582951dac304ca62ea621e96b2b6de1e08a193567
- Convergence Chart Hash (SHA-256)
- 45ce9d7ad631d600adfb526b68646c5c90434806f449aada193640bf90d8d4bb
- Simulation Provenance Hash (SHA-256)
- 5ad0e673045c4b26ae0dd63fff3b3fbfb96b5cf8778039254261238a89571b4c
- Bootstrap Nulls Hash (SHA-256)
- c69eeb27b6e0679658c5f2bf1f3d5c60481ee6de062ae16e1cc901bcbac87916
- Deep-Replay Report Hash (SHA-256)
- 44f2c8377f14df3e996ee2f753b7ec38faae97edd20497772cdd04c992b2ccbe
- Bootstrap-Replay Report Hash (SHA-256)
- c4cf022c5d34bdfba86829f87551591baba694e1bf8351b99f138738deae4e05
- Seed-Grind Experiment Hash (SHA-256)
- 44d45d84a8cd7d54dee3fca146be7e146f4e1fa65385cd34d33fa611ee9d1e78
- RNG Branch-Coverage Hash (SHA-256)
- eb2c74a1ecf84d3dbf3c1cc3a490b9c04ad1a24d201c4f58efc4403bbacb4593
- Verification Results Hash (SHA-256)
- f57cfb406b3537411ecf280854057f6d9e2d396c5e4d6a2ca1ab44fe5f860ea9
- Capture Rig Hash (SHA-256)
- 4a214c87176467dc62d7586ed971c9c361878b817ed2f8ee2417b7e2cfc033e2 — the client-seed generator, re-hashed in Step 7
- Reproduction Gate
- Every run writes only to outputs/run/ and is compared field by field with the committed artifacts (outputs/run/diff.json) — exact content byte for byte, a derived floating-point statistic within 1e-14 + 1e-12 relative; anything beyond tolerance exits 1
- Exit Codes
- 0 Full Pass · 2 a flag with no fail (Conditional Pass) · 5 RTP FIGURE OVERSTATED · 1 hard fail or reproduction beyond tolerance
- Mutation Battery
- 86 declared mutations in
tests/mutations.json, each naming the guard it must break (npm run mutate, outsidenpm test). The package records 0 failures in every battery run it reports, including later runs of the entries for the shuffle, the draw, the site verifier and the RTP claim - Runtime
npm test10 min 22 s on 2 CPU cores (Node 22.22.0);npm run simulate, outsidenpm test, 52 min 13 s on the same machine- Capture Environment
- LIQD’s pre-production environment, qa.liqd.com · one authenticated account, 5 August 2026, 18:31–20:18 UTC
- Audit Date
- August 2026 (dataset captured 5 August 2026); RTP page read 1 October 2026
- Audit ID
- PF-2026-LQ01
Output Artifacts12 files committed
| File | Contents | Size |
|---|---|---|
| verification-results.json | Steps 1–31 results — per-step pass states with evidence detail strings, profile coverage (BLACKJACK-01 … BLACKJACK-30), the full SHA-256 of six pinned artifacts (artifactHashes; Step 25’s detail lists all ten pins, abbreviated), info items, verdict | ~67 KB |
| report-figures.json | The accounting, insurance, natural-frequency and side-bet figures quoted in the report — re-derived field for field by Step 25 | ~3 KB |
| exact-rtp.json | The exact figures of record — the 8-deck basic-strategy solve (99.512325% / 0.487675%), the one-deck sensitivity, the Wizard of Odds anchor and the per-round moments Step 19 uses; npm run rtp regenerates it byte-identically | ~5 KB |
| rtp-claim-table.json | The RTP claim rule’s table — LIQD’s published 99.52% against the one setting, with exact RTP, gap and outcome | ~2 KB |
| simulation-results.json | Pass 0 (30M fair-shuffle rounds), Pass 1 (1M real-shoe rounds, 100 census rows) and Pass 2 (120 seeds × 10,000 nonces) with their retained replay inputs and the convergence trace | ~506 KB |
| simulation-provenance.json | What produced the simulation: master seed, derivation rule, modules, digests and per-pass replay commands | ~4 KB |
| rtp-convergence.html | Static inline-SVG convergence chart of the 30M-round trace (this page re-renders the same trace) | ~6 KB |
| bootstrap-nulls.float64le | The 2,000 retained Pass 2 bootstrap statistics, in generation order | ~16 KB |
| bootstrap-replay-report.json | Independent Python reconstruction of Pass 2’s rank screen — all 2,000 statistics byte for byte, 0 mismatches | ~26 KB |
| deep-replay-report.json | Re-deal of all three simulation passes — 282 values compared, verdict PASS | ~1.5 KB |
| cherry-pick-attack.json | The seed-grind experiment: 50 candidate seeds × 120 epochs against the Step 19 money rule — 34 / 120 flagged, pooled z −23.56 | ~76 KB |
| rng-branch-coverage.json | Branch coverage of the draw over the 2,490,000 captured draws — 0 later-chunk accepts, 0 fallbacks — and the exposure bounds if rejection were omitted | ~1.6 KB |
Repository Structure
liqd-blackjack/ ├── src/ │ ├── rng.ts → commitHash · HMAC draw with rejection (maxFairFor) · blackjackShoe (backward Fisher-Yates, 416 cards) · dealtHands — written from docs/ALGORITHM.md │ ├── bias.ts → the draw-bias bound Step 6 prints, on rng.ts’s own maxFairFor │ ├── config.ts → the rule set (RULES), paytables, phase plan, population constants, artifact pins, simulation shape │ ├── loader.ts → dataset loader + SHA-256 guard; deletes the capture’s own recomputation fields before any step runs │ ├── money.ts → integer 1e-8 settlement units for every money check │ ├── strategy.ts → the total-dependent basic-strategy table, shared by the solver and the simulation │ ├── exact-play.ts → the exact finite-shoe EV solver → outputs/exact-rtp.json (npm run rtp) │ ├── optimal-play.ts → infinite-deck engine (a regression anchor only) │ ├── round-engine.ts → the round engine the simulation passes play, bound to the exact solve │ ├── shuffle.ts · prng.ts → lazy Fisher-Yates and the seeded xoshiro128** stream of the Pass 0 reference shuffle │ ├── simulate.ts · replay.ts · replay-pool.ts · seed-derivation.ts → the three simulation passes from the pinned master seed (npm run simulate) │ ├── deep-replay.ts → re-deals all three passes and compares every published statistic │ ├── cherry-pick-attack.ts → the 50-candidate seed-grind experiment (npm run attack) │ ├── rng-branch-audit.ts → which branches of the draw the captured draws took (npm run branches) │ ├── calibration.ts → fresh-seed calibration of the Step 18 uniformity test (npm run calibrate) │ ├── sidebets.ts · sidebet-edges.ts → side-bet evaluators (Steps 15–16) and their exact edges │ ├── 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 one-setting RTP table and LIQD’s published claim │ ├── report-figures.ts → derived report figures, re-derived field for field by Step 25 │ ├── site-verifier.ts → loads the vendored ProvablyFair.org verifier for the Step 6 parity arm │ ├── capture-rig.ts → binds the shipped capture rig to the data (Step 7) │ ├── profile-map.ts → which Blackjack profile check (BLACKJACK-01 … BLACKJACK-30) each step implements │ └── domains.ts · diff.ts · stats.ts · types.ts → value domains, run-vs-committed diff, statistics, record types ├── anchor/ → oracle.py (the exact-rational tiny-shoe oracle) · policy.json · rules.json ├── docs/ │ ├── ALGORITHM.md → the written specification the RNG is built from │ └── INTERFACE.md → the module interface the RNG implements ├── verifier-parity/ → the ProvablyFair.org site verifier, vendored byte for byte and pinned (Step 6) ├── tests/ │ ├── verify.ts → 31-step verification runner (results under outputs/run/; PF_EMIT=1 to regenerate the committed record) │ ├── blackjack/ → 117 unit tests in eleven files — RNG specification and vectors, exact solver, oracle, Wizard of Odds anchor, round engine, side-bet edges, replay, anti-circularity, artifacts │ ├── mutations.json · mutate.ts → 86 declared mutations and their runner (npm run mutate) │ ├── fixtures/ → a frozen simulation file the replay tests check │ └── steps/ │ ├── commitment.ts → Steps 1–5: commit-reveal integrity, nonce window, epoch size │ ├── parity.ts → Steps 6–7: card recomputation (with the site-verifier arm and the bias census), client-seed dependence │ ├── payouts.ts → Steps 8–10: per-hand payouts, 3:2, insurance │ ├── rules.ts → Steps 11–14: S17, DAS, peek, no surrender │ ├── sidebets.ts → Steps 15–16: Perfect Pairs, 21+3 │ ├── simulation.ts → Steps 17–19: exact RTP vs simulation, Pass 1, Pass 2 │ ├── houseedge.ts → Steps 20–21: house-edge reconciliation (with the RTP claim rule), exchange-rate field │ ├── standardization.ts → Steps 22–31: stakes, side-bet config, phases, artifact hashes, Phase D, split and insurance rules, deal-time invariance, deck model, stake brackets │ └── hands.ts · context.ts → shared helpers ├── data/ │ └── blackjack-6000hands.json → 6,000 rounds · 120 epochs · 6 phases · SHA-256 pinned ├── outputs/ → the committed run; every run writes its own results under outputs/run/ │ ├── verification-results.json · report-figures.json │ ├── exact-rtp.json · rtp-claim-table.json │ ├── simulation-results.json · simulation-provenance.json · rtp-convergence.html │ ├── bootstrap-nulls.float64le · bootstrap-replay-report.json · deep-replay-report.json │ └── cherry-pick-attack.json · rng-branch-coverage.json ├── replay/ → Python replays: the Pass 2 bootstrap, the capture’s play, the served split order ├── evidence/ → E12, E13, E15, E16 screenshots, the E14 rendered reveal card, E18 insurance decline, E23 game settings, E24 client-code note, RTP page labels ├── capture/ → the capture rig (byte for byte), the sanitized capture reference, the insurance-decline recorder and checker ├── scripts/ → standalone build + check, release manifest + release bar, site-verifier check ├── .github/workflows/reproduce.yml → second-machine reproduction on a clean runner ├── test.js · verify.js → dependency-free bundles run by `npm test` ├── standalone-manifest.json → source, unit-test and bundle hashes ├── README.md · MANIFEST.md → the report (findings, evidence register, worked examples, limitations) ├── RELEASE-MANIFEST.json → release identity: tag, framework, artifact hashes, verdict, signing key └── package.json · package-lock.json · tsconfig.json · .mocharc.yml · .nvmrc · .gitattributes · .gitignore · hygiene-allow.json · LICENSE
Step-to-Section Cross-Reference31 verification steps mapped
| Step | Test Name | Report Section |
|---|---|---|
| 1 | Commitment Reveal | S1, S2 |
| 2 | Pre-Commitment Chain | S1 |
| 3 | Within-Epoch Hash Consistency | S1 |
| 4 | Nonce Continuity | S1 |
| 5 | Epoch Size | S1 |
| 6 | Recomputation Parity | S1, S2, S3, S6 |
| 7 | Client-Seed Dependence | S1, S2 |
| 8 | Per-Hand Payout (reconstructed from cards) | S3, S4 |
| 9 | Blackjack Pays 3:2 | S3, S4 |
| 10 | Insurance Pays 2:1 iff Dealer Blackjack | S3, S4 |
| 11 | Dealer Stands on Soft 17 (S17) | S3 |
| 12 | Double-After-Split Allowed (DAS) | S3 |
| 13 | Dealer Peeks for Blackjack | S3 |
| 14 | No Surrender Action Observed | S3 |
| 15 | Perfect Pairs Side Bet | S3, S4 |
| 16 | 21+3 Side Bet | S3, S4 |
| 17 | Basic-strategy RTP vs exact 8-deck solver | S4 |
| 18 | Simulation Pass 1 — first-card uniformity, serial independence, RTP vs the exact solve | S2, S4 |
| 19 | Simulation Pass 2 — cherry-pick detection (casino seeds) | S4 |
| 20 | House-edge reconciliation (nominal config vs actual) | S4 (also scores the RTP claim rule) |
| 21 | No FX haircut observed in this capture | S4 |
| 22 | Bet-Size Invariance | S2, S3, S4 |
| 23 | Side-Bet Config Completeness | S3 |
| 24 | Phase Coverage & Labels | S3 |
| 25 | Artifact Hash Integrity | S1, S2, S4, S7 |
| 26 | Phase D Client-Seed Variation | S1, S2, S3, S6 |
| 27 | Split Rules Verification | S3 |
| 28 | Insurance Offer Condition | S3 |
| 29 | Side-Bet Deal-Time Invariance | S3 |
| 30 | Deck-Model Confirmation | S2, S3 |
| 31 | Stake Bracket Bounds | S3, S4 |
Game Profile CoverageBlackjack profile 1.0.2 · 30 checks
Every check in the ProvablyFair.org Blackjack profile, version 1.0.2, is covered by a scored step, by a package check, or marked n/a with a reason — 23 by scored steps, 6 by package checks, 1 n/a. For BLACKJACK-09 the row states the exact scope it covers. The map ships in src/profile-map.ts and is written into each run’s verification results.
| Check | What it requires | Covered by | Coverage |
|---|---|---|---|
| BLACKJACK-01 | Every revealed seed hashes to its commitment | Step 1 | Full |
| BLACKJACK-02 | Next-seed pre-commitment chain, including the pre-capture link | Step 2 | Full |
| BLACKJACK-03 | One commitment and one client seed per epoch | Step 3 | Full |
| BLACKJACK-04 | Nonce window — no gaps, repeats or out-of-window nonces | Step 4 | Full |
| BLACKJACK-05 | Audited population bound to code constants — epochs, epoch size, hands, per-phase counts and stakes | Steps 5, 24, 31 | Full |
| BLACKJACK-06 | Client-seed influence, and the client-seed generator disclosed | Steps 7, 26 | Full |
| BLACKJACK-07 | Every captured hand recomputes card for card in strict draw order, split rounds included | Step 6 | Full |
| BLACKJACK-08 | Draw-bias bound computed on the implementation’s own draw, with the rejection census | Step 6 | Full |
| BLACKJACK-09 | Reference RNG provenance and known-input vectors | Package check: docs/ALGORITHM.md; rngSpecTests.ts and rngTests.ts in tests/blackjack/ | The specification’s five worked rounds, a real captured shoe and the validation vectors LIQD supplied with its specification; the RNG is our own implementation, written from the specification |
| BLACKJACK-10 | Rules replay — deal order, the action stream, the dealer’s S17 and peek | Steps 6, 11, 13 | Full |
| BLACKJACK-11 | Money path — every per-hand credit rebuilt from the cards, round credits reconciled in exact units | Step 8 | Full |
| BLACKJACK-12 | Bet-size invariance | Step 22 | Full |
| BLACKJACK-13 | Settlement — naturals 3:2, doubles, splits, pushes, insurance 2:1 | Steps 8, 9, 10 | Full |
| BLACKJACK-14 | Side bets — categories and paytables, every category exercised, fixed at the deal | Steps 15, 16, 23, 29 | Full |
| BLACKJACK-15 | Rule table and coverage floors — splits, ace splits, DAS, doubles, surrender, insurance offers, stakes | Steps 12, 14, 27, 28, 31 | Full |
| BLACKJACK-16 | Wallet balance ledger reconciliation | — | N/A — the capture carries no balance field, so wallet debits sit outside the dataset; the credit side is checked exactly in Step 8 |
| BLACKJACK-17 | Deck model — the finite 8-deck shoe confirmed from the data | Step 30 | Full — the hole card, hidden hand state and mid-round rotation are tested live in S5 (LIQD-BJ-EXTRA-001, LIQD-BJ-SEED-004) |
| BLACKJACK-18 | Exact RTP derived from the rules, anchored outside the solver | Step 17 | Full |
| BLACKJACK-19 | Published edge reconciled against the exact figure; the RTP claim rule; the exchange-rate field | Steps 20, 21 | Full |
| BLACKJACK-20 | Simulation Pass 1 — 1,000,000 real-shoe rounds, re-derived rather than trusted | Step 18 | Full |
| BLACKJACK-21 | Cherry-pick screen over every revealed seed, with its power stated | Step 19 | Full |
| BLACKJACK-22 | Live-sample fit (informational) | Step 30 | Full |
| BLACKJACK-23 | Framework fairness-integrity rows, kept as a package checklist — BJ-S5-01 … BJ-S5-18, each tied to a step or a limitation (Section 5 reports the live integrity tests) | Package check: MANIFEST.md matrix | Full |
| BLACKJACK-24 | Player-verifier parity — the ProvablyFair.org verifier, hash-matched and executed | Step 6 | Full |
| BLACKJACK-25 | Player verification path with a worked bet | Package check: MANIFEST.md Verify a bet | Full |
| BLACKJACK-26 | Dataset and rule set pinned before any step scores | Step 25 | Full |
| BLACKJACK-27 | Artifacts of record pinned and re-hashed; every numeric value inside a declared domain | Step 25 | Full |
| BLACKJACK-28 | Release gate — zero-install npm test, pinned runtime, field-by-field reproduction diff | Package check: package.json, .nvmrc, scripts/check-standalone.mjs, src/diff.ts | Full |
| BLACKJACK-29 | Mutation battery | Package check: tests/mutations.json, tests/mutate.ts | Full |
| BLACKJACK-30 | Limitations register and numbered evidence | Package check: MANIFEST.md (limitations register, evidence E01 … E24) | Full |
reproduce-audit.sh
All 31 scored steps and the committed simulation reproduce from release v1.0.1, the pinned artifacts and the commands above. The dataset hash is checked before any step runs, so you are testing the same 6,000 hands; the ten artifacts of record are pinned by SHA-256 and reconciled in Step 25 on every run, and npm run simulate deals the committed simulation again from its pinned master seed.