How provably fair works in Roulette
Here's how to play Roulette on Cloudbet, and how to check that every spin is provably fair.
How to play Roulette
Roulette is a classic casino game where you bet on where a spinning ball will land on a numbered wheel. You place one or more bets on the number table, then the wheel spins and the ball is dropped. Once it settles into a numbered pocket, that's the winning number β any bets that cover it pay out, and everything else loses.
Placing bets
You can bet on the outcome in a few different ways, and you're free to combine several bets in the same round. Inside bets (on specific numbers) pay more but are harder to hit; outside bets (on broader groups) pay less but hit more often:
- Straight-up / Single Number (35:1): bet on one exact number (0β36, or 00 in American Roulette).
- Split (17:1): two adjacent numbers.
- Street (11:1): three numbers in a row.
- Corner (8:1): four numbers meeting at a corner.
- First Four / Top Line (8:1 European / 6:1 American): covers 0 (and 00 in American), 1, 2, 3.
- Six Line (5:1): two adjacent rows, six numbers.
- Column (2:1): one of three vertical columns.
- Dozens (2:1): 1β12, 13β24, or 25β36.
- Red/Black, Odd/Even, High/Low (1:1 each).
European vs. American Roulette
Cloudbet offers both versions. European Roulette has a single zero (37 possible outcomes, 97.30% RTP); American Roulette adds a second "00" pocket (38 possible outcomes, 97% RTP), which slightly increases the house edge. Which version you're playing should be shown clearly before you place a bet. Both include an Autoplay mode for continuous spins with preset chip values and spin counts.
Once your bets are placed and the round resolves, any winning bets are paid out automatically according to their odds, and the table resets for the next round.
This page covers how Roulette turns a provably fair random number sequence into a winning number. For how that random sequence is generated in the first place β server seed, client seed, nonce, hashing, SHAKE256 β see Cloudbet's provable fairness explainer.
Turning random numbers into a Roulette result
For Roulette, the provably fair system has to prove the winning number is chosen by verifiable randomness, not picked after the fact.
The process is simple: in European Roulette, one whole number between 0 and 36 is sampled from the random number sequence β that's the winning number. In American Roulette, the sampled number can range from 0 to 37, with "37" mapped to the double zero, "00"; results 0 through 36 are taken as-is.
How the winning number is selected
The random bytes generated by SHAKE256 (see our provable fairness page) are split into 4-byte unsigned whole numbers, ranging from 0 to 2Β³Β²β1. Roulette's winning number is chosen from 37 possible outcomes (European) or 38 (American) β so in the steps below, P is 37 or 38.
To pick a winning number fairly from those P outcomes, we apply rejection sampling (see How provably fair works), then take the resulting number modulo P β the remainder becomes the winning number (for American Roulette, "37" is displayed as "00").

Once generated, the winning number is saved and hidden server-side β it can't be changed or altered undetected after the round starts. Once you've placed your bet, it's compared against this pre-determined number to settle the round.
Verify your own Roulette results
We've written a small Node.js program accurate to Cloudbet's Roulette implementation (both European and American). Enter your Server Seed, Client Seed, and Nonce to reproduce your result. (Reminder: to reveal a round's Server Seed on Cloudbet, you need to have already rotated to a new seed pair.)
Dependencies:
npm install js-sha3
The code:
// If you are running this yourself, then you can input your own data here
// You can then compare:
// 1) Cloudbet's provided Commitment to the Commitment outputted here. This proves that the Server Seed was not altered during gameplay.
// 2) Cloudbet's provided game outcome to the game outcome outputted here. This proves that your game's outcome was determined solely by the pre-game seeds and was not altered afterwards.
const Version = Object.freeze({ AMERICAN: "american", EUROPEAN: "european" }); // Here the possible versions are listed
let myServerSeed = 'your_server_seed'; // βοΈYOUR_INPUTβοΈ: Enter your (unhashed) Server Seed here
let myClientSeed = 'your_client_seed'; // βοΈYOUR_INPUTβοΈ: Enter your Client Seed here
let myNonce = 1; // βοΈYOUR_INPUTβοΈ: Enter your Nonce here
let myRouletteVersion = Version.AMERICAN; // βοΈYOUR_INPUTβοΈ: Enter your roulette version (either Version.AMERICAN or Version.EUROPEAN)
const usingCustomData = false; // Change this to true if you are using your own data
// Load libraries
const crypto = require('crypto');
const { sha3_256, shake256 } = require('js-sha3');
// Step 1: Generate a Server Seed and Commitment
// And generate a Client Seed - either by player input or by the player's browser
function generateServerSeed() {
let seed = crypto.randomBytes(32).toString('hex');
if (usingCustomData) { seed = myServerSeed; }
const commitment = sha3_256(seed);
return { 'serverSeed': seed, 'commitment': commitment };
}
function getClientSeed() {
let seed = crypto.randomBytes(32).toString('hex');
if (usingCustomData) { seed = myClientSeed; }
return seed;
}
// Step 2: Create a Round Signature with SHA3 using a Server Seed, Client Seed and Nonce
function createRoundSignature(serverSeed, clientSeed, nonce) {
return sha3_256(`${serverSeed}:${clientSeed}:${nonce}`);
}
// Step 3: Create a SHAKE256 stream generator from a Round Signature.
// To simplify this example program, this function doesn't create a stream, but instead a sequence of custom length.
function* createRandomNumberSequence(numbersToGenerate, roundSignature) {
const numOfBytes = 4 * numbersToGenerate; // 4 bytes (32 bits) per random number we generate
const byteStream = shake256.create(8 * numOfBytes); // shake takes bits as input
byteStream.update(roundSignature); // Use the Round Signature to seed the generator
const buf = Buffer.from(byteStream.digest()); // The Buffer class lets us package every 4 bytes into one 32-bit integer
for (let i = 0; i < numbersToGenerate; i++) {
yield buf.readUInt32BE(i * 4); // Read buf in 4-byte steps
}
throw new Error("No more numbers in the random number sequence! Fix: make 'numbersToGenerate' larger.");
}
// Step 4: Draw from the random number sequence to calculate the game result
function getGameResult(rouletteVersion, rng) {
let numOfOutcomes = 0;
if (rouletteVersion === Version.EUROPEAN) {
numOfOutcomes = 37;
} else if (rouletteVersion === Version.AMERICAN) {
numOfOutcomes = 38;
}
let result = String(sample(numOfOutcomes, rng))
// Convert the result '37' to '00'
if (result === '37') {
result = '00';
}
return result;
}
// Sample a number from P number of outcomes using Rejection Sampling
function sample(P, rng) {
const maxAcceptable = Math.floor(0x100000000 / P) * P;
// Rejection sampling
let rand;
do {
rand = rng.next().value;
} while (rand >= maxAcceptable);
// Modulo operation
const sampled_number = rand % P;
return sampled_number;
}
// Example usage
(function main() {
let nonce = 1;
let rouletteVersion = Version.AMERICAN;
if (usingCustomData) { nonce = myNonce; rouletteVersion = myRouletteVersion; }
const { serverSeed, commitment } = generateServerSeed();
const clientSeed = getClientSeed();
const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
const numbersToGenerate = 1 * 2; // We need 1 random number, but there is an improbable case we'll need two
const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);
const value = getGameResult(rouletteVersion, rng);
console.log('π Commitment (shown before the game):', commitment);
console.log('π Server Seed (revealed after the game):', serverSeed);
console.log('π² Client Seed:', clientSeed);
console.log('π’ Nonce:', nonce);
console.log(`π― Winning Number (in the ${rouletteVersion} version):`, value);
})();

