Avatar unlock system — take-home case

Take-home case

Avatar unlock system

A proposed system for earning avatars through play, with rules GameOps can change without a game release.

Reading time
Main ~12 min · Appendix ~10 min
AI software
Claude Code, ChatGPT

At a glance

Problem
Assumption: players lack avatars earned through play. GameOps needs to change avatar rules without a game release.
Design
Players unlock avatars by building up their bets or wins, then choose which one to show. Goals depend on the player's level. Every GameOps change needs a second person to approve it.
Key choice
Each avatar has a separate goal for each player-level tier, and a player's goal stays fixed once progress starts. New players get goals they can reach, and big spenders still have to work for theirs. The economy owner signs off the tier ranges and goals.
Risks
  • Players could get wrong progress or awards. We agree ordered, replayable spin records with the spin team and test against spins with known results.
  • A rule change could give players the wrong goal. A second person approves every change before it goes live.
  • Unlock messages could slow down spins. If they do, the game loads them separately, and the rollout stays at its current share of players until it's fixed.
Measures
  • Adoption: share of active players shown the feature who have a non-default avatar by day 30.
  • Return: how many come back on day 7, compared with players without the feature.

My role

Main document

1 Problem and answer

What we solve, the design in short, and the goals

The problem. Players have no identity earned through play (assumed). GameOps (the live-ops team) must change avatars without a game release and catch a wrong threshold before players see it.

The answer. The server owns avatars as rewards. GameOps publishes reviewed rules; after each spin pays, the server counts it toward progress; the game shows the unlock and lets the player equip it. Players pick from what they own: the default picture, avatars earned by play, and rarely a replacement for a pulled avatar.

Terms. A settled spin is one spin the spin system has recorded and paid. A bar is a player's progress toward one target avatar. A revision is a numbered published catalogue. A notice is a message the server keeps for the game until the game confirms it showed it. A tier is a range of player levels; each earnable avatar has one threshold per tier, so goals suit both a new player and one betting billions (the economy owner confirms that level is a good guide).

Six decisions shape the design: #1 ladder (one avatar chased per mode at a time), #2 what counts, #3 locked goal, #4 where the award is decided, #5 taking an avatar out, #6 publication.

New avatar path. GameOps publishes a new avatar, Golden Cat (owned after winning 2,000,000 coins in total at tier 1) → Ana (a player) spins and fills her bar → the server records ownership and a notice → the game shows "Golden Cat unlocked" → Equip now.

Goals. Player: show who I am and earn something lasting from play. Business: more players return and pick an avatar, from a reward that costs no coins, compared with a holdout kept without the feature.

2 Walkthrough

One avatar change, from Maria's edit to Ana equipping it

Maria (GameOps) edits a rule, Jordi (GameOps) approves and publishes it, and Ana, a player, plays.

3 How unlocking works

What fills a bar, the ladder, and six decisions

The rule. Each earnable avatar has a mode (bet or win) and a threshold in coins for each tier (thresholdsByTier); a bar stores the one it locked. When the bar reaches the threshold or more, the player owns it, once.

Tier Player levels (assumed) Golden Cat, win
1 1–20 2,000,000
2 21–50 200,000,000
3 51 and above 20,000,000,000

The economy owner sets the level ranges and every threshold. Ana is at tier 1 in every example below. The brief left "win X coins" open; the table below is my answer. All slot games in the app count (assumed). The spin system records each spin's kind (assumed); I call it spinKind.

What happened Bet bar Win bar
Ana pays 200K to spin and wins 100K +200K +100K
That spin gives her free spins; they win 900K 0 +900K
Ana pays 2M to buy the bonus; it wins 500K +2M +500K
Marketing gifts her a free spin; it wins 50K 0 0 (assumed)

Bet bar = coins Ana paid. Win bar = coins Ana won. Gifts count for nothing.

Example: Golden Cat needs a 2,000,000 win and the bar is at 1,200,000. A win of 799,999 is not enough; 800,000 or 900,000 unlocks it. The extra 100,000 is dropped; the next avatar's bar starts from zero (#1, ladder).

The ladder. A player has at most two bars, one per mode.

  1. Next target: the live, earnable, not paused, not-yet-owned avatar with the lowest unlockThreshold for the player's tier in that mode (tie: earliest published, then lowest avatarId). Until a spin adds to that mode's bar, it shows as "next up": a locked tile with its rule.
  2. Assignment: that spin makes it the target, locks its rule and the player's tier as they were when the spin settled (not when processed), and counts at once. The tier comes from the player's level recorded with that spin, after any level-up the spin itself caused. The ring appears then. A promotional spin adds nothing, so it assigns nothing.
  3. Fresh bars: after a completion the next bar starts at zero, so one spin unlocks at most one avatar per mode. Only Retire or Pull and replace carries unfinished progress to the next target (A3).
  4. Level-ups: a new tier applies to the next assignment only; a bar in progress keeps its locked tier and threshold. Owned avatars stay owned.
The ladder for Ana, as in Figure 6 Two rows, one per mode, avatars in threshold order. Win mode: Golden Cat, win 2M, owned; Blue Owl, win 3M, current target at 0.2M of 3M; then the next live win avatar. Bet mode: none owned yet; Lucky Dog, bet 5M, current target at 2.5M of 5M; Jade Tiger, bet 20M, next up and locked. OWNED CURRENT TARGET · ONE BAR NEXT UP WINMODE BETMODE Golden Cat Win 2M coins 2M / 2M · owned ✓ Blue Owl Win 3M coins · locked 0.2M / 3M Next win avatar not published yet from 0 after it None yet Lucky Dog Bet 5M coins · locked 2.5M / 5M Jade Tiger 🔒 Bet 20M coins locked, shows its rule after it
Ana's two bars. Win: Golden Cat is owned; Blue Owl is the new target, starting from zero. Bet: Lucky Dog is the target; Jade Tiger stays locked until Lucky Dog is owned.

