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

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

LIQD: Keno Audit

Keno

Provisional

Independent verification, backed by open-source code.

PF-2026-LQ06Audited October 2026 · Framework 1.1 · Release v1.1.0October 2026 · Framework 1.1 · v1.1.0
AUDIT RESULTPassedPassed 21/24 · Hard fails 0VERDICT: PROVABLY FAIR — Full Pass, as printed by npm test. Steps 7, 13 and 16 follow on LIQD’s production site.
Live Bets Verified
3,000
Parity Rate
100%
Simulated Rounds
10M
Simulated RTP 99.0143% against 98.9850% exact on the 40 Classic–High settings
Proven RTP (mean)
98.9867%
98.9031%–99.0689% per setting
21 / 24Verification steps passed

Verification pipeline

Real bets. Independently reproduced.

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

LIQD KENO$0.10 · Classic · 1 pick
HitPickedDrawn
Bet #ZBd-XDlqCy1XFgrFbQBOLNonce 0 · Phase A
60 real bets · Phase ABet 1 of 60
✓ Exact match
0 / 1 hits · 0×

The casino result, reproduced.

Casino outcome
0 of 1 hit · 0×
Our reproduction
0 of 1 hit · 0×
Recorded payout
$0.0000
Open source

Run this audit yourself

Open GitHub ↗

The repo is the credential. You don’t have to trust us — every scored check ships as code. Run npm test to re-run the full audit with no dependency install: 24 unit tests, the 24 verification steps and 3,000 live rounds re-verified — or run the two bundles directly with node test.js && node verify.js.

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

This audit independently validates the Keno game operated by LIQD under the ProvablyFair.org Audit Framework 1.1. We placed 3,000 real rounds across 50 seed epochs — all five risk levels at every pick count from 1 to 10, 60 rounds on each of the 50 settings — and recomputed every draw, every hit count and every credited amount with our own implementation of the algorithm, written from a specification without LIQD’s code. The capture ran on LIQD’s pre-launch environment, so the certificate is provisional.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPassPROVABLY FAIR — Full Pass · Passed 21/24 · Hard fails 0 · Steps 7, 13 and 16 will be completed on LIQD’s production site before certification (subsections 1.4, 3.8 and 4.7). Provisional: captured on LIQD’s pre-launch environment, qa.liqd.com
RTP VerifiedPass98.9031%–99.0689% Proven RTP per setting (mean 98.9867%) · 99.0143% simulated across the 40 Classic, Low, Medium and High settings in the 10M-round run · RTP claim rule: LIQD’s published 1.00% edge holds on all 50 settings, worst gap −0.096947 pp (Low/5) against the ±0.1 pp band (RTP-CLAIM 1.2.0, Step 18)
Live ↔︎ Verifier ParityPass100% — 3,000 / 3,000 draws reproduced in served order, with every hit count, multiplier and credited amount (Steps 6, 9, 11, 12) · LIQD’s own verifier, as a cross-check, returns the served round on 1,144 / 1,144 replies — every WTF round and every observed Classic–High paytable cell, 174 / 174 (Step 24)
Commit-Reveal SystemPass51 / 51 revealed seeds hash to their commitments; the next-seed chain is consistent on 51 / 51 rotations, including the pre-capture link into epoch 0; 3,000 / 3,000 rounds carry their epoch’s commitment and client seed (Steps 1–3)
Client SeedPassPlayer-controlled — each of the 50 epochs ran on a fresh client seed our capture set after LIQD had published the server commitment it pairs with (51 / 51 rotations); every draw recomputes with it, and a changed client seed changes the drawn numbers on 2,400 / 2,400 Classic–High rounds (Steps 2, 6; Step 7’s seed controls)
RNG AnalysisPassOur own implementation, written from the specification: HMAC-SHA256 keyed by the decoded server seed, one 32-bit value per cursor, a 10-step Fisher–Yates over 1–40 — worst-case modulo bias 2.070 × 10⁻¹⁰ per draw, and 0 of the 30,600 captured draw steps land where plain modulo and LIQD’s rejection rule could differ (Steps 6, 8)
Payout LogicPassAll 3,000 settlements verified — every served multiplier equals its declared paytable cell, ×10 on the 16 paying WTF rounds whose lightning number was drawn; every credit is floor(stake × cell) at the 8th decimal with zero tolerance, and 2,000 / 2,000 zero-cell rounds pay exactly 0 (Steps 11, 12, 14)
Anti-CircularityPassΣ P(k hits) × cell, with P(k hits) = C(n,k)·C(40−n,10−k)/C(40,10) from the board alone, gives all 50 settings as exact fractions — the closed form equals a draw-by-draw derivation cell for cell, and no operator odds or RTP field is read (Steps 17, 18)
Fairness IntegrityPass34 fairness integrity tests across 12 categories — 33 pass, 1 remediated (paytable return: corrected by LIQD; on the audited table all 50 settings meet the RTP claim rule, Step 18)
DeterminismPassFull reproducibility confirmed — 3,000 / 3,000 rounds recompute from the recorded inputs: the ten numbers from (server seed, client seed, nonce) — WTF one cursor later, after its lightning number — never from the picks or stake, and the payout from the draw, picks, risk and stake (Steps 6, 10, 12)
Overall StatusPass
PROVABLY FAIR — Full Pass · Passed 21/24 · Hard fails 0 · Steps 7, 13 and 16 will be completed on LIQD’s production site before certification (subsections 1.4, 3.8 and 4.7). Provisional: captured on LIQD’s pre-launch environment, qa.liqd.com
RTP VerifiedPass
98.9031%–99.0689% Proven RTP per setting (mean 98.9867%) · 99.0143% simulated across the 40 Classic, Low, Medium and High settings in the 10M-round run · RTP claim rule: LIQD’s published 1.00% edge holds on all 50 settings, worst gap −0.096947 pp (Low/5) against the ±0.1 pp band (RTP-CLAIM 1.2.0, Step 18)
Live ↔︎ Verifier ParityPass
100% — 3,000 / 3,000 draws reproduced in served order, with every hit count, multiplier and credited amount (Steps 6, 9, 11, 12) · LIQD’s own verifier, as a cross-check, returns the served round on 1,144 / 1,144 replies — every WTF round and every observed Classic–High paytable cell, 174 / 174 (Step 24)
Commit-Reveal SystemPass
51 / 51 revealed seeds hash to their commitments; the next-seed chain is consistent on 51 / 51 rotations, including the pre-capture link into epoch 0; 3,000 / 3,000 rounds carry their epoch’s commitment and client seed (Steps 1–3)
Client SeedPass
Player-controlled — each of the 50 epochs ran on a fresh client seed our capture set after LIQD had published the server commitment it pairs with (51 / 51 rotations); every draw recomputes with it, and a changed client seed changes the drawn numbers on 2,400 / 2,400 Classic–High rounds (Steps 2, 6; Step 7’s seed controls)
RNG AnalysisPass
Our own implementation, written from the specification: HMAC-SHA256 keyed by the decoded server seed, one 32-bit value per cursor, a 10-step Fisher–Yates over 1–40 — worst-case modulo bias 2.070 × 10⁻¹⁰ per draw, and 0 of the 30,600 captured draw steps land where plain modulo and LIQD’s rejection rule could differ (Steps 6, 8)
Payout LogicPass
All 3,000 settlements verified — every served multiplier equals its declared paytable cell, ×10 on the 16 paying WTF rounds whose lightning number was drawn; every credit is floor(stake × cell) at the 8th decimal with zero tolerance, and 2,000 / 2,000 zero-cell rounds pay exactly 0 (Steps 11, 12, 14)
Anti-CircularityPass
Σ P(k hits) × cell, with P(k hits) = C(n,k)·C(40−n,10−k)/C(40,10) from the board alone, gives all 50 settings as exact fractions — the closed form equals a draw-by-draw derivation cell for cell, and no operator odds or RTP field is read (Steps 17, 18)
Fairness IntegrityPass
34 fairness integrity tests across 12 categories — 33 pass, 1 remediated (paytable return: corrected by LIQD; on the audited table all 50 settings meet the RTP claim rule, Step 18)
DeterminismPass
Full reproducibility confirmed — 3,000 / 3,000 rounds recompute from the recorded inputs: the ten numbers from (server seed, client seed, nonce) — WTF one cursor later, after its lightning number — never from the picks or stake, and the payout from the draw, picks, risk and stake (Steps 6, 10, 12)
Audit scope & reference

What Was Audited

✓ Audited

  • The draw is deterministic: every round’s ten numbers reproduce, in order, 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 draw
  • Nonces run 0–59 in every seed epoch with no gaps or reuse
  • The draw is a 10-step Fisher–Yates over the 1–40 board, one HMAC-SHA256 value per step
  • Hits are recounted from the drawn numbers, and every served multiplier equals the declared paytable cell for its risk, picks and hits — 325 cells over 50 settings
  • Payouts settle as floor(stake × cell) at the 8th decimal; zero cells pay exactly 0
  • Proven RTP is exact for every setting — 98.9031% to 99.0689%, mean 98.9867%
  • LIQD’s published 1.00% edge holds on all 50 settings under the RTP claim rule (±0.1 pp)
  • WTF mode: the lightning number drawn first, and the ×10 payout when it is among the ten drawn
  • The numbers you pick and the amount you stake do not influence the draw
  • Players can independently verify every round

What Audit Excludes

  • Infrastructure or server security
  • Wallet, payments, or operational systems outside game logic
  • Cross-account sampling
What Audit Covers
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 server seed hashing, pre-bet commitment, next-seed pre-commitment, reveal on rotation
Client Seed OriginPlayer-controlled seed set after the server commitment — a fresh seed from our own CSPRNG in every one of the 50 epochs
Seed HandlingClient seed control, nonce lifecycle, seed epoch rotation
RNG AnalysisOur own HMAC-SHA256 implementation, written from the specification; LIQD’s verifier used as a cross-check; the 10-step Fisher–Yates draw, modulo-bias bound, the WTF lightning cursor
Payout LogicDeclared paytable cells (325 over 50 settings), served-vs-declared multipliers, the ×10 WTF lightning settlement, floor settlement at the 8th decimal, zero-cell rounds and wallet-field reconciliation
Live ParityIndependent draw, hit-count and payout recomputation vs live game results on all 3,000 rounds, in served draw order
RTP ValidationExact hypergeometric RTP for all 50 settings, the RTP claim rule, simulated RTP (10M rounds), cherry-pick test on the served rounds’ return
Fairness IntegrityIntegrity tests on LIQD’s running service — 34 across 12 categories: race conditions, input validation, atomic balance / idempotency, broken authorization, max-win bypass, nonce manipulation, RNG / PF verification, RNG / information leak, entropy / PF verification, precision / payout, event replay and adversarial seeds
What Audit Guarantees
  • Outcomes are deterministic and reproducible from the recorded inputs
  • Live game results match independent recomputation for the verified sample (3,000 / 3,000)
  • Draws are consistent with the uniform distribution expected from the HMAC-driven Fisher–Yates shuffle — 10,000,000 simulated rounds pass their uniformity screens, and the 3,000 captured draws fit the same law (informational)
  • RTP is proven analytically: Σ P(k hits) × cell, with the exact hypergeometric P(k hits) = C(n,k)·C(40−n,10−k)/C(40,10), gives every one of the 50 settings as an exact fraction
  • LIQD’s published 1.00% edge is met by every one of the 50 settings under the RTP claim rule — Proven RTP 98.9031% to 99.0689%, the closest at −0.096947 pp (Low/5)
  • Simulated RTP of 99.0143% across the 40 Classic, Low, Medium and High settings in a 10,000,000-round run — all 50 paytable rows converge within 5 standard errors of their exact values (largest |z| 1.73)
  • The draw is fixed by the seed pair and nonce before your picks are applied — every scored round’s ten numbers recompute from the seeds and nonce alone, with no pick or stake input, so the numbers you pick never change the numbers drawn
  • Client seed is a genuine, player-controlled input that materially influences results (a changed client seed changes the drawn numbers on 2,400 / 2,400 Classic–High rounds)
  • Every credit is exactly floor(stake × cell) on the declared paytable — 3,000 / 3,000 rounds, with ×10 on the 16 paying WTF rounds whose lightning number was drawn
  • Every fairness integrity test in Section 5 passed or was remediated and re-verified — no test altered, replayed or injected an outcome

References

Keno — Game Rules7 sections▶

How LIQD Keno plays, exactly as verified by this audit — the interactive board below is wired to the audited paytable and hit odds.

Game Rules

Keno draws 10 different numbers from a board of 40. Before the draw you pick 1 to 10 numbers and choose one of five risk levels — Classic, Low, Medium, High or WTF. Your hits are the picks that appear among the ten drawn numbers, and the paytable cell for your risk, pick count and hits is the multiplier. The ten numbers come from HMAC-SHA256 seeded by (serverSeed, clientSeed, nonce) — a 10-step Fisher–Yates shuffle of the board, fixed before your picks and stake are applied; your picks and stake decide which cell pays, never which numbers are drawn. WTF first draws one extra number, the lightning number: a paying WTF round pays 10 × its cell when the lightning number is among the ten drawn.

How to Play

1. Pick your numbers — choose 1 to 10 numbers on the 1–40 board; the pick count selects the paytable row.
2. Choose a risk level — Classic, Low, Medium, High or WTF. Each has its own row for every pick count, so the risk sets how the return is spread across hit counts.
3. Set the stake and play — the platform derives ten numbers from the committed server seed, your client seed and the current nonce (WTF draws its lightning number first).
4. Settle — your hits select the cell; the round credits floor(stake × cell) at the 8th decimal, × 10 on WTF when the lightning number is drawn. A zero cell pays 0. The nonce advances by 1.
5. Verify — after rotating your seed pair, recompute any round from the revealed seed (Section 6 walks it).

Draw-order note: LIQD serves the ten numbers in the order the shuffle draws them, and this audit compares them in that order — 3,000 / 3,000 rounds reproduce the served sequence exactly (Step 6), and LIQD’s own verifier returns the same sequence on 1,144 / 1,144 replies (Step 24).
Win Conditions

One rule for every risk level — hits = |picks ∩ drawn|, and the cell for (risk, picks, hits) is the multiplier. The chance of each hit count depends only on how many numbers you pick; for 3 picks, with the Classic row:

3 columns
HitsExact chanceClassic / 3 pays
0203/494 · 41.0931%0×
1435/988 · 44.0283%1×
2135/988 · 13.6640%3.1×
33/247 · 1.2146%10.4×
The hit count is not taken on trust: it is recounted from the seed-derived draw for every one of the 3,000 rounds in the dataset (Step 9), and every credited amount is rebuilt from the cell and stake alone (Step 12). A captured example — Classic, 5 picks {5, 40, 4, 33, 10}: drawn 33, 35, 8, 10, 36, 39, 30, 4, 26, 40 → 4 hits → 16.5× (bet E5tGJUmA…).
Risk vs Reward

Every risk level returns close to the same 99% — the risk changes the shape of the payouts, never the fairness of the price:

  • Classic — the gentlest spread: small multipliers from a single hit at low pick counts, top cell 100× (10 of 10). Proven RTP 98.9605%–99.0385%
  • Low — frequent small wins, a consolation cell on Low/1 (0.7× on 0 hits), top cells 1,000× at 9 and 10 picks. 98.9031%–99.0689%
  • Medium — fewer small wins and larger middle cells, a consolation cell on Medium/1 (0.4× on 0 hits), top cells 1,000×. 98.9236%–99.0316%
  • High — the return sits in the high-hit cells (3 picks pays only on 3 hits, 81.5×), top cells 1,000×. 98.9058%–99.0083%
  • WTF — a leaner table plus the lightning number: a paying round pays × 10 when the lightning number is drawn, on average 1 round in 4. 98.9625%–99.0398%; the largest possible multiple is 10 × 1,000× = 10,000× (WTF/10)
  • Variance changes with the risk and pick count; the price does not: every one of the 50 settings returns between 98.9031% and 99.0689%
Parameters
3 columns
ParameterValueNotes
Board1 – 4010 numbers drawn per round, all different, served in draw order
Picks1 – 10All ten pick counts captured — 300 rounds each
Risk levelsClassic · Low · Medium · High · WTFAll five captured — 600 rounds each
Settings505 risks × 10 pick counts, 60 rounds each in this capture; every one held to the RTP claim (S4.2)
Paytable325 cellsLIQD’s client table, read 16 October 2026 (E31); 218 cells reached in this capture
House edge0.9311% – 1.0969%Per setting; LIQD states a flat 1.00% (E26), met within ±0.1 pp on all 50
Seed epoch60 roundsNonces 0–59, then the pair rotates — the audit’s cadence, not a platform limit
Settlementfloor at 1e-8floor(stake × cell), × 10 on WTF lightning rounds — reproduced with zero tolerance
Stake captured$0.10All 3,000 rounds, in USDC
Seed Formats

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

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

The payout is a lookup — LIQD’s client paytable gives one multiplier per (risk, picks, hits) cell, and the chance of each hit count follows from the board alone:

P(k hits) = C(n, k) · C(40 − n, 10 − k) / C(40, 10)
m = table[risk][n][k]        × 10 on WTF when the lightning number is drawn
payout = floor(stake × m)    in 1e-8 units
3 columns
Classic rowHits → multiplierProven RTP
1 pick1 → 3.96×99.0000%
3 picks1 → 1× · 2 → 3.1× · 3 → 10.4×99.0182%
5 picks (featured round)1 → 0.25× · 2 → 1.4× · 3 → 4.1× · 4 → 16.5× · 5 → 36×98.9858%
10 picks3 → 1.4× · 4 → 2.25× · 5 → 4.5× · 6 → 8× · 7 → 17× · 8 → 50× · 9 → 80× · 10 → 100×99.0374%
The served multiplier equals the declared cell on 2,984 of the 3,000 rounds and exactly 10 × the cell on the 16 paying WTF rounds whose lightning number was drawn (Step 11). Every credit is floor(stake × multiplier) in 1e-8 units with zero tolerance (Step 12) — the featured round paid $0.10 × 16.5 = $1.65000000.
Why Provably Fair Matters▶

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

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

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

Checklist Reference

Commit-Reveal

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

Seed System

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

RNG & Parity

2 columns
CheckWhat it proves
Draw recomputes from (serverSeed, clientSeed, nonce)No hidden inputs influence the numbers drawn
Ten distinct numbers, in served order, from a forward Fisher–Yates over 1–40Every captured draw is the shuffle the specification describes
Hits recounted from our own draw on 3,000 / 3,000 roundsThe game counts your hits the way its rules say

Payout & RTP

2 columns
CheckWhat it proves
Every served multiplier equals its declared paytable cellThe table the game shows is the table that pays
Σ P(k hits) × cell in exact fractions for all 50 settingsThe RTP is an identity, not a sampled estimate
Closed-form hit law equals a draw-by-draw derivation, cell for cellThe probability side cannot share an arithmetic slip with the formula

Integrity

2 columns
CheckWhat it proves
Zero cells pay exactly 0No round settles money against a losing draw
The draw takes no pick set or stakeYour choices cannot reach the numbers drawn
Dataset, journal and paytable SHA-256 pinned (a mismatch stops the suite); results pins scored in Step 23The evidence itself is tamper-evident
High-Level Flow

One round of LIQD Keno, from commitment to settlement:

1. Commit — the server generates a 32-hex server seed and publishes SHA-256(utf8(seed)) as its commitment. You see only the hash.
2. Seed pair active — your client seed (platform default or your own) and a nonce starting at 0 pair with the committed seed.
3. Bet — you pick 1–10 numbers, a risk level and a stake. The ten numbers are already fixed by (serverSeed, clientSeed, nonce); WTF reads them one cursor later.
4. Draw — ten HMAC-SHA256 values drive a forward Fisher–Yates over 1–40; WTF first draws its lightning number on cursor 0 and takes the ten from cursors 1–10.
5. Settle — your hits select the paytable cell; the round credits floor(stake × cell), × 10 on WTF when the lightning number is drawn. The nonce increments by 1.
6. Rotate & verify — rotation reveals the outgoing seed (and pre-commits the next). Check the hash, recompute any round.

High-Level Flow — illustrated flow
Provably Fair Model

The model splits control of the randomness. The operator controls the server seed but must commit to it before play; the player controls the client seed and can change it after seeing the commitment; the nonce is a public counter. Because the ten numbers are a pure function of those three inputs (WTF reads them one cursor later, after its lightning number) — and the picks and stake enter only afterwards, as the rule and the price — neither side can steer an outcome without the other side's input, and everything needed to check the result is disclosed by design.

Commit-Reveal Model

Three phases, each independently verified by this audit:

Commit-Reveal Model — illustrated flow

Commit phase
The server generates a 32-hex seed and publishes SHA-256 of that string (UTF-8) as its commitment. The raw seed stays secret. Verified for all 51 reveals — the 50 epochs and the pre-capture anchor (Step 1).

Bet phase
Rounds settle under the committed pair; the nonce increments from 0 (0–59 in every epoch of this capture). The draw is fixed per (seed, clientSeed, nonce) before the picks are applied. Verified by full recomputation (Step 6).

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

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

Player-Controlled Client Seed

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

  • Set by the player whenever the seed pair rotates — our capture submitted its own at all 51 rotations, each after the server commitment it pairs with was published
  • Mixed into every HMAC message (clientSeed:nonce:cursor) — it mathematically cannot be ignored
  • Every one of the 50 epochs ran on a fresh client seed from our own CSPRNG, set after LIQD had published the server commitment it pairs with — all 3,000 rounds recomputed exactly
  • Sensitivity control: changing the client seed changed the drawn numbers on 2,400 / 2,400 recomputed Classic–High rounds (Step 7’s seed controls)
Nonce Lifecycle

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

  • Contiguous 0–59 in every one of the 50 epochs
  • 0 gaps, 0 duplicates, 0 resets across 3,000 rounds
  • Every rotation serves nonce 0 for the new pair — 51 / 51 (Step 2)
seed record · nonce window · Verified
"epoch": 7,"nonceStart": 0,"nonceEnd": 59,"hashedServerSeed": "5989c035b566e46540f217cd7aba6bf0…"
Determinism Guarantee

Determinism is what turns a fairness claim into a checkable fact: the same inputs must always produce the same draw. LIQD's derivation is a pure function of three arguments — verified by reproducing every captured draw, number for number and in order, with an independent implementation.

the pure function · Verified
computeDraw(serverSeed, clientSeed, nonce)  → [33, 35, 8, 10, 36, 39, 30, 4, 26, 40]   // ten numbers in draw order — nothing else in, nothing else out
Phase Map

The capture is engineered — Phase A places 60 rounds on every one of the 50 settings, one risk level per epoch and the pick count cycling 1–10 six times in every epoch:

3 columns
RiskRoundsWhat it isolates
Classic600Epochs 0–9 — every pick count on the base table, top cell 100×
Low600Epochs 10–19 — the frequent-win table, including the 0.7× consolation cell
Medium600Epochs 20–29 — larger middle cells, including the 0.4× consolation cell
High600Epochs 30–39 — the return concentrated in the high-hit cells
WTF600Epochs 40–49 — the lightning number, drawn on 125 rounds, and its × 10 settlement
Why the design matters: a random sample proves averages, but a designed capture proves mechanisms. Every setting gets the same 60 rounds, every epoch plays 58–60 distinct pick sets under one seed pair, and every risk is held to its own paytable row.
Technical Glossary7 categories▶

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

Core Concepts
2 columns
TermDefinition
Provably FairA cryptographic scheme in which the game outcome can be independently verified by the player after the fact, using seeds committed to before the round. The player cannot alter the server seed; the server cannot alter it after committing to its hash; neither party can change the draw without detection.
Commit-RevealThe mechanism underlying provably fair. The server publishes the hash of its seed (a commitment) before any round, then reveals the plaintext serverSeed after the seed rotates. The auditor checks SHA-256(utf8(serverSeed)) == commitment for every epoch. All 51 revealed seeds in this dataset — the 50 epochs and the pre-capture anchor — passed.
House EdgeThe operator’s cut. LIQD’s Keno page states “Edge: 1.00%” (E26), a flat 99% RTP. The effective edge is derived independently for each of the 50 settings from the exact hit probabilities and the declared paytable: 0.9311% – 1.0969%, every setting within 0.1 pp of the stated 1.00%.
RTP (Return to Player)1 − houseEdge. For LIQD Keno the RTP depends on the setting: Proven RTP 98.9031% – 99.0689% across the 50 settings, mean 98.9867% — exact fractions from the hit law and the declared paytable (Step 18), checked against 10M simulated rounds (Step 19). Live-capture return (136.94%) is informational only.
Anti-CircularityThe requirement that the RTP derivation and the simulation expectations take the hit probabilities from the board alone — C(n,k)·C(40−n,10−k)/C(40,10) — never from an operator-supplied odds or RTP field, so the RTP claim cannot validate itself (Step 17).
RTP Claim RuleRTP-CLAIM 1.2.0, part of the audit method: the RTP a casino publishes — on the game page, fairness page, help page, bet panel, rules or game settings — must hold at every setting to within ±0.1 percentage points. A house-side miss ends the verdict as RTP FIGURE OVERSTATED. LIQD’s published 1.00% edge holds on all 50 Keno settings; the closest is Low/5 at −0.096947 pp.
Seed System
2 columns
TermDefinition
Server SeedA 32-character (16-byte) hexadecimal string generated by LIQD. Kept secret during play, revealed after epoch rotation. Used as the HMAC key in its hex-decoded form: Buffer.from(serverSeed, 'hex').
Commitment / Server Seed HashSHA-256(utf8(serverSeed hex string)) — the text-hash convention: LIQD hashes the UTF-8 bytes of the 32-character hex string, not the 16 decoded bytes. Published before any round under the pair. A revealed seed that fails this check would prove tampering. Distinct from the HMAC key, which uses the decoded bytes.
Client SeedA string controlled by the player (or generated by the platform), mixed into the HMAC message as ${clientSeed}:${nonce}:${cursor}. Changing it changes every draw under the same server seed. When the player sets an unpredictable seed after the server-seed commitment is published, it is the primary structural defence against server-seed cherry-picking; a predictable seed does not deliver that protection.
NonceAn incrementing integer identifying a round's position within an epoch, starting at 0. Part of the HMAC message. Every epoch in this dataset ran contiguous nonces 0–59 (60 rounds) — no gaps.
EpochThe rounds played under one (serverSeed, clientSeed) pair. At epoch end the seed rotates and the previous server seed is revealed. This dataset covers 50 epochs of exactly 60 rounds (nonces 0–59), one risk level each.
Cryptographic Functions
2 columns
TermDefinition
HMAC-SHA256Hash-based Message Authentication Code using SHA-256: a deterministic 256-bit (32-byte) output from a key and a message. Keno makes one call per draw step; the first 4 bytes of each digest, read as a big-endian uint32, drive that step.
CursorThe third component of the HMAC message, one per draw step. Classic, Low, Medium and High draw on cursors 0–9; WTF spends cursor 0 on its lightning number and draws on cursors 1–10.
Fisher–Yates DrawA forward partial Fisher–Yates over the board [1 … 40]: at step i (0–9), j = i + (value mod (40 − i)) and positions i and j swap; the first ten positions, in order, are the drawn numbers — ten different numbers, every ordered draw equally likely up to the modulo bias below.
Modulo Biasvalue mod r for r = 40 … 31 favours the lowest 2³² mod r residues by at most 2.070 × 10⁻¹⁰ per draw. LIQD’s own code rejects that top tail instead (bias 0); the two rules can disagree only on a draw step that lands in the tail — 0 of the 30,600 draw steps in the scored rounds (Step 8).
Verification Terms
2 columns
TermDefinition
Paytable Celltable[risk][picks][hits] — the multiplier for one combination. The declared table has 325 cells across the 50 settings; the scored rounds reached 218 of them (Step 15).
Declared PaytableLIQD’s own client paytable, read from the Keno page code of qa.liqd.com on 16 October 2026, saved byte for byte and pinned by SHA-256 (E31). The scored capture names it, and every served multiplier equals its cell (Step 11).
Draw OrderThe order in which the ten numbers leave the shuffle. LIQD serves the drawn list in that order, and every comparison of a recomputed draw with a served round is order-sensitive — a stricter test than comparing sets.
Floor Settlementfloor(stake × multiplier) in exact integer arithmetic on 1e-8 units. Reproduces all 3,000 credited amounts with zero tolerance (Step 12).
Game Mechanics
2 columns
TermDefinition
PicksThe 1 to 10 numbers the player selects on the 1–40 board. The pick count and the risk choose the paytable row; the pick set itself never enters the draw — 58–60 distinct pick sets per epoch all recompute under one seed pair (Step 10).
Hits|picks ∩ drawn| — how many picks are among the ten drawn numbers. The served count equals the recount from our own draw on 3,000 / 3,000 rounds (Step 9).
Risk Level (mode)One of five paytables — Classic, Low, Medium, High and WTF — each with a row for every pick count. All five were captured at volume, 600 rounds each.
SettingA (risk, picks) pair: 5 × 10 = 50 settings, each with its own exact RTP. The capture placed 60 rounds on every setting (Step 15).
WTF & Lightning NumberWTF spends HMAC cursor 0 on a lightning number in 1–40, then draws the ten numbers from cursors 1–10. A paying WTF round pays 10 × its cell when the lightning number is among the ten drawn — on average 1 round in 4. In this capture it was drawn on 125 of the 600 WTF rounds; 16 of the 44 paying rounds paid × 10.
Consolation CellA cell that pays on 0 hits: Low/1 pays 0.7× and Medium/1 pays 0.4× when the single pick is not drawn (Step 14).
Audit Terms
2 columns
TermDefinition
Board UniformityA simulation test: how often each of the 40 numbers is drawn in a Pass 1 stream of 1,000,000 rounds, against an equal share, with a statistic corrected for drawing ten numbers without replacement (df 39). All 10 streams pass; the 3,000 captured draws give p = 0.7249 on the same test (Step 21).
Hit LawThe exact hypergeometric distribution of hits for a pick count, C(n,k)·C(40−n,10−k)/C(40,10). Each Pass 1 stream’s hit histogram is tested against it with χ² (smallest p = 0.0841); the captured rounds are fitted to it per pick count too (Step 21, informational).
Bonferroni CorrectionThe 30 Pass 1 screens — board, hit law and lag-1 on each of the 10 streams — each run at α/30 = 3.333 × 10⁻⁵, so the family has level α = 0.001. The smallest p-value of the 30 is 0.0288.
Served-Row ReturnThe Pass 2 cherry-pick statistic: each of the 40 Classic, Low, Medium and High epoch seeds is replayed for 10,000 nonces with its recorded client seed, and the return of the rounds LIQD actually served (nonces 0–59) is compared with a null that moves each window to later, never-served nonces (10,000 replicates). One-sided, failing below p = 0.001: p = 0.8754 on a fixed Classic/3 bet and p = 0.0711 on the served rows.
Cherry-Picking (Server-Seed)The residual risk in any beacon-free commit-reveal scheme: an operator with foreknowledge of the upcoming client seed could pre-evaluate candidate server seeds and commit to an unfavourable one. The structural defence is a client seed set after the commitment; the statistical check is the served-row return in Pass 2, which detects an 11.43 pp house-favouring shift on a fixed Classic/3 bet at 80% power.
Selective OrphaningA distinct residual channel: an operator observing a round server-side and orphaning or re-issuing an unfavourable result rather than surfacing it. In this capture every one of the 3,000 requests settled, with contiguous nonces in every epoch (Steps 4, 5); a signed server-side audit log closes the channel in general.
Hard FailAny FAIL on a scored step — a broken commitment or chain link, a draw, hit, multiplier or credit mismatch, an operator-side nonce gap, a population or artifact-integrity fault. A hard fail ends the verdict as NOT PROVABLY FAIR. None triggered: Passed 21/24, Hard fails 0.
Data Formats
2 columns
TermDefinition
drawnThe ten drawn numbers in draw order, from the settle response (outcome.drawn). Compared as an ordered list with our recomputation on all 3,000 rounds (Step 6).
lightningThe WTF lightning number, 1–40, served with the round’s outcome; null on every Classic, Low, Medium and High round. Recomputed from cursor 0 on 600 / 600 WTF rounds.
hits, multiplier, winningAmountThe served hit count, multiplier and credited amount. hits is recounted from our draw; multiplier equals the declared cell (× 10 on lightning rounds); winningAmount is floor(stake × multiplier) in 1e-8 units, 0 on a zero cell, and equals the wallet-denominated winningAmountWallet on all 3,000 rounds (Step 12).
Phase AThe scored capture: 3,000 rounds at $0.10 in 50 epochs of 60, one risk per epoch (Classic, Low, Medium, High and WTF, 10 epochs each), the pick count cycling 1–10 six times per epoch and each pick set chosen by our own CSPRNG.
01

