Rewards
Bundles, tables, crates, and policies — how players get paid.
Kraty's reward engine has four layers: bundles (deterministic payloads), tables (weighted rolls), crates (openable containers), and policies (who gets what when an event ends).
Bundles
A reward bundle is a deterministic payload. Same input, same output. Use bundles for daily-login rewards, first-completion bonuses, fixed tier rewards — anything where the payload is fixed.
{
"entries": [
{ "type": "currency", "currencyKey": "gold", "amount": 100 },
{ "type": "item", "itemKey": "legendary_sword", "quantity": 1 }
]
}Amounts and quantities
Anywhere a reward entry takes a number — amount on a currency
entry, quantity on an item or crate entry, rolls on a table
entry — the value accepts either a fixed integer or a randomized
range:
{ "type": "currency", "currencyKey": "gold", "amount": 100 }
{ "type": "currency", "currencyKey": "gold", "amount": { "min": 100, "max": 1000 } }
{ "type": "currency", "currencyKey": "gold", "amount": { "min": 100, "max": 1000, "step": 10 } }- Fixed — always grants exactly that number.
- Range
{min, max}— uniform random integer in[min, max], inclusive on both ends. - Range with
step— snaps tomin + k*step, so{100, 1000, step: 10}only ever produces 100, 110, 120 … 1000. When(max - min)isn't a clean multiple ofstep, the top bucket clamps tomaxso the upper bound stays reachable.
Rolls are deterministic per grant — the same logical grant re-rolled by an idempotent retry produces the same result, so the player never gets a "second chance" out of a flaky network. This behavior is inherited by every place a reward entry shows up: direct entries on a reward policy, entries inside bundles, weighted entries in tables, and crate contents (which roll at open time).
Tables
A reward table rolls random outcomes from a weighted list.
{
"weightedEntries": [
{ "weight": 60, "entry": { "type": "currency", "currencyKey": "gems", "amount": 10 } },
{ "weight": 30, "entry": { "type": "currency", "currencyKey": "gems", "amount": 50 } },
{ "weight": 10, "entry": { "type": "item", "itemKey": "legendary_sword", "quantity": 1 } }
]
}Tables roll deterministically per attempt — replays return the same result, so a network retry doesn't grant a second item.
Crates
A crate is a grant the player chooses when to open. Crates roll their reward table at open time, not at grant time, so a player can hoard them and decide when to pop one.
const pending = await kraty.grants.listPending('player_alice');
// Find a crate grant:
const crate = pending.find((g) => g.kind === 'crate');
const opened = await kraty.grants.openCrate('player_alice', crate.id);
opened.contents; // the rolled reward grantReplays of openCrate return the same rolled contents — the open is
idempotent.
Policies
A reward policy decides who gets what when an event ends or a player completes. Built-in policies:
| Policy | What it does |
|---|---|
none | No reward; the event is for the leaderboard only. |
fixed_bundle | Grants a single bundle to everyone who qualifies. |
loot_table | Rolls a single reward table per qualifying player. |
rank_scaled | Different payouts per rank bracket (top 1, top 10, etc.). |
participation_plus_tier | Base reward for everyone, scaled extras per tier. |
composite | Combine multiple of the above — useful for "fixed bundle + loot roll". |
shared_pool | Splits a single prize pool evenly among every attempt that hit a winner predicate. Pays out at window close (not at attempt completion) so the per-winner share knows the final count of winners. |
Policies emit grants. Grants are immutable, signed, and pushed to your backend via webhooks. The SDK can list pending grants, claim them, and open crates.
Milestone rewards
A milestone reward pays out mid-attempt, the first time a watched metric crosses its threshold. They're configured on the event (not the reward policy) because the trigger is metric-driven, not outcome-driven.
{
"milestoneRewards": [
{
"key": "kills_15",
"metricKey": "kills",
"threshold": 15,
"entries": [
{ "type": "currency", "currencyKey": "gold", "amount": 200 }
]
}
]
}Each milestone fires at most once per attempt — Kraty records the milestone key on the attempt the first time it pays out, so subsequent progress updates skip it. Milestones don't replace the terminal reward policy; both fire independently.
Shared pool
shared_pool is the "everyone who reached it splits the prize"
pattern. The split happens at window close because the per-winner
share is pool / numberOfWinners — and we don't know the winner count
until the window is final.
{
"type": "shared_pool",
"parameters": {
"pool": 10000,
"currencyKey": "cash",
"winnerPredicate": {
"type": "metric_at_least",
"metricKey": "streak",
"threshold": 5
}
}
}winnerPredicate is either { type: 'is_complete' } (any completed
attempt) or { type: 'metric_at_least', metricKey, threshold }. The
share is floored to an integer by default (floor: false opts out
for fractional currencies). Grants land with sourceRefId set to
the window id, not an attempt id.
Grant lifecycle
pending → claimed
↘ expired (if past expiresAt)Claim a grant from the game client via kraty.grants.claim(grantId),
or server-side via POST /server/v1/players/{externalId}/grants/{grantId}/ack
if your backend applied the reward before the client could.