Skip to content

Provably Fair ​

Status: available as a per-operator option — every game

Provably fair is a per-operator toggle (Tenants → jurisdiction config → Provably fair). When enabled, each real-money round's RNG is derived deterministically from a seed the server commits to before the bet, and the seed/nonce are exposed so anyone can reproduce the outcome. For the multi-step (Safe Smash, Bit Key Rush) and session games (Fox'n Flock, Katapuuult, Wild Stride) each in-round action derives from its own sub-nonce, so every step is independently reproducible; Pass the Jay, Shots and Red District draw everything when the round starts, from the setup sub-nonce; Slotty Slots and Shatter, single-step games, draw from the bare round block. A public, zero-dependency verification page runs at https://games.slottylabs.com/verify/, and every game's in-play Provably Fair panel (open the game's info/help) shows the round's commitment and links to that page with the round pre-filled for one-click verification. For Safe Smash, Bit Key Rush and Wild Stride the page replays the whole round in the browser and checks what it paid against what the round booked (see Whole-round replay and Wild Stride).

Slotty Labs offers a cryptographic provably-fair system that lets players independently verify that game outcomes were not manipulated. Verification runs entirely in the browser using native Web Crypto — no trust in our servers required.

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, Shatter, 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 SmashOne hit: the multiplier, the hammer roll and, on the cracking hit, the jackpot draws
bitKeyRushMapperBit Key RushOne guess: the pool tier and the trace roll (both drawn on every guess)
foxMapperFox'n FlockMaps bytes to goat multipliers, positions, shepherd AI
catapultMapperKatapuuultMaps bytes to collectible/hazard placement grid
passTheJayMapperPass the JayThe undercover cop's seat; with the recorded passes, where the round ended
shotsMapperShotsThe crash point, which regular drinks and how they go down; with the recorded exit, whether the payout follows from the crash point
redDistrictMapperRed DistrictThe night draw and how many visits go well; with the recorded visits, where the round ended and the step paid
wildStrideMapperWild StrideEvery step's draw (step k at sub-nonce k); with the recorded strides, each step's outcome, the running multiplier and the payout
shatterMapperShatterThe fracture draw (bare round block); with the recorded mode, the catalogue outcome, the tokens collected, the multiplier and the payout

Whole-round replay (Safe Smash, Bit Key Rush) ​

These two games pay per action, from state that runs through the round: Bit Key Rush's secret key and FBI trace, Safe Smash's damage and round balance. One sub-nonce cannot show what a round paid, so the round is replayed whole:

  1. Setup (sub-nonce 0). Bit Key Rush: the 12-character key (12 uniform draws over A–Z 0–9). Safe Smash: how many damaging hits the safe takes.
  2. Every recorded action (sub-nonce 1, 2, …). Each guess or hit is drawn from its own bytes, and its debit (stake, plus any wrong-guess penalty or hammer clawback) and credit (within the round's win cap) are recomputed.
  3. The record. The totals and the way the round ended are compared, to the smallest currency unit, with what the round booked.

The only inputs that come from the operator are the round's own record and the published paytable that priced it (chosen by the round's start time, and shown with its content hash):

GameRecorded inputs
Bit Key RushThe characters guessed, in order; stake per guess; win ceiling
Safe SmashThe number of hits (hitting is the only move); stake per hit; win ceiling

The page computes the replay in the browser, and the public verifier (POST /api/v1/provably-fair/verify with the round) runs its own; the page flags any disagreement between the two. Two historical limits are stated on the page when they apply: Bit Key Rush rounds played before guesses were recorded (20 October 2026) show their key but cannot replay their guesses, and Safe Smash rounds from before 5 October 2026 recorded an unreliable hit count, so their hits are taken from the stake total. Every hit stakes the same amount, so exactly one count fits that total.

Shots: the crash point and the recorded exit ​

