triggair

Blog / retention

Achievements players actually chase

3 min read

Achievements players actually chase cover art

Achievements are the cheapest retention you'll ever build (a progress bar with a dopamine hit at the end), and yet most homemade systems rot fast. You end up tracking counters in local storage, they desync across devices, players unlock the same thing twice, and the "reward" is a toast that vanishes if the app closes at the wrong moment. The idea is great; the plumbing is miserable.

Triggair collapses the whole thing to one server-authoritative call. You configure the achievement once in the dashboard, then report progress from the exact moment it happens in gameplay. Unlocks fire once, and rewards land somewhere durable.

Configure in the dashboard, report from code

An achievement is defined in the dashboard: its name, description, the target you're counting toward, any escrowed reward, and whether it's secret. Your code never hardcodes targets or reward tables; it just reports progress against a key.

ts
// Player just finished a run with 3 more kills. Report the delta.
await tg.achievements.report('kills_total', 3);

// A one-shot milestone - report 1 when the thing happens.
await tg.achievements.report('first_boss_down', 1);

report increments progress toward the configured target. When progress crosses the target, the unlock fires exactly once, on the server. Report kills_total a thousand more times and nothing double-fires. That "once" guarantee is the part you'd spend a weekend getting wrong by hand.

Rewards escrow to the inbox

When an achievement carries a reward, the unlock escrows it to the player's inbox rather than shoving it into your game state mid-frame. The player claims it when they're ready, and the claim is exactly-once.

ts
const items = await tg.inbox.list();
for (const item of items) {
  // Grant into your game only after a successful claim.
  const reward = await tg.inbox.claim(item.id);
  applyReward(reward);
}

This separation is what makes rewards survive a crash. The unlock and the payout are two durable steps: the server knows the reward is owed the instant the achievement fires, and claim settles it once. No lost loot because the app backgrounded at the wrong second.

Report from the natural moment

The single biggest design mistake is building a special "check achievements" screen. Don't. Report from wherever the action actually occurs, the same code path that already knows the player killed something, finished a level, or crafted an item.

ts
function onEnemyKilled(enemy) {
  spawnLoot(enemy);
  tg.achievements.report('kills_total', 1);      // fire-and-forget here
  if (enemy.isBoss) tg.achievements.report('bosses_slain', 1);
}

Going further

A few things that make achievements people actually chase:

  • Mix trivial with aspirational. A "complete the tutorial" unlock in the first two minutes teaches players the system exists; a "win 100 ranked matches" gives the committed something to grind. You need both. The easy one earns trust, the hard one earns hours.
  • Hide a few for delight. Mark an achievement secret in the dashboard and it stays invisible until unlocked. Stumbling into "you did that?" is a better feeling than watching a locked row you were told about.
  • One key, many call sites. The same kills_total key can be reported from melee, ranged, and environmental kills. The dashboard target is the single source of truth; your gameplay code just adds to it.
  • Let the typed errors guide you. If a key isn't configured, the error carries a code and an agent_hint. Your AI assistant can often fix the mismatch without you digging through docs.

Configure once, report from the natural gameplay moment, and let unlocks escrow to the inbox: that's a device-synced, dupe-proof achievement system in about three lines of game code.

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