@indiegems/gem-web-sdk 8.1.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.
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "8.1.0"
5
+ "version": "9.0.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -3978,14 +3978,20 @@
3978
3978
  "url": "https://gemarcade.com/developer/api/game#adformat",
3979
3979
  "signature": "type AdFormat = \"interstitial\" | \"rewarded\"",
3980
3980
  "documented": true,
3981
- "summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n\n⚠️ `rewarded` is part of the vocabulary and is answered `no_ad` today. It is\nhere so a game's own analytics can distinguish the two kinds of break, and so\nthis request has the shape an ad request has everywhere. Even when it is\nserved, the outcome grants nothing — a reward is granted by the platform,\nagainst its own record, never by your game reading `shown`. It arrives on\nyour game's inventory for this player, written by the platform, so re-read\n`gem.me.inventory` after a `rewarded` ask settles rather than waiting for a\nsignal on this pair; there will not be one.",
3981
+ "summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n\n`rewarded` is a button the player pressed for a reward — a revive, an extra\nattempt — that your game config declares for the placement in\n`rewarded_placements`. It is served where this arcade serves it, and answered\n`no_ad` where it does not. ⚠️ The outcome grants nothing: the platform grants\nthe reward, against its own record, never your game reading `shown`. It\narrives on your game's inventory for this player, written by the platform,\nso re-read `gem.me.inventory` after a `rewarded` ask settles rather than\nwaiting for a signal on this pair; there will not be one.",
3982
3982
  "deprecated": null,
3983
3983
  "params": [],
3984
3984
  "returns": null,
3985
3985
  "throws": null,
3986
3986
  "example": null,
3987
3987
  "defaultValue": null,
3988
- "guides": []
3988
+ "guides": [
3989
+ {
3990
+ "slug": "rewarded-ads",
3991
+ "anchor": null,
3992
+ "url": "https://gemarcade.com/developer/docs/rewarded-ads"
3993
+ }
3994
+ ]
3989
3995
  },
