Provably fair

What is Provably Fair?

Provably fair is an approach used in online gaming that allows every result to be independently verified as genuinely random.

It provides a cryptographic guarantee that game outcomes cannot be altered after a bet is placed. By combining player-provided data, server-generated data and publicly verifiable cryptographic commitments, anyone can independently reproduce and verify the outcome of a round.

This ensures that every result remains both unpredictable before the round and verifiable after it has completed.

How We Keep Our Games Provably Fair

Every outcome is generated using three components:

Server Seed

A secret random value generated by the server. Before gameplay begins, the server publishes a cryptographic hash of the Server Seed. This acts as a commitment, proving the Server Seed existed before the round started without revealing its value. After the round resolves, the Server Seed is revealed and can be verified against the previously published hash.

Client Seed

A value contributed by the player. Players may choose their own Client Seed or use a system-generated value. The Client Seed becomes part of the randomness generation process and can be changed at any time.

Nonce

A counter that increases with every random event. The Nonce ensures that each random result remains unique, even when the same Server Seed and Client Seed are used.

Provably Fair - Verification Guide

This guide explains how the provably fair system generates randomness and how you can independently verify the outcome of any round.

The system operates using a simple principle:

Commit  →  Play  →  Reveal

The server first commits to a Server Seed by publishing its hash. Gameplay then takes place using the Server Seed, Client Seed and Nonce. Once the round has completed, the Server Seed is revealed so that the entire process can be independently verified.

Trust Model

Before a round begins, the server generates a secret Server Seed and publishes:

serverSeedHash = SHA256 (serverSeed)

Because cryptographic hashes are one-way functions, the Server Seed remains hidden while the commitment remains publicly visible.

After the round resolves, the Server Seed is revealed.

Players can then verify:

SHA256 (serverSeed) == serverSeedHash

If the values match, the Server Seed could not have been modified after the commitment was made.

Seeds, Hashes and Nonces

Value Origin Meaning
serverSeed Server Secret random value revealed after the round
serverSeedHash Derived SHA256 hash of the Server Seed
clientSeed Player Player-provided source of randomness
nonce Counter Increments with every random event
initialShuffle Server Initial deck or board state (card games only)
initialShuffleHash Derived Commitment to the initial deck or board state
finalShuffle Server Final deck or board state after the round

What Is Published When

Value Before / During Round After Round Resolves
clientSeed
serverSeedHash
nonce
initialShuffleHash
serverSeed
initialShuffle
finalShuffle

Randomness Generation

Every random value is generated from the combination of:

  • Server Seed
  • Client Seed
  • Nonce

The inputs are combined using HMAC-SHA256 to produce a deterministic cryptographic value.

Conceptually:

(Server Seed + Client Seed + Nonce) → HMAC-SHA256 → Cryptographic Value  

This value is then used to initialize a ChaCha20-based random number generator.

ChaCha20 is a widely used cryptographic algorithm designed to generate secure and statistically uniform random values.

Because the process is deterministic, the same inputs will always produce the same output. This allows every outcome to be independently reproduced and verified after the Server Seed has been revealed.

RNG Translation

The random values produced by the generator are converted into the numerical ranges required by each game.

Examples:

  • Dice converts a random value into a roll within the configured range.
  • Roulette converts a random value into a wheel position.
  • Plinko converts a random value into a landing position.
  • Card games use random values to generate deck shuffles.

Where a game requires a random value within a specific range, rejection sampling is used to ensure that every possible outcome remains equally likely and that no statistical bias is introduced.

Shuffling

Card and board games use the Fisher-Yates shuffle algorithm.

Fisher-Yates is a mathematically proven method for generating unbiased random permutations and is widely used throughout the gaming industry.

Conceptually:

Ordered Deck → Fisher-Yates Shuffle → Randomized Deck  

The shuffle is generated from the same Server Seed, Client Seed and Nonce values used throughout the provably fair system.

Because the shuffle is deterministic, any player can reproduce the exact deck order once the Server Seed has been revealed.

Deck Commitment (Card Games)

For card games, the initial deck order is committed before any cards are dealt.

