@indiegems/gem-web-sdk 23.2.0 → 24.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.
@@ -13,13 +13,13 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "5814655859e3d6b223f94b71f1b064783b0f242d409c3ec3078089cb4b7c8ec5",
17
- "updated": "2026-11-04T17:17:14Z"
16
+ "sha256": "3f81025a763f96d0ca17f57377042a3e3890797983265cdb1721902b700aa3a6",
17
+ "updated": "2026-11-09T18:54:26Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
21
- "sha256": "79bd7829d5a6577d3619d7fd831b74065df883e11eadd1b8153754d97a88474a",
22
- "updated": "2026-11-02T18:42:37Z"
21
+ "sha256": "73e77f3bd0124f69d53b42714fb24c7d128dd1be97e203fc3a353a8eb4e3c05a",
22
+ "updated": "2026-11-11T01:56:34Z"
23
23
  },
24
24
  {
25
25
  "file": "arcade-avatars.md",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  {
30
30
  "file": "arcade-economy.md",
31
- "sha256": "108b3225a45dce776907344f8a3865b99b6e16598c6f70ddf0e8b923cf330a7e",
32
- "updated": "2026-11-04T21:50:20Z"
31
+ "sha256": "6fadbb07d8a6ab44cb231b12a6ba89ce65a46bf42841bff932d3813aa4e0931a",
32
+ "updated": "2026-11-11T01:56:34Z"
33
33
  },
34
34
  {
35
35
  "file": "async-opponents.md",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "819160e36c0258c34aa3e8b06ea0f1e26a50bcc83c5919780de2b33f4fac1cd9",
52
- "updated": "2026-11-09T16:19:51Z"
51
+ "sha256": "f58b3ff7bc37b6da56ac6cc57a72baf32d5e8be83be29be1ec2420971076cee9",
52
+ "updated": "2026-11-11T03:16:15Z"
53
53
  },
54
54
  {
55
55
  "file": "daily-challenges.md",
@@ -63,8 +63,8 @@
63
63
  },
64
64
  {
65
65
  "file": "deprecation-policy.md",
66
- "sha256": "8134d5463e2737116ed23341bf177c51cede4bd86dabec54d76a82c5bfdfade1",
67
- "updated": "2026-10-14T22:38:02Z"
66
+ "sha256": "4622fbf889603436d753013296e9aa8f9edd460ebdae91e4e8594698ea5a0500",
67
+ "updated": "2026-11-10T21:45:26Z"
68
68
  },
69
69
  {
70
70
  "file": "friends.md",
@@ -78,8 +78,8 @@
78
78
  },
79
79
  {
80
80
  "file": "getting-started.md",
81
- "sha256": "763113c6da4ae1d154ba655e44e87f32912a66ea7db40a61dca9c17f51fcc234",
82
- "updated": "2026-11-09T16:19:51Z"
81
+ "sha256": "18a2affd0d2420cd543aabeb3f5d2ef366ef654685e303ef1c9ee2e0ed24eeca",
82
+ "updated": "2026-11-11T03:16:15Z"
83
83
  },
84
84
  {
85
85
  "file": "graphics-route.md",
@@ -88,8 +88,8 @@
88
88
  },
89
89
  {
90
90
  "file": "inventory-and-catalogs.md",
91
- "sha256": "f0445a3b56f7a31590acd9ef2874a7f51cc8d89b6fbfc7de9ea5861f1ef555ca",
92
- "updated": "2026-11-08T20:08:14Z"
91
+ "sha256": "22998fb3ff26d4f9f29d2a73ce6db0ccf42ca2b635f171023a2e2214bbea4b1a",
92
+ "updated": "2026-11-11T01:56:34Z"
93
93
  },
94
94
  {
95
95
  "file": "leaderboards.md",
@@ -113,18 +113,18 @@
113
113
  },
114
114
  {
115
115
  "file": "platform-rules.md",
116
- "sha256": "ab5b971555490956ef50f6141b0f58a6a34335a7e4cc1ef0e4d339a259c9a062",
117
- "updated": "2026-10-21T19:36:16Z"
116
+ "sha256": "e9cb3cf1945002d182d917b0f3f7cfdf26f41de9ec9cd34a47c487cb4c615152",
117
+ "updated": "2026-11-11T01:56:34Z"
118
118
  },
119
119
  {
120
120
  "file": "player-sign-in.md",
121
- "sha256": "22c798ff32ee15dda88f08f6701146f83fe92568c737d9321edd3e2550cb2df9",
122
- "updated": "2026-11-09T02:37:14Z"
121
+ "sha256": "ebdaa0a35815c72148bd72f529b5ecee264c2eeb703e73fdfc277066be63fab4",
122
+ "updated": "2026-11-11T01:30:40Z"
123
123
  },
124
124
  {
125
125
  "file": "rewarded-ads.md",
126
- "sha256": "f632d54aa605b34dc071b59d49e7ecfd35ab95eb6f601885ceb0d3d6fc41a949",
127
- "updated": "2026-11-09T15:28:20Z"
126
+ "sha256": "505a6addbac54b9a154ca0b1afd727d319ba35f5f2a9b24d9835ac2a9104ded8",
127
+ "updated": "2026-11-11T01:56:34Z"
128
128
  },
129
129
  {
130
130
  "file": "room-shapes.md",
@@ -143,8 +143,8 @@
143
143
  },
144
144
  {
145
145
  "file": "server-bundles.md",
146
- "sha256": "b712e407e79482f1edc596ebcaab00852429fbaacebb05d04a2a2f9cbe1f5b9c",
147
- "updated": "2026-11-09T16:19:51Z"
146
+ "sha256": "acc4cd517d6c330640fae1affb2ba15a74c671ae72bdc11d13589ba7b66d5432",
147
+ "updated": "2026-11-11T03:16:15Z"
148
148
  },
149
149
  {
150
150
  "file": "serverless-functions-js.md",
@@ -168,8 +168,8 @@
168
168
  },
169
169
  {
170
170
  "file": "what-a-game-must-do.md",
171
- "sha256": "f70fe9fad67c6b386bddc53f1f876bb78f6a5e11c35f565bb93e925d65a99874",
172
- "updated": "2026-11-06T22:48:07Z"
171
+ "sha256": "c5400f43a1218de0d2838d4332dce4c05afe21ff04e352d2e201ef5bb4268bed",
172
+ "updated": "2026-11-09T18:39:56Z"
173
173
  }
174
174
  ]
