Kraty

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 to min + k*step, so {100, 1000, step: 10} only ever produces 100, 110, 120 … 1000. When (max - min) isn't a clean multiple of step, the top bucket clamps to max so 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 grant

Replays 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:

PolicyWhat it does
noneNo reward; the event is for the leaderboard only.
fixed_bundleGrants a single bundle to everyone who qualifies.
loot_tableRolls a single reward table per qualifying player.
rank_scaledDifferent payouts per rank bracket (top 1, top 10, etc.).
participation_plus_tierBase reward for everyone, scaled extras per tier.
compositeCombine multiple of the above — useful for "fixed bundle + loot roll".
shared_poolSplits 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.