Seed, Nonce & Determinism

Can the casino change your outcome after you bet?

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

Commit-Reveal Verification

51 / 51seeds verified

What we verified

  • Every revealed server seed hashes to the commitment in force before its reveal — 51 / 51, the 50 epochs and the pre-capture anchor
  • The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 51 / 51 rotations, including the pre-capture link into epoch 0
  • Client seed is player-controlled and set after the server commitment
  • Every epoch ran on a fresh client seed our capture generated and submitted at rotation — LIQD made the submitted seed active on 51 / 51 rotations
  • Nonce increments by 1 per round with no gaps or duplicates across 50 seed epochs — nonces 0–59 in every one
  • The ten numbers are fully determined by the seed inputs before your picks or stake are applied
  • Identical inputs always produce the same draw — confirmed across all 3,000 rounds, every number in served order
  • Your client seed is a genuine input — changing it changes the draw

What this means for you

  • With the commitment shown first, the casino cannot change the draw after you bet
  • You contribute randomness the server cannot predict or pre-select against
  • Every round has its own derivation — the nonce guarantees no two rounds share the same inputs
  • Any result can be independently verified using the public tools and repo
  • Outcomes stay verifiable months later — the revealed seeds and the algorithm are all you need
  • Server-seed cherry-picking is structurally contained — you can set or rotate your client seed after the commitment is published, so the operator cannot pre-evaluate seeds against it
51 / 51seeds verified
  • Every revealed server seed hashes to the commitment in force before its reveal — 51 / 51, the 50 epochs and the pre-capture anchor
  • The next server seed’s hash is pre-committed at every rotation — the chain is consistent on 51 / 51 rotations, including the pre-capture link into epoch 0
  • Client seed is player-controlled and set after the server commitment
  • Every epoch ran on a fresh client seed our capture generated and submitted at rotation — LIQD made the submitted seed active on 51 / 51 rotations
  • Nonce increments by 1 per round with no gaps or duplicates across 50 seed epochs — nonces 0–59 in every one
  • The ten numbers are fully determined by the seed inputs before your picks or stake are applied
  • Identical inputs always produce the same draw — confirmed across all 3,000 rounds, every number in served order
  • Your client seed is a genuine input — changing it changes the draw
HMAC-SHA256 Pipeline — seed inputs through ten HMAC draws to the 10 drawn numbers — illustrated flow
CheckResultEvidence
Seed hash integrityPass51 / 51 revealed seeds hash to their commitments — SHA-256 of the seed text, rebuilt from the raw rotation responses, 0 mismatches (Step 1)
Next-seed chainPassConsistent on 51 / 51 rotations, including the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPassAll 60 rounds in each of 50 epochs carry their epoch’s commitment and client seed — 3,000 / 3,000, no mid-epoch switching (Step 3)
Nonce sequencingPassNonces 0–59 in every epoch: 0 gaps, 0 duplicates; consumedNonce, the round record and the provablyFair block agree on 3,000 / 3,000 (Step 4)
Client seed originPassSet after the server commitment — a fresh “audit”-prefixed seed from our own CSPRNG in each of the 50 epochs, all 50 distinct, submitted after LIQD had published the server commitment it pairs with (51 / 51 rotations)
Client seed influencePassA wrong client seed (clientSeed + "x") or an empty one changes the drawn numbers on 2,400 / 2,400 Classic–High rounds — Step 7’s seed controls
Deterministic outputPassSame (serverSeed, clientSeed, nonce) always produces the same ten numbers — 3,000 / 3,000 drawn lists reproduced in served order (Step 6)
Draw and hit reproductionPass3,000 / 3,000 served drawn lists hold ten distinct numbers from 1 to 40, and the hits counted from our own draw equal the served hits on every round (Steps 6, 9)
Seed hash integrityPass
51 / 51 revealed seeds hash to their commitments — SHA-256 of the seed text, rebuilt from the raw rotation responses, 0 mismatches (Step 1)
Next-seed chainPass
Consistent on 51 / 51 rotations, including the pre-capture commitment that links into epoch 0 (Step 2)
One commitment per epochPass
All 60 rounds in each of 50 epochs carry their epoch’s commitment and client seed — 3,000 / 3,000, no mid-epoch switching (Step 3)
Nonce sequencingPass
Nonces 0–59 in every epoch: 0 gaps, 0 duplicates; consumedNonce, the round record and the provablyFair block agree on 3,000 / 3,000 (Step 4)
Client seed originPass
Set after the server commitment — a fresh “audit”-prefixed seed from our own CSPRNG in each of the 50 epochs, all 50 distinct, submitted after LIQD had published the server commitment it pairs with (51 / 51 rotations)
Client seed influencePass
A wrong client seed (clientSeed + "x") or an empty one changes the drawn numbers on 2,400 / 2,400 Classic–High rounds — Step 7’s seed controls
Deterministic outputPass
Same (serverSeed, clientSeed, nonce) always produces the same ten numbers — 3,000 / 3,000 drawn lists reproduced in served order (Step 6)
Draw and hit reproductionPass
3,000 / 3,000 served drawn lists hold ten distinct numbers from 1 to 40, and the hits counted from our own draw equal the served hits on every round (Steps 6, 9)
✓  Commit-reveal verified

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

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

Before any round is placed under a seed pair, LIQD shows the SHA-256 hash of a secret 32-character hex server seed as the active commitment (activeServerSeedHash; every round carries it as serverSeedHash). 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. Step 1 rebuilds every check from the raw rotation responses in the request journal, never from the verification fields the capture wrote itself.

Server Seed Commitment — illustrated flow
src/rng.ts · commitHash · Verified
/** * The published commitment for a server seed (spec §1.2): SHA-256 over the * seed's ASCII text (the 32 hex characters themselves), NOT over the 16 bytes * the hex encodes. Lowercase hex output. */export function commitHash(serverSeedHexString: string): string {  return sha256Buffer(Buffer.from(serverSeedHexString, "ascii"));}
Result: 51 / 51 revealed server seeds hash to the commitment in force before their reveal — the 50 epochs and the pre-capture anchor (Step 1). Zero mismatches.

Real Example from Live Data:

liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json · seed record (epoch 7)
{  "epoch": 7,  "phase": "A",  "clientSeed": "audit42a83c667988",  "hashedServerSeed": "5989c035b566e46540f217cd7aba6bf049c9dd393d1a1d1ceb7f0e5431a46793",  "nextHashedServerSeed": "56bcd099abb57c68a09d75124dca3b7c48616e4b1009752fa48e722e5b73e2c5",  "serverSeed": "92c36e93b39a5cde21d275e9747a5794",  "nonceStart": 0,  "nonceEnd": 59}

Verification:

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

Every rotation response publishes two hashes: the active commitment and nextServerSeedHash, the commitment of the seed that will follow. The nextServerSeedHash published before a rotation must equal the activeServerSeedHash after it, so the successor seed is fixed before the previous epoch ends. Step 2 rebuilds the chain from the request journal, in the order the responses arrived, and checks all 51 links — the 50 epoch rotations and the pre-capture anchor. The anchor: the active read at 13:04:04 UTC (commitment 98239c20…, next 6b556dcf…) and the rotation that revealed its seed both came before the first round at 13:04:18 UTC; the revealed seed hashes to that commitment, and its pre-committed next hash became epoch 0’s commitment. Every rotation response also serves nonce 0 for the new pair (51 / 51). A hash chain shows the values are consistent and fixes their order; it does not carry a date. Our capture records — the request journal, in the order the responses arrived — show each commitment was published before the rounds it covers.

