Provably fair
Every House Games result is calculated from three inputs that are fixed before you bet: a server seed we commit to in advance, a client seed you control, and a nonce that counts your bets. Nothing about a round can change after it starts, and you can recompute any result yourself with the code on this page.
How it works
- Server seed
- 64 random hexadecimal characters chosen by us. Before you bet you only see its SHA-256 hash. The seed itself is revealed when you rotate the pair, and you can check that it hashes to the value you were shown.
- Client seed
- Your half of the pair. It is set at random, and you can replace it with any word of 1 to 64 visible characters when you rotate. We committed to the server seed before you chose it, so neither side can steer a result.
- Nonce
- Counts the rounds played on the current pair, starting at 1. Every round uses a new nonce, so a pair never produces the same round twice.
- Rotating
- Rotating ends the current pair. Its server seed is revealed so you can verify every round it settled, and the next pair, whose hash you have already seen, takes over with the nonce back at 1. Each game keeps its own pair.
From seeds to numbers
A round reads bytes from HMAC-SHA256, keyed with the server seed, over the message clientSeed:nonce:block. Block 0 gives the first 32 bytes, block 1 the next 32, and so on, as many as the round needs.
Bytes are read four at a time as an unsigned 32-bit number u from 0 to 2^32 − 1, most significant byte first. Divided by 2^32 it becomes a fraction from 0 to just under 1.
Games that must not repeat an outcome (mines, traps, keno balls) pick without replacement: draw i takes position floor(u × (N − i) / 2^32) among the items not taken yet, kept in ascending order.
Card games that need an exact whole number below n skip any draw at or above 2^32 − (2^32 mod n), so no card is more likely than another.
The code below is shared by every game on this page. Run it with Node.js 18 or newer.
const { createHmac } = require("node:crypto");
const TWO_32 = 2 ** 32;
// The round's bytes: HMAC-SHA256(key = serverSeed, message = "clientSeed:nonce:block")
// for block = 0, 1, 2, … — 32 bytes per block, read in order.
function* bytes(serverSeed, clientSeed, nonce) {
for (let block = 0; ; block++) {
yield* createHmac("sha256", serverSeed)
.update(clientSeed + ":" + nonce + ":" + block)
.digest();
}
}
// Draws: 4 bytes at a time, big-endian, as an unsigned 32-bit number u (0 … 2^32 − 1).
// u / 2^32 equals byte0/256 + byte1/256^2 + byte2/256^3 + byte3/256^4.
function* draws(serverSeed, clientSeed, nonce) {
const b = bytes(serverSeed, clientSeed, nonce);
while (true) {
let u = 0;
for (let i = 0; i < 4; i++) u = u * 256 + b.next().value;
yield u;
}
}
// k of N without repeats: draw i takes position floor(u × (N − i) / 2^32)
// among the items not taken yet, kept in ascending order.
function pick(serverSeed, clientSeed, nonce, N, k) {
const d = draws(serverSeed, clientSeed, nonce);
const left = Array.from({ length: N }, (_, i) => i);
const out = [];
for (let i = 0; i < k; i++) {
out.push(left.splice(Math.floor((d.next().value * (N - i)) / TWO_32), 1)[0]);
}
return out;
}
// A whole number below n with no modulo bias: draws at or above
// 2^32 − (2^32 mod n) are skipped.
function below(d, n) {
const limit = TWO_32 - (TWO_32 % n);
for (;;) {
const u = d.next().value;
if (u < limit) return u % n;
}
}Verify a bet
- In the game's Provably fair window, rotate the seed pair the bet was played on. Its server seed is revealed.
- Check that the SHA-256 hash of the revealed server seed equals the hash you were shown before betting.
- Take the client seed and the bet's nonce, and run the game's function below with the three inputs.
- The result equals the round you played. The Verifier tab in the game does the same calculation for you.
Games
Dice
How a round is made
One draw. The roll is floor(fraction × 10,000) / 100: one of 10,000 equally likely values from 0.00 to 99.99. Your bet compares the roll with your target; the lower edge of a winning zone wins and the upper edge does not.
Payout
The multiplier is 99 ÷ win chance (in %), so every bet returns 99%.
Code
function dice(serverSeed, clientSeed, nonce) {
const u = draws(serverSeed, clientSeed, nonce).next().value;
return Math.floor((u / TWO_32) * 10000) / 100; // 0.00 … 99.99
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
dice(serverSeed, clientSeed, nonce)
// Result: 95.77Limbo
How a round is made
One draw. The result is floor(1e8 ÷ (fraction × 1e8) × 0.99 × 100) ÷ 100, between 1.00× and 1,000,000×. Your bet wins when the result is at or above your target.
Payout
A target t is reached with probability 0.99 ÷ t, so every target returns 99%.
Code
function limbo(serverSeed, clientSeed, nonce) {
const f = draws(serverSeed, clientSeed, nonce).next().value / TWO_32;
const cents = Math.floor((1e8 / (f * 1e8)) * (1 - 0.01) * 100);
const capped = Number.isFinite(cents) ? cents : 1e8;
return Math.min(Math.max(capped, 100), 1e8) / 100; // 1.00× … 1,000,000×
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
limbo(serverSeed, clientSeed, nonce)
// Result: 1.03Mines
How a round is made
The mines are the first m picks without replacement from the board's tiles (25, 36, 49 or 64), numbered from 0, left to right and top to bottom. The whole board is fixed before your first click.
Payout
After k safe tiles with m mines on N tiles the multiplier is 0.99 × C(N, k) ÷ C(N − m, k), so every board and every cash-out returns 99%.
Code
// Tiles 0 … N − 1, left to right, top to bottom; the mines are the first m picks.
function mines(serverSeed, clientSeed, nonce, tiles, m) {
return pick(serverSeed, clientSeed, nonce, tiles, m);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
mines(serverSeed, clientSeed, nonce, 25, 3)
// Result: [23, 13, 19]Blackjack
How a round is made
Card k of the round is floor(u × 52 ÷ 2^32), always drawn from a full deck (an infinite shoe). Cards are taken in the order the table uses them: each seat's first card, the dealer's up card, each seat's second card, the dealer's hole card, then hits, doubles, split hands, burnt cards and the dealer's draws as play reaches them.
Payout
Blackjack pays 3:2 and the dealer stands on all 17s. With best play the main game returns 99.43%.
Code
// Card k of the round, each from a full deck (an infinite shoe).
function blackjack(serverSeed, clientSeed, nonce, count) {
const d = draws(serverSeed, clientSeed, nonce);
return Array.from({ length: count }, () => {
const i = Math.floor((d.next().value * 52) / TWO_32);
return "23456789TJQKA"[i >> 2] + "DHSC"[i & 3];
});
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
blackjack(serverSeed, clientSeed, nonce, 10)
// Result: [AH, 9D, QH, 3H, 9S, 8H, 7D, 5S, 3C, AS]Plinko
How a round is made
One draw per row. The ball goes left when the draw's top bit is 0 (u below 2^31) and right when it is 1. It lands in the bucket equal to the number of rights, from 0 to the number of rows.
Payout
Every rows and risk table is built so that Σ C(rows, k) × multiplier(k) = 0.99 × 2^rows, so each one returns exactly 99%.
Code
// One draw per row: 0 = left, 1 = right. The bucket is the number of rights.
function plinko(serverSeed, clientSeed, nonce, rows) {
const d = draws(serverSeed, clientSeed, nonce);
return Array.from({ length: rows }, () => (d.next().value >= 2 ** 31 ? 1 : 0));
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
plinko(serverSeed, clientSeed, nonce, 16)
// Result: [1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1]Keno
How a round is made
The ten balls are the first ten picks without replacement from 40 numbers (tiles 0 to 39, shown as 1 to 40).
Payout
Every paytable returns 99%, to within one millionth, at every risk level and every number of picks.
Code
// 10 balls from 40 numbers (tiles 0 … 39, shown as 1 … 40).
function keno(serverSeed, clientSeed, nonce) {
return pick(serverSeed, clientSeed, nonce, 40, 10).map((tile) => tile + 1);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
keno(serverSeed, clientSeed, nonce)
// Result: [39, 22, 32, 4, 24, 19, 15, 11, 6, 40]Roulette
How a round is made
One draw. The pocket is floor(u × 37 ÷ 2^32): a European wheel with one zero, pockets 0 to 36.
Payout
A bet covering n pockets wins with probability n ÷ 37 and pays 36 ÷ n times the stake, so every bet returns 36 ÷ 37 = 97.30%.
Code
// European wheel: 37 pockets, 0 … 36.
function roulette(serverSeed, clientSeed, nonce) {
const u = draws(serverSeed, clientSeed, nonce).next().value;
return Math.floor((u * 37) / TWO_32);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
roulette(serverSeed, clientSeed, nonce)
// Result: 35Coin Flip
How a round is made
Flip i is the top bit of draw i: 0 is heads, 1 is tails. A round of n flips reads draws 0 to n − 1; with several coins of F flips each, coin i reads draws i × F to i × F + F − 1.
Payout
A round that needs at least t of n calls right pays 0.99 × 2^n ÷ S(n, t), where S(n, t) is the number of ways to get t or more right, so every round returns at most 99%.
Code
// Flip i is the top bit of draw i: H = heads, T = tails.
function coinflip(serverSeed, clientSeed, nonce, flips) {
const d = draws(serverSeed, clientSeed, nonce);
let out = "";
for (let i = 0; i < flips; i++) out += d.next().value >= 2 ** 31 ? "T" : "H";
return out;
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
coinflip(serverSeed, clientSeed, nonce, 10)
// Result: TTTHTHHHHTBaccarat
How a round is made
A fresh 8-deck shoe of 416 cards is shuffled by Fisher–Yates from the top, using an unbiased draw for each swap, and dealt from position 0. Player takes positions 0 and 2, Banker 1 and 3, and any third cards follow in order, Player first. Standard drawing rules apply.
Payout
Every bet is priced on the fresh 8-deck shoe to return 99%.
Code
// A fresh 8-deck shoe (416 cards) shuffled by Fisher–Yates from the top;
// only the first 6 positions can ever be dealt.
function baccarat(serverSeed, clientSeed, nonce) {
const d = draws(serverSeed, clientSeed, nonce);
const shoe = Array.from({ length: 416 }, (_, k) => k % 52);
for (let k = 0; k < 6; k++) {
const j = k + below(d, 416 - k);
[shoe[k], shoe[j]] = [shoe[j], shoe[k]];
}
return shoe.slice(0, 6).map((id) => "A23456789TJQK"[id % 13] + "SHDC"[Math.floor(id / 13)]);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
baccarat(serverSeed, clientSeed, nonce)
// Result: [8S, AD, QS, 8D, TH, 5D]Hi-Lo
How a round is made
The first card is the one you see when you bet. Every card after it comes from the stream as an unbiased number below 52, each from a full deck (an infinite deck). Aces are lowest and kings highest.
Payout
After right guesses where n₁, n₂, … of the 13 ranks win, the multiplier is 0.99 × 13 ÷ n₁ × 13 ÷ n₂ × …, so the game returns 99% at every cash-out point.
Code
// Every card is independent (an infinite deck); A is lowest, K highest.
function hilo(serverSeed, clientSeed, nonce, count) {
const d = draws(serverSeed, clientSeed, nonce);
return Array.from({ length: count }, () => {
const id = below(d, 52);
return "A23456789TJQK"[id % 13] + "SHDC"[Math.floor(id / 13)];
});
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
hilo(serverSeed, clientSeed, nonce, 8)
// Result: [8S, 8D, 8D, 2D, 9D, 9C, 7S, 5C]Hubie's Road
How a round is made
The road has 25 slots. The traps are the first m picks without replacement (easy 1, medium 3, hard 5, daredevil 10), and lane L is deadly when slot L − 1 is a trap. Every trap is fixed before you start crossing.
Payout
After k safe lanes with m traps the multiplier is 0.99 × C(25, k) ÷ C(25 − m, k), so every difficulty and every stop returns 99%.
Code
// 25 slots; the traps are the first m picks (easy 1, medium 3, hard 5,
// daredevil 10). Lane L (1-based) is deadly when slot L − 1 is a trap.
function road(serverSeed, clientSeed, nonce, traps) {
return pick(serverSeed, clientSeed, nonce, 25, traps);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
road(serverSeed, clientSeed, nonce, 3)
// Result: [23, 13, 19]Wheel
How a round is made
One draw. The segment is floor(u × n ÷ 2^32) on a wheel of n segments (10, 20, 30, 40 or 50), numbered from 0 at the top, clockwise. Risk only changes what the segments pay, never where the wheel stops.
Payout
Every segment is equally likely and each wheel's multipliers add up to 0.99 × its number of segments, so every wheel and every risk returns 99%.
Code
// Segments 0 … n − 1 (10, 20, 30, 40 or 50), segment 0 at the top, clockwise.
// Risk does not enter: the same seeds stop on the same segment on every wheel of that size.
function wheel(serverSeed, clientSeed, nonce, segments) {
const u = draws(serverSeed, clientSeed, nonce).next().value;
return Math.floor((u * segments) / TWO_32);
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
wheel(serverSeed, clientSeed, nonce, 20)
// Result: 19Max Win
How a round is made
One Limbo roll, made exactly as in Limbo above. The spin wins when the roll reaches the mode's target: Diamond 5×, Gold 10×, Silver 100×, Standard 1,000×. The three reels are a picture of that one roll.
Payout
A target t is reached with probability 0.99 ÷ t and a win pays t, so every mode returns 99%.
Code
// One Limbo roll (the limbo function above) against the mode's target:
// Diamond 5×, Gold 10×, Silver 100×, Standard 1,000×. The reels show that one roll.
function maxwin(serverSeed, clientSeed, nonce, target) {
const f = draws(serverSeed, clientSeed, nonce).next().value / TWO_32;
const cents = Math.floor((1e8 / (f * 1e8)) * (1 - 0.01) * 100);
const roll = Math.min(Math.max(Number.isFinite(cents) ? cents : 1e8, 100), 1e8) / 100;
return [roll, roll >= target ? "win" : "loss"];
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
maxwin(serverSeed, clientSeed, nonce, 5)
// Result: [1.03, loss]Dragon Tower
How a round is made
The whole tower is fixed before your first pick: nine floors, bottom first, read from one stream. On a floor of C tiles (numbered from 0, left to right) the D traps come from a partial Fisher–Yates shuffle: for each i below D, tile i swaps with tile i + below(C − i), and the first D tiles are the traps. Easy has 4 tiles and 1 trap, medium 3 and 1, hard 2 and 1, expert 3 and 2, master 4 and 3.
Payout
After k cleared floors the multiplier is 0.99 × (C ÷ (C − D))^k, rounded down once, so every difficulty and every cash-out returns 99%.
Code
// 9 floors, bottom first, from one stream. Each floor of C tiles (0 = left)
// gets D traps by a partial Fisher–Yates shuffle: easy 4/1, medium 3/1,
// hard 2/1, expert 3/2, master 4/3. A floor's traps are joined with "+".
function tower(serverSeed, clientSeed, nonce, tiles, traps) {
const d = draws(serverSeed, clientSeed, nonce);
return Array.from({ length: 9 }, () => {
const a = Array.from({ length: tiles }, (_, i) => i);
for (let i = 0; i < traps; i++) {
const j = i + below(d, tiles - i);
[a[i], a[j]] = [a[j], a[i]];
}
return a.slice(0, traps).join("+");
});
}Worked example
Server seed d2b5b3f6b4a7d4e8c95c1a4f0e3b2a19c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3, client seed housebets, nonce 1.
tower(serverSeed, clientSeed, nonce, 3, 2)
// Result: [1+2, 0+2, 0+2, 0+2, 2+1, 2+1, 2+1, 0+2, 0+1]Crash
Crash is one flight for everyone in the room, so it is proven differently from the other games: there is no client seed or nonce. Every crash point comes from four public inputs, all fixed before betting closes.
A chain committed in advance
We generate a chain of 10 million seeds, each the SHA-256 hash of the next, and publish its last hash before the first round. Rounds use the seeds in reverse, so each round's seed hashes to the previous round's. The commitment is timestamped in Bitcoin, so it cannot be swapped later.
A Bitcoin block nobody could know
When we publish a chain, we name a Bitcoin block that is not mined yet. Its hash is mixed into every crash point of the chain. The chain was fixed before that block existed, so we could not have picked a chain that suits the house.
A public random number for every round
When betting closes, the round takes the next random number from drand, a public randomness network run by independent organisations. Nobody, including us, knows it in advance.
Every bet sealed first
Before the random number is drawn, every bet in the round is sealed into a Merkle root. The root is part of the result, so no bet can be added or changed afterwards.
The crash point
HMAC-SHA256 with the round's seed as the key, over the Bitcoin block hash, the room's root and the random number. The first 52 bits, as a fraction u between 0 and 1, give 0.99 ÷ (1 − u), rounded down to 0.01×, from 1.00× to 200×. The house edge is 1%.
Your receipt
Every bet gets a receipt signed by Housebets with Ed25519: your stake, your auto cash-out, the multiplier you cashed out at and your payout. The public key is published, so the receipt proves what you were paid.
Public records
Everything the checks use is public: the chains and their commitments, the Bitcoin proofs (.ots files you can also check with the OpenTimestamps client), every round's proof, the receipts key and the drand network.
https://games.housebets.com/hubie-crash/fairness/chains
https://games.housebets.com/hubie-crash/fairness/anchors
https://games.housebets.com/hubie-crash/fairness/anchors/chain/<id>.txt.ots
https://games.housebets.com/hubie-crash/fairness/rounds/<n>/proof
https://games.housebets.com/hubie-crash/fairness/key
https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/infoCheck a round
The Provably fair window in the game checks all of this in your browser for any round: the chain, the Bitcoin block, the random number, the room and your receipt. To check a round yourself, run this with Node 18 or later and the round number:
const { createHash, createHmac } = require("node:crypto");
const API = process.env.API || "https://games.housebets.com/hubie-crash/fairness";
const get = async (url, as = "json") => (await fetch(url))[as]();
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
async function verifyCrash(n) {
const p = await get(`${API}/rounds/${n}/proof`);
// 1. The seed is in the chain: it hashes to the previous round's seed
// (or, for a chain's first round, to the chain's published commitment).
const chain = sha256(p.seed) === p.link;
// 2. The salt is the hash of Bitcoin block saltHeight, as anyone can look up.
const block = (await get(`https://mempool.space/api/block-height/${p.saltHeight}`, "text")).trim();
const salt = block === p.saltHash;
// 3. The public random number, fetched from drand itself.
const beacon = await get(`https://api.drand.sh/${p.beaconChain}/public/${p.beaconRound}`);
const drand = beacon.randomness === p.randomness &&
sha256(Buffer.from(beacon.signature, "hex")) === beacon.randomness;
// 4. The crash point: HMAC-SHA256(seed, salt + room + randomness), first 52 bits.
const h = createHmac("sha256", Buffer.from(p.seed, "hex"))
.update(Buffer.concat([p.saltHash, p.roomRoot, beacon.randomness].map((x) => Buffer.from(x, "hex"))))
.digest();
const r = BigInt("0x" + h.subarray(0, 7).toString("hex")) >> 4n;
const two52 = 1n << 52n;
const hundredths = (9900n * two52) / (two52 - r) / 100n;
const crash = Math.max(100, Math.min(20000, Number(hundredths))) / 100;
return { round: p.n, chain, salt, drand, crash, published: p.crashBp / 10000 };
}
verifyCrash(process.argv[2]).then(console.log);