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:
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:
const serverSeedHash = crypto
.createHash('sha256')
.update(serverSeed)
.digest('hex');
// Player sees this BEFORE bettingStep 3: Combined Hash
When a round is played, the final random value is derived from:
const combinedHash = crypto
.createHmac('sha512', serverSeed)
.update(`${clientSeed}:${nonce}`)
.digest('hex');
// 128 hex chars = 64 bytes of randomnessNonce Formats
| Format | Example | Use Case |
|---|---|---|
| Single-step | clientSeed:42 | Standard round (slots, safe smash hit) |
| Multi-step | clientSeed:42:0 | Step within a multi-step round |
| Session init | clientSeed:42:init | Session-based game initialization |
| Free spins | clientSeed:42:fs0 | Free spin rounds (fs0, fs1, fs2...) |
Verification Algorithm
To verify a round outcome, follow these steps:
1. Verify Server Seed Hash
const computedHash = SHA256(revealedServerSeed);
assert(computedHash === serverSeedHash); // Must match the pre-committed hash2. Compute Combined Hash
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:
const cursor = new ByteCursor(hexToBytes(combined));
const outcome = gameMapper(cursor, gameConfig);Game Mappers
Each game type has a dedicated mapping function:
| Mapper | Game | Description |
|---|---|---|
slotMapper | Slotty Slots | Maps bytes to 5 reel positions and bonus triggers |
safeSmashMapper | Safe Smash | Maps bytes to multiplier pool and hammer break point |
bitKeyRushMapper | Bit Key Rush | Maps bytes to correct chars, trace amounts, multipliers |
foxMapper | Fox'n Flock | Maps bytes to goat multipliers, positions, shepherd AI |
catapultMapper | Katapuuult | Maps bytes to collectible/hazard placement grid |
Core Algorithms
Uniform Random (uniformFromBytes)
Generates an unbiased random integer using rejection sampling:
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:
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:
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| State | Description |
|---|---|
active | Currently in use for new rounds |
expired | Replaced by a new seed, awaiting reveal |
revealed | Player manually requested the seed reveal |
auto_revealed | Automatically revealed after seed rotation |
Seed Rotation
When a seed is rotated:
- The old server seed is revealed to the player (raw hex value)
- A new server seed is generated
- The new hash commitment is shared with the player
- The nonce counter resets to 0
Players can request seed rotation at any time (between rounds only).
Client Seed
| Setting | Value |
|---|---|
| Min length | 1 character |
| Max length | 64 characters |
| Default | 16-character random hex string |
| Character set | Alphanumeric + common symbols |
Players can change their client seed at any time between rounds.
Provably Fair WebSocket Events
| Event | Direction | Description |
|---|---|---|
pf.seed_activated | Server → Client | New seed pair activated, includes server seed hash |
pf.round_data | Server → Client | Round verification data (after round completes) |
pf.seed_revealed | Server → Client | Revealed server seed after rotation |
pf.seed_activated
{
"type": "pf.seed_activated",
"payload": {
"serverSeedHash": "a1b2c3d4...",
"clientSeed": "f0e1d2c3...",
"nonce": 0
}
}pf.round_data
{
"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
{
"type": "pf.seed_revealed",
"payload": {
"serverSeed": "deadbeef...",
"serverSeedHash": "a1b2c3d4...",
"totalRounds": 156,
"newServerSeedHash": "x9y8z7w6..."
}
}Verification Interfaces
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