Daily login
Reward players for opening your game on consecutive days. Logins are counted server-side; your client just reads the calendar and shows the reward.
Daily login is a calendar of rewards for consecutive days of play: day 1 pays a little, day 7 pays a lot, and a missed day starts the calendar over. Each game has one.
It is not an event. There is nothing to join, start or report progress to. Kraty counts the days on its own clock and pays the reward by itself; your client's job is to draw the calendar and show the popup.
The model
- A day is a UTC day. A player's streak goes up by one for each consecutive UTC day with at least one session, and drops to 0 the day after a missed one.
- Your game's SDK calls open sessions. Registering, listing events, reading the wallet: whichever call your game makes first. A session opens on first contact and again on the first call after 30 minutes of silence; calls in between belong to the same session. You never report a login.
- The reward is paid on the first session of the day, as an ordinary
grant with
sourceKind: "daily_login". It is paid once per player per UTC day, however many sessions follow. - After the last day the calendar does one of three things, set in
the dashboard:
restart: day 8 of a 7-day calendar pays day 1 again.hold_last: every day after day 7 pays day 7's reward.stop: nothing more until the streak breaks and starts again.
- The dashboard is the source of truth. Days, rewards and the after-last-day rule are edited under Live ops → Daily login and apply from the next payout. Read them from the SDK; don't hardcode them in the client.
Setting it up
In the dashboard, open Live ops → Daily login and follow Set up a
daily login. That opens the Daily login streak template, which asks
for the reward currency and creates the calendar (7 escalating days,
restart, in Staging). Everything is editable afterwards on the Daily login page:
add or remove days, give each day any mix of currencies, items, crates,
bundles or reward tables, pick the after-last-day rule, and choose the
state.
The state works like an event's: Staging pays only players on your test keys, so you can try the calendar in a dev build before turning it Live. A new calendar starts in Staging, like the events templates create, so switch it to Live when you're ready for real players.
In your game
On launch, read the calendar. When that launch opens a new session (the
first of the day, or after 30 minutes away), the read itself pays today's
reward, so paidToday is already true when it returns. A player still in
yesterday's session, playing through midnight UTC, sees paidToday: false
until their next session.
The popup shows the grant, not the calendar: a day configured with a range or a reward table has been rolled into real amounts in the grant.
var cal = await kraty.DailyLogin.ReadAsync();
if (cal == null) return; // no calendar, or it's off for this environment
DrawCalendar(cal.Days, highlightDay: cal.CurrentDay);
// Show today's popup once per grant, not once per launch.
if (cal.PaidToday && PlayerPrefs.GetString("dl_seen") != cal.TodayGrantId)
{
var paid = (await kraty.Grants.ListPendingAsync()).Find(g => g.Id == cal.TodayGrantId);
if (paid != null) ShowRewardPopup(paid.Contents["entries"]);
PlayerPrefs.SetString("dl_seen", cal.TodayGrantId);
}
// Countdown to the next reward.
var next = DateTime.Parse(cal.NextDayAt, null, DateTimeStyles.RoundtripKind);const cal = await kraty.dailyLogin.read();
if (!cal) return; // no calendar, or it's off for this environment
drawCalendar(cal.days, cal.currentDay);
if (cal.paidToday && localStorage.getItem('dl_seen') !== cal.todayGrantId) {
const paid = (await kraty.grants.listPending()).find((g) => g.id === cal.todayGrantId);
if (paid) showRewardPopup(paid.contents['entries']);
localStorage.setItem('dl_seen', cal.todayGrantId!);
}
const msUntilNext = Date.parse(cal.nextDayAt) - Date.now();final cal = await kraty.dailyLogin.read();
if (cal == null) return; // no calendar, or it's off for this environment
drawCalendar(cal.days, highlightDay: cal.currentDay);
final prefs = await SharedPreferences.getInstance();
if (cal.paidToday && prefs.getString('dl_seen') != cal.todayGrantId) {
final pending = await kraty.grants.listPending();
final paid = pending.where((g) => g.id == cal.todayGrantId).firstOrNull;
if (paid != null) showRewardPopup(paid.contents['entries']);
await prefs.setString('dl_seen', cal.todayGrantId!);
}
final next = DateTime.parse(cal.nextDayAt);What the calendar contains
| Field | Meaning |
|---|---|
days | Day 1 first. Each has day and entries, the reward entries as configured, e.g. { "type": "currency", "currencyKey": "gems", "amount": 10 }. Amounts can be ranges and entries can be reward tables, so draw the tiles from this but show what was paid from the grant. |
afterLastDay | restart, hold_last or stop. |
loginStreak | Consecutive UTC days with a session; 0 once a day was missed. |
currentDay | The calendar day the player is on, 1 to days.length; 0 without a streak. Under restart it wraps back to 1, so use this, not loginStreak, to pick the tile. |
paidToday | Whether today's reward has been granted. false under stop once the player is past the last day, or if today's day has no rewards. |
todayGrantId | The grant that paid today. Use it to show the popup once and to find it in your pending grants (in platform-managed inventory it's already deposited but stays there until claimed). If your backend claims grants itself it may be gone; show days[currentDay - 1] instead. |
nextDayAt | Start of the next UTC day, when the next reward can be earned. |
itemKey | A progression item holding currentDay. Handy if your UI already renders from the wallet; otherwise ignore it. |
read() returns null when the game has no calendar, or its state
keeps it out of the key's environment (a Staging calendar is invisible
to live keys).
Receiving the reward
The reward is a normal grant, so it arrives the way your game already handles grants:
- Platform-managed inventory: the grant is deposited on the spot.
The currency is already in the wallet when
read()returns. - Studio-managed inventory: the grant is pending. Claim it with the
SDK's collect-all (
Grants.CollectAllAsync()in Unity,grants.collectAll()in TypeScript and Flutter), or let your backend fulfil it from thegrant.createdwebhook, wheresourceKindisdaily_login.
If you keep an inventory stream open, a day paid while the game is
running arrives as grant_created (with sourceKind: "daily_login")
and wallet_changed. On launch, though, the grant is usually created by
your very first call, before the stream is connected. That is why the
example above shows the popup from paidToday, not from the stream.
A new day's reward is paid by the first session of that day, not
at midnight. A player who plays straight through 00:00 UTC is paid
when their next session opens (after 30 minutes idle, or on the next
launch). If you show a countdown, call read() again after
nextDayAt once the player returns.
Testing
Leave the calendar in Staging (where it starts) and use a test key,
or open your game's Playground, which plays on one. The player detail
page shows the day item and each paid day as a daily_login grant.
Streak days follow the real UTC clock, so a new day needs a new UTC day.