How to verify a crash or roulette round: seed, beacon and result
Check a crash or roulette round yourself: trace the seed to the chain, fetch the drand beacon, recompute the round hash and turn it into a result.
Fact-checked 29 October 2026 · 8 sourcesA crash or roulette round can be checked without relying on the site's own verifier. You need the round's seed, the seed before it, one public drand beacon and a few lines of Node.js, and each step either matches or it doesn't. This guide checks one example round from start to finish: the seed is made up but the beacon is real, so every number below can be reproduced.
What you need
A shared round can't use any one player's seed, so its seed comes from the game's hash chain, whose last hash, the terminal, is published before the first round. The seed is mixed with a drand beacon: round hash = HMAC-SHA256(seed, drand randomness). The check takes these inputs:
| Input | Example |
|---|---|
| The round's seed | 40ff587c085b6ba21ad8f701ef137a98790eb53b5f74bb8f1d82c7c67cd8c94c |
| The previous round's seed (the terminal, for round 1) | fa253ab0fb297bc9352d56154402ed2d347a6e6318303bd63ed0754d470b98fa |
| When betting closed | 06:00:00.4 UTC, 29 October 2026 |
| drand round | 33,483,413 |
| House edge, for crash | 1%, as an example |
The two seeds are rounds 2 and 1 of the five-link example chain in the hash chain article, and the closing time is invented. On cs2.casino, round pages recompute the result in your browser (see also the shared rounds section of the provably fair page). The commands below repeat the check with Node.js and curl, so nothing depends on code the site wrote.
Step 1: check the seed against the chain
Hash the seed with SHA-256, as its 64 characters of text. The result must be the previous round's seed, or the terminal for round 1:
node -e "console.log(require('crypto').createHash('sha256').update(process.argv[1]).digest('hex'))" 40ff587c085b6ba21ad8f701ef137a98790eb53b5f74bb8f1d82c7c67cd8c94c
It prints fa253ab0…98fa, round 1's seed, and hashing that once more gives the example chain's terminal, 124a61d5…72ab. Since SHA-256 can't be run backwards, a seed that hashes to its predecessor was fixed when the terminal went public. Hashing round n's seed n times should reach the terminal, as the hash chain article shows.
Step 2: fetch the beacon from two relays
Drand's quicknet network publishes a beacon every 3 seconds, and relays, the public servers that pass beacons on, serve any past round by its number. Ask two relays run by different organisations, Protocol Labs and Cloudflare. The long hex string in the path is quicknet's chain hash, which identifies the network:
curl -s https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/33483413curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/33483413
On 29 October 2026 both returned the same two values:
| Field | Value |
|---|---|
| randomness | 25f052b695b68b7bf2ffddf6d62db9a5339f13904a4d30a82412c0f2fb9d5422 |
| signature | 99cbe974900e5bcf83273f87af5e90e158d3b8a8088c4c724d6ab94df8aeb44e7780f79db404b5f18ac767b3eeb76d60 |
Compare the values, not the raw replies, because Cloudflare lists the fields in a different order. And ask for the round by its number: /public/latest returns whatever came out most recently.
Step 3: check the randomness against the signature
The published randomness is the SHA-256 hash of the round's signature, as drand's specification shows. This time, hash the 48 bytes that the signature's 96 hex characters stand for, not the text, the opposite of step 1:
node -e "console.log(require('crypto').createHash('sha256').update(Buffer.from(process.argv[1], 'hex')).digest('hex'))" 99cbe974900e5bcf83273f87af5e90e158d3b8a8088c4c724d6ab94df8aeb44e7780f79db404b5f18ac767b3eeb76d60
It prints 25f052b6…5422, the published randomness, which ties the randomness to the signature. The signature itself is checked against quicknet's public key with a BLS verification (the signature scheme drand uses), which drand's client libraries run on every beacon they fetch. Drand explained covers how the signature is made and why it can't be known early unless a threshold of the network's nodes collude.
Step 4: check the beacon came after betting closed
Quicknet's round 1 came out at its genesis time, Unix time 1,692,803,367 (15:09:27 UTC on 23 August 2023), with a new round every 3 seconds. So round r is due at genesis + (r − 1) × 3 seconds:
node -e "console.log(new Date((1692803367 + (33483413 - 1) * 3) * 1000).toISOString())"
That is 1,692,803,367 + 33,483,412 × 3 = 1,793,253,603, or 06:00:03 UTC on 29 October 2026, 2.6 seconds after betting closed. Round 33,483,412 was due at 06:00:00, 0.4 seconds before the close, so a round that used it would fail here. Drand's specification has nodes start signing a round only when its time comes, and a stalled network catches up afterwards, so a beacon can arrive late but isn't made ahead of its slot.
Step 5: recompute the round hash
HMAC, defined in RFC 2104, is a hash computed with a secret key. Here the key is the seed and the message is the drand randomness, each used as its 64 characters of text:
node -e "console.log(require('crypto').createHmac('sha256', process.argv[1]).update(process.argv[2]).digest('hex'))" 40ff587c085b6ba21ad8f701ef137a98790eb53b5f74bb8f1d82c7c67cd8c94c 25f052b695b68b7bf2ffddf6d62db9a5339f13904a4d30a82412c0f2fb9d5422
The round hash is ee20187c1727c32e08f85a27203dcb4d8669fb166fc8e3cff306aef7e745cb72. OpenSSL gives the same digest without Node:
printf '%s' 25f052b695b68b7bf2ffddf6d62db9a5339f13904a4d30a82412c0f2fb9d5422 | openssl dgst -sha256 -hmac 40ff587c085b6ba21ad8f701ef137a98790eb53b5f74bb8f1d82c7c67cd8c94c
Step 6: turn the hash into a crash point
Crash reads the round hash's first 13 hex digits, ee20187c1727c, as a whole number, h = 4,189,145,874,395,772. Divided by 2⁵², that's X = 0.930177, and the crash point is floor(100 × (1 − edge) / (1 − X)) / 100, never below 1.00×. With the example 1% edge, 99 / (1 − X) = 1,417.88, which rounds down to 1,417, so the crash point is 14.17×.
In whole numbers, counting in hundredths, the same formula is (10000 − edge in basis points) × 100 × 2⁵² / ((2⁵² − h) × 10000), where a basis point is 0.01%. Those products reach about 4.5 × 10²¹, far past 2⁵³ − 1, above which JavaScript's ordinary numbers can't hold every whole number exactly, so this command uses BigInt, whose division drops the remainder:
node -e "const h = BigInt('0x' + process.argv[1].slice(0, 13)), e = 2n ** 52n; console.log(Number((9900n * 100n * e) / ((e - h) * 10000n)) / 100)" ee20187c1727c32e08f85a27203dcb4d8669fb166fc8e3cff306aef7e745cb72
It prints 14.17, and anything under 1 means 1.00×. For a real round, replace 9900 with 10000 minus the game's edge in basis points. The crash article explains what the formula means for the odds.
The same hash as a roulette slot
Roulette reads only the first 8 hex digits, the bytes 238, 32, 24 and 124. As a float that's 238/256 + 32/256² + 24/256³ + 124/256⁴ = 0.930177, close to crash's X because both read the start of the hash. Then slot = floor(0.930177 × 15) = floor(13.95) = 13. Slot 0 is the bonus, 1 to 7 are T and 8 to 14 are CT, so this would be CT:
node -e "console.log(Math.floor(parseInt(process.argv[1].slice(0, 8), 16) / 2 ** 32 * 15))" ee20187c1727c32e08f85a27203dcb4d8669fb166fc8e3cff306aef7e745cb72
Reading the four bytes as one number and dividing by 2³² gives the same float. A real roulette round has its own chain and seed, so its hash would differ, but steps 1 to 5 don't change. Dust2 rounds start from the same kind of round hash.
When the numbers don't match
An input in the wrong form gives a different hash, not an error. With the example values, these slips give:
| Slip | Result starts with |
|---|---|
echo adds a newline after the randomness in step 5 | 37fbf4b7 |
| The signature hashed as text in step 3 | ec1cd3bc |
| The signature used as the HMAC message | 24300e31 |
| The seed decoded into 32 bytes for the HMAC key | 30305a39 |
Use printf '%s' or pass values as arguments, as above. Also check that you fetched the round's own beacon rather than the latest one, and used the edge the game shows. If a clean recomputation still disagrees, keep the round number, both seeds, the beacon and the recorded result: together they show whether the result came from the committed inputs.
What a passing check shows
A match on every step shows that the seed was fixed when the terminal was published, that the beacon wasn't due until betting had closed, and that the result follows from both by the published rule. The first part holds only if the terminal was public before the round, so save a copy when you first see it.
None of this changes the odds, which sit in the payout rules. In crash, the chance of reaching a multiplier m is (1 − edge) / m, so no cash-out target returns more than 1 − edge on average, and roulette's 2× and 14× payouts hand back 14 coins per 15 staked on average. Every game shows its house edge, and over time the house wins. Solo games such as dice are checked from a seed pair instead, as the step-by-step guide shows.
Sources
- Protocol Specification — drand
- quicknet is live on the League of Entropy mainnet — drand
- drand HTTP API — drand
- quicknet beacon, round 33,483,413 — api.drand.sh
- quicknet beacon, round 33,483,413 — drand.cloudflare.com
- RFC 2104: HMAC: Keyed-Hashing for Message Authentication — RFC Editor
- Crypto — Node.js Documentation
- BigInt — MDN Web Docs


