triggair

Blog / leaderboards

Stop leaderboard cheaters

3 min read

Stop leaderboard cheaters cover art

Here's the uncomfortable truth about submitScore(999999999): if the number comes from the client, it's already a lie. Anyone with dev tools open can call your submit endpoint directly, type whatever score they like, and land at the top of your board before lunch. No obfuscation saves you. The client is the attacker's machine. The only scores you can trust are the ones the server has a reason to believe.

Triggair assumes every submission is hostile and filters it through layers you configure per board in the dashboard. Your code stays simple; the defense lives server-side.

ts
await tg.leaderboards.submit('weekly_sprint', score, { elapsedMs });
const top = await tg.leaderboards.top('weekly_sprint', { limit: 20 });

That elapsedMs isn't decoration. It feeds the defenses. Three checks watch each submission:

  • A per-board z-score anomaly threshold. The server knows the distribution of real scores on this board. A submission that sits absurdly far above the mean gets flagged as a statistical outlier. You tune how aggressive this is per board. A hardcore speedrun board tolerates a wider spread than a casual daily.
  • A minimum-elapsed-time floor. If your level physically can't be cleared in under, say, the time it takes to load it, a submission claiming otherwise is impossible. The elapsedMs you pass is checked against the floor you set. Scores that beat the clock don't count.
  • A max-submissions-per-hour cap. A real player finishes a run, then plays another. A script submits hundreds. Rate-limiting per player per board turns off the firehose without inconveniencing humans.

These three are configuration, not code. Set them per board where they belong. The values depend entirely on your game's real skill ceiling, which is why there are no magic numbers here: you calibrate the floor to your fastest legitimate run and the z-score tolerance to your real score spread.

For the scores that truly matter, like a global #1 or a prize board, tighten the same three checks until only plausible runs survive. Pass elapsedMs on every submit so the time floor and the z-score have real signal to work with, then set an aggressive minimum-elapsed floor and a tight z-score tolerance for that board in the dashboard.

ts
// The submit call stays the same; the strictness lives in the board's config.
await tg.leaderboards.submit('grand_prix', score, { elapsedMs });

The board's configured anti-cheat does the rest: an impossible time is rejected by the floor, a statistical outlier is flagged by the z-score, and a script is throttled by the per-hour cap. For a prize board where the top slot is genuinely worth cheating for, the most durable defense is to not trust a raw score at all: back the board with a server-authoritative metric (see below) so there's no number to forge in the first place.

Going further

  • Economy-driven scores are already unforgeable. If a stat derives from currency or inventory (coins collected, items owned), it's safe by construction. Currency and inventory are server-authoritative and only granted through validated paths, so there's no number to edit. Prefer economy-backed metrics for boards when you can.
  • Layer, don't pick. Z-score, time floor, and rate cap stack, and together they catch the lazy 95%. Tighten all three per board, and for the boards where the top slot is worth cheating for, back the metric with server-authoritative economy data.
  • Read the rejection. A blocked submission returns a typed error with a code, a human message, and an agentHint. Log it, and you'll see your cheaters knocking.

You can't stop a determined player from lying to their own machine. What you can do is make sure the server never believes them. Triggair gives you the statistical, temporal, and rate-limit layers to make an honest board actually honest.

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