How provably fair works in Plinko

Here's how to play Plinko on Cloudbet, and how to check that every ball's path is provably fair.

How to play Plinko

Plinko is a game of chance where you drop a ball down a pyramid of pins and watch it bounce its way to a payout. Before dropping, you set your bet amount, how many rows the pyramid has, and a risk level. The more rows in play and the higher the risk, the bigger the possible multipliers — but the harder they are to land. Once you drop the ball, it bounces left or right off each row of pins until it lands in one of the buckets along the bottom, each with its own multiplier. Many Plinko games let you drop multiple balls at once for faster play.

Cloudbet Originals Plinko

Cloudbet's own Originals Plinko lets you choose 8, 10, 12, or 16 rows, four risk levels (Low, Medium, High, Extreme), and 1, 5, 10, 20, 50, or 100 balls per drop. It has a 99% RTP. An optional BOOST feature (a 20% surcharge on your bet) adds special 2x, 5x, and 10x ball multipliers, pushing the max multiplier to 100,000x — capped at a $1,000,000 total payout.

For a general introduction to provably fair — server seeds, client seeds, nonces, hashing, and how to verify a result — see Cloudbet's provable fairness system. Below is how it applies specifically to Plinko.

How a ball's path becomes a bucket

Each ball's path down the pyramid is a series of bounces off pins, and every bounce is either leftward or rightward.

That choice can be encoded as a "0" (leftward) or "1" (rightward). A path like "leftward, leftward, rightward" becomes the code "001," and following it lands the ball in Bucket #2.

Here's the key pattern: a path lands in the bucket matching its number of rightward bounces, plus one. So "100," "010," and "001" — all with exactly one "1" — land in Bucket #2 (1 + 1).

This means a random path can be generated and encoded in one step: take a 32-bit random number, read its first N bits (where N is the number of rows), and count how many of those bits are "1." Add one, and that's the bucket the ball lands in. The bucket is calculated and locked in on the server before the round starts, hidden until the round ends and then revealed.

Verify the provable fairness of your own Plinko games

The code below is accurate to Cloudbet's Plinko implementation. Enter your Server Seed, Client Seed, Nonce, number of balls, and number of rows, install the one dependency, and run it.

npm install js-sha3

After installing the dependency, input your own data to the below code block.

// 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.
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 myNumberOfBalls   = 1;                  // ✍YOUR_INPUT✍: Enter your number of balls here
let myNumberOfRows    = 12;                 // ✍YOUR_INPUT✍: Enter your number of rows 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 with a 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: Sample bits to determine ball paths
function pickBuckets(numOfRows, numOfBalls, rng) {
  let bucketIndices = [];
  for (let i = 0; i < numOfBalls; i++) {
    const rawUint32 = rng.next().value;
    let pathArray = rawUint32.toString(2).padStart(32, '0').split("");  // Convert the number into an array of bits
    pathArray.splice(numOfRows);                                        // Select the the first bits for the number of rows
    bucketIndices.push(getBucketIndexFromPath(pathArray));
  }
  return bucketIndices;
}

// Calculates which bucket the ball would land in an array of 1's and 0's where 0 = leftwards, 1 = rightwards
// In principle it counts the amount of times the ball bounces rightwards
function getBucketIndexFromPath(pathArray) {
  let rightCounter = 0;
  for(let i = 0; i < pathArray.length; ++i) {
    if (pathArray[i] === '1') {
      rightCounter += 1;
    } else {
      rightCounter += 0;
    }
  }
  return rightCounter + 1; // Add one so that the left-most bucket is bucket number one
}

// Example usage
(function main() {
  let numOfRows = 12;
  let numOfBalls = 1;
  let nonce = 1;
  if (usingCustomData) { numOfRows = myNumberOfRows; numOfBalls = myNumberOfBalls; nonce = myNonce; }

  const { serverSeed, commitment } = generateServerSeed();
  const clientSeed = getClientSeed();

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = numOfBalls; // Only need to generate one number per ball
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature); 

  const bucketIndices = pickBuckets(numOfRows, numOfBalls, 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('🔁 Round Signature (can be verified after the game):', roundSignature);
  console.log('📋 Number of rows:', numOfRows);
  console.log('🏁 Buckets landed in (Bucket #1 is the leftern-most bucket):', bucketIndices);
})();

Note: to reveal a game's Server Seed on Cloudbet, you need to rotate to a new Server Seed first.

More provably fair game explainers:

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