The standard advice for understanding a cryptographic protocol is to read the specification, work through the math on paper, and then implement it in code. The first two steps are useful but the third is where understanding actually consolidates. Nothing reveals the ambiguities in a specification like sitting down to write code that exercises every corner of it, and provably fair gambling protocols have more corners than their documentation usually admits.
This article walks through building a complete crypto casino verifier from first principles, in both JavaScript and Python, covering the commitment verification, the outcome derivation, and the game-specific conversion for the three most common game types. The goal is not to ship a production tool. The goal is to understand what a production tool has to get right, so that when you use someone else’s verifier you have an informed view of what it is doing on your behalf.
Setting Up the Skeleton
Start with the inputs. A provably fair verifier needs four pieces of information to verify a single bet: the server seed (revealed after the bet), the hash of the server seed (committed before the bet), the client seed (chosen by the player), and the nonce (a sequential counter that identifies which bet in a session is being verified). Some casinos use additional inputs like a cursor for games that derive multiple outcomes from a single nonce, but the core four are universal.
In JavaScript, the minimal skeleton looks like this:
function verifyBet(serverSeed, serverSeedHash, clientSeed, nonce) {
const computedHash = sha256(serverSeed);
if (computedHash !== serverSeedHash) return { valid: false, reason: ‘commitment mismatch’ };
const outcome = deriveOutcome(serverSeed, clientSeed, nonce);
return { valid: true, outcome };
}
The first check is the commitment verification: hash the revealed server seed with SHA-256 and compare it to the hash that was committed before the bet. If these do not match, the operator has violated the commitment property and no further verification is meaningful. Most verifier bugs in the wild skip this check or display a warning that the user ignores, which is a mistake because the commitment property is the strongest guarantee in the whole scheme.
The Outcome Derivation Step
The commitment check is simple. The outcome derivation is where implementations diverge and where most verifier bugs live. The canonical approach, used by nearly every major crypto casino, is to compute HMAC-SHA512 with the server seed as the key and the client seed concatenated with the nonce as the message. The output is 512 bits (64 bytes), which is more than enough entropy for most single-outcome games.
In Python:
import hmac, hashlib
def derive_hash(server_seed, client_seed, nonce):
message = f”{client_seed}:{nonce}:0″.encode()
return hmac.new(server_seed.encode(), message, hashlib.sha512).hexdigest()
The exact format of the message string varies. Some casinos use colons as separators. Some use the client seed directly followed by the nonce as an 8-byte big-endian integer. Some include a cursor value for multi-outcome games. Getting this format wrong is the single most common source of verification failures, and it is the place where a verifier needs to be configurable per casino rather than assuming a single universal format.
The HMAC output is a hex string of 128 characters representing 64 bytes. The next step is to convert those bytes into a game-specific outcome, which is where the three main game types (dice, crash, limbo) diverge.
Dice: The Simplest Conversion
A standard dice game returns a number between 0.00 and 99.99 (or 0 and 10000 in integer form). The conversion from hash bytes to this range uses the first four bytes of the HMAC output, interpreted as a big-endian unsigned integer, modulo 10001, divided by 100 to get the two-decimal result.
In JavaScript:
function diceOutcome(hmacHex) {
const bytes = hmacHex.substring(0, 8);
const intVal = parseInt(bytes, 16);
return (intVal % 10001) / 100;
}
This is almost right. The subtle bug is that modulo 10001 does not produce a perfectly uniform distribution across 0.00 through 99.99 unless the input range is a multiple of 10001. Four bytes gives 4,294,967,296 possible values, and 4,294,967,296 divided by 10001 is 429,453 with a remainder of 343, meaning some outcomes are slightly more likely than others. The bias is tiny (about 0.00008%) but it exists, and a rigorous implementation rejects inputs in the biased range and tries the next four bytes instead. Most production verifiers do not bother with this correction because the bias is below any realistic detection threshold, but it is worth knowing it is there.
Crash: The Multiplier Derivation
Crash games derive a multiplier from 1.00x upward, with a distribution designed to give the house its edge. The standard construction uses the first eight bytes of HMAC output as a 64-bit unsigned integer, applies the house edge through a specific transformation, and maps the result to a multiplier.
The common formula for a 1% house edge crash is:
def crash_outcome(hmac_hex, house_edge=0.01):
h = int(hmac_hex[:13], 16) # 52-bit sample
e = 2 ** 52
return max(1.00, (100 e – h) / (e – h) / 100 (1 – house_edge))
The 52-bit sample is used because JavaScript’s native number type has 53 bits of precision, and 52-bit samples avoid precision issues if the tool runs in a browser. The specific formula comes from stake.com‘s published reference implementation, and variants of it appear in nearly every crash game in the wild. Some casinos include a small probability of an instant 1.00x crash, which is the house edge being implemented as a probability of immediate loss rather than a continuous multiplier shift. The verification for these games requires checking that specific condition before applying the main formula.
Limbo: The Inverse Construction
Limbo is essentially crash with a different user interaction (player sets the target multiplier before betting rather than watching it climb), but the outcome derivation uses the same hash output. The multiplier is calculated from a uniform random number between 0 and 1, with the house edge applied:
function limboOutcome(hmacHex) {
const h = parseInt(hmacHex.substring(0, 13), 16);
const e = Math.pow(2, 52);
const float = h / e;
const houseEdge = 0.01;
return Math.max(1.00, (1 – houseEdge) / (1 – float));
}
The key insight here is that limbo and crash are the same game mathematically, differing only in presentation. A verifier that supports one should be able to support the other with minimal code duplication. In practice, each casino implements small variations (different house edges, different minimum multipliers, different rounding conventions), and a verifier needs to be configurable per casino to handle these correctly.
The Testing Problem
Once you have implementations of the commitment check and the three game derivations, you need test cases to validate that your implementations match what real casinos do. This is where most verifier projects get stuck, because there is no standardized test vector set. Each casino publishes their algorithm (sometimes) and their reference implementation (rarely), and the algorithm documentation is often subtly wrong or incomplete.
The practical approach is to take your own bet history from whatever casino you want to verify, extract the seeds and nonces, run them through your implementation, and compare the outputs to what the casino showed you. If they match for a dozen bets, your implementation is probably correct for that casino. If they do not match, you have a bug somewhere: in the message format, in the byte slicing, in the integer conversion, or in the final formula.
A debugging technique that helps is to print every intermediate value. The raw HMAC output, the hex substring being converted, the integer value after conversion, the modular reduction or division result, and the final outcome. Compare each of these to the casino’s own verification interface if it exposes them. The divergence will usually occur at one specific step, and once you find it, the fix is usually a one-line change in either the message format or the byte slicing convention.
What Makes an Implementation Production-Ready
The skeleton implementations above are correct for simple cases but fall short of production in several ways. A production verifier needs to handle casino-specific configurations (different message formats, different edges, different game types), validate input formats before processing (hex strings of the right length, non-negative nonces, etc.), and return intermediate values so the user can debug verification failures.
It also needs to handle multi-outcome games, where a single nonce produces multiple game events. Mines, plinko, and shuffled-deck games all fall into this category. The standard approach is to use a cursor value in the message format and iterate, taking additional hash chunks as needed. The cursor starts at 0 for the first outcome and increments by 1 for each additional hash call. A verifier that does not support cursors will fail for any multi-outcome game, even if it works for simple dice.
If you want to see a production implementation that handles all of the above correctly and supports the major casino conventions, the reference I keep returning to is at toolsgambling.com/casino/provably-fair, which handles the casino-specific variations and shows intermediate values, making it useful for both verification and debugging your own implementations.
The Lessons That Transfer to Other Cryptographic Work
Building a verifier from scratch is valuable beyond the specific domain of casino fairness. The patterns that show up here recur in any commitment-reveal protocol: zero-knowledge proof systems, verifiable delay functions, certificate transparency logs, and on-chain governance mechanisms. The core structure is always the same. A party commits to some value they do not yet want to reveal. Later, they reveal the value and the counterparty verifies the commitment was honest.
The implementation details that trip up casino verifiers also trip up these other protocols. Byte ordering conventions are easy to get wrong. Hash function selection matters. Intermediate value slicing is where bugs hide. Input format validation is usually omitted and then causes problems. The debugging technique of printing every intermediate value is what unblocks stuck implementations across all these domains.
A developer who has written a correct provably fair verifier has internalized the right mental model for building any commitment-reveal system. The gambling application happens to be one of the most accessible places to learn this, because bet histories are plentiful, test cases are free, and the math is simple enough to work through without a cryptography background. If you are going to build anything that involves public commitments and later reveals, writing a casino verifier first is an unusually good warm-up exercise, and the code you write is directly useful for checking your own bets in the meantime.