3990
3996
  {
3991
3997
  "id": "AdRequest",
@@ -4012,7 +4018,7 @@
4012
4018
  "url": "https://gemarcade.com/developer/api/game#adrequest-placement",
4013
4019
  "signature": "placement: string",
4014
4020
  "documented": true,
4015
- "summary": "Your own name for the break — `level-complete`, `between-rounds`.\n\n**1 to 32 characters of `a-z`, `0-9`, `-` and `_`.** Lowercase, no spaces,\nno accents: `'Boss Fight'` is not a placement, `'boss-fight'` is. The\ngrammar is the platform's own, and it is narrow deliberately — this is the\nfield an ad unit is keyed by, so a name that does not fit it is a name that\nwould stop working later rather than now. A placement outside it is\nanswered `no_ad` and reported to your `onError` handler.\n\nThe platform matches it against nothing today and never shows it to a\nplayer. It is for your own telemetry, so two break points in your game can\nbe told apart.",
4021
+ "summary": "Your own name for the break — `level-complete`, `between-rounds`.\n\n**1 to 32 characters of `a-z`, `0-9`, `-` and `_`.** Lowercase, no spaces,\nno accents: `'Boss Fight'` is not a placement, `'boss-fight'` is. The\ngrammar is the platform's own, and the SDK enforces it so a name that does\nnot fit is refused at your keyboard rather than in the field. A placement\noutside it is answered `no_ad` and reported to your `onError` handler.\n\nFor `rewarded`, it names the reward your game config declares in\n`rewarded_placements`: the platform matches it against that config, and a\nplacement the config does not declare is answered `no_ad`. For\n`interstitial`, the platform matches it against nothing; there it is for\nyour own telemetry, so two break points in your game can be told apart.\nEither way it is never shown to a player and **never sent to an ad\nnetwork**.",
4016
4022
  "deprecated": null,
4017
4023
  "params": [],
4018
4024
  "returns": null,
@@ -4046,7 +4052,7 @@
4046
4052
  "url": "https://gemarcade.com/developer/api/game#adoutcomekind",
4047
4053
  "signature": "type AdOutcomeKind = \"shown\" | \"no_ad\" | \"unavailable\"",
4048
4054
  "documented": true,
4049
- "summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `shown` — a panel was painted and has now closed. Resume. **It grants\n nothing.**\n- `no_ad` — nothing was painted. Carry on exactly as if you had not asked.\n This is the common answer.\n- `unavailable` — the arcade could not render just now. Not a softer no; read\n it exactly as you read `no_ad`.\n\nNo reason is given for a `no_ad`, and one will never be added: a reason would\ntell your game something about the player — an age, a consent state, a\nsubscription — that game code is deliberately not told.\n\nThe one thing you can learn is about **your own economy**, and not from this\nanswer: [AdsApi.rewardedStatus](https://gemarcade.com/developer/api/game#adsapi-rewardedstatus) reports your own declared rewarded caps\nas the platform holds them (a platform-wide per-placement maximum, where one\nis set, can lower a cap). It never reports anything about the player.",
4055
+ "summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `shown` — a panel was painted and has now closed. Resume. **It grants\n nothing.**\n- `no_ad` — nothing was painted. Carry on exactly as if you had not asked.\n This is the common answer. After a `rewarded` ask the reward may still\n have landed, with no ad, so re-read your inventory either way.\n- `unavailable` — the arcade could not render just now. Not a softer no; read\n it exactly as you read `no_ad`.\n\nNo reason is given for a `no_ad`, and one will never be added: a reason would\ntell your game something about the player — an age, a consent state, a\nsubscription — that game code is deliberately not told.\n\nThe one thing you can learn is about **your own economy**, and not from this\nanswer: [AdsApi.rewardedStatus](https://gemarcade.com/developer/api/game#adsapi-rewardedstatus) reports your own declared rewarded caps\nas the platform holds them (a platform-wide per-placement maximum, where one\nis set, can lower a cap). It never reports anything about the player.",
4050
4056
  "deprecated": null,
4051
4057
  "params": [],
4052
4058
  "returns": null,
@@ -4114,7 +4120,7 @@
4114
4120
  "url": "https://gemarcade.com/developer/api/game#adsapi-requestad",
4115
4121
  "signature": "requestAd(req: AdRequest): Promise<AdOutcome>",
4116
4122
  "documented": true,
4117
- "summary": "Ask whether the arcade has anything to show at this break in play.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it.",
4123
+ "summary": "Ask whether the arcade has anything to show at this break in play.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it. A `rewarded` placement's reward is granted by the\nplatform into your game's inventory; re-read `gem.me.inventory` once this\nsettles, whatever it settled as.",
4118
4124
  "deprecated": null,
4119
4125
  "params": [
4120
4126
  {
@@ -4128,13 +4134,7 @@
4128
4134
  "throws": null,
4129
4135
  "example": "```ts\nconst { outcome } = await gem.ads.requestAd({\n placement: 'level-complete',\n format: 'interstitial',\n});\n// `no_ad` is the ordinary answer. There is nothing to show and nothing\n// to draw; carry straight on to the next level.\nstartNextLevel();\n```",
4130
4136
  "defaultValue": null,
4131
- "guides": [
4132
- {
4133
- "slug": "cross-promotion",
4134
- "anchor": null,
4135
- "url": "https://gemarcade.com/developer/docs/cross-promotion"
4136
- }
4137
- ]
4137
+ "guides": []
4138
4138
  },
4139
4139
  {
4140
4140
  "id": "AdsApi.rewardedStatus",
@@ -7534,9 +7534,9 @@
7534
7534
  "anchor": "functionfailure",
7535
7535
  "group": "functions",
7536
7536
  "url": "https://gemarcade.com/developer/api/game#functionfailure",
7537
- "signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\"",
7537
+ "signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\" | \"output_too_large\" | \"argument_too_large\"",
7538
7538
  "documented": true,
7539
- "summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/game#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.",
7539
+ "summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/game#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.\n- `output_too_large` — your function's result was over the 1 MiB output cap.\n- `argument_too_large` — a value your function handed to a platform call\n (`log`, `call-api`, `download-link` or `upload-link`) was over the 16 MiB\n per-call limit.",
7540
7540
  "deprecated": null,
7541
7541
  "params": [],
7542
7542
  "returns": null,
@@ -7770,7 +7770,7 @@
7770
7770
  "url": "https://gemarcade.com/developer/api/game#arcade_embedders",
7771
7771
  "signature": "ARCADE_EMBEDDERS: readonly string[]",
7772
7772
  "documented": true,
7773
- "summary": "The origins the Gem Arcade embeds a game from, which every game accepts\nwithout naming them: the website, and the Gem Arcade app.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](https://gemarcade.com/developer/api/game#gemoptions-allowedembedders) adds to them rather than replacing them.",
7773
+ "summary": "The origins the platform embeds a game from, which every game accepts\nwithout naming them: the Gem Arcade website, the Meme Games website, and\nthe Gem Arcade app.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](https://gemarcade.com/developer/api/game#gemoptions-allowedembedders) adds to them rather than replacing them.",
7774
7774
  "deprecated": null,
7775
7775
  "params": [],
7776
7776
  "returns": null,
@@ -7804,7 +7804,7 @@
7804
7804
  "url": "https://gemarcade.com/developer/api/game#gemoptions-allowedembedders",
7805
7805
  "signature": "readonly allowedEmbedders?: readonly string[]",
7806
7806
  "documented": true,
7807
- "summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe website and the Gem Arcade app are always allowed — see\n[ARCADE_EMBEDDERS](https://gemarcade.com/developer/api/game#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](https://gemarcade.com/developer/api/game#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
7807
+ "summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe platform's websites and the Gem Arcade app are always allowed — see\n[ARCADE_EMBEDDERS](https://gemarcade.com/developer/api/game#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](https://gemarcade.com/developer/api/game#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
7808
7808
  "deprecated": null,
7809
7809
  "params": [],
7810
7810
  "returns": null,
@@ -21951,9 +21951,9 @@
21951
21951
  "anchor": "functionfailure",
21952
21952
  "group": "functions",
21953
21953
  "url": "https://gemarcade.com/developer/api/server#functionfailure",
21954
- "signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\"",
21954
+ "signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\" | \"output_too_large\" | \"argument_too_large\"",
21955
21955
  "documented": true,
21956
- "summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.",
21956
+ "summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.\n- `output_too_large` — your function's result was over the 1 MiB output cap.\n- `argument_too_large` — a value your function handed to a platform call\n (`log`, `call-api`, `download-link` or `upload-link`) was over the 16 MiB\n per-call limit.",
21957
21957
  "deprecated": null,
21958
21958
  "params": [],
21959
21959
  "returns": null,
package/docs/changelog.md CHANGED
@@ -16,6 +16,14 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 9.0.0
20
+
21
+ Upgrading from 8.1.0. Impact: required.
22
+
23
+ ### Must change
24
+
25
+ - FunctionFailure now has two more reasons, output_too_large and argument_too_large. A function that returns more than the output cap, or passes a platform call a value over the per-call size limit, used to fail as trap. It now fails with the matching new reason. Update any code that switches on FunctionFailure so it handles both new reasons. An exhaustive check over the old four reasons will stop compiling until you do. If your game treated trap as the sign of an oversized output or argument, handle the new reasons there too. Then rebuild your game against this release.
26
+
19
27
  ## 8.1.0
20
28
 
21
29
  Upgrading from 8.0.0. Impact: required.
@@ -4,6 +4,11 @@ The Arcade can show a **full-screen panel about another game** at a break in you
4
4
  for one; the Arcade decides whether to show anything, draws it in its own chrome over your paused
5
5
  game, and tells you when it is done.
6
6
 
7
+ The same call with `format: 'rewarded'` is a button the player presses for something your game
8
+ gives — a revive, an extra attempt. That one has its own page,
9
+ [`rewarded-ads.md`](rewarded-ads.md). This page is about the interstitial; where a rule here reads
10
+ differently for a rewarded ask, it says so.
11
+
7
12
  ```js
8
13
  const { outcome } = await gem.ads.requestAd({
9
14
  placement: 'level-complete',
@@ -37,6 +42,12 @@ So write the `no_ad` path as your ordinary path:
37
42
  - Do **not** hold up your own flow waiting for a fill. Ask, and continue when the promise settles.
38
43
  - Do **not** retry on a no. A second ask at the same break gets the same answer and spends a slot.
39
44
 
45
+ **A rewarded ask reads differently, because the player pressed your button and is waiting on it.**
46
+ Its `no_ad` can arrive with the reward already granted (§3), so branch on your inventory, never on
47
+ the outcome; and where nothing landed, a neutral line in your own UI — "No revive this time" — is
48
+ right, with no reason given. [`rewarded-ads.md`
49
+ §5](rewarded-ads.md#5-draw-the-button-and-name-the-reward) is that button.
50
+
40
51
  ## 2. Nothing rejects
41
52
 
42
53
  `requestAd` always resolves. There is no error branch and no `try`/`catch` to write. Even a refusal
@@ -49,36 +60,38 @@ resolves `no_ad`, so your game keeps exactly one branch. (The mistake is still r
49
60
  `shown` says a panel was painted and has closed. It is **not** a receipt, **not** an entitlement,
50
61
  and **not** permission to hand anything out.
51
62
 
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.
63
+ That holds for `format: 'rewarded'` too, the one format that pays the player. The platform grants a
64
+ rewarded placement's reward itself, against its own record of what the player did on the Arcade's
65
+ offer sheet — never your game, by reading an outcome. An outcome travels over the same channel as
66
+ everything else your frame receives, and a game that unlocks a level or grants a currency on the
67
+ strength of one has an unlock that any page embedding your game can forge.
57
68
 
58
69
  Treat a break as a break. If your game wants to give the player something for reaching the end of a
59
70
  level, give it to them for reaching the end of the level.
60
71
 
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:
72
+ **Where a rewarded grant arrives.** On your game's own inventory for this player, written by the
73
+ platform — never by your game — and read through `gem.me.inventory`, the same place a purchase
74
+ lands, read the same way. (That is the game-scoped inventory `gem.me` shows you, not the player's
75
+ platform wallet.) There is **no fourth outcome and no extra field** on the answer. `outcome` is
76
+ pinned at three values because a new value would break every game bundle already in the field: its
77
+ validator refuses the whole message. A new field would not break anything — validators ignore
78
+ fields they do not know — but it is not coming either: anything on that answer can be forged by the
79
+ page embedding your game, so a reward carried there would be a reward anyone could hand out. So
80
+ re-read your snapshot once a rewarded ask settles, whatever it settled as:
71
81
 
72
82
  ```js
73
83
  await gem.ads.requestAd({ placement: 'run-banked', format: 'rewarded' });
74
84
  gem.me.inventory.refresh(); // drop what is cached
75
- const snap = await gem.me.inventory.snapshot(); // the grant, if there was one, is here
85
+ const snap = await gem.me.inventory.snapshot(); // a reward granted before the answer is here
76
86
  ```
77
87
 
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.
88
+ A `no_ad` can come back with the reward already in that snapshot, because the platform grants
89
+ without an ad in some cases, so branch on the inventory and never on the outcome. And a reward that
90
+ an ad provider confirms from its own server can land after this re-read, so read again when the
91
+ screen that spends it opens; [`inventory-and-catalogs.md`
92
+ §6](inventory-and-catalogs.md#6-when-to-re-read) is the general form of this.
93
+ [`rewarded-ads.md`](rewarded-ads.md) is the rest: declaring what a placement grants, spending it,
94
+ drawing the button, and testing.
82
95
 
83
96
  **Whether a rewarded button could pay right now.** A rewarded placement is a button the player
84
97
  presses, so you need to know whether to draw it. `gem.ads.rewardedStatus(placement)` reads one
@@ -124,20 +137,23 @@ If you have not wired the pause at all yet, that is the thing to fix first;
124
137
 
125
138
  At a break **the player already expects**: a level ending, a round finishing, a run being banked, a
126
139
  return to a menu. Never mid-input, never during a timed sequence, and never on a screen the player
127
- reached by accident.
140
+ reached by accident. (A rewarded ask goes where the player pressed your button for it, and nowhere
141
+ else; [`rewarded-ads.md`](rewarded-ads.md) covers the button.)
128
142
 
129
- `placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`. The
130
- platform matches it against nothing today, never shows it to a player, and does not vary its
131
- decision on it. It exists so your own analytics can tell two break points apart, so name it for
132
- yourself.
143
+ `placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`. For
144
+ an interstitial, the platform matches it against nothing, never shows it to a player, and does not
145
+ vary its decision on it; it exists so your own analytics can tell two break points apart, so name
146
+ it for yourself. For `format: 'rewarded'`, the placement names the reward your game config declares
147
+ in `rewarded_placements`, and the platform matches it against that config. Either way it **never
148
+ reaches an ad network**, and neither does any other string your game writes.
133
149
 
134
150
  **It has a grammar, and it is narrow on purpose: 1 to 32 characters of `a-z`, `0-9`, `-` and `_`.**
135
151
  Lowercase, no spaces, no accents — `'Boss Fight'` is not a placement, `'boss-fight'` is. Anything
136
152
  else resolves `no_ad` and is reported to your `onError` handler, so you meet it while you are
137
- building rather than later. The reason it is enforced now, while nothing depends on it, is that
138
- this is the field a promotion is eventually **keyed by**: the Arcade will forward it unchanged, and
139
- a name that does not fit the platform's grammar would be a name that stopped working after your
140
- game had shipped. The bound is the platform's, not the SDK's, and the two agree.
153
+ building rather than later. The platform holds the same grammar where it reads the name: for a
154
+ rewarded ask the Arcade forwards the placement unchanged, and a config that declares one outside
155
+ the grammar is refused at upload, so a name that did not fit would be a reward your shipped game
156
+ could never reach. The bound is the platform's, not the SDK's, and the two agree.
141
157
 
142
158
  ```js
143
159
  async function onLevelComplete() {
@@ -160,7 +176,11 @@ schedule that does not depend on which branch produced it, so there is nothing i
160
176
  read either. (`shown` is not held back — how long that one takes is how long your player spent
161
177
  looking at a panel.)
162
178
 
163
- The one thing you can read is your own economy, and never from the answer:
179
+ A rewarded ask is the one case where something else reaches your game: the reward itself, in your
180
+ own inventory, when one was granted. That is what the ask is for. It says that a reward landed,
181
+ never why, so do not read a reason into it.
182
+
183
+ The other thing you can read is your own economy, and never from the answer:
164
184
  `gem.ads.rewardedStatus` reports **your own declared** rewarded caps as the platform holds them —
165
185
  the platform can hold a placement's daily cap below what you declared, never above — so you can
166
186
  tell when a rewarded `no_ad` was your own cap. It never reports anything about the player, the
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "8.1.0"
53
+ "@indiegems/gem-web-sdk": "9.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -33,13 +33,13 @@
33
33
  },
34
34
  {
35
35
  "file": "changelog.md",
36
- "sha256": "f13808374edf0988e0aef58b857c82298702a67d450962522a532b4827b74c91",
37
- "updated": "2026-10-07T16:20:42Z"
36
+ "sha256": "dd8afde50d0e34052ebdb8b894d30faa4814d4209fc62644fdc5ab53a1395e32",
37
+ "updated": "2026-10-08T00:55:05Z"
38
38
  },
39
39
  {
40
40
  "file": "cross-promotion.md",
41
- "sha256": "289cb4826a56b4e7423160e4187af65f4167c80218c2792ca0c01fbd977ecfec",
42
- "updated": "2026-10-06T18:49:36Z"
41
+ "sha256": "4336d72595115950919361c36ab1af9beaa787c3c57d27fea27d42dac69d81cf",
42
+ "updated": "2026-10-07T19:15:38Z"
43
43
  },
44
44
  {
45
45
  "file": "daily-challenges.md",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "222dcf81a2ed6d8fc8ffc37d5f7ef146dcf80c4b76f84fd9b8dd53ea1cbce23b",
72
- "updated": "2026-10-07T16:20:42Z"
71
+ "sha256": "95f645a73bfb522537fe14562d4a87b10a7f06af036c5658996c05bddced875b",
72
+ "updated": "2026-10-08T00:55:05Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
@@ -83,8 +83,8 @@
83
83
  },
84
84
  {
85
85
  "file": "match-session.md",
86
- "sha256": "738c54766f5489ba7509f5f31ef1387a2a80ad89f03eaccd52113a18f2765b0d",
87
- "updated": "2026-09-25T21:03:24Z"
86
+ "sha256": "33fdef4edc170cadd2284ff399fcf16455b7691d9aeac52bd09bade24b8b051a",
87
+ "updated": "2026-10-07T19:27:16Z"
88
88
  },
89
89
  {
90
90
  "file": "p2p-and-parties.md",
@@ -101,15 +101,20 @@
101
101
  "sha256": "df1e39628ccba04b6f7bcd0b90381dfa34a4480330a6af041d9b71bcaefddb5f",
102
102
  "updated": "2026-09-17T20:51:57Z"
103
103
  },
104
+ {
105
+ "file": "rewarded-ads.md",
106
+ "sha256": "39332bd7b08adbb33d3b6382142d214e17d8f24a7776061a97f372d658e48974",
107
+ "updated": "2026-10-07T19:17:55Z"
108
+ },
104
109
  {
105
110
  "file": "room-shapes.md",
106
- "sha256": "5aab02d65a7573ac690649b70e5db4fed4f779c3102c35e64a8ced02218410a9",
107
- "updated": "2026-09-29T23:14:43Z"
111
+ "sha256": "78fad1eee7602b10a9b39f100d3daedfed6c313a884f95382233434a3206930b",
112
+ "updated": "2026-10-07T19:27:16Z"
108
113
  },
109
114
  {
110
115
  "file": "rooms-and-matches.md",
111
- "sha256": "d8c397bc3657d7f46dba7ca0af81dcf181148df798dbaced27dfd294889a1da6",
112
- "updated": "2026-09-25T02:14:45Z"
116
+ "sha256": "0abf3717a42c94ec832dfa1310c2d93a9f58236fd75b0aacaab301976bffe286",
117
+ "updated": "2026-10-07T19:27:16Z"
113
118
  },
114
119
  {
115
120
  "file": "saving-player-progress.md",
@@ -118,8 +123,8 @@
118
123
  },
119
124
  {
120
125
  "file": "server-bundles.md",
121
- "sha256": "ffc7ec338f3436f73b16f8ba92a54de93e55bd5e9adbe562647e27b4867e4619",
122
- "updated": "2026-10-07T16:20:42Z"
126
+ "sha256": "584ace965c1be20ca90ee1cd3beff7c1473f1ca9bccef428b3efb6846b2315a7",
127
+ "updated": "2026-10-08T00:55:05Z"
123
128
  },
124
129
  {
125
130
  "file": "serverless-functions-js.md",
@@ -128,8 +133,8 @@
128
133
  },
129
134
  {
130
135
  "file": "serverless-functions.md",
131
- "sha256": "1edaa60a8aaf20499f24d661f169790cacfdb11c26529735a93e5b7b996e5432",
132
- "updated": "2026-10-03T00:26:43Z"
136
+ "sha256": "b1562b4442b69bb6895198def7b00ccd7a8c56133862a939c2cce9b6e9d30290",
137
+ "updated": "2026-10-07T19:27:16Z"
133
138
  },
134
139
  {
135
140
  "file": "trading-and-gifting.md",
@@ -143,8 +148,8 @@
143
148
  },
144
149
  {
145
150
  "file": "what-a-game-must-do.md",
146
- "sha256": "3d3ed15c93368a03b11c0e4e1924797e2638aff5be264a799a259ee065d2dbfc",
147
- "updated": "2026-10-06T23:08:59Z"
151
+ "sha256": "7dc6bdc55e3cfbc88d667bbc2ebf908e357a1b6d768d5e3f4ce486ded614c1bf",
152
+ "updated": "2026-10-07T19:15:38Z"
148
153
  }
149
154
  ]
150
155
  }
@@ -204,7 +204,7 @@ next round is one call:
204
204
 
205
205
  **A template's `auto_start_host` starts the first match, not the second.** It fires when the room
206
206
  reaches `min_players`; a reopened room already has them, and nothing fires again. A shape whose
207
- first match started itself — a hub, a worker, a matchmade lobby — still needs a client to call
207
+ first match started itself — a hub, a matchmade lobby — still needs a client to call
208
208
  `startMatch()` for the next one, and that client is whoever `gem.rooms.isHost` says. Watch
209
209
  `hostChanged`: if the host closed their tab during the match, the room has a new host, and the
210
210
  start belongs to them now.