@indiegems/gem-web-sdk 7.4.0 → 7.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +54 -0
  2. package/dist/gem-web-sdk.esm.js +1 -1
  3. package/dist/gem-web-sdk.game.js +1 -1
  4. package/dist/gem-web-sdk.min.js +1 -1
  5. package/dist/gem-web-sdk.sdk.js +1 -1
  6. package/dist/gem-web-sdk.server.js +1 -1
  7. package/docs/achievements.md +232 -0
  8. package/docs/analytics.md +271 -0
  9. package/docs/api/api-reference-game.md +15308 -0
  10. package/docs/api/api-reference-server.md +4397 -0
  11. package/docs/api/reference.json +24130 -0
  12. package/docs/arcade-economy.md +263 -0
  13. package/docs/async-opponents.md +396 -0
  14. package/docs/brand-art.md +334 -0
  15. package/docs/cross-promotion.md +137 -0
  16. package/docs/daily-challenges.md +583 -0
  17. package/docs/dedicated-server-flow.md +879 -0
  18. package/docs/deprecation-policy.md +124 -0
  19. package/docs/friends.md +233 -0
  20. package/docs/game-economy-tutorial.md +326 -0
  21. package/docs/getting-started.md +552 -0
  22. package/docs/inventory-and-catalogs.md +1035 -0
  23. package/docs/leaderboards.md +277 -0
  24. package/docs/manifest.json +135 -0
  25. package/docs/match-session.md +288 -0
  26. package/docs/p2p-and-parties.md +783 -0
  27. package/docs/platform-rules.md +372 -0
  28. package/docs/player-sign-in.md +89 -0
  29. package/docs/room-shapes.md +195 -0
  30. package/docs/rooms-and-matches.md +779 -0
  31. package/docs/saving-player-progress.md +457 -0
  32. package/docs/server-bundles.md +1041 -0
  33. package/docs/serverless-functions.md +748 -0
  34. package/docs/trading-and-gifting.md +98 -0
  35. package/docs/what-a-game-must-do.md +1231 -0
  36. package/package.json +7 -2
  37. package/schema/bridge.v1.json +5 -5
  38. package/schema/capabilities.public.json +189 -0
