Provably fair
Verify a round
= 1.000000 KC
Result
- Multiplier
- —
- Win chance
- —
- Payout
- —
- —
Derivation
- SHA-256(server seed bytes)—
- SHA-256(server seed hex text)—
- Message—
- HMAC-SHA256(server seed, message)—
- Bytes 0–3: — = ——
- Payout—
The rule, exactly
Server seed: 32 random bytes, shown as 64 hex characters. The published hash is SHA-256 of those 32 bytes, not of their hex text.
Message: the client seed, a colon, then the nonce in decimal with no leading zeros, encoded as UTF-8 — for example Xq3vL9mTz2RkPw7a:1.
HMAC-SHA256, keyed with the 32 server-seed bytes, over the message. The result is 32 bytes.
Read those 32 bytes as eight 4-byte big-endian numbers. Use the first one below 4,294,960,000, the largest multiple of 10,000 under 2³². A number at or above it is skipped and the next four bytes are read. This keeps all 10,000 rolls exactly equally likely.
If all eight are skipped — a chance of about 6.9 × 10⁻⁴⁷ — the server refuses the bet and records nothing.
Roll = that number mod 10,000, divided by 100: one of 0.00, 0.01, … 99.99.
Target: a multiple of 0.01 — roll over from 0.01 to 99.98, roll under from 0.02 to 99.99. A new bet’s win chance is at most 98.00%: roll over from 1.99, roll under up to 98.00.
Roll over wins only when the roll is greater than the target, roll under only when it is smaller. A roll equal to the target loses.
Win chance, in percent: 99.99 − target for roll over, the target for roll under.
Multiplier: 95 ÷ win chance — a 5% house edge — floored to six decimals. It is computed in standard double-precision arithmetic, exactly as the server does: at 2 of the 19,996 possible target and direction pairs this floors one millionth lower than exact arithmetic would, so roll over 98.71 pays 74.218749×.
Payout: wager × multiplier, floored to a whole base unit — a millionth of a KC. A loss pays 0. Nothing is converted to another currency.
Reference implementation
// KOCase dice v1 — reference verifier. WebCrypto only: Bun, Deno, Node 20+ or a browser.export async function verifyDiceV1( serverSeedHex: string, // the revealed server seed: 64 lowercase hex characters hashScheme: 'bytes' | 'hex', // what the seed's published hash was taken over clientSeed: string, nonce: bigint, // the first bet on a seed is nonce 1 target: number, // a multiple of 0.01 direction: 'over' | 'under', wager: bigint, // in base units: 10^-6 KC) { if (!/^[0-9a-f]{64}$/.test(serverSeedHex)) throw new Error('the server seed must be 64 lowercase hex characters'); if (hashScheme !== 'bytes' && hashScheme !== 'hex') throw new Error('the hash scheme is bytes or hex'); const { subtle } = globalThis.crypto; const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, '0')).join(''); const seed = new Uint8Array(serverSeedHex.match(/../g)!.map((h) => parseInt(h, 16))); const committed = hashScheme === 'hex' ? new TextEncoder().encode(serverSeedHex) : seed; // 'hex': the 64-character text; 'bytes': the 32 bytes it spells const serverSeedHash = hex(await subtle.digest('SHA-256', committed)); const key = await subtle.importKey('raw', seed, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const message = new TextEncoder().encode(`${clientSeed}:${nonce}`); const mac = new DataView(await subtle.sign('HMAC', key, message)); let roll: number | undefined; for (let offset = 0; offset < 32 && roll === undefined; offset += 4) { const value = mac.getUint32(offset); // big-endian if (value < 4_294_960_000) roll = (value % 10_000) / 100; // floor(2^32 / 10000) * 10000 } if (roll === undefined) throw new Error('all eight windows rejected: the server refuses this bet'); const chance = direction === 'over' ? 99.99 - target : target; // percent, in doubles as the server does const scaled = BigInt(Math.floor((95 / chance) * 1_000_000)); // multiplier, floored to 6 decimals const win = direction === 'over' ? roll > target : roll < target; // a roll equal to the target loses const payout = win ? (wager * scaled) / 1_000_000n : 0n; // bigint division floors const multiplier = `${scaled / 1_000_000n}.${(scaled % 1_000_000n).toString().padStart(6, '0')}`; return { serverSeedHash, roll, win, multiplier, payout };}What this proves, and what it does not yet
It proves that a revealed seed matches its published hash, and that the seed, client seed and nonce produce exactly the roll, result and payout shown.
The hash has to be one you saw before betting. A hash first read afterwards commits to nothing about that bet.
Your first client seed is drawn in your browser and activates a server seed committed when you signed up. On accounts opened before that, the server chose the first seed’s client seed, and it stays until you rotate; for that seed a check shows only that the rolls follow from the values shown, not that the server could not pick both seeds together.
It checks one round at a time. A revealed seed’s full round list is under Provably Fair in the game, where the listed nonces are checked against the seed’s bet count for gaps and repeats.