Provably Fair
You don't have to trust us. Every result is calculated from seeds that neither side can change once a bet is placed, and you can recalculate any round yourself.
We commit
Before you bet, we generate a secret server seed and show you its SHA-256 hash. That locks it in: a different seed would have a different hash.
You add randomness
Your client seed goes into every result, and you can change it anytime. The nonce goes up by one per bet, so no two bets match.
You verify
When you rotate your seed, we reveal the old server seed. Check that it matches the hash, then recalculate your bets below.
Verify a round
This runs in your browser, using the same code the server uses to settle bets.
Fill in the details and press Verify.
The algorithm
Random numbers come from HMAC_SHA256(serverSeed, clientSeed:nonce:cursor). The cursor supplies more numbers to games that need them.
import { createHmac, createHash } from "node:crypto";
// We show you sha256(serverSeed) before you bet, and reveal serverSeed when you rotate.
const commitment = createHash("sha256").update(serverSeed).digest("hex");
function floats(serverSeed, clientSeed, nonce, count) {
const out = [];
for (let cursor = 0; out.length < count; cursor++) {
const bytes = createHmac("sha256", serverSeed)
.update(`${clientSeed}:${nonce}:${cursor}`)
.digest();
// Each 32-byte block gives 8 floats, at 4 bytes per float.
for (let i = 0; i < 32 && out.length < count; i += 4) {
out.push(
bytes[i] / 256 +
bytes[i + 1] / 256 ** 2 +
bytes[i + 2] / 256 ** 3 +
bytes[i + 3] / 256 ** 4
);
}
}
return out;
}How each game uses them
// Dice: 0.00 to 100.00
const roll = Math.floor(floats(...seeds, 1)[0] * 10001) / 100;
// Limbo: floored to 2 decimals, never below 1.00x or above 1,000,000x
const limbo = Math.floor(((1 - edge) / floats(...seeds, 1)[0]) * 100) / 100;
// Coinflip
const side = floats(...seeds, 1)[0] < 0.5 ? "heads" : "tails";
// Mines: draw without replacement from the 25 tiles, one float per mine
const tiles = [...Array(25).keys()];
const mines = floats(...seeds, mineCount).map(
(float, drawn) => tiles.splice(Math.floor(float * (25 - drawn)), 1)[0]
);
// Plinko: one float per row, below 0.5 goes left. Rights counted gives the bucket
const bucket = floats(...seeds, rows).filter((float) => float >= 0.5).length;
// Roulette: one of the 37 pockets
const number = Math.floor(floats(...seeds, 1)[0] * 37);
// Paper Game: two floats per draw, player then house, until they differ
const draw = (float) => Math.floor(float * topNumber) + 1;
// Blackjack: draw without replacement from a 6-deck shoe of 312 cards, one float per card.
// Dealt in order: player, dealer, player, dealer, then each card as it's needed
const shoe = [...Array(312).keys()].map((index) => index % 52);
const cards = floats(...seeds, 80).map(
(float, drawn) => shoe.splice(Math.floor(float * (312 - drawn)), 1)[0]
);
// Crash: from the round's hash. Hashing it gives the previous round's hash,
// all the way back to the terminal hash we published before any round
const value = parseInt(hash.slice(0, 13), 16);
const point = Math.floor(((1 - edge) * 2 ** 52 * 100) / (2 ** 52 - value)) / 100;
// PvP Coinflip: the seed is committed when the lobby opens. The reveal value is the
// joining player's client seed, which nobody knew then
const side = floats(serverSeed, revealValue, 0, 1)[0] < 0.5 ? "heads" : "tails";
// Jackpot: the seed is committed when the round opens. The reveal value is every entry's
// client seed, joined with "|". The ticket lands in one entry's share of the pot
const ticket = Math.floor(floats(serverSeed, revealValue, 0, 1)[0] * pot);