The setup
A loot table maps outcomes to weights. A common pattern:
const LOOT_TABLE = [
{ item: "Common Sword", weight: 600 }, // 60%
{ item: "Uncommon Shield", weight: 300 }, // 30%
{ item: "Rare Potion", weight: 90 }, // 9%
{ item: "Epic Helmet", weight: 9 }, // 0.9%
{ item: "Legendary Artifact", weight: 1 }, // 0.1%
];
Weights don't have to sum to 100 or 1000 — they're relative.
The algorithm
- Sum all weights →
total. - Pull one random integer in
[1, total]from the API. - Walk the table accumulating weights until you pass the random value.
async function pullLoot(clientSeed, table) {
const total = table.reduce((s, e) => s + e.weight, 0);
const url = new URL("https://api.provable.io/api/ints");
url.searchParams.set("clientSeed", clientSeed);
url.searchParams.set("count", "1");
url.searchParams.set("min", "1");
url.searchParams.set("max", String(total));
const res = await fetch(url, {
headers: { "x-api-key": process.env.PROVABLE_KEY }
});
const { outcome, serverHash } = await res.json();
const roll = outcome[0];
let cumulative = 0;
for (const entry of table) {
cumulative += entry.weight;
if (roll <= cumulative) {
return { item: entry.item, roll, total, serverHash };
}
}
}
console.log(await pullLoot("user_42_pull_001", LOOT_TABLE));
Pulling 10 at once
For a "10x pull" box, request count=10 and walk the table for each roll:
async function pullTen(clientSeed, table) {
const total = table.reduce((s, e) => s + e.weight, 0);
const url = `https://api.provable.io/api/ints?clientSeed=${clientSeed}&count=10&min=1&max=${total}`;
const { outcome } = await fetch(url).then(r => r.json());
return outcome.map(roll => {
let c = 0;
for (const e of table) { c += e.weight; if (roll <= c) return e.item; }
});
}
Why this is provably fair
The split is deterministic — the same roll always maps to the same item. The roll itself comes from a committed serverHash. Publish the table, the seed, and the hash, and a player who pulls a Common can confirm "the API really did roll 437 out of 1000, and 437 falls in the Common Sword band".
Production tips
- Versioned tables. Pin the loot table to a version, log it with the roll, and never silently mutate weights — that breaks reproducibility.
- Pity timers. Add a counter alongside the API call (e.g. force an Epic if the user hits 50 pulls without one). The roll stays fair; the pity logic is a deterministic transform on top.
- Per-user seeds. Use the user ID or session as the
clientSeedso pulls don't collide across users.