@genex-ai/cli-demo 1.36.5 → 1.36.6-dev.862

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.36.5",
3
+ "version": "1.36.6-dev.862",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  // - "unlocked" → a bottom-center pill, "Click to aim — Esc pauses". Also shown on
13
13
  // the "needs-gesture" re-emit (a re-lock that needs a fresh click).
14
14
  // - "unavailable" → the same pill, "Drag to look" (lock was refused, e.g. a
15
- // third-party embed without allow="pointer-lock" — drag still works).
15
+ // sandboxed embed without the allow-pointer-lock sandbox token —
16
+ // drag still works).
16
17
  // - "off" / "paused" → nothing (aim disabled, or a menu owns the cursor).
17
18
  //
18
19
  // Games that want a custom HUD can skip this and read `onAimChange` directly.
@@ -116,7 +116,7 @@ export type FollowCameraAimState =
116
116
  | "unlocked" // aim available, waiting for a click
117
117
  | "locked" // pointer locked, mouse drives the view
118
118
  | "paused" // suspended by the game (menu open / driving) — clicks don't re-lock
119
- | "unavailable"; // lock permanently rejected (e.g. an iframe without allow="pointer-lock") → drag-orbit fallback
119
+ | "unavailable"; // lock permanently rejected (e.g. a sandboxed embed without the allow-pointer-lock sandbox token) → drag-orbit fallback
120
120
 