175
175
  }
@@ -326,41 +326,50 @@ longer one, are in [`server-bundles.md`](server-bundles.md#how-long-your-server-
326
326
 
327
327
  ## 8. A player's Boost is for display, never a gate
328
328
 
329
- Decided by the platform on **2026-09-26**, and a contract rather than an implementation fact.
329
+ Decided by the platform on **2026-09-26**, restated for VIP on **2026-11-10**, and a contract
330
+ rather than an implementation fact.
330
331
 
331
- The **Boost** is something the Arcade sells for real money, in its own store. While a player has
332
- one, they earn **Gems and Coins** faster — every grant is multiplied — and their earning limits
333
- and holdings cap are raised by the same tier. It **never** touches achievement points. The platform applies all of
334
- it on its own side, when it pays: a game does nothing to make a boost work, and cannot make one
335
- work harder.
332
+ **Gate on the pass, never on the boost.** What a player paid for is unlocked by the pass, and the
333
+ multiplier it brings is only ever shown.
336
334
 
337
- A game **may show** it. `gem.player.boost` carries each boostable currency's multiplier and the
338
- whole days left, so a reward screen can say "2× Coins"; see
335
+ A player's earnings multiplier comes from **VIP**, a pass that pays +50% Coins, or from a **Boost**
336
+ bought before the Boost was retired. The platform applies it on its own side, when it pays: every
337
+ Coins grant is multiplied, and the earning limits with it. It **never** touches achievement points.
338
+ A game does nothing to make it work, and cannot make it work harder.
339
+
340
+ A game **may show** it. `gem.player.boost` carries each currency's multiplier and the whole days
341
+ left, so a reward screen can say "+50% Coins"; see
339
342
  [what a game must do](what-a-game-must-do.md#the-signed-in-players-boost-show-it-never-gate-on-it).
340
- A dedicated server can read the same effective boost for a player in its match, and the same rule
341
- holds there.
343
+ To show what a payout will be, use `gem.gameConfig.preview`, never your own arithmetic on the
344
+ multiplier. A dedicated server can read the same multiplier for a player in its match
345
+ (`gem.match.playerBoost`), and the same rule holds there.
342
346
 
343
- A game **must not gate** anything on it:
347
+ A game **must not gate** anything on the multiplier:
344
348
 
345
349
  - **No content or power behind it.** No level, item, character, mode, stat, difficulty setting or
346
- extra attempt may depend on whether the player is boosted. A boosted and an unboosted player play
347
- the same game.
348
- - **No prices that depend on it**, and no discount or surcharge keyed on boost status.
349
- - **No matchmaking on it.** Never exclude, separate or wait on a player because of their boost.
350
- - **No nagging.** No "you're not boosted" loops, interstitials or countdown pressure. The Arcade owns
351
- the Boost store; a game does not sell or advertise boosts.
352
-
353
- Three reasons, any one of which would be enough. It is **advisory**: the browser value is pushed on
354
- connect and on change, so it can lag a boost that just started or ended, and the platform's own
355
- figure is what pays. It is **forgeable** inside your page, so a gate on it is a gate anyone can
356
- open. And gating on it turns a faster faucet into pay-to-win, which is the line the Arcade's economy
357
- is drawn on.
350
+ extra attempt may depend on whether, or by how much, the player earns faster. A player at 1x and a
351
+ player at 1.5x play the same game.
352
+ - **No prices that depend on it**, and no discount or surcharge keyed on it.
353
+ - **No matchmaking on it.** Never exclude, separate or wait on a player because of it.
354
+ - **No nagging.** No "you're not boosted" loops, interstitials or countdown pressure.
355
+
356
+ **What VIP unlocks goes behind the pass.** A VIP room, a VIP roll or a VIP track is a
357
+ `requires: [{ kind: 'holds_pass', pass: 'vip' }]` on the item, recipe, bonus or track, which the
358
+ platform checks when it runs; or a decision your function or server makes from its pass read
359
+ (`gem.me.passes` in a function, `gem.match.playerPasses` on a server). The browser's
360
+ `gem.me.passes` draws the door; it never opens it. Offer VIP only before a reward, by the rules in
361
+ [inventory-and-catalogs.md](inventory-and-catalogs.md#offering-vip-before-a-reward-the-upsell).
362
+
363
+ Three reasons not to gate on the multiplier, any one of which would be enough. It is **advisory**:
364
+ the browser value is pushed on connect and on change, so it can lag, and the platform's own figure
365
+ is what pays. It is **forgeable** inside your page, so a gate on it is a gate anyone can open. And it
366
+ **is not the purchase**: a player can hold an old Boost without VIP, so a gate on the multiplier
367
+ opens for the wrong player and stays shut for none.
358
368
 
359
369
  It carries **a day count, never an instant — and capped at 7**, so `7` means "7 or more" — and
360
- nothing about the tier, the price or whether the player paid at all: free and paid boosts look
361
- identical to a game. A second-precision expiry would be a near-unique value per player that two
362
- games could join on, and an exact day count would jump by a purchase's term the moment the player
363
- paid. Show `7` as "7+ days", never as an end date.
370
+ nothing about the tier, the price or whether the player paid at all. A second-precision expiry
371
+ would be a near-unique value per player that two games could join on, and an exact day count would
372
+ jump by a purchase's term the moment the player paid. Show `7` as "7+ days", never as an end date.
364
373
 
365
374
  ## Where the rest of it is
366
375
 
@@ -17,8 +17,65 @@ signInButton.onclick = async () => {
17
17
  };
18
18
  ```
19
19
 
20
- That is the whole surface. Your game never draws a sign-in form and never handles a password; it
21
- could not be trusted with either, and it does not need to be.
20
+ That is almost the whole surface. Your game never draws a sign-in form and never handles a
21
+ password; it could not be trusted with either, and it does not need to be.
22
+
23
+ ## Before you draw the button
24
+
25
+ Not every player can be shown a sign-in. Before you draw "Sign in to claim", read
26
+ `gem.account.signInAvailable`, and listen for it to change:
27
+
28
+ ```js
29
+ const drawUpsell = (available) => {
30
+ signInButton.hidden = !available;
31
+ };
32
+ drawUpsell(gem.account.signInAvailable);
33
+ gem.account.onSignInAvailableChange(drawUpsell);
34
+ ```
35
+
36
+ - **`true`**: pressing the button will normally put the Arcade's sign-in up for this player. It can
37
+ still answer `declined` at a busy moment, such as while the Arcade has a sheet or a menu open.
38
+ - **`false`**: pressing it would show nothing. Don't draw the offer. Either the player is already
39
+ signed in (an ask answers `signed_in` at once), or nothing can be shown here (it answers
40
+ `unavailable`). It is also `false` until the player is known, and again once the game is
41
+ disconnected.
42
+
43
+ **In current Gem Arcade app builds (v3.33.174 and later), every player is already signed in.** The
44
+ app signs each player in to a full account: a device account, Play Games or Game Center, or another
45
+ sign-in. So `signInAvailable` is `false` there, and an ask answers `signed_in` at once, with nothing
46
+ shown. Don't draw a sign-in offer in the app. Older app builds run an older Arcade, where the next
47
+ paragraph applies: `signInAvailable` reads `true` there.
48
+
49
+ On an Arcade older than this signal, `signInAvailable` reads `true` for every player, a full
50
+ account included. That is how a sign-in ask behaved before the signal existed. So a game that reads
51
+ it keeps its button there, and a player who is already signed in and presses it gets `signed_in`
52
+ at once, with nothing shown. Keep handling every answer quietly (section 3).
53
+
54
+ `onSignInAvailableChange` runs only when the answer changes: when the player arrives, when a guest
55
+ signs up in place, and when the game is disconnected. It returns a function that stops it.
56
+
57
+ ### Gating a sign-up gift
58
+
59
+ If your game gives a gift for signing up, offer it only while **`gem.player.isGuest`** is `true`:
60
+
61
+ ```js
62
+ const gem = createGem({
63
+ // Runs for each pushed player, again when a guest signs up in place.
64
+ onPlayer: (player) => (signUpGift.hidden = !player.isGuest),
65
+ });
66
+ ```
67
+
68
+ `isGuest` is `true` only when the Arcade says the player is a guest it can sign up here. It is
69
+ `false` for a full account, where the Arcade offers no sign-up, and on an older Arcade that does not
70
+ say, so a gift gated on it never reaches a player who already has an account. On such an older
71
+ Arcade no gift shows at all: the safe way to be wrong.
72
+
73
+ Use it rather than `signInAvailable`, which reads `true` for every player on an older Arcade (it
74
+ keeps a sign-in button working there), and rather than `gem.store.offers()` answering
75
+ `sign_in_required`, which no longer means a guest: a current Arcade answers a guest's `offers()`
76
+ with `ok`, because a guest can buy, and signs them up first. Pressing the ask answers `signed_in`
77
+ for a player who was signed in all along too, so that answer is no proof of a sign-up either; what
78
+ the gift pays is granted by your function or server, which reads the account itself.
22
79
 
23
80
  ## 1. Save before you ask
24
81
 
@@ -56,7 +113,7 @@ and nothing has changed; hide the upsell.
56
113
  |---|---|---|
57
114
  | `signed_in` | the player is signed in | re-read, then hide the upsell |
58
115
  | `declined` | the player closed the prompt, or the Arcade chose not to show one | return to the game silently |
59
- | `unavailable` | the Arcade does not offer this here, or never answered | return to the game silently |
116
+ | `unavailable` | the Arcade does not offer this here, or never answered | return to the game silently; `signInAvailable` was `false` for most of these |
60
117
 
61
118
  `requestSignIn` always resolves when it resolves at all. There is no error branch and no
62
119
  `try`/`catch` to write.
@@ -213,7 +213,7 @@ the player then did, so **branch on your inventory, never on the outcome**, as f
213
213
  The upload refuses it, and `ad: false` with no `platform_prices` too, since then nothing unlocks
214
214
  the placement.
215
215
  - **Each currency's spend scope applies,** as it does to an item's price
216
- ([`inventory-and-catalogs.md`](inventory-and-catalogs.md#what-coins-and-gems-may-buy-the-spend-scope)):
216
+ ([`inventory-and-catalogs.md`](inventory-and-catalogs.md#what-coins-may-buy-the-spend-scope)):
217
217
  where Coins may price cosmetics only, a Coin price on a placement that grants anything else is
218
218
  refused. Gems may price any placement. `gemctl platform config` lists which currencies your
219
219
  game may price in, and each one's spend scope: author against it, never against a list you
@@ -226,6 +226,37 @@ for a placement yet.** Until then your prices are not offered, the placement unl
226
226
  alone, `ad: false` included, and `rewardedStatus` carries no `options` (§5). Nothing in your game
227
227
  needs to change when it goes live: draw the button from `rewardedStatus` and it follows.
228
228
 
229
+ ### A placement or the store?
230
+
231
+ Now that a placement can take Coins and Gems, pick by what the player is buying:
232
+
233
+ - **A single-reward button** (a hint, an extra life, a skip) is a **rewarded placement**, with
234
+ `platform_prices` for Coins and Gems. Whether it offers the ad, Coins, Gems or any mix is then a
235
+ config switch (`ad` and `platform_prices`), not code: the button and its `requestAd` stay the
236
+ same, and the Arcade shows the player the choices.
237
+ - **A real shop** (several items, quantities, locked items) is **store items and recipes** with
238
+ `platform_prices`
239
+ ([`inventory-and-catalogs.md`](inventory-and-catalogs.md#16-selling-for-a-platform-currency)).
240
+ A placement grants one fixed reward per press: it has no list of items, no quantity, and no
241
+ prerequisite to lock anything behind.
242
+
243
+ A hint button the player can unlock with an ad or 2 Gems:
244
+
245
+ ```yaml
246
+ rewarded_placements:
247
+ - placement: hint # exactly the string your game passes
248
+ grants:
249
+ - { type: item_grant, item_definition_slug: hint_token, quantity: 1 }
250
+ max_per_day: 5 # limits the ad only
251
+ platform_prices:
252
+ - { currency: GEMS, price: 2 }
253
+ ```
254
+
255
+ Add `{ currency: ARCADE_TICKETS, price: 20 }` beside it for Coins where Coins' spend scope lets
256
+ them price a hint (above), and `ad: false` to drop the ad. Do not also sell `hint_token` in your
257
+ shop: the reward then counts as sold for a platform currency (§1), the upload warns, and outside
258
+ test ads its ad is only served where the platform verifies completion with the ad provider.
259
+
229
260
  ## 3. Ask from the press, then re-read
230
261
 
231
262
  ```js
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.2.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v23.2.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v24.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v24.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -829,11 +829,13 @@ The short version: the browser half is what a player's **session** can do, and t
829
829
  what a **match** can do. `gem.rooms.join()` has no meaning to a process that exists because a match
830
830
  was already allocated.
831
831
 
832
- **`gem.me.passes` is browser and function only**, and that is a limit rather than a principle. The
833
- platform has a match read of which passes a player in your match holds, but your bundle cannot call
834
- it: it requires the allocation nonce, which the runtime never hands to a bundle, as the boost read
835
- does, and it answers `403` to every caller until game servers are granted `passes.read`.
836
- [arcade-and-store.md](arcade-and-store.md#on-a-dedicated-server) has what to do instead.
832
+ **`gem.me.passes` is browser and function only, and `gem.match.playerPasses(playerId)` is the
833
+ server's.** Both routes behind a player's passes and Boost in your match require the allocation
834
+ nonce, which never enters your bundle, so the platform half of the runtime makes the read for you,
835
+ as it does `gem.match.end()`: `gem.match.playerPasses(playerId)` for what to gate on, and
836
+ `gem.match.playerBoost(playerId)` for display only. Each throws on a runtime image that predates it
837
+ and on a listen host. [arcade-and-store.md](arcade-and-store.md#on-a-dedicated-server) has the
838
+ details.
837
839
 
838
840
  **Rooms are the case where those two answers differ**, so they are worth stating separately rather
839
841
  than filed under "browser-only". `gem.rooms` — join, leave, ready up, quick-join — is a player
@@ -2404,10 +2404,13 @@ Not on the list above either. When a player reaches for something only an accoun
2404
2404
  `gem.account.requestSignIn` asks the Arcade to sign them in, in its own chrome. Your game draws no
2405
2405
  form and passes nothing.
2406
2406
 
2407
- Two things before you reach for it: **save first**, because the Arcade may sign the player in by
2408
- taking the whole page away, and your game then starts again rather than hearing an answer; and on
2409
- `signed_in`, **re-read what you loaded**, because the player underneath your game may not be the
2410
- one who was there a moment ago. [`player-sign-in.md`](player-sign-in.md) is the page.
2407
+ Draw the offer only while `gem.account.signInAvailable` is `true`: it is `false` for a player
2408
+ already signed in (every player in a current Gem Arcade app build, v3.33.174 and later, is), and
2409
+ wherever nothing can be shown. Two
2410
+ things before you reach for it: **save first**, because the Arcade may sign the player in by taking
2411
+ the whole page away, and your game then starts again rather than hearing an answer; and on `signed_in`, **re-read what you loaded**, because the player
2412
+ underneath your game may not be the one who was there a moment ago.
2413
+ [`player-sign-in.md`](player-sign-in.md) is the page.
2411
2414
 
2412
2415
  ## A Share button, if your game has something worth sending a friend
2413
2416
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "23.2.0",
3
+ "version": "24.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -840,6 +840,47 @@
840
840
  ]
841
841
  }
842
842
  }
843
+ },
844
+ "signInAvailable": {
845
+ "kind": "boolean",
846
+ "optional": true,
847
+ "$comment": [
848
+ "Whether a `gem.requestSignIn` from a player's press would put the",
849
+ "Arcade's sign-in prompt up for THIS player HERE.",
850
+ "The SDK reads it as `gem.account.signInAvailable`, so a game hides a",
851
+ "\"Sign in to claim\" offer that could show nothing.",
852
+ "",
853
+ "`true`: a browser guest on a surface that offers sign-up. An ask can",
854
+ "still answer `declined` at a busy moment (a sheet, a menu, a parked",
855
+ "game, a second ask); those holds are momentary and not part of this",
856
+ "field. `false`: an ask shows nothing — it answers `signed_in` at once",
857
+ "(a full account, which every player in the Gem Arcade app is) or",
858
+ "`unavailable` (a garden that offers no sign-up, a surface with no",
859
+ "sign-in). A parent",
860
+ "that knows sends it on EVERY `gem.player`, `false` included, and",
861
+ "re-sends the message when it changes (a guest who signs up in place).",
862
+ "",
863
+ "ABSENT means an older parent, and a frame reads it as `true`, which is",
864
+ "what `gem.requestSignIn` did there before this field: a game that adopts",
865
+ "it keeps its button on an old host. So a parent must never omit it to",
866
+ "mean `false`.",
867
+ "",
868
+ "A BOOLEAN, NEVER AN ENUM. A new enum value is breaking here: an N-1",
869
+ "frame's validator would refuse the whole message and cost the game its",
870
+ "player's name.",
871
+ "",
872
+ "Platform-assigned, so it sits outside `untrusted`. Ungated like the rest",
873
+ "of `gem.player`; it means something only beside the `signIn`",
874
+ "capability, without which the SDK reads `false` whatever it says.",
875
+ "",
876
+ "DISCLOSURE. `true` tells a game \"this is a browser guest\": the fact",
877
+ "`gem.requestSignIn` already discloses, since a",
878
+ "full account answers `signed_in` at once and a guest's answer follows a",
879
+ "person. `false` reveals less: it does not tell a signed-in player from",
880
+ "a surface with no sign-in. And `gem.player` is withheld until the launch is",
881
+ "confirmed, so an unconfirmed launch learns nothing. It carries no",
882
+ "sanction, session or age band."
883
+ ]
843
884
  }
844
885
  },
