A provably fair dice roll is one you can recompute yourself, from scratch, after the fact. On Lightning Faucet every dice roll comes from three published inputs and one hash. If you have the server seed, the client seed and the nonce, you can reproduce the exact number the roll landed on, and confirm that the payout you were quoted is the payout the engine actually applied.
Here is the whole thing in one block. This is the real derivation our dice engine runs, not a simplified version of it:
combined_hash = HMAC_SHA256( key = server_seed, message = client_seed + ":" + nonce )
base_roll = int( combined_hash[first 8 hex chars], 16 ) mod 100000 -> 0 to 99,999
dice_roll = base_roll mod 10000 -> 0 to 9,999
Three lines. That is the entire outcome pipeline for dice. Everything else in this article is the detail that makes your recomputation actually match ours: where each input comes from, what our server quietly does to your client seed before storing it, why the nonce you expect is often not the nonce we used, and the two reductions that trip up almost every hand written verifier.
The short definition
Provably fair dice verification is the process of proving that a dice result was fixed before you bet, by re-deriving it from a server seed the operator committed to in advance, a client seed you chose, and a counter called a nonce. The commitment is a SHA-256 hash of the server seed, published before you play. The reveal is the server seed itself, published after you stop using it. If the revealed seed hashes to the commitment you were shown, the operator could not have swapped it mid-session.
That is the guarantee. It is a narrow guarantee, and it is worth stating what it does not cover, which we do further down.
The three inputs, exactly as our engine stores them
Server seed: committed as a hash, revealed only when you rotate
When you first play a casino game on Lightning Faucet, we generate 32 random bytes, store them as a 64 character hex string on your account, and immediately compute sha256(server_seed). Only that hash is visible to you. It travels back with every dice roll as server_seed_hash, alongside your client seed, your nonce and the resulting roll.
You get the raw server seed by rotating it. Rotation does four things in one transaction: it archives the old server seed, its hash, the client seed and the number of games played into your seed history, it hands you the old server seed in the response, it mints a fresh 32 byte seed with a new published hash, and it resets your nonce to zero.
There is a fifth thing rotation does that catches people out, and it is not obvious from any generic explainer: rotation also resets your client seed back to default. If you had set a custom client seed, set it again after rotating, or the next batch of rolls will be committed under default and your saved verification notes will not line up.
Client seed: yours, but sanitized before storage
You can set your client seed to anything you like. What we store is not necessarily what you typed. The setter strips every character that is not a to z, A to Z or 0 to 9, then truncates to 64 characters. If the result is empty, it becomes the literal string default.
So my-lucky-seed! is stored, and hashed, as myluckyseed. 2026-07-04 is stored as 20260704. An emoji only seed becomes default.
This single behaviour is responsible for more failed verifications than every other cause combined. People verify against the string they typed into the box, get a completely different hash, and conclude something is wrong. Always verify against the client_seed value returned in the roll response or shown in your seed history, never against what you typed.
Nonce: per account, shared across every casino game, and pre-increment
The nonce is a single counter on your account. It is not a per game counter. Slots, dice, roulette, scratchcard and blackjack all draw from the same seed_nonce, and each play advances it by one.
Two consequences that matter enormously when you sit down to verify:
- Your dice nonces are usually not consecutive. Roll dice at nonce 12, spin a slot, then roll dice again: that second dice roll is nonce 14, not 13. If you are verifying a session by counting your dice rolls, you will be off by however many other games you played in between.
- The nonce used is the value before the increment. Your very first game on a fresh seed uses nonce
0, not1. After that game your stored counter reads1, which is the nonce the next game will use.
Every dice roll response carries the exact nonce that was consumed, in the fairness block. Copy that number. Do not derive it.
The increment and the hash computation happen inside the same database transaction, under a row lock on your account. That is what stops two simultaneous rolls from sharing a nonce and therefore sharing an outcome, which is the classic race condition in naive commit and reveal implementations.
The derivation, step by step
Step 1: the combined hash
combined_hash = HMAC_SHA256(key = server_seed, message = client_seed + ":" + nonce)
Note the argument order. The server seed is the key. The message is the client seed, a single colon, and the nonce rendered as a decimal integer with no padding. A meaningful number of provably fair guides show the opposite arrangement, with the client seed as the HMAC key, and several crypto libraries take their arguments in an order that makes the swap easy to commit by accident. Swapped arguments produce a perfectly valid looking 64 character hash that has nothing to do with your roll.
Step 2: the base roll, 0 to 99,999
base_roll = int(combined_hash[first 8 hex chars], 16) mod 100000
We take the first 8 hex characters, which is 32 bits, giving an integer from 0 to 4,294,967,295. That stays comfortably inside a native integer on every platform, which is deliberate: it means a verifier written in JavaScript, where numbers above 2^53 quietly lose precision, produces bit for bit the same answer as our PHP engine. Reductions from a full 64 character hash are common elsewhere and are a frequent source of JavaScript verifiers that disagree with the operator on a small fraction of results for reasons nobody can reproduce.
This base roll is shared infrastructure. Every casino game on Lightning Faucet runs through this same 0 to 99,999 value before mapping it onto its own outcome space.
Step 3: the dice roll, 0 to 9,999
dice_roll = base_roll mod 10000
Dice has 10,000 outcomes, numbered 0 through 9,999. The base roll is reduced a second time to land on one.
Why the double reduction matters, and why dice gets a free pass
Most dice verification tutorials on the internet tell you to compute int(hash[:8], 16) mod 10000 in one step. For Lightning Faucet dice, that shortcut happens to be exactly correct, and it is worth understanding why, because the same shortcut is wrong on other games running on this identical engine.
100,000 is an exact multiple of 10,000. Reducing modulo 100,000 and then modulo 10,000 is mathematically identical to reducing modulo 10,000 once. No information about the final answer survives the first reduction that the second one needs. So a hand written dice verifier that collapses the two steps will agree with us on every single roll, forever.
Contrast that with roulette on the same engine. Roulette does go through the same base roll, and then reduces onto 37 pockets. 100,000 is not a multiple of 37 (it leaves a remainder of 26), so collapsing the two reductions into one gives a different pocket on the overwhelming majority of spins. A verifier built by analogy from a dice tutorial would conclude the wheel was rigged, when the only defect was the missing intermediate step.
That is the practical takeaway for anyone writing verification code against any provably fair site: do not restate the derivation, replicate it. Two reductions that happen to commute on one game will not commute on the next, and the failure mode is a false accusation rather than an obvious crash.
The one place uniformity is not perfect
We would rather tell you this than have you find it: the mapping from a 32 bit integer to 10,000 outcomes is not perfectly uniform, because 4,294,967,296 is not a multiple of 10,000. It leaves a remainder of 7,296, so results 0 through 7,295 each have one more preimage than results 7,296 through 9,999.
The size of that asymmetry is about 0.00017 percent of total probability mass, spread across 7,296 of the 10,000 outcomes. It is roughly a thousand times smaller than the rounding applied to the payout multiplier, it is invisible below several billion recorded rolls, and it is inherent to any modulo based mapping that does not use rejection sampling. We are documenting it because a verifier who reproduces our math should understand its properties, not discover an undisclosed one later.
Verify a roll yourself
1. Capture the roll
Every dice roll response includes a fairness block with server_seed_hash, client_seed, nonce and roll. Save all four verbatim. The client_seed there is the post-sanitization value, which is the one you need.
2. Reveal the server seed
Rotate your seed. The response hands you the old server_seed plus the server_seed_hash it was published under. First check sha256(server_seed) against that hash and against the hash you saw on the roll itself. If those three agree, the seed you are about to verify with is provably the seed that was live at the time you bet. Then reset your custom client seed, because rotation cleared it.
Your seed history keeps every revealed seed with its first nonce, last nonce and games played, so you can verify a batch of rolls long after the fact rather than one at a time.
3. Recompute
Python:
import hmac, hashlib
server_seed = "the 64 hex chars revealed when you rotated"
client_seed = "myluckyseed" # the SANITIZED value from the roll response
nonce = 41 # the exact nonce from the roll response
combined = hmac.new(
server_seed.encode(), # key = server seed
f"{client_seed}:{nonce}".encode(), # message = client_seed + ":" + nonce
hashlib.sha256,
).hexdigest()
base_roll = int(combined[0:8], 16) % 100000 # 0 to 99,999
dice_roll = base_roll % 10000 # 0 to 9,999
print(combined, base_roll, dice_roll)
JavaScript:
const crypto = require('crypto');
const combined = crypto
.createHmac('sha256', serverSeed) // key = server seed
.update(`${clientSeed}:${nonce}`) // message = client_seed + ":" + nonce
.digest('hex');
const baseRoll = parseInt(combined.slice(0, 8), 16) % 100000;
const diceRoll = baseRoll % 10000;
dice_roll must equal the roll from the response. If it does, the number was fully determined by a seed we committed to before you chose your target, and neither side could have influenced it.
4. Cross check against our verifier
You do not have to trust your own implementation either. The Provably Fair panel on the site calls a verify_game action that runs the same code path as the live game, with game_type set to dice, and takes your server_seed, client_seed and nonce. Pass the optional target, direction and wager too and it returns the resolved bet as well: the roll, whether it won, the win chance percentage, the multiplier and the payout in sats.
Because it calls the identical service class the live roll used, there is no second implementation to drift out of sync with the first. If your code and the endpoint disagree, the bug is in your code, and the fields it returns will usually tell you where.
There is one more fingerprint you can check. Every dice game row stores a game_hash computed as sha256(combined_hash + ":dice:" + roll), plus the base roll and the final roll separately. Recomputing that hash confirms not only the roll but that the stored record binds to the same combined hash, so an altered row cannot pass unnoticed.
How the roll becomes a win or a loss
Verification of the number is only half the job. The other half is confirming the bet resolved correctly.
You pick a target from 1 to 9,998 and a direction, over or under. Resolution is strict on both sides:
- Under wins when
roll < target. - Over wins when
roll > target.
Both comparisons are strict, which means a roll exactly equal to your target is a loss in either direction. Under a target of 5,000 you win on 0 through 4,999, which is 5,000 of the 10,000 outcomes. Over a target of 5,000 you win on 5,001 through 9,999, which is 4,999 outcomes. That asymmetry is real, it is priced into the multiplier the interface quotes you before you commit, and the verifier reproduces it from the same function the live game used.
Two more mechanical details that show up in verification:
- The multiplier is floored to four decimal places, and the payout is floored to whole sats. Lightning Faucet is sat denominated end to end, with no sub-sat accounting, so a fractional payout always rounds down to the whole sat.
- Dice enforces a maximum payout. Extremely long shot targets can produce a nominal payout above that ceiling, in which case the response returns
payout_cappedand themax_payoutvalue that applied. If you are reconciling a very high multiplier win, check those two fields before assuming a mismatch.
What verification proves, and what it does not
Provable fairness answers exactly one question: was this outcome fixed before the bet, by a seed the operator committed to in advance and cannot retroactively change? On Lightning Faucet the answer is yes, and you can demonstrate it yourself with twelve lines of code.
It does not make a losing session into a winning one, and it does not change the odds of any individual bet. The win chance for your chosen target is displayed before you roll, verification recomputes it from the same function, and no amount of seed rotation moves it. Anyone selling seed rotation as a way to improve results is selling you nothing.
It also does not, by itself, prove the operator paid you. That is a separate property, and on Lightning Faucet it is a Lightning property: balances are in sats, deposits and withdrawals run over Lightning, and withdrawals use LNURL-withdraw, so you scan a QR code or tap a link and your wallet fetches the invoice automatically. Verifiable outcomes and instant settlement are two independent things you should expect together.
Common reasons a verification fails
In rough order of how often we see them:
- Verifying against the client seed you typed rather than the sanitized one. Punctuation, spaces and emoji are stripped before storage. Use the value from the response.
- Using the wrong nonce. The counter is shared across all casino games and is consumed pre-increment. Copy the nonce from the roll, do not count your dice rolls.
- HMAC arguments swapped. Server seed is the key,
client_seed:nonceis the message. - Comparing against a rotated seed. A revealed seed only verifies the rolls made while it was active. Check
sha256(server_seed)against theserver_seed_hashon the specific roll first. - Forgetting the client seed reset. Rotation returns the client seed to
default. Rolls made after rotating and before you set a custom seed again were committed underdefault. - Precision loss. Reducing from more than 8 hex characters in a language with 53 bit floats will disagree with us intermittently. Take 32 bits.
Try it on a live roll
The fastest way to internalise any of this is to do it once on a real bet. Open dice, set a client seed you will remember, roll once at the minimum stake, then rotate your seed and recompute that roll with the Python snippet above. It takes about two minutes and it is the difference between believing a fairness claim and having checked one.
If you do not have a balance yet, the free spin and the other earn surfaces fund an account in sats without a deposit, and every game they fund is committed under the same seed model.
The same commit and reveal machinery runs underneath the rest of the casino, with each game applying its own mapping to the shared base roll. Multiplayer Lightning poker uses a variant built for a shared deck: the seed belongs to the table rather than to one player, the nonce advances once per hand, any seated player can set the table client seed, and revealing rotates the table seed so every hand played under it can be re-derived. And on prediction markets there is no seed at all, because outcomes resolve against real world events rather than a hash. Knowing which of those three models you are looking at is most of what it takes to evaluate any fairness claim you encounter.