Shots draws everything when the round starts, from the setup sub-nonce, in this order:

  1. The crash draw n, a uniform integer in [0, 2^52 − 1]. The crash point, in hundredths of a multiplier, is floor(rtpBasisPoints × 2^52 / (100 × (n + 1))), using the RTP of the published paytable that priced the round (chosen by the round's start time). Below 1.01× the regular is out on the first shot; the cap is 1,000×.
  2. The regular, a uniform integer over the published list of regulars. When the bet named the regular already at the bar, the draw is still made and shown as not used, and the recorded regular is the one who drank.
  3. How they go down, a uniform integer in [0, 99]: below 55 they puke, otherwise they pass out.

The rest of the round is when the player left. The round record carries it, written by the operator's game service and never supplied by the caller:

Recorded inputMeaning
autoCashout100The automatic exit the player set, in hundredths (absent when none)
capMultiplier100The round's cap, in hundredths: the game's 1,000× or a lower operator or jurisdiction maximum win for that stake
exitThe exit paid, if any: kind (manual, auto or cap), multiplier100 and elapsedMs after the first shot
patronChosenThe regular the bet named (already at the bar on the player's screen); absent when the regular was drawn

An exit at m pays m × stake only when the crash point is at least m, so the verifier checks that the recorded exit follows from the crash point: a manual or automatic exit at or below it (and below the automatic exit and cap, for a manual one), a cap exit only if the crash point reached the cap, and no exit only if the round crashed before its automatic exit and its cap. Anything else is reported as inconsistent. Without a record the verifier still reproduces the crash point, the regular and the ending, which is everything the seed decides.

The server seed is held while a round is open and revealed only after it ends: with it, the crash point could be computed before the player leaves.

Red District: the night draw and the recorded visits ​

Red District makes one draw when the round starts, from the setup sub-nonce: the night draw n, a uniform integer in [0, 2^52 − 1] (purpose label lucky_visits). With the steps in hundredths of a multiplier (1.30× = 130) and the RTP of the published paytable that priced the round (chosen by the round's start time), visit k goes well exactly when

n < T_k = floor(rtpBp × 2^52 / (100 × step_k))      k = 1 … 8

so the number of visits that go well is how many of the eight thresholds n is below. A visit draws nothing, and the window the player picks is recorded but never read by the math.

The rest of the round is how many visits the player made before leaving. The round record carries it, written by the operator's game service and never supplied by the caller:

Recorded inputMeaning
visitsThe visits made, a losing one included (0 … 8)

If every recorded visit went well, the round paid the step for that many visits (0 visits: step 0, the RTP itself; 8: the top step, which ends the round by itself). If the last one did not, it must be the first visit beyond the good ones, and the round paid nothing. Anything else is reported as inconsistent. Without a record the verifier still reproduces the draw and the number of good visits, which is everything the seed decides. The verification page recomputes all of it in the browser and compares stake × step with what the round booked.

The server seed is held while a round is open and revealed only after it ends: with it, the number of good visits could be computed before the player leaves.

Wild Stride: one draw per step and the recorded strides ​

Wild Stride draws nothing when the round starts. Every forward step makes one draw at that step, from its own sub-nonce — step k at sub-nonce k (purpose label step_outcome): a uniform integer u in [0, 2^52 − 1]. The stride the player chose for the step (short, long or wild) picks the table in the published paytable that priced the round (chosen by the round's start time). Each table lists its step multipliers largest first, with integer weights over weightScale, and the fall last. With c_i the cumulative weight of the first i outcomes,

S   = rtpBp on the round's first step, 10000 on every later step
T_i = floor(c_i × 2^52 × S / (weightScale × 10000))
u < T_1 → the largest multiplier;  T_(i−1) ≤ u < T_i → multiplier i;  u ≥ T_last → the fall

Each published table is an exactly even bet (Σ weight × multiplier = weightScale), so the first step's scaling by the RTP is the only place the house edge lives, and the floors only ever move probability towards smaller multipliers and the fall. On paytables published with "capRule": "fair-at-cap" (engine 1.1.0 and later) a step that can reach the round's cap is scaled to stay fair counting the capped win: with the cap in the total's fixed point C = ceil(maxWin × 10^8 / stake), the total before the step M and V_i = min(floor(M × f_i / 100), C), if some V_i reaches C,

A   = Σ weight_i × V_i
T_i = floor(c_i × 2^52 × S × M / (10000 × A))

and otherwise the thresholds above. Rows without capRule (engine 1.0.0) use the plain thresholds on every step; the verifier refuses a row with any other value. The round's multiplier is the product of its step multipliers in fixed point (multiplierScale = 10^8), rounded down after every step; collecting pays floor(stake × multiplier / 10^8) (collecting before the first step: floor(stake × rtpBp / 10000)), a fall pays nothing, and every payout stops at the round's win cap — a step that reaches it ends the round.

The rest of the round is the player's choices. The round record carries them, written by the operator's game service and never supplied by the caller:

Recorded inputMeaning
stridesThe stride of every step taken, in order, a final fall included
maxWinThe win cap the round was locked to when it started (minor units)

With them (and the round's stake) the verifier replays every step's draw with its stride's table, the running multiplier, the cap and the payout, and compares the result with what the round booked. A step recorded after the round had ended, a stride the game does not offer, or more steps than a round can take is reported as inconsistent. Verifying a single sub-nonce k also shows step k's draw and what it gives under each stride. The verification page does all of it in the browser, independently of the server.

Shatter: one draw at the bet and the recorded mode ​

Shatter is a single-step game: a round is one strike and makes one draw, at the bet, from the bare round block clientSeed:nonce (no sub-nonce, as for Slotty Slots; purpose label fracture). The draw is a uniform integer u in [0, W − 1], where W is the draw space of the fracture mode the player struck in (the published row's modes.<mode>.drawSpace, the sum of that mode's weights — the same in every RTP variant). With the weights of the published paytable that priced the round (chosen by the round's start time), listed in catalogue order, the outcome is

outcome = the first i with  u < w_0 + w_1 + … + w_i

so every outcome's chance is exactly w_i / W. The outcome's tokens are the ones on the fragments that fall; the round pays

multiplier (hundredths) = rawSum(outcome) × scaleHundredths(mode)
payout                  = floor(stake × multiplier / 100), at most the round's max win

The mode is the player's choice, fixed before the strike. The round record carries it, written by the operator's game service and never supplied by the caller:

Recorded inputMeaning
modeThe fracture mode: controlled, unstable or extreme
maxWinThe round's money cap, minor units (the stake comes from the round's own total bet)

With them the verifier reproduces the draw, the outcome, the tokens collected, the multiplier and the payout; the verification page recomputes all of it in the browser and compares the payout with what the round booked. A row whose outcomes, tokens or weights do not add up (a draw space that is not the sum of its weights, a raw sum that does not match its tokens, an unknown order) is refused rather than read. Without a recorded mode, the verifier shows what the seed gives in each mode — each mode draws over its own range, so the draw itself differs per mode.

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 ​

Transport

Gameplay and provably-fair data are delivered over HTTPS REST today. The round response already carries provablyFair (serverSeedHash, clientSeed, nonce, and per-action sub-nonce), and the in-game Provably Fair panel plus the /verify/ page use that. The WebSocket event shapes below are the planned real-time protocol and are not yet delivered — treat them as roadmap.

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