845
886
  "rateLimit": { "perMinute": 12 }
@@ -6002,7 +6043,7 @@
6002
6043
  "transport": "port",
6003
6044
  "$comment": [
6004
6045
  "Ask what the Arcade Store sells to this player, here, for real money:",
6005
- "Gem packs, Boosts, passes and bundles. Gated by the `store` capability.",
6046
+ "Coin packs, Boosts, passes and bundles. Gated by the `store` capability.",
6006
6047
  "Answered by `gem.storeOffersResult`.",
6007
6048
  "",
6008
6049
  "WHY THE PARENT ANSWERS IT. The price a player pays depends on the",
@@ -6016,9 +6057,13 @@
6016
6057
  "",
6017
6058
  "A READ, NOT AN ACT: no activation, no mutex. Per-session state comes",
6018
6059
  "back as a reason: `store_off` where the store is withheld (an age or",
6019
- "region policy, a TV, an in-app browser that takes no payment) and",
6020
- "`sign_in_required` for a guest, who may then be offered",
6021
- "gem.requestSignIn."
6060
+ "region policy, a TV, an in-app browser that takes no payment).",
6061
+ "",
6062
+ "A GUEST IS ANSWERED `ok`, with what the store would sell them:",
6063
+ "buying takes a guest through sign-up first, in the parent's own",
6064
+ "chrome, and back to the offer. An",
6065
+ "older parent answered a guest `sign_in_required`, and a frame must",
6066
+ "still read that as nothing for sale."
6022
6067
  ],
