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). Until the player's next settled spin 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.
- Fresh bars: each new bar starts at zero, so one spin unlocks at most one avatar per mode.
Six decisions
| Question | Our answer | What we gave up |
|---|---|---|
| #1 What does a player chase? | One avatar per mode (bet, win) at a time; each new 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.
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 | 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 |
Approver is not the author (production); a published revision never changes |
| Ownership | (playerId, avatarId) |
source, grantedAt |
One row per player and avatar, kept after Retire and Pull and replace |
| Selection | playerId |
equippedAvatarId, selectionVersion |
Must be owned and not pulled; version +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 | 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
Response A higher On publish the avatar becomes 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; an existing push channel may notify a closed app (assumed). Response
A mode with no target returns, for example,
The game adds what it counted after that spin, or, if the spin is not in its local history (say, another device), everything eligible since the state arrived. A higher Each has a stable
Retry At the pinned threshold, or on a no-target mode's first eligible spin, the game refetches after a short delay, then a few times with growing gaps; each answer resets 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 }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 Each has a stable Errors Retry Acknowledgements: 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. Step 1. Maria edits the rule (rule editor): an image from the asset library, availability, and the unlock rule. Retry Only a Acknowledgements: 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 Only a Admin 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 Only a Errors Approve and publish are separate calls. If Maria edits in between, the approval is cleared and the publish is refused. Retry 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" } ] } }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.
PUT /v1/me/equipped-avatar · Game · Session
Purpose. Equip an avatar the player owns.
| Field | Type | Required | Meaning |
|---|---|---|---|
avatarId |
number | yes | Avatar to equip |
selectionVersion |
number | yes | Version the device last saw; stale → SELECTION_CONFLICT |
Response 200: the full player state, with selectionVersion +1.
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.
Retry. Safe. If the first call was saved, the retry returns SELECTION_CONFLICT; the game treats it as success when details.equipped is the avatar it asked for, and otherwise redraws. If the first call was not saved, the retry works like a first call.
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 too, published the same way. Approve and publish are separate calls. If Maria edits in between, the approval is cleared and the publish is refused. Maria wrote c-311; Jordi approved 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 reads the record (assumed contract). The spin system adds 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: SELF_APPROVAL_NOT_ALLOWED, AVATAR_NOT_FOUND, AVATAR_NOT_OWNED, AVATAR_NOT_EQUIPPABLE, SELECTION_CONFLICT, CHANGE_CONFLICT, PRECONDITION_FAILED, VALIDATION_FAILED and SERVICE_UNAVAILABLE.
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 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).
| 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 settled spin in the rollout |
| 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. Watched against the holdout: share of active players with a non-default avatar at day 30. 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,spinKind, per-spin increments),avatarNotices, speed and freshness targets. The spin reply also carriesgameIdandsettledSpinId, 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
Auth column: "Session" is the signed-in player, "GameOps" the existing back-office role, "Senior" the senior GameOps permission, "Service" an internal service.
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" },
"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. The game adds what it counted after that spin, or, if the spin is not in its local history (say, another device), everything eligible since the state arrived. A higher catalogueRevision triggers a catalogue refetch.
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. Each bar shows the last confirmed amount plus eligible contributions after processedThrough. At the pinned threshold, or on a no-target mode's first eligible spin, the game refetches after a short delay, then a few times with growing gaps; each answer resets the estimate. a noticeId already shown is skipped while local history lasts. Login, app return and the avatar screen also fetch. No new connection; 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:
| 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 |
Settled-spin record. 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.
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, 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. Only a PUT edit raises changeVersion by 1. A published change is closed and read-only. A draft or in-review change can be discarded with If-Match; it closes as discarded, audited. "Approve and publish" 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 A higher On publish the avatar becomes 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 | Session | Yes (read) | Player state | |||||||||||||||||
Purpose Login, app return and the avatar screen also fetch. No new connection; an existing push channel may notify a closed app (assumed). Response
A mode with no target returns, for example,
The game adds what it counted after that spin, or, if the spin is not in its local history (say, another device), everything eligible since the state arrived. A higher Each has a stable
Retry At the pinned threshold, or on a no-target mode's first eligible spin, the game refetches after a short delay, then a few times with growing gaps; each answer resets 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 }Each code is defined once, in A1. | ||||||||||||||||||||
| PUT | Session | Yes, by selectionVersion | Equip an owned, equippable avatar; returns the player state | |||||||||||||||||
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 | Session | Yes, by noticeId | Mark a notice as shown | |||||||||||||||||
Purpose Each has a stable Errors Retry Acknowledgements: Each code is defined once, in A1. | ||||||||||||||||||||
Back-office endpoints
19If-Match carries changeVersion in quotes (If-Match: "5"), the value GET /admin/v1/changes/{changeId} returns, also as its ETag. Stale version: 412 PRECONDITION_FAILED. Lifecycle conflict: 409 CHANGE_CONFLICT.
| 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 Last updated first. A live avatar with an edit in review stays Live and shows "Change in review". Draft and In review labels appear only on never-published avatars. 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. Retry Admin Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | The change with changeVersion, items and status | |
Response Only a Errors Each code is defined once, in A1. | ||||
| PUT | GameOps | If-Match: changeVersion | Edit items: create, edit, retire, pull_and_replace (with replacementAvatarId, pullReason); clears an 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. Step 1. Maria edits the rule (rule editor): an image from the asset library, availability, and the unlock rule. Retry Only a Acknowledgements: 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 Only a Admin 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 The review screen shows the version history and the audit log below the diff and approval. Errors Each code is defined once, in A1. | ||||
| POST | GameOps, not the author (production) | Idempotency-Key, If-Match | Record approvedBy | |
Purpose "Approve and publish" makes two calls: approve, then publish. Retry Only a Admin 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 | GameOps | Idempotency-Key, If-Match | in_review → draft, with a comment | |
Purpose An edit clears the approval; Send back returns it to Draft. Retry Only a Admin Each code is defined once, in 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 "Approve and publish" makes two calls: approve, then publish. Response Only a Errors Approve and publish are separate calls. If Maria edits in between, the approval is cleared and the publish is refused. Retry 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" } ] } }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 Admin 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. A banner says: "Republishing changes future assignments only. Players already chasing an avatar keep their pinned target, and owned avatars stay owned." Errors Retry Admin 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 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".
Retry Admin 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 | |
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 Admin 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. Retry Admin Each code is defined once, in A1. | ||||
| POST | Senior, not the proposer | Idempotency-Key | Record the second approval; the correction then runs | |
Errors Retry Admin Each code is defined once, in A1. | ||||
| GET | GameOps | Yes (read) | Audit log, filtered by avatar, user, action or time | |
Purpose The review screen shows the version history and the audit log 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. | ||||
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 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. | ||||||||||||||||||||
| — | Service | Keyed by (gameId, settledSpinId) | Assumed contract: the spin system writes it, the Evaluator reads it; no call is made | |||||||||||||||||
Purpose The Evaluator reads the record (assumed contract). Request Spin-system team: replayable records kept in a fixed order per player (
Retry Settled spins, 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 spin system adds pending notices to its reply as the new optional 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 Response Each has a stable
Retry At the pinned threshold, or on a no-target mode's first eligible spin, the game refetches after a short delay, then a few times with growing gaps; each answer resets the estimate. a | ||||||||||||||||||||
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. 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 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, keyed by (gameId, settledSpinId), count once. (2) Equip is keyed by selectionVersion; a stale retry that passes the earlier checks gets SELECTION_CONFLICT with the current choice: success if that is the requested avatar, else redraw. (3) Acknowledgements: noticeId; admin edits: changeVersion. (4) Admin POSTs: Idempotency-Key; a repeat 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.
A2
More screens
Version history and audit log
OpenClose
A2
More screens
Version history and audit log
The review screen shows the version history and the audit log 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. A banner says: "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), 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.
Labels. Last updated first. A live avatar with an edit in review stays Live and shows "Change in review". Draft and In review labels appear only on never-published avatars.
Player screens. The picker shows live earnable avatars and every avatar the player owns; retired and grant-only ones appear only to owners. Only current targets have a ring, one per mode (win amber, bet blue); tapping one repeats it in the preview. Players see short numbers (1.4M / 2M, "Win 2M coins"); the back office and API, full ones. A newly owned avatar shows NEW until the player equips it or saves a choice after seeing it (kept by the game, assumed; no new field). The slot screen shows only the unlock toast (Equip now / Later); progress is in the picker. Settings has no ring.
A3
Edge cases
Retire, pull, pause and late spins
OpenClose
A3
Edge cases
Retire, pull, pause and late spins
Retire (a reviewed change item). The avatar stays in the catalogue as retired; owners keep and can equip it; 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 amount moves to that mode's bar, seeds the next target and sends progress_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). A change item with a pullReason and a live grant_only replacementAvatarId; second approval as for any production change; audited. On publish the avatar becomes retired with its pullReason and leaves everyone's catalogue; its image stops being served, controlled copies are removed (assumed asset process), and clients stop showing it. ID, name, status, ownership and audit history stay. one step per owner, keyed by player, pulled avatar and revision, runs once. It grants the replacement (one ownership row per player, so never twice). It sends one replacement notice instead of owned, even if the replacement was already owned, without naming the pulled avatar. It equips the replacement with selectionVersion + 1 only if the pulled avatar is still equipped, so a newer choice stays. 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 owned notice, the cleared target and the discarded overflow.
Rule changes and removals, as players see them:
| Case | Server | Player sees |
|---|---|---|
| Rule changed mid-chase | Players already chasing keep their target; later assignments use the new revision | Ana keeps 2,000,000; Ben (after revision 8) gets 3,000,000 |
| Golden Cat retired | Owners keep and can equip it; progress moves on, with a progress_moved notice |
"Golden Cat has left; your 1.2M now counts toward Blue Owl" |
| Golden Cat pulled and replaced | Image removed; owners get Phoenix, equipped if they still wear the cat | Owners: "Avatar removed and replaced with Phoenix"; players chasing it: "Your progress now counts toward Blue Owl" (wording from Legal) |
Health signals and what to do:
| Health signal | Action |
|---|---|
| Spin reply time; notice-read timeouts | Notice read off; stop the rollout |
| Award lag, backlog | Alert; hold the rollout |
| Bars at or above their 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 the spin system breaks per-player order: a late spin may credit the target pinned 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 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 unfair, the correction "apply revision N to players in progress on this rule" lowers pinned 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 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 |