Six decisions

Question Our answer What we gave up
#1 What does a player chase? One avatar per mode (bet, win) at a time; after a completion the next bar starts at zero Chasing every avatar at once; extra coins past the target are lost
#2 What counts as progress? Bet bar: coins paid (the stake, or a bonus price, once). Win bar: coins won, spin by spin Round totals, and wins minus the bet
#3 Can the goal change mid-chase? No. The threshold and the player's tier lock when the chase starts; a rule change or a level-up applies only to later chases A wrong locked threshold needs a logged manual fix
#4 When is the award decided? After the spin has paid, from the settled-spin record The unlock shows a moment after the coins
#5 Can an owned avatar be taken away? Retire: owners keep it. Pull and replace (rare, for legal or offensive content): owners get a named replacement Replacement art must exist before a pull
#6 How do changes go live? As reviewed revisions, with a second approver in production and a kill switch for emergencies Changes take longer to publish

Product assumption. Every account starts with the default avatar (today's silhouette), owned and equipped. Others are earned.

4 Back office

How GameOps edit, review and publish an avatar

Who is who. Players never enter the back office. GameOps staff sign in with the existing back-office login and role; the top bar of every screen shows who is signed in. Maria (GameOps) edits a rule, Jordi (GameOps) approves and publishes it, and Ana plays. Back-office screens show full numbers (2,000,000), player screens short ones (1.4M / 2M). Win rings are amber, bet rings blue.

Change workflow. Maria opens a change (Draft) → submits it (In review; checks run) → another GameOps user approves it → it goes live as the next revision. An edit clears the approval; Send back returns it to Draft.

Step 1. Maria edits the rule (rule editor): an image from the asset library, availability, and the unlock rule with one threshold per tier. She raises tier 1 only and submits it.

Figure 2. Rule editor, signed in as Maria: change c-311 raises Golden Cat's tier-1 threshold from 2,000,000 to 3,000,000What to look at. Signed in as Maria (top right). The unlock rule shows only for an earnable avatar, with one threshold per tier; she raises tier 1 only. The right column reads it back: “Tier 1 players own Golden Cat after winning 3,000,000 coins”. Saving c-311 in review clears its approval.

5 Player screens

The avatar picker and the unlock toast

Avatar picker (a dialog over Settings). Ana sees what she owns and what she can still earn; grant-only avatars stay hidden until granted. A ring marks her target in each mode; other earnable avatars show their rule for her current tier ("Win 3M coins", "Bet 20M coins"). One line explains: "Goals depend on your level when you start. Started goals stay fixed."

Figure 5. Avatar picker: Ana chasing two targetsWhat to look at. Each current target carries a ring in its mode colour: Golden Cat 1.4M / 2M (win, amber) and Lucky Dog 1.7M / 5M (bet, blue). Other earnable avatars show their rule.
Figure 6. Avatar picker a few spins after Golden Cat unlockedWhat to look at. Golden Cat is owned, with a NEW badge. Blue Owl is the new win target, its bar started from zero (0.2M / 3M); Lucky Dog reads 2.5M / 5M.

Slot screen. One addition: the toast "Golden Cat unlocked" with Equip now / Later, after the server records the grant. It does not block play.

Figure 7. Slot screen at the unlockWhat to look at. The only change to the slot screen: the toast with Equip now and Later, shown once the server has recorded the grant. Play is not blocked.

6 System parts

The building blocks and the data they keep

Figure 1. System picture: new parts blue, existing greyWhat to look at. The existing spin system pays spins. The new Evaluator reads each settled spin and updates the Avatar store. The game and back office use the Avatar API; pending notices can also ride on the spin reply.

New parts blue, existing grey. The game reads the catalogue from the Avatar API, so changes need no game release. Reading notices adds a small, capped delay to the spin reply.

Entity Key Rule
Avatar avatarId Rule fields only when earnable: a mode and one threshold per tier; a pulled avatar is retired with a pullReason
Tier tierId Level ranges from the economy owner; fixed configuration, not edited in the avatar module (v1)
Change → revision changeId; revision Approver neither wrote nor edited it (production); a published revision never changes
Ownership (playerId, avatarId) One row per player and avatar, kept after Retire and Pull and replace
Selection playerId Must be owned and not pulled; version +1 per change
Bar (playerId, mode) Threshold and tier locked at assignment; only an audited correction may lower it
Spin processing (gameId, settledSpinId) Saved with bars, grants and notices in one step; kept while the spin record can be replayed

Notices (owned, replacement, progress_moved) wait until the game confirms it showed them. Audit entries record who changed what and when, with before and after values, for every change, approval, publish and pause.

7 APIs

The main calls, with examples and errors

Three kinds of calls: player (the player comes from the session), admin (back-office login; the existing GameOps role, Senior GameOps for pause and corrections; players have no access, A4) and internal (service identity). The game never reports an award; the server decides (#4, where it is decided). The main calls:

Player

Game

5
MethodPathPurpose
GETLive and retired avatars of the current revision
GETThe player's state: owned, equipped, bars, pending notices
PUTEquip an owned, equippable avatar
GET/v1/players/{playerId}/avatarAnother player's equipped picture, nothing else
POSTMark a notice as shown
Back office

Back office

3
MethodPathPurpose
PUTEdit a change; clears any approval
POSTRecord the approver, in production not the author
POSTValidate and publish the approved version as the next revision
Internal

Spin system

1
MethodPathPurpose
GETPending notices for the spin reply, under a short timeout

Other players see only your equipped picture, via GET /v1/players/{playerId}/avatar, on an existing social screen with its own visibility rules, such as friends (assumed); progress stays private.

Keeping the catalogue fresh. The phone keeps the catalogue with its revision number. Every player-state reply carries the current number; if it is newer, the phone downloads the catalogue again (all caches: A5).

How the phone notices a new catalogue The phone keeps the catalogue with revision 7. A player-state reply says catalogueRevision 8. Eight is newer than seven, so the phone downloads the catalogue again. If the numbers match, it downloads nothing. Phone keeps the catalogue, revision 7 Player-state reply catalogueRevision: 8 8 is newer than 7 download the catalogue again If the numbers match, the phone keeps its copy and downloads nothing.

8 Earn and equip

How the game and server talk when Ana earns and equips

Earning. Ana (tier 1) has Golden Cat at 1,400,000 of 2,000,000 (win) and Lucky Dog at 1,700,000 of 5,000,000 (bet). The spin reply goes back as today; the award is decided afterwards (#4, where it is decided).

Figure 8. Earning an avatarWhat to look at. Ana's spin s-123 completes Golden Cat: it pays first (1–3), the Evaluator records the unlock in one save (4–5), the game learns on a refresh (6–9) or the next spin reply.

The game never announces an award before the server has recorded it. It estimates each bar from the spins it has seen and asks the server only when an unlock is likely. The award reaches the game by whichever comes first: a refresh shortly after the estimate reaches the threshold, or avatarNotices in the next spin reply.

Equipping. Ana uses a phone and a tablet, both at selectionVersion 12.

Figure 9. Equipping on two devicesWhat to look at. Phone and tablet both start at version 12: the phone saves first and the tablet gets SELECTION_CONFLICT and redraws. Below, a timed-out save is retried and the conflict is treated as success.

When things go wrong

Case Server Player sees
Same settled spin twice Counted once No second toast
Evaluator down; notice read times out Spins pay as today; on recovery, spins are processed in order; lag alarm The toast comes later
Last spin completes a target Unlock and notice saved The refresh shows the toast, or the next fetch
Fetch fails, or the catalogue is empty, out of date or missing an avatar the latest player state names Keep the last good copy; retry Cached or default picture; Save waits

9 User stories

What must be true, as Given / When / Then

Story 1. As a GameOps user, I publish an avatar rule, and a wrong value does not reach players unchecked (#6, publication).

Given When Then
Maria raises Golden Cat's tier-1 unlockThreshold from 2,000,000 to 3,000,000 in production she submits diff against revision 7; "Tier 1 players own Golden Cat after winning 3,000,000 coins"
Maria is the author she approves SELF_APPROVAL_NOT_ALLOWED
Review warnings shown Jordi approves, not all ticked WARNING_NOT_ACKNOWLEDGED; not approved
Jordi approved Maria edits approval cleared; the change stays In review
Jordi approved, then Maria edits the publish call arrives PRECONDITION_FAILED with the version Jordi approved, CHANGE_CONFLICT with the new one; revision 7 stays
image not ready, or a threshold check fails (A1) anyone publishes VALIDATION_FAILED names the field; revision 7 stays
Jordi approved he publishes revision 8 live, audited; CHANGE_CONFLICT if a newer revision went live

Story 2. As a player, I earn an avatar by spinning (#1, ladder; #2, what counts).

Given When Then
separate fixtures: Golden Cat locked at 2,000,000, bar 1,200,000 credited win 799,999 / 800,000 / 900,000 not owned / owned / owned; overflow discarded
Lucky Dog locked at 5,000,000, bar 0 24 / 25 / 26 paid spins of 200,000 not owned / owned / owned; spin 26 assigns Jade Tiger (44), whose bar reads 200,000
Golden Cat locked at tier 1 (2,000,000); Ana reaches level 21 (tier 2) her next spins Golden Cat still needs 2,000,000; her next win target uses tier 2, so her two bars can hold different tiers
a spin settled at level 20 is processed after Ana reached level 21 it assigns her next target tier 1, from the level recorded at settlement
Ana's bar is locked at tier 1, and she is now tier 2 a correction applies revision 8 her tier-1 threshold from revision 8 is used, not tier 2's
win bar 1,900,000, bet bar 4,900,000 a 200,000 paid spin pays 100,000 both owned in one save, two notices
s-123 was processed it arrives again no change

Story 3. As a player, I equip an avatar I own (#5, taking an avatar out).

Given When Then
Ana owns retired Silver Wolf, version 12 she equips it equipped, version 13
she does not own Blue Owl (21) she equips it AVATAR_NOT_OWNED
the tablet holds 12 after the phone saved 13 it saves SELECTION_CONFLICT with the current choice; redraw
Golden Cat is equipped its Pull and replace by Phoenix is published Phoenix owned and equipped (selectionVersion + 1), one replacement notice; Golden Cat leaves the catalogue and owned, and equipping it returns AVATAR_NOT_EQUIPPABLE

10 Launch and measures

How we roll out, switch off and measure

Release controls.

Control Run by Effect
Staged rollout: internal, 1%, 10%, 50%, then all but the holdout The existing feature-flag tool (assumed) Advances while health signals hold; a bar starts at the player's first spin in the rollout that adds to it
Rule pause A senior GameOps user, audited Progress keeps counting, but nothing unlocks and no new chase starts until resumed
Kill switch (feature off) The game's on-call engineer, in the feature-flag tool Hides avatar screens, bars and toasts; shows the default picture; the server keeps recording
Notice read off The game's on-call engineer, in the feature-flag tool Spin replies carry no avatarNotices; the game gets notices on its next fetch

Before players see it. QA checks progress and awards against test spins with known expected results. The rollout widens only while the health checks in A3 pass. If spins slow down or unlock messages take too long to load, those messages stop coming with the spin and arrive the next time the game fetches updates, and the rollout stops widening. If awards arrive late or unprocessed spins pile up, the team is alerted and the rollout waits. If a bar or an award is wrong, that rule is paused and the affected progress and locked targets are corrected under audit before it resumes; avatars already granted stay owned.

Measures. Adoption: share of exposed active players with a non-default avatar at day 30. Return: day-7 return of players with the feature against the holdout, counted from assignment. Progress is never shown as a near-win.

11 Dependencies

What other teams must deliver, in order

Dependencies, in order. The server team builds the Avatar API, Evaluator and Avatar store once item 1 is agreed.

  1. Spin-system team: replayable records kept in a fixed order per player (settledSpinId unique per game, a playerSpinSequence that only goes up per player across games and devices, spinKind, per-spin increments, the player's level at settlement), avatarNotices, and measurable limits on added spin time and unlock delay plus the expected peak load, agreed before the build and checked in load tests. The spin reply also carries gameId, settledSpinId and playerSpinSequence, to match processedThrough (assumed). Fallback: the same evaluation inside the spin service after payout, one player at a time.
  2. Back-office owner: GameOps role, second approver, Senior permission; a correction needs a second Senior.
  3. Asset library: ready, versioned, removable images; default and replacement art.
  4. Client teams: the game's avatar screen and the back-office Avatar module, built against an API stub. GameOps with the economy and responsible-play owner: agree the first catalogue, the tier level ranges, pacing and every threshold before submitting. QA: boundaries (also tier edges, levels 20 → 21 and 50 → 51), duplicate and late spins, replay order, two devices, load, on a settled-spin test feed.
  5. Legal: the Pull and replace message, and whether a won avatar counts as digital content.

Releases (proposed).

Release Scope
v1, this proposal Bet and win modes, thresholds per player tier, the automatic ladder, the default avatar, picker and unlock toast, reviewed publishing, Retire, Pull and replace, pause, corrections and the kill switch
v2 A player-chosen goal and ladder order; a starter set
Later VIP gifts via grant_only

Appendix

Full API contracts, more screens and edge cases

A1 API reference

Every endpoint, payload and error

OpenClose

For engineers: payloads, fields, the back-office calls and every error.

Auth column: "GameOps" is the existing back-office role, "Senior" the senior GameOps permission.

The shared catalogue. The game asks "has it changed since revision 7?"; if not, the server answers "not modified" and sends nothing (standard HTTP caching: the ETag is the revision, the answer a 304). Every publish raises the revision. Only a Pull and replace takes an avatar out of the catalogue; a retired avatar stays in it as retired.

Example: revision 7, the live catalogue before c-311 is published, with three of its avatars.Show the example · 14 linesHide the example
{ "revision": 7,
  "avatars": [
    { "avatarId": 17, "name": "Golden Cat", "status": "live", "availability": "earnable",
      "mode": "win", "thresholdsByTier": { "T1": 2000000, "T2": 200000000, "T3": 20000000000 },
      "imageUrl": "…/golden-cat?v=3" },
    { "avatarId": 9, "name": "Silver Wolf", "status": "retired", "availability": "earnable",
      "mode": "win", "thresholdsByTier": { "T1": 1000000, "T2": 100000000, "T3": 10000000000 },
      "imageUrl": "…/silver-wolf?v=1" },
    { "avatarId": 44, "name": "Jade Tiger", "status": "live", "availability": "earnable",
      "mode": "bet", "thresholdsByTier": { "T1": 20000000, "T2": 2000000000, "T3": 200000000000 },
      "imageUrl": "…/jade-tiger?v=1" } ],
  "tiers": [ { "tierId": "T1", "minLevel": 1, "maxLevel": 20 },
             { "tierId": "T2", "minLevel": 21, "maxLevel": 50 },
             { "tierId": "T3", "minLevel": 51 } ] }

Revision 7, live before c-311, holds seven avatars: default (1), Silver Wolf (9, retired), Golden Cat (17, win 2,000,000), Blue Owl (21, win 3,000,000), Lucky Dog (30, bet 5,000,000), Phoenix (40, grant only), Jade Tiger (44, bet 20,000,000). Amounts are tier 1; tiers 2 and 3 are 100 and 10,000 times higher in these samples.

Player state, the success response of GET /v1/me/avatars and of equip:

Example: Ana's state a few spins after s-123: Golden Cat owned, Silver Wolf equipped, Blue Owl the win target at 200,000.Show the example · 13 linesHide the example
{ "owned": [ { "avatarId": 1, "source": "default" },
             { "avatarId": 9, "source": "rule_completion" },
             { "avatarId": 17, "source": "rule_completion" } ],
  "equipped": { "avatarId": 9, "selectionVersion": 13 },
  "currentTierId": "T1",
  "bars": [ { "mode": "win", "targetAvatarId": 21, "tierId": "T1", "unlockThreshold": 3000000,
              "revision": 8, "amount": 200000 },
            { "mode": "bet", "targetAvatarId": 30, "tierId": "T1", "unlockThreshold": 5000000,
              "revision": 6, "amount": 2100000 } ],
  "notices": [],
  "pausedAvatarIds": [],
  "processedThrough": { "gameId": "slot-42", "settledSpinId": "s-140", "playerSpinSequence": 1220 },
  "catalogueRevision": 8 }

owned leaves out pulled avatars. The picker labels avatars not yet chased with currentTierId's thresholds and each target with its bar's locked one; the two bars can hold different tiers. equipped.avatarId is the stored equippedAvatarId. pausedAvatarIds lists paused rules; it lives here because a pause makes no catalogue revision. A mode with no target returns, for example, { "mode": "win", "nextUpAvatarId": 21, "amount": 0 }; nextUpAvatarId is computed on read and copied into progress_moved. processedThrough is the last settled spin this state includes; its playerSpinSequence is the player's running spin number across games and devices. The game adds only the spins it saw with a higher number, so a spin from another device is never counted twice or missed. A higher catalogueRevision triggers a catalogue refetch.

The game fetches this state after a short delay when a bar's estimate reaches its locked threshold, or on the first spin that adds to a mode with no target, then a few more times with growing gaps, within a fixed budget (say, five tries in a minute) and respecting Retry-After. After that it waits for the next spin, login, app return or avatar screen, so a toast may be late but is never lost. Each answer resets the estimate. Login, app return and the avatar screen also fetch. A noticeId already shown is skipped while local history lasts. No new connection is opened; an existing push channel may notify a closed app (assumed). A newly owned avatar shows NEW until the player equips it or saves a choice after seeing it; the game keeps this mark (assumed), with no new field.

Notices

Notice What it carries Player sees Made once per
owned avatarId "Golden Cat unlocked" with Equip now grant; a replacement grant makes a replacement notice instead
replacement avatarId, replacedAvatarId "Avatar removed and replaced with Phoenix" player, pulled avatar and pull revision, also when the replacement was already owned
progress_moved removedAvatarId, mode, amount, nextUpAvatarId "Your progress now counts toward Blue Owl" player, removed target and revision

All model fields.

Entity Fields
Avatar avatarId (number, not reused), name, status (draft, in_review, live, retired), availability (default, earnable, grant_only), mode (bet, win), thresholdsByTier (coins per tierId, inclusive), assetId, imageVersion, firstPublishedAt, pullReason (legal, offensive), paused (rule pause; players get it as pausedAvatarIds)
Change changeId, status (draft, in_review, published, discarded), changeVersion, baseRevision, authorId, editorIds, approvedBy, publishedBy, published at, items (create, edit, retire, pull_and_replace with replacementAvatarId and pullReason); published as revision (whole number per environment) with its effective time
Ownership playerId, avatarId, source (default, rule_completion, replacement), grantedAt, gameId and settledSpinId of the completing spin
Selection playerId, equippedAvatarId, selectionVersion
Bar playerId, mode, targetAvatarId, locked tierId, unlockThreshold and revision, amount (with no target, carried progress)
Tier (fixed configuration) tierId, minLevel, maxLevel (none for the top tier); ranges cover every level without gaps or overlaps
Spin processing gameId, settledSpinId, playerId, processedAt, increments, assignments, grants
Settled-spin record (existing, assumed) gameId, settledSpinId, playerSpinSequence (only goes up per player, across games and devices), playerId, settledAt, playerLevelAtSettlement (the server's level after any level-up from that spin), spinKind, paidStake, creditedWin. The spin system writes it and the Evaluator reads it; no call is made. paidStake and creditedWin never count the same coins twice. Triggered free spins carry paidStake 0. A bought feature's price is only on its purchase record (bought_feature); its free games are bought_feature records with paidStake 0
Back office

Back-office endpoints

16

Two headers keep changes safe:

  • If-Match carries the changeVersion the caller last saw, in quotes (If-Match: "5"). It is the value GET /admin/v1/changes/{changeId} returns, also as its ETag. Edit, submit, approve, send back, publish and discard all carry it. If the change moved on since, the call fails with 412 PRECONDITION_FAILED.
  • Idempotency-Key is a key the caller picks for one action. A repeat with the same key returns the first result instead of doing it twice. The key is bound to the user, method, path, body and If-Match; reusing it with different input returns 400 INVALID_REQUEST. Keys, their bound requests and their original results are kept permanently, alongside the audit history. An exact repeat returns the original result before checking the current version or state, and never runs the action again.

An action the change's state does not allow (for example, editing a published change) returns 409 CHANGE_CONFLICT.

Change lifecycle.

  1. Every successful edit, submit, approve, send back, discard and publish raises changeVersion by 1 and returns it as the new ETag; publish sends the version that approve returned.
  2. A published change is closed and read-only.
  3. A draft or in-review change can be discarded with If-Match. It closes as discarded, audited.
  4. "Approve and publish" makes two calls: approve, then publish.
  5. After an avatar's first publish, its mode and availability are fixed; to change them, create a new avatar and retire the old one. Only the default avatar is default.
  6. The review returns threshold warnings as {avatarId, tierId, field, reason}. Approve sends the ones the approver ticked; for c-311 both: { "acknowledgedWarnings": [ { "avatarId": 17, "tierId": "T1", "field": "thresholdsByTier", "reason": "LARGE_THRESHOLD_CHANGE" }, { "avatarId": 17, "tierId": "T1", "field": "thresholdsByTier", "reason": "THRESHOLD_TIE" } ] }. The server works the warnings out again for that changeVersion; a missing one returns 422 VALIDATION_FAILED with WARNING_NOT_ACKNOWLEDGED and nothing is approved. The approval records the ticks, an edit clears them, and publish checks them again.

Opening a change, POST /admin/v1/changes, sends { "baseRevision": 7 } and returns 201 with changeId, changeVersion 1 and status draft. An edit sends the whole item list: PUT /admin/v1/changes/c-311 with If-Match: "2" and { "items": [ { "action": "edit", "avatarId": 17, "thresholdsByTier": { "T1": 3000000, "T2": 200000000, "T3": 20000000000 } } ] } returns 200 with the change and ETag: "3". A create item sends name, availability, assetId and imageVersion, plus mode and thresholdsByTier when earnable, and no avatarId; the server assigns the avatarId on the first save, later edits and retries keep it, and IDs are never reused. Send back sends { "comment" }; acknowledging a notice returns 204. A correction, POST /admin/v1/corrections, is one of two operations. { "operation": "apply_revision", "avatarId", "revision", "playerIds", "reason" } lowers those players' locked thresholds to that revision's value for each bar's locked tier and keeps their amounts. { "operation": "adjust_progress", "avatarId", "reason", "players": [ { "playerId", "lockedRevision", "delta" } ] } adds each delta once, in that player's processing order, so spins credited meanwhile stay. Either returns 201 with its correctionId and status proposed and runs after a second senior approves it; a player whose target or locked revision changed since is skipped and listed for a new review.

MethodPathAuthSafe to retry?Purpose
GETGameOpsYes (read)Catalogue list, newest first: status, availability, mode, thresholdsByTier, updatedAt (latest change or revision touching it), open changeId and its status, live revision
GETGameOpsYes (read)Search the existing asset library; returns assetId, imageVersion, ready state
POSTGameOpsIdempotency-KeyOpen a change on the live revision (baseRevision)
GETGameOpsYes (read)The change with changeVersion, items and status
POSTGameOpsIdempotency-Key, If-Matchdraft → in_review; runs validation
GETGameOpsYes (read)Diff against live, preview sentences, validation results
POSTGameOpsIdempotency-Key, If-Matchin_review → draft, with a comment
POSTGameOpsIdempotency-Key, If-MatchClose a draft or in_review change as discarded, audited; stale version returns 412
GETGameOpsYes (read)Version history
GETGameOpsYes (read)One revision's full content
POSTGameOpsIdempotency-KeyOpen a change that restores that revision; normal review and lifecycle validation
POSTSeniorIdempotency-KeyRule pause: the rule keeps crediting progress, grants nothing and is not assigned as a new target, until resumed
POSTSeniorIdempotency-KeyEnd the pause: make the due grants against the corrected record, then assign the rule again
POSTSeniorIdempotency-KeyPropose an audited player-state correction for one rule, run while it is paused
POSTSenior, not the proposerIdempotency-KeyRecord the second approval; the correction then runs
GETGameOpsYes (read)Audit log, filtered by avatar, user, action or time

Errors. Every error is { "error": { "code", "message", "requestId", "details"? } }; message is a short English sentence for logs and support, never shown to players.

Every code, its HTTP status, when it happens and what the caller does.Show all 13 error codesHide the error codes
Code HTTP When Caller does
INVALID_REQUEST 400 Malformed body, unknown field value, or an Idempotency-Key reused with different input Fix the call
UNAUTHENTICATED 401 No or expired session Sign in again
FORBIDDEN 403 Role lacks the action (for example pause without the senior role) Ask someone with the role
SELF_APPROVAL_NOT_ALLOWED 403 Someone who wrote or edited the change approves it in production Ask another GameOps user
AVATAR_NOT_OWNED 403 Equip of an avatar the player does not own Show it locked
AVATAR_NOT_FOUND 404 Unknown avatarId Refetch the catalogue
NOT_FOUND 404 Unknown changeId, noticeId, revision or correctionId; an unknown player, or one the caller may not see, on the other-player avatar read (both get the same answer). acknowledging a notice twice succeeds Reload the list
AVATAR_NOT_EQUIPPABLE 409 Owned, but pulled Refetch catalogue and state
SELECTION_CONFLICT 409 Stale selectionVersion Redraw from details
CHANGE_CONFLICT 409 A newer revision went live after baseRevision; the version being published is not the approved one (say, edited after approval); or an edit or action hits a closed change (published, discarded). reads of a closed change still work; a repeated publish with the same Idempotency-Key returns its first result Reload, reapply, review again; if never approved, get approval; if already published, reload the live revision
PRECONDITION_FAILED 412 If-Match names a stale changeVersion (the change moved on since the caller loaded it) Reload the change and re-check
VALIDATION_FAILED 422 Failing checks on submit, approve or publish; each detail names field and reason: IMAGE_NOT_READY, TIER_MISSING, THRESHOLD_INVALID_TYPE, THRESHOLD_OUT_OF_BOUNDS, TIER_ORDER_INVALID, THRESHOLD_NOT_APPLICABLE, MODE_MISSING, MODE_INVALID, WARNING_NOT_ACKNOWLEDGED (threshold checks below), AVAILABILITY_MISSING, FIXED_AFTER_PUBLISH (mode or availability of a published avatar), REPLACEMENT_INVALID, DEFAULT_PROTECTED Fix the fields
SERVICE_UNAVAILABLE 503 Avatar service down; Retry-After set Keep cached state; retry later

details: SELECTION_CONFLICT → equipped {avatarId, selectionVersion}; VALIDATION_FAILED → a list of {avatarId, tierId (for threshold checks), field, reason}; other codes have none.

Threshold checks, run for every tier on submit and again on publish against the catalogue that would result. A block stops the change; a warning shows on the review, and the approver must tick it before approving (an edit clears the ticks). Players already chasing an avatar keep their locked target (#3).

When Result
Earnable: a tier is missing from thresholdsByTier, or a tierId is unknown Block: TIER_MISSING
Earnable: a tier's threshold is not a whole number of coins sent as a JSON integer (a string or a decimal) Block: THRESHOLD_INVALID_TYPE
Earnable: a tier's threshold is below 1 or above 1,000,000,000,000 coins (the cap is set per environment; economy owners set the real values) Block: THRESHOLD_OUT_OF_BOUNDS
Earnable: a higher tier's threshold is lower than the tier below it (equal is allowed) Block: TIER_ORDER_INVALID
Default or Grant only: thresholdsByTier is sent Block: THRESHOLD_NOT_APPLICABLE
Earnable: mode is sent but is neither bet nor win (an absent mode is MODE_MISSING) Block: MODE_INVALID
Another live avatar in the same mode has the same threshold in the same tier Warning THRESHOLD_TIE; the tie-break in #1 applies
The change reorders a tier's ladder in that mode for players who start later Warning LADDER_ORDER_CHANGED
A tier's threshold changes by 50% or more from its published value Warning LARGE_THRESHOLD_CHANGE
Approve with a warning not ticked Block: WARNING_NOT_ACKNOWLEDGED

Another player's avatar. GET /v1/players/{playerId}/avatar uses the game session and the game's existing friend and leaderboard visibility; a player the caller may not see returns 404 NOT_FOUND. A pulled avatar resolves to its replacement or the default.

{ "avatarId": 17, "imageUrl": "https://cdn.example/avatars/golden-cat/v3.png", "imageVersion": 3 }

Retries. Each repeat below is matched by its key ("keyed by"), so it never applies twice.

  1. Settled spins are keyed by (gameId, settledSpinId). Each counts once.
  2. Equip is keyed by selectionVersion. A stale retry that passes the earlier checks gets SELECTION_CONFLICT with the current choice. The game treats it as success if that is the requested avatar, and otherwise redraws.
  3. Acknowledgements are keyed by noticeId, admin edits by changeVersion.
  4. Admin POSTs are keyed by Idempotency-Key.
  5. Only publish creates a revision.

Versions. /v1 changes only for breaking changes; the game ignores unknown fields and renders an unknown equipped avatar as the default.

A2 Version history and audit log

What the review screen shows below the diff

OpenClose

For GameOps: what the review screen shows below the diff and approval.

Figure 4b. Review: history and audit logWhat to look at. One version history per environment, each row with Republish; below it the read-only audit log with before and after values and filters.

Version history, one table per environment, newest first. Columns: revision, published at, publishedBy, approvedBy, kind (publish, republish), avatars touched, and "View" (the full revision and its diff with the one before). Republish on a row opens a change that restores that revision, diffed against live, with normal review and lifecycle validation. It never brings back a pulled avatar or its image. An avatar missing from that revision is retired the normal way (owners keep it, progress moves), never deleted. A banner says: "Republishing changes future assignments only. Players already chasing an avatar keep their locked target unless it is retired, and owned avatars stay owned."

Audit log. Threshold entries name the tier (for example thresholdsByTier.T1). Columns: time, user, action (create, edit, retire, pull and replace, submit, approve, send back, discard, publish, republish, pause, resume, correction), object (avatar or change), field, before, after, revision. Filters: avatar, user, action, date range. Read-only. Kill-switch use is audited in the feature-flag tool. Looking up one player's avatars is out of scope for now.

A3 Edge cases

Retire, pull, pause and late spins

OpenClose

For GameOps and engineers: what Retire, Pull and replace and Rule pause do, how players see them, and rare cases.

Retire (a reviewed change item).

  1. The avatar stays in the catalogue as retired. Owners keep it and can equip it. The picker shows it only to owners.
  2. It is no longer assigned or credited. A spin settled before the Retire went live still credits, and can complete, the retired target, even if processed later.
  3. After those spins, the unfinished amount moves to that mode's bar, seeds the next target and sends progress_moved. That next target and its tier are chosen and locked at the next spin that assigns one (#1), not at the Retire.
  4. A Pull applies to every spin processed after it commits, so it never completes the pulled avatar.

Pull and replace (rare: legal or offensive content).

  1. It is a change item with a pullReason and a live grant_only replacementAvatarId. It needs a second approval, as any production change does, and is audited.
  2. On publish, the avatar becomes retired with its pullReason and leaves everyone's catalogue. Its image stops being served, controlled copies are removed (assumed asset process), and clients stop showing it once they load the new catalogue revision.
  3. Its ID, name, status, ownership and audit history stay.
  4. One step per owner, keyed by player, pulled avatar and revision, runs once, and before that owner's next read or equip if it has not run yet. It:
    • grants the replacement (one ownership row per player, so never twice);
    • sends one replacement notice instead of owned, even if the replacement was already owned, without naming the pulled avatar;
    • equips the replacement with selectionVersion + 1 only if the pulled avatar is still equipped, so a newer choice stays;
    • drops any undelivered owned notice for the pulled avatar; a game that already received one stops showing it once it loads the new catalogue revision.
  5. Progress moves as for Retire.
  6. No coin compensation. Deleting offline copies is not promised.
  7. In an emergency before approval, the kill switch hides the whole feature.

Rule pause (senior, audited).

  1. It applies to spins processed after it takes effect.
  2. A paused rule still credits progress, but grants nothing and is not assigned as a new target. The picker shows it greyed as "Paused".
  3. Audited corrections run during the pause.
  4. At resume, one step per player, keyed by player, avatar and resume, makes the due grants against the corrected record, in the player's processing order, never twice.
  5. Each grant is one save, as for a completing spin: ownership, the owned notice, the cleared target and the discarded overflow.

Rule changes and removals, as players see them:

Case Server Player sees
Rule changed mid-chase Players already chasing keep their target; later assignments use the new revision Ana keeps 2,000,000; Ben (after revision 8) gets 3,000,000
Golden Cat retired Owners keep and can equip it; progress moves on, with a progress_moved notice "Golden Cat has left; your 1.2M now counts toward Blue Owl"
Golden Cat pulled and replaced Image removed; owners get Phoenix, equipped if they still wear the cat Owners: "Avatar removed and replaced with Phoenix"; players chasing it: "Your progress now counts toward Blue Owl" (wording from Legal)

Health signals and what to do:

Health signal Action
Spin reply time; notice-read timeouts Notice read off; stop the rollout
Award lag, backlog Alert; hold the rollout
Bars at or above their locked threshold without ownership, outside a rule pause; unprocessed records Pause the rule, replay, audited correction
Equip errors after a client release Hold that release
Catalogue or image failures, avatar-screen crashes Kill switch, fix, switch back on

More edge cases

Case What happens
A settled spin arrives late or out of order Only if the spin system breaks per-player order: a late spin may credit the target locked when it is processed, not the next one; results can drift from settled order by more than one spin; a replay keeps recorded assignments; the processing record stops a second credit
Carried progress already meets the next target's threshold That avatar is granted at the assigning spin; the remainder is discarded, as on completion
Every avatar in a mode owned, or none live No bar; the game shows "all collected" or nothing to earn; the first spin that adds to that mode after a new avatar goes live assigns it
A cheaper avatar is published mid-chase It becomes the target at the next assignment, not now
A threshold is lowered Players with a locked target keep the old one; no automatic sweep. If that is unfair, the correction "apply revision N to players in progress on this rule" lowers locked targets only. It runs during a rule pause, so anyone already at or above the new target is granted at resume
A mistyped threshold went live Pause the rule, find the affected locked targets and grants, approve an audited correction, resume. A rollback fixes future assignments only and reverses no grant
Retire or Pull and replace of the default avatar Refused: VALIDATION_FAILED with DEFAULT_PROTECTED
An approved change goes stale because another revision went live Publish returns CHANGE_CONFLICT; the author reapplies the edit on the new revision and it is reviewed again
A jackpot win Counts in win mode as credited; it completes at most one win-mode avatar
The player spins on two devices at once Spins are processed one at a time per player, in settled order
An old app version without the avatar screen Nothing changes for it; progress and ownership are recorded and appear after the update
A player leaves the rollout (rollout reduced) Their progress and ownership are kept; the flag hides the feature
A settled spin arrives without the player's level That player's avatar processing waits, in order, until the level recorded for that spin is recovered; the current level is never used instead
Republishing an older revision Restores its thresholds for future assignments only; bars keep their locked tier and threshold
A guest signs in Assumed: the account keeps the same player ID, so progress and ownership carry over
A notice is not acknowledged (for example, the app is uninstalled) It stays pending; fetched ownership remains the authority on the next login

A4 Access and roles

Access and roles: who can do what

OpenClose

For GameOps and engineers: who can do what, and where roles come from. The module reuses the existing back-office login and GameOps role. Senior GameOps is a permission on top of it, for pause, resume and player-state corrections. Assumed: the existing back-office access system assigns both and audits the assignments.

Who Where Can do
Player The game only (session login) Earn and equip avatars; no back-office access
GameOps Back office, Avatars Create and edit, submit, send back, discard, open a republish, approve and publish changes they did not write or edit, retire, pull and replace, read the history and audit log
Senior GameOps Back office, Avatars Everything GameOps can, plus rule pause and resume on their own, and player-state corrections, which another Senior approves
Back-office access owner (assumed) Back-office access settings, outside the avatar module Assigns GameOps and the Senior permission; audited there
Figure 10. Access: who can change avatars (sample people and dates)What to look at. Who can change avatars in production and with which role (sample people and dates). Assumed: the existing back-office access system assigns the roles; this screen only lists them.

Admin screens show an error next to the field it concerns, with a copyable request ID; operational error logs stay in the existing monitoring tools (assumed).

A5 Caching

Caching: what is kept where

OpenClose

For engineers: what is cached, where, and how it is cleared. Caches make reads faster; grants and equips always use the server's stored state.

What Where and key Kept until
Catalogue Phone; environment and revision Checked on login, app return and opening the picker (ETag = revision, 304 if unchanged); refetched when a player-state reply names a higher revision
Images CDN and phone; assetId and imageVersion A new image version is a new address. Pull and replace stops the origin serving the image and purges our CDN; phones stop showing it after their next catalogue refresh, though cached or offline copies may stay on the device
Rules of a revision Evaluator; environment and revision Never changes; kept while late spins may still use that revision
Current revision and paused rules Server Not cached; read live
Player state Phone; account Until the next fetch; cleared on logout; never used to decide a grant or an equip. It also carries the tier the picker uses for its labels
Another player's avatar Phone; environment, caller's account and playerId Cache-Control: private, no-cache: every display revalidates, and the server checks the session and current visibility before any 200 or 304. The ETag comes from the selection and catalogue revisions, after a pull resolves to its replacement. If revalidation fails, the cached picture is not shown; cleared on logout
/v1/me/*, notices, admin reads and every write No shared or HTTP cache Cache-Control: no-store; the game still keeps its last player state as above
Figure
100 %
Drag to move · + / − to zoom · 0 fits, 1 is 100 % · Esc, Back or a click outside the picture closes