Main document
Part 1All three areas
Problem and answer
The problem, the design on one page, and how we would judge it
Recommendation. Make avatars server-owned rewards: GameOps (the team that runs the game live) publishes reviewed rules, the server credits progress from settled spins (spins whose payout is recorded) after the coins are paid, and the game shows the unlock and lets the player equip it. Players choose from the pictures they own: the default picture everyone has, avatars earned by play, and, rarely, a replacement GameOps grants when an avatar is pulled.
Where the brief is answered.
- Back-office UI: Part 3 (Figures 2–4a), Story 1 in Part 7, A2, and the A3 lifecycle actions.
- APIs: Parts 4 and 5, and the reference in A1.
- Game and server interaction: Parts 2 and 6, Stories 2 and 3, and the A3 edge cases.
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.
The problems. Player problem (assumed): players lack an identity earned through play. GameOps: catalogue changes need no game release, and a wrong threshold must not reach players unchecked.
Six decisions and what they rest on (full table in Part 2)
| Decision | Rests on (confirmed / assumed) |
|---|---|
| Players chase one avatar per mode at a time; after an unlock the next bar starts from zero, and progress carries over only when GameOps removes the target (#1, ladder) | One big win should not unlock the whole catalogue |
| A bet counts the paid stake once; a win counts each spin's credited win (#2, what counts) | Progress is cumulative (confirmed); it counts on every slot game, "win X coins" is the credited win gross of stake, and promotional spins add nothing |
| A player's target and threshold are fixed when the chase starts (#3, pinned terms) | A goal does not move while a player chases it |
| Awards are decided after the spin has paid, from the settled-spin record (#4, where it is decided) | Replayable settled-spin records in per-player order; spin settlement does not wait on the avatar service |
| Retiring an avatar leaves it with its owners; a rare Pull and replace gives owners a named replacement (#5, taking an avatar out) | Only legal or offensive content justifies taking an owned avatar away |
| Changes go live as reviewed revisions, with a second approver in production and a kill switch (feature off) (#6, publication) | GameOps has a back-office role (confirmed); a second approver and a feature-flag tool exist |
(confirmed) = confirmed with the hiring team, 2026-11-05; everything else in the right column is assumed.
Golden path. GameOps publishes Golden Cat (win 2,000,000) → Ana's settled spin completes her bar → server records ownership and the notice → "Golden Cat unlocked" → Equip now.
Goals. Player: show who I am and earn something lasting from play. Business: more players returning and picking an avatar, from a reward that costs no coins, measured against a holdout (players kept without the feature). Targets to watch, not results: (1) adoption: share of active players with a non-default avatar equipped at day 30; (2) median paid spins to first unlock, per threshold; (3) return visits: day-7 return of players in the rollout against the holdout. Guardrails: spend and session length not above the holdout. Day-7 return of players with an unlock is explored separately; it is not the rollout-versus-holdout result, because players who unlock already play more. Responsible play: progress is not shown as a near-win, and thresholds pass the economy and responsible-play owner before publish.
Part 2Game and server
How unlocking works
Progress rule and the six consequential decisions
The rule. Each earnable avatar has one rule: a mode (bet or win) and an
unlockThreshold in coins. The player's bar adds up what counts (table below). When it reaches
the threshold (inclusive), the player owns the avatar, once. Cumulative progress is confirmed
with the hiring team, 2026-11-05. Two choices are mine: spins on every slot game in the app
count (assumed), and "win X coins" means what the table defines.
Settled spin (spinKind) |
Bet mode adds | Win mode adds the credited win (gross, not net of stake) |
|---|---|---|
Paid spin (paid) |
the paid stake, once | its credited win |
Free spin or feature it triggered (triggered_feature) |
nothing | each spin's credited win |
Bought feature (bought_feature) |
the purchase price, once | each free game's credited win |
Promotional free spin (promotional) |
nothing | nothing (assumed) |
Increments are read per spin, not also as a round summary; a bought feature's price sits on its purchase settlement only. Golden Cat (win 2,000,000), bar at 1,200,000: a credited win of 799,999 leaves it unowned; 800,000 or 900,000 owns it, the extra 100,000 not carried.
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). Until the player's next settled spin it shows as "next up": a locked tile with its rule. - Assignment: that spin assigns the target, pins its terms and credits it at once, using the revision that was live when the spin settled, not when it is processed. The ring appears from that spin.
- Fresh bars: a new bar starts at zero, except progress carried over when GameOps removes the current target. So one spin completes at most one avatar per mode.
Six decisions that commit the design (cited elsewhere by number, #1–#6)
| Decision | Chosen | Rejected | Why | Cost | Hard to reverse? |
|---|---|---|---|---|---|
| Players chase one avatar per mode at a time; after an unlock the next bar starts from zero, and progress carries over only when GameOps removes the target | |||||
| #1 Ladder | One target per mode; bars from zero | All rules at once; lifetime counter | One big win does not sweep the catalogue | Overflow discarded | Yes: progress |
| A bet counts the paid stake once; a win counts each spin's credited win | |||||
| #2 What counts | Credited win per spin; paid stake once | Round total; net win | No overlap, no round boundary | spinKind on every record |
Yes: contributions |
| A player's target and threshold are fixed when the chase starts | |||||
| #3 Pinned terms | Target and threshold fixed at assignment | Latest rule | Stable goals; replays match | Fixes need an audited correction | Yes: pins |
| Awards are decided after the spin has paid, from the settled-spin record | |||||
| #4 Where it is decided | After settlement, from the settled-spin record | Inside the spin call | Spin settlement does not wait on the avatar service | Award lags the coins | Medium: integration |
| Retiring an avatar leaves it with its owners; a rare Pull and replace gives owners a named replacement | |||||
| #5 Taking an avatar out | Retire; rare Pull and replace for legal or offensive content | Taking it from owners without a replacement; coins | Owners keep what they earned, or get a named replacement | Replacement art | Pull: yes |
| Changes go live as reviewed revisions, with a second approver in production and a kill switch (feature off) | |||||
| #6 Publication | Reviewed revisions; second approver in production | Live edits | Checked changes with a preview | Slower changes (a kill switch, feature off, for emergencies) | No: workflow |
Product assumption. Every account starts with the default avatar (today's silhouette), owned and equipped; all others are earned, and earning does not equip ("Equip now").
Part 3Back-office UI
Back office
Back-office UI and the player screens it drives
Sample data of Parts 5–7: Maria edits, Jordi approves, Ana plays. Back-office numbers are full (2,000,000), player numbers compact (1.4M / 2M); rings take the mode colour, win amber and bet blue.
Change workflow. Maria opens a change on the live revision (Draft) → submits it (In review; checks run) → a second GameOps user approves it (in production, not its author) → it is published as revision N+1. An edit clears the approval; Send back returns it to Draft.
Catalogue (Jordi, production). Last updated first. Golden Cat stays Live while c-311 is in review, with a "Change in review · Review" chip; each row's menu holds Retire and Pull and replace (#5, taking an avatar out).
Rule editor (Maria). An image from the asset library, availability, and the unlock rule only for Earnable; the right column states the rule in Part 7's words. With c-311 in review, "Save changes" keeps it In review and clears its approval (#6, publication).
Review (Jordi). Diff against revision 7, checks, and what players see: those already chasing Golden Cat keep their pinned 2,000,000 (#3, pinned terms). "Approve and publish" is A1's one control; the author's approval is refused, and the second approver applies in production only. Version history and audit log: Figure 4b in A2.
Part 3Game and server
Player screens
Avatar picker · Slot screen (Figures 5–7)
Avatar picker (Ana; a dialog over Settings; full rules in A2). It shows what Ana owns and the
live earnable avatars, so grant-only Phoenix (given only by GameOps; in this version only as a
Pull and replace replacement) is hidden. A ring marks each current target, one per mode; other
earnable avatars show their rule ("Win 3M coins", "Bet 20M coins"). An avatar whose rule is paused
(pausedAvatarIds, A1) is greyed with "Paused" until resume.
- Figure 5: Golden Cat 1.4M / 2M (win) and Lucky Dog 1.7M / 5M (bet).
- Figure 6, a few spins later (after A1's sample state): Golden Cat is owned, with a NEW badge. Blue Owl is the new win target; its bar started from zero at the next settled spin and reads 0.2M / 3M (#1, ladder). Lucky Dog reads 2.5M / 5M.
Slot screen. One addition to the existing screen: the toast "Golden Cat unlocked" with Equip now / Later, once the server has recorded the grant (Part 6). The toast does not block play.
Part 4APIs
System parts
Logical components and small data model
Figure 1 shows these components: new parts blue, existing grey. Catalogue changes need no game release: the game reads the catalogue from the Avatar API. The design builds on the existing spin system (confirmed with the hiring team, 2026-11-05); the notice read adds a bounded overhead to the spin reply.
- Avatar module (new): the back-office screens GameOps uses for the catalogue, the rule editor and the review; it sends changes and publish requests to the Avatar API, which searches the asset library, checks that images are ready and stops serving pulled images.
- Avatar API (new): the player, admin and internal calls of Part 5.
- Evaluator (new): reads settled-spin records in each player's settled order and makes one all-or-nothing save per spin: credits, assignments, grants and notices (#4, where it is decided).
- Avatar store (new): where catalogue revisions, bars, ownership and the equipped choice are saved, each update in one all-or-nothing save.
| Entity | Key | Main fields | Rule |
|---|---|---|---|
| Avatar | avatarId |
status, availability, mode, unlockThreshold, assetId, pullReason |
Rule fields only when earnable; a pulled avatar is retired with a pullReason |
| Change → revision | changeId; revision |
changeVersion, baseRevision, approvedBy, items |
In production the approver is not the author; a revision is not edited and keeps its effective time |
| Ownership | (playerId, avatarId) |
source, grantedAt |
One row per player and avatar, kept after Retire and Pull and replace |
| Selection | playerId |
equippedAvatarId, selectionVersion |
Owned and equippable (live, or retired and not pulled); +1 per change |
| Bar | (playerId, mode) |
targetAvatarId, pinned unlockThreshold and revision, amount |
Pinned at assignment; only an audited correction may lower it |
| Spin processing | (gameId, settledSpinId) |
increments, assignments, grants | One all-or-nothing save with bars, grants and notices; kept while the record can be replayed (period agreed with the spin team, dependency 1) |
Notices (owned, replacement, progress_moved) stay pending until acknowledged; audit
entries (who, what, when, before, after) cover every change, approval, publish and pause;
kill-switch use is audited in the feature-flag tool. API values are lowercase; the back office shows Draft, In review, Live and
Retired, with the actions Create, Edit, Retire and Pull and replace. All fields are in A1.
Part 5APIs
APIs
APIs: contracts with representative requests, responses and errors
The Avatar API has three call classes: player calls take the player from the session; admin calls use the back-office login and the existing GameOps role (confirmed with the hiring team, 2026-11-05); internal calls use a service identity. No call lets the game report an award: the server decides (#4, where it is decided). The main calls at a glance (every endpoint, payload and error in A1):
Game
4| Method | Path | Purpose | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Live and retired avatars of the current revision (ETag = revision) | |||||||||||||
Purpose
Response It lists every live and retired avatar; pulled ones are absent. A newer Once published, the avatar is 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" } ] }Error definitions: A1. | ||||||||||||||
| GET | The player's state: owned, equipped, bars, pending notices | |||||||||||||
Purpose Login, app return and the avatar screen fetch too; no new connection is added, and an existing push channel may notify a closed app (assumed). Response
A mode with no target returns, for example,
If that spin is not in the game's local history (for example, it was played on another device), the game counts every eligible contribution since it received that state. A newer Each has a stable
Retry At the pinned threshold, or the first eligible spin of a mode with no target, it refreshes from the Avatar API after a short delay and a few times more with growing gaps; each answer rebases the estimate. 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" },
"catalogueRevision": 8 }Error definitions: A1. | ||||||||||||||
| PUT | Equip an owned, equippable avatar (below) | |||||||||||||
Purpose Equip an avatar the player owns and may equip. The server decides; the reply is the full player state. Request
Response
Errors Errors, checked in this order:
Retry Safe. A retry runs the usual checks in order. If the first call was saved and the avatar is still equippable, 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 } } } }Error definitions: A1. | ||||||||||||||
| POST | Mark a notice as shown | |||||||||||||
Purpose Each has a stable Errors
Retry Acknowledgements are keyed by Error definitions: A1. | ||||||||||||||
Back office
3| Method | Path | Purpose |
|---|---|---|
| PUT | Edit a change's items (If-Match: changeVersion); clears an approval | |
Purpose With c-311 in review, "Save changes" keeps it In review and clears its approval (#6, publication). Request Retire and Pull and replace are change items ( An image from the asset library, availability, and the unlock rule only for Earnable; the right column states the rule in Part 7's words. Errors
Retry
Acknowledgements are keyed by Error definitions: A1. | ||
| POST | Record the approver, in production not the author | |
Purpose The back office's one "Approve and publish" control makes two calls: approve, then publish. "Approve and publish" is A1's one control; the author's approval is refused, and the second approver applies in production only. Errors
Retry
Every admin Approve and publish are separate calls; an edit between them clears the approval. Error definitions: A1. | ||
| POST | Validate and publish the approved version as the next revision | |
Purpose The back office's one "Approve and publish" control makes two calls: approve, then publish. Response
Errors
Approve and publish are separate calls; an edit between them clears the approval. A publish sent with the old Retry Every admin 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" } ] } }Error definitions: A1. | ||
Spin system
1| Method | Path | Purpose | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Pending notices for the spin reply, under a short timeout | |||||||||||||
Purpose The Evaluator consumes the record (an assumed contract, dependency 1 in Part 7); the spin system adds the pending notices to its reply as the new optional The design builds on the existing spin system (confirmed with the hiring team, 2026-11-05); the notice read adds a bounded overhead 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 avatarNoticesError definitions: A1. | ||||||||||||||
PUT /v1/me/equipped-avatar · Game · Session (the player comes from the session, not the
body)
Purpose. Equip an avatar the player owns and may equip. The server decides; the reply is the full player state.
| Field | Type | Required | Meaning |
|---|---|---|---|
avatarId |
number | yes | Avatar to equip |
selectionVersion |
number | yes | Version the device last saw; stale → SELECTION_CONFLICT |
Response 200: player state (A1): owned, equipped {avatarId, selectionVersion +1},
bars, notices, pausedAvatarIds, processedThrough, catalogueRevision.
Errors, checked in this order: 404 AVATAR_NOT_FOUND → 403 AVATAR_NOT_OWNED → 409 AVATAR_NOT_EQUIPPABLE → 409 SELECTION_CONFLICT.Show 4 error codes with what the game doesHide the error codes
| HTTP | Code | When | Game does |
|---|---|---|---|
| 404 | AVATAR_NOT_FOUND |
Unknown avatarId |
Refetch the catalogue |
| 403 | AVATAR_NOT_OWNED |
Not owned | Show it locked |
| 409 | AVATAR_NOT_EQUIPPABLE |
Owned, but pulled | Refetch catalogue and state |
| 409 | SELECTION_CONFLICT |
Stale selectionVersion; details.equipped = {avatarId, selectionVersion} |
Redraw from details |
Also: 400 INVALID_REQUEST, 401 UNAUTHENTICATED, 503 SERVICE_UNAVAILABLE (A1).
Retry. Safe. A retry runs the usual checks in order. If the first call was saved and the
avatar is still equippable, the retry returns SELECTION_CONFLICT, treated as success when
details.equipped matches the request (Figure 9, step 4); if the avatar was pulled in between,
it returns AVATAR_NOT_EQUIPPABLE. If the first call was not saved, the retry succeeds only if
its selectionVersion is still current and the avatar is still equippable. Any other
SELECTION_CONFLICT redraws from details.
Example: the phone saves avatar 9 as version 13; the tablet, still on 12, gets 409 SELECTION_CONFLICT with the current choice.Show the example · 6 linesHide the 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 } } } }
requestId is for diagnosis.
Admin: publish a change. Retire and Pull and replace are change items (retire;
pull_and_replace with replacementAvatarId and pullReason), published the same way.
Approve and publish are separate calls; an edit between them clears the approval. A publish
sent with the old changeVersion then returns 412 PRECONDITION_FAILED, and one sent with the
new version returns CHANGE_CONFLICT, because that version was never approved. Maria authored c-311; Jordi approved it and publishes it:
Example: Jordi publishes c-311 as revision 8; if a check fails, nothing is published and 422 VALIDATION_FAILED names the field.Show the example · 7 linesHide the 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" } ] } }
Internal: the settled-spin record and the notice read. The Evaluator consumes the record
(an assumed contract, dependency 1 in Part 7); the spin system adds the pending notices to its
reply as the new optional avatarNotices.
Example: the settled-spin record of s-123, then the notice read that adds n-41 to the spin reply, or nothing on 503 or a timeout.Show the example · 7 linesHide the example
Record: gameId slot-42 · settledSpinId s-123 · playerId p-882 · settledAt 2026-11-06T10:15:02.120Z
spinKind paid · paidStake 200000 · creditedWin 900000
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 avatarNotices
Errors. Domain codes: SELF_APPROVAL_NOT_ALLOWED, AVATAR_NOT_FOUND, AVATAR_NOT_OWNED,
AVATAR_NOT_EQUIPPABLE (owned but pulled), SELECTION_CONFLICT, CHANGE_CONFLICT,
PRECONDITION_FAILED (412, stale If-Match), VALIDATION_FAILED (each detail names field and
reason) and SERVICE_UNAVAILABLE.
Retry keys and versioning: A1.
Part 6Game and server
Earn and equip
Game and server interaction: earning an avatar and equipping one, with timeout and retry
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 does not announce an award before the server has recorded it; how it estimates the bar
and when it refreshes are in A1. The 300,000 overflow is not carried; Blue Owl is assigned at
Ana's next settled spin. The game estimates each bar from the spins it has seen, so it asks the
server only when an unlock is likely, with no new connection. The award can reach it two ways
because either may come first: the short-delay refresh once the estimate reaches the threshold,
or avatarNotices on the next spin reply if Ana spins again before that.
Equipping. Ana uses a phone and a tablet, both at selectionVersion 12.
When things go wrong
| Case | Server | Player sees |
|---|---|---|
| Same settled spin twice | The processing record stops a second credit | Repeated noticeId skipped |
| Evaluator down; notice read times out | Spins pay as today; records processed in settled order on recovery; lag alarm | Toast with a later reply, refresh or fetch |
| Final spin completes a target | Grant and notice stored | The refresh shows the toast, or else the next fetch |
Fetch fails, or the catalogue is empty, older, or lacks an avatar that owned, equipped or a bar names (checked against the state returned with or after it) |
Keep the last valid cache; retry | Cached or default picture; Save waits |
A rule change, a Retire and a Pull and replace, as players see them: A3.
Part 7All three areas
Acceptance tests
Acceptance criteria, release controls, measures and open dependencies
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 pinned at 2,000,000, bar 1,200,000 | credited win 799,999 / 800,000 / 900,000 | not owned / owned / owned; overflow discarded |
| Lucky Dog pinned 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, in the last row).
| 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 and no owned notice; the cat leaves the catalogue and owned and is AVATAR_NOT_EQUIPPABLE |
Part 7All three areas
Launch and measures
Release controls · Measures
Release controls.
| Control | Run by | Effect |
|---|---|---|
| Staged rollout: internal, 1%, 10%, 50%, then everyone except the holdout (Part 1) | The existing feature-flag tool (assumed) | Advances while the health signals hold (A3); bars start at a player's first settled spin inside it |
| Rule pause | A senior GameOps user, audited | Progress keeps counting, but the rule grants nothing and is not assigned as a new target until resumed (A3) |
| Kill switch (feature off) | The game's on-call engineer, in the existing feature-flag tool (assumed) | Hides the avatar screen, bars and toasts and shows the default picture; ownership, selection and processing carry on |
| Notice read off | The game's on-call engineer, in the existing feature-flag tool (assumed) | The spin reply goes without avatarNotices; notices stay pending until the next player-state fetch |
Measures. Goals, the three metrics, guardrails and responsible play: Part 1. A threshold with under 1% unlocks in two weeks is reviewed.
Part 7All three areas
Dependencies
Dependencies, in order · With more time
Dependencies, in order. The server team builds the Avatar API, Evaluator and Avatar store once dependency 1 is agreed; the rest work as in dependency 4.
- Spin-system team: durable, replayable records in a deterministic per-player order
(
settledSpinIdunique per game,spinKind, per-spin increments),avatarNotices, latency and freshness targets. The spin reply also gives the game the spin'sgameIdandsettledSpinId, so it can 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 (Part 3) and the back-office Avatar module, built against a stub of the Part 5 contracts. GameOps with the economy and responsible-play owner: first catalogue, pacing, and every threshold agreed before the change is submitted. 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 manual ladder order; a free starter set and support
or VIP gifts through grant_only avatars.
Appendix
A1APIs
API reference
API reference: payloads, endpoints, errors
Payloads and errors behind Part 5. "Session" = the signed-in player; "GameOps" = the existing back-office role; "senior" = the senior GameOps permission; "service" = service identity.
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, the live catalogue before c-311 is published, holds seven avatars, three shown above: the default avatar (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) and Jade Tiger (44, bet 20,000,000).
Player state (the 200 of GET /v1/me/avatars and of equip in Part 5):
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" },
"catalogueRevision": 8 }
owned leaves out pulled avatars. equipped.avatarId is the stored equippedAvatarId.
pausedAvatarIds lists the avatars whose rule is paused (A3); it is in the player state, not the
catalogue, because a pause creates no revision and the catalogue is cached by revision. A mode
with no target returns, for example, { "mode": "win", "nextUpAvatarId": 21, "amount": 0 };
nextUpAvatarId is derived at read time and copied into a progress_moved notice.
processedThrough is the last settled spin, in the player's order, that the state includes; the
game keeps the contributions it saw after it. If that spin is not in the game's local history
(for example, it was played on another device), the game counts every eligible contribution
since it received that state. A newer catalogueRevision than the cached one makes the game refetch the catalogue.
The game estimates each bar locally and asks the server only when an unlock is likely; a refresh or the next spin reply brings the award.Show the estimate and refresh rule · 1 paragraphHide the rule
Client estimate and refresh (behind Part 6). The game estimates each mode as the confirmed
amount (the bar's progress in the last state) plus eligible contributions after
processedThrough. At the pinned threshold, or the first eligible spin of a mode with no
target, it refreshes from the Avatar API after a short delay and a few times more with growing
gaps; each answer rebases the estimate. It skips a repeated noticeId while its local history
lasts. Login, app return and the avatar screen fetch too; no new connection is added, and an
existing push channel may notify a closed app (assumed).
Notices. Each has a stable noticeId, player, type and avatar IDs, is saved with the
change that caused it and stays pending until acknowledged:
| Type | Fields | Keyed by |
|---|---|---|
owned |
avatarId |
the grant (a replacement grant creates the replacement notice instead of an owned one) |
replacement |
avatarId, replacedAvatarId |
player, pulled avatar and pull revision, also when the replacement was already owned |
progress_moved |
removedAvatarId, mode, amount, nextUpAvatarId |
player, removed target and revision |
Settled-spin record. paidStake, like creditedWin, is made of non-overlapping
increments: a triggered free spin carries 0; a bought feature's price appears only on its
purchase settlement (bought_feature), its free games are bought_feature records with
paidStake 0, and no paid spin includes it.
All model fields.
| Entity | Fields |
|---|---|
| Avatar | avatarId (number, not reused), name, status (draft, in_review, live, retired), availability (default, earnable, grant_only; shown as Default, Earnable, Grant only), mode (bet, win), unlockThreshold (coins, inclusive), assetId, imageVersion, firstPublishedAt, pullReason (legal, offensive), paused (rule pause; sent to players as pausedAvatarIds in the player state) |
| Change | changeId, status (draft, in_review, published, discarded), changeVersion, baseRevision, authorId, 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, pinned unlockThreshold and revision, amount (with no target, carried progress) |
| Spin processing | gameId, settledSpinId, playerId, processedAt, increments, assignments, grants |
Change lifecycle. changeVersion +1 on every PUT edit only; submit, approve and
send-back keep it; a published change is closed and read-only (status published). A change in
draft or in review can be discarded, with If-Match like an edit, which closes the change
(status discarded, audited). The back office's one "Approve and publish" control makes two calls: approve, then
publish.
Game endpoints
4| Method | Path | Auth | Safe to retry? | Purpose | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Session | Yes (read, ETag = revision) | Live and retired avatars of the current revision; pulled ones left out | |||||||||||||
Purpose
Response It lists every live and retired avatar; pulled ones are absent. A newer Once published, the avatar is 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" } ] }Error definitions: A1. | ||||||||||||||||
| GET | Session | Yes (read) | Player state (above) | |||||||||||||
Purpose Login, app return and the avatar screen fetch too; no new connection is added, and an existing push channel may notify a closed app (assumed). Response
A mode with no target returns, for example,
If that spin is not in the game's local history (for example, it was played on another device), the game counts every eligible contribution since it received that state. A newer Each has a stable
Retry At the pinned threshold, or the first eligible spin of a mode with no target, it refreshes from the Avatar API after a short delay and a few times more with growing gaps; each answer rebases the estimate. 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" },
"catalogueRevision": 8 }Error definitions: A1. | ||||||||||||||||
| PUT | Session | Yes, by selectionVersion | Equip an owned, equippable avatar; returns the player state | |||||||||||||
Purpose Equip an avatar the player owns and may equip. The server decides; the reply is the full player state. Request
Response
Errors Errors, checked in this order:
Retry Safe. A retry runs the usual checks in order. If the first call was saved and the avatar is still equippable, 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 } } } }Error definitions: A1. | ||||||||||||||||
| POST | Session | Yes, by noticeId | Mark a notice as shown | |||||||||||||
Purpose Each has a stable Errors
Retry Acknowledgements are keyed by Error definitions: A1. | ||||||||||||||||
Back-office endpoints
19If-Match carries the change's changeVersion, as
GET /admin/v1/changes/{changeId} returns it (also as its ETag), sent quoted: If-Match: "5";
a stale version returns 412 PRECONDITION_FAILED; lifecycle conflicts return 409 CHANGE_CONFLICT
| Method | Path | Auth | Safe to retry? | Purpose |
|---|---|---|---|---|
| GET | GameOps | Yes (read) | Catalogue list, newest first: status, availability, mode, unlockThreshold, updatedAt (derived from the latest change or revision touching it), the open changeId with its status, live revision | |
Response The catalogue lists avatars last updated first. A live avatar with an edit under review keeps its Live label and shows "Change in review"; Draft and In review appear as labels only for an avatar not yet published. Error definitions: 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. Error definitions: A1. | ||||
| POST | GameOps | Idempotency-Key | Open a change on the live revision (baseRevision) | |
Purpose Maria opens a change on the live revision (Draft) → submits it (In review; checks run) → a second GameOps user approves it (in production, not its author) → it is published as revision N+1. Retry Every admin Error definitions: A1. | ||||
| GET | GameOps | Yes (read) | The change with changeVersion, items and status | |
Response
Errors
Error definitions: A1. | ||||
| PUT | GameOps | If-Match: changeVersion | Edit items: create, edit, retire, pull_and_replace (with replacementAvatarId, pullReason); clears an approval | |
Purpose With c-311 in review, "Save changes" keeps it In review and clears its approval (#6, publication). Request Retire and Pull and replace are change items ( An image from the asset library, availability, and the unlock rule only for Earnable; the right column states the rule in Part 7's words. Errors
Retry
Acknowledgements are keyed by Error definitions: A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match | draft → in_review; runs validation | |
Purpose Maria opens a change on the live revision (Draft) → submits it (In review; checks run) → a second GameOps user approves it (in production, not its author) → it is published as revision N+1. Errors
Retry
Every admin Error definitions: A1. | ||||
| GET | GameOps | Yes (read) | Diff against live, preview sentences, validation results | |
Purpose Diff against revision 7, checks, and what players see: those already chasing Golden Cat keep their pinned 2,000,000 (#3, pinned terms). Response The version history and the audit log are shown on the review screen, below its diff and approval (Figure 4a, Part 3). Errors
Error definitions: A1. | ||||
| POST | GameOps, not the author (production) | Idempotency-Key, If-Match | Record approvedBy | |
Purpose The back office's one "Approve and publish" control makes two calls: approve, then publish. "Approve and publish" is A1's one control; the author's approval is refused, and the second approver applies in production only. Errors
Retry
Every admin Approve and publish are separate calls; an edit between them clears the approval. Error definitions: 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. Errors
Retry
Every admin Error definitions: A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match: changeVersion | Validate and publish as the next revision; publishes only the version that was approved, otherwise CHANGE_CONFLICT | |
Purpose The back office's one "Approve and publish" control makes two calls: approve, then publish. Response
Errors
Approve and publish are separate calls; an edit between them clears the approval. A publish sent with the old Retry Every admin 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" } ] } }Error definitions: A1. | ||||
| POST | GameOps | Idempotency-Key, If-Match | From draft or in_review: closes the change; status discarded, audited; a stale version returns 412 | |
Purpose A change in draft or in review can be discarded, with Errors
Retry Every admin Error definitions: A1. | ||||
| GET | GameOps | Yes (read) | Version history (A2) | |
Response Columns: Error definitions: A1. | ||||
| GET | GameOps | Yes (read) | One revision's full content | |
Errors
Error definitions: A1. | ||||
| POST | GameOps | Idempotency-Key | Open a change that restores that revision; normal review and lifecycle validation (A2) | |
Purpose Each row has Republish, which opens a change restoring that revision; it shows the diff against the live revision and goes through the normal review. Response Republish passes the normal lifecycle validation and does not restore a pulled avatar or reverse its image removal; existing ownership and pinned targets remain unchanged. Errors
Retry Every admin Error definitions: 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 A pause applies to spins the server processes after it takes effect. A paused rule keeps crediting progress but grants nothing and is not assigned as a new target; the picker greys it with "Paused" (
Errors
Retry Every admin Error definitions: A1. | ||||
| POST | Senior | Idempotency-Key | Rule pause: the rule keeps crediting progress, grants nothing and is not assigned as a new target, until resumed | |
Purpose At resume, a per-player step keyed by player, avatar and resume makes the due grants, so a retry never grants twice. Response Grants that fall due during the pause are made at resume against the corrected record; audited corrections run while the rule is paused. The step runs in the player's processing order (dependency 1), and each grant is one save, as for a completing spin: ownership, the Errors
Retry Every admin Error definitions: A1. | ||||
| POST | Senior | Idempotency-Key | Propose an audited player-state correction for one rule, run while it is paused | |
Purpose Grants that fall due during the pause are made at resume against the corrected record; audited corrections run while the rule is paused. Retry Every admin Error definitions: A1. | ||||
| POST | Senior, not the proposer | Idempotency-Key | Record the second approval; the correction then runs | |
Errors
Retry Every admin Error definitions: A1. | ||||
| GET | GameOps | Yes (read) | Audit log, filtered by avatar, user, action or time (A2) | |
Purpose The version history and the audit log are shown on the review screen, below its diff and approval (Figure 4a, Part 3). 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; kill-switch use is audited in the feature-flag tool), object (avatar or change), field, before, after, Rows are read-only on the screen. Error definitions: A1. | ||||
Internal
1 + 2| Method | Path | Auth | Safe to retry? | Purpose | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GET | Service | Yes (read) | Pending notices for the spin system's reply, under a short timeout | ||||||||||||||||
Purpose The Evaluator consumes the record (an assumed contract, dependency 1 in Part 7); the spin system adds the pending notices to its reply as the new optional The design builds on the existing spin system (confirmed with the hiring team, 2026-11-05); the notice read adds a bounded overhead 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 avatarNoticesError definitions: A1. | |||||||||||||||||||
| — | Service | Keyed by (gameId, settledSpinId) | Assumed contract (Part 5): the spin system writes it, the Evaluator reads it; consumed, not called | ||||||||||||||||
Purpose The Evaluator consumes the record (an assumed contract, dependency 1 in Part 7); the spin system adds the pending notices to its reply as the new optional Request Spin-system team: durable, replayable records in a deterministic per-player order (
Increments are read per spin, not also as a round summary; a bought feature's price sits on its purchase settlement only.
Retry Settled spins are keyed by ( Example Record: gameId slot-42 · settledSpinId s-123 · playerId p-882 · settledAt 2026-11-06T10:15:02.120Z
spinKind paid · paidStake 200000 · creditedWin 900000 | |||||||||||||||||||
| — | Existing session | By noticeId | New optional field on the spin system's existing reply to the game | ||||||||||||||||
Purpose The Evaluator consumes the record (an assumed contract, dependency 1 in Part 7); the spin system adds the pending notices to its reply as the new optional The game estimates each bar from the spins it has seen, so it asks the server only when an unlock is likely, with no new connection. The award can reach it two ways because either may come first: the short-delay refresh once the estimate reaches the threshold, or Response Each has a stable
Retry It skips a repeated | |||||||||||||||||||
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 or unknown field value | 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 | The author approves their own change 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 already-acknowledged notice is not unknown, and its acknowledgement succeeds again |
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 (for example, the change was edited after approval), or the call is a new edit or action on a closed change (published, discarded). Reads of a closed change still work, and a repeated publish with the same Idempotency-Key returns its first result |
Reload, reapply, review again; if it was never approved, get it approved; if it is already published, reload the live revision |
PRECONDITION_FAILED |
412 | If-Match names a stale changeVersion (the change was edited 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, 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. (1) Settled spins are keyed by (gameId, settledSpinId); a second copy changes
nothing. (2) Equip is keyed by selectionVersion: a stale retry that passes the earlier checks returns
SELECTION_CONFLICT and the current choice; if it equals the requested avatar, the game treats
it as success, otherwise it redraws. (3) Acknowledgements are keyed by noticeId, admin edits by
changeVersion. (4) Every admin POST takes an Idempotency-Key; a repeated key returns the
first result; 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.
A2Back-office UI
More screens
Extra screens
The version history and the audit log are shown on the review screen, below its diff and approval (Figure 4a, Part 3).
Version history (one table per environment, newest first). Columns: revision, published
at, publishedBy, approvedBy, kind (publish, republish), avatars touched, and a "View" link to
the full revision with its diff against the one before. Each row has Republish, which opens
a change restoring that revision; it shows the diff against the live revision and goes through
the normal review. Republish passes the normal lifecycle validation and does not restore a
pulled avatar or reverse its image removal; existing ownership and pinned targets remain
unchanged. A banner states the consequence: "Republishing changes future assignments only.
Players already chasing an avatar keep their pinned target, 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; kill-switch use is audited in the feature-flag
tool), object (avatar
or change), field, before, after, revision. Filters: avatar, user, action, date range. Rows
are read-only on the screen. A support lookup of one player's avatars is out of scope for this
version.
Labels. The catalogue lists avatars last updated first. A live avatar with an edit under review keeps its Live label and shows "Change in review"; Draft and In review appear as labels only for an avatar not yet published.
Player screens. The picker shows the live earnable avatars and every avatar the player owns; a retired or grant-only avatar appears only to its owners. Only the current targets carry a progress ring, one per mode. Player screens show compact numbers (1.4M / 2M, "Win 2M coins"); the back office and the API keep full numbers. A newly owned avatar shows a NEW badge until the player equips it or saves a choice after seeing it (a display state kept by the game, assumed; no new field). The slot screen shows only the unlock toast with Equip now / Later; progress is shown in the picker. Win rings are amber; bet rings are blue. Tapping a current target repeats its ring in the preview. The Settings icon has no progress ring.
A3All three areas
Edge cases
Lifecycle actions and more edge cases
Retire (#5, taking an avatar out; a change item, reviewed). The avatar stays in the
catalogue as retired; owners keep and can equip it; it is no longer assigned or credited. A
Retire applies to spins settled after its revision went live; a spin that settled earlier but is
processed later still credits, and can complete, the retired target (as for assignment, Part 2).
A player's unfinished amount toward it moves to that mode's bar and seeds the next target, with
a progress_moved notice. The evaluator makes the move in the player's settled order, after the
spins settled before the Retire went live; 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; a change item with a pullReason and a
live grant_only replacementAvatarId, reviewed by a second approver like any production
change, audited). Once published, the avatar is retired with its pullReason, it leaves the
catalogue for everyone, its image is no longer served and controlled copies are removed through
the assumed asset process; clients receiving the revision stop rendering it. ID, name, status,
ownership and audit history are kept. After publish, a per-owner step keyed by
player, pulled avatar and revision (as the notice is) grants the replacement once (one ownership
row per player, so a retry grants nothing more, and an owner who already has it gets nothing
more). Every affected owner gets that pull's replacement notice, keyed by player, pulled
avatar and revision, also when the replacement was already owned; a replacement grant creates
the replacement notice instead of an owned one, and the message does not name the pulled
avatar. The same step equips the replacement,
with selectionVersion + 1, only if the pulled avatar is still equipped at that step, so a newer
choice stays. Unfinished progress moves as for Retire. No coin compensation;
deletion of offline copies is not promised. For an emergency before the change is approved, the
kill switch hides the whole feature.
Rule pause (senior, audited). A pause applies to spins the server processes after it takes
effect. A paused rule keeps crediting progress but grants nothing and is not assigned as a new
target; the picker greys it with "Paused" (pausedAvatarIds in the player state, A1). Grants
that fall due during the pause are made at resume against the corrected record; audited
corrections run while the rule is paused. At resume, a per-player step keyed by player, avatar and
resume makes the due grants, so a retry never grants twice. The step runs in the player's
processing order (dependency 1), and 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 (from Part 6):
| Case | Server | Player sees |
|---|---|---|
| Rule changed mid-chase | Existing pins kept (#3, pinned terms); 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 (#5, taking an avatar out); progress moves on, 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 where the cat still is (above) | Owners: "Avatar removed and replaced with Phoenix"; players chasing it: "Your progress now counts toward Blue Owl" (wording from Legal, dependency 5) |
Health signals (Part 7's release controls):
| 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 pinned 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 dependency 1's per-player order is not met: a late spin may credit the target still pinned when it is processed, not its successor; results can differ from settled order by more than one spin; a replay keeps the 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 settled spin 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 | Pinned players keep the old target; no automatic sweep. If that is wrong for players, the correction "apply revision N to players in progress on this rule" only lowers pinned targets; it runs while the rule is paused, so anyone already at or above the lowered target is granted at resume |
| A mistyped threshold went live | Pause the rule, find the affected pins 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 |