The commitment is generated as:

initialShuffleHash = SHA256 (initialShuffle + serverSeed)  

The hash is published before gameplay begins, while the deck itself remains hidden.

After the round resolves, the initial deck order is revealed and can be verified against the previously published commitment.

This ensures that the deck could not have been modified after the commitment was made.

Outcome Mapping

Each game maps the primitives above to a concrete result:

  • Dice draws a roll with getRandom (min, max); the bet wins on the over/under comparison against your target.
  • Roulette draws a pocket with nextInt(wheelSize); a bet wins when the pocket is one of the covered positions.
  • Plinko selects a bucket by a weighted random pick whose weights follow a binomial profile; the bucket's multiplier is the payout.
  • Card games (Baccarat / Blackjack / Hi-Lo) - build a known ordered deck, shuffle it (Fisher-Yates, as above), then deal from the top. initialShuffle is the pre-deal order; finalShuffle is the post-round order.

To verify a card game: reconstruct the ordered deck, replay the shuffle with the revealed seeds, confirm it equals initialShuffle, and check the dealt cards.

How to Verify a Result

For a completed round, verification consists of three steps.

Step 1 - Verify the Server Seed

Confirm that the revealed Server Seed matches the previously published Server Seed Hash:

SHA256(serverSeed) == serverSeedHash  

This proves that the Server Seed was committed before gameplay began.

Step 2 - Reproduce the Outcome

Using the revealed Server Seed, Client Seed and Nonce, reproduce the random values generated during the round. The reproduced values should match the published game result exactly.

Step 3 - Verify the Deck Commitment (Card Games)

For card games, verify:

SHA256(initialShuffle + serverSeed) == initialShuffleHash  

This confirms that the starting deck was committed before gameplay began and was not modified afterwards.

Reference Verifier (JavaScript)

The verifier below is standalone—it depends only on the Node.js crypto module and reproduces the random draws exactly.

Critical detail: Node's crypto.createCipheriv ('chacha20', key, iv) follows the OpenSSL convention: the IV is 16 bytes = counter (uint32, little-endian) || 12-byte nonce. The verifier rebuilds the IV as counterLE(4) || nonce12. Integers are read big-endian.

const crypto = require('crypto');

function deriveKeyNonce(serverSeed, clientSeed, nonce) {
  const hmac = crypto.createHmac('sha256', Buffer.from(serverSeed, 'utf8'))
    .update(`${clientSeed}:${nonce}`, 'utf8').digest();           // 32 bytes
  const key = crypto.createHash('sha256').update(hmac).digest();  // 32-byte ChaCha20 key
  const nonce12 = hmac.subarray(0, 12);                           // 96-bit ChaCha20 nonce
  return { hmac, key, nonce12 };
}

function uint32LE(n) { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b; }

// ChaCha20 keystream = encrypt zero bytes. IV = counter(LE) || nonce12 (OpenSSL/Node).
function chachaKeystream(key, nonce12, counter, numBytes) {
  const iv = Buffer.concat([uint32LE(counter), nonce12]);         // 16 bytes
  const c = crypto.createCipheriv('chacha20', key, iv);
  return Buffer.concat([c.update(Buffer.alloc(numBytes)), c.final()]);
}

// Whole-value path: nextLong -> nextDouble in [0,1)
function nextDouble(serverSeed, clientSeed, nonce) {
  const { key, nonce12 } = deriveKeyNonce(serverSeed, clientSeed, nonce);
  const ks = chachaKeystream(key, nonce12, 0, 8);
  let x = 0n;
  for (let i = 0; i < 8; i++) x = (x << 8n) | BigInt(ks[i]);      // big-endian uint64
  return Number(x >> 11n) * 1.1102230246251565e-16;              // (>>>11) * 2^-53
}

