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.
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) → 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 an unlockThreshold in coins. When the bar reaches the threshold or more, the player owns it, once. The brief left "win X coins" open; the table 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.
- Next target: the live, earnable, not paused, not-yet-owned avatar with the lowest
unlockThresholdin that mode (tie: earliest published, then lowestavatarId). Until a spin adds to that mode's bar, it shows as "next up": a locked tile with its rule. - Assignment: that spin makes it the target, locks its rule as it was when the spin settled (not when processed), and counts at once. The ring appears then. A promotional spin adds nothing, so it assigns nothing.
- 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).
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 locks when a player starts the chase; a rule change applies only to players who start later | 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. 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. She submits it.
Step 2. Jordi finds it in the catalogue. Golden Cat stays Live while c-311 is in review, marked "Change in review · Review". Each row's menu has Retire and Pull and replace. An avatar never published shows Draft or In review; a live one stays Live and gets a "Change in review" chip.
Step 3. Jordi reviews and publishes. He sees the diff against revision 7, the checks, and what players will see: those already chasing Golden Cat keep 2,000,000 (#3, locked goal). One button, "Approve and publish", does both.
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 ("Win 3M coins", "Bet 20M coins").
Slot screen. One addition: the toast "Golden Cat unlocked" with Equip now / Later, after the server records the grant. It does not block play.
6 System parts
The building blocks and the data they keep
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.
- Avatar module (new): the back-office screens: catalogue, rule editor and review. It sends changes and publish requests to the Avatar API.
- Avatar API (new): player, admin and internal calls.
- Evaluator (new): reads each player's settled spins in order and saves each spin's results in one all-or-nothing step (#4, where it is decided).
- Avatar store (new): holds catalogue revisions, bars, ownership and the equipped choice.
| Entity | Key | Rule |
|---|---|---|
| Avatar | avatarId |
Rule fields only when earnable; a pulled avatar is retired with a pullReason |
| 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) |
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, GameOps role) and internal (service identity). The game never reports an award; the server decides (#4, where it is decided). The main calls:
Game
4| Method | Path | Purpose | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Live and retired avatars of the current revision | |||||||||||||||||
Purpose The game asks "has it changed since revision 7?"; if not, the server answers "not modified" and sends nothing (standard HTTP caching: the Response A higher Every publish raises the revision. Only a Pull and replace takes an avatar out of the catalogue; a retired avatar stays in it as Example { "revision": 7,
"avatars": [
{ "avatarId": 17, "name": "Golden Cat", "status": "live", "availability": "earnable",
"mode": "win", "unlockThreshold": 2000000, "imageUrl": "…/golden-cat?v=3" },
{ "avatarId": 9, "name": "Silver Wolf", "status": "retired", "availability": "earnable",
"mode": "win", "unlockThreshold": 1000000, "imageUrl": "…/silver-wolf?v=1" },
{ "avatarId": 44, "name": "Jade Tiger", "status": "live", "availability": "earnable",
"mode": "bet", "unlockThreshold": 20000000, "imageUrl": "…/jade-tiger?v=1" } ] }Each code is defined once, in A1. | ||||||||||||||||||
| GET | The player's state: owned, equipped, bars, pending notices | |||||||||||||||||
Purpose Login, app return and the avatar screen also fetch. No new connection is opened; an existing push channel may notify a closed app (assumed). Response
A mode with no target returns, for example,
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 A notice is a message the server keeps for the game until the game confirms it showed it.
Retry 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 Example { "owned": [ { "avatarId": 1, "source": "default" },
{ "avatarId": 9, "source": "rule_completion" },
{ "avatarId": 17, "source": "rule_completion" } ],
"equipped": { "avatarId": 9, "selectionVersion": 13 },
"bars": [ { "mode": "win", "targetAvatarId": 21, "unlockThreshold": 3000000,
"revision": 8, "amount": 200000 },
{ "mode": "bet", "targetAvatarId": 30, "unlockThreshold": 5000000,
"revision": 6, "amount": 2100000 } ],
"notices": [],
"pausedAvatarIds": [],
"processedThrough": { "gameId": "slot-42", "settledSpinId": "s-140", "playerSpinSequence": 1220 },
"catalogueRevision": 8 }Each code is defined once, in A1. | ||||||||||||||||||
| PUT | Equip an owned, equippable avatar | |||||||||||||||||
Purpose Equip an avatar the player owns. Request
Response
Errors Errors, checked in this order: Retry Safe. If the first call was saved, the retry returns Equip is keyed by Example Phone: PUT /v1/me/equipped-avatar { "avatarId": 9, "selectionVersion": 12 }
200 full player state (A1): equipped avatar 9, selectionVersion 13
Tablet, still on 12: { "avatarId": 17, "selectionVersion": 12 }
409 { "error": { "code": "SELECTION_CONFLICT",
"message": "The equipped avatar changed since you last loaded it", "requestId": "r-5501",
"details": { "equipped": { "avatarId": 9, "selectionVersion": 13 } } } }Each code is defined once, in A1. | ||||||||||||||||||
| POST | Mark a notice as shown | |||||||||||||||||
Purpose A notice is a message the server keeps for the game until the game confirms it showed it. Errors Retry Acknowledgements are keyed by Each code is defined once, in A1. | ||||||||||||||||||
Back office
3| Method | Path | Purpose |
|---|---|---|
| PUT | Edit a change; clears any approval | |
Purpose An edit clears the approval; Send back returns it to Draft. Request Retire and Pull and replace are change items too, published the same way. An edit sends the whole item list: A Retry Every successful edit, submit, approve, send back, discard and publish raises A Acknowledgements are keyed by Each code is defined once, in A1. | ||
| POST | Record the approver, in production not the author | |
Purpose "Approve and publish" makes two calls: approve, then publish. Retry Every successful edit, submit, approve, send back, discard and publish raises A
Only publish creates a revision. Approve and publish are separate calls. If Maria edits in between, the approval is cleared and the publish is refused. Each code is defined once, in A1. | ||
| POST | Validate and publish the approved version as the next revision | |
Purpose "Approve and publish" makes two calls: approve, then publish. Response Every successful edit, submit, approve, send back, discard and publish raises A Errors Approve and publish are separate calls. If Maria edits in between, the approval is cleared and the publish is refused. Retry
Only publish creates a revision. Example POST /admin/v1/changes/c-311/publish Idempotency-Key: k-8812 If-Match: "5"
200 { "changeId": "c-311", "revision": 8, "publishedBy": "jordi", "approvedBy": "jordi" }
If a check fails, nothing is published:
422 { "error": { "code": "VALIDATION_FAILED",
"message": "A publish check failed; nothing was published", "requestId": "r-7730",
"details": [ { "avatarId": 17, "field": "assetId",
"reason": "IMAGE_NOT_READY" } ] } }Each code is defined once, in A1. | ||
Spin system
1| Method | Path | Purpose | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Pending notices for the spin reply, under a short timeout | |||||||||||||||||
Purpose The Evaluator reads the record (assumed contract). The spin system adds pending notices to its reply as the new optional Reading notices adds a small, capped delay to the spin reply. Response
Errors Example GET /internal/v1/players/p-882/avatar-notices
200 { "notices": [ { "noticeId": "n-41", "type": "owned", "avatarId": 17 } ] }
503 { "error": { "code": "SERVICE_UNAVAILABLE",
"message": "The avatar service is unavailable", "requestId": "r-9120" } }
on 503 or no answer within the timeout, the spin reply goes without avatarNoticesEach code is defined once, in A1. | ||||||||||||||||||
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.
8 Earn and equip
How the game and server talk when Ana earns and equips
Earning. Ana 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).
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.
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 unlockThreshold from 2,000,000 to 3,000,000 in production |
she submits | diff against revision 7; "Players own Golden Cat after winning 3,000,000 coins" |
| Maria is the author | she approves | SELF_APPROVAL_NOT_ALLOWED |
| 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 unlockThreshold missing or at or below 0 coins |
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 |
| 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 |
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.
- Spin-system team: replayable records kept in a fixed order per player (
settledSpinIdunique per game, aplayerSpinSequencethat only goes up per player across games and devices,spinKind, per-spin increments),avatarNotices, speed and freshness targets. The spin reply also carriesgameId,settledSpinIdandplayerSpinSequence, to matchprocessedThrough(assumed). Fallback: the same evaluation inside the spin service after payout, one player at a time. - Back-office owner: GameOps role, second approver, senior permission for pause.
- Asset library: ready, versioned, removable images; default and replacement art.
- 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, pacing and every threshold before submitting. QA: boundaries, duplicate and late spins, replay order, two devices, load, on a settled-spin test feed.
- Legal: the Pull and replace message, and whether a won avatar counts as digital content.
With more time: a player-chosen goal and ladder order; a starter set; VIP gifts via grant_only.
Appendix
Full API contracts, more screens and edge cases
A1 API reference
Every endpoint, payload and error
OpenClose
A1 API reference
Every endpoint, payload and error
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.
Example: revision 7, the live catalogue before c-311 is published, with three of its avatars.Show the example · 8 linesHide the example
{ "revision": 7,
"avatars": [
{ "avatarId": 17, "name": "Golden Cat", "status": "live", "availability": "earnable",
"mode": "win", "unlockThreshold": 2000000, "imageUrl": "…/golden-cat?v=3" },
{ "avatarId": 9, "name": "Silver Wolf", "status": "retired", "availability": "earnable",
"mode": "win", "unlockThreshold": 1000000, "imageUrl": "…/silver-wolf?v=1" },
{ "avatarId": 44, "name": "Jade Tiger", "status": "live", "availability": "earnable",
"mode": "bet", "unlockThreshold": 20000000, "imageUrl": "…/jade-tiger?v=1" } ] }
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).
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 · 12 linesHide the example
{ "owned": [ { "avatarId": 1, "source": "default" },
{ "avatarId": 9, "source": "rule_completion" },
{ "avatarId": 17, "source": "rule_completion" } ],
"equipped": { "avatarId": 9, "selectionVersion": 13 },
"bars": [ { "mode": "win", "targetAvatarId": 21, "unlockThreshold": 3000000,
"revision": 8, "amount": 200000 },
{ "mode": "bet", "targetAvatarId": 30, "unlockThreshold": 5000000,
"revision": 6, "amount": 2100000 } ],
"notices": [],
"pausedAvatarIds": [],
"processedThrough": { "gameId": "slot-42", "settledSpinId": "s-140", "playerSpinSequence": 1220 },
"catalogueRevision": 8 }
owned leaves out pulled avatars. 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), unlockThreshold (coins, 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 unlockThreshold and revision, amount (with no target, carried progress) |
| 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, 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 endpoints
16Two headers keep changes safe:
If-Matchcarries thechangeVersionthe caller last saw, in quotes (If-Match: "5"). It is the valueGET /admin/v1/changes/{changeId}returns, also as itsETag. Edit, submit, approve, send back, publish and discard all carry it. If the change moved on since, the call fails with 412PRECONDITION_FAILED.Idempotency-Keyis 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 andIf-Match; reusing it with different input returns 400INVALID_REQUEST.
An action the change's state does not allow (for example, editing a published change) returns 409 CHANGE_CONFLICT.
Change lifecycle.
- Every successful edit, submit, approve, send back, discard and publish raises
changeVersionby 1 and returns it as the newETag; publish sends the version that approve returned. - A
publishedchange is closed and read-only. - A draft or in-review change can be discarded with
If-Match. It closes asdiscarded, audited. - "Approve and publish" makes two calls: approve, then publish.
- After an avatar's first publish, its
modeandavailabilityare fixed; to change them, create a new avatar and retire the old one. Only the default avatar isdefault.
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, "unlockThreshold": 3000000 } ] } returns 200 with the change and ETag: "3". A create item sends name, availability, assetId and imageVersion, plus mode and unlockThreshold 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 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.
| Method | Path | Auth | Safe to retry? | Purpose |
|---|---|---|---|---|
| GET | GameOps | Yes (read) | Catalogue list, newest first: status, availability, mode, unlockThreshold, updatedAt (latest change or revision touching it), open changeId and its status, live revision | |
Response An avatar never published shows Draft or In review; a live one stays Live and gets a "Change in review" chip. Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | Search the existing asset library; returns assetId, imageVersion, ready state | |
Response Asset library: ready, versioned, removable images; default and replacement art. Each code is defined once, in A1. | ||||
| POST | GameOps | Idempotency-Key | Open a change on the live revision (baseRevision) | |
Purpose Maria opens a change (Draft) → submits it (In review; checks run) → another GameOps user approves it → it goes live as the next revision. Request Opening a change, Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | The change with changeVersion, items and status | |
Response Every successful edit, submit, approve, send back, discard and publish raises A Errors Each code is defined once, in A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match | draft → in_review; runs validation | |
Purpose Maria opens a change (Draft) → submits it (In review; checks run) → another GameOps user approves it → it goes live as the next revision. Retry Every successful edit, submit, approve, send back, discard and publish raises A
Only publish creates a revision. Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | Diff against live, preview sentences, validation results | |
Purpose He sees the diff against revision 7, the checks, and what players will see: those already chasing Golden Cat keep 2,000,000 (#3, locked goal). Response For GameOps: what the review screen shows below the diff and approval. Errors Each code is defined once, in A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match | in_review → draft, with a comment | |
Purpose An edit clears the approval; Send back returns it to Draft. Request Send back sends Retry Every successful edit, submit, approve, send back, discard and publish raises A
Only publish creates a revision. Each code is defined once, in A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match | Close a draft or in_review change as discarded, audited; stale version returns 412 | |
Purpose A draft or in-review change can be discarded with Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | Version history | |
Response Columns: Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | One revision's full content | |
Errors Each code is defined once, in A1. | ||||
| POST | GameOps | Idempotency-Key | Open a change that restores that revision; normal review and lifecycle validation | |
Purpose Republish on a row opens a change that restores that revision, diffed against live, with normal review and lifecycle validation. Response 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." Errors Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| POST | Senior | Idempotency-Key | Rule pause: the rule keeps crediting progress, grants nothing and is not assigned as a new target, until resumed | |
Response It applies to spins processed after it takes effect. A paused rule still credits progress, but grants nothing and is not assigned as a new target. The picker shows it greyed as "Paused".
Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| POST | Senior | Idempotency-Key | End the pause: make the due grants against the corrected record, then assign the rule again | |
Purpose Audited corrections run during the pause. 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. Response Each grant is one save, as for a completing spin: ownership, the Errors Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| POST | Senior | Idempotency-Key | Propose an audited player-state correction for one rule, run while it is paused | |
Purpose Audited corrections run during the pause. Request A correction, Either returns 201 with its Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| POST | Senior, not the proposer | Idempotency-Key | Record the second approval; the correction then runs | |
Errors Retry
Only publish creates a revision. Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | Audit log, filtered by avatar, user, action or time | |
Purpose For GameOps: what the review screen shows below the diff and approval. Request Filters: avatar, user, action, date range. Response 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, Read-only. Kill-switch use is audited in the feature-flag tool. Each code is defined once, in A1. | ||||
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. 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 or publish; each detail names field and reason: IMAGE_NOT_READY, THRESHOLD_OUT_OF_BOUNDS (missing or not above 0 coins), MODE_MISSING, 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, field, reason}; other codes have none.
Retries. Each repeat below is matched by its key ("keyed by"), so it never applies twice.
- Settled spins are keyed by (
gameId,settledSpinId). Each counts once. - Equip is keyed by
selectionVersion. A stale retry that passes the earlier checks getsSELECTION_CONFLICTwith the current choice. The game treats it as success if that is the requested avatar, and otherwise redraws. - Acknowledgements are keyed by
noticeId, admin edits bychangeVersion. - Admin
POSTs are keyed byIdempotency-Key. - Only publish creates a revision.
Versions. / 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
A2 Version history and audit log
What the review screen shows below the diff
For GameOps: what the review screen shows below the diff and approval.
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. 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
A3 Edge cases
Retire, pull, pause and late spins
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).
- The avatar stays in the catalogue as
retired. Owners keep it and can equip it. The picker shows it only to owners. - 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.
- After those spins, the unfinished
amountmoves to that mode's bar, seeds the next target and sendsprogress_moved. - 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).
- It is a change item with a
pullReasonand a livegrant_onlyreplacementAvatarId. It needs a second approval, as any production change does, and is audited. - On publish, the avatar becomes
retiredwith itspullReasonand 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. - Its ID, name, status, ownership and audit history stay.
- 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
replacementnotice instead ofowned, 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
ownednotice for the pulled avatar; a game that already received one stops showing it once it loads the new catalogue revision.
- Progress moves as for Retire.
- No coin compensation. Deleting offline copies is not promised.
- In an emergency before approval, the kill switch hides the whole feature.
Rule pause (senior, audited).
- It applies to spins processed after it takes effect.
- A paused rule still credits progress, but grants nothing and is not assigned as a new target. The picker shows it greyed as "Paused".
- Audited corrections run during the pause.
- 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.
- Each grant is one save, as for a completing spin: ownership, the
ownednotice, 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 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 |

