Why this matters for collections
Most generative collections pre-render every token off-chain and only reveal after mint. That's fine if collectors trust the team. Provable.io lets you derive traits from a committed seed instead — anyone can recompute a token's traits from public inputs.
Define the trait categories
const TRAITS = {
background: [
{ name: "Sky", weight: 50 },
{ name: "Sunset", weight: 30 },
{ name: "Void", weight: 15 },
{ name: "Aurora", weight: 5 },
],
body: [
{ name: "Robot", weight: 60 },
{ name: "Ape", weight: 30 },
{ name: "Alien", weight: 10 },
],
hat: [
{ name: "None", weight: 50 },
{ name: "Cap", weight: 30 },
{ name: "Crown", weight: 15 },
{ name: "Halo", weight: 5 },
],
};
One call per token, all traits at once
Each trait category needs one random integer. Compute the LCM (or just use a wide range and modulo per category) and batch them in a single request.
async function rollTraits(tokenId, collectionSeed) {
const categories = Object.entries(TRAITS);
const clientSeed = `${collectionSeed}:token:${tokenId}`;
const url = new URL("https://api.provable.io/api/ints");
url.searchParams.set("clientSeed", clientSeed);
url.searchParams.set("count", String(categories.length));
url.searchParams.set("min", "1");
url.searchParams.set("max", "1000000");
const res = await fetch(url, {
headers: { "x-api-key": process.env.PROVABLE_KEY }
});
const { outcome, serverHash } = await res.json();
const traits = {};
categories.forEach(([category, options], i) => {
const total = options.reduce((s, o) => s + o.weight, 0);
const roll = (outcome[i] % total) + 1;
let c = 0;
for (const o of options) { c += o.weight; if (roll <= c) { traits[category] = o.name; break; } }
});
return { tokenId, traits, clientSeed, serverHash };
}
console.log(await rollTraits(1337, "myproject_v1_mainnet"));
// { tokenId: 1337, traits: { background: "Sunset", body: "Ape", hat: "Crown" }, ... }
The reveal flow
- Pre-mint: publish the
collectionSeedprefix and the committedserverHashon-chain or in a signed announcement. - Mint: users mint token IDs. No traits assigned yet.
- Reveal: run
rollTraits(tokenId, collectionSeed)for each token, render the art, store the metadata. - Audit: any holder can re-run the same call. If the result differs, the team is caught.
Tips
- Lock the trait tables. Publish them with the seed commitment. Changing them later breaks verification.
- Combinatorial conflicts. If two traits can't coexist (e.g. "Hat: Crown" + "Body: Alien"), re-roll the specific category with an extended seed like
:retry:1— keep it deterministic. - 1/1s and specials. Reserve specific token IDs ahead of time and skip the trait roll for them. Document the reservations alongside the seed.
Next steps
- Weighted loot tables (same technique, different use case)
- How provably fair works