@@ -0,0 +1,334 @@
1
+ # The Gems, Tickets and trophy art
2
+
3
+ The SDK ships the platform's own art for its two currencies, **Gems** and **Tickets**, and for
4
+ **trophies**. You need it only if your game draws Gems, Tickets or a trophy on its own screens. Read
5
+ §1 before you pick a file: each currency has two kinds of art, and which one you use depends on what
6
+ the screen is for.
7
+
8
+ **Currency display belongs to the arcade.** The arcade draws the player's Gems and Tickets totals in
9
+ its own chrome while your game runs, and it animates every payout of either into its total. Your game
10
+ draws Gems or Tickets only in the three places §5 allows — an amount on offer or just paid, a reward
11
+ screen, and how much more your game can still pay (the earn allowance) — and never a balance of its
12
+ own. §5 is the creator's ruling and is not optional.
13
+
14
+ > **The Tickets art is generated art, open to a designer pass.** It was generated on 2026-09-28
15
+ > (gemforge asset `e540c7ac-850c-4ef0-9b9c-f11227098ce6`): a solid tangerine ticket with a vertical
16
+ > gradient, so it never reads as the rainbow gem or the gold trophy. It replaced the first art of
17
+ > 2026-09-27, a ticket cut from the gem's rainbow glass. If a designer redraws it, the new art
18
+ > replaces these files **under the same names** in a later SDK release, so a game that imports them
19
+ > picks it up on its next SDK update. Lay your screens out from the sizes on this page, not from the
20
+ > drawing.
21
+
22
+ ## 1. Two kinds of currency art: pick by the ROLE of the screen
23
+
24
+ | You are drawing | Use | Drawn at |
25
+ | --------------- | --- | -------- |
26
+ | Gems **inline**, beside a number: a price, a "+15" reward label, a small chip | the **icon**, `gem-icon-20-*` or `gem-icon-32-*` | 16–40 CSS px |
27
+ | Gems as **the subject of a reward presentation**: a victory or reward screen where the gem is what the player looks at, such as a victory overlay with a sunburst behind the reward | the **hero**, `gem-hero-*` | 64–256 CSS px |
28
+ | Tickets **inline**, beside a number | the **icon**, `ticket-icon-20-*` or `ticket-icon-32-*` | 16–40 CSS px |
29
+ | Tickets as **the subject of a reward presentation** | the **hero**, `ticket-hero-*` | 64–256 CSS px |
30
+
31
+ The role decides first and the size follows from it. A price tag is inline even if you make it
32
+ large; a reward screen is a presentation even on a small phone.
33
+
34
+ - **Never use the hero at icon sizes.** It is one render drawn to be seen large; shrunk to 20 px its
35
+ outline goes under a pixel, its facet lines vanish and the gem smudges into the background.
36
+ - **Never use the icon at hero sizes.** Each icon file is a separate render drawn for one small box,
37
+ with a heavier outline and fewer details than a large gem needs. Stretched, it is visibly coarse.
38
+ - **Nothing ships for 40–64 CSS px.** If a design lands there, it is undecided between the two roles:
39
+ make the inline element smaller or the presentation bigger.
40
+
41
+ The **trophy** comes only as a hero (§3). There is no trophy icon.
42
+
43
+ The Gems and Tickets marks differ in **shape** — a faceted gem, and a ticket with a notch in each
44
+ end — as well as colour, so a player can tell the two apart without seeing colour. Keep the currency's
45
+ name beside or behind every amount (in words, or for a screen reader) all the same.
46
+
47
+ ## 2. The Gems icon
48
+
49
+ The same icon the arcade draws in its own wallet, rendered from the 3D model of the Gems gem **once
50
+ per display size**, not scaled from one image. Each size is its own render with its own outline
51
+ weight and detail, and each device-pixel ratio of it is rendered on its own pixel grid.
52
+
53
+ | Rendition | For a CSS box of | File | Pixels |
54
+ | --------- | ---------------- | ---- | ------ |
55
+ | 20 | up to about 24 px | `gem-icon-20-1x.png` | 20 × 20 |
56
+ | | | `gem-icon-20-1.5x.png` | 30 × 30 |
57
+ | | | `gem-icon-20-2x.png` | 40 × 40 |
58
+ | | | `gem-icon-20-3x.png` | 60 × 60 |
59
+ | 32 | above 24, up to about 40 px | `gem-icon-32-1x.png` | 32 × 32 |
60
+ | | | `gem-icon-32-1.5x.png` | 48 × 48 |
61
+ | | | `gem-icon-32-2x.png` | 64 × 64 |
62
+ | | | `gem-icon-32-3x.png` | 96 × 96 |
63
+
64
+ Each file is square, 8-bit RGBA, with a transparent background. **The gem fills the width of its
65
+ box** — its widest points touch the left and right edges, with clear rows above and below — because
66
+ at 20 px a margin is gem the player loses. Size the box to the gem, and leave any spacing to your
67
+ layout.
68
+
69
+ The names use `-1.5x`, not `@1.5x`: a Gem web bundle refuses any file name with a character outside
70
+ `A–Z a–z 0–9 . _ -`, and your bundler keeps the name when it copies the file into your build.
71
+
72
+ ### Where the icon comes from
73
+
74
+ These are the Arcade's own icon renders — the files its wallet draws — with the same pixels, byte
75
+ for byte, apart from stripping the colour-space and resolution tags every browser already assumes. They are the gem
76
+ of the arcade's Gems flight — a 3D model rendered in Blender, a round cut with the icon's six facet
77
+ colours — at rest and without its motion blur, so the gem a player sees in your price is the gem
78
+ that flies into their wallet. Each size has its own recipe, in CSS px, so it carries the same visual
79
+ weight on every screen:
80
+
81
+ | Size | Outline | Girdle line | Facet lines | Sparkle |
82
+ | ---- | ------- | ----------- | ----------- | ------- |
83
+ | 20 px | 1.5 px | 0.75 px | none | 3 px arms |
84
+ | 32 px | 2 px | 0.8 px | none | 5 px arms |
85
+
86
+ Both take the render's gleam at a third of its strength and colour the crown's pale centre facet
87
+ light cyan and light yellow. They are PNG rather than WebP because the renderer's WebP is lossy at
88
+ every quality setting and moved a 20 px icon's facet colours visibly. The renders are deterministic
89
+ and the pipeline lives with the platform's art sources; if the gem changes, the platform re-renders
90
+ both sizes and a new SDK release ships them. Never edit the files by hand.
91
+
92
+ ### The Tickets icon
93
+
94
+ The same two renditions and four ratios, under the same rules: the ticket spans the full width of its
95
+ box, with clear rows above and below it (more of them than the gem, because a ticket is wider than it
96
+ is tall), and each size is drawn on its own grid with its own outline weight — 1.5 px at 20, 2 px at
97
+ 32.
98
+
99
+ | Rendition | For a CSS box of | File | Pixels |
100
+ | --------- | ---------------- | ---- | ------ |
101
+ | 20 | up to about 24 px | `ticket-icon-20-1x.png` | 20 × 20 |
102
+ | | | `ticket-icon-20-1.5x.png` | 30 × 30 |
103
+ | | | `ticket-icon-20-2x.png` | 40 × 40 |
104
+ | | | `ticket-icon-20-3x.png` | 60 × 60 |
105
+ | 32 | above 24, up to about 40 px | `ticket-icon-32-1x.png` | 32 × 32 |
106
+ | | | `ticket-icon-32-1.5x.png` | 48 × 48 |
107
+ | | | `ticket-icon-32-2x.png` | 64 × 64 |
108
+ | | | `ticket-icon-32-3x.png` | 96 × 96 |
109
+
110
+ It is the hero's ticket — solid tangerine, a notch in each end and a perforated stub (see the note at
111
+ the top of this page) — downscaled onto each file's own pixel grid, inside the gem icon's violet
112
+ outline. The arcade's wallet draws the same ticket from the same master, and a simplified vector
113
+ version of it at 16–20 px.
114
+
115
+ ## 3. The hero art
116
+
117
+ Three marks — the gem, the ticket and the trophy — each at five sizes. Every file is a square PNG with a
118
+ transparent background, no outline, and the mark centred with a small margin, so the files swap for
119
+ one another at any size without moving your layout.
120
+
121
+ | File | Pixels |
122
+ | ---- | ------ |
123
+ | `gem-hero-128.png`, `ticket-hero-128.png`, `trophy-hero-128.png` | 128 × 128 |
124
+ | `gem-hero-256.png`, `ticket-hero-256.png`, `trophy-hero-256.png` | 256 × 256 |
125
+ | `gem-hero-384.png`, `ticket-hero-384.png`, `trophy-hero-384.png` | 384 × 384 |
126
+ | `gem-hero-512.png`, `ticket-hero-512.png`, `trophy-hero-512.png` | 512 × 512 |
127
+ | `gem-hero-768.png`, `ticket-hero-768.png`, `trophy-hero-768.png` | 768 × 768 |
128
+
129
+ The hero is drawn to be displayed at **64 to 256 CSS pixels**. Pick the smallest file at or above the
130
+ size you draw it multiplied by the screen's `devicePixelRatio`:
131
+
132
+ | Drawn at | 1× screen | 2× screen | 3× screen |
133
+ | -------- | --------- | --------- | --------- |
134
+ | 64 px | 128 | 128 | 256 |
135
+ | 128 px | 128 | 256 | 384 |
136
+ | 256 px | 256 | 512 | 768 |
137
+
138
+ **Do not draw it larger than 256 CSS pixels.** On a 3× screen that needs more than the largest file,
139
+ and the art softens as it is stretched. Below 64 px, use the icon (§2).
140
+
141
+ ## 4. Loading and drawing the art
142
+
143
+ All of it is in the package you already install, under `brand/`, and published beside it on the CDN —
144
+ the same bytes either way, for the version you are on.
145
+
146
+ ### In a game with a bundler
147
+
148
+ Import the file by its path in the package. Your bundler copies it into your build and hands you its
149
+ URL, so it loads from your game's own origin like the rest of your art, and only the files you import
150
+ reach a player:
151
+
152
+ ```js
153
+ import g20x1 from '@indiegems/gem-web-sdk/brand/gem-icon-20-1x.png';
154
+ import g20x15 from '@indiegems/gem-web-sdk/brand/gem-icon-20-1.5x.png';
155
+ import g20x2 from '@indiegems/gem-web-sdk/brand/gem-icon-20-2x.png';
156
+ import g20x3 from '@indiegems/gem-web-sdk/brand/gem-icon-20-3x.png';
157
+ ```
158
+
159
+ The package is the one [getting-started.md](getting-started.md) installs; there is nothing else to
160
+ add. **In TypeScript, the type of a `.png` import comes from your bundler, not from this package**:
161
+ with Vite, that is `vite/client` in your `tsconfig.json` `types`. Vite inlines a file under 4 KB as a
162
+ `data:` URL rather than copying it, which several of the small icon files are; that works, and
163
+ `build.assetsInlineLimit` changes it if you want every file as its own request.
164
+
165
+ ### The icon in an `<img>`
166
+
167
+ Offer all four files in **width descriptors** and set `sizes` to the CSS box you draw it in, so the
168
+ browser picks the file for that box on this screen:
169
+
170
+ ```js
171
+ const gem = document.createElement('img');
172
+ gem.src = g20x1;
173
+ gem.srcset = `${g20x1} 20w, ${g20x15} 30w, ${g20x2} 40w, ${g20x3} 60w`;
174
+ gem.sizes = '20px'; // the CSS box: the same value as the width and height below
175
+ gem.width = 20;
176
+ gem.height = 20;
177
+ gem.alt = ''; // decorative: the amount beside it says "Gems" in words or to a screen reader
178
+ priceRow.append(gem, document.createTextNode(' 250'));
179
+ ```
180
+
181
+ For the 32 rendition, the widths are `32w, 48w, 64w, 96w`. If your box is in `rem`, set `sizes` in
182
+ the same `rem` — `sizes` is how the browser knows the box, and a wrong value picks a blurry file.
183
+
184
+ ### The icon as a CSS background
185
+
186
+ `image-set()` picks by device-pixel ratio. Keep a plain `background-image` first for a browser that
187
+ lacks it:
188
+
189
+ ```js
190
+ const chip = document.querySelector('.gems-chip-icon');
191
+ chip.style.backgroundImage = `url("${g20x2}")`;
192
+ chip.style.backgroundImage = `image-set(url("${g20x1}") 1x, url("${g20x15}") 1.5x, url("${g20x2}") 2x, url("${g20x3}") 3x)`;
193
+ ```
194
+
195
+ ```css
196
+ .gems-chip-icon {
197
+ width: 20px;
198
+ height: 20px;
199
+ background: center / contain no-repeat;
200
+ }
201
+ ```
202
+
203
+ ### The icon on a canvas, or as a Pixi or three.js texture
204
+
205
+ A canvas has no `srcset`. **Load the 3x file of the rendition your box calls for, and scale it
206
+ down** — never up:
207
+
208
+ ```js
209
+ // 2D canvas: a 20 CSS px icon at (x, y) in CSS px, on a canvas scaled for devicePixelRatio.
210
+ const img = new Image();
211
+ img.src = g20x3; // 60 px: enough for any screen up to 3x
212
+ await img.decode();
213
+ ctx.imageSmoothingQuality = 'high';
214
+ ctx.drawImage(img, x, y, 20, 20);
215
+ ```
216
+
217
+ ```js
218
+ // PixiJS
219
+ const texture = await PIXI.Assets.load(g20x3);
220
+ const gem = new PIXI.Sprite(texture);
221
+ gem.width = gem.height = 20;
222
+ ```
223
+
224
+ ```js
225
+ // three.js: an icon on a HUD sprite
226
+ const map = new THREE.TextureLoader().load(g32x3);
227
+ map.colorSpace = THREE.SRGBColorSpace;
228
+ const gem = new THREE.Sprite(new THREE.SpriteMaterial({ map, transparent: true }));
229
+ ```
230
+
231
+ Keep mipmapping on (three.js does by default) so the scaled-down icon stays crisp rather than
232
+ shimmering.
233
+
234
+ ### The hero
235
+
236
+ The same import, and a `srcset` in density descriptors, picked per §3:
237
+
238
+ ```js
239
+ import gem256 from '@indiegems/gem-web-sdk/brand/gem-hero-256.png';
240
+ import gem512 from '@indiegems/gem-web-sdk/brand/gem-hero-512.png';
241
+
242
+ const art = document.createElement('img');
243
+ art.src = gem256;
244
+ art.srcset = `${gem256} 1x, ${gem512} 2x`;
245
+ art.alt = ''; // decorative: say "Gems" in the text beside it
246
+ ```
247
+
248
+ The same URL works as a texture — `new THREE.TextureLoader().load(gem512)` — because it is your own
249
+ origin.
250
+
251
+ ### Without a bundler, or from another engine
252
+
253
+ Every file is also published at a permanent URL, under the version you install:
254
+
255
+ ```text
256
+ https://downloads.gemarcade.com/sdk/v<version>/brand/gem-icon-20-2x.png
257
+ https://downloads.gemarcade.com/sdk/v<version>/brand/gem-hero-256.png
258
+ ```
259
+
260
+ The CDN lets any origin read these files, so a canvas or a WebGL texture can use one — set
261
+ `crossOrigin = 'anonymous'` on the image before its `src`, or the texture is refused as tainted. A
262
+ path never changes once published, so a URL you ship keeps working, and `artifact-manifest.json` in
263
+ the same version folder lists each file's `sha384` hash under `brand`.
264
+
265
+ **Prefer copying the file into your build over loading it by URL at play time.** A load at play time
266
+ is one more request that can be slow or fail, and your screen then shows a gap where the art should
267
+ be. If you do load it live, lay the screen out so it reads correctly without the image.
268
+
269
+ ### The platform currency's `icon_url`
270
+
271
+ gem-api's public platform currency catalogue, `GET /v1/inventory/currencies`, returns each platform
272
+ currency with an `icon_url`. For Gems that is one rendered icon image, being set now. It exists for
273
+ an engine or a tool that cannot bundle files and needs to ask the platform what Gems look like.
274
+ **A bundled game should prefer the renditions in this package**: they come in two sizes and four
275
+ densities, and they cost no request at play time.
276
+
277
+ ## 5. The rules: Gems and Tickets display belongs to the arcade
278
+
279
+ The creator's ruling (2026-09-24): **"Gems display belongs to the arcade."** Tickets, the platform's
280
+ second currency, is held to the same ruling: read every rule below about Gems as a rule about Tickets
281
+ too, with the ticket in place of the gem. Amended 2026-09-30 for the earn allowance (the platform's
282
+ decision of that date); the ruling's other rules are unchanged. In rules:
283
+
284
+ - **Keep no Gems counter.** Your game shows no Gems balance, running total or wallet of its own, on
285
+ any screen. The arcade's wallet is the only place Gems accrue, and the only place a total is shown.
286
+ - **Draw no Gems flight or celebration of your own.** No particles flying to a corner, no counting-up
287
+ number, no burst of gems. `gem.cueReward` is the animation, and the arcade draws it.
288
+ - **Show no Gems amount at all when you are not connected to the arcade.** Without the arcade there is
289
+ nothing to pay from and nothing to accrue to, so a price or a reward in Gems means nothing. Hide
290
+ every Gems element unless `gem.awaitBridge()` settled `connected` (and `onStateChange` has not
291
+ since reported otherwise).
292
+ - **At a payout, cue the arcade from your gem:**
293
+
294
+ ```js
295
+ const { x, y, width, height } = rewardGem.getBoundingClientRect(); // the gem element itself
296
+ gem.cueReward({
297
+ currencySlug: 'GEMS',
298
+ expected: 15,
299
+ origin: { x: x + width / 2, y: y + height / 2 },
300
+ });
301
+ ```
302
+
303
+ `origin` is the **centre of the Gems icon (or hero) on screen**, in CSS px of your game's own
304
+ viewport. Measure that gem element's own box — not the row, the chip or the panel around it — so
305
+ the flight leaves from the gem the player is looking at. On a canvas, convert from canvas pixels
306
+ with the canvas's `getBoundingClientRect()`.
307
+
308
+ A Tickets payout is cued the same way, from the ticket, with `currencySlug: 'ARCADE_TICKETS'`. The
309
+ arcade's wallet shows Gems and Tickets side by side and flies each payout into the total of the
310
+ currency the cue names, so the slug is what decides where your ticket lands.
311
+ - **If `cueReward` returns `false`, do nothing.** There is no fallback celebration. `false` means the
312
+ arcade will not show the payout here — the bridge is not up, or the embedder has no wallet — and
313
+ your game does not replace it with one of its own. Your reward screen still says what was earned;
314
+ it just does not animate Gems anywhere.
315
+ - **Draw the art as it is.** Scale it and place it freely; do not recolour it, tint it, greyscale it
316
+ for a disabled state, crop it, add an outline or a drop shadow, or redraw it. A player learns these
317
+ marks on the arcade, and a variant reads as a different thing.
318
+ - **The gem means Gems, the platform's currency** — never your own currency. Next to a number it
319
+ states an amount of Gems, so show only amounts the platform pays, charges or reports: the reward
320
+ your mapping grants, the price the platform charges, and the remaining earn allowance read from
321
+ `gem.platform.earnAllowance.get()` ("40 Gems left to earn today"). The icon may sit beside that
322
+ figure. It is still not a balance, a counter or a running total, and it is never a number your game
323
+ computes. Your server's order response is the receipt, not your screen.
324
+ - **The ticket means Tickets, the platform's cosmetics currency** — never your own currency, and
325
+ never a raffle, an entry or a pass. The gem rule above applies to it word for word.
326
+ - **The trophy means an achievement**: use it where your game shows what a player has earned.
327
+ Whether the trophy also becomes the mark of the player's Achievement Score — the sum of their
328
+ achievement points, which the arcade now shows — is an open design question; until it is answered,
329
+ use the trophy for achievements.
330
+ - **Never inside a panel dressed as the arcade's own screens.** A player must always be able to tell
331
+ your game from the platform, most of all where money or an account is involved.
332
+
333
+ How a payout reaches the wallet, and what the arcade does with the cue:
334
+ [inventory-and-catalogs.md](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet).
@@ -0,0 +1,137 @@
1
+ # Cross-promotion
2
+
3
+ The Arcade can show a **full-screen panel about another game** at a break in yours. Your game asks
4
+ for one; the Arcade decides whether to show anything, draws it in its own chrome over your paused
5
+ game, and tells you when it is done.
6
+
7
+ ```js
8
+ const { outcome } = await gem.ads.requestAd({
9
+ placement: 'level-complete',
10
+ format: 'interstitial',
11
+ });
12
+ // Nothing to handle. Carry on.
13
+ startNextLevel();
14
+ ```
15
+
16
+ That is the whole surface. The rest of this page is the four things a game gets wrong when nobody
17
+ tells it.
18
+
19
+ ## 1. The answer is usually no, and no is ordinary
20
+
21
+ `outcome` is one of exactly three values:
22
+
23
+ | outcome | what happened | what your game does |
24
+ |---|---|---|
25
+ | `no_ad` | nothing was shown | carry on exactly as if you had not asked |
26
+ | `shown` | a panel was painted and has closed | carry on — and grant nothing |
27
+ | `unavailable` | the Arcade could not draw one just now | carry on; read it identically to `no_ad` |
28
+
29
+ **`no_ad` is the common answer**, not an edge case. There may be nothing suitable, the player may
30
+ be somebody the Arcade will not interrupt, or the break may simply be too soon after the last one.
31
+ You are not told which, and there is nothing to infer.
32
+
33
+ So write the `no_ad` path as your ordinary path:
34
+
35
+ - Do **not** render a placeholder, a spinner, or a "nothing to show right now" message. Empty means
36
+ nothing appears.
37
+ - Do **not** hold up your own flow waiting for a fill. Ask, and continue when the promise settles.
38
+ - Do **not** retry on a no. A second ask at the same break gets the same answer and spends a slot.
39
+
40
+ ## 2. Nothing rejects
41
+
42
+ `requestAd` always resolves. There is no error branch and no `try`/`catch` to write. Even a refusal
43
+ of your own arguments — a `placement` outside the grammar below, a `format` outside the two —
44
+ resolves `no_ad`, so your game keeps exactly one branch. (The mistake is still reported to your
45
+ `onError` handler, so you find out in development.)
46
+
47
+ ## 3. ⚠️ `shown` grants nothing
48
+
49
+ `shown` says a panel was painted and has closed. It is **not** a receipt, **not** an entitlement,
50
+ and **not** permission to hand anything out.
51
+
52
+ `format: 'rewarded'` exists in the vocabulary and is answered `no_ad` today. Even when that
53
+ changes, a reward will be granted by the platform against its own record of what was shown — never
54
+ by your game reading an outcome. An outcome travels over the same channel as everything else your
55
+ frame receives, and a game that unlocks a level or grants a currency on the strength of one has an
56
+ unlock that any page embedding your game can forge.
57
+
58
+ Treat a break as a break. If your game wants to give the player something for reaching the end of a
59
+ level, give it to them for reaching the end of the level.
60
+
61
+ **Where a rewarded grant will arrive, so you can write the code now.** On your game's own inventory
62
+ for this player, written by the platform — never by your game — and read through
63
+ `gem.me.inventory`, the same place a purchase lands, read the same way. (That is the game-scoped
64
+ inventory `gem.me` shows you, not the player's platform wallet.) There will be **no fourth outcome
65
+ and no extra field** on the answer. `outcome` is pinned at three values because a new value would
66
+ break every game bundle already in the field: its validator refuses the whole message. A new field
67
+ would not break anything — validators ignore fields they do not know — but it is not coming either:
68
+ anything on that answer can be forged by the page embedding your game, so a reward carried there
69
+ would be a reward anyone could hand out. If you serve `rewarded`, re-read your snapshot once the ask
70
+ settles, whatever it settled as:
71
+
72
+ ```js
73
+ await gem.ads.requestAd({ placement: 'run-banked', format: 'rewarded' });
74
+ gem.me.inventory.refresh(); // drop what is cached
75
+ const snap = await gem.me.inventory.snapshot(); // the grant, if there was one, is here
76
+ ```
77
+
78
+ Today that re-read finds nothing new, because nothing is served. It costs one request at a break
79
+ your player is already sitting in, and it is the whole of what changes on the day one is — no
80
+ rebuild. [`inventory-and-catalogs.md` §6](inventory-and-catalogs.md#6-when-to-re-read) is the
81
+ general form of this.
82
+
83
+ ## 4. The Arcade pauses your game for you
84
+
85
+ While a panel is up, your game is paused the same way it is paused when a player switches tabs —
86
+ the Arcade sends the pause your game already handles, and lifts it when the panel closes. You do
87
+ not need to pause anything yourself, and you should not resume on a timer: resume when the promise
88
+ settles.
89
+
90
+ If you have not wired the pause at all yet, that is the thing to fix first;
91
+ [`what-a-game-must-do.md`](what-a-game-must-do.md) covers it.
92
+
93
+ ## Where to put the call
94
+
95
+ At a break **the player already expects**: a level ending, a round finishing, a run being banked, a
96
+ return to a menu. Never mid-input, never during a timed sequence, and never on a screen the player
97
+ reached by accident.
98
+
99
+ `placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`. The
100
+ platform matches it against nothing today, never shows it to a player, and does not vary its
101
+ decision on it. It exists so your own analytics can tell two break points apart, so name it for
102
+ yourself.
103
+
104
+ **It has a grammar, and it is narrow on purpose: 1 to 32 characters of `a-z`, `0-9`, `-` and `_`.**
105
+ Lowercase, no spaces, no accents — `'Boss Fight'` is not a placement, `'boss-fight'` is. Anything
106
+ else resolves `no_ad` and is reported to your `onError` handler, so you meet it while you are
107
+ building rather than later. The reason it is enforced now, while nothing depends on it, is that
108
+ this is the field a promotion is eventually **keyed by**: the Arcade will forward it unchanged, and
109
+ a name that does not fit the platform's grammar would be a name that stopped working after your
110
+ game had shipped. The bound is the platform's, not the SDK's, and the two agree.
111
+
112
+ ```js
113
+ async function onLevelComplete() {
114
+ showResultsScreen();
115
+ await gem.ads.requestAd({ placement: 'level-complete', format: 'interstitial' });
116
+ // Settled either way. The results screen is still up; the player taps Next.
117
+ enableNextButton();
118
+ }
119
+ ```
120
+
121
+ ## What never crosses
122
+
123
+ Your game never learns what was shown — no title, no image, no id, nothing about the other game.
124
+ You receive an outcome and nothing else.
125
+
126
+ You are also never told *why* the answer was no. That is deliberate and permanent: a reason would
127
+ tell your game something about the player that game code is not told, and there will not be a
128
+ fourth outcome value naming one. The Arcade also settles every `no_ad` and `unavailable` on a
129
+ schedule that does not depend on which branch produced it, so there is nothing in the timing to
130
+ read either. (`shown` is not held back — how long that one takes is how long your player spent
131
+ looking at a panel.)
132
+
133
+ ## If you do nothing
134
+
135
+ Nothing happens. A game that never calls `gem.ads.requestAd` is never interrupted, and the call is
136
+ not on the list of things a game must do to ship. This page exists for games that want a break to
137
+ be worth something.