Lootbox Provably Fair Draws
How lootbox rewards are decided by a commit–reveal scheme combined with blockchain randomness — and how to re-run the math yourself to verify any draw.
Jul 3
Overview
Every lootbox uses a commit–reveal scheme combined with blockchain randomness to guarantee that:
- We can't pick your reward — the server commits to its secret seed before the randomness exists
- You can't predict your reward — the draw depends on a future blockchain block
- Anyone can verify the result — after the box resolves, all inputs are revealed and the draw can be recomputed in your browser
The Core Math
Each box's reward is decided by a draw seed:
drawSeed = SHA256(serverSeed || randaoValue || lootboxId);| Component | Description |
|---|---|
serverSeed | A secret 256-bit value generated by our backend when the box is created — hidden until the box resolves |
randaoValue | Randomness from the blockchain (e.g. Ethereum RANDAO) at the box's
|
lootboxId | The box's unique id (UTF-8), so two boxes sealed against the same block still get independent draws |
The seeds are concatenated as raw bytes (serverSeed and randaoValue are hex-decoded, lootboxId is UTF-8-encoded) and hashed with SHA-256.
The Commitment
When you buy a box, before any randomness exists, the server publishes:
| Field | Description |
|---|---|
serverSeedHash |
|
revealBlockNumber | The future block whose randomness will be used |
randomnessSource | The chain the randomness comes from |
items (with weights) | The full drop table the draw will run against |
This commitment is what makes cheating impossible:
- We can't change the seed after seeing the randomness —
serverSeedHashlocks in our choice before blockrevealBlockNumberexists - We can't grind for a favorable seed — the seed is a 256-bit random value committed up front; picking a new one after seeing the RANDAO would break the hash
- You can't snipe a good block — the seed you'd need to predict the outcome stays secret until after the block is finalized
The Weighted Draw
The draw seed picks one item from the box's drop table, proportionally to each item's weight:
function weightedDraw(items: { weight: number }[], drawSeed: string): number {
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
// First 8 bytes of the draw seed, as an integer, modulo the total weight
const pick = BigInt("0x" + drawSeed.slice(0, 16)) % BigInt(totalWeight);
let running = 0n;
for (let i = 0; i < items.length; i++) {
running += BigInt(items[i].weight);
if (pick < running) return i;
}
return items.length - 1;
}An item with weight 700 out of a total of 1000 wins 70% of the time — exactly the odds shown in the drop table before you open.
What You'll See in the UI
Before the Box Resolves (Seed Hidden)
| Field | Description |
|---|---|
serverSeedHash | Our commitment to the secret seed |
revealBlockNumber | The block whose randomness decides the draw |
randomnessSource | Where that block lives (with an explorer link) |
After the Box Resolves (Full Transparency)
| Field | Description |
|---|---|
serverSeed | The secret seed (now revealed) |
randaoValue | The randomness taken from the reveal block |
drawnItemIndex | The item the server says you won |
How to Verify Fairness
In-App (One Click)
Open Lootbox History, pick any opened box, and press Verify draw. Your browser re-runs the full SHA-256 + weighted-draw math locally and checks both that the commit matches the reveal and that the drawn item follows from the seed. Nothing is trusted from the server except the revealed inputs.
Step 1: Verify the Randomness is Real
Check that the blockchain randomness wasn't fabricated:
- Open the block explorer link next to the commit (or find block
revealBlockNumberyourself) - Confirm the block's
prevRandaomatches the revealedrandaoValue
Step 2: Verify We Didn't Change the Seed
Confirm the revealed seed matches the hash we committed to at purchase:
const crypto = require("crypto");
const seedHash = crypto
.createHash("sha256")
.update(Buffer.from(serverSeed, "hex"))
.digest("hex");
console.log(seedHash === serverSeedHash); // must be trueStep 3: Recompute the Draw Seed
const crypto = require("crypto");
function computeDrawSeed(
serverSeed: string,
randaoValue: string,
lootboxId: string,
): string {
const combined = Buffer.concat([
Buffer.from(serverSeed, "hex"),
Buffer.from(randaoValue.replace(/^0x/, ""), "hex"),
Buffer.from(lootboxId, "utf8"),
]);
return crypto.createHash("sha256").update(combined).digest("hex");
}
const drawSeed = computeDrawSeed(serverSeed, randaoValue, lootboxId);Step 4: Recompute the Winning Item
Run weightedDraw(items, drawSeed) (code above) against the box's item list, in the exact order the box lists them. The returned index must equal drawnItemIndex — the item you were awarded.
Summary
| Phase | What's Public | What's Hidden |
|---|---|---|
| At purchase |
| serverSeed |
| While resolving | The finalized block's randaoValue (on-chain) | serverSeed |
| After resolve | Everything | Nothing |
The combination of hash commitment + future blockchain randomness + client-side recomputation means you never have to trust the reward you were dealt — you can verify it.