What it is
A weighted loot drop endpoint built on /api/pick. You hand it the items and their weights — common: 70, rare: 25, epic: 4, legendary: 1 — and it returns one pull plus a serverHash a player can paste into /verify to confirm the published odds were actually applied.
A pull and its proof
This is what anyone can check. The three values in the steps are all a verifier needs, and the replay button recomputes the pull right here, without calling Provable.
{
"clientSeed": "player_42_crate_2026_05_25",
"nonce": 0,
"serverHash": "8b9046884f4d34e634c7cce0e8d8d7ceb8869ec7bc2779ac37e0398661404afd",
"outcome": "uncommon",
"index": 1
}- Before the pull, the server commits to a seedPublish this hash before the pull, so the seed can't be swapped afterwards.
serverHash = sha256(serverSeed) = 8b9046884f4d34e634c7cce0e8d8d7ceb8869ec7bc2779ac37e0398661404afd - The pull is a pure function of both seeds
HMAC-SHA256(serverSeed, "player_42_crate_2026_05_25:0:0") → u × 100 lands in weight bucket 1 = uncommon - After the pull, the seed is revealed and anyone can replay it
serverSeed = 40bff36b5c60686305fbc0f550daeae9c01ce20678c9a08451cea1fce2f82ffd
Try it live — a single pull from a weighted drop table
curl "https://api.provable.io/api/pick?clientSeed=player_42_crate_2026_05_25&items=common,uncommon,rare,epic,legendary&weights=70,20,7,2.5,0.5"
A 10-pull from a banner (issue 10 sequential pulls with a per-pull nonce). The first call is shown; bumping the nonce gives pull #2, #3, etc.:
curl "https://api.provable.io/api/pick?clientSeed=banner_summer_2026_pull_1&items=3star,4star,5star&weights=94.3,5.1,0.6"
Integration snippet
// 1. Publish the loot table somewhere immutable (a blog post,
// a /rates page on your site, the patch notes).
const TABLE = [
{ id: "common", weight: 70.0 },
{ id: "uncommon", weight: 20.0 },
{ id: "rare", weight: 7.0 },
{ id: "epic", weight: 2.5 },
{ id: "legendary", weight: 0.5 },
];
async function openCrate(playerId, crateId) {
const url = new URL("https://api.provable.io/api/pick");
// One pull per (playerId, crateId) — same seed never re-pulls.
url.searchParams.set("clientSeed", `crate_${playerId}_${crateId}`);
url.searchParams.set("items", TABLE.map((t) => t.id).join(","));
url.searchParams.set("weights", TABLE.map((t) => t.weight).join(","));
const res = await fetch(url, {
headers: { "x-api-key": process.env.PROVABLE_KEY }
});
const { outcome, serverHash, shortId } = await res.json();
return { tier: outcome, serverHash, permalink: `/o/${shortId}` };
}
Why this is fair
- Published weights, published seed. The drop table is public and the per-pull
serverHashgoes in the player's inventory record. The pull becomes a reproducible calculation, not a marketing claim. - Cannot be shadow-nerfed. Changing the live weights changes the public table — which any data-mining player will notice instantly. The fairness contract is on the published page, not in the server's heart.
- Pity mechanics still work. Pity floors are just a server-side overlay on the raw pull — apply your guarantee after looking at the verifiable outcome, and document the pity rule.
Common patterns
- Item-level rarity. First pick the rarity (weighted), then a uniform pick inside that rarity bucket. Two calls, both verifiable.
- Banner-specific items. Use a banner-prefixed
clientSeed(banner_2026_summer_pull_N) so banner pulls don't share state with the standard pool. - Soft / hard pity. Apply the pity rule server-side after reading the raw outcome. Publish the rule, publish the threshold counter, players can still verify each underlying pull.
- Drop-rate disclosure pages. Pair the loot box with a
/ratesroute that lists current weights and links to the open-source provable-core library.
Where it fits
- Free-to-play mobile games with gacha banners.
- Crate / skin marketplaces for shooters and MOBAs.
- NFT mint reveals with rarity tiers.
- Trading card games opening digital booster packs.