Kraty

Player tools

Live-ops tools for when something goes wrong in production: force-complete, retry roll, manual grants, force-claim, and a read-only player profile endpoint.

The Player lookup screen (inside any game's nav) is the single search for everything Kraty knows about a player: profile snapshot, recent attempts, reward and crate grants, lobby memberships. It is also where you fix things when they break.

The four admin actions

Every action is permission-gated, audited (with reason captured where applicable), and idempotent where possible. Use them sparingly, because they bypass the normal player flow, so reach for them only to fix broken state, not as a routine operation.

Force-complete an attempt

When a player's attempt is stuck in in_progress (client crashed between progress reports, network partition, etc.), force-complete from the attempt row.

You can optionally override the final score. The leaderboard catches up with the new score automatically. Rewards are not auto-rolled; chain with Retry roll if the attempt should produce a grant.

  • Permission: attempt.force_complete (GAME_ADMIN, GAME_DEBUGGER)
  • Audit: attempt.force_complete with the reason in metadata.

Retry the reward roll

When the original roll failed or never happened (e.g. after a force-complete), re-run the reward pipeline from the attempt row.

The route refuses to roll a second time if any grants are already linked to the attempt: there is no duplicate-rewards path.

  • Permission: grant.retry_reward_roll (GAME_ADMIN, GAME_DEBUGGER)
  • Audit: grant.retry_reward_roll with { grantCount } in metadata.

Issue a manual grant

Make-goods, promotional one-offs, and recovery from edge cases. Compose the grant's contents as JSON in the Issue a manual grant card. The grant fires a normal grant.created webhook so your studio backend picks it up the same way as an event-completion grant.

  • Permission: grant.manual_create (GAME_ADMIN)
  • Audit: grant.manual_create with the reason and the external player id in metadata.

Force-claim a grant

Use after verifying server-side that a claim succeeded but the ack never came back (network hiccup mid-deploy on the receiver, etc.). Marks the grant claimed regardless of current status.

  • Permission: grant.manual_force_claim (GAME_ADMIN, GAME_DEBUGGER)
  • Audit: grant.manual_force_claim with full before/after diff.

From your backend (without the portal)

For support workflows that need to read player state programmatically (a customer-service tool, a fraud-review job, a Slack bot), call the player profile endpoint directly with a server_integration API key:

GET /server/v1/players/{externalPlayerId}
Authorization: Bearer <your-server-integration-key>

Returns the same shape the portal's Player lookup uses:

{
  "data": {
    "player": {
      "id": "...",
      "externalPlayerId": "alice_42",
      "firstSeenAt": "...",
      "lastSeenAt": "...",
      "lastContextSnapshot": { "country": "PT", "level": 7 }
    },
    "attempts": [ /* recent N */ ],
    "grants":   [ /* recent N */ ],
    "lobbies":  [ /* recent N */ ],
    "summary": {
      "attemptCount": 12,
      "attemptsCompleted": 8,
      "attemptsExpired": 1,
      "grantsPending": 2,
      "grantsClaimed": 9,
      "lobbiesActive": 0
    }
  }
}

The endpoint is read-only: no force-complete, no manual grant, no force-claim from the server side. Those mutations need a member session through the portal so they leave an actor trail. If your support team needs to issue make-goods programmatically, use POST /server/v1/players/:p/grants (which already audits the grant and fires the same grant.created webhook).

  • Auth: server_integration API key (a client_sdk key gets 403).
  • Scope: the key's (studio, game); the path does not carry them.
  • Limits: ?limit= caps each of the three lists (1–200, default 50).

Synthetic identity

Every player row carries two values Kraty derives for you, both shown on the player-detail page and returned to your game:

  • Country: resolved server-side (your CDN's geo header — CloudFront-Viewer-Country, CF-IPCountry, … — falling back to an offline GeoIP lookup on the client IP) on register, attempt-start, and leaderboard join. Stored on the player and usable as a country leaderboard segment with no client changes: you never send it (a playerContext.country you do send, or an operator override, wins). It comes back to the client in two places so you can render a flag: on register (as country) and on every leaderboard entry (entry.country; null for bots).

  • Synthetic identity: a stable fake { name, avatar } composed once from the game's default identity pool (or the built-in system pool) and keyed off the player, so it never changes on its own. Use it to show a player under a privacy-preserving alias (e.g. on a public board or a "recent players" feed) without exposing their real profile. Returned from POST /sdk/v1/players/:externalId/register as syntheticIdentity.

Two identities per player: display + anonymized

Every player carries two identities in parallel:

  • Display identity (displayIdentity) — what setIdentity writes. Optional; falls back to the anonymized pool value when the player never renamed themselves. This is the "real" name that leaderboards render as the primary label.
  • Anonymized identity (anonymizedIdentity) — the immutable synthetic pool value. setIdentity never touches this. Safe to surface on public boards or cross-game aggregations where the real name shouldn't leak.

Both are enriched with the player's country (identical on either view — a player has one real country regardless of which name you render). The register response carries both envelopes; every LeaderboardEntry mirrors them via name / avatar (display) and anonymizedName / anonymizedAvatar (immutable pool value).

Client SDKs expose two accessors for reading the calling player's own identity fresh from the server:

// TypeScript client SDK
const real = await kraty.players.getIdentity();            // display, fallback to pool
const anon = await kraty.players.getAnonymizedIdentity();  // always the pool value
// Flutter client SDK
final real = await kraty.players.getIdentity();
final anon = await kraty.players.getAnonymizedIdentity();
// Unity client SDK
var real = await kraty.Players.GetIdentityAsync();
var anon = await kraty.Players.GetAnonymizedIdentityAsync();

Customize a player's identity

The display identity is optional, but you can set it with a custom name and avatar — e.g. to honor a player's chosen handle. The anonymized identity stays untouched. From your backend with the server SDK:

// TypeScript server SDK
await kraty.players.setIdentity('player_42', {
  name: 'ShadowStrike',
  avatar: 'https://cdn.example.com/avatars/42.png', // URL, asset key, or id
});
# Python server SDK
kraty.players.set_identity("player_42", name="ShadowStrike", avatar="hero_blue")
curl -X PUT https://api.kraty.io/server/v1/players/player_42/identity \
  -H "Authorization: Bearer $KRATY_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"ShadowStrike","avatar":"hero_blue"}'

The new name + avatar flow onto leaderboard entries on the next read. Operators can also edit it from the player-detail page (Fake identity → Edit), and set a player's country there too.

Let players rename themselves (from the game client)

If your game has a "choose your username" screen, the player can set their own identity directly from the client SDK — no backend round-trip. It's authorized by the player's own secret, so a client can only ever rename itself, never another player:

// TypeScript client SDK — changes the active player's own identity
await kraty.players.setIdentity({ name: 'ShadowStrike', avatar: 'hero_blue' });
// kraty.syntheticIdentity now reflects it immediately.
// Flutter client SDK
await kraty.players.setIdentity('ShadowStrike', avatar: 'hero_blue');
// Unity client SDK
await kraty.Players.SetIdentityAsync("ShadowStrike", avatar: "hero_blue");

This trusts the client. If you need to moderate display names (profanity, impersonation, length rules), don't expose the client path — set names from your backend with the server SDK players.setIdentity above, after your own validation. Both write the same field; pick the one that matches how much you trust the caller.

Custom player metadata

Anything else you want to attach to a player — level, cohort, VIP tier, a chosen region — lives in player context, a free-form JSON bag. The client sends it on events.start (playerContext), and operators can edit it from the player-detail page (Edit context). Context keys double as leaderboard segments, so sending playerContext: { league: 'gold' } lets you rank players per league with no extra config.

Dev triggers: fast-forward end-of-period (test keys only)

While you're wiring Kraty into your game you'll want to see what happens when an event window closes or a weekly/monthly leaderboard rolls over (the grants, the promotion/relegation, the webhooks) without waiting for the real clock. Two server endpoints trigger those paths on demand, scoped to your key's game:

POST /server/v1/dev/events/{eventKey}/end-now
POST /server/v1/dev/leaderboards/{leaderboardKey}/rollover-now
Authorization: Bearer <your-TEST-server-integration-key>
  • events/{eventKey}/end-now closes the event's currently-active window(s) immediately, finalizing leaderboards, expiring in-progress attempts, dispatching window-end rewards, and closing lobbies, exactly as the scheduler would at the real end time. Returns the windows that were closed.
  • leaderboards/{leaderboardKey}/rollover-now rolls one shared leaderboard forward by a period right now, snapshotting it and paying out per-rank (and per-division) rewards plus any promotion/relegation. Returns rolledOver: false for an all-time board (nothing to roll).

These require a test-environment server_integration key; a live key gets a 403. Test and live are fully isolated: each environment gets its own event windows, leaderboard rankings, and period rollovers, so a test trigger only fast-forwards your test window / period and never touches live state.

The operations are idempotent: re-firing against an already-closed window or an already-rolled period is a safe no-op.

When NOT to use these

These tools are for fixing broken state. They are not for routine operations:

  • For periodic make-goods, generate them server-to-server via POST /server/v1/players/:p/grants, not by hand in the portal.
  • If an attempt regularly needs force-completing, the event config is probably wrong (e.g. target unreachable); fix the config rather than the symptom.
  • If grants regularly need force-claiming, your webhook receiver is unreliable; fix retries on your side instead of papering over with manual claims.

Resolve a "lost my reward" support ticket

Your support engineer gets a ticket: "I finished the daily race but the gold never arrived." The full path from ticket to resolution, all auditable:

Open the game's Players tab and search for the player by their external id (or the email they signed up with; both are indexed).

The profile shows their last reported context, ban status, and totals. Open the Attempts card and find the one they are complaining about. Its status is one of:

  • completed with grants attached → look at the Grants card; if pending they are waiting for the SDK collectAll() call. Click Force-claim to deliver immediately.
  • completed with no grants attached → the reward roll failed. Click Retry reward roll; it's idempotent, so the fix lands exactly once even if you double-click.
  • in_progress or expired → the attempt never completed server-side. Click Force-complete with an override score and a reason; the engine rolls rewards as if the attempt finished normally.

For a make-good on top of recovering the original payout, open the Wallet card and click + Credit. Enter the currency key, amount, and an audit reason. The credit appears instantly and emits a wallet.changed webhook to your CRM.

Every action lands in the studio Audit log with the actor, timestamp, and before/after diff. Linking the audit row in your ticketing system gives you a permanent paper trail.

Merge a guest player into an authenticated account

When a guest who has been playing locally signs in with a social provider for the first time, you usually want their progress to follow them, not start over. Run this from your backend, not the portal:

// `@kraty/server-sdk`: server_integration key required.
await server.players.merge('guest_device_abc', 'auth_user_42');

What happens, in one transaction:

  • Attempts and grants are reassigned to the authenticated id.
  • Item quantities are summed (guest had 2 potions, authed had 1 → 3).
  • Wallet balances are summed.
  • Lobby seats are re-pointed.
  • The guest's externalPlayerId is anonymised so the slot can be reused on the next guest signup from the same device.

A single player.merged webhook fires with the full counts so your analytics pipeline can stitch the two journeys together.