@indiegems/gem-web-sdk 8.0.0 → 9.0.0

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.
@@ -0,0 +1,464 @@
1
+ # Rewarded ads
2
+
3
+ A rewarded placement is a button your player presses for something your game gives — a revive, an
4
+ extra attempt at today's puzzle. Your game asks the Arcade. Where an ad serves, the Arcade shows its
5
+ own offer sheet and then the ad; either way the platform decides whether the player is rewarded and
6
+ writes the reward into **your game's own inventory** for that player. Your game never grants the
7
+ reward, and never learns from the answer whether one was granted. It reads its inventory.
8
+
9
+ ```js
10
+ await gem.ads.requestAd({ placement: 'classic_revive', format: 'rewarded' });
11
+ gem.me.inventory.refresh(); // drop what is cached
12
+ const snap = await gem.me.inventory.snapshot(); // a reward granted before the answer is here
13
+ ```
14
+
15
+ That is the whole of the asking. What makes it pay is four things you set up once:
16
+
17
+ 1. **A reward** — an item or currency of your own that the player keeps (§1).
18
+ 2. **A placement** in your game config, saying what one press grants and how often (§2).
19
+ 3. **A spend** — the recipe your game runs when the player uses the reward (§4).
20
+ 4. **A button** that names the reward (§5).
21
+
22
+ The same asking and spending code runs whether the Arcade plays a test ad, grants the reward with no
23
+ ad, or plays a real one (§8), and moving between those needs no new SDK version and no config
24
+ change.
25
+
26
+ ## 1. The reward is your own, and the player keeps it
27
+
28
+ A reward is one to four grants of **your game's own** items or currencies, up to 100 of each (a
29
+ platform currency only as below). It must be something the player keeps and cannot pass on, and
30
+ the upload refuses a config where it is not:
31
+
32
+ - **Soulbound.** Every item or currency it grants has `transfer_policy: none`. A reward cannot be
33
+ traded, gifted or moved.
34
+ - **An ordinary item or a currency.** Not a bundle, not a loot box, and not a progression.
35
+ - **A platform currency only where the platform lists it.** The upload refuses a reward that
36
+ names a platform currency unless `gemctl platform config` lists that currency, for your game, as
37
+ grantable by `rewarded`.
38
+ - **Never convertible into one of those.** The upload follows every recipe and catalog entry that
39
+ spends the reward, then everything that spends what those grant, and so on. If that chain reaches
40
+ anything transferable, a platform currency `gemctl platform config` does not list as grantable by
41
+ `rewarded`, a bundle, a loot box, or a progression that fires a platform grant, the upload is
42
+ refused, and the finding names the path from the reward to the thing it reached.
43
+ - **Never spent on a loot draw.** Neither the reward nor anything it converts into may be a cost
44
+ of a recipe that rolls a loot table, so "spend a token to spin the wheel" is refused too.
45
+ - **A plain `display_name`.** The Arcade prints it on its offer sheet — "1 × Revive token" — in
46
+ its own chrome, so a name with markup or invisible characters is refused.
47
+
48
+ And three things the upload warns about rather than refuses:
49
+
50
+ - **Keep `client_writable: false` on the reward.** If your game's client may write it, a modified
51
+ client can grant itself one without an ad, and the placement is decoration.
52
+ - **Prefer a reward that is not also sold for a platform currency.** A reward counts as sold for
53
+ one when it carries a platform price itself, when a recipe priced in a platform currency grants
54
+ it, or when anything it converts into (the chain above) is sold either way. Such a placement is
55
+ allowed, with a warning, but it must say `without_ads: nothing` (§2) — the upload refuses one
56
+ that says `grant` — and outside test ads it is only ever served behind an ad whose completion
57
+ the platform verifies with the ad provider.
58
+ - **Keep its `display_name` to 40 characters or fewer.** The offer sheet cuts a longer one short.
59
+
60
+ For a revive, the reward is a single-use token:
61
+
62
+ ```yaml
63
+ items:
64
+ - id: '<uuid>'
65
+ slug: revive_token
66
+ item_type: item
67
+ display_name: 'Revive token' # the offer sheet says "1 × Revive token"
68
+ fungible: true
69
+ stack_size: 1
70
+ consumable: true
71
+ transfer_policy: none # a reward is never transferable
72
+ client_writable: false # or a modified client can grant itself one
73
+ ```
74
+
75
+ `gemctl config schema` is the authoritative field list for all of this, and for §2 and §4.
76
+
77
+ ## 2. Declare the placement in your game config
78
+
79
+ Each placement your game asks for is one entry in the `rewarded_placements` section, keyed by the
80
+ same string your game passes as `placement`:
81
+
82
+ ```yaml
83
+ rewarded_placements:
84
+ - placement: classic_revive # exactly the string your game passes
85
+ grants:
86
+ - { type: item_grant, item_definition_slug: revive_token, quantity: 1 }
87
+ max_per_day: 5 # optional: leave it out for no daily cap
88
+ max_held: 1 # no grant while the player already holds one
89
+ without_ads: grant # required: what happens where no ad serves
90
+ - placement: daily_drop_extra
91
+ grants:
92
+ - { type: item_grant, item_definition_slug: daily_drop_attempt, quantity: 1 }
93
+ max_per_day: 2
94
+ max_held: 1
95
+ without_ads: grant
96
+ ```
97
+
98
+ | field | what it says |
99
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `placement` | The name your game passes. 1 to 32 characters of `a-z`, `0-9`, `-` and `_`, unique in the document. At most 16 placements per game. |
101
+ | `grants` | 1 to 4 `item_grant` or `currency_grant` operations on your own items and currencies (§1). A `currency_grant` takes an `amount`; an `item_grant` takes a `quantity`. |
102
+ | `max_per_day` | Grants per player per UTC day. **Optional**: leave it out and the placement has no daily cap (§6). |
103
+ | `cooldown_seconds` | Seconds between two grants to one player, 0 to 86400. Defaults to 0. |
104
+ | `max_held` | No grant while the player already holds at least this many of the granted item. 1 to 100, and only on a placement whose grants are all items. |
105
+ | `without_ads` | **Required, with no default:** `grant` or `nothing`. What an ask gets where no ad serves your channel (§8). |
106
+ | `allowance_currency_slug` | Optional: one of your own currencies that each grant also spends 1 of. Use it when another path in your game must share the same daily cap. It must not be `client_writable`, must have `transfer_policy: none`, and needs a bucket that resets (daily, say) or it never refills. |
107
+
108
+ The placement only **selects** a reward your own config declared. Your game sends no reward, no
109
+ amount and no item in the request — nothing from your frame is trusted for that — so a modified
110
+ client can ask for a declared placement and nothing else.
111
+
112
+ Ship it the way every config section ships: upload, then promote to a channel. A channel serves
113
+ the placements in the config version promoted to it, and a reward can be retuned, or `without_ads`
114
+ flipped, with a new upload and a promote, with no new build.
115
+
116
+ ⚠️ **Check that `gemctl config schema` lists `rewarded_placements` before you rely on it.** The
117
+ upload drops a top-level section the backend does not know without a word, so a placement uploaded
118
+ where the schema does not list it does nothing at all.
119
+
120
+ ## 3. Ask, then re-read
121
+
122
+ ```js
123
+ await gem.ads.requestAd({ placement: 'classic_revive', format: 'rewarded' });
124
+ gem.me.inventory.refresh();
125
+ const snap = await gem.me.inventory.snapshot();
126
+ ```
127
+
128
+ The answer is one of the same three outcomes every `requestAd` gets, and for `rewarded` none of
129
+ them says whether a reward was granted:
130
+
131
+ | outcome | what happened | was anything granted? |
132
+ | ------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133
+ | `shown` | the Arcade painted its offer sheet, and it has closed, whatever the player chose in it | if the player watched to the end, yes, before the answer; if they declined or skipped, or no ad was available after all, no |
134
+ | `no_ad` | nothing was painted | maybe: in some cases the platform grants with no ad — where no ad serves and the placement says `without_ads: grant` (§8), for one — and then the reward was granted before the answer. Otherwise no |
135
+ | `unavailable` | the Arcade could not ask | almost always no, but read your inventory anyway |
136
+
137
+ So **branch on your inventory, never on the outcome.** Re-read it once the ask settles, whatever it
138
+ settled as.
139
+
140
+ **A reward can also arrive after that re-read.** Some ad providers confirm a finished ad from their
141
+ own server, a little after the player closes it. The Arcade waits a few seconds for that, then
142
+ tells the player "Your reward is on its way", and the ask answers `shown` when its sheet closes,
143
+ with the reward still to come. So also re-read when the screen that spends the reward opens, and
144
+ spend a token the player already holds before asking for another (§5's code does both).
145
+ [`inventory-and-catalogs.md` §6](inventory-and-catalogs.md#6-when-to-re-read) is the general rule.
146
+
147
+ A granted reward shows in your order history like any other change, as an order the platform made
148
+ with `source` `reward`.
149
+
150
+ **While the Arcade's sheet is up, your game is paused,** the same way it is for an interstitial:
151
+ the Arcade sends the pause your game already handles and lifts it when the sheet closes. Resume
152
+ when the promise settles, not on a timer. A second `requestAd` while one is still in flight
153
+ resolves `no_ad` at once, so disable the button between the press and the answer.
154
+
155
+ ## 4. Spend it with a recipe
156
+
157
+ Using the reward — the revive itself — is a server call: a recipe that removes the token. There is
158
+ no `consume` recipe type; a consume is an `exchange` whose `costs` remove the item. In one of your
159
+ catalogs:
160
+
161
+ ```yaml
162
+ - id: '<uuid>'
163
+ slug: use_revive
164
+ recipe_type: exchange
165
+ client_writable: true # the RECIPE is client-executable; the ITEM stays client_writable: false
166
+ costs:
167
+ - { type: item_remove, item_definition_slug: revive_token, quantity: 1 }
168
+ ```
169
+
170
+ ```js
171
+ await gem.me.mutations.executeRecipe(ACTIONS_CATALOG_ID, USE_REVIVE_RECIPE_ID);
172
+ revive(); // only after the recipe resolves
173
+ ```
174
+
175
+ The server checks that the player holds a token and removes it in the same step, which a modified
176
+ client cannot fake. **Apply the revive only after the recipe resolves.**
177
+
178
+ **If it fails, do not send it again; re-read the inventory.** A timeout or a dropped connection
179
+ proves nothing — the spend may have landed — so let the inventory decide, as it does for the
180
+ reward itself: a token that was there before the call and is gone now was spent, and one that is
181
+ still there was not. [`inventory-and-catalogs.md`
182
+ §11](inventory-and-catalogs.md#11-conditional-writes-and-why-most-writes-do-not-want-them) is the
183
+ general rule for a write that timed out.
184
+
185
+ Why not remove the item from the client with a mutation? Because that needs the item to be
186
+ `client_writable`, and then a modified client can grant it too (§1).
187
+
188
+ **If the reward feeds anything competitive, check the spend on your server.** An extra attempt at a
189
+ daily puzzle that posts to a leaderboard is only as honest as whatever accepts the result: the
190
+ server or function that does should confirm each attempt beyond the free one spent a token. A
191
+ client-side check is a check a modified client skips.
192
+
193
+ ## 5. Draw the button, and name the reward
194
+
195
+ **Label the button with what the player gets — "Revive", "+1 attempt" — and nothing else.**
196
+
197
+ - **Do not write "Watch an ad to …".** Where no ad serves (§8), no ad plays, and the label would be
198
+ false. The Arcade's offer sheet is the ad disclosure: it says that it is an ad and what the player
199
+ gets, and offers "No thanks", before anything plays, and it appears only when an ad will.
200
+ - **Never a label that reads as the only way forward.** "Tap to continue" on a button that asks
201
+ for an ad disguises the ask. Draw the ordinary way on — end the run, back to the menu — beside
202
+ it, so pressing it is a choice.
203
+ - **When the press paid nothing**, show a neutral line in your own UI — "No revive this time" —
204
+ and carry on. Never guess a reason: you are not told one.
205
+
206
+ Your own rules decide when the button appears at all — once per run, only on the death screen,
207
+ never in a ranked mode — because the platform cannot see a run (§6).
208
+
209
+ **Whether it can pay right now is `gem.ads.rewardedStatus`** (SDK 8.1.0 and later). It reads the
210
+ placement's caps as the platform holds them, `declared`, `grantable`, `remainingToday`,
211
+ `nextEligibleAt`, `heldLimitReached` and `resetsAt`, so you can grey the button out or say when it
212
+ is back instead of offering a press that cannot pay. `grantable` is not a promise: an ask can still
213
+ answer `no_ad` for reasons the read never reports. It reserves nothing, and it never rejects. See
214
+ [cross-promotion.md](cross-promotion.md):
215
+
216
+ ```js
217
+ const status = await gem.ads.rewardedStatus('classic_revive');
218
+ reviveButton.disabled = !status.grantable;
219
+ ```
220
+
221
+ For a revive, the button's handler:
222
+
223
+ ```js
224
+ async function reviveTokens() {
225
+ gem.me.inventory.refresh(); // drop what is cached
226
+ try {
227
+ // Every revive_token the player holds, read by the slug from your config.
228
+ const held = await gem.me.inventory.instances('revive_token');
229
+ return held.reduce((n, item) => n + item.quantity, 0);
230
+ } catch (err) {
231
+ // A player who holds none of an item is answered `not_found`, not an empty
232
+ // list. That is a count of zero, and it is the usual case: the first press,
233
+ // and the re-read after a spend that took the last token.
234
+ if (err?.reason === 'not_found') return 0;
235
+ throw err;
236
+ }
237
+ }
238
+
239
+ async function spendRevive(before) {
240
+ try {
241
+ await gem.me.mutations.executeRecipe(ACTIONS_CATALOG_ID, USE_REVIVE_RECIPE_ID);
242
+ return true;
243
+ } catch {
244
+ // Refused, or no answer. A spend that timed out may still have landed, so never
245
+ // send it again: the inventory decides (§4).
246
+ return (await reviveTokens()) < before;
247
+ }
248
+ }
249
+
250
+ async function onRevivePressed() {
251
+ if (run.revived) return; // the game's own rule: one revive per run
252
+ reviveButton.disabled = true; // only between the press and the answer
253
+ let revived = false;
254
+ try {
255
+ if ((await reviveTokens()) === 0) {
256
+ // A token left over from earlier is spent first, so ask only when there is none.
257
+ await gem.ads.requestAd({ placement: 'classic_revive', format: 'rewarded' });
258
+ }
259
+ const held = await reviveTokens();
260
+ revived = held > 0 && (await spendRevive(held));
261
+ } catch {
262
+ // A read failed. That is no revive this time; the player can press again.
263
+ }
264
+ reviveButton.disabled = false;
265
+ if (revived) {
266
+ run.revived = true;
267
+ revive(); // only once the spend is known to have landed
268
+ } else {
269
+ showNoRevive(); // "No revive this time": neutral, and no reason
270
+ }
271
+ }
272
+ ```
273
+
274
+ ## 6. Caps live on the placement
275
+
276
+ The caps are **yours to declare and the platform's to enforce.** The reward is your game's own
277
+ economy, so you choose its limits, and the platform holds them on its side, where a modified client
278
+ cannot reach them. They apply the same with an ad and without one.
279
+
280
+ | limit | declared by | enforced by |
281
+ | ---------------------------------------------------------------------------------------- | --------------------- | ------------- |
282
+ | `max_per_day` — grants per player per UTC day | you, optionally | the platform |
283
+ | `cooldown_seconds` — the time between two grants | you | the platform |
284
+ | `max_held` — no grant while the player holds this many | you, for item rewards | the platform |
285
+ | at least 15 seconds between two rewarded offers to one player in one game | the platform | the platform |
286
+ | at least 15 seconds between two rewards granted with no ad, in any of the player's games | the platform | the platform |
287
+ | once per run, only on the death screen, never in ranked | you | **your game** |
288
+
289
+ - **The second spacing reaches across games.** If another game granted this player a reward with
290
+ no ad a few seconds ago, an ask in yours that would also grant with no ad (§8) answers `no_ad`
291
+ with nothing granted, even though your game has offered nothing.
292
+ - **What counts toward a cap** is a grant, with or without an ad, and an ad that is still playing.
293
+ An offer the player declined, one no ad was available for, and an ad that was skipped or failed
294
+ do not count, so "No thanks" costs the player nothing against your caps. Every offer does start
295
+ the first spacing, though: a player who says "No thanks" and presses again within 15 seconds
296
+ gets `no_ad` and nothing.
297
+ - **The platform may hold a daily cap lower than the one you declared, never higher.** It sets no
298
+ daily limit of its own unless one is configured, and a configured one applies to every placement
299
+ from the next ask, with no upload from you.
300
+ - ⚠️ **Nothing protects your economy by default but your own caps.** A placement with no
301
+ `max_per_day` and no `cooldown_seconds` can pay once every 15 seconds — with an ad, or, under
302
+ `without_ads: grant`, without one. `max_held: 1` stops a player banking a second token; it does
303
+ not stop them spending one and earning the next. Declare the daily cap and cooldown your economy
304
+ needs.
305
+ - **"Once per run" is yours.** The platform has no idea what a run is. `max_held: 1` is the
306
+ backstop that keeps it hard to abuse: a player can never hold two revives at once.
307
+
308
+ This is the opposite of the rule for platform currencies, where a game declares no caps and the
309
+ platform sets them all. A rewarded reward is your economy, so the caps are yours.
310
+
311
+ ## 7. Pacing
312
+
313
+ - **A rewarded ask is never held back by an interstitial.** A player who presses your button is not
314
+ refused because a promotion played a minute ago, in your game or another.
315
+ - **An ad that starts holds off the next interstitial.** Do not follow a rewarded ad with an
316
+ interstitial at the same break; that ask answers `no_ad`.
317
+ - **Rewarded has its own spacing:** your placement's `cooldown_seconds`, and the platform's 15
318
+ seconds (§6). An ask inside any of them answers `no_ad` with nothing granted, so do not re-ask on
319
+ a timer. A player who presses again later gets a fresh ask.
320
+ - A grant made with no ad holds nothing off, because nothing was shown.
321
+
322
+ ## 8. Where an ad plays, and where it does not
323
+
324
+ Each channel of your game is in one of these states at a time. Which one is the platform's to set,
325
+ not yours, and moving between them needs **no rebuild and no config change**:
326
+
327
+ | state | channels | what pressing the button does |
328
+ | -------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
+ | **Off** | any | Every rewarded ask answers `no_ad`, and nothing is granted. |
330
+ | **A — test ads** | `dev` and `test` only | The Arcade paints a sheet marked **Test ad**. Finishing the test ad grants the reward for real, into that channel's inventory only (§10). |
331
+ | **B — no ad source** | `release` | No ad plays. Under `without_ads: grant` the reward is granted within your caps, with no ad, and the ask answers `no_ad`. Under `nothing`, nothing is granted. |
332
+ | **C — an ad source** | `release` | The Arcade's offer sheet, then an ad; the reward is granted once the ad finishes. `without_ads` does not apply. |
333
+
334
+ **On gemarcade.com, every channel is Off today**, `dev` and `test` included. Every rewarded ask
335
+ there answers `no_ad` and grants nothing, whatever your placement's `without_ads` says, so a button
336
+ you ship pays nothing yet. Test ads (state A) are switched on there first; until then you can ship
337
+ the code in §3–§5, which answers `no_ad`, and the day a channel's state changes it starts working
338
+ with no rebuild. The platform's internal test environment already runs state A on `dev` and `test`
339
+ and state B on `release`, which is where the platform rehearses it before gemarcade.com.
340
+ **Test ads never serve on `release`**, and a reward granted on `dev` or `test` never reaches
341
+ `release`: inventory is per channel.
342
+
343
+ What each state means for a game:
344
+
345
+ - **Build the final shape now.** The code in §5 is the same code in every state. Under `without_ads:
346
+ grant`, a game whose reward should be free until ads serve — the same benefit, under the same caps
347
+ — gets exactly that in state B, and the same placement shows an ad first in state C, with nothing
348
+ to change. Choose `nothing` if a reward should only ever follow an ad.
349
+ - **In state C, some players get nothing for a press.** A player the ad source cannot serve gets
350
+ `no_ad` and no reward, and `without_ads` does not apply to them. Your game is not told which
351
+ players those are. So a placement that is free for everyone in state B is not free for every
352
+ player in state C. Write §5's "No revive this time" path as an ordinary path, because it is one.
353
+ - **A visitor with no Arcade account, not even a guest one, is never rewarded**, in any state.
354
+
355
+ ## 9. The placement stays on the platform
356
+
357
+ Your placement name goes to the platform, which matches it against your config, and **never to an
358
+ ad network** — and neither does any other string your game writes. So:
359
+
360
+ - **Name a placement for the reward, for your own reading** — `classic_revive`, `daily_drop_extra`.
361
+ There is nothing to provision and nothing to register with a network, so a placement you declare
362
+ later serves like any other.
363
+ - **Do not put an identifier in it**, and do not expect to see it on any ad report. Per-placement
364
+ numbers are the platform's, not a network's.
365
+ - **Never embed an ad SDK or ad tag in your game.** The Arcade plays every ad itself; your game
366
+ asks, and reads its inventory.
367
+
368
+ ## 10. Testing it
369
+
370
+ Where your `dev` and `test` channels are in state A (§8), rewarded is testable end to end against
371
+ the **test ad** source, before any real ad serves. Test grants are real: they land in that
372
+ channel's inventory, so your game's re-read finds them and §4's recipe spends them.
373
+
374
+ Before you start:
375
+
376
+ 1. `gemctl config schema` lists `rewarded_placements` (§2).
377
+ 2. The config version promoted to the channel declares your placement, and your items and recipe.
378
+ 3. The channel has a build and a store page, so it appears in the Arcade at all.
379
+
380
+ ### At the Arcade, on `dev` or `test`
381
+
382
+ Play your game on its `dev` or `test` channel. `dev` is open to your team and `test` to your
383
+ testers. Pressing the button paints the Arcade's sheet, marked **Test ad**, naming your reward and
384
+ saying the reward lands on that channel only. **Watch** opens a test stage with a short countdown,
385
+ then **Claim reward**; the reward lands, the sheet says what was received, and the ask answers
386
+ `shown` when the player presses **Continue**. **No thanks** answers `shown` with nothing granted,
387
+ and so does a sheet nobody answers, which declines itself after a minute.
388
+
389
+ The test sheet also has **Simulate no fill**, **Simulate failure** and **Skip (no reward)**, each
390
+ answering `shown` with nothing granted. To pick a behaviour before the sheet opens, add
391
+ `gem_test_ads=<mode>` to the play page's address:
392
+
393
+ | `gem_test_ads=` | what it does |
394
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
395
+ | `instant` | after **Watch**, claims the reward when the countdown ends, with no **Claim reward** click; **Continue** still closes the sheet |
396
+ | `no_fill` | the sheet reports that no ad was available; **Close** answers `shown`, no reward |
397
+ | `fail` | **Watch** fails at once; **Continue** answers `shown`, no reward |
398
+ | `skip` | **Watch** starts the ad and skips it: `shown`, no reward |
399
+ | `free` | asks as if no ad source served the channel, so your placement's `without_ads` decides: `grant` grants with no sheet and answers `no_ad`; `nothing` gives nothing. This is how you see state B before you ship |
400
+
401
+ **Only `free` needs no click.** The default, `instant`, `fail` and `skip` open on the offer sheet
402
+ and wait for a person to press **Watch**, so an automated test that cannot press it waits out the
403
+ minute, after which the sheet declines itself and the ask answers `shown` with no reward.
404
+ `no_fill` has no such timer: its sheet waits for **Close**, so an unattended test gets
405
+ `unavailable` when the SDK stops waiting, after three minutes, with the sheet still up.
406
+
407
+ `gem_test_ads` is for the Arcade's play page, not your game, and it does nothing on `release`.
408
+
409
+ On `dev` and `test`, when an ask paints no sheet for a reason that is about your config or your
410
+ caps, the Arcade writes **one console line** saying which. It is never shown to a player, and never
411
+ written on `release`:
412
+
413
+ ```text
414
+ [gem] rewarded 'clasic_revive' on dev: placement not declared in this channel's config; nothing granted
415
+ [gem] rewarded 'classic_revive' on dev: the player already holds the placement's max_held; nothing granted
416
+ [gem] rewarded 'classic_revive' on dev: granted without an ad (no ad source on this channel)
417
+ ```
418
+
419
+ The lines it can write are config and cap facts only: the placement is not declared, no config is
420
+ promoted to the channel, the daily cap is reached, the cooldown is running, `max_held` is reached,
421
+ the ask came inside the 15-second spacing, the allowance is spent, the reward was granted with no
422
+ ad, or rewarded is off on the channel. Your game sees none of them, and a no that is about the
423
+ player is never written there.
424
+
425
+ **No sheet and no console line at all** means the no was about something those lines never cover:
426
+
427
+ - rewarded is not switched on in the Arcade you are playing in at all (where it is on but off for
428
+ your channel, the line says so);
429
+ - your game asked while its page was hidden, or while another Arcade panel was over it;
430
+ - the reward would not fit under a limit on the item or currency itself — a currency's maximum
431
+ balance, say. Only `max_held` is reported, so declare it rather than lean on the item's own
432
+ limit;
433
+ - under `gem_test_ads=free`, a placement that says `without_ads: nothing`;
434
+ - something about the player or their session, which is never written.
435
+
436
+ ### In the dev host
437
+
438
+ `gemctl serve` and the Gem Editor play a rewarded ask through the same test-ad source, for your
439
+ game's **`dev`** channel, as **your own account**. A claimed reward is real: it lands in your own
440
+ `dev` inventory for that game, so your re-read and §4's recipe behave as they will at the Arcade.
441
+
442
+ - **Pick the behaviour on the framing page's Start screen**: the default sheet, then **Watch**, a
443
+ 2-second test stage, then **Claim reward**; `instant`, which pays with no clicks (the one an
444
+ automated test wants); `decline`; `abandon`; `no_fill`; `error`, which answers `unavailable`
445
+ with nothing asked; and `free`, which rehearses `without_ads` with no sheet (§8). You can also add
446
+ `?rewarded=<mode>` to the framing page's own address, but not to the link `gemctl serve` prints,
447
+ because its redirect drops the query.
448
+ - **Each ask writes one line** saying what happened, and why when nothing arrived, to
449
+ `gemctl serve log`, the Editor's Log page and the page's console.
450
+ - **Signed out, a game not linked to your project, the Editor's game window, or another device on
451
+ the network** get a panel marked **Simulated**, and nothing is granted.
452
+ - **It works where your game's `dev` channel serves test ads (state A).** The dev host talks to
453
+ gemarcade.com, so until test ads are switched on there, every offer answers that rewarded ads
454
+ are not served yet.
455
+ - **To test a cap again today**, clear your own rewarded use for the day:
456
+ `gemctl rewarded reset --game <game id> --channel dev` (or `test`, and `--placement P` for one
457
+ placement). It is refused on `release`, it touches only your own account, and it leaves your
458
+ inventory alone, so a `max_held` that is reached stays reached until you spend the reward.
459
+
460
+ ## If you do nothing
461
+
462
+ Nothing happens. A game that never asks for `rewarded` is never shown a sheet, and nothing here is
463
+ on the list of things a game must do to ship. A rewarded ask for a placement your config does not
464
+ declare answers `no_ad` and grants nothing.