How provably fair works in Limbo
Here's how to play Limbo on Cloudbet, and how to check that every multiplier is provably fair.
How to play Limbo
Limbo is a fast, simple multiplier game: before each round, you set a target multiplier â how big a win you're aiming for. Place your bet, then a result multiplier is generated. If it lands at or above your target, you win your target multiplier times your bet. If it lands below your target, you lose the round.
The higher the target multiplier you set, the bigger your potential win â but the less likely the result is to reach it. You can set your target as low or as high as you like before each bet.
Cloudbet Originals Limbo
Cloudbet's own Originals Limbo has a 99% RTP, a max multiplier of x1,000,000, and accepts bets from $0.01 to $20,000.
Limbo's random numbers are generated the same verifiable way as every other Cloudbet Original â see Cloudbet's provably fair system 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 Limbo: how that random number becomes the multiplier you're racing against.
How the multiplier is calculated
Limbo starts from a random 32-bit number â generated by SHAKE256, uniformly distributed, meaning every value between 0 and 2ÂłÂČ-1 is equally likely.
That's not the shape we need, though. A multiplier should land on small values far more often than huge ones â the odds of a 1.01x should be much higher than the odds of a 100,000x. So the random number is reshaped, step by step, into that distribution:
- Rejection sampling is used, but lightly â only to avoid drawing the single largest possible number, since the number is about to be incremented by one.
- The number is incremented by one (so it's never zero, which matters for the division in step 4).
- It's scaled down to a decimal between 0 and 1, by dividing by its maximum possible value.
- 0.99 (Limbo's RTP) is divided by that decimal â this is the step that turns a uniform spread into one where small multipliers are exponentially more common than large ones.
- The result is rounded down to two decimal places.
- Finally, it's "clipped" to Limbo's valid range: anything under 1 becomes 1 (the minimum bet is 1.01, so sub-1 outcomes are all just a loss), and anything over 1,000,000 becomes 1,000,000 (Limbo's maximum payout).

Once generated, the multiplier is fixed and hidden â it can't be changed after your bet is placed. Cashing out (or not) before it's reached is what decides the round.
Verify your own Limbo games
The code below is accurate to Cloudbet's Limbo implementation. Enter your Server Seed, Client Seed, and Nonce to reproduce your own result. BTW: To reveal a game's Server Seed on Cloudbet you need to start using another Server Seed.
Dependencies:
npm install js-sha3
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.
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 = 0; // âïž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
}
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(rng) {
let randomInt32 = rng.next().value; // 'randomInt32' ranges from 0 and 4294967295
while (randomInt32 == 0xFFFFFFFF) { randomInt32 = rng.next().value; } // This can basically be ignored
const fraction = (randomInt32 + 1) / 0xFFFFFFFF; // 'fraction' ranges from 0 to 1
const inverse = 0.99 / fraction; // 'inverse' ranges from 0 to (basically) infinity
let crashPoint = Math.floor(inverse * 100) / 100; // Cut off everything past the second decimal place
// We force the crashpoint to range from 1 to 1,000,000
if (crashPoint < 1.0) {
crashPoint = 1.0;
}
if (crashPoint > 1000000) {
crashPoint = 1000000;
}
return crashPoint;
}
// Example usage
(function main() {
let nonce = 1;
if (usingCustomData) { nonce = myNonce; }
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(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('âïžâđ„ Break Point:', value);
})();
