You can check a provably fair result by hand with three inputs: the server seed the site revealed after your bets, the client seed you supplied, and the nonce, a counter that numbers each bet made under that pair of seeds. The check has two parts. First, the SHA-256 hash of the revealed server seed has to equal the one the site published before you played. Second, all three values go through HMAC-SHA256, and the first bytes of the output, converted to a number, have to reproduce the game result.
The worked example below uses invented seeds and the common SHA-256 and HMAC-SHA256 construction rather than any one site's specification, and Python's standard library and Node's crypto module cover it. The proof is also narrower than its name suggests: it shows where a result came from and leaves the odds where the paytable put them.
The hash shown before play locks in one server seed
Every seed pair starts with the site. It generates a server seed, keeps it secret and shows you only its SHA-256 hash, 64 hexadecimal characters that stand for 256 bits. Nobody can work backward from that hash to a properly random seed, which is why the site can afford to publish it ahead of your first bet.
Publishing that hash first commits the site to one seed, since editing the seed would, in practice, change the hash as well. FIPS 180-4, the NIST standard whose August 2015 edition defines SHA-256, puts the property this way: "Any change to a message will, with a very high probability, result in a different message digest." A site that swapped in a friendlier seed after seeing your bets would have to reveal one whose hash no longer matched the hash it had shown you.
One habit follows from that. Copy the hash somewhere of your own before you bet, because a verifier that fetches the original hash from the site at checking time trusts the site's memory of its own promise. On Ethereum, the chain keeps that copy for everyone. The Speedrun Ethereum guide, which lists sealed-bid auctions and voting among the scheme's uses, builds its base contract to store each commitment on-chain and reject a second one from the same address.
A client seed only protects you if it arrives after the hash
The server seed alone would let the site settle everything in advance, so the scheme adds a value from your side. A site that knew your client seed when it committed could, in principle, have generated a great many server seeds, computed your first results under each and kept the one it liked. A client seed entered after the new hash appears closes that door.
So change the default, and change it again each time you retire a pair. A client seed the site generated for you may have been known to it at commit time, and so may one you reused from an earlier pair. It doesn't need to be long or secret, and no value is luckier than another; its only job is to be something the site couldn't have predicted.
The nonce does the rest, rising by one with each bet under the same pair, so one pair of seeds yields a fresh message every round, devto-reader:1 and then devto-reader:2. The server seed stays hidden while the pair is in use, and it has to, since anyone holding it could compute the next result early. Verification is therefore retrospective. You retire the pair, the site reveals the old server seed and commits to a new one, and only then can you check.
Six steps and two library calls rebuild the result
Each step below is a plain operation in any language with a hash library, and none of them needs another value from the site beyond the four in step 1.
- Collect four values from the retired pair: the hash you saved before betting, the seed the site revealed afterward, the client seed you set, and the nonce of the round you want to check.
- Compute the SHA-256 hash of the revealed server seed and compare it with the saved hash, character by character. If they differ, stop, because nothing after this step can be trusted.
- Build the message by joining the client seed, a colon and the nonce, which gives devto-reader:1 for the first bet in the example.
- Compute HMAC-SHA256 with the server seed as the key and the message as the data, and write the result out as 64 hexadecimal characters.
- Take the first eight of those characters, the first 32 bits, read them as an unsigned integer and divide by 4,294,967,296, which is 2 to the power of 32, to get a fraction from 0 up to, but never reaching, 1.
- Multiply the fraction by the number of possible outcomes, 37 for a single-zero roulette wheel, and round down. The whole number you get is the pocket.
In Python, step 2 is one call to hashlib.sha256 and step 4 one call to hmac.new, which takes the key as bytes and, since version 3.8, insists that you name the digest. Node's crypto.createHmac takes the algorithm name and the key. The rest is string handling and arithmetic.
Every input moves the pocket, but only one breaks the commitment
Here are the steps run on seeds short enough to type, where a real server seed would be a long random string. The server seed is orchard-lamp-5521, the client seed devto-reader and the nonce 1. The server seed's hash begins with d9e7e144. The HMAC output begins at 60626324, the integer 1,617,060,644, and dividing by 4,294,967,296 gives 0.3765. Times 37 that is 13.93, so the ball lands in pocket 13. Each later row changes one thing.
|
Scenario |
What changed |
Seed hash begins |
HMAC begins |
As an integer |
Fraction |
|
|
Reference Round |
Nothing: orchard-lamp-5521, devto-reader, nonce 1 |
d9e7e144 (matches) |
60626324 |
1,617,060,644 |
0.3765 |
13 |
|
Next Nonce |
Nonce 2, the following bet |
d9e7e144 (matches) |
e00ba11b |
3,758,858,523 |
0.8752 |
32 |
|
Edited Client Seed |
Client seed devto-readers |
d9e7e144 (matches) |
a531a965 |
2,771,495,269 |
0.6453 |
23 |
|
Swapped Key Order |
Verifier used the message as the key |
d9e7e144 (matches) |
3e0b9ccc |
1,040,948,428 |
0.2424 |
8 |
|
Replaced Server Seed |
Server seed orchard-lamp-5522 |
8e69f737 (fails) |
972f1261 |
2,536,444,513 |
0.5906 |
21 |
Run the reference round twice, or in two languages, and it lands on 13 every time, which is what makes an outside check possible at all. Change any input by one character and you get what amounts to a fresh draw. Adding an s to the client seed moves the ball from 13 to 23, not to 12 or 14.
The last two rows fail in different places. The swapped-key row still passes the hash check and lands on 8, an error that lives entirely inside the verifier. Changing the server seed's final digit gives a hash beginning 8e69f737 instead of d9e7e144, so that round fails at step 2, before its pocket means anything.
When the numbers disagree, suspect the verifier first
Before blaming a site, test the HMAC function itself. RFC 4231, a standards-track document from December 2005, publishes test vectors for HMAC-SHA-256 that it says "have been cross-verified by three independent implementations." Its second test case uses the key "Jefe" and the message "what do ya want for nothing?", and the correct output begins 5bdcc146. Any other answer means the bug is in your code.
Past that test, look for input slips. Two of them move the reference round straight off pocket 13. A newline copied from a terminal onto the end of the message sends it to 31, and counting nonces from 0 when the site counts from 1 gives pocket 0 for what you think is the first bet.
The key can go wrong too. A server seed written as 64 hexadecimal characters is a 64-byte key when passed as text, exactly SHA-256's block size by RFC 4231's count, and a 32-byte key when decoded from hex first. Both are valid keys, and they produce different digests.
Unicode is the quietest trap. The client seed café can be five bytes, with a composed é, or six, with a plain e and a combining accent, and with the example's server seed at nonce 1 the two versions land on pockets 34 and 22. Node's documentation warns that it "does not normalize character representations," so stick to plain ASCII letters and digits.
The 4,294,967,296 possible values don't divide evenly by 37
Step 6 hides a small unevenness that no seed can remove. Thirty-two bits give 4,294,967,296 possible integers, and 37 doesn't divide that number, which leaves a quotient of 116,080,197.19. Share the integers among 37 pockets as evenly as arithmetic allows and seven pockets get 116,080,198 while the other 30 get 116,080,197.
The remainder operator makes the effect easy to see. Payam Bijeh's DEV post on modulo bias, from February 2026, shows it at toy scale, where 16 possible outputs squeezed into six outcomes give four of them three chances each and the other two only two. Taking the integer modulo 37 hands the seven extra values to pockets 0 through 6, while step 6's multiply-and-round-down spreads them to pockets 0, 5, 10, 15, 21, 26 and 31.
Either way the unevenness is about one part in 116 million, against a house edge of one part in 37 on a single-zero wheel, far too small to change anything at the table. It still matters to a verifier, because the two methods disagree about individual rounds. The reference integer, 1,617,060,644, lands on 13 by scaling and on 27 by remainder, so whichever method a site documents is the one to copy.
A verified round still pays 35 to 1 on 37 pockets
Suppose the reference round had been a real bet on 13. A straight-up bet on a single-zero wheel pays 35 to 1 against 37 pockets, so the expected return is 36/37 of the stake, or 97.30%. That return only shows up across a very large number of bets and promises nothing about one session, while the house keeps its 2.70% however many rounds check out.
The seeds can't vouch for games they don't feed, so the scheme matters most for games an operator builds itself, where its own server decides each result and no outside studio's certification stands behind the draw. Shuffle, a crypto casino that holds its gaming license in Curacao, draws the line in that same place: its homepage at https://shuffle.com/ attaches a provably fair algorithm and a built-in round checker to the in-house Originals, Crash, Plinko, Mines and Dice among them, and says outside studios' games are tested and certified by external bodies instead.
Its terms bar anyone located or living in the US, the UK, a dozen other named places or anywhere its service isn't permitted by law or would need a local license, and nothing in this tutorial depends on an account there. A clean check says nothing about what your money is worth, either, since a crypto balance can fall against the dollar between deposit and withdrawal while every round in between verifies perfectly.
That leaves one decision, and it concerns who runs the arithmetic. A site's built-in checker is convenient, but its code comes from the same operator whose results it checks. The independent version is the few lines you wrote yourself, tested against the Jefe vector and pointed at the reference round until it printed 13.

Top comments (0)