Kraty

Common integration tasks

Recipes for the integrations every game studio ends up building — IAP fulfilment, daily quests, tournaments, matchmaker handoff, migration.

These are the shapes most studios need on day one. Each recipe maps a real product goal to the smallest set of Kraty calls that gets you there.

In-app purchase fulfilment

You verified a Google Play / App Store / Stripe receipt on your backend and need to credit the player. Use the server SDK — never the game client.

Use the receipt id as the idempotency key so retries (network glitches, store webhooks firing twice) never double-grant.

// On your backend, after receipt verification succeeds:
await kraty.wallet.credit(externalPlayerId, 'gems', {
  amount: 500,
  reason: 'iap',
  sourceRefId: receipt.transactionId,
  idempotencyKey: receipt.transactionId,
});
await kraty.inventory.grant(externalPlayerId, 'starter_chest', {
  quantity: 1,
  reason: 'iap',
  sourceRefId: receipt.transactionId,
  idempotencyKey: receipt.transactionId,
});

What it gives you:

  • Atomic, audited credit + grant.
  • Replay-safe — calling the same key twice is a no-op.
  • A webhook (wallet.changed, inventory.changed) you can reflect into your own analytics.

Daily quest

A daily challenge with a fixed reward — same shape works for any single-completion challenge.

  1. In the portal: create an event with availability.mode: 'recurring_windows' (daily reset), leaderboard.mode: 'global' or 'none', and a fixed_bundle reward policy.
  2. In the game client, start an attempt and push progress as the player plays:
await kraty.events.start(playerId, 'daily_kill_10_enemies');
// later, on every kill:
await kraty.events.progress(playerId, eventKey, attemptId, {
  mode: 'increment',
  metricValues: { 'kills': 1 },
});
  1. When the threshold is hit, Kraty fires the reward as a pending grant. The client claims it:
final pending = await kraty.grants.listPending(playerId);
for (final g in pending) {
  await kraty.grants.claim(playerId, g.id);
}

Quests reset on the next window automatically — the player can re-enter the event tomorrow with no extra wiring on your side.

Tournament with prize pool

A 7-day competition where the top 10 players share a prize pool.

In the portal:

  • Event with availability.mode: 'exact' (a one-shot week-long window) and leaderboard.mode: 'global'.
  • Reward policy: rank_scaled with brackets like [1,1] → 1000, [2,5] → 250, [6,10] → 100.

That's the whole setup. Players post scores through the SDK as usual; when the window closes, Kraty rolls grants for the top 10 and webhooks notify your backend. The player picks up their grant on next login.

For the live UI, stream the leaderboard via SSE so the rankings repaint without polling.

Push a lobby from your own matchmaker

If you already run matchmaking (Steam, GameLift, Photon, in-house), hand Kraty the resulting roster and let it host the leaderboard + scoring window.

Requires the event's leaderboard.mode to be 'lobby_matched'.

// Your matchmaker chose the roster; tell Kraty:
const lobby = await kraty.lobbies.push('your_game_id', 'quick_brawl', {
  key: 'matchmaker_match_abc123', // idempotent on YOUR id
  externalPlayerIds: ['alice', 'bob', 'carol'],
  capacity: 4,
  fillBots: true, // pad the empty slot with a bot
});

Players' game clients then start an attempt against that lobby normally — Kraty links it up via the leaderboardId returned by events.start.

Migrate from another platform

Bringing players, balances, and inventory over from PlayFab, Firebase, or an in-house backend. /migrate endpoints take up to 1,000 rows per call and surface per-row failures so a single bad row doesn't take out the batch.

// Each row's idempotencyKey is typically your stable id for that
// player / wallet entry / inventory holding — so retries are safe.
const outcome = await kraty.migrate.players([
  { externalPlayerId: 'p_1', idempotencyKey: 'p_1' },
  { externalPlayerId: 'p_2', idempotencyKey: 'p_2', contextSnapshot: { country: 'PT' } },
]);
console.log(`${outcome.applied} created, ${outcome.skipped} replayed`);
if (outcome.failures.length) {
  // Inspect outcome.failures and retry just those rows.
}

Wallet + inventory follow the same shape. Webhooks are NOT emitted during migration so a 100k-player import doesn't flood your backend — you get a clean cutover.

Handle a GDPR erasure request

A player asks you to delete their data ("right to be forgotten", GDPR Article 17). Your studio is the data controller; Kraty is a processor. The flow:

  1. Your support / account-settings UI receives the request and verifies the user's identity.
  2. Your backend calls kraty.players.delete(externalPlayerId).
  3. Kraty anonymizes the player row + cascade (attempts, lobbies, Redis leaderboard meta), emits one final player.deleted webhook with the original external id, and returns an outcome.
  4. Your own systems (CRM, analytics, BI) remove or anonymize the player as well, using the webhook as the trigger.
const outcome = await kraty.players.delete('alice', {
  reason: 'gdpr_erasure', // recorded on Kraty's audit log
});

switch (outcome.status) {
  case 'erased':
    // First-time deletion — the cascade ran, webhook fired.
    log.info({ playerId: outcome.playerId }, 'player erased');
    break;
  case 'no_op_never_existed':
    // GDPR-success — Kraty had no data on this player.
    break;
  case 'no_op_already_erased':
    // Idempotent replay against the placeholder row (rare).
    break;
}

What survives the deletion: the financial ledger (grants, item ledger, wallet ledger) is retained per audit requirements but points at an anonymized player row whose external id is now a __deleted_<uuid>__ placeholder. Nothing links those rows back to a person.

Companion: data export (GDPR Article 15, right of access). If the player asks for a copy of their data instead of (or before) deletion, call:

const bundle = await kraty.players.export('alice');
// bundle.player, bundle.attempts, bundle.grants, bundle.inventory,
// bundle.wallet, bundle.lobbies — all the data Kraty has.
// Returns 404 if Kraty has never seen this player.

The Python SDK has the same surface: kraty.players.delete(external_player_id, reason='gdpr_erasure') and kraty.players.export(external_player_id).

Daily login streak

Track consecutive-day logins, pay a bonus on milestones.

The cleanest shape uses two events:

  1. A single-metric event with daily recurrence whose start counts as the login.
  2. A streak event whose streak metric is bumped from your game on events.start of (1), with resetOn configured to zero streak if a day was missed.

Milestone rewards fire mid-attempt the first time streak crosses each threshold (3 days, 7 days, 30 days). See Rewards → Milestone rewards for the wire shape.

See also