121
121
  /**
122
122
  * Payload for {@link FollowCameraOptions.onAimChange}.
@@ -843,8 +843,9 @@ export class FollowCamera {
843
843
  * - No usable user gesture (a resume outside a click, or Chrome's post-Esc
844
844
  * cooldown): skip the doomed request, keep the "click to aim" cue up (a
845
845
  * same-state "needs-gesture" re-emit), and arm a keydown retry.
846
- * - SecurityError (permission denied — e.g. an iframe without
847
- * allow="pointer-lock"): permanent, fall back to drag-orbit ("unavailable").
846
+ * - SecurityError (permission denied — e.g. a sandboxed embed without the
847
+ * allow-pointer-lock sandbox token): permanent, fall back to drag-orbit
848
+ * ("unavailable").
848
849
  * - NotSupportedError from `unadjustedMovement`: retry the plain request (still
849
850
  * inside the transient-activation window, so no new gesture is needed).
850
851
  * - Any other rejection: transient — treat like the missing-gesture case.
@@ -78,10 +78,15 @@ const screen = new THREE.Mesh(
78
78
  );
79
79
  scene.add(screen);
80
80
 
81
- // start playback on a user gesture (never swallow the rejection):
82
- window.addEventListener("keydown", () => {
81
+ // start playback on the first input (never swallow the rejection). Both
82
+ // events: a click or tap is the first input a game usually gets, not a key.
83
+ const start = (): void => {
84
+ window.removeEventListener("pointerdown", start);
85
+ window.removeEventListener("keydown", start);
83
86
  video.play().catch((e) => console.warn("video play", e));
84
- }, { once: true });
87
+ };
88
+ window.addEventListener("pointerdown", start);
89
+ window.addEventListener("keydown", start);
85
90
  ```
86
91
 
87
92
  One `VideoTexture` can feed **many** surfaces — a bank of monitors, N sprays. Cost is
@@ -157,7 +157,7 @@ vendored code from memory of another engine.
157
157
  | sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
158
158
  | requested UI/HUD/menu/interface work, a visible UI problem, or an interface you decided this game wants built with generated art | `$genex-threejs-game-ui` |
159
159
  | selling anything for platform coin: a shop, an item catalog, boosts, cosmetics, "make it earn"; also any request for a loot box, gacha, wager, casino mechanic or donation prompt, which that skill refuses and replaces | `$genex-monetization` |
160
- | the game calls a language model AT RUNTIME on the player's money: NPCs that answer in their own words, dialogue or quests written per save, a judge reading what the player typed — one-time calls, or a standing budget the player approves once. Check the lane with `npx genex llm models` before designing it in | `$genex-llm-in-games` |
160
+ | the game calls a language model AT RUNTIME on the player's credits, billed as used under a per-call ceiling the game benchmarks with `npx genex llm bench`: NPCs that answer in their own words, dialogue or quests written per save, a judge reading what the player typed — one-time calls, or a standing budget the player approves once. Check the lane with `npx genex llm models` before designing it in | `$genex-llm-in-games` |
161
161
  | cinematic menu/title/pause/victory/defeat/lobby/credits video treatment | `$genex-ai-menu` |
162
162
  | drawn HUD chrome the game's style wants—one element or a matched set of frames, masks, and icons | `$genex-ai-hud` |
163
163
  | the game works but feels flat, floaty, or unresponsive: input response, camera, impacts, cooldowns, difficulty, fail/retry | `$genex-threejs-game-feel` |
@@ -165,6 +165,7 @@ vendored code from memory of another engine.
165
165
  | players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
166
166
  | players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
167
167
  | player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
168
+ | anything the game asks the BROWSER or the DEVICE for: share, an invite link, copy to clipboard, open a link, save or download a file, fullscreen, tilt or gyro, vibration, microphone / camera / location, keep the screen on, its own `?params` or `#hash`, a browser dialog (`alert` / `confirm` / `prompt`). What works differs between the game's Genex page, the Genex app and the game's own link, usually with no error — find the row before the first call | `$genex-threejs-embed-auth` (`references/shells.md`) |
168
169
  | the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
169
170
 
170
171
  Request-shape rules, working modes and the protected-lane handoffs stay in the
@@ -161,7 +161,7 @@ downloads the game too, binary assets and all:
161
161
 
162
162
  ```bash
163
163
  mkdir my-game && cd my-game
164
- npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
164
+ npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
165
165
  npm install
166
166
  ```
167
167
 
@@ -225,7 +225,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
225
225
  and your own files are never touched:
226
226
 
227
227
  ```bash
228
- npx @genex-ai/cli-demo@latest init
228
+ npx @genex-ai/cli-demo@dev init
229
229
  ```
230
230
 
231
231
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: genex-llm-in-games
3
- description: Call a language model from inside a running game — an NPC that answers in its own words, a quest written for this save, a judge that reads what the player typed. The PLAYER pays and approves, on a Genex surface the game cannot forge. Covers the two modes (a popup per call, or one standing budget then many silent calls), benchmarking the price before declaring it, the receiver pattern, and honest handling of every refusal.
3
+ description: Call a language model from inside a running game — an NPC that answers in its own words, a quest written for this save, a judge that reads what the player typed. The PLAYER pays and approves, on a Genex surface the game cannot forge. Covers the two modes (a popup per call, or one standing budget then many silent calls), benchmarking the per-call ceiling before declaring it, the receiver pattern, and honest handling of every refusal.
4
4
  ---
5
5
 
6
6
  # Genex LLM in Games
@@ -8,10 +8,11 @@ description: Call a language model from inside a running game — an NPC that an
8
8
  A Genex game can call a language model **while the player is playing** and get
9
9
  back text or JSON. Nothing else: no images, no code execution, no tools.
10
10
 
11
- **The player pays, and the player approves.** Funding is coin from their Genex
12
- wallet or their own Claude / ChatGPT plan, chosen on a Genex-drawn surface your
13
- game cannot render, skin or bypass. The game holds no provider key, sees no
14
- credential, and never talks to a model vendor.
11
+ **The player pays, and the player approves.** Funding is their Genex credits —
12
+ billed as used, never past the per-call ceiling your game declares — or their
13
+ own Claude / ChatGPT plan, chosen on a Genex-drawn surface your game cannot
14
+ render, skin or bypass. The game holds no provider key, sees no credential, and
15
+ never talks to a model vendor.
15
16
 
16
17
  Two modes. Picking the wrong one is the most expensive mistake on this lane:
17
18
 
@@ -25,6 +26,23 @@ Two modes. Picking the wrong one is the most expensive mistake on this lane:
25
26
  A popup per NPC turn is not a feature, it is an interruption. More than a call
26
27
  or two per session means a grant.
27
28
 
29
+ **Deciding, not writing? Use the judge.** When the game needs to KNOW something
30
+ — is this NPC lying, which of five moods is the player in, how threatening is
31
+ the scene on a scale — and not to produce words, ask the grant's classifier with
32
+ `judge()` instead of asking `generate()` for a label. It answers typed questions
33
+ (`choice`, `score`, `noul`) with calibrated probabilities in about a third of a
34
+ second, writes no prose and cannot drift out of your options. It is grant-only
35
+ and billed as used: each call is a small fraction of a credit and accrues on the
36
+ budget, so a check every turn is affordable where a generate every turn is not.
37
+ `generate()` for words, `judge()` for decisions; a feature often wants both —
38
+ the judge picks the branch, generate writes the line.
39
+
40
+ **Words the player watches appear? Stream them.** Dialogue, narration, an NPC's
41
+ reply in a speech bubble: under a grant, `generateStream()` is the same call as
42
+ `generate({ grantId })`, but the text arrives while the model writes it instead
43
+ of all at once after it. Keep `generate()` for answers the game only uses whole
44
+ (a quest object, a list of decisions) — nobody watches those being written.
45
+
28
46
  ## Step 0 — is the lane live on this stand?
29
47
 
30
48
  ```bash
@@ -61,15 +79,33 @@ identity must be resolved first — `$genex-threejs-embed-auth`.
61
79
  rest only if the player asks. Each row carries `label`, `vendor`,
62
80
  `contextLength`, `personalPlan` and `structuredOutputs`; render `label`
63
81
  verbatim, so the name on screen is the model that gets billed.
64
- - `generate({ modelId, prompt, outputFormat, schema?, estimateCoins,
82
+ - `generate({ modelId, prompt, outputFormat, schema?, maxCredits,
65
83
  allowExternal?, idempotencyKey?, grantId?, timeoutMs? })` →
66
84
  `{ status, generationId, output, source, error }` plus billing fields
67
- (`billingStatus`, `reservedCoins`, `chargedCoins`, their display-USD twins).
68
- - `requestSpendGrant({ models, perCallMaxCoins, perCallEstimateCoins,
69
- disclosure: { periodLabel, estimatedCallsPerPeriod, estimatedCoinsPerPeriod },
70
- maxConcurrent?, maxCallsPerMinute?, allowExternal?, idempotencyKey? })` →
85
+ (`billingStatus`, `maxCredits`, `reservedCredits`, `chargedCredits`, their
86
+ display-USD twins). `maxCredits` is the call's CEILING, never a price.
87
+ - `requestSpendGrant({ models, perCallMaxCredits, perCallEstimateCredits?,
88
+ disclosure: { periodLabel, estimatedCallsPerPeriod, estimatedCreditsPerPeriod },
89
+ maxConcurrent?, maxCallsPerMinute?, allowExternal?, judge?: { enabled,
90
+ estimatedCallsPerPeriod }, idempotencyKey? })` →
71
91
  `{ status, grantId, … the limits the player approved }`; status is `active` |
72
- `canceled` | `expired` | `failed` | `pending`.
92
+ `canceled` | `expired` | `failed` | `pending`. `perCallEstimateCredits` is the
93
+ honest per-call figure the disclosure is built on; left out, it is the
94
+ ceiling.
95
+ - `judge({ grantId, state, questions, idempotencyKey?, timeoutMs? })` →
96
+ `{ status, generationId, answers, error, billing }` — the grant's classifier,
97
+ answered in the same response. `questions` is 1–64 named `choice`
98
+ (`{ instructions, criteria: { option: description } }`), `score`
99
+ (`{ instructions, criteria: [levels, lowest first] }`) or `noul`
100
+ (`{ instructions }`); each answer has its question's type, and `noul` is a
101
+ probability — pick your own threshold. Needs a budget requested with `judge`.
102
+ - `generateStream({ …the generate() options, grantId, onDelta?(text, soFar),
103
+ onPartialField?(name, partialText), signal? })` → the same result as
104
+ `generate()`. Grant-only. `onDelta` hands over each piece and all the text so
105
+ far; for `outputFormat: 'json'`, `onPartialField` reports the schema's FIRST
106
+ top-level string property as far as it is written — put the spoken line first
107
+ in the schema. `signal` stops the reading, never the call (it still finishes
108
+ and is charged).
73
109
  - `getSpendGrant(grantId)` — live state and counters; the ONE source for an
74
110
  in-game budget readout.
75
111
  - `stopSpendGrant(grantId)` — the game's own stop door. Prospective: no further
@@ -77,13 +113,22 @@ identity must be resolved first — `$genex-threejs-embed-auth`.
77
113
  - `waitForGeneration(id)` / `getGeneration(id)` — re-attach to a call already
78
114
  started, including after a reload.
79
115
  - `generationErrorMessage(code)` — one player-facing sentence for an error code.
116
+ - `requestWorkflow(…)` — a registered workflow's own door, unchanged: keep
117
+ sending what that workflow's offerings ask for.
118
+
119
+ The credit names arrived in `@genex-ai/embed-sdk` 0.27.0. On `generate()`,
120
+ `generateStream()` and `requestSpendGrant()` the coin spellings —
121
+ `estimateCoins`, `perCallMaxCoins`, `perCallEstimateCoins` and
122
+ `disclosure.estimatedCoinsPerPeriod` — are deprecated: still accepted, and read
123
+ as the same numbers in credits. Write the credit names in anything new, and
124
+ rename them when you touch old code.
80
125
 
81
126
  ```ts
82
127
  askButton.addEventListener('click', async () => { // a real click
83
128
  const res = await generate({ // FIRST statement, no await before it
84
129
  modelId, outputFormat: 'json', schema: ANSWER_SCHEMA,
85
130
  prompt: askPrompt(npc, playerLine),
86
- estimateCoins: NPC_CALL_PRICE, // benchmarked — see below
131
+ maxCredits: NPC_CALL_MAX, // benchmarked — see below
87
132
  idempotencyKey: `npc:${npc.id}:${turnId}`,
88
133
  });
89
134
  applyGeneration(res); // the one writer — see below
@@ -95,40 +140,63 @@ handler, before any `await`.** The approval popup is reserved synchronously off
95
140
  the gesture; an `await` in front of it loses the gesture and nothing opens.
96
141
  `generate({ grantId })` needs no gesture at all — that is what a grant buys.
97
142
 
98
- ## `estimateCoins` is a price, not an estimate
99
-
100
- You declare it; the platform charges it. Declare 5 and 5 is charged — on a
101
- success, a failure, a cancel, and when the model stops at its budget. Only an
102
- attempt with no model work at all costs nothing. A number picked by feel is
103
- money taken from your players for nothing, or a call that cannot fund itself.
143
+ **Two shells have no approval surface, and say so before anything is asked or
144
+ charged.** Inside the Genex app every call on this lane resolves
145
+ `{ status: 'failed', error: 'native_unsupported' }`; on a dev server in local
146
+ test mode it is `local_test_unsupported`. Neither is a fault in the game. When
147
+ `getShell()` from `@genex-ai/embed-sdk` is `"app"`, leave the feature's entry
148
+ point out and play the authored fallback; never show it as an error. Which
149
+ shell draws what is in `$genex-threejs-embed-auth` (`references/shells.md`).
150
+
151
+ ## `maxCredits` is a ceiling, not a price
152
+
153
+ The player is billed **as used**: the call's real provider cost plus the
154
+ platform fee, in credits. A one-time call is captured rounded UP to a whole
155
+ credit when it settles; a call under a budget adds its exact fraction to the
156
+ budget, and the player's credits move one whole credit at a time. A failed or
157
+ canceled attempt is billed the work the model did; one with no model work at
158
+ all costs nothing.
159
+
160
+ So the game declares no price. It declares a per-call **ceiling** — `maxCredits`
161
+ on `generate()`, `perCallMaxCredits` on a budget — and no call is ever billed
162
+ past it. The ceiling has a second job: **it also decides how long the answer
163
+ may be**, because each call's room to answer is funded from it. A number picked
164
+ by feel either cuts answers off or asks the player to approve far more than a
165
+ call ever costs.
104
166
 
105
167
  **Benchmark, then declare:**
106
168
 
107
169
  ```bash
108
170
  npx genex llm bench "<the real prompt, with a real example filled in>" \
109
- --schema ./answer.schema.json --samples 3 --max-coins <n> --user-approved
171
+ --schema ./answer.schema.json --samples 3 --max-credits <n> --user-approved
110
172
  ```
111
173
 
112
- It runs on **your own coins**, on the development lane, and prints what each
113
- sample actually charged plus the recommendation to declare: p95 of the charged
114
- coins with the server's own recommended headroom already applied. Declare that
115
- printed number. Never guess it, never work it out from a vendor's price list,
116
- never add a margin of your own. For a standing budget the run prints a second
117
- line, `Grant perCallMaxCoins`, and that one is `perCallMaxCoins` — declare it
118
- verbatim as well rather than deriving a ceiling from `max`, which lands under
119
- the price and makes `requestSpendGrant()` refuse before it reaches the network. Full procedure — reading p50/p95, turning the
120
- loop into disclosure numbers, re-benchmarking after a prompt change — is in
121
- [references/pricing.md](references/pricing.md).
122
-
123
- **The declared price also decides how long the answer may be.** Each call's
124
- room to answer is funded from the price it declares, so a price that covers
125
- what an answer cost can still cut it off. The bench's recommendation already
126
- leaves that room — always declare what it prints, never the bare charged
127
- number. A sample cut off at your `--max-coins` is not a sample: re-run with a
128
- higher one. When the bench says the answer is longer than one call on this
129
- stand may produce, ask for a shorter answer.
130
-
131
- Only samples that **succeeded and settled** are priced from. A sample the
174
+ It runs on **your own credits**, on the development lane, and prints what each
175
+ sample really cost and was charged, then the numbers to declare:
176
+
177
+ - `Declare maxCredits` — the p95 real cost with the platform fee and the
178
+ server's own recommended headroom applied, never below the room the answer's
179
+ length needs. That is `maxCredits` on every call of this prompt.
180
+ - `Grant perCallMaxCredits` — the same over the worst sample. That is a
181
+ budget's `perCallMaxCredits`; declare it verbatim rather than deriving a
182
+ ceiling from the charged `max`, which carries neither the headroom nor the
183
+ answer's length and gets the budget's calls refused.
184
+ - `Per-call estimate: about … credits per call` and
185
+ `Grant perCallEstimateCredits` — the measured average, fee included, and that
186
+ average rounded up to a whole credit: the honest figures a budget's disclosure
187
+ is built on.
188
+
189
+ Declare the printed numbers. Never guess them, never work them out from a
190
+ vendor's price list, never add a margin of your own. Full procedure — reading
191
+ p50/p95, turning the loop into disclosure numbers, re-benchmarking after a
192
+ prompt change — is in [references/pricing.md](references/pricing.md).
193
+
194
+ A sample cut off at your `--max-credits` is not a sample: re-run with a higher
195
+ one. When the bench says the answer is longer than one call on this stand may
196
+ produce, ask for a shorter answer. `--max-coins` is the old name of
197
+ `--max-credits` and still works, with a line saying so.
198
+
199
+ Only samples that **succeeded and settled** are measured. A sample the
132
200
  provider refused at its door (`provider_http_<status>`) ran no inference and
133
201
  cost nothing; the bench prints the code, the provider's own message and, for a
134
202
  401 or 403, that this is the stand's provider configuration refusing the model
@@ -168,12 +236,12 @@ same object — a schema that passed the bench passes the game.
168
236
  ```ts
169
237
  const grant = await requestSpendGrant({ // inside the click handler
170
238
  models: [modelId],
171
- perCallMaxCoins: NPC_CALL_CEILING,
172
- perCallEstimateCoins: NPC_CALL_PRICE,
239
+ perCallMaxCredits: NPC_GRANT_MAX, // the bench's "Grant perCallMaxCredits"
240
+ perCallEstimateCredits: NPC_CALL_ESTIMATE, // its "Grant perCallEstimateCredits"
173
241
  disclosure: {
174
242
  periodLabel: 'minute',
175
243
  estimatedCallsPerPeriod: 10, // 5 NPCs, one decision each per 30s
176
- estimatedCoinsPerPeriod: 10 * NPC_CALL_PRICE,
244
+ estimatedCreditsPerPeriod: Math.ceil(10 * NPC_CALL_AVERAGE), // calls × the measured average
177
245
  },
178
246
  maxConcurrent: 2,
179
247
  maxCallsPerMinute: 30,
@@ -185,8 +253,45 @@ await savePlayerState({ ...state, grantId: grant.grantId });
185
253
  **The disclosure is computed from this game's own loop, never wished for.** Five
186
254
  NPCs deciding once every thirty seconds is ten calls a minute — write that
187
255
  arithmetic into `DESIGN.md` beside the feature. The player sees your estimate
188
- attributed to the game, beside the platform's own worst case; an estimate that
189
- is transparently low is a grant that dies mid-session.
256
+ attributed to the game, beside the platform's own worst case built from
257
+ `perCallMaxCredits`; an estimate that is transparently low is a grant that dies
258
+ mid-session. Each call under the budget still declares its own `maxCredits`,
259
+ and it may not be above the budget's `perCallMaxCredits`:
260
+
261
+ ```
262
+ perCallEstimateCredits ≤ perCallMaxCredits ≤ the limit the player approves
263
+ maxCredits on each call ≤ perCallMaxCredits
264
+ ```
265
+
266
+ **Using the judge too?** Add `judge: { enabled: true, estimatedCallsPerPeriod }`
267
+ to the same request — the checks you really expect per `periodLabel`, from the
268
+ same loop arithmetic. The sheet tells the player the game also uses a fast
269
+ classifier billed as used, with the platform's own worst case beside your count.
270
+ A judge-only feature passes `models: []`. A budget with the judge is paid from
271
+ the player's credits only, and judge calls share the budget's `maxConcurrent` and
272
+ `maxCallsPerMinute`: batch questions into one call (up to 64) rather than one
273
+ call per question.
274
+
275
+ **Streaming a reply under the budget:**
276
+
277
+ ```ts
278
+ const res = await generateStream({
279
+ grantId, modelId, outputFormat: 'json', schema: LINE_SCHEMA, // `line` is its first property
280
+ prompt: npcPrompt(npc, playerLine), maxCredits: NPC_CALL_MAX,
281
+ idempotencyKey: `npc:${npc.id}:${turnId}`,
282
+ onPartialField: (_name, soFar) => npc.bubble.show(soFar), // provisional
283
+ });
284
+ applyGeneration(res); // the answer is THIS
285
+ ```
286
+
287
+ The pieces are provisional: a call can still fail after text has shown (the
288
+ model broke off, the JSON missed the schema), so the bubble shows them and
289
+ `applyGeneration` decides what the game keeps — the same one writer as always.
290
+ Ceiling, benchmark and receipt are `generate()`'s: a started stream is billed the
291
+ work it did even when the player walks away mid-sentence. A stream holds one
292
+ of the budget's `maxConcurrent` slots until it has settled. Sometimes the reply
293
+ arrives whole — a busy stand, a retried call, a budget on the player's own plan —
294
+ and `onDelta` then gets the whole text once; the game needs no second path.
190
295
 
191
296
  **Then keep the burn low, because you wrote the loop:** batch those five NPCs
192
297
  into ONE call returning five decisions, cache a decision until the situation
@@ -201,10 +306,10 @@ Grant endings are ordinary game states with in-fiction copy, never an error toas
201
306
  | `grant_stopped` | the player pressed Stop | accept silently, keep playing |
202
307
  | `grant_expired` | 24h passed, or the session ended | as stopped; re-request on the next deliberate click |
203
308
  | `waiting_for_plan` | their own plan is rate-limited | wait out the stated time — not a failure, and there is no paid fallback |
204
- | `grant_insufficient_funds` | the wallet cannot fund the next call | pause the thinking NPCs, say it once, stay playable |
309
+ | `grant_insufficient_funds` | the player's credits ran out before the next call | pause the thinking NPCs, say it once, stay playable |
205
310
 
206
- Draw the readout from `getSpendGrant(grantId)` — calls made, coins settled, what
207
- remains — never from a counter the game keeps itself. A finished grant may be
311
+ Draw the readout from `getSpendGrant(grantId)` — calls made, credits settled,
312
+ what remains — never from a counter the game keeps itself. A finished grant may be
208
313
  re-requested, but only from a **fresh deliberate click**: a silent auto-renew is
209
314
  the exact shape a standing approval exists to prevent.
210
315
 
@@ -232,18 +337,18 @@ function applyGeneration(res) { // THE only place output becomes game state
232
337
  `modelProvenance: 'unverified'` because it is user-supplied — check it exactly
233
338
  as you would check typed player input.
234
339
 
235
- ## Errors land on the player's wallet
340
+ ## Errors land on the player's credits
236
341
 
237
342
  There is no compensation lane, so this is all work you do before the call:
238
343
 
239
- - **Validate inputs first** — a malformed prompt is still charged.
344
+ - **Validate inputs first** — a malformed prompt is still billed.
240
345
  - **Always set `schema` for `outputFormat: 'json'`** — unschema'd JSON is the
241
346
  commonest way a call is charged and the result is unusable. Write it in the
242
347
  accepted dialect above; a refused schema is `invalid_schema` and costs
243
348
  nothing, but it is a feature that never runs.
244
- - **Keep prompts short.** Long context is the price.
349
+ - **Keep prompts short.** Long context is most of the bill.
245
350
  - **Never loop `generate()` without a grant**, and never retry in a loop — each
246
- attempt is a separate charge.
351
+ attempt is a separate charge, and a one-time call is at least one whole credit.
247
352
  - **Map every code through `generationErrorMessage(code)`** into in-fiction
248
353
  copy. A player should never read a raw error code inside your game.
249
354
  - **`status: 'unknown'` is not a failure.** It means the charge is not known
@@ -252,12 +357,12 @@ There is no compensation lane, so this is all work you do before the call:
252
357
 
253
358
  ## What the Genex side already does — do not rebuild it
254
359
 
255
- The approval sheet shows the model, the prompt, the price and the terms; the
360
+ The approval sheet shows the model, the prompt, the ceiling and the terms; the
256
361
  game renders no price sheet. **Subscription funding is chosen only there** —
257
362
  never add a "Your plan" row to the game's model picker, because a game cannot
258
363
  offer a funding source. When the player's own watcher is online the personal-plan
259
364
  answer arrives by itself and the game just waits, exactly as it waits for a
260
- coin-funded call. The Genex dashboard header shows progress, active grants with
365
+ credit-funded call. The Genex dashboard header shows progress, active grants with
261
366
  their spend, and a Stop; a Stop pressed there reaches the game as `grant_stopped`.
262
367
 
263
368
  ## Never
@@ -268,7 +373,8 @@ their spend, and a Stop; a Stop pressed there reaches the game as `grant_stopped
268
373
  on your machine, never something a shipped build does.
269
374
  - **Never hand-roll fetch to the runtime API** — the SDK owns the approval
270
375
  handshake, and a hand-rolled call cannot obtain one.
271
- - **Never hardcode a price, a model id, a stand URL, or a margin.**
376
+ - **Never hardcode a ceiling the bench did not print, a model id, a stand URL,
377
+ or a margin.**
272
378
  - **Never let the model be an authority over money, items or rewards**
273
379
  (`$genex-monetization` owns what may move a wallet).
274
380
 
@@ -280,14 +386,18 @@ These are source contracts, not a claim that every stand runs this lane —
280
386
  - [ ] `npx genex llm models` was run and its verdict is in the handoff
281
387
  - [ ] Model ids come from `getGenerationModels()`, never from source
282
388
  - [ ] The picker offers the featured set, labelled with the server's own `label`
283
- - [ ] `estimateCoins` is the figure `npx genex llm bench` printed, verbatim — never the bare charged number
389
+ - [ ] `maxCredits` — and a budget's `perCallMaxCredits` / `perCallEstimateCredits` — are the figures `npx genex llm bench` printed, verbatim — never a charged number
390
+ - [ ] No coin spelling (`estimateCoins`, `perCallMaxCoins`, `estimatedCoinsPerPeriod`) in new code
284
391
  - [ ] The schema uses only the accepted dialect (no `$ref`, `pattern`, `format`, `anyOf`, `default`)
285
392
  - [ ] A bench refused as `generation_limit` was answered with `npx genex llm status`, never a retry
286
393
  - [ ] `generate()` / `requestSpendGrant()` is the first statement of a click handler
287
394
  - [ ] A repeated-call feature uses a grant; a one-off uses `generate()`
288
- - [ ] Disclosure numbers derive from the real loop and are written in `DESIGN.md`
395
+ - [ ] A decision (a label, a yes/no, a level) is a `judge()` question under the grant, not a `generate()` asked for a word
396
+ - [ ] Dialogue the player watches appear uses `generateStream()` under the grant; the resolved result, not the pieces, goes to `applyGeneration()`
397
+ - [ ] Disclosure numbers derive from the real loop and the bench's measured average, and are written in `DESIGN.md`
289
398
  - [ ] Calls are batched and cached; nothing fires on an invisible timer
290
399
  - [ ] Every grant-ending code has in-fiction copy and a playable fallback
400
+ - [ ] In the Genex app (`getShell() === "app"`) the feature's entry point is left out and the authored fallback plays
291
401
  - [ ] The in-game readout comes from `getSpendGrant()`
292
402
  - [ ] Exactly one `applyGeneration()` writer; boot re-attaches with `waitForGeneration()`
293
403
  - [ ] `generationId` is saved BEFORE the await
@@ -303,18 +413,50 @@ Nothing to fix in the game: ship the fallback and say so.
303
413
  **Nothing opens when the player clicks** — an `await` ran before `generate()`
304
414
  and the gesture was lost. Move the call to the first line of the handler.
305
415
 
306
- **`player_wallet_required`** — ONE code for the two early dead ends: the lane
307
- refuses a guest and a PREVIEW build on the same line. `waitForPlayer()` tells
308
- them apart. `guest: true` — guests play but hold no wallet, so show the feature
309
- as sign-in-to-use rather than hiding it (`$genex-threejs-embed-auth`). Signed
310
- in and still refused — this is a `genex preview` draft, which never spends:
311
- check the layout there, and the call itself only after `genex promote`. The
312
- SDK's stock sentence for this code is "Sign in to Genex to use this", which is
313
- right for the guest and wrong on a draft, so write the in-fiction line per
314
- cause rather than showing it for both.
315
-
316
- **`grant_price_unreasonable`** — the declared per-call price is far above what
317
- that prompt can cost on that model. Re-benchmark and declare what it prints.
416
+ **`player_wallet_required`** — ONE code for the two early dead ends: a guest on
417
+ any build, and anyone but the game's OWNER on a `genex preview` test build.
418
+ `waitForPlayer()` tells them apart. `guest: true` — guests play but have no
419
+ credits to spend, so show the feature as sign-in-to-use rather than hiding it
420
+ (`$genex-threejs-embed-auth`). Signed in and still refused on a test build —
421
+ that account does not own the game. Open the preview signed in as the owner and
422
+ the call works there, before any player sees it: the same sheet, the same
423
+ terms, billed to the owner's own credits or plan, marked "test build" on Genex.
424
+ Everyone else can use it after `genex promote`. The SDK's stock sentence for
425
+ this code covers both causes; an in-fiction line per cause reads better.
426
+
427
+ **`grant_price_unreasonable`** — a call under the budget declared a `maxCredits`
428
+ above the budget's `perCallMaxCredits` (or a judge call's worst case is above
429
+ it). Declare the bench's `Declare maxCredits` on each call and its
430
+ `Grant perCallMaxCredits` on the budget — the second is never below the first.
431
+
432
+ **`insufficient_credits`** — the player's credits cannot cover this one-time
433
+ call's ceiling. Say it once in-fiction, keep the authored fallback, and never
434
+ retry in a loop; the Genex sheet already offers them a top-up. From
435
+ `npx genex llm bench` it is YOUR balance: the bench prints what it can spend.
436
+
437
+ **`credits_unverified`** — the player's Genex email is not verified, and credits
438
+ pay only for a verified account. Show the feature as verify-to-use rather than
439
+ hiding it; nothing was charged.
440
+
441
+ **`generation_paused`** — the platform has paused paid generation for now. Not
442
+ the game's fault and not the player's: keep the authored fallback, try again
443
+ on a later deliberate click, never in a loop.
444
+
445
+ **`judge_not_enabled`** — the budget was approved without the judge. Request a
446
+ new one with `judge` from the next deliberate click; never auto-renew.
447
+ **`judge_requires_grant`** — the judge's model was sent to `generate()` with no
448
+ budget; the judge is grant-only and is called with `judge()`.
449
+ **`judge_requires_credits`** — a judge call under a budget that is not paid
450
+ from the player's credits (their own plan cannot pay the judge). Request the
451
+ budget with `judge` and let it be credit-funded; keep the authored branch.
452
+ **`judge_period_limit`** — the checks reached the ceiling the player approved for
453
+ the period (your own `estimatedCallsPerPeriod` at the largest size): wait, and
454
+ if it keeps happening your declared rate is too low — fix the loop or the
455
+ estimate, never retry in a tight loop.
456
+
457
+ **`stream_requires_grant`** — `generateStream()` was called without a
458
+ `grantId`. Streaming is grant-only: a one-time call waits on the player's
459
+ approval popup, so use `generate()` for it.
318
460
 
319
461
  **`generation_limit`** — three ad-hoc calls are already in flight for this
320
462
  account, or recently stopped with their bill still pending; a pending call
@@ -331,9 +473,9 @@ stand's provider configuration refusing the model — tell the operator, and
331
473
  build nothing around it in the game.
332
474
 
333
475
  **`provider_token_limit`** — the answer was cut off: the model ran out of room
334
- before it finished, and the attempt is still charged. The declared price is too
335
- low for the answer's length — re-benchmark and declare what the bench prints,
336
- never the bare charged number — or the answer is longer than one call on this
476
+ before it finished, and the attempt is still billed. The declared `maxCredits`
477
+ is too low for the answer's length — re-benchmark and declare what the bench
478
+ prints, never a charged number — or the answer is longer than one call on this
337
479
  stand may produce, and the fix is a shorter answer (fewer fields, shorter
338
480
  strings, a length the prompt states).
339
481
 
@@ -345,13 +487,17 @@ the subset above; `description` and `title` are allowed.
345
487
  **`grant_concurrency` / `grant_rate_limited`** — the game calls faster than the
346
488
  grant's own limits. Batch and cache; do not raise the limits to hide it.
347
489
 
490
+ **`payer_busy`** — the player's account was busy on Genex for a moment and
491
+ nothing was charged or started. Retry once after a short pause, from the same
492
+ action; it is not a refusal.
493
+
348
494
  **`grant_not_active`** — the saved `grantId` is finished. Clear the stored id
349
495
  and re-request from a fresh click.
350
496
 
351
497
  **`external_request_active`** — that player already has one personal-plan
352
- request running. Wait for it; never fall back to charging coin instead.
498
+ request running. Wait for it; never fall back to charging credits instead.
353
499
 
354
- **The call is charged but the result is unusable** — `outputFormat: 'json'`
500
+ **The call is billed but the result is unusable** — `outputFormat: 'json'`
355
501
  without a `schema`. Add one; the charge already happened.
356
502
 
357
503
  **A reload lost the answer** — `generationId` was not saved before the await, or