← all posts
fairness · · 7 min read

Anatomy of a provably-fair roll

Every spin on mumu commits to a server seed BEFORE you click. Here is the full lifecycle of a roll, from commit to reveal, with the math, the timing, and a worked example you can run in a browser tab.

Every roll on mumu commits before you click. That sounds like marketing, but it has a precise meaning: the moment a round opens, the server publishes a hash of the seed it will use to decide the outcome. You can save that hash, play out the round, and verify after the fact that the seed it reveals matches. No re-rolls. No stalling for "just one more block".

This post walks the full lifecycle of a single roll, from the pre-commit hash to the post-reveal verification, with code you can paste into a console, the math behind the modulo, and a worked example using one of last week's actual roulette rounds.

1. The commit

When a round opens the server generates a random serverSeed and immediately publishes its SHA-512 digest. The seed itself never leaves the database until the round settles. That ordering is the whole game:

  1. Server picks a 32-byte random serverSeed.
  2. Server publishes commitHash = sha512(serverSeed).
  3. Players see the commit hash before placing any bets.
  4. After the round closes, the server reveals serverSeed.
  5. Anyone can verify sha512(serverSeed) === commitHash.

If the server tried to cherry-pick a friendlier seed mid-round, the revealed seed wouldn't hash back to the published commit and the cheat would be visible to every player who saved the commit hash. That's the whole load-bearing primitive.

1.1 What "before you click" actually means

A round is committed at the moment the server inserts the row. Browsers receive the commit hash via WebSocket within a few milliseconds, but the commit is final the second the database row lands, not the second your browser receives it. Even if your network stalls, the seed for that round is already locked.

Commitment is a property of the database, not the UI. The hash you see in DevTools is a projection. The source of truth is the row and its append-only commit log.

internal fairness doc, march 2026

2. The reveal

After the round closes the server emits a RouletteRoundSettled event containing the original serverSeed alongside the per-player clientSeed values (also generated client-side, also pre-committed). The roll is derived from a digest of both:

async function deriveRoll(serverSeed: string, clientSeed: string): Promise<number> {
  const buf = await crypto.subtle.digest(
    'SHA-512',
    new TextEncoder().encode(`${serverSeed}:${clientSeed}`),
  );
  const hex = Array.from(new Uint8Array(buf))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
  // 1-indexed roll in [1, rangeMax]
  return Number(BigInt(`0x${hex}`) % BigInt(rangeMax)) + 1;
}

The digest is read as a big-endian unsigned integer, then mapped to the range [1, rangeMax] via modulo. Modulo bias is negligible at the range we use (rangeMax is in the millions; the digest is 512 bits ≈ 1.3×10154).

2.1 What rangeMax actually is

Each game picks a rangeMax that matches the weight resolution of its drop table. Common values:

GamerangeMaxWhy
Box opening1,000,000Drop weights stored as parts-per-million
Roulette1515 wheel positions (8 pink, 6 lavender, 1 gold)
Coinflip2Heads or tails
Upgrader10,000,000Fine-grained probability slices for the wheel

3. Worked example: roulette round #499,474,437

Here's a real round from last Tuesday. Open DevTools, paste the snippet, hit Enter:

// Round commitments (these were published live at 14:23:01 UTC):
const serverSeed = '7c5e2f...c8d4a1';  // 64 hex chars
const commitHash = '8a3f9b...e1c20d';  // 128 hex chars (sha512)

// Sanity check:
const verify = await crypto.subtle.digest('SHA-512',
  new TextEncoder().encode(serverSeed));
const verifyHex = Array.from(new Uint8Array(verify))
  .map(b => b.toString(16).padStart(2, '0')).join('');

console.log(verifyHex === commitHash); // true

If you see true, the server didn't lie. If false, screenshot it and email [email protected] because something is wrong and we owe you a bounty.


What this doesn't fix

Provably-fair commits prove the seed wasn't tampered with. They do not prove:

  • that your bet sizing makes mathematical sense (it usually doesn't);
  • that the house edge is reasonable (it's published in each game's footer);
  • that you should be gambling at all (you probably shouldn't);
  • that the cow loves you (she does, but not in a useful way).

If you want to dig deeper, the provably fair page has the per-game range maxes, the SHA-512 commit format, and links to every player's last 100 rolls with proof hashes. Have fun. Don't tilt.

mumu engineering