6023
6068
  "fields": {
6024
6069
  "requestId": {
@@ -6049,6 +6094,23 @@
6049
6094
  "instead, and a purchase of that offer is answered `declined` /",
6050
6095
  "`cancelled`.",
6051
6096
  "",
6097
+ "WHAT A PASS SAYS, AND WHAT IT MUST NOT. A pass offer names its",
6098
+ "offering slug in `pass` and the earning",
6099
+ "multipliers it carries in `earnMultipliers`, so a game can find VIP by",
6100
+ "slug and preview what it would add without naming a SKU or a number.",
6101
+ "A parent lists a pass only where it would open that pass's sheet: an",
6102
+ "offering with `visible_to_games`, to a player who has not declined",
6103
+ "its sheet twice today. For a KNOWN MINOR it leaves the pass out",
6104
+ "exactly as if it were not on sale - the same `ok`, the same other",
6105
+ "offers, no reason - so a game can never tell a minor from a pass that",
6106
+ "is not sold here. A holder still sees it: the sheet adds time.",
6107
+ "",
6108
+ "COIN PACKS ARE `gem_pack`. The kind enum is closed (a fifth value",
6109
+ "would be refused whole by an N-1 frame), so a pack of any platform",
6110
+ "currency crosses as `gem_pack` with `grant` naming the currency, and",
6111
+ "the SDK reports a pack whose `grant.currency` is not GEMS as",
6112
+ "`coin_pack`.",
6113
+ "",
6052
6114
  "THE NAMES ARE THE PLATFORM'S, not a game developer's: the Arcade",
6053
6115
  "Store's own catalog copy, which is why `name` and `description` sit",
6054
6116
  "outside an `untrusted` object.",
@@ -6090,7 +6152,7 @@
6090
6152
  "kind": {
6091
6153
  "kind": "enum",
6092
6154
  "values": ["gem_pack", "boost", "pass", "bundle"],
6093
- "$comment": "What sort of thing it is. A new sort of SKU is left out of this list by the parent, never sent as a fifth value: an N-1 frame would refuse the whole message."
6155
+ "$comment": "What sort of thing it is. A new sort of SKU is left out of this list by the parent, never sent as a fifth value: an N-1 frame would refuse the whole message. `gem_pack` is a pack of ANY platform currency, Coins included: `grant` says which."
6094
6156
  },
6095
6157
  "name": {
6096
6158
  "kind": "string",
@@ -6117,12 +6179,12 @@
6117
6179
  "grant": {
6118
6180
  "kind": "object",
6119
6181
  "optional": true,
6120
- "$comment": "For a Gem pack: the currency and amount it grants. Absent for every other kind.",
6182
+ "$comment": "For a currency pack (`gem_pack`): the currency and amount it grants. Absent for every other kind.",
6121
6183
  "fields": {
6122
6184
  "currency": {
6123
6185
  "kind": "string",
6124
6186
  "maxLength": 32,
6125
- "$comment": "The platform currency's exact slug, e.g. GEMS."
6187
+ "$comment": "The platform currency's exact slug, e.g. ARCADE_TICKETS for Coins."
6126
6188
  },
6127
6189
  "amount": {
6128
6190
  "kind": "int",
@@ -6131,6 +6193,34 @@
6131
6193
  "$comment": "How much of it."
6132
6194
  }
6133
6195
  }
6196
+ },
6197
+ "pass": {
6198
+ "kind": "string",
6199
+ "maxLength": 64,
6200
+ "optional": true,
6201
+ "$comment": "For a pass, or a bundle carrying one: the pass's offering slug, e.g. `vip` - the slug `gem.me.passes` and a `holds_pass` requirement name. Absent for everything else, and from an older parent. Additive: no `v` bump."
6202
+ },
6203
+ "earnMultipliers": {
6204
+ "kind": "array",
6205
+ "maxItems": 8,
6206
+ "optional": true,
6207
+ "$comment": "For an offer that makes the player earn a platform currency faster while held - a pass with the `earn_boost` feature, or a Boost: one entry per currency, as `gem.player`'s `boost.multipliers` carry them. Absent or empty when it boosts nothing. The platform's multiplier, never a game's: the preview reads it to say what holding the pass would add.",
6208
+ "items": {
6209
+ "kind": "object",
6210
+ "fields": {
6211
+ "currencySlug": {
6212
+ "kind": "string",
6213
+ "maxLength": 32,
6214
+ "$comment": "The currency's slug, e.g. ARCADE_TICKETS."
6215
+ },
6216
+ "multiplierBp": {
6217
+ "kind": "int",
6218
+ "min": 10000,
6219
+ "max": 1000000,
6220
+ "$comment": "Basis points: 10000 is 1x, 15000 is 1.5x. gem-api clamps what it applies to 10x."
6221
+ }
6222
+ }
6223
+ }
6134
6224
  }
6135
6225
  }
6136
6226
  }