Sports

Games

General

Custom Games - Provably Fair

How It's Used in Custom Games

Every bet in a YEET Original starts from a round seed. The round seed is an HMAC-SHA256 of your client seed and the bet's nonce, keyed with a server seed chosen by YEET. The game then draws every random value it needs for that bet from the round seed. The server seed is shown to you hashed before you play and revealed when you rotate your seed pair, so you can recompute the round seed of any past bet and check that its result was not changed after you bet.

Other verifiers may not match

Verification sites made for other casinos can generate random numbers differently, so checking a YEET bet there can give a different result. Verify YEET bets in the Verify tab of a game's Fairness window, or with the code on this page.

Random Number Generation

Each bet is generated in two steps. First, your seed pair and the bet's nonce give one round seed. Second, the round seed produces a stream of random bytes, and the game reads unbiased whole numbers from that stream. The same inputs always give the same numbers, which is what makes a result verifiable.

1. The inputs

  • Server seed — 64 random hex characters (32 bytes) generated by YEET. You see its SHA-256 hash before you bet; the seed itself is revealed when you rotate to a new pair.
  • Client seed — any string you choose. Change it at any time from the Fairness panel of a game; changing it rotates the server seed too.
  • Nonce — starts at 0 and goes up by one with every bet on the current seed pair.
  • Cursor — the last part of the round seed message. It is always 0.

2. The round seed

The round seed is an HMAC-SHA256 keyed with the server seed (as 32 bytes) over the text clientSeed:nonce:0. The result is 32 bytes, written as 64 hex characters. One bet has exactly one round seed.

async function deriveGameSeed(serverSeed, clientSeed, nonce) {
  const seedBytes = hexToBytes(serverSeed);
  if (seedBytes.length !== 32) {
    throw new Error(`Server seed must be 32 bytes (64 hex characters), got ${seedBytes.length}.`);
  }

  const key = await crypto.subtle.importKey(
    'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  const message = new TextEncoder().encode(`${clientSeed}:${nonce}:0`);
  return bytesToHex(new Uint8Array(await crypto.subtle.sign('HMAC', key, message)));
}

3. Entropy from the round seed

The round seed is the key of a second HMAC-SHA256. Each call signs an 8-byte counter (little-endian, starting at 0) and gives a block of 32 bytes; the counter goes up by one per block. Reads take bytes from the current block in order. When a read needs more bytes than are left in the block, the rest of the block is skipped and the next block is generated.

class GameEntropy {
  constructor(seedBytes) {
    if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes (64 hex characters).');
    this.buffer = new Uint8Array(32);
    this.offset = 32;
    this.cursor = 0;
    this.key = crypto.subtle.importKey(
      'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
    );
  }

  async next(count) {
    if (count < 1 || count > 32) throw new RangeError('count must be between 1 and 32');

    if (this.offset + count > 32) {
      const message = new ArrayBuffer(8);
      const view = new DataView(message);
      view.setUint32(0, this.cursor >>> 0, true);
      view.setUint32(4, Math.floor(this.cursor / 0x100000000) >>> 0, true);

      const hash = await crypto.subtle.sign('HMAC', await this.key, message);
      this.buffer.set(new Uint8Array(hash));
      this.cursor++;
      this.offset = 0;
    }

    const slice = this.buffer.slice(this.offset, this.offset + count);
    this.offset += count;
    return slice;
  }
}

4. Unbiased numbers

Outcomes are drawn as whole numbers. readUint32 reads 4 bytes as an unsigned 32-bit integer (little-endian). unbiasedUint(entropy, n) returns a number from 0 to n − 1, each with the same chance. When n is a power of two it takes the remainder directly. Otherwise it accepts only values below the largest multiple of n that fits in 32 bits and reads again when a value is at or above it, so no outcome is favoured.

async function readUint32(entropy) {
  const bytes = await entropy.next(4);
  return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true);
}

async function unbiasedUint(entropy, range) {
  if (range <= 0) throw new Error('Range must be positive');
  if (range === 1) return 0;

  if ((range & (range - 1)) === 0) {
    return (await readUint32(entropy)) % range;
  }

  const maxValue = 0xffffffff;
  const threshold = maxValue - (maxValue % range) - 1;

  for (;;) {
    const raw = await readUint32(entropy);
    if (raw <= threshold) return raw % range;
  }
}

5. Helpers

function hexToBytes(hex) {
  if (typeof hex !== 'string') throw new TypeError('Hex value must be a string.');
  const clean = hex.trim().replace(/^0x/i, '');
  if (clean.length % 2 !== 0) throw new Error('Hex value must have an even length.');
  if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('Hex value contains non-hex characters.');

  const bytes = new Uint8Array(clean.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
  }
  return bytes;
}

function bytesToHex(bytes) {
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}

function toArrayBuffer(bytes) {
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}

6. Verifying a bet

  • Open the bet from your history and copy the client seed, the nonce and the hashed server seed.
  • Rotate your seed pair to reveal the server seed, and check that its SHA-256 hash matches the hash you saw before the bet.
  • Run deriveGameSeed with the server seed, client seed and nonce to get the round seed.
  • Create a GameEntropy from the round seed, apply the game's conversion (see Conversions) and compare the result with your bet.