How provably fair works in Keno
How to play Keno
Keno is a numbers-drawing game: you pick a set of numbers from a board, then the game draws its own set of numbers. The more of your picks match the drawn numbers, the bigger your payout — how much bigger depends on how many numbers you picked to begin with. Once you've made your picks and placed your bet, the draw happens and any matches are revealed instantly.
Cloudbet Originals Keno
Cloudbet's own Originals Keno uses a 40-number board: you pick 1 to 10 numbers, and the game draws 10. It has a 99% RTP, a max multiplier of x1,000, and accepts bets from $0.01 to $20,000. Fewer picks means higher volatility and bigger payouts per hit; more picks means more frequent wins but lower returns per hit. Auto Bet is available to automate a number of rounds.
Keno's draw is generated the same verifiable way as every other Cloudbet Original — see our provably fair overview for how the Server Seed, Client Seed, and Nonce combine into a Round Signature and then a stream of random numbers via SHAKE256. This page covers what's specific to Keno: how that random number sequence becomes the actual set of drawn numbers.
Turning random numbers into a Keno draw
Keno's board is numbered 1 to 40. Ten of those 40 numbers need to be drawn, one at a time, without repeating any number.

We call the number of not-yet-drawn numbers remaining P. P starts at 40 and drops by 1 each time a number is drawn.

A random 4-byte (32-bit) number is drawn from the SHAKE256 output for each of the 10 draws, ranging from 0 to 4,294,967,295. To pick fairly among the P remaining numbers, we apply rejection sampling (see our guide to rejection sampling), then take the result modulo P to get the next drawn number's position. If more numbers still need drawing, the chosen number is removed from the pool, P drops by 1, and the process repeats.

This continues until all 10 numbers are drawn.


Gameplay
The full set of 10 drawn numbers is generated and hidden on the server before you make your picks — it can't be changed or altered undetected afterward, thanks to the Commitment already shared with you. Your picks are simply checked against this pre-drawn set once the round resolves, and your payout is calculated from how many matches you have.
Code for independently verifying your Cloudbet Keno results
Same flag as above applies here — this code mirrors Mines' confirmed pickUniquePositions/rejection-sampling implementation exactly, just adapted for a 40-number board and a fixed 10 draws (instead of a 25-tile grid and a variable mine count). Please confirm against Cloudbet's actual Keno implementation before this ships.
Enter your Server Seed, Client Seed, and Nonce to verify your own results. Install the one dependency, and run it yourself to reconstruct your game's result.
Note: to reveal a completed game's Server Seed on Cloudbet, you need to have already started a new round (which rotates 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 mine positions to the mine positions outputted here. This proves that your game's outcome was determined solely by the pre-game seeds and was not altered afterwards.
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
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
}
}
// Step 4: Pick a number of unique positions
function pickUniquePositions(numOfPicks, totalPositions, rng) {
const available = Array.from({ length: totalPositions }, (_, i) => i);
const picks = \[];
for (let i = 0; i < numOfPicks; ++i) {
const index = sample(available.length, rng);
picks.push(available[index]);
available.splice(index, 1); // Remove chosen pump from list of non-faulty pumps
}
return picks;
}
// Rejection sampling for fair results
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;
if (usingCustomData) { nonce = myNonce; }
const numOfHits = 10;
const totalPositions = 40;
const { serverSeed, commitment } = generateServerSeed();
const clientSeed = getClientSeed();
const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
const numbersToGenerate = totalPositions * 2; // We need at least one random number per position, but there is an improbable possibility we'll need two
const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);
let hits = pickUniquePositions(numOfHits, totalPositions, rng);
hits = hits.map((value)=> value + 1 );
hits.sort((a, b)=>a-b);
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('🔁 Round Signature (can be verified after the game):', roundSignature);
console.log(`🎯 Hits:`, hits);
})();
