Daily rewards and streaks that bring players back
3 min read
Every game that keeps players comes back to one loop: give someone a small reason to open the app tomorrow. A daily reward is that reason. But rolling your own is a trap: the moment the reward depends on Date.now(), someone sets their phone clock forward and drains a week of bonuses in ten seconds. And if you're not careful, a flaky network retry hands out the same reward twice.
Triggair's daily system solves both. The clock lives on the server, each day can be claimed exactly once, and the reward doesn't land directly in a wallet: it escrows to the player's inbox, so the grant is a first-class, auditable event.
Read the streak state
Before you render anything, ask the server where the player stands. tg.daily.status() tells you the current streak, whether today is claimable, and what the next reward will be.
const status = await tg.daily.status();
// { streak_count, longest_streak, claimable, day_index, next_reward }
if (status.claimable) {
showClaimButton(status.next_reward);
} else {
showComeBackLater(status.streak_count);
}
Notice you never compute "is it a new day?" yourself. claimable is the server's answer, gated on server time. There's nothing on the client to spoof.
Claim the reward
When the player taps the button, call tg.daily.claim(). It returns the new streak count and the reward that was granted.
try {
const { streak_count, reward } = await tg.daily.claim();
playConfetti();
renderStreak(streak_count);
toast(`Day ${streak_count}! You earned ${reward.label}.`);
} catch (err) {
// Already claimed today, or the streak window hasn't opened yet.
console.warn(err.code, err.agentHint);
}
If a retry fires twice, the second call fails cleanly instead of double-granting, since the exactly-once guarantee is enforced server-side. Every error carries a code, a human message, and an agentHint, so a coding agent can read the failure and correct the flow without guessing.
Collect what you earned
The reward escrows to the inbox rather than dropping straight into a balance. That's deliberate: it gives you one consistent place where all grants (daily bonuses, match payouts, purchases) arrive and get claimed. Pull the inbox and claim the pending item.
const items = await tg.inbox.list();
for (const item of items) {
const result = await tg.inbox.claim(item.id); // exactly-once
applyToGame(result);
}
You can claim inline right after the daily claim, or batch it the next time the player opens their mailbox. The escrow waits.
Rendering the come-back loop
The streak is the retention mechanic, so show it off. A row of seven pips with day_index highlighted, a "longest streak: 12" badge from longest_streak, and a preview of next_reward all make the cost of missing a day feel real. Players protect streaks they can see.
Gotchas
- Don't cache
claimableacross a session. The player might cross the server's day boundary while the app is open. Re-calltg.daily.status()when the app regains focus. - The reward cycle is configured in the dashboard, not in code. Set the escalating reward table (day 1 through day 7, loop or cap) there, and
next_rewardreflects it automatically, so you can tune your economy without shipping a build. - A missed day resets the streak per your dashboard rules; surface that risk in the UI rather than after the fact.
Wire up status on launch, claim on tap, and inbox.claim to collect, and you have a tamper-proof retention loop in about a dozen lines.