Loot boxes with disclosed odds that stay compliant
3 min read
Loot boxes are two problems wearing one trench coat. The first is trust: if the client rolls the dice, players are right not to believe your drop rates, and a determined one can just reroll until they win. The second is compliance: platforms and a growing pile of regulations require you to disclose the odds and gate loot behind an adult age check. Get either wrong and you're not shipping. You're delisted.
Triggair handles both. The server owns the roll (verifiably, and idempotently), and the SDK gives you the disclosure and age-gate primitives you need to stay in the stores.
Always show the odds before anyone opens anything. They're public by design, the same numbers the server rolls against.
const odds = await tg.economy.loot.odds(crateKey);
// [{ target, probability }]
odds.forEach(o => console.log(`${o.target}: ${(o.probability * 100).toFixed(1)}%`));
Render that in your crate UI. It's not just good manners; disclosed odds are a hard requirement on the major platforms, and pulling them from the same source the server uses means they can never silently disagree.
Opening is a single call. The server rolls, verifies, and grants the reward atomically, and the client never sees a dice throw it could tamper with.
const res = await tg.economy.loot.open(crateKey);
console.log('you got', res.result.amount, 'x', res.result.target);
Because open() is idempotent, a double-tapped button or a retried request is one roll, not two. The player can't fish for a better outcome by spamming, and an honest player never gets charged for a crate they only meant to open once.
Now the age gate. Loot boxes are 18+, so the first open() for a player who hasn't confirmed their age will throw. Catch that specific error, record the bracket, and retry.
async function openCrate(crateKey) {
try {
return await tg.economy.loot.open(crateKey);
} catch (err) {
if (err.code === 'age_restricted') {
await tg.compliance.setAge({ bracket: 'adult' });
return await tg.economy.loot.open(crateKey); // retry once
}
throw err;
}
}
Surface a real age-confirmation UI before calling setAge. You're asserting the player is an adult, so ask them. Once the bracket is set it sticks for that player, so this dance happens at most once.
Gotchas
- Odds and open read from the same server config. Don't hardcode drop rates in your UI copy; fetch
odds()so your displayed numbers can never drift from what actually rolls. age_restrictedis the only retry-after-fix case here. Any othercodeis a real failure, so let it propagate. Each error carries amessagefor players and anagent_hintfor AI copilots.- Idempotency is not a refund button. A
replay-style repeat returns the original result; it doesn't undo or re-roll. Design your "open" button around one intentional roll.
Disclosed odds keep players trusting you and platforms approving you; a server-side verifiable roll keeps the outcome honest; the age gate keeps you listed. Wire those three together and loot boxes go from a legal liability to a feature you can ship this afternoon.