2026-05-1512 min read

How Provably Fair Algorithms Work (With Code)

A deep dive into the cryptography behind provably fair gaming — HMAC-SHA256 seeding, hash chains, and client-verifiable outcomes with working code examples.

#provably-fair#cryptography#tutorial

What is Provably Fair?

Provably fair is a cryptographic system that allows players to independently verify the fairness of each game round. Unlike traditional casinos that rely on third-party audits, provably fair systems give players the mathematical tools to verify every single outcome themselves.

The core idea is simple: the casino commits to the outcome before the player makes their bet, but reveals the commitment only after the round ends. This makes it impossible for either party to manipulate the result.

The Three Seeds

Every provably fair system relies on three components:

1. **Server Seed** — A random string generated by the server before the round. Its SHA-256 hash is shared with the player before betting begins.

2. **Client Seed** — A random string provided by the player (or auto-generated by their browser). This ensures the server can't predict the final outcome alone.

3. **Nonce** — A counter that increments with each bet, preventing replay attacks and ensuring each round produces a unique outcome even with the same seeds.

How the Algorithm Works

The outcome is calculated by combining all three components using HMAC-SHA512. Here's a working implementation:

import crypto from 'crypto';

function getProvablyFairResult(
  serverSeed: string,
  clientSeed: string,
  nonce: number
): number {
  // Combine seeds with HMAC-SHA512
  const hash = crypto
    .createHmac('sha512', serverSeed)
    .update(`${clientSeed}:${nonce}`)
    .digest('hex');

  // Take first 8 characters (32 bits)
  const subHash = hash.slice(0, 8);

  // Convert to a number between 0 and 1
  const decimal = parseInt(subHash, 16) / 0xFFFFFFFF;

  return decimal;
}

// For a crash game multiplier:
function getCrashPoint(
  serverSeed: string,
  clientSeed: string,
  nonce: number,
  houseEdge: number = 0.04
): number {
  const result = getProvablyFairResult(serverSeed, clientSeed, nonce);

  // 4% house edge: 1 in 25 rounds instantly crashes
  if (result < houseEdge) return 1.0;

  // Calculate crash point using inverse function
  return Math.floor((1 / (1 - result)) * 100) / 100;
}

Verification Flow

Here's how the complete verification flow works:

1. **Before the round**: Server generates a random server seed and sends its SHA-256 hash to the player.

2. **Player bets**: The player submits their bet along with their client seed.

3. **Round plays out**: The outcome is calculated using the combined seeds.

4. **After the round**: The server reveals the original server seed. The player can now hash it themselves and verify it matches the hash they received before betting.

5. **Outcome verification**: Using the revealed server seed, their client seed, and the nonce, the player recalculates the outcome and confirms it matches what happened in the game.

Hash Chain for Seed Rotation

Production systems use hash chains to pre-commit an entire sequence of server seeds. The server generates 10 million seeds by repeatedly hashing a single initial seed. The last hash in the chain is published first, and seeds are revealed in reverse order. This proves that all outcomes were determined before any bets were placed.

function generateHashChain(initialSeed: string, length: number): string[] {
  const chain: string[] = [initialSeed];

  for (let i = 1; i < length; i++) {
    const prev = chain[chain.length - 1];
    chain.push(
      crypto.createHash('sha256').update(prev).digest('hex')
    );
  }

  // Reverse so we use them from last-generated to first
  return chain.reverse();
}

// Publish chain[0] (the last hash) as the public commitment
// Use chain[1], chain[2], etc. as server seeds for rounds
// After each round, reveal the server seed — players verify
// it hashes to the previously known value

Security Considerations

When implementing provably fair systems, keep these critical points in mind:

- **Never reuse server seeds** — each round must use a unique seed from the hash chain. - **Use cryptographically secure randomness** for initial seed generation (crypto.randomBytes, not Math.random). - **Store commitments immutably** — once a hash is sent to a player, it must not change. - **Allow client seed rotation** — players should be able to change their client seed at any time. - **Provide a public verification tool** — a page where anyone can input seeds and verify outcomes. - **Consider timing attacks** — ensure seed generation and commitment happen well before bets are accepted.

Need help building this?

I build production iGaming platforms. Let's talk about your project.

Get In Touch