Add a global leaderboard in five minutes
2 min read
Every game wants a leaderboard, and every leaderboard is the same annoying project: design a scores table, write a submit endpoint, add auth so nobody spoofs a billion points, index it for fast top-N reads, then build a cron job so the "daily" board actually resets at midnight. That's a weekend of plumbing before a single player sees a rank.
Triggair gives you the whole thing behind two methods. No schema, no server, no cron.
Submit a score
After tg.login(), submitting is one call. The board name is just a string, and the first score you send creates it.
const result = await tg.leaderboards.submit('high_score', 4200);
// { ok: true, best_score: 4200, period_key: '2026-07-09' }
// Timed games? Pass elapsedMs as a tiebreaker.
await tg.leaderboards.submit('speedrun', level, { elapsedMs: 18240 });
submit keeps the player's best, so a worse score leaves their rank alone. The returned best_score and period_key tell you what's on record and which period bucket it landed in.
Read the top N
top returns ranked entries with everything you need to render a row, so there's no second lookup to resolve names.
const { entries } = await tg.leaderboards.top('high_score', { limit: 10 });
for (const e of entries) {
console.log(`#${e.rank} ${e.display_name} ${e.score}`);
}
Each entry is { rank, player_id, display_name, handle, score }. Wrap the call and degrade gracefully. If the network hiccups, the typed error carries a code and an agentHint, so you (or your agent) know exactly what to do.
Global vs. daily vs. weekly
This is the part that usually costs you a cron job. The same board reads across periods: pass a periodKey to fetch a specific day or week, or omit it for the current one.
// This week's board
const weekly = await tg.leaderboards.top('high_score', {
limit: 25,
periodKey: '2026-W28',
});
// A specific past day, straight from submit's return value
const { period_key } = await tg.leaderboards.submit('high_score', score);
const today = await tg.leaderboards.top('high_score', { periodKey: period_key });
The daily board rolls over on its own. Nobody wakes up at midnight to truncate a table. The period bucketing is built in, and yesterday's rankings stay queryable by their periodKey.
Friends and around-me slices
A global top-10 is motivating for the top ten players. Everyone else wants to see themselves and their friends. Both slices come from the same board.
// Players ranked just above and below the current player
const nearby = await tg.leaderboards.aroundMe('high_score', {
window: 5,
});
// Only the player's friends
const friends = await tg.leaderboards.friends('high_score');
The around-me slice is the one that keeps mid-table players coming back. "You're 3 spots from #40" beats a number they'll never reach.
Gotchas
submitis best-wins by default. If your game genuinely wants last-score-wins (say, current ELO), model it as a stat withtg.stats.update([{ key, op: 'set', value }])instead.period_keyformats differ by period. You get a date for daily and an ISO week for weekly. Don't hand-build them; read them back fromsubmitor pass ones you got fromtop.- Names come from the profile. Anonymous players show a default
display_nameuntil they set one withtg.players.updateProfile.
Two methods, and you've got global, daily, and weekly boards with friends and around-me slices, with no table, no endpoint, and no midnight cron. Add the submit call to your game-over screen and you're done.