Last updated:
RaseBox uses a Provably fair model for box openings, free boxes, battles and upgrades. Each roll is generated deterministically with HMAC-SHA256 from a secret server seed, a client seed, a nonce and a context that identifies the game. Before you play, we store the hash of every server seed and give it to you as a commitment: for box openings, free boxes and upgrades it is sent to your browser, which saves it, and for battles it is published in the battle manifest. Box openings, free boxes and upgrades use a new single-use server seed and new client entropy from your browser for every play, so the nonce is always 0: it is not needed to tell plays apart. In battles, the nonce is the round index, starting at 0 in the first round. Box-opening, free-box and upgrade records created before pre-play commitments were introduced in September 2026 show the per-account play counter that was used as the nonce at the time; older battles are described below.
Boxes: every item in a box has a weighted chance. The generated roll is mapped against that weighted table to select the winning item. The order in which items appear on the reel is shuffled with separate server randomness each time the page loads, kept for the following spins on that page and recorded with each opening for replay. It is not derived from any seed and does not affect the roll or the weighted table, which keeps its saved order for verification.
Battles: when a battle is created, a separate server seed is generated for every round and slot, plus one for the jackpot in jackpot modes, and their hashes are published in the battle manifest. Each human player commits fresh random entropy from their browser, the participant list is frozen, and those contributions are then revealed and combined into one battle client seed shared by every slot; bots add no entropy of their own. Every opening is generated and stored before playback. Each roll is bound to the battle ID, round, slot and box, and its nonce is the round index (0 in the first round). The roll selects an item from the same weighted table used by a regular box opening. The reel only presents the stored result; it does not decide it. At battle start, every participant, bots included, also receives an independent random reel order for each round. It is recorded in the battle’s animation plan, sealed by that plan’s own hash, and used for live playback and replays without affecting any roll. Battles created before pre-play commitments used each player’s own client seed with their account play counter plus the round index as the nonce; bots used the hash of a separately generated seed with the round index alone as the nonce.
Battle winners are derived from those verifiable item values: Normal and Crazy compare the highest and lowest total respectively; Terminal and Degen compare only the highest and lowest final-round value, combined for teams; Group shares the result evenly. Jackpot and Crazy Jackpot add a separate committed HMAC draw with its own committed server seed, the same battle client seed and a nonce of 0, weighted proportionally to each player or team’s total for Jackpot and inversely to that total for Crazy Jackpot.
Upgrades: chance is calculated from your contribution and the target value. If the generated roll is below that chance threshold, the upgrade wins; otherwise it loses.
During a battle, the server seed hashes are visible while the seeds remain hidden. After completion, the /api/battles/{battleId}/verify endpoint reveals the seeds, nonces, contexts, rolls and frozen item tables needed to independently recompute every opening, aggregate, winner, jackpot draw and payout.
Each paid or free opening uses its own server seed. The browser saves its hash before generating fresh client entropy. Verification compares the revealed result with that earlier receipt; older openings without a receipt cannot prove a prior commitment.
1import crypto from 'node:crypto'
2
3// Protocol v1: paid cases AND free reward cases.
4// 1. On page load: POST /api/fairness/commitments with
5// { purpose: 'box' or 'free-box', batchId: a fresh identifier }.
6// The response contains 4 independent SHA-256 commitments, not seeds.
7// Each expires after 15 minutes. Save them in the browser before play.
8// 2. Only AFTER saving a hash, the browser samples a fresh 32-byte
9// clientSeed with crypto.getRandomValues. One commitment = one draw.
10// 3. POST the opening with fairness: { commitmentId, serverSeedHash,
11// clientSeed }. Keep the whole request + idempotency key for retries.
12// 4. The server consumes that seed in the SAME transaction as the game.
13// It cannot consume the same commitment again. No new seed is picked
14// while resolving the opening. nonce is fixed at 0 for this protocol.
15// 5. After the result, verify the revealed seed against your saved hash.
16// The next draw uses another seed; prefetch avoids an extra click delay.
17
18const TOTAL = 1_000_000n
19const BOUND = 2n ** 64n - (2n ** 64n % TOTAL)
20const sha256 = value => crypto.createHash('sha256').update(value).digest('hex')
21
22function deriveRoll({ serverSeed, clientSeed, nonce, context }) {
23 for (let counter = 0; counter < 2048; counter++) {
24 const digest = crypto.createHmac('sha256', serverSeed)
25 .update(`${clientSeed}:${nonce}:${context}:${counter}`).digest()
26 const random64 = digest.readBigUInt64BE(0)
27 // Reject the incomplete final interval to avoid modulo bias.
28 if (random64 < BOUND) return Number(random64 % TOTAL)
29 }
30 throw new Error('Unable to derive an unbiased roll')
31}
32
33function verifyCase({ savedReceipt, opening, selectionTable }) {
34 // savedReceipt is the browser's earlier record, not a hash first
35 // obtained alongside this result. Historical openings lack this proof.
36 if (!savedReceipt || !opening.commitment) return false
37 if (opening.commitment.id !== savedReceipt.id ||
38 opening.serverSeedHash !== savedReceipt.serverSeedHash ||
39 sha256(opening.serverSeed) !== savedReceipt.serverSeedHash ||
40 opening.clientSeed !== savedReceipt.clientSeed ||
41 Number(opening.nonce) !== 0) return false
42
43 const roll = deriveRoll({ ...opening, nonce: 0, context: opening.boxId })
44 if (roll !== Number(opening.rollUnits)) return false
45 // Walk the canonical selectionTable returned by the verify endpoint, in
46 // its saved order. Never walk animationPlan.items: that is the reel's
47 // display order, shuffled per page load and recorded only for replay.
48 let cumulative = 0
49 for (const entry of selectionTable) {
50 cumulative += Number(entry.chanceUnits)
51 if (roll < cumulative) return entry.itemId === opening.selectedItemId
52 }
53 return false
54}