// Bounded / shuffle path: 64-byte block stream + rejection sampling
function makeStream(key, nonce12) {
  let blockCounter = 0, block = Buffer.alloc(0), pos = 0;
  const nextByte = () => {
    if (pos >= block.length) { block = chachaKeystream(key, nonce12, blockCounter++, 64); pos = 0; }
    return block[pos++];
  };
  const nextInt = () => {
    const b0 = nextByte(), b1 = nextByte(), b2 = nextByte(), b3 = nextByte();
    return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0;      // big-endian uint32
  };
  return { nextInt };
}
function normalize(stream, n) {
  const bound = 2 ** 32, limit = bound - (bound % n);
  let c = stream.nextInt();
  while (c >= limit) c = stream.nextInt();                        // reject biased tail
  return c % n;
}
function getRandom(serverSeed, clientSeed, nonce, min, max) {     // inclusive
  const { key, nonce12 } = deriveKeyNonce(serverSeed, clientSeed, nonce);
  return normalize(makeStream(key, nonce12), max - min + 1) + min;
}
function shuffle(serverSeed, clientSeed, nonce, list) {
  const { key, nonce12 } = deriveKeyNonce(serverSeed, clientSeed, nonce);
  const s = makeStream(key, nonce12), a = list.slice();
  for (let i = a.length - 1; i > 0; i--) { const r = normalize(s, i + 1); [a[r], a[i]] = [a[i], a[r]]; }
  return a;
}

module.exports = { deriveKeyNonce, nextDouble, getRandom, shuffle };

To verify one draw at nonce k, pass nonce k. To verify a whole session, call the functions for nonces 1, 2, 3, ... in the same order the game drew them.

Worked Example

The values below are produced by the verifier above.

serverSeed = "3e8f1c0b7a96d5e4f2c1b0a9d8e7f6c5b4a3928170615243f9e8d7c6b5a40312"
clientSeed = "player-client-seed-001"

Commitment:


serverSeedHash = SHA256 (serverSeed)
               = b1198418007c4216afd19221dc11274595663fc55219aaf85b6291e2d5bdebee

Intermediate derivation for nonce = 1 (msg = "player-client-seed-001:1"):


hmac        = a0b0aa1106b25fa2c5f1094aa54a5c6ca45b6939b4dbca71146593406b6297e6
streamKey   = SHA256 (hmac)
            = bca5445a75efacdd2cac20b2ed25256721fdc5c451569444631884e190f71cca
streamNonce = hmac[0..12) a0b0aa1106b25fa2c5f1094a

Whole-value draw at nonce = 1:


nextDouble(nonce= 1) -> 0.19902418913672115

Bounded draws five dice rolls getRandom (1, 6) at nonce = 1..5:


dice [6, 6, 3, 6, 4]

Shuffle Fisher-Yates of [0..9] at nonce = 1:


shuffle([0,1,2,3,4,5,6,7,8,9]) -> [0, 9, 1, 5, 6, 4, 8, 7, 2, 3]

Run the verifier with these seeds to reproduce every number above.

const { nextDouble, getRandom, shuffle } = require('./provably-fair-verifier');
const serverSeed = "3e8f1c0b7a96d5e4f2c1b0a9d8e7f6c5b4a3928170615243f9e8d7c6b5a40312";
const clientSeed = "player-client-seed-001";

console.log(nextDouble(serverSeed, clientSeed, 1));                    // 0.19902418913672115
console.log([1,2,3,4,5].map(n => getRandom(serverSeed, clientSeed, n, 1, 6)));  // [6,6,3,6,4]
console.log(shuffle(serverSeed, clientSeed, 1, [0,1,2,3,4,5,6,7,8,9])); // [0,9,1,5,6,4,8,7,2,3]

Since the values match, the Server Seed was correctly committed before gameplay began.

Using the same Server Seed, Client Seed and Nonce in the reference verifier will always reproduce the same outcome.

Notes

  • Server Seed, Client Seed and Nonce fully determine every outcome.
  • The same inputs will always generate the same result.
  • HMAC-SHA256 is used to derive cryptographic randomness from the seed values.
  • ChaCha20 is used as the deterministic random number generator.
  • Fisher-Yates is used for deck shuffling in card games.
  • Rejection sampling is used where necessary to maintain uniform distributions.
  • Any modification to the Server Seed after commitment would invalidate the published hash and be immediately detectable.
  • Every result can be independently reproduced and verified after the Server Seed has been revealed.