tests/steps/primary.ts · Step 2 · Verified
export function p2One(v: V2Context, s: CaptureSpec): Part {  const L = chain(v), ds = v.ds, J = v.journal;  let linked = 0; const bad: string[] = [];  for (const l of L) { if (l.nextBefore !== null && l.nextBefore === l.after.active) linked++; else bad.push(`rotation ${l.k}`); }  const nonceZero = L.filter(l => l.after.nonce === s.nonceBase).length;  // The anchor: an active read BEFORE any bet, then a rotation that reveals the seed behind the commitment that read showed.  const firstBetAt = ds.bets.map(b => b.at).sort()[0];  const pre = J.pre;  const anchorRot = L[0];  const anchorBeforeBets = !!pre && pre.at < firstBetAt && !!anchorRot && anchorRot.at < firstBetAt && anchorRot.closes === 'pre';  const anchorCommit = !!pre && !!anchorRot && commitHash(anchorRot.revealed) === pre.raw.activeServerSeedHash;  const anchorLink = !!pre && !!anchorRot && pre.raw.nextServerSeedHash === anchorRot.after.active;  // …
Result: 51 / 51 next-seed links consistent, anchored before the first round. The pre-committed next server seed hash matched the promoted active hash at every rotation — the chain has no breaks.
1.3Hash Consistency Within an Epoch

Each round carries the commitment of the pair it was played under (serverSeedHash) and its client seed. Step 3 does not trust the capture’s own epoch tag: it places every round in the rotation interval its timestamp falls in, taken from the journal, and then checks that the round carries that interval’s commitment and client seed, that the raw provablyFair block of LIQD’s response says the same, and that the capture’s epoch tag agrees — 3,000 / 3,000 on every count. A second commitment inside one epoch would indicate a mid-epoch seed substitution — a critical commit-reveal violation. The capture never rotated while a round was in flight. What binds each round to its seed is the recomputation: the round’s commitment matches the reveal (Step 1), and the revealed seed reproduces every draw in the epoch (Step 6).

tests/steps/primary.ts · Step 3 · Verified
export function p3One(v: V2Context, s: CaptureSpec): Part {  const E = epochsFromChain(v), ds = v.ds;  let inEpoch = 0, csOk = 0, tagOk = 0, pfOk = 0; const bad: string[] = [];  for (const b of ds.bets) {    const e = E.find(x => x.openedAt <= b.at && (x.closedAt === null || b.at <= x.closedAt));    if (!e) { bad.push(`${b.betId} outside every rotation interval`); continue; }    if (b.serverSeedHash === e.active) inEpoch++; else bad.push(`${b.betId} hash ${b.serverSeedHash.slice(0, 8)} ≠ epoch ${e.index} ${e.active.slice(0, 8)}`);    if (b.clientSeed === e.clientSeed && b.roundClientSeed === e.clientSeed) csOk++; else bad.push(`${b.betId} client seed`);    if (b.epoch === e.index) tagOk++; else bad.push(`${b.betId} tagged epoch ${b.epoch}, timestamp says ${e.index}`);    if (b.raw?.provablyFair?.serverSeedHash === b.serverSeedHash && b.raw?.provablyFair?.clientSeed === b.clientSeed) pfOk++; else bad.push(`${b.betId} raw provablyFair`);  }  // …
Result: 50 / 50 epochs — every one of the 3,000 rounds carries its epoch’s commitment and client seed, and agrees with LIQD’s raw response. Zero mid-epoch changes detected.
1.4Client Seed Origin & Control

Players control their client seed: it is set when the seed pair rotates, after the server-seed commitment for the coming epoch is already published, and it enters every HMAC-SHA256 computation — changing it produces a completely different draw. The server commits to its seed before the client seed for that epoch is set, which structurally contains cherry-picking when that seed is unpredictable: an operator that cannot know the upcoming client seed cannot pre-evaluate candidate server seeds against it. In this capture every one of the 50 epochs used our own “audit”-prefixed seed — "audit" + crypto.randomBytes(6).toString("hex"), 48 bits of fresh CSPRNG material each — submitted in the rotation that opened the epoch, and LIQD made the submitted seed active on all 51 rotations (the pre-capture anchor and the one closing each epoch; Step 7’s seed controls). One example per risk level:

  • audit30c06ab27bf2 — epoch 0, Classic
  • audit974fa5e0e780 — epoch 10, Low
  • audit6a3912c79520 — epoch 20, Medium
  • audit332d56ae3677 — epoch 30, High
  • auditaee3840b3554 — epoch 40, WTF
Result: Client seeds are player-controlled and set after the commitment — 50 distinct CSPRNG seeds, one per epoch, and every round of every epoch recomputes under its seed (Step 6). The featured round’s pair uses audit42a83c667988 (epoch 7). Evidence: liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json
Production: The player-set client-seed rounds will be completed in full on LIQD’s production site before certification.
1.5Nonce Incrementation

The nonce begins at 0 and increments by 1 for each round under the same seed pair — one nonce per round. The nonce resets to 0 when the player rotates their seed, starting a new epoch. In this audit we rotated every 60 rounds as our chosen sampling cadence (nonces 0–59 per epoch, 50 epochs); epoch length is not enforced by the casino — players can rotate at any time. Step 4 audits every epoch for gaps, reuse, and resets: a skipped nonce is where a discarded unfavourable outcome would hide, so the sequence must be contiguous. The epoch size is bound to the constant in src/pins.ts rather than read from the dataset header, so a withheld round cannot be absorbed by shrinking the declared epoch. The round’s nonce must also equal the consumedNonce LIQD reports, the round record’s own nonce and the nonce in the provablyFair block — 3,000 / 3,000 — and every rotation serves nonce 0 for the new pair (Step 2).

Nonce Incrementation — illustrated flow
tests/steps/primary.ts · Step 4 · Verified
export function p4One(v: V2Context, s: CaptureSpec): Part {  const ds = v.ds;  const byHash = new Map<string, V2Bet[]>();  for (const b of ds.bets) (byHash.get(b.serverSeedHash) ?? byHash.set(b.serverSeedHash, []).get(b.serverSeedHash)!).push(b);  let exactWindows = 0, fieldsAgree = 0; const bad: string[] = [];  for (const [h, arr] of byHash) {    const ns = arr.map(b => b.nonce).sort((a, b) => a - b);    const want = Array.from({ length: s.epochSize }, (_, i) => s.nonceBase + i);    if (JSON.stringify(ns) === JSON.stringify(want)) exactWindows++; else bad.push(`${h.slice(0, 8)} nonces ${ns[0]}…${ns[ns.length - 1]} (${ns.length})`);  }  for (const b of ds.bets) {    if (b.nonce === b.consumedNonce && b.nonce === b.roundNonce && b.nonce === b.raw?.provablyFair?.nonce && b.nonce === b.raw?.consumedNonce) fieldsAgree++;    else bad.push(`${b.betId} nonce fields disagree`);  }  // …
Result: 0 gaps and 0 duplicates across all 50 epochs. Nonces run 0–59 contiguously in every epoch — 3,000 rounds in a complete, ordered ledger. Single client seed per epoch.
1.6Deterministic Mapping

The RNG algorithm is fully deterministic: given the same server seed, client seed and nonce, computeDraw always produces the exact same ten numbers in the same order. It is a forward Fisher–Yates over the board 1–40, stopped after ten swaps: at step i (0–9) the HMAC-SHA256 value of cursor i, taken modulo 40 − i, picks which of the remaining numbers moves into position i, and the first ten positions are the draw (WTF reads cursors 1–10, after its lightning number on cursor 0 — see 2.7). No timing, no picks, no stake, no UI state enters the derivation. Our implementation, written from the specification, reproduced all 3,000 captured drawn lists number for number, in served order. The round below is the featured round used throughout this report — recompute it yourself with the snippet.

Deterministic Mapping — illustrated flow
src/rng.ts · computeDraw · Verified
/** * Forward partial Fisher-Yates over 1..40 (spec §2.3), draw cursors starting * at c0: step i uses r = 40 - i and swaps a[i] with a[i + integer(c0+i, r)]. * Returns a[0..9] in that order (the served order). */function partialFisherYates(serverSeed: string, clientSeed: string, nonce: number, c0: number): number[] {  const a: number[] = Array.from({ length: GRID }, (_, k) => k + 1);  for (let i = 0; i < DRAWN; i++) {    const r = DRAW_RANGES[i];    const j = i + generateProvablyFairNumber(serverSeed, clientSeed, nonce, c0 + i, r);    const t = a[i];    a[i] = a[j];    a[j] = t;  }  return a.slice(0, DRAWN);} /** * Normal modes (classic/low/medium/high): no lightning, draw cursors start * at 0 (spec §2.2 else-branch, §2.3). Picks and stake never enter (spec §1.1). */export function computeDraw(serverSeed: string, clientSeed: string, nonce: number): number[] {  return partialFisherYates(serverSeed, clientSeed, nonce, 0);}
Result: All 3,000 rounds: our recomputation matches the served drawn list in order — 2,400 / 2,400 Classic, Low, Medium and High rounds under computeDraw and 600 / 600 WTF rounds under wtfDraw (Step 6). Zero mismatches, zero skipped. No external entropy is mixed into the Keno RNG.

Real Round Verified:

liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json · bet E5tGJUmA4g… (epoch 7, nonce 14) · Verified
{  "betId": "E5tGJUmA4gmbhcUreVar7",  "phase": "A",  "epoch": 7,  "nonce": 14,  "clientSeed": "audit42a83c667988",  "mode": "classic",  "picks": [5, 40, 4, 33, 10],  "stake": "0.10000000",  "drawn": [33, 35, 8, 10, 36, 39, 30, 4, 26, 40],  "hits": 4,  "multiplier": 16.5,  "winningAmount": "1.65000000"}

Verification:

verify-keno.js · Verified
// verify-keno.js — recompute a LIQD Keno draw from its seedsconst { createHmac } = require('node:crypto');function draw(seed, client, nonce, wtf = false) {  const value = cursor => createHmac('sha256', Buffer.from(seed, 'hex'))    .update(`${client}:${nonce}:${cursor}`).digest().readUInt32BE(0);  const lightning = wtf ? value(0) % 40 + 1 : null;          // WTF only: cursor 0  const board = Array.from({ length: 40 }, (_, k) => k + 1);  for (let i = 0; i < 10; i++) {                             // forward Fisher–Yates    const j = i + value((wtf ? 1 : 0) + i) % (40 - i);    [board[i], board[j]] = [board[j], board[i]];  }  return { drawn: board.slice(0, 10), lightning };}const { drawn } = draw('92c36e93b39a5cde21d275e9747a5794', 'audit42a83c667988', 14);console.log(drawn.join(', '));                                  // → 33, 35, 8, 10, 36, 39, 30, 4, 26, 40console.log([5, 40, 4, 33, 10].filter(p => drawn.includes(p)).length);  // → 4 hits
1.7Draw Reproduction, Hit Count & Client Seed Influence

Determinism covers the numbers; Steps 6 and 9 cover what LIQD did with them. Step 6 compares every served drawn list with our recomputation as an ordered list — 2,400 / 2,400 Classic, Low, Medium and High rounds under computeDraw, and 600 / 600 WTF rounds under the WTF rule, drawn list and lightning number together. Step 9 then checks each round itself: 3,000 / 3,000 served lists hold ten distinct numbers from 1 to 40, every pick set is legal, the served hits value equals |picks ∩ drawn|, and the hits counted from our own draw equal the served hits on every round; lightning is a number from 1 to 40 on every WTF round and null on every other. To confirm the client seed is a genuine input, Step 7 re-derives every Classic–High round with a wrong client seed (clientSeed + "x") and with an empty one. An unrelated draw repeats the same ten numbers about once in C(40,10) = 847,660,528 rounds, so each control’s gate is E + 5σ = 8.416 × 10⁻³ unchanged rounds; both controls leave 0 of 2,400 unchanged. The seed is demonstrably load-bearing.

tests/steps/primary.ts · Step 7 seed controls · Verified
  // The controls run on the scored classic / low / medium / high rounds: their draw is the established function.  const bets = S.bets.filter(b => !isWtf(b));  let wrongUnchanged = 0, emptyUnchanged = 0, reproduces = 0;  for (const b of bets) {    const ss = S.seedMap.get(b.serverSeedHash)!;    const own = computeDraw(ss, b.clientSeed, b.nonce);    if (sameDraw(own, b.drawn)) reproduces++;    if (sameSet(computeDraw(ss, b.clientSeed + 'x', b.nonce), own)) wrongUnchanged++;    if (sameSet(computeDraw(ss, '', b.nonce), own)) emptyUnchanged++;  }  const n = bets.length, E = n / Number(comb(GRID, DRAWN)), gate = E + 5 * Math.sqrt(E);
Result: 3,000 / 3,000 served drawn lists reproduced in order (Step 6); hits recounted from our own draw on 3,000 / 3,000 rounds (Step 9); a wrong or an empty client seed changes the drawn numbers on 2,400 / 2,400 rounds (Step 7’s seed controls). Client seed is a genuine, material input.
Technical Evidence & Verification5 sections
1.8Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Seed hash integrity (Step 1)51 / 51 revealed seeds hash-verified — 50 epochs and the pre-capture anchorPass
Next-seed pre-commitment chain (Step 2)51 / 51 rotations, including the pre-capture link into epoch 0Pass
Hash consistency (Step 3)50 / 50 epochs (60 rounds each); 3,000 / 3,000 rounds placed by timestampPass
Nonce continuity (Step 4)0 gaps, 0 duplicates across 50 epochs; nonce fields agree on 3,000 / 3,000Pass
Draw recomputation (Step 6)3,000 / 3,000 drawn lists in served order, with our own implementationPass
Draw structure & hit counting (Step 9)3,000 / 3,000 — ten distinct numbers from 1 to 40; hits recounted from our own drawPass
Client seed influence (Step 7’s seed controls)Wrong and empty client seeds change the draw on 2,400 / 2,400 roundsPass
1.9Code References7 source files
2 columns
FilePurpose
tests/verify.ts24-step verification pipeline (Steps 1–4, 6 and 9, with Step 7’s seed controls, cover S1)
tests/steps/commitment.tsSteps 1–5: commit-reveal integrity, nonce continuity and population binding
tests/steps/determinism.tsSteps 6–10: draw recomputation, client-seed controls, bias bound, structure & hits, pick-set independence
tests/steps/primary.tsThe capture-scoring part of each step — the chain rebuilt from the journal’s raw rotation responses
src/rng.tsHMAC-SHA256 draw (commitHash, computeDraw, wtfDraw, countHits) — written from docs/ALGORITHM.md
src/loader.tsDataset, journal and paytable loading + SHA-256 hash guard
capture/capture-keno-v2.mjsThe capture script: pre-capture anchor, 60-round epochs, CSPRNG client seeds, a journal of every raw response; the session cookie is never written
1.10Datasets UsedSource artifacts
Primary: data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json
2 columns
PropertyValue
SourceLive Keno game data from LIQD’s pre-launch environment (qa.liqd.com) — 3,000 real rounds, all five risk levels, USDC
Capture environmentPre-launch host qa.liqd.com · one authenticated account · one session
Total Records3,000 rounds across 50 epochs (60 rounds each)
Captured20 October 2026 (13:04–13:32 UTC)
SHA-2567744010540dedbc9fe996a5cf9a75d4ee4a6007b08e56c2e7b9b9053b9831b1e
Request journal6,103 records — a header record, the pre-capture read, 51 raw rotation responses, 50 epoch opens, 3,000 intents and 3,000 settlements · SHA-256 8f52aceba87bba7a86e9d3651e9f3a96b6597736eb3294f69cb44aaa9f30db76

Fields used: serverSeed, hashedServerSeed, nextHashedServerSeed, serverSeedHash, clientSeed, nonce, consumedNonce, mode, picks, stake, drawn, hits, multiplier, winningAmount, winningAmountWallet, and from each raw response the provablyFair block and outcome.lightning; from the journal, every rotation’s activeServerSeedHash, nextServerSeedHash, revealedServerSeed, clientSeed and nonce

src/loader.ts recomputes the SHA-256 of the capture, its journal and the declared paytable, and aborts the suite before any step runs on a mismatch (Step 22). Every computational claim downstream of the dataset is reproducible from the file itself.

1.11Verified Invariants10 invariants
2 columns
InvariantResult
SHA-256(utf8(serverSeed)) = the commitment in force before its reveal, for all 51 revealed seedsPass
Next-seed pre-commitment chain intact on all 51 rotations, anchored before the first roundPass
Every round carries its epoch’s commitment and client seed — 3,000 / 3,000, placed by timestampPass
Zero nonce gaps within any epoch (contiguous 0–59)Pass
Zero nonce duplicates within any epochPass
Every epoch contains exactly 60 rounds (50 / 50), bound to the code constant, not the dataset headerPass
Every rotation serves nonce 0 for the new pair (51 / 51)Pass
Same inputs produce the same ten numbers in the same order for all 3,000 roundsPass
Hits counted from our own draw equal the served hits on all 3,000 roundsPass
A wrong or an empty client seed changes the drawn numbers on 2,400 / 2,400 Classic–High roundsPass
1.12Reproduction InstructionsRun locally

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

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

  [PASS] Step 1: Seed hash integrity
  [PASS] Step 2: Forward commitment chain
  [PASS] Step 3: Hash consistency within epoch
  [PASS] Step 4: Nonce continuity
  [PASS] Step 6: Outcome recomputation (drawn list)
  [PASS] Step 9: Draw structure & hit counting
02

RNG & Entropy Model

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

This section verifies that LIQD’s Keno random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. Each round makes ten HMAC-SHA256 draws — one per cursor, the first 4 bytes of each digest taken modulo the count of numbers still on the board — and a forward Fisher–Yates shuffle turns them into ten distinct numbers from 1 to 40; WTF adds one more draw, its lightning number. The implementation is our own, written from a specification without LIQD’s code; it reproduces all 3,000 live draws, and LIQD’s own verifier serves only as a cross-check.

Cryptographic Randomness Verification

3,000 / 3,000captured draws reproduced

What we verified

  • HMAC-SHA256 produces cryptographically sound, unpredictable output for each draw step
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • The modulo step’s bias is at most 2.070 × 10⁻¹⁰ per draw, and no captured round came near the values where it could matter
  • The RNG is our own implementation, written from the specification — it reproduces all 3,000 captured draws, in order
  • Board uniformity, the hit law and serial independence pass their screens across 10M simulated rounds
  • A changed client seed changes the draw — 2,400 / 2,400 rounds under both seed controls

What this means for you

  • Each draw is generated fairly and cannot be skewed
  • All 40 numbers are equally likely to be drawn — no number is favoured
  • No hidden randomness or server-side tricks influence the draw
  • Consecutive rounds are not correlated — past results don’t affect future outcomes
  • The algorithm depends only on seeds you can verify
3,000 / 3,000captured draws reproduced
  • HMAC-SHA256 produces cryptographically sound, unpredictable output for each draw step
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • The modulo step’s bias is at most 2.070 × 10⁻¹⁰ per draw, and no captured round came near the values where it could matter
  • The RNG is our own implementation, written from the specification — it reproduces all 3,000 captured draws, in order
  • Board uniformity, the hit law and serial independence pass their screens across 10M simulated rounds
  • A changed client seed changes the draw — 2,400 / 2,400 rounds under both seed controls
RNG Pipeline — HMAC-SHA256 → uint32 → mod (40 − i) → Fisher–Yates swap → drawn number — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — one value per draw step, ten per round (eleven on WTF), no hidden entropy
Entropy purityPassNo timestamps, external APIs, Math.random, or server-side state
Our own implementationPassWritten from the specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 96 specification checks pass and it reproduces all 3,000 draws in served order (Step 6)
LIQD verifier cross-checkPassLIQD’s verify endpoint, queried as-is, returns the served drawn list in order on 1,144 / 1,144 scored rounds — every WTF round and every observed Classic–High paytable cell (Step 24) — a cross-check, not the source
Draw bias boundPassPlain modulo on ranges 40…31: worst bias 2.070 × 10⁻¹⁰ per draw, below 2⁻³²; 0 of the 30,600 captured draw steps fall where LIQD’s rejection rule would draw differently (Step 8)
Key encoding verifiedPassServer seed hex-decoded to bytes for the HMAC key, confirmed by 3,000 / 3,000 recomputed rounds (Step 6); the commitment hashes the seed text, confirmed by 51 / 51 reveals (Step 1)
Serial independencePassLag-1 correlation of hit counts in each 1,000,000-round stream: |ρ| ≤ 0.0018, smallest p = 0.0724 — clear of the 3.333 × 10⁻⁵ gate on all 10 streams (Step 19)
Client seed influencePassA wrong client seed or an empty one changes the drawn numbers on 2,400 / 2,400 Classic–High rounds — Step 7’s seed controls
RNG derived only from disclosed inputsPass
HMAC-SHA256(serverSeed, clientSeed:nonce:cursor) — one value per draw step, ten per round (eleven on WTF), no hidden entropy
Entropy purityPass
No timestamps, external APIs, Math.random, or server-side state
Our own implementationPass
Written from the specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code; its 96 specification checks pass and it reproduces all 3,000 draws in served order (Step 6)
LIQD verifier cross-checkPass
LIQD’s verify endpoint, queried as-is, returns the served drawn list in order on 1,144 / 1,144 scored rounds — every WTF round and every observed Classic–High paytable cell (Step 24) — a cross-check, not the source
Draw bias boundPass
Plain modulo on ranges 40…31: worst bias 2.070 × 10⁻¹⁰ per draw, below 2⁻³²; 0 of the 30,600 captured draw steps fall where LIQD’s rejection rule would draw differently (Step 8)
Key encoding verifiedPass
Server seed hex-decoded to bytes for the HMAC key, confirmed by 3,000 / 3,000 recomputed rounds (Step 6); the commitment hashes the seed text, confirmed by 51 / 51 reveals (Step 1)
Serial independencePass
Lag-1 correlation of hit counts in each 1,000,000-round stream: |ρ| ≤ 0.0018, smallest p = 0.0724 — clear of the 3.333 × 10⁻⁵ gate on all 10 streams (Step 19)
Client seed influencePass
A wrong client seed or an empty one changes the drawn numbers on 2,400 / 2,400 Classic–High rounds — Step 7’s seed controls
✓  Unbiased and Cryptographically Sound

The Keno RNG uses only the disclosed inputs, and our own implementation — written from the specification — reproduces every captured draw, number for number and in order. The simulated draws are consistent with a uniform board and the exact hit law across 10,000,000 rounds, and the serial screen finds no sign of dependence in any stream. The client seed is a genuine input — a changed seed alters the draw on every control round.

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

Each Keno round makes ten draws, one per HMAC cursor. An HMAC-SHA256 digest is computed with the hex-decoded server seed as key and clientSeed:nonce:cursor as the message; its first 4 bytes, read as a big-endian unsigned integer, give value(cursor), and value mod r picks a position among the r numbers still on the board. Cursor i (0–9) draws with r = 40 − i, so the ranges run 40, 39, … 31, and a forward Fisher–Yates swap moves the chosen number into position i; the first ten positions, in order, are the drawn list. WTF spends cursor 0 on its lightning number and draws on cursors 1–10 (2.7). The picks and stake never enter this function — they are applied to the drawn list afterwards.

2 columns
ComponentDetail
Hash functionHMAC-SHA256 (32-byte digest)
Keyhex_decode(serverSeed) — 16 raw bytes (distinct from the commitment domain, which hashes the seed text)
MessageclientSeed:nonce:cursor — UTF-8, literal colons, no padding
Cursor scheduleCursors 0–9 on Classic, Low, Medium and High; on WTF cursor 0 is the lightning number and the draw uses cursors 1–10
ExtractionFirst 4 digest bytes → readUInt32BE → uint32; the specification reads no further bytes, and LIQD’s code takes the next 4 only on a rejection (2.3)
Reductionuint32 mod r with r = 40 − i — ranges 40, 39, … 31 (and 40 for the lightning number)
Board[1 … 40]; step i swaps position i with position i + (uint32 mod r)
Output orderPositions 0–9 after the ten swaps, in that order — the served drawn list, compared as an ordered list
OutputTen distinct numbers per round (plus the lightning number on WTF) — the picks are compared with them afterwards
src/rng.ts · cursorValue, generateProvablyFairNumber · Verified
const U32_SPACE = 0x1_0000_0000; // 2^32// …/** * value(cursor) (spec §2.1): HMAC-SHA256 keyed by the decoded seed over the * UTF-8 message, first 4 digest bytes read as an unsigned big-endian 32-bit * integer. The remaining 28 bytes are unused. */function cursorValue(serverSeed: string, clientSeed: string, nonce: number, cursor: number): number {  const digest = createHmac("sha256", hmacKey(serverSeed))    .update(Buffer.from(hmacMessage(clientSeed, nonce, cursor), "utf8"))    .digest();  return digest.readUInt32BE(0);}// …export function maxFairFor(range: number): number {  assertRange(range);  return U32_SPACE;} /** * integer(cursor, range) (spec §2.1): value(cursor) mod range, in [0, range). * One chunk per cursor; under the spec's rule it is always accepted, so the * "throw if none accepted" path is unreachable but kept as a guard. */export function generateProvablyFairNumber(  serverSeed: string,  clientSeed: string,  nonce: number,  cursor: number,  range: number,): number {  assertRange(range);  const v = cursorValue(serverSeed, clientSeed, nonce, cursor);  if (v >= maxFairFor(range)) {    throw new Error(`no accepted value for cursor ${cursor}, range ${range}`);  }  return v % range;}
Result: Our implementation matches all 3,000 live rounds with zero draw mismatches, in served order (Step 6). It was written from a specification (docs/ALGORITHM.md) by an implementer who never opened LIQD’s code, its client bundle or any port of it, and the specification’s 96 vector and property checks pass (tests/keno/rngSpecTests.ts). LIQD’s own verify endpoint is queried as-is and used only as a cross-check — it returns the served drawn list on 1,144 / 1,144 scored rounds (Step 24).
2.2Entropy Sources

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

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness — the HMAC key, committed via SHA-256 hash of the seed text before play
Client SeedPlayerPlayer-contributed entropy — mixed into every HMAC message
NonceSystemUniqueness per round (0–59 within each epoch, audited gap-free)
CursorSystemThe draw step: 0–9, or 0 for the lightning number and 1–10 for the draw on WTF. An index, not an entropy source
No mixed entropy sources detected. Run the same inputs multiple times — results are always identical. Timing, picks, stake, balance, and session state are all absent from the derivation. 3,000 / 3,000 confirmed.

How we know: 3,000 / 3,000 rounds were recomputed using only the declared inputs. If any hidden entropy source existed, recomputation would fail. It does not.

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

2.3Modulo Bias Analysis

Each draw step takes uint32 mod r for r from 40 down to 31. Modulo bias occurs because 2^32 is not evenly divisible by most of these ranges — for r = 40 the 32-bit space holds 107,374,182 complete cycles plus a remainder of 16 values, so the residues 0–15 are each reachable one extra time. Our specification takes plain modulo, which leaves a bias far below any measurable level; LIQD’s own code instead rejects any value at or above floor(2^32 / r) × r and takes the next 4 bytes, which makes the bias exactly 0. The two rules can draw differently only when a step’s value lands in that top zone:

range r   2^32 mod r   worst bias per draw (plain mod)
  40         16          1.397e-10
  39         22          1.313e-10
  38          6          1.961e-10
  37          7          1.888e-10
  36          4          2.070e-10   ← worst
  35         11          1.597e-10
  34         18          1.233e-10
  33          4          2.046e-10
  32          0          0 (exact)
  31          4          2.028e-10
  40         16          1.397e-10   ← WTF lightning, cursor 0
  2^-32 for comparison   2.328e-10

expected zone steps  = 92 / 2^32 ≈ 2.142e-8 per round (108 / 2^32 ≈ 2.515e-8 on WTF)
a different round    ≈ 1 in 4.8 × 10^7 (1 in 4.1 × 10^7 on WTF)
zone steps observed  = 0 of 30,600 (24,000 Classic–High + 6,600 WTF incl. the lightning step)
Result: Worst-case bias 2.070 × 10⁻¹⁰ per draw — below 2⁻³² on every range — and LIQD’s rejection rule removes even that. No captured draw step fell in the zone where the two rules differ, so every scored round is the same under both (Step 8).

The bound is computed in exact integer arithmetic from the ranges the draw uses (src/bias.ts), so the figures describe the code that produced the draws: every range’s worst bias stays below 2⁻³², and Step 8 fails the audit if one does not. Step 8 also counts, over every scored round, the steps whose value falls in the zone where plain modulo and LIQD’s rejection rule part ways: 0 of 24,000 Classic–High steps (expected 5.141 × 10⁻⁵) and 0 of 6,600 WTF steps including the lightning step (expected 1.509 × 10⁻⁵). With no zone step in the capture, the two rules give the same draw on every scored round.

2.4RNG Isolation & Pick-Set Independence

Each round uses a unique nonce in the HMAC message (clientSeed:nonce:cursor), ensuring per-round outputs are cryptographically independent. HMAC-SHA256 is a pseudorandom function — knowing one round’s draw gives no usable information about any other round under the same seed pair. There is no shared state between rounds or between epochs, and the picks never meet the RNG: the ten numbers are produced first, in isolation, and the picks are compared with them afterwards. WTF is the one risk level that changes the cursor schedule — it spends cursor 0 on its lightning number, so a WTF round draws from cursors 1–10 — but even there the pick set and the stake never enter.

RNG Isolation & Pick-Set Independence — illustrated flow
Result: RNG isolation confirmed. computeDraw takes three arguments and names no pick, risk or stake field; 40 / 40 Classic–High epochs changed the pick set from round to round under one seed pair (58–60 distinct pick sets per epoch), and all 2,400 of their rounds recompute by the same picks-free function (Step 10).

Evidence: The computeDraw implementation in 1.6 confirms this — it is a pure function with no class state, no external calls, and no cross-round memory. It takes exactly three arguments (serverSeed, clientSeed, nonce) — no picks, no risk, no stake, no timestamp, no account identifier — and returns ten numbers. Same inputs always produce the same output; if any hidden input influenced the draw, the reference implementation (which lacks that input) could not reproduce 3,000 / 3,000 rounds. The strongest in-capture evidence: every epoch changes the pick count and the pick set from round to round — 58–60 distinct pick sets per epoch under one seed pair — and every draw still recomputes.

2.5Monte Carlo Simulation (10M Rounds)

A 10,000,000-round Monte Carlo simulation checks the algorithm’s distribution at scale. It runs ten streams of 1,000,000 rounds, one per pick count (pick set 1…n), on seed pairs derived from a pinned master seed — independent of LIQD’s seeds — so the whole experiment replays byte for byte: npm run test:full regenerates the committed artifact and compares it field by field, and every verify replays the first 50,000 rounds of each stream exactly. computeDraw takes no pick, risk or stake argument (Step 10), so the four standard risk levels of a pick count share one stream’s draws, and WTF’s draw — the same shuffle, one cursor later — follows the same hit law: each stream prices every risk level’s row, WTF’s before its × 10. Each stream records integer counts only — how often each of the 40 numbers was drawn, the hit histogram and the sums for the serial screen — and Step 19 re-derives every statistic from those integers.

2 columns
MetricValue
Rounds10,000,000 — 10 streams × 1,000,000, one per pick count
SeedsOur own, from the pinned master seed b5f1c0de6b656e6f2d706173732d3120 — replayable byte for byte
Replay on every verifyThe first 50,000 rounds of all 10 streams reproduce the recorded census exactly
Board uniformity (df 39)10 / 10 streams pass; smallest p = 0.0288 (1 pick)
Hit law (χ², exact probabilities)10 / 10 streams pass; smallest p = 0.0841 (1 pick)
Serial screen (lag-1 of hit counts)10 / 10 streams pass; |ρ| ≤ 0.0018, smallest p = 0.0724
4 columns
PicksBoard uniformity pHit law χ² pLag-1 ρ (p)
10.02880.0841−0.00180 (0.0724)
20.64420.85310.00103 (0.3009)
30.22410.9181−0.00067 (0.5042)
40.69050.23100.00035 (0.7275)
50.34090.21750.00064 (0.5221)
60.29440.7791−0.00118 (0.2382)
70.60260.93340.00005 (0.9564)
80.48110.5592−0.00065 (0.5129)
90.66770.5674−0.00099 (0.3202)
100.09110.93880.00075 (0.4508)
src/sim.ts · Pass 1 parameters · Verified
export const PASS1_MASTER_SEED = 'b5f1c0de6b656e6f2d706173732d3120';export const PASS1_ROUNDS = 1_000_000;export const PASS1_PREFIX = 50_000;export const PASS1_PICK_COUNTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];// …export function pass1Seeds(picks: number): { serverSeed: string; clientSeed: string } {  return { serverSeed: deriveSeedHex(PASS1_MASTER_SEED, 'pass1-server', picks), clientSeed: deriveSeedHex(PASS1_MASTER_SEED, 'pass1-client', picks) };}
Result: 10M rounds simulated. Board uniformity, the hit law and the serial screen pass on all ten streams at the Bonferroni-corrected level; the smallest p-value of the thirty is 0.0288 against a gate of 3.333 × 10⁻⁵. The results are consistent with a uniform draw of ten distinct numbers from 1 to 40 (Step 19).

All thirty screens pass. Each stream gets three tests — board uniformity (the 40 number counts against an equal share, with a statistic corrected for drawing ten numbers without replacement, df 39), the hit law (the hit histogram against the exact hypergeometric probabilities, χ²) and the lag-1 correlation of consecutive hit counts — each judged against α / 30 = 3.333 × 10⁻⁵, so the family of 30 has level α = 0.001. The smallest of the 30 p-values is 0.0288.

Methodology: a Keno round draws ten numbers without replacement, so the 40 counts are negatively correlated and the plain χ² has mean 40 − 10 = 30 rather than 39; multiplying it by (40 − 1) / (40 − 10) restores the χ²(39) reference. The hit law is the direct test of what the player experiences — how many of their picks come up — and it is checked against exact probabilities, never against an estimate. The RTP convergence of every paytable row, priced from the same streams, is reported in S4.5.

2.6Serial Independence

Serial independence ensures consecutive rounds are not correlated — a round with many hits does not make the next one more or less likely to hit. The screen runs on every Pass 1 stream, and the 3,000 captured rounds are fitted as well, informationally:

Lag-1 autocorrelation: The correlation between consecutive hit counts in each 1,000,000-round stream, computed from the integer sums the stream records (Σh, Σh², Σh_t·h_{t+1}), standardised as z = ρ · √n and judged two-sided at α / 30. All ten streams clear it: |ρ| ≤ 0.0018, smallest p = 0.0724 (Step 19).

Live-sample fit (informational): The 3,000 captured draws against a uniform board give corrected T = 33.346, df 39, p = 0.7249; the served hit counts fit the exact law at every pick count (p from 0.2100 to 0.9034, 300 rounds each); the WTF lightning number is uniform over 1–40 (χ² = 42.933, df 39, p = 0.3064) and lands in the drawn list on 125 of 600 rounds, against 1 in 4 expected (Step 21). These fits are reported for information rather than scored; the draw itself is proven round by round in Step 6.

src/stats.ts · lag1 · Verified
/** Lag-1 correlation from integer sums: n, Σx, Σx², Σ x_t·x_{t+1} (n−1 pairs). */export function lag1(n: number, sx: number, sxx: number, sxy: number): number {  const mean = sx / n, v = sxx / n - mean * mean;  if (v <= 0 || n < 3) return 0;  return (sxy / (n - 1) - mean * mean) / v;}
Result: The lag-1 screen clears on all ten streams (smallest p = 0.0724 against 3.333 × 10⁻⁵), and the live sample fits the uniform board, the exact hit law and a uniform lightning number. No sign of serial dependence (Steps 19, 21).
2.7WTF Draw — the Lightning Number

WTF is the one risk level with an extra draw. Before the ten numbers, cursor 0 draws the lightning number from all 40: lightning = value(0) mod 40 + 1. The ten numbers then come from the same forward Fisher–Yates on cursors 1–10. Because the lightning number has its own cursor, it is independent of the draw and lands among the ten drawn numbers with probability 10 / 40 — the basis of WTF’s × 10 settlement in S4.8. LIQD publishes no written WTF specification, so our rule is checked two ways: it reproduces the served drawn list in order and the served lightning number on 600 / 600 scored WTF rounds, where the Classic schedule (cursors 0–9) reproduces none of them, and LIQD’s own verifier, asked about every one of the 600 WTF rounds, returns the same drawn list and lightning number on all 600 (Step 24). The capture’s largest win, traced:

bet            f2t0JeOrZXSsgu0xyMvoQ · WTF · epoch 46, nonce 6
message        audit14226566ad31:6:0
cursor 0       first 4 bytes d5d73553 → uint32 3,587,650,899
               3,587,650,899 mod 40 = 19 → lightning = 20
cursors 1–10   drawn 13, 28, 12, 22, 11, 21, 7, 20, 4, 18
               lightning 20 is drawn (position 8 of 10)
served         drawn 13, 28, 12, 22, 11, 21, 7, 20, 4, 18 · lightning 20
LIQD verifier  drawn 13, 28, 12, 22, 11, 21, 7, 20, 4, 18 · lightning 20
src/rng.ts · wtfDraw · Verified
/** * WTF mode (spec §2.2): lightning = integer(0, 40) + 1 from cursor 0, then the * same draw as §2.3 with cursors starting at 1. Lightning is independent of * the draw and may or may not land among the drawn numbers. */export function wtfDraw(serverSeed: string, clientSeed: string, nonce: number): { lightning: number; drawn: number[] } {  const lightning = generateProvablyFairNumber(serverSeed, clientSeed, nonce, 0, GRID) + 1;  const drawn = partialFisherYates(serverSeed, clientSeed, nonce, 1);  return { lightning, drawn };}
Result: 600 / 600 scored WTF rounds reproduce — drawn list in order and lightning number — under the WTF rule, and LIQD’s own verifier agrees on all 600 (Steps 6, 24). Across the capture the lightning number was among the drawn numbers on 125 of the 600 rounds (Step 21).
2.8Worked Example — Full Draw Trace

Real round from the dataset — bet E5tGJUmA4gmbhcUreVar7 (epoch 7, nonce 14), Classic, 5 picks. Verified from the scored capture. Ten HMAC digests, one per cursor; each step picks a position among the numbers still on the board and swaps it into place:

serverSeed = 92c36e93b39a5cde21d275e9747a5794   (HMAC key: its 16 decoded bytes)
clientSeed = audit42a83c667988
nonce      = 14
risk       = Classic → draw cursors 0–9
picks      = 5, 40, 4, 33, 10
message    = audit42a83c667988:14:<cursor>
digest (cursor 0) = d60f3c302a85be89f1e68b1e2c31b9a06cdd93178f3a3671e98ae74a31847824
board      = [1, 2, …, 40]; step i takes the first 4 bytes of cursor i mod (40 − i)
7 columns
cursorrangefirst 4 bytesuint32j′ = uint32 mod rswap a[i] ↔ a[i + j′]drawn[i]
040d60f3c303,591,322,672321 ↔ 3333
139f7e857314,159,199,025332 ↔ 3535
23892045e372,449,759,79953 ↔ 88
33775eb6d9c1,978,363,29264 ↔ 1010
4363d5efad31,029,634,771315 ↔ 3636
5353788994a931,699,018336 ↔ 3939
634473b71cd1,195,078,093237 ↔ 3030
7338726d4f12,267,469,04123 ↔ 44
832cf341cf13,476,298,993179 ↔ 2626
931d28e55c93,532,543,433303 ↔ 4040
Parity verified: Bet E5tGJUmA4g… — the drawn list matches the ten-step HMAC recomputation exactly, in order, and LIQD’s own verifier returns the same list (Step 24). Against the picks 5, 40, 4, 33, 10 the draw holds 40, 4, 33, 10 — 4 hits, so the Classic / 5 cell pays 16.5×; the payout is traced in S3.9.
Live Game33, 35, 8, 10, 36, 39, 30, 4, 26, 40
Verifier33, 35, 8, 10, 36, 39, 30, 4, 26, 40
Technical Evidence & Verification5 sections
2.9Evidence Coverage Summary12 checks
3 columns
Verification AreaCoverageResult
Our implementation (Step 6)3,000 / 3,000 rounds in served order — written from the specification; its 96 specification checks passPass
LIQD verifier cross-check (Step 24)1,144 / 1,144 replies return the served drawn list — every WTF round and every observed Classic–High cellPass
Key encoding (hex vs UTF-8)HMAC keys the decoded bytes (3,000 / 3,000 recomputed, Step 6); the commitment hashes the text (51 / 51 reveals, Step 1)Pass
Draw bias bound (Step 8)Worst 2.070 × 10⁻¹⁰ per draw, below 2⁻³² on every range; 0 of 30,600 captured steps in the rejection zonePass
External entropy non-participation (Step 6)3,000 / 3,000 reproduced from disclosed inputs alonePass
Pick-set independence (Step 10)No pick, risk or stake argument; 58–60 pick sets per epoch, 2,400 / 2,400 recomputePass
Board uniformity (Step 19)10 / 10 streams of 1,000,000 rounds; smallest p = 0.0288 against 3.333 × 10⁻⁵Pass
Hit law (Step 19)10 / 10 streams fit the exact hypergeometric probabilities; smallest p = 0.0841Pass
Serial independence (Step 19)Lag-1 of hit counts on 10 / 10 streams; |ρ| ≤ 0.0018, smallest p = 0.0724Pass
Live-sample fit (Step 21, informational)The 3,000 captured draws: board p = 0.7249; hit law p 0.2100–0.9034; lightning p = 0.3064Pass
WTF lightning draw (Steps 6, 24)600 / 600 drawn lists and lightning numbers reproduced; LIQD’s verifier agrees on 600 / 600Pass
Client seed influence (Step 7’s seed controls)Wrong and empty client seeds change the draw on 2,400 / 2,400 roundsPass
2.10Code References10 source files
2 columns
FilePurpose
src/rng.tsHMAC-SHA256 draw (commitHash, generateProvablyFairNumber, computeDraw, wtfDraw) — written from docs/ALGORITHM.md
docs/ALGORITHM.mdThe specification the RNG was written from — inputs, commitment, derivation, the WTF lightning number, hits, payout and their sources
src/bias.tsThe draw-bias bound and the rejection-zone census over every scored round (Step 8)
src/sim.tsSimulation kernels shared by the producer and the verifier — Pass 1 streams from the pinned master seed
src/simulate.tsnpm run simulate — produces the simulation artifact; --compare regenerates it and diffs it field by field
src/stats.tsBoard uniformity corrected for drawing without replacement, χ² fit (regularized incomplete gamma), lag-1 from integer sums
tests/steps/determinism.tsSteps 6–10: draw recomputation, client-seed controls, bias bound, structure & hits, pick-set independence
tests/steps/simulation.tsSteps 19–20: Pass 1 statistics re-derived from the artifact’s integer rows and the 50,000-round replay; Pass 2
tests/keno/rngSpecTests.tsThe specification’s 96 vector and property checks
tests/keno/rngTests.ts12 unit tests — the commitment convention, draw order, client-seed sensitivity, the bias bound, hit counting and the WTF rule
2.11Verified Invariants12 invariants
2 columns
InvariantResult
HMAC-SHA256 output matches the live game for all 3,000 rounds — drawn list in orderPass
Key is hex-decoded (16 bytes, not UTF-8) — wrong encoding produces wrong drawsPass
Message format is clientSeed:nonce:cursor — literal colons, no paddingPass
First 4 digest bytes, big-endian — one value per cursorPass
Ranges 40 … 31 in step order; forward Fisher–Yates over 1–40; output order = served orderPass
Plain-modulo bias below 2⁻³² on every range (worst 2.070 × 10⁻¹⁰ per draw)Pass
Zero rejection-zone steps across all 30,600 captured draw steps (expected 6.650 × 10⁻⁵)Pass
No external entropy sources required for draw computationPass
computeDraw takes no pick, risk or stake argument — 2,400 / 2,400 Classic–High rounds recompute by itPass
WTF: lightning from cursor 0, the draw from cursors 1–10 — 600 / 600 rounds reproducePass
Board uniformity, hit law and lag-1 pass on all 10 simulated streams at α / 30Pass
A client seed change produces a different draw on 2,400 / 2,400 rounds under both controlsPass
2.12Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json — 10M rounds (Pass 1: ten 1,000,000-round streams) + the 40 Classic–High epoch seeds of the capture replayed for 10,000 nonces each (Pass 2)
Primary dataset: data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json — 3,000 live rounds for draw recomputation (Step 6)
Simulation output: outputs/simulation-results.json — Pass 1 (10 streams × 1,000,000 rounds: number counts, hit histograms, serial sums, 50,000-round prefixes) + Pass 2 (40 seeds × 10,000 nonces) · SHA-256 a4a2b1119491c983…
Verification output: outputs/verification-results.json — the 24 steps, including Step 19’s per-stream statistics re-derived from the artifact’s integer rows
LIQD verifier replies: evidence/operator-verify-api-raw-2026-10-21/…ndjson (E34) — LIQD’s verify endpoint called with no bet, every reply stored as received · SHA-256 04661d2b15fe26fd…
Reference vectors: tests/keno/rngSpecTests.ts (the specification’s 96 checks) and tests/keno/rngTests.ts — 12 known-input unit tests
2.13Reproduction InstructionsRun locally

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

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

  [PASS] Step 6: Outcome recomputation (drawn list)
  [PASS] Step 8: Draw bias bound
  [PASS] Step 10: Pick-set independence
  [PASS] Step 19: Simulation Pass 1 (fresh seeds)
  [PASS] Step 21: Live-sample fit (informational)
  [PASS] Step 24: Operator-verifier parity

The regenerated outputs/run/simulation-results.json matches the committed outputs/simulation-results.json field for field (SHA-256 a4a2b111…): REPRODUCED, with no difference even within tolerance.
03

Verifier Parity

Does the live game actually follow its own rules?

This section validates that our implementation produces the exact same draw as the live game for every round — all ten numbers, in the order they were drawn. It also recounts every hit from the recomputed draw, matches every multiplier to its cell in LIQD’s declared paytable (× 10 when a WTF round’s lightning number is drawn), and confirms every credited amount to the last of its 8 decimals. Any mismatch would invalidate the fairness guarantee.

Live ↔︎ Verifier Parity

3,000 / 3,000rounds matched

What we verified

  • Every round independently recomputed from seeds — all ten drawn numbers in order, not just the payout
  • Hits recounted from our own draw on every round; every multiplier equals its declared paytable cell (× 10 on the 16 paying WTF rounds whose lightning number was drawn)
  • Payout correctness: all 3,000 credits equal floor(stake × multiplier) in integer 1e-8 units with zero tolerance; the 2,000 zero-cell rounds pay exactly 0
  • LIQD’s own verifier agrees: 1,144 / 1,144 replies equal the served round, covering every WTF round and every observed paytable cell
  • All 50 settings covered — 5 risk levels × 1–10 picks, 60 rounds each

What this means for you

  • The verifier isn’t a simulation — it produces the exact same draw as the live game
  • Every round you play can be independently recomputed by anyone
  • No hidden logic alters the draw based on the numbers you pick
  • The game LIQD served in the capture follows the specified algorithm and its published paytable exactly
3,000 / 3,000rounds matched
  • Every round independently recomputed from seeds — all ten drawn numbers in order, not just the payout
  • Hits recounted from our own draw on every round; every multiplier equals its declared paytable cell (× 10 on the 16 paying WTF rounds whose lightning number was drawn)
  • Payout correctness: all 3,000 credits equal floor(stake × multiplier) in integer 1e-8 units with zero tolerance; the 2,000 zero-cell rounds pay exactly 0
  • LIQD’s own verifier agrees: 1,144 / 1,144 replies equal the served round, covering every WTF round and every observed paytable cell
  • All 50 settings covered — 5 risk levels × 1–10 picks, 60 rounds each
Parity Verification Flow — seeds → recompute the draw → hits → paytable cell → exact match — illustrated flow
3,000Live Rounds Tested
100%Parity Rate
$0.10Bet Size Tested
0Mismatches
CheckResultEvidence
Draw recomputationPass3,000 / 3,000 drawn lists reproduced in served order — 2,400 Classic–High rounds by computeDraw, 600 WTF rounds by wtfDraw with their lightning number (Step 6)
Hit countingPass3,000 / 3,000 served hits equal |picks ∩ drawn| and the hits counted from our own draw; ten distinct numbers from 1 to 40 on every round (Step 9)
Paytable cell matchPass2,400 / 2,400 Classic–High multipliers equal the declared cell exactly; on WTF 584 equal the cell and 16 exactly 10 × the cell, each of the 16 with its lightning number drawn (Step 11)
Payout arithmeticPassAll 3,000 credits: winningAmount == floor(stake × cell) in integer 1e-8 units, × 10 on the 16 paying lightning rounds, with zero tolerance; 2,000 / 2,000 zero-cell rounds pay exactly 0 (Step 12)
Wallet fieldsPassstakeWallet and winningAmountWallet equal the game fields, and result follows the multiplier, on 3,000 / 3,000 rounds (Step 12)
LIQD verifier parityPass1,144 / 1,144 replies from LIQD’s own verifier equal the served round — drawn order, lightning, hits and multiplier — covering all 600 WTF rounds and all 174 observed Classic–High cells (Step 24)
Configuration coveragePass50 / 50 settings at exactly 60 rounds; 218 of the 325 paytable cells reached (Step 15)
Draw recomputationPass
3,000 / 3,000 drawn lists reproduced in served order — 2,400 Classic–High rounds by computeDraw, 600 WTF rounds by wtfDraw with their lightning number (Step 6)
Hit countingPass
3,000 / 3,000 served hits equal |picks ∩ drawn| and the hits counted from our own draw; ten distinct numbers from 1 to 40 on every round (Step 9)
Paytable cell matchPass
2,400 / 2,400 Classic–High multipliers equal the declared cell exactly; on WTF 584 equal the cell and 16 exactly 10 × the cell, each of the 16 with its lightning number drawn (Step 11)
Payout arithmeticPass
All 3,000 credits: winningAmount == floor(stake × cell) in integer 1e-8 units, × 10 on the 16 paying lightning rounds, with zero tolerance; 2,000 / 2,000 zero-cell rounds pay exactly 0 (Step 12)
Wallet fieldsPass
stakeWallet and winningAmountWallet equal the game fields, and result follows the multiplier, on 3,000 / 3,000 rounds (Step 12)
LIQD verifier parityPass
1,144 / 1,144 replies from LIQD’s own verifier equal the served round — drawn order, lightning, hits and multiplier — covering all 600 WTF rounds and all 174 observed Classic–High cells (Step 24)
Configuration coveragePass
50 / 50 settings at exactly 60 rounds; 218 of the 325 paytable cells reached (Step 15)
✓  Live game and verifier fully aligned

All 3,000 rounds matched our recomputation exactly — drawn numbers in order, lightning numbers, hits, multipliers and credited amounts, across all 50 settings. Every multiplier is the declared paytable cell — × 10 on the paying WTF rounds whose lightning number was drawn — and LIQD’s own verifier returns the same result on all 1,144 rounds it was asked about.

How It Works — Verifier Parity9 sections
3.1Why Parity Matters

If the verifier produces results that differ from the live game, players cannot trust the verification — the entire provably fair system becomes meaningless. Commit-reveal shows the seed didn’t change; parity shows the game the player experienced is the game the seed dictates. For every captured round the audit rebuilds the drawn list from raw inputs with our own code, recounts the hits, looks up the multiplier in LIQD’s declared paytable and recomputes the credit — then compares all of it against what LIQD actually served and credited. 100% parity is required: a single mismatch anywhere in 3,000 rounds would indicate a bug in the verification logic, manipulation in the live game, or an inconsistent RNG implementation.

Why Parity Matters — illustrated flow
3.2Collection Design

The scored capture is one structured phase built for coverage: every risk level and every pick count, played at the same stake. Each risk level has its own ten seed epochs, and inside every epoch the pick count cycles 1–10 six times, so each of the 50 settings gets exactly 60 rounds. Pick sets are drawn at random for every round (58–60 distinct sets per epoch), and every epoch runs on a fresh client seed of our own.

5 columns
PhaseRoundsConfigBet AmountPurpose
A — Classic600Classic, picks 1–10 × 60 rounds each (epochs 0–9)$0.10Draw, hits, cell and payout on the 10 Classic settings
A — Low600Low, picks 1–10 × 60 rounds each (epochs 10–19)$0.10The Low settings, including the Low / 1 consolation cell (0.7× on 0 hits)
A — Medium600Medium, picks 1–10 × 60 rounds each (epochs 20–29)$0.10The Medium settings, including the Medium / 1 consolation cell (0.4× on 0 hits)
A — High600High, picks 1–10 × 60 rounds each (epochs 30–39)$0.10The most volatile standard risk level — per-round variance up to 270.75 (High / 6)
A — WTF600WTF, picks 1–10 × 60 rounds each (epochs 40–49)$0.10The lightning draw and its × 10 settlement on all 10 WTF settings
Total: 3,000 rounds across 50 seed epochs of exactly 60 rounds each (nonces 0–59), one risk level per epoch — epoch counts, nonce windows and the 60 rounds per setting are verified by Steps 4, 5 and 15 against constants in code, not the dataset header. Total wagered: $300.00, every round at $0.10 USDC, placed on 20 October 2026 between 13:04 and 13:32 UTC.
3.3Draw Recomputation (Step 6)

For every round, the audit computes the drawn list as computeDraw(serverSeed, clientSeed, nonce) with the epoch’s revealed server seed — ten HMAC-SHA256 steps of a forward Fisher–Yates shuffle over 1–40 — and compares it with the served drawn as an ordered list: the same ten numbers in the same order, no tolerance. WTF rounds use wtfDraw, which spends cursor 0 on the lightning number and draws the ten numbers at cursors 1–10; there the served lightning number must match as well. Every revealed seed is used and every round is compared — coverage equals the population.

Draw Recomputation (Step 6) — illustrated flow
src/v2.ts · deriveV2 + tests/steps/primary.ts · Step 6 (wtfCounts, p6) · Verified
  for (const b of ds.bets) {    const ss = seedMap.get(b.serverSeedHash);    if (ss === undefined) continue;    recomputed.set(b.betId, computeDraw(ss, b.clientSeed, b.nonce));    if (b.mode === 'wtf') wtfRecomputed.set(b.betId, wtfDraw(ss, b.clientSeed, b.nonce));  }// …    const w = wtfRecomputed.get(b.betId);    if (w && sameDraw(w.drawn, b.drawn) && w.lightning === lightningOf(b)) variant++;// …  const bets = S.bets.filter(b => !isWtf(b));  let match = 0; const bad: string[] = [];  for (const b of bets) { const r = S.recomputed.get(b.betId); if (r && sameDraw(r, b.drawn)) match++; else bad.push(`${b.betId} (${b.mode}/${b.picks.length} epoch ${b.epoch} nonce ${b.nonce})`); }
Result: 3,000 / 3,000 drawn lists reproduced in served order, 0 skipped — 2,400 / 2,400 Classic, Low, Medium and High rounds under computeDraw and 600 / 600 WTF rounds under wtfDraw, lightning number included. Every computed list matches the dataset’s drawn field.
3.4Paytable Cell & Multiplier (Step 11)

Keno has no multiplier formula: each (risk, picks, hits) cell of LIQD’s paytable is its own multiplier, so the table itself is the payout rule. The audit’s declared table is LIQD’s client paytable — the object literal shipped in the Keno page code of qa.liqd.com, read on 16 October 2026 and saved byte for byte (E31, SHA-256 767b7230…). It is parsed as data and never evaluated: 5 risk levels × 1–10 picks = 50 rows, 325 cells. The scored capture names the same file and hash in its own header as the table it was priced by, and that table prices 3,000 / 3,000 scored rounds. Step 11 looks up every round’s cell from our own hit count — never the served one — and compares the served multiplier with it as an exact rational: 2,400 / 2,400 Classic, Low, Medium and High rounds equal their cell exactly. On WTF, 584 / 600 equal the cell; the other 16 are paying rounds paid 10 × the cell — compared as served, within 1e-9 of 10 × the cell, and all 16 are exact — and each of them has its lightning number among the drawn, while none of the 28 other paying WTF rounds does. The × 10 factor is the settlement LIQD’s own verifier applies: its verify endpoint returns 10 × the cell on all 16 paying lightning rounds and the plain cell on the other 584 of the 600 scored WTF rounds (Step 24). No operator odds or probability field is read.

Paytable Cell & Multiplier (Step 11) — illustrated flow
2 columns
MetricCount
Classic–High rounds where multiplier equals the declared cell, exactly2,400 / 2,400
WTF rounds where multiplier equals the declared cell584 / 600
Paying WTF rounds paid 10 × the cell16 — lightning number drawn on 16 / 16
Other paying WTF rounds with the lightning number drawn0 of 28
Scored rounds priced by the declared table3,000 / 3,000
Paytable cells reached by the capture218 of 325 (174 Classic–High, 44 WTF)
tests/steps/primary.ts · Step 11 (priceWith, classify) · Verified
export function priceWith(table: ClientTable, recomputed: Map<string, number[]>, wtfRecomputed: Map<string, { lightning: number; drawn: number[] }>, b: V2Bet) {  if (isWtf(b)) {    const w = wtfRecomputed.get(b.betId);    const hits = w ? countHits(b.picks, w.drawn) : countHits(b.picks, b.drawn);    const cell = clientCell(table, b.mode, b.picks.length, hits);    const inDraw = !!w && w.drawn.includes(w.lightning);    const factor = cell && cell.q.n > 0n && inDraw ? WTF_LIGHTNING_FACTOR : 1;    return { hits, cell, factor, inDraw };  }  const r = recomputed.get(b.betId);  const hits = countHits(b.picks, r ?? b.drawn);  return { hits, cell: clientCell(table, b.mode, b.picks.length, hits), factor: 1, inDraw: false };}// …    if (!isWtf(b)) { if (eq(p.cell.q, decimalToQ(b.multiplier))) std++; else bad.push(`${tag}${b.betId} ${b.mode}/${b.picks.length}/${p.hits} served ${b.multiplier} declared ${p.cell.text}`); continue; }    if (p.cell.q.n > 0n) paying++;    if (eq(p.cell.q, decimalToQ(b.multiplier))) { plain++; if (p.cell.q.n > 0n && p.inDraw) plainPayingInDraw++; continue; }    // …    const ten = toNumber(mul(p.cell.q, q(BigInt(WTF_LIGHTNING_FACTOR))));    if (p.cell.q.n > 0n && Math.abs(b.multiplier - ten) <= 1e-9 * ten) { boosted++; if (!p.inDraw) boostedNotInDraw++; }
Result: Every served multiplier is the declared cell — × 10 exactly on the paying WTF rounds whose lightning number is drawn — on 3,000 / 3,000 rounds, with hits from our own recomputed draw (Step 11). Zero multiplier errors.
3.5Payout Arithmetic & Wallet Fields (Step 12)

Step 12 replays the money in exact integer arithmetic on 1e-8 units: winningAmount must equal floor(stake × cell) — × 10 on a WTF lightning round — with zero tolerance, and a zero cell must pay exactly 0. At the $0.10 stake every product falls exactly on the 8-decimal grid (3,000 / 3,000), so no round needed rounding at all. The wallet-denominated settle fields stakeWallet and winningAmountWallet must equal the game fields, and result must read won exactly when the multiplier is above 0. Step 14 adds a bound on every round: no payout may exceed the stake times its row’s top cell (× 10 on WTF).

3 columns
ScopeCoverageVerified
Credit — winningAmount == floor(stake × cell), × 10 on lightning rounds, in 1e-8 units3,000 rounds3,000 / 3,000
Zero cells — winningAmount == 02,000 rounds2,000 / 2,000
Wallet fields — stakeWallet / winningAmountWallet equal the game fields3,000 rounds3,000 / 3,000
Label — result is won exactly when the multiplier is above 03,000 rounds3,000 / 3,000
Bound — payout ≤ stake × the row’s top cell (× 10 on WTF), Step 143,000 rounds3,000 / 3,000
tests/steps/primary.ts · Step 12 (payouts) · Verified
    const p = priceWith(table, recomputed, wtfRecomputed, b);    if (!p.cell) { bad.push(`${tag}no cell ${b.betId}`); continue; }    const m = mul(p.cell.q, q(BigInt(p.factor)));    const prod = mul(q(units8(b.stake)), m);           // exact, in 1e-8 units    const floor = prod.n / prod.d, rem = prod.n % prod.d;    const round = rem * 2n >= prod.d ? floor + 1n : floor;    const win = units8(b.winningAmount);    if (rem === 0n) onGrid++; else { offGrid++; if (floor !== round) { decisive++; if (win === floor) floorOnDecisive++; } }    if (win === floor) paid++; else bad.push(`${tag}${b.betId} stake ${b.stake} × ${p.cell.text}${p.factor > 1 ? ` × ${p.factor}` : ''} paid ${b.winningAmount}`);    if (m.n === 0n) { zeroN++; if (win === 0n) zeroOk++; }    if (b.stakeWallet === b.stake && b.winningAmountWallet === b.winningAmount) walletSame++;    if (b.result === (m.n > 0n ? 'won' : 'lost')) labelOk++; else bad.push(`${tag}label ${b.betId} ${b.result}`);
Result: All 3,000 credits reproduced exactly from the stake and the declared cell (× 10 on the 16 paying lightning rounds); the 2,000 zero-cell rounds pay exactly 0; wallet fields and won/lost labels reconcile on every round (Steps 12, 14).
3.6Hit Counting & Draw Structure (Step 9)

Before a hit can be counted, the round itself must be well formed. Step 9 checks every served round: the drawn list holds ten distinct whole numbers from 1 to 40; the picks are a legal set of 1–10 distinct numbers from 1 to 40; the served hits value equals |picks ∩ drawn|; and the recorded fields equal the raw settlement response. It then counts the hits again from our own recomputed draw and requires it to equal the served count — 2,400 / 2,400 Classic–High rounds and 600 / 600 WTF rounds. lightning must be a whole number from 1 to 40 on every WTF round and null on every other round.

tests/steps/primary.ts · Step 9 + src/rng.ts · countHits · Verified
  const legalDraw = (d: unknown) => Array.isArray(d) && d.length === DRAWN && new Set(d).size === DRAWN && d.every(x => Number.isInteger(x) && x >= INDEX_BASE && x < INDEX_BASE + GRID);  const legalPicks = (p: unknown) => Array.isArray(p) && p.length >= PICKS_MIN && p.length <= PICKS_MAX && new Set(p).size === p.length && p.every(x => Number.isInteger(x) && x >= INDEX_BASE && x < INDEX_BASE + GRID);// …/** hits = |set(picks) intersect set(drawn)| (spec §2.4). */export function countHits(picks: readonly number[], drawn: readonly number[]): number {  const d = new Set(drawn);  let hits = 0;  for (const p of new Set(picks)) if (d.has(p)) hits++;  return hits;}
Result: 3,000 / 3,000 rounds well formed; hits recounted from our own draw equal the served hits on 3,000 / 3,000; lightning legal on 600 / 600 WTF rounds and null on the other 2,400 (Step 9).
3.7LIQD Verifier Cross-Check (Step 24)

LIQD publishes its own verifier — the Verify tab inside the Keno game, backed by POST /api/v1/originals/keno/verify. On 21 October 2026 we called that endpoint for 1,144 scored rounds, with no bet placed, and stored every reply as received (E34). The rounds were chosen by a fixed rule: all 600 WTF rounds; every Classic–High round in the ten cells LIQD corrected (4.2; 74 rounds); the first, middle and last round of every other observed cell; and the first round of any epoch not yet covered — 544 Classic–High rounds in all (Classic 127, Low 137, Medium 151, High 129), covering all 174 observed Classic–High cells and all 40 Classic–High epochs. Each reply must carry the round’s own seeds, nonce, risk and picks and return the served drawn list in order, the lightning number, the hits and the multiplier. LIQD’s verifier is a cross-check, not the source: all 3,000 rounds are recomputed by our own implementation (Step 6).

2 columns
CheckResult
Replies stored as received, HTTP 200 — 1,144 distinct scored rounds1,144 / 1,144
Request = the round’s revealed seeds, nonce, risk and picks1,144 / 1,144
Drawn list, in order = served = our recomputation1,144 / 1,144
Lightning number (WTF) · none on other rounds1,144 / 1,144
Hits1,144 / 1,144
Multiplier = served = the declared cell (× 10 on the 16 paying lightning rounds)1,144 / 1,144
WTF rounds covered600 / 600
Observed Classic–High cells covered174 / 174
Classic–High epochs covered40 / 40
src/operator-verify-2026-10-21.ts · one reply against its served round · Verified
  const rule = ss ? (isWtf ? wtfDraw(ss, b.clientSeed, b.nonce) : { lightning: null, drawn: computeDraw(ss, b.clientSeed, b.nonce) }) : null;  const drawn = !!rule && sameList(o?.drawn, b.drawn) && sameList(o?.drawn, served?.drawn) && sameDraw(rule.drawn, o?.drawn ?? []);  const lt = lightningOf(b);  const lightning = isWtf ? (lt !== null && o?.lightning === lt && rule?.lightning === lt) : ((o?.lightning ?? null) === null && lt === null);  const hits = !!row && !!o?.drawn && row.hits === b.hits && row.hits === served?.hits && countHits(b.picks, o.drawn) === row.hits;  const multiplier = !!row && row.multiplier === b.multiplier && row.multiplier === served?.multiplier;
Result: LIQD’s own verifier returns the served round on all 1,144 calls — drawn order, lightning, hits and multiplier — and its multiplier is the declared cell on every one (Step 24).
3.8Bet-Size Invariance

In the specification, Keno’s draw cannot see your stake: computeDraw(serverSeed, clientSeed, nonce) takes three arguments and its body names no pick, risk or stake field (Step 10), and wtfDraw adds the lightning cursor and nothing else. LIQD’s declared paytable is keyed by risk, picks and hits only — it has no stake dimension, so it specifies the same multiplier at every stake — and in the specification the stake enters once, at settlement, as floor(stake × multiplier).

Verified at the scored $0.10 stake: 2,400 / 2,400 Classic–High rounds reproduce by the stake-free draw function (Step 10); each of the 174 observed Classic–High cells paid exactly its declared multiplier, and every WTF round its cell or exactly 10 × it (Step 11); every credit equals the stake times its multiplier, to the last of its 8 decimals (Step 12).
Production: The elevated-stake rounds will be completed in full on LIQD’s production site before certification.
3.9Worked Example — Full Parity Verification

Real round from the scored capture — bet E5tGJUmA4gmbhcUreVar7 (epoch 7, nonce 14), Classic, 5 picks at $0.10 — the capture’s largest Classic payout. Verified from liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json — the same round LIQD’s own verifier reproduced in the Step 24 cross-check, traced cursor by cursor in S2.8:

serverSeed = 92c36e93b39a5cde21d275e9747a5794
clientSeed = audit42a83c667988
nonce      = 14
risk       = Classic · picks 5, 40, 4, 33, 10
betAmount  = $0.10
recorded   = drawn 33, 35, 8, 10, 36, 39, 30, 4, 26, 40
             hits 4 · multiplier 16.5 · winningAmount 1.65000000
commitment = 5989c035b566e465…
3 columns
StepProcessOutput
1Recompute the commitment — SHA-256(utf8(serverSeed))Matches the pre-published hash
2Run computeDraw(serverSeed, clientSeed, 14)33, 35, 8, 10, 36, 39, 30, 4, 26, 40 — the served list, in order
3Count hits — picks ∩ drawn40, 4, 33, 10 → 4 hits — the served hits
4Look up the declared cell — Classic, 5 picks, 4 hits16.5× — the served multiplier
5Settle floor(0.10 × 16.5) in 1e-8 units1.65000000 — the credited winningAmount and its wallet field, verbatim
HMAC-SHA256 recomputation + paytable + settlement · Verified
sha256(utf8(serverSeed))                 → 5989c035b566e465… = the published commitment ✓computeDraw(serverSeed, clientSeed, 14)  → 33, 35, 8, 10, 36, 39, 30, 4, 26, 40 = the served list, same order ✓picks ∩ drawn = {40, 4, 33, 10}          → 4 hits = served hits ✓table.classic[5][4]                      → 16.5 = the served multiplier ✓floor(10,000,000 × 16.5), 1e-8 units     → 165,000,000 = 1.65000000 = winningAmount ✓
Parity verified: Bet E5tGJUmA4g… — commitment, drawn order, hits, declared cell and credited payout all match exactly between the live game and our recomputation. This example exercises Steps 1, 6, 9, 11, 12 and 24 end to end; its RNG trace is S2.8.
Live Game$1.65
Our recomputation$1.65
Technical Evidence & Verification5 sections
3.10Evidence Coverage Summary9 checks
3 columns
Verification AreaCoverageResult
Draw recomputation (Step 6)3,000 / 3,000 drawn lists in served order — 2,400 by computeDraw, 600 by wtfDraw with the lightning numberPass
Draw structure & hit counting (Step 9)3,000 / 3,000 rounds well formed; hits recounted from our own drawPass
Pick-set independence (Step 10)computeDraw takes (serverSeed, clientSeed, nonce) only — no pick, risk or stake field; 2,400 / 2,400 recomputePass
Paytable provenance & cell match (Step 11)Declared table pinned by SHA-256 and named by the capture; 2,400 / 2,400 equal the cell; WTF 584 the cell + 16 × 10 with lightning drawnPass
Payout arithmetic & wallet fields (Step 12)3,000 / 3,000 credits = floor(stake × cell [× 10]) in 1e-8 units; 2,000 zero-cell rounds pay 0; wallet fields equalPass
Payout bounds (Step 14)3,000 / 3,000 payouts within stake × the row’s top cell (× 10 on WTF)Pass
Configuration & cell coverage (Step 15)50 / 50 settings at exactly 60 rounds; 218 / 325 cells reachedPass
Dataset & paytable pins (Step 22)Capture, journal and declared table match their SHA-256 pins; the journal’s 3,000 settled bets equal the dataset’s, in orderPass
LIQD verifier parity (Step 24)1,144 / 1,144 replies equal the served round; 600 / 600 WTF rounds; 174 / 174 observed Classic–High cellsPass
3.11Code References8 source files
2 columns
FilePurpose
tests/steps/determinism.tsSteps 6–10: draw recomputation, client-seed controls, bias bound, structure & hits, pick-set independence
tests/steps/payouts.tsSteps 11–16: paytable provenance, payout arithmetic, bet-size checks, payout bounds and coverage
tests/steps/primary.tsThe capture-scoring part of each step — cell lookup and classification (priceWith, classify) and the settlement in 1e-8 units (payouts)
tests/steps/standardization.tsSteps 22–24: dataset and paytable pins, artifacts of record, operator-verifier parity
src/operator-verify-2026-10-21.tsStep 24: every stored E34 reply checked against its served round — request, drawn order, lightning, hits, multiplier and the declared cell
src/client-paytable.tsParses LIQD’s client paytable literal as data, never evaluated (clientCell, clientRowQ)
src/rng.tscomputeDraw, wtfDraw, countHits and sameDraw — written from docs/ALGORITHM.md
src/v2.tsLoads the scored capture and recomputes every round from its epoch’s revealed seed (deriveV2)
3.12Datasets UsedSource artifacts
Primary dataset: data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json — 3,000 live rounds for draw, hit, cell and payout parity (Steps 6, 9, 11, 12)
Declared paytable: evidence/client-paytable/liqd-keno-client-paytable-2026-10-16.js.txt (E31) — LIQD’s client paytable, 50 rows and 325 cells, the table every scored round is priced by · SHA-256 767b723066344398…
Request journal: data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.journal.ndjson — every rotation response, intent and settlement as received, in order · SHA-256 8f52aceba87bba7a…
LIQD verifier replies: evidence/operator-verify-api-raw-2026-10-21/…ndjson (E34) — LIQD’s verify endpoint called on 1,144 scored rounds, no bet placed, every reply stored as received · SHA-256 04661d2b15fe26fd…
Verification output: outputs/verification-results.json — Steps 6, 9–12, 14, 15, 22 and 24
3.13Verified Invariants12 invariants
2 columns
InvariantResult
Computed drawn list equals the served list, in order, for all 3,000 roundsPass
Computed lightning number equals the served one on all 600 WTF rounds; null on all 2,400 othersPass
hits == |picks ∩ drawn|, counted from our own draw, for all 3,000 roundsPass
multiplier == the declared cell for all 2,400 Classic–High rounds; == the cell or exactly 10 × it on all 600 WTF roundsPass
× 10 paid exactly on the paying WTF rounds whose lightning number is drawn — 16 / 16, and on none of the 28 other paying WTF roundsPass
winningAmount == floor(stake × cell [× 10]) in integer 1e-8 units for all 3,000 rounds, zero tolerancePass
winningAmount == 0 for all 2,000 zero-cell roundsPass
Wallet settle fields equal the game fields on all 3,000 roundsPass
Every payout ≤ stake × its row’s top cell (× 10 on WTF)Pass
No hidden inputs: the draw takes (serverSeed, clientSeed, nonce) only — no picks, no stakePass
LIQD’s verifier returns the served round on 1,144 / 1,144 callsPass
All 50 settings present at exactly 60 roundsPass
3.14Reproduction InstructionsRun locally

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

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

  [PASS] Step 6: Outcome recomputation (drawn list)
  [PASS] Step 9: Draw structure & hit counting
  [PASS] Step 10: Pick-set independence
  [PASS] Step 11: Paytable provenance & served-vs-declared
  [PASS] Step 12: Payout arithmetic
  [PASS] Step 14: Paytable shape & bounds
  [PASS] Step 15: Configuration & cell coverage
  [PASS] Step 22: Dataset & config pins
  [PASS] Step 24: Operator-verifier parity
04

RTP & Payout Logic

Is the house edge what the casino claims?

This section verifies that LIQD’s advertised 1.00% house edge holds on every one of Keno’s 50 settings — 5 risk levels × 1–10 picks, each with its own row of the paytable. The key test is anti-circularity: we derive every RTP from first principles, not from any casino-supplied figure — the chance of k hits is the hypergeometric C(n,k)·C(40−n,10−k)/C(40,10), counted from the 40-number board, and a setting’s RTP is the exact sum of those chances times LIQD’s declared cells. We hold LIQD’s 99% to all 50 settings under the RTP claim rule, compare them with 10 million simulated rounds, and test whether the casino pre-selected seeds against the player.

Return to Player Verification

98.9867%theoretical RTP (mean)

What we verified

  • LIQD’s advertised 1.00% edge holds on all 50 settings — every exact RTP within ±0.1 pp of 99%, from 98.9031% (Low / 5) to 99.0689% (Low / 9)
  • RTP proven from first principles: hypergeometric hit odds from the 40-number board × LIQD’s declared cells, in exact rationals — no casino probability used
  • The hit law equals an independent draw-by-draw derivation, cell for cell, for every pick count (Step 17)
  • WTF’s × 10 lightning settlement priced exactly: the lightning number lands among the ten drawn with probability 10/40, so every WTF row returns its table RTP × 13/4
  • 10M simulated rounds: all 50 paytable rows within ±5 SE of their exact RTP, WTF’s before its × 10 (largest |z| 1.73)
  • Cherry-pick test: the returns on the 40 Classic–High seeds LIQD served are consistent with chance — p = 0.8754 and 0.0711 against a 0.001 rule, no sign of seed selection
  • Every payout is the stake times its declared cell — × 10 on a paying WTF round whose lightning number is drawn — settled to 8 decimals: 3,000 / 3,000 rounds

What this means for you

  • The house edge on Keno is about 1%: between 0.93% and 1.10% depending on risk level and pick count
  • The 99% LIQD advertises is what each setting returns, within a tenth of a point
  • The RTP proof is derived independently — it doesn’t rely on trusting the casino
  • The casino’s seeds show no sign of being chosen against the player
  • You are paid exactly the paytable multiplier for your hits — × 10 in WTF when the lightning number is drawn
  • WTF pays rarely but big — the × 10 lightning boost is built into its 99%
98.9867%theoretical RTP (mean)
  • LIQD’s advertised 1.00% edge holds on all 50 settings — every exact RTP within ±0.1 pp of 99%, from 98.9031% (Low / 5) to 99.0689% (Low / 9)
  • RTP proven from first principles: hypergeometric hit odds from the 40-number board × LIQD’s declared cells, in exact rationals — no casino probability used
  • The hit law equals an independent draw-by-draw derivation, cell for cell, for every pick count (Step 17)
  • WTF’s × 10 lightning settlement priced exactly: the lightning number lands among the ten drawn with probability 10/40, so every WTF row returns its table RTP × 13/4
  • 10M simulated rounds: all 50 paytable rows within ±5 SE of their exact RTP, WTF’s before its × 10 (largest |z| 1.73)
  • Cherry-pick test: the returns on the 40 Classic–High seeds LIQD served are consistent with chance — p = 0.8754 and 0.0711 against a 0.001 rule, no sign of seed selection
  • Every payout is the stake times its declared cell — × 10 on a paying WTF round whose lightning number is drawn — settled to 8 decimals: 3,000 / 3,000 rounds
INDEPENDENT DERIVATION
Σₖ P(k hits) × paytable cell= 0.989031–0.990689
Hypergeometric C(n,k)·C(40−n,10−k)/C(40,10) computed independently · LIQD’s declared paytable · All 50 settings (WTF: its row × 13/4 for the × 10 lightning, P = 1/4) · Mean 98.9867%
98.9867%Theoretical RTP (mean of all 50 settings)
99.0143%Simulated (Pass 1 · 40 Classic–High settings · 10M rounds)
1.00%House Edge (advertised)
50 / 50Settings within 0.1 pp of LIQD’s 99%
CheckResultEvidence
Anti-circularityPassP(k hits) = C(n,k)·C(40−n,10−k)/C(40,10) equals an independent draw-by-draw derivation cell for cell for n = 1–10, sums to exactly 1 with mean n·10/40; no casino probability exists or is read; both negative controls rejected (Step 17)
RTP claim (RTP-CLAIM 1.2.0)PassLIQD shows “Edge: 1.00%” on the Keno game page; all 50 settings within ±0.1 pp of 99% — lowest Low / 5 at 98.903053% (−0.096947 pp), highest Low / 9 at 99.068864% (Step 18)
Paytable provenancePassThe declared table is LIQD’s client paytable, saved byte for byte and pinned by SHA-256 (325 cells); the capture names it as its own, and it prices 3,000 / 3,000 scored rounds (Step 11)
Simulated RTP (Pass 1)Pass10 × 1M rounds — board uniformity, hit law and lag-1 clear on all 10 streams (smallest p 0.0288 against 3.333 × 10⁻⁵); 50 / 50 paytable rows within ±5 SE of their exact RTP (WTF’s before its × 10), largest |z| 1.73 (Step 19)
Cherry-pick test (Pass 2)Pass40 Classic–High casino seeds × 10,000 nonces against 10,000 relocations: Classic / 3 p = 0.8754, every round as played p = 0.0711 — no significant return deficit (the rule fails below 0.001) (Step 20)
WTF lightning settlementPassLightning from its own HMAC cursor, drawn with probability 10/40 → every WTF row returns its table RTP × 13/4 (98.9625%–99.0398%); 16 / 16 × 10 rounds had the lightning drawn; LIQD’s verifier agrees on 600 / 600 (Steps 6, 11, 18, 24)
Paytable shape & coveragePassTop cell 1,000× (10,000× with lightning); every payout within stake × its row’s top cell (3,000 / 3,000); two consolation cells and no other irregular cell; 50 / 50 settings at 60 rounds, 218 / 325 cells reached (Steps 14, 15)
Anti-circularityPass
P(k hits) = C(n,k)·C(40−n,10−k)/C(40,10) equals an independent draw-by-draw derivation cell for cell for n = 1–10, sums to exactly 1 with mean n·10/40; no casino probability exists or is read; both negative controls rejected (Step 17)
RTP claim (RTP-CLAIM 1.2.0)Pass
LIQD shows “Edge: 1.00%” on the Keno game page; all 50 settings within ±0.1 pp of 99% — lowest Low / 5 at 98.903053% (−0.096947 pp), highest Low / 9 at 99.068864% (Step 18)
Paytable provenancePass
The declared table is LIQD’s client paytable, saved byte for byte and pinned by SHA-256 (325 cells); the capture names it as its own, and it prices 3,000 / 3,000 scored rounds (Step 11)
Simulated RTP (Pass 1)Pass
10 × 1M rounds — board uniformity, hit law and lag-1 clear on all 10 streams (smallest p 0.0288 against 3.333 × 10⁻⁵); 50 / 50 paytable rows within ±5 SE of their exact RTP (WTF’s before its × 10), largest |z| 1.73 (Step 19)
Cherry-pick test (Pass 2)Pass
40 Classic–High casino seeds × 10,000 nonces against 10,000 relocations: Classic / 3 p = 0.8754, every round as played p = 0.0711 — no significant return deficit (the rule fails below 0.001) (Step 20)
WTF lightning settlementPass
Lightning from its own HMAC cursor, drawn with probability 10/40 → every WTF row returns its table RTP × 13/4 (98.9625%–99.0398%); 16 / 16 × 10 rounds had the lightning drawn; LIQD’s verifier agrees on 600 / 600 (Steps 6, 11, 18, 24)
Paytable shape & coveragePass
Top cell 1,000× (10,000× with lightning); every payout within stake × its row’s top cell (3,000 / 3,000); two consolation cells and no other irregular cell; 50 / 50 settings at 60 rounds, 218 / 325 cells reached (Steps 14, 15)
✓  RTP Behaves as Advertised

Every one of Keno’s 50 settings returns between 98.903053% and 99.068864%, proven exactly from the hypergeometric hit law and LIQD’s declared paytable — mean 98.9867%, all 50 within ±0.1 pp of the advertised 99%. This is a first-principles proof, not a statistical estimate; 10M simulated rounds agree with it, the cherry-pick test finds no sign of seed selection, and every scored payout is the stake times its declared cell, × 10 on the paying WTF rounds whose lightning number was drawn. WTF’s × 10 lightning boost is priced into its 99% exactly.

How It Works — RTP & Payout Logic11 sections
4.1Anti-Circularity Proof — Hypergeometric Hit Probability (Step 17)

The anti-circularity proof establishes each setting’s RTP from first principles without using any casino-supplied probability. In Keno the probability side is pure combinatorics: ten numbers are drawn from 40, so the chance that exactly k of your n picks are among them is the hypergeometric C(n,k)·C(40−n,10−k)/C(40,10) — computed from the board alone. Step 17 checks that closed form against an independent derivation that plays the ten draws out one number at a time (sequentialDraw), cell for cell in exact BigInt rationals, for every pick count from 1 to 10; each distribution must also sum to exactly 1 and have mean exactly n·10/40. Two negative controls prove the check can fail: moving 1/C(40,10) of probability from 0 hits to 1 hit is rejected, and nudging one declared cell by 0.01 changes the exact RTP. A setting’s RTP is then Σ P(k) × cell(k) over its row of LIQD’s declared paytable — the casino supplies the multipliers it pays, never a probability:

Anti-Circularity Proof — Hypergeometric Hit Probability (Step 17) — illustrated flow
3 columns
ComponentFormulaSource
Hit probabilityP(k | n) = C(n,k)·C(40−n,10−k) / C(40,10)Board arithmetic — computed by the audit
Independent checksequentialDraw: ten draws without replacement, one number at a timeEqual to the closed form cell for cell for n = 1–10; sums to 1, mean n·10/40 (Step 17)
Multipliercell(k) — the declared paytable row for (risk, n)LIQD’s client paytable, pinned by SHA-256 (Step 11)
RTPΣ P(k | n) × cell(k), an exact rational50 / 50 settings equal outputs/rtp-claim-table.json (Step 18)
WTFrow RTP × 13/4 — × 10 when the lightning number is drawn, P = 10/40The lightning settlement (4.8)
4 columns
Hits (Classic / 3 picks)Exact P(k)CellP(k) × cell
0203/494 = 41.0931%0×0
1435/988 = 44.0283%1×435/988
2135/988 = 13.6640%3.1×837/1976
33/247 = 1.2146%10.4×12/95
Total1—9783/9880 = 99.018219%
Result: For n = 1–10 the hypergeometric law equals the independent draw-by-draw derivation cell for cell, sums to exactly 1 and has mean n·10/40; both negative controls are rejected (Step 17). No operator probability or odds field exists or is read. Theoretical RTP proof is non-circular and exact.

Anti-Circularity Verification:

src/exact.ts · sequentialDraw + tests/steps/rtp.ts · modelHolds (Step 17) · Verified
/** Independent derivation: draw d numbers one at a time without replacement, tracking hits. */export function sequentialDraw(n: number, N = GRID, d = DRAWN): Q[] {  let dist: Q[] = [q(1n)];                       // dist[h] after 0 draws  for (let step = 0; step < d; step++) {    const remaining = BigInt(N - step);    const next: Q[] = Array.from({ length: Math.min(n, step + 1) + 1 }, () => q(0n));    dist.forEach((p, h) => {      if (p.n === 0n) return;      const picksLeft = BigInt(n - h);      const pHit = q(picksLeft, remaining), pMiss = q(remaining - picksLeft, remaining);      if (picksLeft > 0n) next[h + 1] = add(next[h + 1], mul(p, pHit));      next[h] = add(next[h], mul(p, pMiss));    });    dist = next;  }  while (dist.length < n + 1) dist.push(q(0n));  return dist.slice(0, n + 1);}// …/** * The model check, as a function so the negative control can feed it a corrupted vector and show it * is rejected: a distribution must equal the independent sequential derivation cell for cell, sum to * exactly 1, and have mean exactly n·d/N. */export function modelHolds(n: number, probs: Q[]): boolean {  const seq = sequentialDraw(n);  if (probs.length !== seq.length || !probs.every((p, k) => eq(p, seq[k]))) return false;  const total = probs.reduce((a, p) => add(a, p), q(0n));  const mean = probs.reduce((a, p, k) => add(a, mul(p, q(BigInt(k)))), q(0n));  return eq(total, q(1n)) && eq(mean, q(BigInt(n * DRAWN), BigInt(GRID)));}

Why this proof is non-circular: P(k | n) comes from counting subsets of the 40-number board, and the cells are the multipliers LIQD’s client ships and the game pays — nothing in the sum is a casino-supplied probability, return figure or edge. What the RTP depends on is that every one of the C(40,10) = 847,660,528 possible draws is equally likely: Section 2 shows the draw is a uniform shuffle reproduced from the seeds, and Section 3 shows the multipliers paid are the declared cells. What the audit adds is evidence that LIQD’s implementation honours the table on every captured round (Steps 11, 12) and that the probability side is derived without touching a single operator-supplied number.

4.2RTP Claim Rule (Step 18)

The RTP claim rule (RTP-CLAIM 1.2.0) is part of the audit method: the RTP a casino publishes must hold at every setting the game offers. A published figure counts wherever a player can find it — on the game page, the fairness page, the help page, the bet panel, the rules, or in the game settings the site loads. Each setting’s RTP is computed exactly, as a rational number, from the hit law and its row of the declared paytable — never from the casino’s figure — and must sit within ±0.1 percentage points of the claim. A setting below that band on the house side is a miss: the verdict then reads RTP FIGURE OVERSTATED, even when every draw check passes. A setting above the claim never fails. Keno’s settings do not all land on exactly 99% — a row’s return weights its cells (3.1×, 1.86×, 16.5×…) by exact hit odds, and those sums rarely come to 99% exactly — so the exact returns spread from 98.903053% to 99.068864%. The rule is scored on Step 18, the step that prices every setting.

2 columns
ItemDetail
Claim99% RTP — a flat 1.00% house edge
Where LIQD publishes itThe Keno game page (qa.liqd.com/originals/keno), in the Description box under the game: “Released: September 2026 | Edge: 1.00%” — read logged in on 3 October 2026, no bet placed (evidence/page-read-2026-10-03/liqd-keno-page-description-edge-2026-10-03.png, E26); the page’s meta description also says “1% house edge”
Settings tested50 — every risk level (Classic, Low, Medium, High, WTF) × 1–10 picks, every row of the declared table (325 cells)
MethodExact rational: Σ P(k hits) × cell; WTF rows × 13/4 for the lightning settlement
Band±0.1 percentage points (house side fails, player side never fails)
True minimum · maximum98.903053% (Low / 5) · 99.068864% (Low / 9)
Worst gap−0.096947 pp (Low / 5), inside the band
Settings beyond the band0 on the house side · 0 on the player side
Scored onStep 18 (profile check KENO-19), rule enforced
ResultPass — the claim holds at every setting
5 columns
SettingExact RTPRTPGapResult
Low / 536155/3655698.903053%−0.096947 ppPass
High / 49039/913998.905788%−0.094212 ppPass
Low / 418081/1827898.922202%−0.077798 ppPass
Medium / 8130046/13146198.923635%−0.076365 ppPass
Low / 933861599/3417986099.068864%+0.068864 ppPass
WTF / 62785/281299.039829%+0.039829 ppPass
Classic / 2103/10499.038462%+0.038462 ppPass
All 50 settingsΣ P(k) × cell98.903053%–99.068864%−0.096947 pp to +0.068864 ppPass — 50 / 50
Result: Pass — LIQD’s advertised 1.00% edge holds at all 50 settings: the true minimum is 98.903053% (Low / 5, −0.096947 pp) and the true maximum 99.068864% (Low / 9), both inside the ±0.1 pp band (Step 18). The full per-setting table ships as outputs/rtp-claim-table.json, pinned by SHA-256 and re-derived on every run.
Paytable correction during the audit: Before LIQD’s correction, its Keno paytable, priced the same way, returned more than 0.1 pp below 99% on 10 settings (lowest 98.653846%, on Medium / 2 and High / 2). LIQD raised one cell in each of those settings — Medium / 2 at 2 hits, for example, from 5.1× to 5.16× — and the audited table passes all 50. The live game pays the corrected table: all 74 scored rounds that landed in a corrected cell were paid the new multiplier (Step 11). Section 5 lists this as a remediated finding.
4.3Full RTP Table — All 50 Settings

Every setting LIQD offers, priced exactly from the hit law and its row of the declared table, beside its simulated return from Pass 1 (4.5). The gap is the RTP claim rule’s: each setting’s distance from LIQD’s 99%. The theoretical and gap columns are outputs/rtp-claim-table.json, row for row; the simulated column is priced from outputs/simulation-results.json.

4 columns
SettingTheoretical RTPSimulated RTP (Pass 1)Gap to 99%
Classic / 199.000000%99.2962% (+1.73 SE)0.000000 pp
Classic / 299.038462%99.1051% (+0.53 SE)+0.038462 pp
Classic / 399.018219%99.0606% (+0.29 SE)+0.018219 pp
Classic / 498.960499%99.0762% (+0.79 SE)−0.039501 pp
Classic / 598.985803%98.9703% (−0.08 SE)−0.014197 pp
Classic / 698.966517%98.9676% (+0.01 SE)−0.033483 pp
Classic / 798.981535%98.8902% (−0.52 SE)−0.018465 pp
Classic / 899.022828%98.9679% (−0.27 SE)+0.022828 pp
Classic / 998.975340%98.9545% (−0.11 SE)−0.024660 pp
Classic / 1099.037352%99.1769% (+1.03 SE)+0.037352 pp
Low / 199.000000%99.0868% (+1.73 SE)0.000000 pp
Low / 299.019231%99.0845% (+0.55 SE)+0.019231 pp
Low / 399.003036%98.9685% (−0.12 SE)+0.003036 pp
Low / 498.922202%99.4538% (+1.16 SE)−0.077798 pp
Low / 598.903053%98.0552% (−1.39 SE)−0.096947 pp
Low / 699.008371%99.2005% (+0.28 SE)+0.008371 pp
Low / 798.938760%98.8901% (−0.10 SE)−0.061240 pp
Low / 899.004180%98.9587% (−0.25 SE)+0.004180 pp
Low / 999.068864%99.1389% (+0.61 SE)+0.068864 pp
Low / 1099.031310%99.0610% (+0.43 SE)+0.031310 pp
Medium / 199.000000%99.1765% (+1.73 SE)0.000000 pp
Medium / 299.000000%99.0680% (+0.51 SE)0.000000 pp
Medium / 398.987854%98.9336% (−0.10 SE)−0.012146 pp
Medium / 498.996881%99.6318% (+1.23 SE)−0.003119 pp
Medium / 598.944086%97.8075% (−1.46 SE)−0.055914 pp
Medium / 699.031623%99.1454% (+0.12 SE)+0.031623 pp
Medium / 798.961786%98.7404% (−0.27 SE)−0.038214 pp
Medium / 898.923635%98.7840% (−0.35 SE)−0.076365 pp
Medium / 998.942020%99.1716% (+1.00 SE)−0.057980 pp
Medium / 1098.974315%99.1147% (+0.98 SE)−0.025685 pp
High / 199.000000%99.2962% (+1.73 SE)0.000000 pp
High / 299.000000%99.0921% (+0.23 SE)0.000000 pp
High / 398.987854%98.7943% (−0.22 SE)−0.012146 pp
High / 498.905788%100.2619% (+1.08 SE)−0.094212 pp
High / 598.968706%97.7545% (−1.21 SE)−0.031294 pp
High / 698.998796%99.1324% (+0.08 SE)−0.001204 pp
High / 798.961786%98.9204% (−0.04 SE)−0.038214 pp
High / 898.957105%99.3305% (+0.36 SE)−0.042895 pp
High / 998.964456%98.7135% (−0.31 SE)−0.035544 pp
High / 1099.008323%99.3412% (+0.92 SE)+0.008323 pp
WTF / 198.962500%Table row 30.5411% (+1.73 SE)−0.037500 pp
WTF / 299.000000%Table row 30.4899% (+0.23 SE)0.000000 pp
WTF / 398.980263%Table row 30.4626% (+0.06 SE)−0.019737 pp
WTF / 499.004267%Table row 30.8344% (+1.18 SE)+0.004267 pp
WTF / 598.986486%Table row 30.1103% (−1.25 SE)−0.013514 pp
WTF / 699.039829%Table row 30.4887% (+0.04 SE)+0.039829 pp
WTF / 799.009497%Table row 30.5032% (+0.10 SE)+0.009497 pp
WTF / 898.981827%Table row 30.3330% (−0.32 SE)−0.018173 pp
WTF / 998.992073%Table row 30.3783% (−0.26 SE)−0.007927 pp
WTF / 1098.976463%Table row 30.6035% (+0.37 SE)−0.023537 pp
Result: 50 / 50 settings within ±0.1 pp of 99% — 15 above, 28 below and 7 exactly on it; mean 98.9867%. Every simulated figure is within ±5 SE of its exact RTP (Step 19).
Reading the simulated column: Pass 1 runs one stream of 1,000,000 simulated rounds per pick count and prices every risk level’s row from it, so the four risks of a pick count share their draws and their standard error depends on the row: from 0.0502 pp (Low / 1) to 1.6455 pp (High / 6). High / 4’s 100.2619% is +1.08 SE — ordinary sampling at this depth; the scored gate is ±5 SE. WTF rows are priced by the table row before the × 10 lightning settlement; the player’s return is that row’s exact RTP × 13/4 (4.8).
4.4House Edge & Settlement (Step 12)

The house edge is what a setting keeps: 100% minus its RTP — from 0.9311% (Low / 9) to 1.0969% (Low / 5) against the 1.00% LIQD advertises. Settlement turns a cell into money: winningAmount = floor(stake × cell) in units of 1e-8, × 10 on a WTF lightning round, so a payout never exceeds the exact product. Cells are written with at most two decimals on Classic, Low, Medium and High and at most three on WTF (260 whole, 47 with one decimal, 17 with two, 1 with three), so at the $0.10 stake every product in the capture lands exactly on the 8-decimal grid — 3,000 / 3,000 rounds settle with no rounding at all (Step 12). Two cells pay on zero hits — Low / 1 pays 0.7× and Medium / 1 pays 0.4× — which is how those 1-pick rows return 99% while paying back part of the stake on a miss.

Result: Settlement = floor(stake × cell), × 10 on the 16 paying lightning rounds, in integer 1e-8 units on 3,000 / 3,000 rounds; every product exact on the money grid at $0.10; the 2,000 zero-cell rounds pay exactly 0 (Step 12). The edge on every setting is the paytable’s, with nothing taken at settlement.
Variance: Keno’s return is spread unevenly. On high pick counts and High risk most rounds pay nothing and a few pay a lot: the per-round variance of the return runs from 0.25 (Low / 1) to 270.75 (High / 6) in squared stakes, and to 452.1 on WTF with lightning (WTF / 6). Over a session, results can sit far from 99% in either direction; the RTP describes the long run.
4.5Simulation Pass 1 — Our Own Seeds (Step 19)

Section 4.1 proves each setting’s RTP mathematically; the simulation checks it at scale. We simulated 10 million rounds with our own implementation — the one that reproduces all 3,000 captured rounds — and our own seeds: one stream of 1,000,000 rounds for each pick count, from seeds derived from a pinned master seed, replayable byte for byte. Classic, Low, Medium and High share one draw function with no pick, risk or stake input (Step 10), so those four risk levels of a pick count share that stream’s draws and each setting’s simulated RTP is its row priced on the stream’s hit counts; the WTF row is priced on the same hit counts, since its draw follows the same hit law. Every statistic is judged against a gate of 3.333 × 10⁻⁵ (α = 0.001 over 10 streams × 3 tests); the RTP of every paytable row must sit within ±5 SE of its exact value:

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

Board uniformity: Each stream’s 10,000,000 drawn numbers spread over the 40 values, corrected for drawing without replacement — 10 / 10 streams clear, smallest p 0.0288.

Hit law: Each stream’s hit counts against the exact hypergeometric probabilities — 10 / 10 clear, smallest p 0.0841.

Serial screen: Lag-1 correlation of consecutive hit counts — |ρ| ≤ 0.0018 on 10 / 10 streams, smallest p 0.0724.

RTP convergence: Every paytable row’s simulated RTP against its exact RTP (WTF’s before its × 10) — 50 / 50 within ±5 SE, largest |z| 1.73 (the 1-pick rows, which share one stream). The 40 Classic–High settings average 99.0143% simulated against 98.9850% exact.

3 columns
Stream (1,000,000 rounds)Largest |z| of its five rowsSetting
1 pick1.73all five rows (one shared stream)
2 picks0.55Low / 2
3 picks0.29Classic / 3
4 picks1.23Medium / 4
5 picks1.46Medium / 5
6 picks0.28Low / 6
7 picks0.52Classic / 7
8 picks0.36High / 8
9 picks1.00Medium / 9
10 picks1.03Classic / 10
src/simulate.ts · Pass 1 streams + tests/steps/simulation.ts · RTP convergence (Step 19) · Verified
  const streams = PASS1_PICK_COUNTS.map(n => {    const s = runPass1Stream(n, rounds);    process.stderr.write(`  pass1 picks=${n} done\n`);    return s;  });// …  // RTP convergence on EVERY configuration of the declared table: the stream of that pick count, priced by that risk's row.  let converged = 0, worstZ = 0, worstCfg = '';  const rtpParts: string[] = [];  for (const c of ALL_CONFIGS) {    const s = streams.find(x => x.picks === c.picks);    if (!s) { problems.push(`no stream for ${c.mode}/${c.picks}`); continue; }    const row = clientRowQ(ctx.clientTable, c.mode, c.picks), P = hypergeometric(c.picks);    const exact = toNumber(rtpExact(P, row)), v = toNumber(varianceExact(P, row));    const simR = s.hits.reduce((a, n, k) => a + n * toNumber(row[k]), 0) / s.rounds;    const z = (simR - exact) / Math.sqrt(v / s.rounds);    if (Math.abs(z) > 5) problems.push(`${c.mode}/${c.picks} RTP ${simR} is ${z.toFixed(2)} SE from exact ${exact}`); else converged++;    if (Math.abs(z) > Math.abs(worstZ)) { worstZ = z; worstCfg = `${c.mode}/${c.picks}`; }    if (c.mode === 'classic' && c.picks === 3 || Math.abs(z) > 3) rtpParts.push(`${c.mode}/${c.picks} simulated ${(100 * simR).toFixed(4)}% vs exact ${(100 * exact).toFixed(4)}% (${z.toFixed(2)} SE)`);  }
Result: 10M rounds. 10 / 10 streams pass board uniformity, the hit law and the serial screen above the gate (smallest p 0.0288); 50 / 50 paytable rows within ±5 SE of their exact RTP, largest |z| 1.73. The first 50,000 rounds of every stream are replayed on every verify, and npm run test:full regenerated the whole artifact for this report — REPRODUCED. The simulated payouts are consistent with the exact RTPs (Step 19).

What is a Monte Carlo simulation? Instead of proving fairness with algebra alone, we simulate millions of game rounds. Each round derives a draw from scratch using the same HMAC-SHA256 pipeline as the live game, counts the hits on a fixed pick set and records what each risk level’s row would pay. We then compare the aggregate return to the exact RTP. If they match within tolerance, the math holds in practice — not just on paper.

How the artifact is scored, not believed: Step 19 does not read the simulation’s own pass/fail fields. It re-derives every statistic from the artifact’s integer rows — number counts, hit histograms and serial sums — checks that every histogram adds up to its stream, prices every setting from the hit counts against the declared table, and replays the first 50,000 rounds of every stream from the derived seeds. Because the seeds come from a pinned master seed, npm run test:full regenerates the whole 10M-round experiment and compares it field by field with the committed artifact.

4.6Cherry-Pick Test — Pass 2 (Step 20)

Could the casino have chosen server seeds that pay players less? Pass 2 takes the Classic, Low, Medium and High server seeds LIQD actually used — all 40 revealed epoch seeds — and replays each one for 10,000 nonces under the epoch’s recorded client seed. The window the player saw is the epoch itself, nonces 0–59; everything after it is reference. Both statistics were fixed before the test ran. Each compares the return in the played window with the same window moved to later nonces the casino never served — 10,000 relocations — so a seed set chosen against the player would show up as a low return in the window that was actually played. The test is one-sided and fails below p = 0.001. Step 20 recomputes every seed row from the revealed seeds in every run.

Primary defence — structural: The client seed is player-editable and is set in the rotation after the server-seed commitment is published (1.4). An operator that cannot predict your seed cannot pre-evaluate candidates against it — the foreknowledge channel is removed by design.

Statistical check: The two relocation statistics above — a direct check on the money, backing the structural defence.

2 columns
TestDetail
Seeds tested40 — every revealed Classic–High epoch seed, keyed by its own epoch
Replay depth10,000 nonces per seed — the played window is nonces 0–59
Statistic 1 — Classic / 3Picks 1, 2, 3 at every window nonce: S = 82.86 stakes over 2,400 rounds against a relocation null of 2.52 ± 69.79 — p = 0.8754; detects an 11.43 pp house-favouring shift at 80% power
Statistic 2 — rounds as playedEvery recorded round by its own pick set and row: S = −347.61 stakes over 2,400 rounds against 12.77 ± 297.68 — p = 0.0711 (exact-variance z −1.201); detects 48.77 pp at 80% power
RuleFails if either p < 0.001 — both clear
Re-derivation40 / 40 recorded seed rows reproduced in this run
src/sim.ts · pass2Statistic (Step 20) · Verified
  let S = 0, N = 0;  for (const w of windows) { const len = w.end - w.start + 1; S += sumWin(w.hashPrefix, w.start, len) - muTenths * len; N += len; }  const rnd = prng(PASS2_BOOTSTRAP_SEED);  let atOrBelow = 0; let sumNull = 0, sumNull2 = 0;  for (let b = 0; b < PASS2_BOOTSTRAP_REPS; b++) {    let s = 0;    for (const w of windows) {      const len = w.end - w.start + 1;      const lo = len, hi = PASS2_NONCES - len;      if (hi < lo) throw new Error(`early window of ${w.hashPrefix} (${len} nonces) leaves no later window to relocate to`);      const a = lo + Math.floor(rnd() * (hi - lo + 1));      s += sumWin(w.hashPrefix, a, len) - muTenths * len;    }    if (s <= S) atOrBelow++;    sumNull += s; sumNull2 += s * s;  }  const nullMean = sumNull / PASS2_BOOTSTRAP_REPS, nullSd = Math.sqrt(Math.max(sumNull2 / PASS2_BOOTSTRAP_REPS - nullMean ** 2, 0));  return { S, N, p: (atOrBelow + 1) / (PASS2_BOOTSTRAP_REPS + 1), nullMean, nullSd };
Result: PASS. The returns LIQD served on its 40 Classic–High seeds are consistent with what the same seeds give at nonces it never served: p = 0.8754 and p = 0.0711 against the 0.001 rule, every seed row re-derived from the revealed seeds (Step 20). The test finds no sign of seed selection.
Why is this included? Cherry-pick testing is a standard part of our audit methodology. Commit-reveal constrains what happens after a commitment, not which seed gets committed. LIQD lets players set or rotate the client seed after the server-seed commitment is published, so cherry-picking is structurally contained for a player who sets an unpredictable seed. Pass 2 adds a statistical check on the money itself, and it replays identically on every run.
4.7Bet-Size Invariance

A setting’s RTP is a property of its row: Σ P(k) × cell(k) contains no stake. In the specification nothing gives the stake a way in before settlement: the draw takes (serverSeed, clientSeed, nonce) only (Step 10), the hits come from the draw and the picks, and LIQD’s declared paytable is keyed by risk, picks and hits only — no stake dimension, so the same multiplier at every stake. The stake enters once, at settlement, as floor(stake × multiplier): it scales the payout, floored at the 8th decimal (4.4).

3 columns
StageComputed fromStake input in the specification
Draw (Step 6)computeDraw(serverSeed, clientSeed, nonce); WTF adds the lightning cursorNone — three arguments, no stake field (Step 10)
Hits (Step 9)|picks ∩ drawn|None
Multiplier (Step 11)The declared cell for (risk, picks, hits); × 10 on a paying WTF lightning roundNone — the declared table is keyed by risk, picks and hits only
Payout (Step 12)floor(stake × multiplier) in 1e-8 unitsIts only appearance — it scales the payout, floored at the 8th decimal
Verified at the scored $0.10 stake: the draw reproduces from (serverSeed, clientSeed, nonce) alone on 3,000 / 3,000 rounds (Steps 6, 10); every multiplier is the declared cell, × 10 on the 16 paying lightning rounds (Step 11); every credit equals the stake times its multiplier exactly (Step 12).
Production: The elevated-stake rounds will be completed in full on LIQD’s production site before certification.
4.8WTF Mode — the Lightning Multiplier

WTF is Keno’s fifth risk level. Before the draw, HMAC cursor 0 picks a lightning number from 1 to 40 (uint32 mod 40 + 1); the ten numbers are then drawn at cursors 1–10 by the same shuffle as every other risk level. A paying WTF round pays 10 × its cell when the lightning number is among the ten drawn, and its cell otherwise. The lightning number comes from its own cursor, so it is independent of the draw: whatever ten numbers are drawn, it lands among them with probability exactly 10/40. Every WTF row therefore returns row RTP × (30/40 × 1 + 10/40 × 10) = row RTP × 13/4. LIQD’s WTF rows are priced for exactly that: by the table alone they return 30.450000%–30.473794%, and × 13/4 that is 98.962500%–99.039829%, all 10 within ±0.1 pp of 99% (Step 18).

LIQD publishes WTF’s rule through its own verifier — the in-game Verify tab and its endpoint — rather than in writing, and our written specification (docs/ALGORITHM.md §2.2) reproduces it exactly: 600 / 600 scored WTF rounds by wtfDraw, drawn list and lightning number (Step 6), with LIQD’s verifier returning the same draw, lightning number and multiplier on all 600 (Step 24). In the capture the lightning number was among the drawn on 125 of the 600 rounds, and 16 of the 44 paying WTF rounds were paid × 10 (4.10). One WTF row, in full:

4 columns
Hits (WTF / 7 picks)Exact P(k)Cell — lightning not drawn (30/40)Lightning drawn (10/40)
01305/11951 = 10.9196%0×0×
115225/47804 = 31.8488%0×0×
216443/47804 = 34.3967%0×0×
327405/155363 = 17.6393%0×0×
47105/155363 = 4.5732%3×30×
51827/310726 = 0.5880%18×180×
6105/310726 = 0.0338%169×1,690×
71/155363 = 0.0006%700×7,000×
Row RTPΣ P × cell30.464461%× 13/4 → 99.009497%
Result: Lightning settlement priced exactly — every WTF row returns its table RTP × 13/4 (98.9625%–99.0398%); 16 / 16 × 10 rounds had the lightning number drawn and 0 / 28 other paying WTF rounds did (Step 11); 600 / 600 WTF rounds reproduced with their lightning number (Step 6), LIQD’s verifier agreeing on 600 / 600 (Step 24).
4.9Paytable Shape, Coverage & Bounds (Steps 14, 15)

LIQD’s declared table has 325 cells: 5 risk levels × 1–10 picks, each row running from 0 hits to n hits. Its top cell is 1,000× — on Low, Medium and High at 9 and 10 picks and on WTF at 10 picks — and 10,000× with WTF lightning. Every scored payout sits at or below the stake times its row’s top cell (× 10 on WTF): 3,000 / 3,000 (Step 14). The table’s shape is regular: no row has a zero between two paying cells and no cell pays less than the one below it; two cells pay on zero hits — Low / 1 at 0.7× and Medium / 1 at 0.4× — the consolation cells. The capture placed 60 rounds on every one of the 50 settings (Step 15) and reached 218 of the 325 cells, every non-paying cell among them; the 107 cells it did not reach are all paying cells — the rare high-hit prizes, which together carry 28.0355% of the theoretical return. That is why Keno’s RTP is proven by exact enumeration (Step 18), not estimated from the capture. LIQD’s stake limits are covered by the max-win tests in Section 5.

2 columns
ItemValue
Cells325 — 50 rows of 2 to 11 cells
Top cell1,000× (Low, Medium and High at 9 and 10 picks; WTF at 10) · 10,000× with lightning
Consolation cells (pay on 0 hits)Low / 1 0.7× · Medium / 1 0.4×
A zero between paying cells · less pay for more hitsNone · none
Payouts within stake × the row’s top cell (× 10 on WTF)3,000 / 3,000
Settings at 60 rounds50 / 50
Cells reached · paying cells reached218 / 325 · 95 / 202
Share of the theoretical return in never-reached cells28.0355% (pooled, WTF × 13/4)
Result: Every scored payout within its row’s bound; the table is regular apart from the two disclosed consolation cells; every setting played at its planned 60 rounds (Steps 14, 15).
4.10Informational Items (Not Scored)

These items are reported for transparency but are not scored audit steps. They provide context on the returns observed during data collection. At 60 rounds per setting, the capture’s own return is not a measure of fairness — the exact RTPs in 4.1–4.3 are the authoritative evidence.

3 columns
ItemValueContext
Live capture RTP136.94%$410.83 returned on $300.00 wagered — variance illustration only; the single 1,690× WTF win paid $169.00 of it (80.61% without it)
Live return by risk levelClassic 103.23% · Low 103.35% · Medium 77.62% · High 53.82% · WTF 346.71%$60.00 staked on each; High paid on 50 of 600 rounds, WTF on 44
Win rate33.33% (1,000 / 3,000)Rounds paying more than 0, all settings — informational
Largest win$169.00 — 1,690× on a $0.10 stakeWTF / 7 picks, 6 hits, lightning drawn (4.11)
Lightning number in the draw125 / 600 (20.8%)The rule’s exact rate is 1 in 4 (150 rounds). The 600 lightning numbers are uniform over 1–40 (p = 0.3064, Step 21) and each is reproduced from its own HMAC cursor (Step 6)
WTF × 10 payouts16 of 44 paying WTF roundsEach with its lightning number drawn (Step 11)
Rounds per setting60 on each of the 50The pick count cycles 1–10 six times in every epoch
Live-sample fit (Step 21)Board p = 0.7249 · hit law p 0.2100–0.9034The 3,000 captured draws against the uniform board and the exact hit law — reported, never scored
4.11Worked Example — Payout Verification

Real round from the scored capture — bet f2t0JeOrZXSsgu0xyMvoQ (epoch 46, nonce 6), WTF, 7 picks at $0.10 — the capture’s largest win. Verified from liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json — the round whose lightning draw is traced in S2.7, and a round LIQD’s own verifier reproduced in the Step 24 cross-check:

serverSeed = 430373a8b48a104773e948c25dd0b8f7
clientSeed = audit14226566ad31
nonce      = 6
risk       = WTF · picks 28, 7, 4, 13, 22, 2, 21
betAmount  = $0.10
recorded   = lightning 20 · drawn 13, 28, 12, 22, 11, 21, 7, 20, 4, 18
             hits 6 · multiplier 1690 · winningAmount 169.00000000
commitment = 98cf35e524d387ed…

Step 1 — Lightning number: HMAC-SHA256(seed, "audit14226566ad31:6:0") → first 4 bytes d5d73553 = 3,587,650,899 → mod 40 = 19 → lightning 20 — matches the record.

Step 2 — Draw: Cursors 1–10 through the same shuffle → 13, 28, 12, 22, 11, 21, 7, 20, 4, 18 — the served list, in order; the lightning number 20 is the 8th number drawn.

Step 3 — Hits and cell: Picks ∩ drawn = 28, 7, 4, 13, 22, 21 → 6 hits → the declared WTF / 7 cell for 6 hits is 169.

Step 4 — Payout: Lightning drawn → 169 × 10 = 1690 → floor(0.10 × 1690) = $169.00000000, the credited winningAmount and its wallet field, verbatim.

3 columns
StepProcessOutput
1Recompute the lightning number — cursor 020 — matches the record
2Recompute the draw — cursors 1–10The served list, in order
3Count hits and look up the declared cell6 hits → 169×
4Apply the lightning factorLightning 20 is drawn → × 10 = 1,690×
5Settle floor(0.10 × 1,690)$169.00 credited
RTP proof for WTF / 7 picks · Verified
P(k | 7)  = C(7,k)·C(33,10−k) / C(40,10)             k = 0…7row WTF/7 = 0, 0, 0, 0, 3, 18, 169, 700      (cells, 0–7 hits)row RTP   = 7105/155363 × 3 + 1827/310726 × 18 + 105/310726 × 169 + 1/155363 × 700          = 94661/310726 = 30.464461%RTP       = row RTP × (30/40 × 1 + 10/40 × 10) = row RTP × 13/4          = 94661/95608 = 99.009497%   (gap +0.009497 pp, inside ±0.1 pp)
Parity verified: Bet f2t0JeOrZX… — lightning number, drawn order, hits, declared cell, × 10 factor and payout all match; LIQD’s verifier returns the same draw and lightning 20. RTP for WTF / 7 picks = 99.009497%, proven exactly from the hit law, LIQD’s declared cells and its × 10 lightning rule — no casino probability or return figure used.
Live Game$169.00
Our recomputation$169.00
Technical Evidence & Verification5 sections
4.12Evidence Coverage Summary10 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Step 17)n = 1–10: closed form = independent draw-by-draw derivation, sums to 1, mean n·10/40; both negative controls rejectedPass
Exact RTP & claim rule (Step 18)50 / 50 settings within ±0.1 pp of 99% — 98.903053%–99.068864%; outputs/rtp-claim-table.json re-derived and pinnedPass
Paytable provenance (Step 11)Declared table pinned by SHA-256 and named by the capture; prices 3,000 / 3,000 roundsPass
Settlement (Step 12)3,000 / 3,000 credits = floor(stake × cell [× 10]) in 1e-8 units; 2,000 zero-cell rounds pay 0Pass
Paytable shape & bounds (Step 14)3,000 / 3,000 payouts within stake × the row’s top cell; two consolation cells, no other irregular cellPass
Configuration & cell coverage (Step 15)50 / 50 settings at 60 rounds; 218 / 325 cells reachedPass
Simulation Pass 1 (Step 19)10 × 1M rounds — uniformity, hit law and serial clear; 50 / 50 paytable rows within ±5 SEPass
Simulation Pass 2 (Step 20)40 seeds × 10,000 nonces: p = 0.8754 and 0.0711 against 0.001Pass
WTF lightning settlement (Steps 6, 11, 18, 24)Row × 13/4 on 10 / 10 WTF rows; 16 / 16 × 10 rounds with lightning drawn; LIQD’s verifier 600 / 600Pass
Artifacts of record (Step 23)Simulation and verification outputs match their pins; 1,665 numeric values inside their declared domainsPass
4.13Code References12 source files
2 columns
FilePurpose
tests/steps/rtp.tsSteps 17, 18, 21: the hit-law model and its negative controls, the exact RTP of every setting against the claim, the live-sample fit
src/exact.tshypergeometric, sequentialDraw, rtpExact, varianceExact — BigInt rationals, no floating point
src/rtp-claim.tsThe RTP claim rule, RTP-CLAIM 1.2.0 — a byte-identical copy of the framework’s rule file
src/rtp-settings.tsThe claim (LIQD’s flat 99%) and the 50-setting exact RTP table it is held to
src/client-paytable.tsParses LIQD’s client paytable literal as data (clientRowQ) — the cells every RTP is priced from
tests/steps/payouts.tsSteps 11–16: paytable provenance, payout arithmetic, bet-size checks, payout bounds and coverage
tests/steps/simulation.tsSteps 19–20: Pass 1 statistics and RTP convergence re-derived from the artifact’s integer rows; Pass 2 recomputed in every run
src/sim.tsPass 1 streams from the pinned master seed; the Pass 2 relocation statistics
src/sim-inputs.tsPass 2 inputs from the scored capture — the 40 Classic–High epoch seeds, their client seeds and every played round’s row
src/simulate.tsnpm run simulate — produces the simulation artifact; --compare regenerates it and diffs it field by field
src/stats.tsBoard uniformity corrected for drawing without replacement, χ² fits, lag-1 from integer sums
src/domains.tsDeclared value domains for every numeric field of the artifacts of record (Step 23)
4.14Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json — Pass 1 (10 streams × 1,000,000 rounds: number counts, hit histograms, serial sums, 50,000-round prefixes) + Pass 2 (40 seeds × 10,000 nonces, 10,000 relocations) · SHA-256 a4a2b1119491c983…
RTP claim table: outputs/rtp-claim-table.json — all 50 settings with exact RTP, gap and outcome under RTP-CLAIM 1.2.0 · SHA-256 5025110bbc6ba4d6…
Declared paytable: evidence/client-paytable/liqd-keno-client-paytable-2026-10-16.js.txt (E31) — LIQD’s client paytable, the 325 cells every RTP is priced from · SHA-256 767b723066344398…
Primary dataset: data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json — payout reconciliation, the Pass 2 seeds and the informational returns
Claim evidence: evidence/page-read-2026-10-03/liqd-keno-page-description-edge-2026-10-03.png (E26) — the Keno game page’s Description box, “Edge: 1.00%” · SHA-256 4ad8010ddb418ba5…
LIQD verifier replies: evidence/operator-verify-api-raw-2026-10-21/…ndjson (E34) — the multipliers LIQD’s verify endpoint returned for all 600 scored WTF rounds, × 10 exactly on the 16 paying lightning rounds · SHA-256 04661d2b15fe26fd…
4.15Verified Invariants14 invariants
2 columns
InvariantResult
P(k | n) = C(n,k)·C(40−n,10−k)/C(40,10) equals the independent draw-by-draw derivation, cell for cell, for n = 1–10 (non-circular)Pass
Each hit distribution sums to exactly 1 and has mean exactly n·10/40Pass
Negative controls: a shifted distribution is rejected; a nudged cell changes the exact RTPPass
The exact RTP of all 50 settings equals outputs/rtp-claim-table.jsonPass
LIQD’s advertised 1.00% edge holds at all 50 settings — worst gap −0.096947 pp against ±0.1 ppPass
Every WTF row returns its table RTP × 13/4 under the lightning settlement — 10 / 10 within the bandPass
The served multiplier is the declared cell on 3,000 / 3,000 rounds (× 10 on the 16 paying lightning rounds)Pass
winningAmount == floor(stake × cell [× 10]) in 1e-8 units on 3,000 / 3,000; every product exact on the money gridPass
Every payout ≤ stake × its row’s top cell (× 10 on WTF)Pass
10 / 10 Pass 1 streams clear board uniformity, the hit law and the lag-1 screen at 3.333 × 10⁻⁵Pass
50 / 50 paytable rows’ simulated RTP within ±5 SE of the exact RTP (largest |z| 1.73)Pass
The first 50,000 rounds of 10 / 10 streams replay exactly; the full artifact regenerates field for field (REPRODUCED)Pass
No significant return deficit on the 40 Classic–High casino seeds (Pass 2 — p = 0.8754 and 0.0711 against 0.001; 40 / 40 seed rows reproduced)Pass
The draw takes no stake argument — computeDraw(serverSeed, clientSeed, nonce) (Step 10)Pass
4.16Reproduction InstructionsRun locally

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

reproduce-s4.sh · 6 lines · Verified
git clone https://github.com/ProvablyFair-org/liqd-keno.gitcd liqd-keno && git checkout v1.1.0npm cinpm run test:full   # regenerates the 10M-round simulation from its pinned master seed, compares it, then verifieshead -40 outputs/rtp-claim-table.jsoncat outputs/simulation-results.json | head -40
The RTP claim rule and the S4-related steps, as printed, without their detail lines:

  rtp-claim: ENFORCED — PASS
  rtp-claim:   band ±0.10 pp · 50 settings · true min 98.903053% (risk=low picks=5) · true max 99.068864% (risk=low picks=9)
  rtp-claim:   house side beyond band: 0 · player-favourable beyond band: 0 · worst risk=low picks=5 at -0.096947 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 5025110bbc6ba4d6… matches the pin

  [PASS] Step 11: Paytable provenance & served-vs-declared
  [PASS] Step 12: Payout arithmetic
  [PASS] Step 14: Paytable shape & bounds
  [PASS] Step 15: Configuration & cell coverage
  [PASS] Step 17: Anti-circularity (hit probability)
  [PASS] Step 18: Per-config RTP by exact enumeration vs claim
  [PASS] Step 19: Simulation Pass 1 (fresh seeds)
  [PASS] Step 20: Simulation Pass 2 (cherry-pick)
  [PASS] Step 21: Live-sample fit (informational)
  [PASS] Step 23: Artifacts of record

The simulation is deterministic: npm run test:full regenerates outputs/simulation-results.json and compares it field by field with the committed artifact — REPRODUCED on 22 October 2026 (Node 22.22.0) — and the verdict is scored against the committed, SHA-256-pinned artifact. The chart above is drawn from that artifact and the exact RTP table.
05

Fairness Integrity Testing

Does the implementation maintain fairness under non-standard conditions?

Sections 1–4 establish that the game is mathematically fair; Section 5 tests whether the live implementation keeps its integrity under non-standard conditions. It covers 34 integrity tests run against LIQD’s running service across 12 categories: race conditions, input validation, idempotency, authorization, outcome and payout fields, nonce and seed handling, information disclosure, and the provably fair surface itself. 33 passed. One was remediated: the paytable in place at testing missed the displayed 1.00% edge by more than 0.1 percentage points on 10 settings; LIQD corrected it, and the audited table returns within 0.1 percentage points of 99% on all 50 settings. No test altered, replayed or injected an outcome.

Fairness Integrity Testing

33pass1remediated

What we verified

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

What this means for you

  • Across the 34 tests, no request path let anyone alter, replay or inject an outcome
  • Once a bet is placed, its draw cannot be changed or replayed
  • Each bet gets its own server-assigned nonce under a committed seed
  • Your bets are bound to your session — no other account can act on them
  • LIQD corrected the paytable finding: the audited table returns within 0.1 percentage points of 99% on all 50 settings
33pass1remediated
  • Race conditions — can parallel bets, a high-stake over-commit, extreme bursts, or a seed rotation racing a live bet change an outcome or double-settle?
  • Nonce tampering — can the sequence be forced, replayed, or skipped?
  • Seed handling — can unusual client seeds, or the active server seed, be exposed or overridden?
  • Outcome replay — can an identical bet be settled twice, or a timed-out bet settle twice?
  • Cross-player isolation — can one player act on another’s account, or after logging out?
  • Payout tampering — can drawn numbers, hits, multiplier or payout be injected?
  • Parameter limits — can invalid picks, risk modes, stakes or currencies be submitted?
5 / 5Race Conditions
5 / 5Input Validation
3 / 3Atomic Balance / Idempotency
2 / 2Broken Authorization
4 / 4Max-win Bypass
4 / 4Nonce Manipulation
4 / 4RNG / PF Verification
1 / 1RNG / Information Leak
1 / 1Entropy / PF Verification
1 / 1Precision / Payout
1 / 1Event Replay
3 / 3Adversarial Seeds

Category Coverage

Race Conditions5/5
Input Validation5/5
Atomic Balance / Idempotency3/3
Broken Authorization2/2
Max-win Bypass4/4
Nonce Manipulation4/4
RNG / PF Verification4/4
RNG / Information Leak1/1
Entropy / PF Verification1/1
Precision / Payout1/1
Event Replay1/1
Adversarial Seeds3/3
CheckResultEvidence
Nonce integrityPassServer-assigned and sequential: injected reused, skipped, zero or negative and overflow-sized nonces are all refused (NONCE-001 … NONCE-004); the nonce advances by one per bet (PF-002) and restarts at 0 on rotation (ENTROPY-004)
Seed integrityPassThe active seed is never disclosed before rotation (DISCLOSE-001); 70 / 70 rotations racing live bets revealed the committed seed and never reused one, and every raced round settled on the seed it reported (EXTRA-001); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003)
Outcome determinismPassAll 2,770 rounds captured during the integrity testing with a revealed seed recompute in draw order, with LIQD’s verifier agreeing on every live round checked against it (EXTRA-002, PF-001, PARAM-003)
Round & player isolationPass13 / 13 foreign and malformed identifiers rejected; post-logout reuse rejected; parallel identical and high-stake bets settled once each and reconciled to the wallet
Payout integrityPassInjected draw, hits, payout, status and multiplier fields rejected; 200 consecutive rounds settled with no payout mismatch and reconciled to the balance
Paytable returnRemediatedThe paytable in place at testing missed the displayed 1.00% edge by more than 0.1 percentage points on 10 settings (Medium); LIQD corrected it, and the audited table returns within 0.1 percentage points of 99% on all 50 settings (FAIR-001; Section 4)
Nonce integrityPass
Server-assigned and sequential: injected reused, skipped, zero or negative and overflow-sized nonces are all refused (NONCE-001 … NONCE-004); the nonce advances by one per bet (PF-002) and restarts at 0 on rotation (ENTROPY-004)
Seed integrityPass
The active seed is never disclosed before rotation (DISCLOSE-001); 70 / 70 rotations racing live bets revealed the committed seed and never reused one, and every raced round settled on the seed it reported (EXTRA-001); unusual and Unicode client seeds handled (SEED-001, SEED-002), and each bet uses the seed set just before it (SEED-003)
Outcome determinismPass
All 2,770 rounds captured during the integrity testing with a revealed seed recompute in draw order, with LIQD’s verifier agreeing on every live round checked against it (EXTRA-002, PF-001, PARAM-003)
Round & player isolationPass
13 / 13 foreign and malformed identifiers rejected; post-logout reuse rejected; parallel identical and high-stake bets settled once each and reconciled to the wallet
Payout integrityPass
Injected draw, hits, payout, status and multiplier fields rejected; 200 consecutive rounds settled with no payout mismatch and reconciled to the balance
Paytable returnRemediated
The paytable in place at testing missed the displayed 1.00% edge by more than 0.1 percentage points on 10 settings (Medium); LIQD corrected it, and the audited table returns within 0.1 percentage points of 99% on all 50 settings (FAIR-001; Section 4)
✓  34 of 34 Pass or Remediated

34 integrity tests: 33 pass and 1 remediated (paytable return — corrected by LIQD; the audited table returns within 0.1 percentage points of 99% on all 50 settings). No test altered, replayed or injected an outcome. Test procedures and payloads are withheld by policy — see 5.5.

How It Works — Fairness Integrity Testing2 sections
5.1Framework Overview

Testing follows the ProvablyFair.org Fairness Integrity Framework — a structured methodology derived from real, historically observed failures in provably fair systems. Cryptographic verification (S1–S3) shows the game computed every captured draw from its disclosed inputs; fairness-integrity testing asks whether the surface around that computation holds up under non-standard use — seeds swapped, rounds replayed, parameters injected, settlements raced. These tests ran against LIQD’s running service, not against a dataset. This section covers 34 tests across 12 categories. Each card below gives the control assessed, the broad testing approach, the observed result, its impact and any limitation.

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

Two scales apply in this section. The audit framework decides whether a result can pass at all — any test that lets money settle against a tampered or unverifiable outcome is an automatic hard fail. The integrity matrix records each test’s status — Pass, Remediated, Warning or Fail — and each finding is graded Critical, High, Medium or Low. On this page: one Remediated test, graded Medium — the paytable’s return against the displayed edge, a disclosure finding rather than a draw issue, corrected by LIQD and re-verified.

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

34 tests · 33 pass · 1 remediated

Race Conditions5/5
LIQD-KENO-EXTRA-001Pass

Seed rotation racing live bets

Control assessed
A rotation racing bets cannot reveal a seed early or inconsistently, or bring a revealed seed back into use, and each raced round settles on the seed it reports.
Testing approach
Seventy rotations fired against bets in flight; each revealed seed checked against its pre-race commitment and each raced round recomputed.
Observed result
All 70 rotations succeeded. Each revealed seed matched its pre-race commitment, none reappeared as a later active seed, and every settled raced round recomputed under the seed it reported; four raced bets returned a generic server error.
Impact
None on fairness — seed tracking held; the four errors were generic rather than structured responses.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-RACE-001Pass

Nonce uniqueness under parallel bets

Control assessed
Every accepted bet gets its own server-assigned nonce in sequence, even under parallel load.
Testing approach
Bursts of parallel bets from one account; nonces and response order compared.
Observed result
All 55 bets were accepted with unique nonces and no server error; ordered by arrival, the nonces ascend across the whole run.
Impact
None — no nonce was shared or skipped.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-RACE-002Pass

Identical bets in parallel, at scale

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

High-stake parallel over-commit

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

Burst load and error disclosure

Control assessed
Excess load is held back by rate limiting or answered with structured errors, with no duplicate nonce and no internal detail in error bodies.
Testing approach
Bursts of bets from one account at increasing volume; status codes, error bodies and nonces checked.
Observed result
The smaller bursts all settled; the largest was answered with structured busy and rate-limit responses, with no generic server errors. Accepted nonces were unique, and no error body disclosed internal detail.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
Input Validation5/5
LIQD-KENO-PARAM-001Pass

Picks and risk-mode validation

Control assessed
Valid selections are accepted; out-of-range and wrong-type selections are refused.
Testing approach
The valid boundary selections, out-of-range and wrong-type picks and risk modes, with a control bet.
Observed result
The control and every valid boundary were accepted, and every out-of-range and wrong-type selection was rejected by input validation.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-PARAM-002Pass

Stake edge cases

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

Maximum pick count, recomputed

Control assessed
Eleven picks are refused; a 10-pick round settles and reproduces from its revealed seed.
Testing approach
An 11-pick and a 10-pick bet; the 10-pick round recomputed after the reveal and checked against LIQD’s verify endpoint.
Observed result
Eleven picks were rejected and ten accepted; the revealed seed matched its commitment, and the round recomputed to the settled draw and payout, with LIQD’s verifier agreeing.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-PARAM-004Pass

Content-type variations around validation

Control assessed
Changing how the request body is encoded cannot let an invalid selection settle.
Testing approach
Invalid selections sent under each alternative content type, with a control bet.
Observed result
The control settled; both invalid selections were rejected under every content type, so no body-parsing path skipped validation.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-TOKEN-001Pass

Currency identifier manipulation

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

Same-key retry after a timeout

Control assessed
A retry with the same key returns the original round, with no second debit.
Testing approach
Bets sent with very short client timeouts, then retried with the same key; wallet reconciled over the nonces used.
Observed result
All 20 retries returned their original round; all 12 timed-out bets had settled server-side, and the wallet reconciled exactly.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-IDEMP-002Pass

Orphaned-debit check at scale

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

Disconnect while a bet is processed

Control assessed
Every consumed nonce is explained, and no debit is left unexplained.
Testing approach
Bets abandoned by the client at different moments; rounds and balance reconciled.
Observed result
The abandoned bet still settled, and the other three settled normally; the balance reconciled exactly over the consumed nonces.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
Broken Authorization2/2
LIQD-KENO-AUTH-001Pass

Foreign and malformed user identifiers

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

Session reuse after logout

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

Single outcome field injected

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

Pick-selection field injection

Control assessed
No injected field changes the selection, and malformed modes are refused.
Testing approach
Bets carrying alias selection fields, malformed modes and a parameter override.
Observed result
All three alias fields and every malformed mode were rejected; the override bet settled exactly as sent. No selection diverged from the request.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-MAXWIN-001Pass

Stake boundary enforcement

Control assessed
Out-of-range stake values are refused without touching the balance.
Testing approach
Oversized, negative and malformed stakes alongside a free-play control.
Observed result
The control settled; all four invalid stakes were rejected and none consumed balance.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-MAXWIN-002Pass

Several outcome fields injected at once

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

Reused nonce

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying a reused nonce.
Observed result
The replayed nonce was rejected at validation and never echoed; the rejected request moved no balance.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-NONCE-002Pass

Skipped-ahead nonce

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying a skipped-ahead nonce.
Observed result
The skipped-ahead nonce was rejected at validation and never echoed; the server did not skip ahead.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-NONCE-003Pass

Zero and negative nonces

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying zero and negative nonces.
Observed result
All three zero and negative nonces were rejected at validation; none was honoured.
Impact
None — the server kept its own sequence.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-NONCE-004Pass

Overflow-sized nonces

Control assessed
The client cannot choose the nonce: an injected value is refused, and the server keeps its own sequence.
Testing approach
Bets carrying overflow-sized nonces.
Observed result
All three overflow-sized nonces were rejected at validation, with no server error.
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-KENO-FAIR-001MediumRemediated

Draw distribution and paytable return

Control assessed
The draw shows no bias, and every setting returns the 99% LIQD displays (a 1.00% edge).
Testing approach
A 2,000-draw live sample recomputed from its revealed seed and tested for uniformity and hit counts; the paytable’s exact return computed for every setting.
Observed result
The draw showed no bias: all 2,000 draws recomputed exactly, the 20,000 drawn numbers passed the uniformity test, and hit counts fit the exact distribution. The paytable in place at the time of testing missed the displayed 1.00% edge by more than 0.1 percentage points on 10 settings (lowest 98.653846%). LIQD adjusted the paytable, and the audited table returns within 0.1 percentage points of 99% on all 50 settings (Section 4).
Impact
Medium · return disclosure, not a draw issue. Remediated: LIQD corrected the paytable, and the corrected table was re-verified.
Limitations
The re-verification is the exact computation over the audited paytable in Section 4, under the RTP claim rule’s 0.1 percentage-point band.
LIQD-KENO-EXTRA-002Pass

Independent draw and outcome check

Control assessed
Live draws can be reproduced independently, in order, from the revealed inputs.
Testing approach
Rounds recomputed with our own implementation and checked against LIQD’s verify endpoint, then every round captured during the integrity testing recomputed.
Observed result
All five rounds recomputed to the settled draw, in order, with LIQD’s verifier agreeing; all 2,770 rounds captured during the integrity testing with a revealed seed reproduced.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-PF-001Pass

Provably-fair data and commit / reveal

Control assessed
Each bet carries every provably-fair field, the revealed seed matches its commitment, and each outcome reproduces.
Testing approach
Three bets checked field by field, then the seed revealed and the rounds recomputed and checked against LIQD’s verify endpoint.
Observed result
All fields were present and the nonce advanced by exactly 3; the revealed seed hashed to its commitment, and all three rounds recomputed to the settled draw, with LIQD’s verifier agreeing.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-PF-002Pass

Nonce sequence within a seed period

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

Active server-seed disclosure

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

Nonce reset after seed rotation

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

Payout consistency at scale

Control assessed
Every payout follows the game’s settlement rule, and the wallet reconciles over the run.
Testing approach
Two hundred consecutive rounds; each payout and the aggregate balance change checked.
Observed result
All 200 rounds settled with no payout mismatch; the balance change matched the summed outcome exactly.
Impact
None — settlement arithmetic held.
Limitations
None specific to this test; the general scope is in 5.5.
Event Replay1/1
LIQD-KENO-REPLAY-001Pass

Duplicate identical bets sent together

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

Client-seed length and character sets

Control assessed
Client seeds have a consistent length limit, and the game stays usable after every accepted or refused change.
Testing approach
Client seeds of increasing length and unusual character sets, each followed by a bet.
Observed result
Seeds up to 255 characters were accepted and longer ones consistently rejected; empty and path-like seeds were rejected, while other character sets, markup included, were accepted and stored as plain text. Every following bet settled.
Impact
None — the control held.
Limitations
None specific to this test; the general scope is in 5.5.
LIQD-KENO-SEED-002Pass

Unicode forms of one seed

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

Rapid seed rotation

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

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

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

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

3 columns
TestsFindingStatus
FAIR-001The paytable in place at testing missed the displayed 1.00% edge by more than 0.1 percentage points on 10 settings. LIQD corrected it; all 50 settings are now within 0.1 percentage points of 99%.Remediated · Medium
5.5Scope & LimitationsScope note
What is published here is coverage, not method. Each test in this section appears with its identifier and category, the control it assessed, the broad testing approach, the observed result, its impact and any limitation; a finding also carries its severity, where graded, and its remediation state. Request payloads, preconditions, step sequences, endpoints, timings and response captures are withheld: they show how to repeat a test, and add nothing a reader needs to judge its result. The full record is retained and is available to LIQD and to regulators on request.
Coverage published, method withheld: The classes of issue tested are standard and publicly documented. What would help someone repeat a test — volumes, timings, settings and request sequences — stays in the engagement record.
Point-in-time, per build: Each result is scoped to the build tested; the paytable correction is verified on the audited table (Section 4).
Scope: The tests target the provably fair surface and the money path around it — seeds, rounds, placement and settlement. Wallet custody, account management and infrastructure hardening are outside their scope.
Test environment: Every test ran against LIQD’s pre-launch environment (qa.liqd.com), not the public site.
5.6Evidence & Disclosure

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

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

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

Player Verification

Can a player verify their own bets without trusting anyone?

Every Keno round can be independently reproduced using publicly disclosed inputs. No hidden variables, no private backend data. If the revealed seed matches the hash you saw before betting and your recomputed draw matches the game — the ten numbers in order, your hits and, on WTF, the lightning number — the round was provably fair. This section walks you through the process — with LIQD’s own panel, with the independent ProvablyFair.org verifier, or with a few lines of code you run yourself.

Independent Result Verification

4 Stepsto verify any bet

Key principles

  • Every Keno round can be independently reproduced
  • No hidden variables — no private backend data
  • If the revealed seed matches the hash you saw before betting and your recomputed draw matches the game, the round was provably fair
  • Most players can verify directly through LIQD’s Provably Fair panel

What you need

  • Server Seed — revealed after seed rotation (casino entropy)
  • Client Seed — your player-controlled seed
  • Nonce — the bet number in sequence (ensures uniqueness)
  • Picks & Risk — the 1–10 numbers you picked and the risk you played (Classic, Low, Med, High or WTF)
4 Stepsto verify any bet
  • Every Keno round can be independently reproduced
  • No hidden variables — no private backend data
  • If the revealed seed matches the hash you saw before betting and your recomputed draw matches the game, the round was provably fair
  • Most players can verify directly through LIQD’s Provably Fair panel
From Bet to Independent Verification — 4-step flow — illustrated flow
Server SeedRevealed after rotation
Client SeedPlayer-controlled input
NonceRecorded bet sequence
Picks & Risk1–10 numbers · Classic, Low, Med, High or WTF
  1. 1

    Place a Keno Bet

    Pick 1 to 10 numbers on the 40-number board — by hand or with the Number Picker — choose a risk level and play. The ten numbers are drawn from the seeds and the nonce alone: the draw takes no pick or stake input. Your picks decide your hits, the risk decides the paytable row, and on WTF the lightning number is drawn first, so the ten numbers come one cursor later.

  2. 2

    Open the Provably Fair Panel and Rotate the Seed

    Open the Provably Fair panel from the game page. Its Seeds tab shows your active seed pair and its commitment — the hashed server seed — with the hash of the next server seed already published. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saw before betting — across this audit’s 51 rotations it always did.

  3. 3

    Enter the Seeds in the Verify Tab

    Open the Verify tab, which recomputes any round from a client seed, server seed, nonce, risk and picks, and enter the revealed server seed, your client seed, the round’s nonce, the risk and your picks.

  4. 4

    Recompute & Confirm

    Press Verify — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed and the recomputed draw matches your round — the ten numbers, your hits and the multiplier, and on WTF the lightning number — the round was provably fair. LIQD’s tab shows the ten numbers as a set; the scripts under Manual Verification also check their order and the credit.

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

Pick 1 to 10 numbers on the board — tap them, or set the Number Picker and press Pick for a random set — choose a risk level and play. The platform draws the ten numbers from (serverSeed, clientSeed, nonce): your picks decide your hits and the risk decides the paytable row, and on WTF the lightning number is drawn first. The panel below is LIQD’s Keno bet panel with three numbers picked — 11, 12 and 13 — on Classic; the row under the board is that setting’s paytable: 0×, 1×, 3.1× or 10.4× for 0 to 3 hits.

LIQD Keno on liqd.com — picks 11, 12 and 13 on Classic, with the paytable row for three picks under the board (0×, 1×, 3.1×, 10.4×).

LIQD Keno on liqd.com — picks 11, 12 and 13 on Classic, with the paytable row for three picks under the board (0×, 1×, 3.1×, 10.4×).

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

The panel has two tabs, Seeds and Verify. The Seeds tab holds your active seed pair and its commitment. Rotation ends the epoch: the outgoing server seed is revealed for verification, and the next server seed — whose hash LIQD had already published — becomes active with the client seed you set. That advance commitment is the chain the audit’s Step 2 checks across all 51 rotations, including the link into the first epoch.

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

Active Client Seed — your player-controlled input; the seed set when you rotate becomes the active one.

Total bets made with pair — the bet counter under the active pair: it starts at 0 after every rotation and locates each round’s nonce within the epoch.

Rotate Seed Pair — ends the epoch and reveals the server seed behind the hash you saw.

The Seeds tab before rotating — Active Server Seed (Hashed) 359a1ff3…cf02c2 after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 4c895a3c…73a803, hashes to 359a1ff3…cf02c2.

The Seeds tab before rotating — Active Server Seed (Hashed) 359a1ff3…cf02c2 after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 4c895a3c…73a803, hashes to 359a1ff3…cf02c2.

6.3Step 3: Enter the Seeds in the Verify Tab

The Verify tab takes the inputs — client seed, server seed, nonce, the risk and your picks — and recomputes a round on demand. Enter the server seed revealed in step 2, your client seed, the round’s nonce, the risk and your picks.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 0, risk Classic and the three picks.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 0, risk Classic and the three picks.

6.4Step 4: Recompute & Confirm

Press Verify. The tab recomputes the ten numbers from the three seed inputs — on WTF the lightning number first — then counts your hits and reads the multiplier from the paytable: it marks the ten drawn numbers on the board (as a set, not in draw order) and the lightning number on WTF, and shows your hits and multiplier. A match shows the draw is the one the committed seed produces. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python, pre-loaded with two rounds from this report — and they add the commitment check, the draw order and the credit.

The result: ten numbers drawn, 12 and 13 hit — “2 of 3 hit: 3.1x”. Our scripts in 6.8–6.9 reproduce it exactly: draw order 13, 22, 14, 18, 40, 12, 15, 10, 26, 17, two hits, 3.1×.

The result: ten numbers drawn, 12 and 13 hit — “2 of 3 hit: 3.1x”. Our scripts in 6.8–6.9 reproduce it exactly: draw order 13, 22, 14, 18, 40, 12, 15, 10, 26, 17, two hits, 3.1×.

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

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

6.6How the Algorithm Works (Plain English)

Six moving parts, in the order they act:

  • Commit — before you play, LIQD publishes SHA-256 of the server seed, hashed as the 32-character hex string. The seed itself stays secret until rotation.
  • Draw — for cursor i = 0 … 9: HMAC-SHA256(key = decoded seed, message = clientSeed:nonce:i); the first 4 bytes, read big-endian, modulo 40 − i give j′, and board positions i and i + j′ swap on the board 1 … 40. The first ten board entries, in order, are the drawn numbers.
  • Hits — the number of your picks among the ten drawn numbers; the draw itself never reads your picks.
  • Cell — the multiplier is the declared paytable cell for your risk, pick count and hits, PAYTABLE[risk][picks][hits] — LIQD’s table read 16 October 2026.
  • Pay — the credit is floor(stake × multiplier) at the 8th decimal.
  • WTF — cursor 0 draws the lightning number (its value modulo 40, plus 1), the ten numbers use cursors 1–10, and a paying round whose lightning number is drawn pays 10 × its cell.

Note the two hashing domains: the commitment hashes the hex string as UTF-8 text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game — both scripts below get it right, and both list the expected output of two rounds from this report in their comments so you can self-check the tool before trusting it. The other classic slip is the swap: position i trades with position i + j′, counted from i, on a board numbered 1 to 40. LIQD’s own code also rejects the few 32-bit values where plain modulo is uneven; that changes a round about once in 4.8 × 10⁷ (once in 4.1 × 10⁷ on WTF) and changed none of the 3,000 scored rounds (2.3).

6.7Casino Verifier vs ProvablyFair.org Verifier4 records

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

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureDirectly — its verify endpoint returned the served round on all 1,144 scored rounds it was asked about: every WTF round and every observed Classic–High cell (Step 24, E34)The scripts in 6.8–6.9 implement the derivation this audit specifies — HMAC-SHA256 draw, WTF lightning rule, the declared paytable — and reproduce all 3,000 scored rounds: commitment, draw order, lightning, hits, multiplier and credit
If they disagreeSuspect the casino’s tool or a tampered roundThe repo + scripts are the tiebreaker anyone can run
6.8JavaScript Verification Script

Complete, dependency-free, and pre-loaded with the featured round as its default inputs — run it as-is to see a full match, then swap in your own round. Run on every one of the 3,000 scored rounds, it reproduces each one: commitment, drawn numbers in order, lightning number, hits, multiplier and credit.

verify-keno.js · Standalone Node.js
// verify-keno.js — standalone LIQD Keno verifier (Node.js, no dependencies)// Usage: node verify-keno.js <serverSeed> <clientSeed> <nonce> <risk> <picks, comma-separated> [stake]const { createHash, createHmac } = require('node:crypto'); const PAYTABLE = {   // LIQD's declared paytable, read 16 October 2026: PAYTABLE[risk][picks][hits]  classic: [[], [0,3.96], [0,1.9,4.5], [0,1,3.1,10.4], [0,.8,1.8,5,22.5], [0,.25,1.4,4.1,16.5,36],    [0,0,1,3.68,7,16.5,40], [0,0,.47,3,4.5,14,31,60], [0,0,0,2.2,4,13,22,55,70],    [0,0,0,1.55,3,8,15,44,60,85], [0,0,0,1.4,2.25,4.5,8,17,50,80,100]],  low: [[], [.7,1.86], [0,2,3.83], [0,1.1,1.39,26], [0,0,2.2,7.9,90], [0,0,1.5,4.2,13,300],    [0,0,1.1,2,6.2,100,700], [0,0,1.1,1.6,3.5,15,225,700], [0,0,1.1,1.5,2,5.5,39,100,800],    [0,0,1.1,1.3,1.7,2.5,7.5,50,250,1e3], [0,0,1.1,1.2,1.3,1.8,3.9,13,50,250,1e3]],  medium: [[], [.4,2.76], [0,1.8,5.16], [0,0,2.8,50], [0,0,1.71,10,100], [0,0,1.4,4,14,390],    [0,0,0,3,9,181,710], [0,0,0,2,7,30,400,800], [0,0,0,2,4,11,67,400,900],    [0,0,0,2,2.5,5,15,100,500,1e3], [0,0,0,1.6,2,4,7,26,100,500,1e3]],  high: [[], [0,3.96], [0,0,17.16], [0,0,0,81.5], [0,0,0,10,259], [0,0,0,4.51,48,450],    [0,0,0,0,11,350,710], [0,0,0,0,7,90,400,800], [0,0,0,0,5,20,270,600,900],    [0,0,0,0,4,11,56,500,800,1e3], [0,0,0,0,3.5,8,13,63,500,800,1e3]],  wtf: [[], [0,1.218], [0,0,5.28], [0,0,1.34,10], [0,0,0,4,64], [0,0,0,2,10,131],    [0,0,0,0,8,45,470], [0,0,0,0,3,18,169,700], [0,0,0,0,0,15,75,330,800],    [0,0,0,0,0,7,31,150,500,900], [0,0,0,0,0,0,35,104,300,600,1e3]],}; const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, risk, picksArg, stake = '0.10'] = argv.length  ? argv  : ['92c36e93b39a5cde21d275e9747a5794', 'audit42a83c667988', '14', 'classic', '5,40,4,33,10'];const nonce = Number(nonceArg), picks = picksArg.split(',').map(Number), wtf = risk === 'wtf'; // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — the hash LIQD showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) The draw — HMAC-SHA256 keyed on the DECODED bytes, message clientSeed:nonce:cursor, first 4 bytes big-endianconst value = cursor => createHmac('sha256', Buffer.from(serverSeed, 'hex'))  .update(`${clientSeed}:${nonce}:${cursor}`).digest().readUInt32BE(0);const lightning = wtf ? value(0) % 40 + 1 : null;           // WTF only: cursor 0 draws the lightning numberconst board = Array.from({ length: 40 }, (_, k) => k + 1);  // the board 1…40for (let i = 0; i < 10; i++) {                              // forward Fisher–Yates: swap i with i + j′  const j = i + value((wtf ? 1 : 0) + i) % (40 - i);        // WTF draws at cursors 1–10  [board[i], board[j]] = [board[j], board[i]];}const drawn = board.slice(0, 10);console.log('drawn      =', drawn.join(', '));if (wtf) console.log('lightning  =', lightning, drawn.includes(lightning) ? '(drawn)' : '(not drawn)'); // 3) Hits pick the cell; a paying WTF round with its lightning number drawn pays 10 × the cellconst hits = picks.filter(p => drawn.includes(p)).length;const cell = PAYTABLE[risk][picks.length][hits];const factor = wtf && cell > 0 && drawn.includes(lightning) ? 10 : 1;const [ci, cf = ''] = String(cell).split('.');              // the cell digit for digit: 16.5 → 165 / 10const num = BigInt(ci + cf) * BigInt(factor), den = 10n ** BigInt(cf.length);console.log(`hits       = ${hits} | multiplier = ${Number(num) / Number(den)}${factor === 10 ? ` (cell ${cell} × 10)` : ''}`); // 4) Payout = floor(stake × multiplier) at the 8th decimal, in exact integer arithmeticconst [si, sf = ''] = stake.split('.');const paid = BigInt(si + sf.padEnd(8, '0')) * num / den;    // stake in 1e-8 units; BigInt division floorsconsole.log('payout     =', `${paid / 100000000n}.${String(paid % 100000000n).padStart(8, '0')}`);// Expected for the defaults (bet E5tGJUmA4gmbhcUreVar7, Classic / 5, epoch 7 nonce 14, at $0.10)://   commitment = 5989c035b566e46540f217cd7aba6bf049c9dd393d1a1d1ceb7f0e5431a46793//   drawn      = 33, 35, 8, 10, 36, 39, 30, 4, 26, 40//   hits       = 4 | multiplier = 16.5//   payout     = 1.65000000// Expected for bet f2t0JeOrZXSsgu0xyMvoQ (WTF / 7, epoch 46 nonce 6, at $0.10)://   node verify-keno.js 430373a8b48a104773e948c25dd0b8f7 audit14226566ad31 6 wtf 28,7,4,13,22,2,21 0.10//   commitment = 98cf35e524d387eda36334e55c1bef9ee8c03215f8640eec2db2fed4405e1ba2//   drawn      = 13, 28, 12, 22, 11, 21, 7, 20, 4, 18//   lightning  = 20 (drawn)//   hits       = 6 | multiplier = 1690 (cell 169 × 10)//   payout     = 169.00000000
6.9Python Verification Script

The same verifier in Python 3, standard library only — the payout is computed in exact rational arithmetic, so it doubles as an independent check on the JavaScript version. It reproduces all 3,000 scored rounds as well:

verify-keno.py · Standalone Python
# verify-keno.py — standalone LIQD Keno verifier (Python 3, stdlib only)# Usage: python3 verify-keno.py <serverSeed> <clientSeed> <nonce> <risk> <picks, comma-separated> [stake]import hashlib, hmac, sysfrom fractions import Fraction PAYTABLE = {   # LIQD's declared paytable, read 16 October 2026: PAYTABLE[risk][picks][hits]    'classic': [[], [0,3.96], [0,1.9,4.5], [0,1,3.1,10.4], [0,.8,1.8,5,22.5], [0,.25,1.4,4.1,16.5,36],        [0,0,1,3.68,7,16.5,40], [0,0,.47,3,4.5,14,31,60], [0,0,0,2.2,4,13,22,55,70],        [0,0,0,1.55,3,8,15,44,60,85], [0,0,0,1.4,2.25,4.5,8,17,50,80,100]],    'low': [[], [.7,1.86], [0,2,3.83], [0,1.1,1.39,26], [0,0,2.2,7.9,90], [0,0,1.5,4.2,13,300],        [0,0,1.1,2,6.2,100,700], [0,0,1.1,1.6,3.5,15,225,700], [0,0,1.1,1.5,2,5.5,39,100,800],        [0,0,1.1,1.3,1.7,2.5,7.5,50,250,1e3], [0,0,1.1,1.2,1.3,1.8,3.9,13,50,250,1e3]],    'medium': [[], [.4,2.76], [0,1.8,5.16], [0,0,2.8,50], [0,0,1.71,10,100], [0,0,1.4,4,14,390],        [0,0,0,3,9,181,710], [0,0,0,2,7,30,400,800], [0,0,0,2,4,11,67,400,900],        [0,0,0,2,2.5,5,15,100,500,1e3], [0,0,0,1.6,2,4,7,26,100,500,1e3]],    'high': [[], [0,3.96], [0,0,17.16], [0,0,0,81.5], [0,0,0,10,259], [0,0,0,4.51,48,450],        [0,0,0,0,11,350,710], [0,0,0,0,7,90,400,800], [0,0,0,0,5,20,270,600,900],        [0,0,0,0,4,11,56,500,800,1e3], [0,0,0,0,3.5,8,13,63,500,800,1e3]],    'wtf': [[], [0,1.218], [0,0,5.28], [0,0,1.34,10], [0,0,0,4,64], [0,0,0,2,10,131],        [0,0,0,0,8,45,470], [0,0,0,0,3,18,169,700], [0,0,0,0,0,15,75,330,800],        [0,0,0,0,0,7,31,150,500,900], [0,0,0,0,0,0,35,104,300,600,1e3]],} args = sys.argv[1:] or ['92c36e93b39a5cde21d275e9747a5794', 'audit42a83c667988', '14', 'classic', '5,40,4,33,10']server_seed, client_seed, nonce, risk = args[0], args[1], int(args[2]), args[3]picks = [int(p) for p in args[4].split(',')]stake = Fraction(args[5] if len(args) > 5 else '0.10')wtf = risk == 'wtf' # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) The draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:cursordef value(cursor):    digest = hmac.new(bytes.fromhex(server_seed), f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest()    return int.from_bytes(digest[:4], 'big')        # the first 4 bytes, big-endian lightning = value(0) % 40 + 1 if wtf else None     # WTF only: cursor 0 draws the lightning numberboard = list(range(1, 41))                          # the board 1…40for i in range(10):                                 # forward Fisher–Yates: swap i with i + j′    j = i + value((1 if wtf else 0) + i) % (40 - i) # WTF draws at cursors 1–10    board[i], board[j] = board[j], board[i]drawn = board[:10]print('drawn      =', ', '.join(map(str, drawn)))if wtf:    print('lightning  =', lightning, '(drawn)' if lightning in drawn else '(not drawn)') # 3) Hits pick the cell; a paying WTF round with its lightning number drawn pays 10 × the cellhits = sum(p in drawn for p in picks)cell = PAYTABLE[risk][len(picks)][hits]factor = 10 if wtf and cell > 0 and lightning in drawn else 1multiplier = Fraction(repr(cell)) * factor          # the cell digit for digit: 16.5 → 33/2, exactshown = str(multiplier.numerator) if multiplier.denominator == 1 else repr(float(multiplier))print(f'hits       = {hits} | multiplier = {shown}' + (f' (cell {cell:g} × 10)' if factor == 10 else '')) # 4) Payout = floor(stake × multiplier) at the 8th decimal, in exact rational arithmeticunits = stake * multiplier * 10**8paid = units.numerator // units.denominator         # floor, in 1e-8 unitsprint(f'payout     = {paid // 10**8}.{paid % 10**8:08d}')# Expected for the defaults (bet E5tGJUmA4gmbhcUreVar7, Classic / 5, epoch 7 nonce 14, at $0.10):#   commitment = 5989c035b566e46540f217cd7aba6bf049c9dd393d1a1d1ceb7f0e5431a46793#   drawn      = 33, 35, 8, 10, 36, 39, 30, 4, 26, 40#   hits       = 4 | multiplier = 16.5#   payout     = 1.65000000# Expected for bet f2t0JeOrZXSsgu0xyMvoQ (WTF / 7, epoch 46 nonce 6, at $0.10):#   python3 verify-keno.py 430373a8b48a104773e948c25dd0b8f7 audit14226566ad31 6 wtf 28,7,4,13,22,2,21 0.10#   commitment = 98cf35e524d387eda36334e55c1bef9ee8c03215f8640eec2db2fed4405e1ba2#   drawn      = 13, 28, 12, 22, 11, 21, 7, 20, 4, 18#   lightning  = 20 (drawn)#   hits       = 6 | multiplier = 1690 (cell 169 × 10)#   payout     = 169.00000000
6.10Evidence Screenshots & Records4 records

The walkthrough’s screenshots are page captures from liqd.com; the evidence files below ship in the package.

2 columns
FileShows
page-read-2026-09-27/liqd-keno-bet-panel-2026-09-27.webpE20 · The Keno bet panel — the 40-number board, Manual / Auto, the bet amount, the Number Picker, the five risk levels and Add Second Bet
client-paytable/liqd-keno-client-paytable-2026-10-16.js.txtE31 · LIQD’s client paytable, read 16 October 2026 — the declared table the scripts embed (S3.4, S4.14)
operator-verify-api-raw-2026-10-21/liqd-keno-reverify-inpage-2026-10-21T13-02-26-114Z.ndjsonE34 · LIQD’s verify endpoint, every reply stored as received — 1,144 of them on the scored rounds, the evidence behind the casino-verifier row in 6.7 (S3.7)
page-read-2026-10-03/liqd-keno-page-description-edge-2026-10-03.pngE26 · The Keno game page’s Description box, “Edge: 1.00%” — the RTP claim (S4.2)
From Bet to Independent Verification — 4-step flow — illustrated flow
How it works
  1. 1

    Place a Keno Bet

    Pick 1 to 10 numbers on the 40-number board — by hand or with the Number Picker — choose a risk level and play. The ten numbers are drawn from the seeds and the nonce alone: the draw takes no pick or stake input. Your picks decide your hits, the risk decides the paytable row, and on WTF the lightning number is drawn first, so the ten numbers come one cursor later.

  2. 2

    Open the Provably Fair Panel and Rotate the Seed

    Open the Provably Fair panel from the game page. Its Seeds tab shows your active seed pair and its commitment — the hashed server seed — with the hash of the next server seed already published. Rotating ends the epoch and reveals the outgoing server seed in plaintext. First check the commitment: the SHA-256 of the revealed seed’s hex text must equal the hash you saw before betting — across this audit’s 51 rotations it always did.

  3. 3

    Enter the Seeds in the Verify Tab

    Open the Verify tab, which recomputes any round from a client seed, server seed, nonce, risk and picks, and enter the revealed server seed, your client seed, the round’s nonce, the risk and your picks.

  4. 4

    Recompute & Confirm

    Press Verify — or enter the same inputs in the ProvablyFair.org verifier. If the commitment check in step 2 passed and the recomputed draw matches your round — the ten numbers, your hits and the multiplier, and on WTF the lightning number — the round was provably fair. LIQD’s tab shows the ten numbers as a set; the scripts under Manual Verification also check their order and the credit.

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

Pick 1 to 10 numbers on the board — tap them, or set the Number Picker and press Pick for a random set — choose a risk level and play. The platform draws the ten numbers from (serverSeed, clientSeed, nonce): your picks decide your hits and the risk decides the paytable row, and on WTF the lightning number is drawn first. The panel below is LIQD’s Keno bet panel with three numbers picked — 11, 12 and 13 — on Classic; the row under the board is that setting’s paytable: 0×, 1×, 3.1× or 10.4× for 0 to 3 hits.

LIQD Keno on liqd.com — picks 11, 12 and 13 on Classic, with the paytable row for three picks under the board (0×, 1×, 3.1×, 10.4×).

LIQD Keno on liqd.com — picks 11, 12 and 13 on Classic, with the paytable row for three picks under the board (0×, 1×, 3.1×, 10.4×).

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

The panel has two tabs, Seeds and Verify. The Seeds tab holds your active seed pair and its commitment. Rotation ends the epoch: the outgoing server seed is revealed for verification, and the next server seed — whose hash LIQD had already published — becomes active with the client seed you set. That advance commitment is the chain the audit’s Step 2 checks across all 51 rotations, including the link into the first epoch.

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

Active Client Seed — your player-controlled input; the seed set when you rotate becomes the active one.

Total bets made with pair — the bet counter under the active pair: it starts at 0 after every rotation and locates each round’s nonce within the epoch.

Rotate Seed Pair — ends the epoch and reveals the server seed behind the hash you saw.

The Seeds tab before rotating — Active Server Seed (Hashed) 359a1ff3…cf02c2 after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 4c895a3c…73a803, hashes to 359a1ff3…cf02c2.

The Seeds tab before rotating — Active Server Seed (Hashed) 359a1ff3…cf02c2 after 1 bet, and the next server seed’s hash already published. The seed revealed by the rotation, 4c895a3c…73a803, hashes to 359a1ff3…cf02c2.

6.3Step 3: Enter the Seeds in the Verify Tab

The Verify tab takes the inputs — client seed, server seed, nonce, the risk and your picks — and recomputes a round on demand. Enter the server seed revealed in step 2, your client seed, the round’s nonce, the risk and your picks.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 0, risk Classic and the three picks.

The Verify tab filled in for that round — client seed, the server seed revealed in step 2, nonce 0, risk Classic and the three picks.

6.4Step 4: Recompute & Confirm

Press Verify. The tab recomputes the ten numbers from the three seed inputs — on WTF the lightning number first — then counts your hits and reads the multiplier from the paytable: it marks the ten drawn numbers on the board (as a set, not in draw order) and the lightning number on WTF, and shows your hits and multiplier. A match shows the draw is the one the committed seed produces. The scripts in the Manual Verification section below do the same with nothing but Node.js or Python, pre-loaded with two rounds from this report — and they add the commitment check, the draw order and the credit.

The result: ten numbers drawn, 12 and 13 hit — “2 of 3 hit: 3.1x”. Our scripts in 6.8–6.9 reproduce it exactly: draw order 13, 22, 14, 18, 40, 12, 15, 10, 26, 17, two hits, 3.1×.

The result: ten numbers drawn, 12 and 13 hit — “2 of 3 hit: 3.1x”. Our scripts in 6.8–6.9 reproduce it exactly: draw order 13, 22, 14, 18, 40, 12, 15, 10, 26, 17, two hits, 3.1×.

Technical evidence
6.5Why Manual Verification Matters

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

6.6How the Algorithm Works (Plain English)

Six moving parts, in the order they act:

  • Commit — before you play, LIQD publishes SHA-256 of the server seed, hashed as the 32-character hex string. The seed itself stays secret until rotation.
  • Draw — for cursor i = 0 … 9: HMAC-SHA256(key = decoded seed, message = clientSeed:nonce:i); the first 4 bytes, read big-endian, modulo 40 − i give j′, and board positions i and i + j′ swap on the board 1 … 40. The first ten board entries, in order, are the drawn numbers.
  • Hits — the number of your picks among the ten drawn numbers; the draw itself never reads your picks.
  • Cell — the multiplier is the declared paytable cell for your risk, pick count and hits, PAYTABLE[risk][picks][hits] — LIQD’s table read 16 October 2026.
  • Pay — the credit is floor(stake × multiplier) at the 8th decimal.
  • WTF — cursor 0 draws the lightning number (its value modulo 40, plus 1), the ten numbers use cursors 1–10, and a paying round whose lightning number is drawn pays 10 × its cell.

Note the two hashing domains: the commitment hashes the hex string as UTF-8 text, while the HMAC key is the 16 decoded bytes. Mixing them up is the most common reason a hand-rolled verifier disagrees with the game — both scripts below get it right, and both list the expected output of two rounds from this report in their comments so you can self-check the tool before trusting it. The other classic slip is the swap: position i trades with position i + j′, counted from i, on a board numbered 1 to 40. LIQD’s own code also rejects the few 32-bit values where plain modulo is uneven; that changes a round about once in 4.8 × 10⁷ (once in 4.1 × 10⁷ on WTF) and changed none of the 3,000 scored rounds (2.3).

6.7Casino Verifier vs ProvablyFair.org Verifier4 records

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

3 columns
LIQD Built-in (in-game panel)ProvablyFair.org
Who controls the codeThe casinoProvablyFair.org — open source
What it proves aloneInternal consistency of LIQD’s own mathIndependent recomputation from raw inputs
Checked against this captureDirectly — its verify endpoint returned the served round on all 1,144 scored rounds it was asked about: every WTF round and every observed Classic–High cell (Step 24, E34)The scripts in 6.8–6.9 implement the derivation this audit specifies — HMAC-SHA256 draw, WTF lightning rule, the declared paytable — and reproduce all 3,000 scored rounds: commitment, draw order, lightning, hits, multiplier and credit
If they disagreeSuspect the casino’s tool or a tampered roundThe repo + scripts are the tiebreaker anyone can run
6.8JavaScript Verification Script

Complete, dependency-free, and pre-loaded with the featured round as its default inputs — run it as-is to see a full match, then swap in your own round. Run on every one of the 3,000 scored rounds, it reproduces each one: commitment, drawn numbers in order, lightning number, hits, multiplier and credit.

verify-keno.js · Standalone Node.js
// verify-keno.js — standalone LIQD Keno verifier (Node.js, no dependencies)// Usage: node verify-keno.js <serverSeed> <clientSeed> <nonce> <risk> <picks, comma-separated> [stake]const { createHash, createHmac } = require('node:crypto'); const PAYTABLE = {   // LIQD's declared paytable, read 16 October 2026: PAYTABLE[risk][picks][hits]  classic: [[], [0,3.96], [0,1.9,4.5], [0,1,3.1,10.4], [0,.8,1.8,5,22.5], [0,.25,1.4,4.1,16.5,36],    [0,0,1,3.68,7,16.5,40], [0,0,.47,3,4.5,14,31,60], [0,0,0,2.2,4,13,22,55,70],    [0,0,0,1.55,3,8,15,44,60,85], [0,0,0,1.4,2.25,4.5,8,17,50,80,100]],  low: [[], [.7,1.86], [0,2,3.83], [0,1.1,1.39,26], [0,0,2.2,7.9,90], [0,0,1.5,4.2,13,300],    [0,0,1.1,2,6.2,100,700], [0,0,1.1,1.6,3.5,15,225,700], [0,0,1.1,1.5,2,5.5,39,100,800],    [0,0,1.1,1.3,1.7,2.5,7.5,50,250,1e3], [0,0,1.1,1.2,1.3,1.8,3.9,13,50,250,1e3]],  medium: [[], [.4,2.76], [0,1.8,5.16], [0,0,2.8,50], [0,0,1.71,10,100], [0,0,1.4,4,14,390],    [0,0,0,3,9,181,710], [0,0,0,2,7,30,400,800], [0,0,0,2,4,11,67,400,900],    [0,0,0,2,2.5,5,15,100,500,1e3], [0,0,0,1.6,2,4,7,26,100,500,1e3]],  high: [[], [0,3.96], [0,0,17.16], [0,0,0,81.5], [0,0,0,10,259], [0,0,0,4.51,48,450],    [0,0,0,0,11,350,710], [0,0,0,0,7,90,400,800], [0,0,0,0,5,20,270,600,900],    [0,0,0,0,4,11,56,500,800,1e3], [0,0,0,0,3.5,8,13,63,500,800,1e3]],  wtf: [[], [0,1.218], [0,0,5.28], [0,0,1.34,10], [0,0,0,4,64], [0,0,0,2,10,131],    [0,0,0,0,8,45,470], [0,0,0,0,3,18,169,700], [0,0,0,0,0,15,75,330,800],    [0,0,0,0,0,7,31,150,500,900], [0,0,0,0,0,0,35,104,300,600,1e3]],}; const argv = process.argv.slice(2);const [serverSeed, clientSeed, nonceArg, risk, picksArg, stake = '0.10'] = argv.length  ? argv  : ['92c36e93b39a5cde21d275e9747a5794', 'audit42a83c667988', '14', 'classic', '5,40,4,33,10'];const nonce = Number(nonceArg), picks = picksArg.split(',').map(Number), wtf = risk === 'wtf'; // 1) Commitment: SHA-256 of the hex STRING (UTF-8) — the hash LIQD showed before you playedconsole.log('commitment =', createHash('sha256').update(serverSeed, 'utf8').digest('hex')); // 2) The draw — HMAC-SHA256 keyed on the DECODED bytes, message clientSeed:nonce:cursor, first 4 bytes big-endianconst value = cursor => createHmac('sha256', Buffer.from(serverSeed, 'hex'))  .update(`${clientSeed}:${nonce}:${cursor}`).digest().readUInt32BE(0);const lightning = wtf ? value(0) % 40 + 1 : null;           // WTF only: cursor 0 draws the lightning numberconst board = Array.from({ length: 40 }, (_, k) => k + 1);  // the board 1…40for (let i = 0; i < 10; i++) {                              // forward Fisher–Yates: swap i with i + j′  const j = i + value((wtf ? 1 : 0) + i) % (40 - i);        // WTF draws at cursors 1–10  [board[i], board[j]] = [board[j], board[i]];}const drawn = board.slice(0, 10);console.log('drawn      =', drawn.join(', '));if (wtf) console.log('lightning  =', lightning, drawn.includes(lightning) ? '(drawn)' : '(not drawn)'); // 3) Hits pick the cell; a paying WTF round with its lightning number drawn pays 10 × the cellconst hits = picks.filter(p => drawn.includes(p)).length;const cell = PAYTABLE[risk][picks.length][hits];const factor = wtf && cell > 0 && drawn.includes(lightning) ? 10 : 1;const [ci, cf = ''] = String(cell).split('.');              // the cell digit for digit: 16.5 → 165 / 10const num = BigInt(ci + cf) * BigInt(factor), den = 10n ** BigInt(cf.length);console.log(`hits       = ${hits} | multiplier = ${Number(num) / Number(den)}${factor === 10 ? ` (cell ${cell} × 10)` : ''}`); // 4) Payout = floor(stake × multiplier) at the 8th decimal, in exact integer arithmeticconst [si, sf = ''] = stake.split('.');const paid = BigInt(si + sf.padEnd(8, '0')) * num / den;    // stake in 1e-8 units; BigInt division floorsconsole.log('payout     =', `${paid / 100000000n}.${String(paid % 100000000n).padStart(8, '0')}`);// Expected for the defaults (bet E5tGJUmA4gmbhcUreVar7, Classic / 5, epoch 7 nonce 14, at $0.10)://   commitment = 5989c035b566e46540f217cd7aba6bf049c9dd393d1a1d1ceb7f0e5431a46793//   drawn      = 33, 35, 8, 10, 36, 39, 30, 4, 26, 40//   hits       = 4 | multiplier = 16.5//   payout     = 1.65000000// Expected for bet f2t0JeOrZXSsgu0xyMvoQ (WTF / 7, epoch 46 nonce 6, at $0.10)://   node verify-keno.js 430373a8b48a104773e948c25dd0b8f7 audit14226566ad31 6 wtf 28,7,4,13,22,2,21 0.10//   commitment = 98cf35e524d387eda36334e55c1bef9ee8c03215f8640eec2db2fed4405e1ba2//   drawn      = 13, 28, 12, 22, 11, 21, 7, 20, 4, 18//   lightning  = 20 (drawn)//   hits       = 6 | multiplier = 1690 (cell 169 × 10)//   payout     = 169.00000000
6.9Python Verification Script

The same verifier in Python 3, standard library only — the payout is computed in exact rational arithmetic, so it doubles as an independent check on the JavaScript version. It reproduces all 3,000 scored rounds as well:

verify-keno.py · Standalone Python
# verify-keno.py — standalone LIQD Keno verifier (Python 3, stdlib only)# Usage: python3 verify-keno.py <serverSeed> <clientSeed> <nonce> <risk> <picks, comma-separated> [stake]import hashlib, hmac, sysfrom fractions import Fraction PAYTABLE = {   # LIQD's declared paytable, read 16 October 2026: PAYTABLE[risk][picks][hits]    'classic': [[], [0,3.96], [0,1.9,4.5], [0,1,3.1,10.4], [0,.8,1.8,5,22.5], [0,.25,1.4,4.1,16.5,36],        [0,0,1,3.68,7,16.5,40], [0,0,.47,3,4.5,14,31,60], [0,0,0,2.2,4,13,22,55,70],        [0,0,0,1.55,3,8,15,44,60,85], [0,0,0,1.4,2.25,4.5,8,17,50,80,100]],    'low': [[], [.7,1.86], [0,2,3.83], [0,1.1,1.39,26], [0,0,2.2,7.9,90], [0,0,1.5,4.2,13,300],        [0,0,1.1,2,6.2,100,700], [0,0,1.1,1.6,3.5,15,225,700], [0,0,1.1,1.5,2,5.5,39,100,800],        [0,0,1.1,1.3,1.7,2.5,7.5,50,250,1e3], [0,0,1.1,1.2,1.3,1.8,3.9,13,50,250,1e3]],    'medium': [[], [.4,2.76], [0,1.8,5.16], [0,0,2.8,50], [0,0,1.71,10,100], [0,0,1.4,4,14,390],        [0,0,0,3,9,181,710], [0,0,0,2,7,30,400,800], [0,0,0,2,4,11,67,400,900],        [0,0,0,2,2.5,5,15,100,500,1e3], [0,0,0,1.6,2,4,7,26,100,500,1e3]],    'high': [[], [0,3.96], [0,0,17.16], [0,0,0,81.5], [0,0,0,10,259], [0,0,0,4.51,48,450],        [0,0,0,0,11,350,710], [0,0,0,0,7,90,400,800], [0,0,0,0,5,20,270,600,900],        [0,0,0,0,4,11,56,500,800,1e3], [0,0,0,0,3.5,8,13,63,500,800,1e3]],    'wtf': [[], [0,1.218], [0,0,5.28], [0,0,1.34,10], [0,0,0,4,64], [0,0,0,2,10,131],        [0,0,0,0,8,45,470], [0,0,0,0,3,18,169,700], [0,0,0,0,0,15,75,330,800],        [0,0,0,0,0,7,31,150,500,900], [0,0,0,0,0,0,35,104,300,600,1e3]],} args = sys.argv[1:] or ['92c36e93b39a5cde21d275e9747a5794', 'audit42a83c667988', '14', 'classic', '5,40,4,33,10']server_seed, client_seed, nonce, risk = args[0], args[1], int(args[2]), args[3]picks = [int(p) for p in args[4].split(',')]stake = Fraction(args[5] if len(args) > 5 else '0.10')wtf = risk == 'wtf' # 1) Commitment — SHA-256 of the hex string as UTF-8 textprint('commitment =', hashlib.sha256(server_seed.encode('utf-8')).hexdigest()) # 2) The draw — HMAC-SHA256 keyed on the 16 decoded bytes, message clientSeed:nonce:cursordef value(cursor):    digest = hmac.new(bytes.fromhex(server_seed), f'{client_seed}:{nonce}:{cursor}'.encode(), hashlib.sha256).digest()    return int.from_bytes(digest[:4], 'big')        # the first 4 bytes, big-endian lightning = value(0) % 40 + 1 if wtf else None     # WTF only: cursor 0 draws the lightning numberboard = list(range(1, 41))                          # the board 1…40for i in range(10):                                 # forward Fisher–Yates: swap i with i + j′    j = i + value((1 if wtf else 0) + i) % (40 - i) # WTF draws at cursors 1–10    board[i], board[j] = board[j], board[i]drawn = board[:10]print('drawn      =', ', '.join(map(str, drawn)))if wtf:    print('lightning  =', lightning, '(drawn)' if lightning in drawn else '(not drawn)') # 3) Hits pick the cell; a paying WTF round with its lightning number drawn pays 10 × the cellhits = sum(p in drawn for p in picks)cell = PAYTABLE[risk][len(picks)][hits]factor = 10 if wtf and cell > 0 and lightning in drawn else 1multiplier = Fraction(repr(cell)) * factor          # the cell digit for digit: 16.5 → 33/2, exactshown = str(multiplier.numerator) if multiplier.denominator == 1 else repr(float(multiplier))print(f'hits       = {hits} | multiplier = {shown}' + (f' (cell {cell:g} × 10)' if factor == 10 else '')) # 4) Payout = floor(stake × multiplier) at the 8th decimal, in exact rational arithmeticunits = stake * multiplier * 10**8paid = units.numerator // units.denominator         # floor, in 1e-8 unitsprint(f'payout     = {paid // 10**8}.{paid % 10**8:08d}')# Expected for the defaults (bet E5tGJUmA4gmbhcUreVar7, Classic / 5, epoch 7 nonce 14, at $0.10):#   commitment = 5989c035b566e46540f217cd7aba6bf049c9dd393d1a1d1ceb7f0e5431a46793#   drawn      = 33, 35, 8, 10, 36, 39, 30, 4, 26, 40#   hits       = 4 | multiplier = 16.5#   payout     = 1.65000000# Expected for bet f2t0JeOrZXSsgu0xyMvoQ (WTF / 7, epoch 46 nonce 6, at $0.10):#   python3 verify-keno.py 430373a8b48a104773e948c25dd0b8f7 audit14226566ad31 6 wtf 28,7,4,13,22,2,21 0.10#   commitment = 98cf35e524d387eda36334e55c1bef9ee8c03215f8640eec2db2fed4405e1ba2#   drawn      = 13, 28, 12, 22, 11, 21, 7, 20, 4, 18#   lightning  = 20 (drawn)#   hits       = 6 | multiplier = 1690 (cell 169 × 10)#   payout     = 169.00000000
6.10Evidence Screenshots & Records4 records

The walkthrough’s screenshots are page captures from liqd.com; the evidence files below ship in the package.

2 columns
FileShows
page-read-2026-09-27/liqd-keno-bet-panel-2026-09-27.webpE20 · The Keno bet panel — the 40-number board, Manual / Auto, the bet amount, the Number Picker, the five risk levels and Add Second Bet
client-paytable/liqd-keno-client-paytable-2026-10-16.js.txtE31 · LIQD’s client paytable, read 16 October 2026 — the declared table the scripts embed (S3.4, S4.14)
operator-verify-api-raw-2026-10-21/liqd-keno-reverify-inpage-2026-10-21T13-02-26-114Z.ndjsonE34 · LIQD’s verify endpoint, every reply stored as received — 1,144 of them on the scored rounds, the evidence behind the casino-verifier row in 6.7 (S3.7)
page-read-2026-10-03/liqd-keno-page-description-edge-2026-10-03.pngE26 · The Keno game page’s Description box, “Edge: 1.00%” — the RTP claim (S4.2)
✓  Any Player Can Reproduce Keno Results

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

07

Reproducibility & Artifacts

Can anyone independently reproduce every scored check in this audit?

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

Open-Source Audit Repository

v1.1.0release audited

Repository Details

Prerequisites

  • Node.js 22.x (package.json engines and .nvmrc) — no dependency install for npm test or node test.js && node verify.js
  • Git
  • TypeScript toolchain via npm ci (TypeScript 5.9.3 · ts-node 10.9.2 · Mocha 11.7.6) — for npm run verify, npm run test:full and the source commands
  • Python 3 — only for the optional standalone checker in S6
v1.1.0release audited
github.com/ProvablyFair-org/liqd-keno

Commands to Reproduce

The scored capture, its request journal, the declared paytable and the committed results ship inside the repo — no separate download, and the capture, journal and declared table are SHA-256-checked before any step runs. npm test is zero-install: it runs the stale-build check, the 24 unit tests, the 24-step verification and the release-manifest check, and exits with the verdict’s code; npm ci adds the TypeScript tools for the source commands. Node.js 22.x is required.

sh
git clone https://github.com/ProvablyFair-org/liqd-keno.gitcd liqd-kenogit checkout v1.1.0npm ci # Run the entire audit end-to-end (stale-build check + 24 unit tests + 24-step verification + release-manifest check)npm test # Or the two bundles directly, with nothing but Node.jsnode test.js && node verify.js
Expected output
…[standalone] 24/24 unit specs passed (src/pins.ts EXPECTED_UNIT_TESTS).…  Passed:     21/24  Hard fails: 0…  VERDICT: PROVABLY FAIR — Full Pass…  Reproduction vs the committed artifacts on Node 22.22.0: REPRODUCED…  [release-manifest] OK: release bar met; 10 artifact hashes match disk, verdict "PROVABLY FAIR — Full Pass", scored 21/24…  RESULT: exit 0 — every other stage passed; the exit code is the verifier's verdict code

Audit Reproducibility Pinning

Framework
ProvablyFair.org Audit Framework 1.1 · Keno profile 1.0.2 · RTP-CLAIM 1.2.0
Release Signature
Release commit and tag signed with the ProvablyFair.org release key (SSH, SHA256:gEujiJm2pRyq7lQsfYQfQ1r97H/J7GdaKHbOW8GtikY), recorded in RELEASE-MANIFEST.json
Node Version
22.x (package.json engines, .nvmrc 22) — release runs on 22.23.1, this report’s runs on 22.22.0
Primary Dataset
data/liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json (3,000 rounds, 50 seed epochs)
Primary Dataset Hash (SHA-256)
7744010540dedbc9fe996a5cf9a75d4ee4a6007b08e56c2e7b9b9053b9831b1e
Request Journal Hash (SHA-256)
8f52aceba87bba7a86e9d3651e9f3a96b6597736eb3294f69cb44aaa9f30db76
Declared Paytable Hash (SHA-256)
767b72306634439808976894b9c1794f9cc1b092576306ce9adfc9e414d6f53a
Simulation Artifact Hash (SHA-256)
a4a2b1119491c983a1fd95c15470b925ae53dfe50d41fb3f490e53f63f496086
RTP Claim Table Hash (SHA-256)
5025110bbc6ba4d6f47fb63d1341d4341fb7d126de03811d39131ebfa672977d
LIQD Verifier Evidence Hash (SHA-256)
04661d2b15fe26fd485f3f218484a76239ff581bfeedd9d5352fcb57283cb6f9
Reproduction Gate
Every run writes only to outputs/run/ and is compared field by field with the committed results (outputs/run/diff.json) — integers and strings exactly, other numbers within 1e-14 + 1e-12 relative; anything beyond tolerance exits 1
Exit Codes
0 Full Pass · 2 Conditional Pass · 5 RTP FIGURE OVERSTATED · 1 NOT PROVABLY FAIR, a reproduction beyond tolerance, or a failed stage
Mutation Battery
94 of 94 live entries caught, 0 survived — every scored step catches at least one
Runtime
npm test ≈ 72 s · npm run verify ≈ 76 s · node test.js ≈ 2.5 s · npm run test:full ≈ 15 min (Node 22.22.0; declared budget 120 s and 900 s)
Capture Environment
Pre-launch host qa.liqd.com · one authenticated account, one currency (USDC)
Audit Date
October 2026 — declared paytable read 16 October, scored capture 20 October, LIQD’s verifier called 21 October 2026; release v1.1.0 21 October 2026
Audit ID
PF-2026-LQ06
Output Artifacts3 files committed
3 columns
FileContentsSize
verification-results.jsonSteps 1–24 results — per-step status with evidence detail strings and profile ids, profile coverage (KENO-01 … KENO-32), dataset, journal and paytable hashes, verdict~84 KB
simulation-results.jsonPass 1 (10 streams × 1,000,000 rounds, one per pick count: board, hit-law and lag-1 counts, from the pinned master seed) + Pass 2 (the 40 Classic–High epoch seeds × 10,000 nonces, the relocation null and both statistics)~31 KB
rtp-claim-table.jsonThe RTP claim rule’s table — LIQD’s published 99% against all 50 settings, each with exact RTP, gap and outcome~9 KB
Repository Structure
liqd-keno/
├── src/
│   ├── rng.ts                  → commitment hash · HMAC draw (computeDraw) · WTF lightning rule (wtfDraw) — written from docs/ALGORITHM.md
│   ├── bias.ts                 → the draw-bias bound and the rejection-zone census (Step 8)
│   ├── config.ts               → the game’s declared shape (40-number board, 10 drawn, 1–10 picks, five risks) + population constants
│   ├── client-paytable.ts      → LIQD’s client paytable read as data — the declared table (E31), parsed, never evaluated
│   ├── exact.ts                → exact BigInt-rational hit law, two independent derivations (Step 17)
│   ├── 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 50-setting exact RTP table and LIQD’s published claim
│   ├── profile-map.ts          → which Keno profile check (KENO-01 … KENO-32) each step implements
│   ├── pins.ts                 → integrity pins and the audited population — every count a constant
│   ├── loader.ts · v2.ts       → capture loader + SHA-256 guard (aborts before any step on a mismatch) · the capture schema
│   ├── operator-verify-2026-10-21.ts → reader for LIQD’s verify-endpoint replies on the scored rounds (E34, Step 24)
│   ├── sim.ts · simulate.ts · sim-inputs.ts · seed-derivation.ts → Pass 1 (10 × 1,000,000 rounds) + Pass 2, from pinned seeds → outputs/
│   ├── stats.ts                → χ² tail, board uniformity, hit-law fit and lag-1, re-derived from integer counts
│   ├── domains.ts              → value domains for every numeric field of the committed artifacts (Step 23)
│   ├── diff.ts                 → field-level diff of a run vs the committed artifacts (outputs/run/diff.json)
│   └── verify-tab.ts · operator-verify-wtf.ts · prod-capture-2026-10-21.ts · types.ts → readers for the pinned Verify-tab, verifier and production evidence · shared types
├── docs/
│   ├── ALGORITHM.md            → the specification the RNG is written from (the draw, the WTF lightning rule)
│   └── INTERFACE.md            → the module interface the RNG implements
├── tests/
│   ├── verify.ts · history.ts · production.ts → 24-step verification runner (results under outputs/run/; PF_EMIT=1 to regenerate the committed record)
│   ├── keno/                   → 24 unit tests (exact model 8 · verifier-reply reader 3 · spec self-test 1 [96 checks] · RNG adapter 12)
│   ├── mutations.ts · mutations.json → the step-named mutation battery: 97 entries (94 live), run in disposable copies
│   └── steps/
│       ├── commitment.ts       → Steps 1–5 (seed hashes, chain + pre-capture anchor, epoch consistency, nonce windows, population)
│       ├── determinism.ts      → Steps 6–10 (draw recompute, client-seed controls, bias bound, structure & hits, pick-set independence)
│       ├── payouts.ts          → Steps 11–16 (paytable provenance, payout arithmetic, bet size, shape & bounds, cell coverage, one multiplier per cell)
│       ├── rtp.ts              → Steps 17, 18, 21 (anti-circularity, exact RTP vs the claim, live-sample fit)
│       ├── simulation.ts       → Steps 19–20 (Pass 1 reconciliation and replay, Pass 2 re-derived from the scored seeds)
│       ├── standardization.ts  → Steps 22–24 (pins, artifacts of record, LIQD verifier parity)
│       └── primary.ts          → the capture-scoring part of every step
├── scripts/                    → npm test’s stage runner, release manifest + release bar, standalone-bundle builder and its check
├── capture/
│   ├── capture-keno-v2.mjs · liqd-capture-core.mjs → the capture runner and its engine: pre-capture anchor, CSPRNG client seeds, request journal
│   ├── capture-keno-verify-2026-10-21.mjs · inpage/ → the call of LIQD’s verify endpoint on the scored rounds (E34), run in the logged-in tab
│   ├── console/ · prod-2026-10-21/ · capture-keno-two-set.mjs · capture-keno-verify-raw.mjs → further runners and browser-console readers
│   ├── selftest-*.mjs · mock-*.mjs · req-*.py → offline self-tests against a simulated endpoint · the HTTP transport
│   └── out/                    → raw run output, copied byte for byte into data/ and evidence/
├── data/
│   ├── liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.json          → the scored capture: 3,000 rounds · 50 epochs · SHA-256 pinned (E32)
│   ├── liqd-keno-capture-v2-2026-10-20T13-04-03-496Z.journal.ndjson → its request journal: 6,103 records · SHA-256 pinned (E33)
│   └── … further pinned data files
├── outputs/
│   ├── verification-results.json    → 24 steps + profile coverage + dataset and paytable hashes + verdict
│   ├── simulation-results.json      → Pass 1 (10 streams × 1,000,000 rounds) + Pass 2 (40 epoch seeds × 10,000 nonces)
│   └── rtp-claim-table.json         → the RTP claim rule over all 50 settings
├── evidence/
│   ├── client-paytable/             → E31 LIQD’s client paytable, read 16 October 2026 — the declared table
│   ├── operator-verify-api-raw-2026-10-21/ → E34 LIQD’s verify endpoint, every reply stored as received — 1,144 on the scored rounds
│   ├── page-read-2026-10-03/        → E26 the game page’s “Edge: 1.00%” — the RTP claim
│   ├── page-read-2026-09-27/        → E20 the bet panel (S6)
│   └── … 7 further evidence folders and files, SHA-256 pinned
├── .github/workflows/reproduce.yml → second-machine reproduction on a clean runner (ubuntu-latest, Node from .nvmrc, npm ci && npm test)
├── test.js · verify.js         → dependency-free bundles (Node 22)
├── README.md · MANIFEST.md     → the report (findings, evidence register, worked examples, limitations register)
├── RELEASE-MANIFEST.json       → release identity: tag, framework, artifact hashes, verdict, signing key
├── package.json                → npm test = scripts/run-stages.mjs (build check, unit tests, verifier, release manifest) · verify · test:full · mutations
└── package-lock.json · tsconfig.json · .mocharc.yml · .nvmrc · .gitattributes · .gitignore · standalone-manifest.json · hygiene-allow.json · LICENSE
Step-to-Section Cross-Reference24 verification steps mapped
3 columns
StepTest NameReport Section
1Seed hash integrityS1, S3
2Forward commitment chainS1, S6
3Hash consistency within epochS1, S6
4Nonce continuityS1, S3, S6
5Population & epoch bindingS3
6Outcome recomputation (drawn list)S1, S2, S3, S4
7Client-seed influence & controlsS1, S2 · in production (1.4)
8Draw bias boundS2
9Draw structure & hit countingS1, S3, S4
10Pick-set independenceS2, S3, S4
11Paytable provenance & served-vs-declaredS3, S4
12Payout arithmeticS3, S4
13Bet-size invariance (elevated stake)S3, S4 · in production (3.8, 4.7)
14Paytable shape & boundsS3, S4
15Configuration & cell coverageS3, S4
16Stake invariance (same multiplier at every stake)S3, S4 · in production (3.8, 4.7)
17Anti-circularity (hit probability)S4
18Per-config RTP by exact enumeration vs claimS4
19Simulation Pass 1 (fresh seeds)S2, S4
20Simulation Pass 2 (cherry-pick)S4
21Live-sample fit (informational)S2, S4
22Dataset & config pinsS1, S3, S7
23Artifacts of recordS4, S7
24Operator-verifier parityS2, S3, S4, S6
Game Profile CoverageKeno profile 1.0.2 · 32 checks

Every check in the ProvablyFair.org Keno profile, version 1.0.2, is covered by a scored step, by a package check, or marked n/a with a reason — 24 by scored steps, 7 by package checks, 1 n/a. KENO-06, KENO-12 and KENO-31 will be completed on LIQD’s production site before certification; for KENO-09, KENO-14, KENO-24, KENO-28 and KENO-32 the row states the exact scope it covers. The map ships in src/profile-map.ts and is written beside the step results on every run; the committed outputs/verification-results.json carries it.

4 columns
CheckWhat it requiresCovered byCoverage
KENO-01Every revealed seed hashes to its commitmentStep 1Full
KENO-02Forward commitment chain, anchored before the first betStep 2Full
KENO-03Every round carries its epoch’s commitment and client seedStep 3Full
KENO-04Nonce continuity — every epoch holds its nonces exactly onceStep 4Full
KENO-05Audited population bound to code constants, not the dataset headerStep 5Full
KENO-06Client-seed influence and controls, player-set seeds includedStep 7On LIQD’s production site — the seed controls pass on the scored capture (1.4)
KENO-07Every served draw recomputes in draw order — WTF with its lightning numberStep 6Full
KENO-08Draw-bias bound computed from the draw’s own rangesStep 8Full
KENO-09Known-input unit vectors reproduce captured roundsPackage check: tests/keno/ (rngTests, rngSpecTests, exactTests)Spec self-test (96 checks) and captured-round vectors; the RNG is our own implementation, written from the specification
KENO-10Draw structure and hit countingStep 9Full
KENO-11Settlement arithmetic — floor(stake × cell [× 10]) to 8 decimals; zero cells pay 0Step 12Full
KENO-12Bet-size invariance at an elevated stakeStep 13On LIQD’s production site (3.8)
KENO-13Paytable provenance — every served multiplier equals its declared cellStep 11Full
KENO-14Paytable shape and bounds — every payout within the quoted maximumStep 14Every scored payout within stake × its row’s top cell, × 10 on WTF lightning rounds (Step 14)
KENO-15Every setting played at or above the profile floor; cell coverageStep 15Full
KENO-16Wallet balance ledger reconciliation—N/A — no response carries a per-bet balance; Step 12 checks the settle-response wallet fields on 3,000 / 3,000
KENO-17The draw is independent of the pick setStep 10Full
KENO-18Anti-circularity — hit probability from the board alone, two derivationsStep 17Full
KENO-19Exact RTP per setting against the published claim (RTP-CLAIM)Step 18Full
KENO-20Simulation Pass 1 — fresh seeds, convergence, uniformity; replayableStep 19Full
KENO-21Cherry-pick test — Simulation Pass 2Step 20Full
KENO-22Live-sample fit (informational)Step 21Full
KENO-23Framework fairness-integrity rows S5-1 … S5-8, kept as a package checklist (Section 5 reports the live integrity tests)Package check: MANIFEST.md fairness-integrity matrixFull
KENO-24Verifier parity — LIQD’s verifier on the served rounds; the ProvablyFair.org site verifier matched to the derivationStep 24LIQD’s verifier: 1,144 / 1,144 replies — every WTF round and every observed Classic–High cell (E34)
KENO-25Player verification path, in LIQD’s field namesPackage check: MANIFEST.md Verify a betFull
KENO-26Captures, journals and evidence pinned by SHA-256; abort on a mismatchStep 22Full
KENO-27Artifacts of record — no run metadata, every numeric value in a declared domainStep 23Full
KENO-28Release gate — zero-install entry points, pinned runtime, audit IDPackage check: scripts/check-standalone.mjs, package.json, .nvmrc, src/pins.tsZero-install npm test exits 0 on Node 22.23.1 (release runs) and 22.22.0 (this report); runtime pinned in package.json and .nvmrc; audit ID PF-2026-LQ06 in src/pins.ts
KENO-29Mutation battery — every scored step fires on at least one mutationPackage check: tests/mutations.json + tests/mutations.tsFull
KENO-30Limitations register and numbered evidencePackage check: MANIFEST.mdFull
KENO-31The same multiplier at every stakeStep 16On LIQD’s production site (4.7) — each of the 174 observed Classic–High cells paid one multiplier at the scored stake
KENO-32Auto-picked rounds settle as manual onesPackage check: the bet panel (E20)The bet panel offers Pick (E20); the 3,000 scored rounds carry the pick sets the capture chose, and the draw takes no pick input (Step 10)
reproduce-audit.sh
reproduce-audit.sh
# Clone and setupgit clone https://github.com/ProvablyFair-org/liqd-keno.gitcd liqd-kenogit checkout v1.1.0npm ci # Run the full audit end-to-end (24 unit tests + the 24 verification steps + release-manifest check)npm test # Or the dependency-free path — the standalone bundles need only Node.js 22node test.js && node verify.js # Or run individual componentsnpm run verify           # 24-step verification — writes outputs/run/, never the committed resultsnpm run test:full        # rebuild check, unit tests, the full simulation regenerated and diffed, then verifynpm run mutations        # mutation battery — 94 of 94 live entries caught, 0 survived # View the committed resultscat outputs/verification-results.jsoncat outputs/rtp-claim-table.json | head -40cat outputs/simulation-results.json | head -40
provablyfair.org/verify-bets/liqd
This is an open-source audit. Every scored check, dataset and line of verification code is public. You don't need to trust us or the casino. Clone the repo, run the tests, and verify every claim yourself.
✓  Fully Reproducible

All scored results reproduce from release v1.1.0, the pinned capture and the commands above. The capture, its request journal and the declared paytable are hash-checked before any step runs, so you are provably testing the same 3,000 rounds against the same table; the simulation and the RTP claim table are pinned by SHA-256, and every run is compared field by field with the committed results. The simulation is deterministic — npm run test:full regenerates it from its pinned seeds, equal to the committed artifact field for field.