triggair

Blog / leaderboards

Add a global leaderboard in five minutes

2 min read

Add a global leaderboard in five minutes cover art

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.

ts
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.

ts
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.

ts
// 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.

ts
// 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

  • submit is best-wins by default. If your game genuinely wants last-score-wins (say, current ELO), model it as a stat with tg.stats.update([{ key, op: 'set', value }]) instead.
  • period_key formats differ by period. You get a date for daily and an ISO week for weekly. Don't hand-build them; read them back from submit or pass ones you got from top.
  • Names come from the profile. Anonymous players show a default display_name until they set one with tg.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.

Build this into your game.

Everything above is one import and a publishable key away. Start with the quickstart, or read the guide for the exact feature.

Keep reading