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.

Diagram showing Cloudbet's provably fair Keno board — 40 numbered tiles, each with its own index for fair number selection.

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.

Worked example of Keno's provably fair draw algorithm: the first number is randomly selected from all 40 positions on the board.

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.

3. Worked example showing Keno's provably fair algorithm drawing the next number from the 39 remaining positions after the first draw is removed.

This continues until all 10 numbers are drawn.

4. The final provably fair Keno board with all 10 randomly and verifiably drawn numbers highlighted.

5. Flowchart of Cloudbet's provably fair Keno calculation: random bytes and rejection sampling determine each drawn number's position on the board.

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);
})();

More provably fair game explainers:

  1. Provably fair Mines
  2. Provably fair Pump
  3. Provably fair Plinko
  4. Provably fair Dice
  5. Provably fair Dice: Golden Ticket
  6. Provably fair Limbo
  7. Provably fair Roulette