Skip to content
This is a test announcement!

Provably fair

Verify a round

Direction
Coin

= 1.000000 KC

Result

Roll—
Multiplier
—
Win chance
—
Payout
—
—

Derivation

  1. SHA-256(server seed bytes)—
  2. SHA-256(server seed hex text)—
  3. Message—
  4. HMAC-SHA256(server seed, message)—
  5. Bytes 0–3: — = ——
  6. Payout—

The rule, exactly

dice v1
  1. 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.

  2. Message: the client seed, a colon, then the nonce in decimal with no leading zeros, encoded as UTF-8 — for example Xq3vL9mTz2RkPw7a:1.

  3. HMAC-SHA256, keyed with the 32 server-seed bytes, over the message. The result is 32 bytes.

  4. 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.

  5. If all eight are skipped — a chance of about 6.9 × 10⁻⁴⁷ — the server refuses the bet and records nothing.

  6. Roll = that number mod 10,000, divided by 100: one of 0.00, 0.01, … 99.99.

  7. 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.

  8. 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.

  9. Win chance, in percent: 99.99 − target for roll over, the target for roll under.

  10. 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×.

  11. 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.