triggair

Blog / teams

Clans with a shared leaderboard

3 min read

Clans with a shared leaderboard cover art

Players who show up with friends stay longer. But "add clans" usually means a new table for teams, another for memberships, join requests, roles, an invite flow, and then the real ask: a clan leaderboard. Most teams stop at the roster because the standings board looks like a whole second system.

It isn't. If your players already submit scores to a board, clan standings come for free.

Create a clan and manage the roster

A team has a name, a short tag, and a privacy mode. open lets anyone join instantly, closed blocks joins, invite_only gates on invites and join requests.

ts
const { team } = await tg.teams.create('Neon Foxes', 'NFX', { privacy: 'invite_only' });

// Discover and join
const results = await tg.teams.browse({ q: 'fox', limit: 20 });
await tg.teams.requestJoin(team.id);      // invite_only / closed
await tg.teams.join(anotherOpenTeamId);   // open teams join instantly

// The player's own clans, with their role in each
const memberships = await tg.teams.mine(); // [{ id, name, tag, role }]

To render a clan page, pull the roster with get, and use the admin actions to run it. Roles gate who can do what.

ts
const { members } = await tg.teams.get(team.id);

// Invite flow
await tg.teams.invite(team.id, playerId);
// The invitee accepts by invite id, taken from their pending invites:
const [invite] = await tg.teams.myInvites(); // [{ id, team_id, name, tag }]
await tg.teams.acceptInvite(invite.id);

// Admin actions (require the right role)
await tg.teams.setRole(team.id, playerId, 'admin');
await tg.teams.kick(team.id, playerId);
await tg.teams.ban(team.id, playerId);
await tg.teams.transfer(team.id, newOwnerId); // hand over ownership
await tg.teams.leave(team.id);

The payoff: a shared leaderboard for free

Here's the part that usually costs a sprint. tg.teams.leaderboard takes a board you already submit scores to and aggregates every member's score into a clan standing. Pick how to combine them: sum rewards big active rosters, avg rewards small elite ones, max ranks clans by their single best player.

ts
// Members already do this in normal play:
// await tg.leaderboards.submit('weekly_score', 4200);  // your existing board

const standings = await tg.teams.leaderboard('weekly_score', { agg: 'sum' });
// standings: [{ team_id, name, tag, value, members }]

for (const row of standings) {
  console.log(`${row.tag}  ${row.value}  (${row.members} members)`);
}

No new writes, no clan-score plumbing. Members play, submit to the board they already use, and the clan table assembles itself.

Gotchas

tg.teams.leaderboard always returns highest-value-first, regardless of what the board measures. For a points board that's exactly right. For a lower-is-better board (fastest lap, fewest moves, speedrun time) the default order is backwards. Sort it client-side:

ts
const board = await tg.teams.leaderboard('best_time_ms', { agg: 'avg' });
const ranked = [...board].sort((a, b) => a.value - b.value); // ascending for time

A couple more things worth knowing. Admin actions are role-gated, so a kick or setRole from a member will fail. Every SDK error carries a code, a human message, and an agentHint, so catch it and show the reason instead of a dead button. And avg on a board where not every member has submitted only averages the members who have, which can make a two-player clan look elite; if that matters for your game, prefer sum or gate standings on a minimum roster size.

Takeaway

Clans are a roster plus a privacy mode plus role-gated admin actions, and the leaderboard you already run turns into clan standings with one call. Wire the aggregation, remember to sort lower-is-better boards yourself, and you've shipped competitive social play on top of scores you were already collecting.

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