How to verify a dice roll step by step (with Python and JavaScript)
Reproduce a provably fair dice result from its seeds in five steps, using real published test vectors, plus copy-paste code for Python and the browser.
Reading about commit–reveal schemes is useful. Reproducing a result with your own code is better, because it turns a claim into something you have checked. This tutorial walks through one dice roll from start to finish using a public test vector, then gives you code you can reuse on your own rounds.
We use the published scheme of one operator, Rubio's dice game, because its rules page documents every byte of the process and offers downloadable test vectors. The same approach works for any site that publishes its message format.
What you need
From the game's fairness or verify panel, note four values for the round:
- the server seed hash you were shown before the bet
- the client seed
- the nonce (the bet counter)
- the revealed server seed, which appears after you rotate seeds

For this walkthrough we use the first public test vector, which is safe to share because it was generated for testing and never used for real rounds:
| Field | Value |
|---|---|
| Server seed | d5896eaf36440771cd9328ebdb3192ac20746d40f047d628eb11c31d455238d5 |
| Server seed hash | 8f36c9940c87b7d330a86b0a202c1dc4c46f5d91ba30b3bb7bb72e13646b3291 |
| Client seed | test-client-0 |
| Nonce | 1 |
Step 1: check the commitment
Hash the revealed server seed with SHA-256 and compare it with the hash shown before the bet. One detail trips people up: on this scheme the hash is taken over the 64-character hex text of the seed (its UTF-8 bytes), not over the 32 raw bytes it represents. The rules page says so explicitly, which is exactly the kind of detail good documentation includes.
import hashlib
seed = "d5896eaf36440771cd9328ebdb3192ac20746d40f047d628eb11c31d455238d5"
print(hashlib.sha256(seed.encode()).hexdigest())
# 8f36c9940c87b7d330a86b0a202c1dc4c46f5d91ba30b3bb7bb72e13646b3291
The output matches the published hash, so the seed was fixed before the bet.
Step 2: build the message
For dice the message is simply the client seed and nonce joined by a colon: test-client-0:1. Other games add tags, for example :limbo or :mines:3, so always read the per-game format.
Step 3: compute the HMAC
HMAC-SHA256 (RFC 2104) takes a key and a message. The key is the server seed text, again as UTF-8 bytes of the hex string.
import hmac
digest = hmac.new(seed.encode(), b"test-client-0:1", hashlib.sha256).hexdigest()
print(digest)
# 10d60f21f36057fd3c8d0d2236fa23b7cb08d67894fe722c4a2540a96f62794a
Step 4: turn bytes into a number
Take the first 8 hex characters (4 bytes), read them as an unsigned integer and divide by 2³² (4,294,967,296). That gives a number from 0 up to, but not including, 1.
- First 8 hex characters:
10d60f21 - As an integer: 282,464,033
- Divided by 4,294,967,296: 0.0657662826…
Step 5: map it to the roll
Dice multiplies by 100, giving a roll in the range 0 to 99.99…:
roll = 6.5766…
The published vector lists a roll of 6.576628261245787, so our result matches to every digit. With an "over 50" bet this round loses; with "under 50" it wins. The game interface shows rolls with two decimals.

The whole thing in one function
import hashlib, hmac
def dice_roll(server_seed: str, client_seed: str, nonce: int) -> float:
msg = f"{client_seed}:{nonce}".encode()
h = hmac.new(server_seed.encode(), msg, hashlib.sha256).hexdigest()
return int(h[:8], 16) / 2**32 * 100
def check_commitment(server_seed: str, shown_hash: str) -> bool:
return hashlib.sha256(server_seed.encode()).hexdigest() == shown_hash
And the same in the browser with the Web Crypto API, no libraries needed:
async function diceRoll(serverSeed, clientSeed, nonce) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", enc.encode(serverSeed), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(`${clientSeed}:${nonce}`)));
const n = ((sig[0] << 24) >>> 0) + (sig[1] << 16) + (sig[2] << 8) + sig[3];
return n / 2 ** 32 * 100;
}
If you would rather not paste code anywhere, our browser verifier does the same calculation locally.
Checking the payout too
Matching the roll proves the randomness. To check the money, apply the published payout formula. On this dice game the multiplier is (100 − edge) ÷ win chance. At the current 3% edge, a roll-under-50 bet has a 50% chance and pays 97 ÷ 50 = 1.94×. Payouts are rounded down to the smallest unit of the coin.

A well-documented operator also stores the edge used for each bet, so a checker can confirm that the payout you received matches the formula at that edge, not just the current one.
Common mistakes
- Hashing the decoded bytes. If your hash does not match, check whether the scheme hashes the hex text or the raw bytes.
- Off-by-one nonces. Some systems start at 0, some at 1. The verify panel shows the nonce for each bet; use that value.
- Trailing spaces in the client seed. The message is byte-exact.
- Using the active seed. You cannot verify until the seed is rotated and revealed.
FAQ
Do I need to verify every bet?
No. Spot-check a few rounds after rotating your seed. One mismatch is enough to stop playing; repeated matches across sessions build confidence.
Why divide by 2³²?
Four bytes hold values from 0 to 4,294,967,295. Dividing by 4,294,967,296 maps them evenly onto the range 0 to just under 1.
Can I use an online HMAC calculator?
You can, but prefer a local tool or your own code so the seeds are not sent to a third party. Once revealed they are harmless, but good habits matter.