Skip to content

Provably Fair

Slotty Labs uses a cryptographic provably fair system that allows players to independently verify that game outcomes were not manipulated. The verification library is browser-compatible and shared across all games.

Overview

Every game round uses a combination of server-generated and player-chosen seeds to produce verifiable random outcomes. The server commits to a seed hash before the player places a bet, ensuring the outcome cannot be changed after the fact.

Hash Chain Mechanism

Step 1: Server Seed Generation

The server generates a cryptographically secure 32-byte random seed:

typescript
const serverSeed = crypto.randomBytes(32).toString('hex');
// e.g. "a1b2c3d4e5f6...64 hex chars"

Step 2: Hash Commitment

Before play begins, the server shares the SHA-256 hash of the server seed:

typescript
const serverSeedHash = crypto
  .createHash('sha256')
  .update(serverSeed)
  .digest('hex');
// Player sees this BEFORE betting

Step 3: Combined Hash

When a round is played, the final random value is derived from:

typescript
const combinedHash = crypto
  .createHmac('sha512', serverSeed)
  .update(`${clientSeed}:${nonce}`)
  .digest('hex');
// 128 hex chars = 64 bytes of randomness

Nonce Formats

FormatExampleUse Case
Single-stepclientSeed:42Standard round (slots, safe smash hit)
Multi-stepclientSeed:42:0Step within a multi-step round
Session initclientSeed:42:initSession-based game initialization
Free spinsclientSeed:42:fs0Free spin rounds (fs0, fs1, fs2...)

Verification Algorithm

To verify a round outcome, follow these steps:

1. Verify Server Seed Hash

typescript
const computedHash = SHA256(revealedServerSeed);
assert(computedHash === serverSeedHash); // Must match the pre-committed hash

2. Compute Combined Hash

typescript
const combined = HMAC_SHA512(revealedServerSeed, `${clientSeed}:${nonce}`);

3. Map to Game Outcome

Each game uses a specific mapper that converts the raw bytes into game-specific outcomes using a ByteCursor:

typescript
const cursor = new ByteCursor(hexToBytes(combined));
const outcome = gameMapper(cursor, gameConfig);

Game Mappers

Each game type has a dedicated mapping function:

MapperGameDescription
slotMapperSlotty SlotsMaps bytes to 5 reel positions and bonus triggers
safeSmashMapperSafe SmashMaps bytes to multiplier pool and hammer break point
bitKeyRushMapperBit Key RushMaps bytes to correct chars, trace amounts, multipliers
foxMapperFox'n FlockMaps bytes to goat multipliers, positions, shepherd AI
catapultMapperKatapuuultMaps bytes to collectible/hazard placement grid

Core Algorithms

Uniform Random (uniformFromBytes)

Generates an unbiased random integer using rejection sampling:

typescript
function uniformFromBytes(cursor: ByteCursor, max: number): number {
  const bytesNeeded = Math.ceil(Math.log2(max + 1) / 8);
  const maxValid = Math.floor((256 ** bytesNeeded) / (max + 1)) * (max + 1) - 1;

  while (true) {
    let value = 0;
    for (let i = 0; i < bytesNeeded; i++) {
      value = (value << 8) | cursor.next();
    }
    if (value <= maxValid) {
      return value % (max + 1);
    }
    // Reject and retry with next bytes
  }
}

Weighted Selection (weightedFromBytes)

Selects from a weighted pool using cumulative distribution:

typescript
function weightedFromBytes(
  cursor: ByteCursor,
  items: Array<{ value: unknown; weight: number }>,
): unknown {
  const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
  const roll = uniformFromBytes(cursor, totalWeight - 1);

  let cumulative = 0;
  for (const item of items) {
    cumulative += item.weight;
    if (roll < cumulative) return item.value;
  }
  return items[items.length - 1].value;
}

Fisher-Yates Shuffle (shuffleFromBytes)

Produces an unbiased permutation of an array:

typescript
function shuffleFromBytes<T>(cursor: ByteCursor, array: T[]): T[] {
  const result = [...array];
  for (let i = result.length - 1; i > 0; i--) {
    const j = uniformFromBytes(cursor, i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

Seed Lifecycle

active → expired → revealed / auto_revealed
StateDescription
activeCurrently in use for new rounds
expiredReplaced by a new seed, awaiting reveal
revealedPlayer manually requested the seed reveal
auto_revealedAutomatically revealed after seed rotation

Seed Rotation

When a seed is rotated:

  1. The old server seed is revealed to the player (raw hex value)
  2. A new server seed is generated
  3. The new hash commitment is shared with the player
  4. The nonce counter resets to 0

Players can request seed rotation at any time (between rounds only).

Client Seed

SettingValue
Min length1 character
Max length64 characters
Default16-character random hex string
Character setAlphanumeric + common symbols

Players can change their client seed at any time between rounds.

Provably Fair WebSocket Events

EventDirectionDescription
pf.seed_activatedServer → ClientNew seed pair activated, includes server seed hash
pf.round_dataServer → ClientRound verification data (after round completes)
pf.seed_revealedServer → ClientRevealed server seed after rotation

pf.seed_activated

json
{
  "type": "pf.seed_activated",
  "payload": {
    "serverSeedHash": "a1b2c3d4...",
    "clientSeed": "f0e1d2c3...",
    "nonce": 0
  }
}

pf.round_data

json
{
  "type": "pf.round_data",
  "payload": {
    "roundId": "round-abc123",
    "serverSeedHash": "a1b2c3d4...",
    "clientSeed": "f0e1d2c3...",
    "nonce": 42,
    "gameType": "slots",
    "outcome": { "reelPositions": [3, 7, 12, 1, 9] }
  }
}

pf.seed_revealed

json
{
  "type": "pf.seed_revealed",
  "payload": {
    "serverSeed": "deadbeef...",
    "serverSeedHash": "a1b2c3d4...",
    "totalRounds": 156,
    "newServerSeedHash": "x9y8z7w6..."
  }
}

Verification Interfaces

typescript
interface VerificationInput {
  serverSeed: string;      // Revealed server seed (hex)
  serverSeedHash: string;  // Pre-committed hash
  clientSeed: string;      // Player's client seed
  nonce: number;           // Round nonce
  gameType: string;        // e.g. "slots"
  gameConfig: GameConfig;  // Game-specific configuration
}

interface VerificationOutput {
  valid: boolean;            // Overall verification result
  hashMatch: boolean;        // Server seed hash matches commitment
  combinedHash: string;      // The HMAC-SHA512 combined hash
  outcome: GameOutcome;      // Mapped game outcome
  steps: VerificationStep[]; // Step-by-step trace
}

interface VerificationStep {
  step: number;
  description: string;
  input: string;
  output: string;
}

Step-by-Step Verification Trace

Example trace for a Slotty Slots round:

Step 1: Verify server seed hash
  Input:  serverSeed = "deadbeef1234..."
  Output: SHA256 = "a1b2c3d4..." ✅ matches commitment

Step 2: Compute combined hash
  Input:  HMAC-SHA512("deadbeef1234...", "f0e1d2c3...:42")
  Output: "8f3a7b2c..." (128 hex chars)

Step 3: Map reel 1 position
  Input:  bytes[0..1] = [0x8F, 0x3A] → uniformFromBytes(max=29)
  Output: position = 3

Step 4: Map reel 2 position
  Input:  bytes[2..3] = [0x7B, 0x2C] → uniformFromBytes(max=29)
  Output: position = 7

... (continues for all 5 reels and bonus triggers)

Final outcome: [3, 7, 12, 1, 9] ✅ matches game result