Kraty

Rewards

Bundles, tables, crates, and policies, and how players get paid in Kraty.

Kraty's reward engine has four layers:

  • Bundles: deterministic payloads.
  • Tables: weighted random rolls.
  • Crates: openable containers the player decides when to pop.
  • Policies: who gets what when an event ends.

Each layer composes with the next. You almost never write all four by hand; most events use a single policy that references a bundle or a table.

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) is not 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 does not 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();
// Find a crate grant:
const crate = pending.find((g) => g.kind === 'crate');
const opened = await kraty.grants.open(crate.id);
opened.contents; // the rolled reward grant

Replays of open 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 are 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 do not 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 do not 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.

Inventory management

Who actually holds the items and currency a grant contains is a per-game setting (Settings → Engine defaults → Inventory management):

ModeWho owns inventoryCan the game client create grants?
studio (default)Your backend. Grants stay claimable; you listen on grant.created and apply them to your own inventory.No
platformKraty. Grants auto-deposit into player holdings; the SDK reads inventory / wallet and does atomic consume.No
permissiveKraty (like platform), plus the game client may grant to its own player.Yes — self only

studio is the default so nothing changes for an existing game until you opt in.

A grant issued from the dashboard always lands in Kraty's tables. In studio mode that's all it does — your backend still has to react to grant.created / inventory.changed to apply it to your own inventory. If a grant looks like it "did nothing" in-game, this is almost always why. The player's Economy tab shows which mode the game is in.

Keeping a running game in sync

Changes don't only come from the client: an operator grants from the dashboard, your backend credits a currency, the engine pays out an event reward or a level-up. In platform-managed modes the SDK can subscribe to all of it instead of polling:

const stop = await kraty.inventory.watch(
  (ev) => {
    if (ev.kind === 'inventory_changed') refreshBackpack();
    if (ev.kind === 'wallet_changed') setBalance(ev.economyKey, ev.balance);
    if (ev.kind === 'grant_created') showRewardPopup(ev.contents);
  },
  { ignoreOwnWrites: true },
);

Each event carries an origin (client / server / admin / engine), so ignoreOwnWrites skips the echo of the client's own writes. See the TypeScript SDK guide for reconnect handling.

What new players start with

Every currency, progression item, and item has a starting amount on its page — 0 unless you change it. Set it to anything above zero and every player who registers from then on is handed that much the moment we first see them: 100 gems, 3 revive tokens, the bronze-league trophy floor, a starter sword.

It arrives as an ordinary grant (sourceKind: starting_balance), so it behaves exactly like a reward you'd hand out yourself:

  • Studio-managed inventory: you get a grant.created webhook with every starting entry in it, and seed your own tables from it.
  • Platform-managed / permissive: it's deposited straight into the player's wallet and inventory, readable from /inventory and /wallet on their very first call. If one of the starting resources drives a derived level, the level is computed too — a starting xp of 500 puts the player at whatever level your curve says, and any per-level rewards that implies pay out.

Two things worth knowing:

  • It only applies going forward. Raising a starting amount later doesn't top up players who already exist — that would be a silent payout across your whole economy. Grant it explicitly from your backend if you want existing players to get it too.
  • Derived items can't have one. A level computed from XP is recomputed on every XP write, so set the starting amount on the source item (xp) and the level follows.

Progression items

Economy resources come in two flavours, picked when you create one:

  • a currency is spendable (gold, gems);
  • a progression item accumulates (XP, trophies, ranks, levels).

Progression items are what leaderboards segment by, what promotion and relegation ladders move, and what the two features below are about.

Where are players on it? The Progression page ends with a Progression distribution panel: pick an item — including the built-in sessions and login_streak — and an install cohort to see how many real players sit at each value, with the headline numbers above the chart (players counted, median, share still at the start value, highest reached). When the item has tiers, a band under the axis names them. Hover a column for exact numbers, click it to open the players there, or switch to the table view; wide-ranging items like XP are binned into equal ranges so the chart stays readable.

Levels that follow XP automatically

Rather than have your game compute XP and level and keep both in sync, tell Kraty that level is derived from xp and it owns the number from then on. On the progression item's page, tick "Derive this value from another progression item" and pick:

  • the source item (xp);
  • the thresholds — the same editor as division tiers, so you can list ranges by hand or generate them from a linear ("one level per 100 XP") or exponential ("each level costs 1.5× the last") curve;
  • the first level (1 by default; use 0 if your game counts from zero).

The preview table shows exactly which XP range maps to which level, and a test box converts any amount so you can sanity-check the curve before saving.

From then on, every write to xp recomputes level in the same transaction — whether the XP came from the client, your server, an event reward, or an operator adjustment. It also moves down: refund some XP and the player is demoted, which is the whole point of deriving rather than pushing. Writing level directly is refused (the next XP write would overwrite it anyway); write the source instead.

Rewards for each level gained

Add reward entries under "Reward per level gained" and every level crossed pays out — "100 coins per level" pays 300 when a single award jumps three levels, not 100. Use per-level overrides for milestone levels (a trophy at level 10) and an explicit empty list for levels that shouldn't pay.

Payouts are ordinary grants (sourceKind: progression_level), so they deposit or stay claimable exactly like event rewards depending on your inventory mode. Dropping back down never claws them back — the player earned them at the time.

Letting the client push progress

Set "Let the game client push progress into this resource" on a progression item and the SDK can add to it directly:

const res = await kraty.wallet.progress('xp', { amount: 250 });
res.derived; // [{ economyKey: 'level', previous: 1, current: 3, ... }]

That saves a backend round-trip for values the client is genuinely the source of truth for (XP from a finished run, distance travelled). It's off by default and per resource, and it can never be enabled on a spendable currency — a client that could mint gems could mint IAPs. Spending was always allowed; this opens the gaining side, so turn it on deliberately. Anything server-authoritative stays on the server API.

Client-side grants (permissive mode)

In permissive mode the game client can grant items / currency / crates directly to its own player — handy for prototypes, jams, and trusted single-player games where a server round-trip for every reward is overkill:

// Only works when the game is in "permissive" inventory mode.
await kraty.grants.grant({
  entries: [
    { type: 'item', itemKey: 'gold_sword', quantity: 1 },
    { type: 'currency', currencyKey: 'coins', amount: 250 },
  ],
});

// It auto-deposits, so you can read it straight back:
const inv = await kraty.inventory.list(); // gold_sword ×1
const wallet = await kraty.wallet.list(); // coins: 250

Two guard rails, always enforced by the server:

  • Self only. The player secret authenticates the caller, so a client can only ever grant to itself — never to another player.
  • Opt-in only. In any other mode the call fails with 403 inventory_not_permissive, so a client can't mint items in a game that never enabled it. Create grants from your backend with a server_integration key instead (POST /server/v1/players/{externalId}/grants).