@genex-ai/cli-demo 1.4.2 → 1.5.0-dev.389

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/README.md CHANGED
@@ -11,6 +11,7 @@ genex preview # build + push to the hosted draft URL (unlisted)
11
11
  genex publish # build + push, then list the game in the gallery
12
12
  genex make-remixable # make this game remixable — migrate a private source to a public repo
13
13
  genex model "<prompt>" # generate a 3D model → prints an asset URL
14
+ genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
14
15
  genex sfx "<prompt>" # generate a sound fx → prints an asset URL
15
16
  genex texture "<prompt>" # generate a texture → prints an asset URL
16
17
  genex image "<prompt>" # generate an image → prints an asset URL
@@ -87,7 +88,7 @@ baked at build time via `GENEX_CLI_CHANNEL` (see `src/config.ts`).
87
88
 
88
89
  ## Generating assets
89
90
 
90
- `genex model | sfx | texture | image | video "<prompt>"` turn a prompt into a
91
+ `genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
91
92
  real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
92
93
  stream), then prints its public URL — the game loads it directly, nothing is
93
94
  downloaded or committed:
@@ -95,6 +96,7 @@ downloaded or committed:
95
96
  | Command | Provider | Prints (role) |
96
97
  | --- | --- | --- |
97
98
  | `genex model "<prompt>"` | Tripo | `…/generations/<id>/model-glb` |
99
+ | `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
98
100
  | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
99
101
  | `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
100
102
  | `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "latest";
11
+ var RAW_CHANNEL = "dev";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -991,7 +991,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
991
991
  var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
992
992
  # Genex build contract (always in effect for this game)
993
993
 
994
- Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
994
+ Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
995
995
 
996
996
  Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Use genex capabilities as an extension, but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
997
997
 
@@ -1013,6 +1013,7 @@ Important note: put soul into your creations, with many details and love. Aim to
1013
1013
  16. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
1014
1014
  17. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
1015
1015
  18. NEVER delete, empty, move, rename, or overwrite anything you did not create yourself. This folder may hold the player's own reference images, notes, sketches, or an earlier attempt \u2014 files that exist nowhere else and have no undo, no trash, no backup. A non-empty folder is normal and is NEVER something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck interactive prompt, and every setup tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files") \u2014 scaffold into a fresh subfolder and copy in instead. You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes. This binds hardest during setup, where it runs fast and automatically before the player has asked for anything at all.
1016
+ 19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
1016
1017
  ${CONTRACT_END}
1017
1018
  `;
1018
1019
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
@@ -1118,11 +1119,6 @@ var REMOVED_SKILLS = [
1118
1119
  "genex-explore",
1119
1120
  "genex-threejs-procedural-materials",
1120
1121
  // folded into genex-threejs-procedural-assets (AG-881)
1121
- // 2026-08-12: the generated-panorama lane is paused at the provider, and
1122
- // every sky is built in code now. Leaving this SKILL.md
1123
- // installed would keep pointing agents at `npx genex skybox`, which the CLI
1124
- // refuses.
1125
- "genex-ai-skybox",
1126
1122
  "genex-threejs-skill-router",
1127
1123
  "genex-threejs-bloom",
1128
1124
  "genex-threejs-screen-space-ambient-occlusion",
@@ -4895,6 +4891,307 @@ async function runMakeRemixable(opts) {
4895
4891
  }
4896
4892
  }
4897
4893
 
4894
+ // src/commands/domain.ts
4895
+ var SUBCOMMANDS = ["add", "list", "verify", "remove"];
4896
+ function statusWord(d) {
4897
+ if (d.status === "active") return c.green("live");
4898
+ if (d.status === "failed") return c.red("stopped");
4899
+ return c.dim("waiting for DNS");
4900
+ }
4901
+ async function runDomain(opts) {
4902
+ const log = createLogger({ quiet: opts.quiet });
4903
+ const sub = (opts.name ?? "list").trim();
4904
+ if (!SUBCOMMANDS.includes(sub)) {
4905
+ log.error(`Unknown subcommand \`${sub}\`. Use: ${SUBCOMMANDS.join(", ")}.`);
4906
+ process.exitCode = 1;
4907
+ return;
4908
+ }
4909
+ const needsHost = sub !== "list";
4910
+ const hostname = opts.hostname?.trim();
4911
+ if (needsHost && !hostname) {
4912
+ log.error(`\`genex domain ${sub}\` needs a hostname, e.g. ${c.cyan(`genex domain ${sub} play.yourdomain.com`)}.`);
4913
+ process.exitCode = 1;
4914
+ return;
4915
+ }
4916
+ const meta = await readProject();
4917
+ if (!meta?.id) {
4918
+ log.error("This folder isn't linked to a game.");
4919
+ log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
4920
+ process.exitCode = 1;
4921
+ return;
4922
+ }
4923
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
4924
+ let token = opts.token ?? await readUserToken(opts.envPath);
4925
+ if (!token) {
4926
+ if (opts.noAuth) {
4927
+ log.error("Not signed in. Re-run without --no-auth to connect.");
4928
+ process.exitCode = 1;
4929
+ return;
4930
+ }
4931
+ log.plain("Not signed in \u2014 connecting\u2026");
4932
+ try {
4933
+ token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
4934
+ log,
4935
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
4936
+ });
4937
+ } catch (err) {
4938
+ if (err instanceof AuthPendingError) {
4939
+ printAuthHandoff(log, err);
4940
+ log.dim(`Then re-run ${c.cyan(`genex domain ${sub}`)}.`);
4941
+ return;
4942
+ }
4943
+ log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
4944
+ process.exitCode = 1;
4945
+ return;
4946
+ }
4947
+ await writeUserToken(token, opts.envPath);
4948
+ }
4949
+ const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
4950
+ const auth = { Authorization: `Bearer ${token}` };
4951
+ const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, "Content-Type": "application/json", ...init2?.headers ?? {} } });
4952
+ let res;
4953
+ try {
4954
+ if (sub === "list") res = await call(base);
4955
+ else if (sub === "add") res = await call(base, { method: "POST", body: JSON.stringify({ hostname }) });
4956
+ else if (sub === "verify")
4957
+ res = await call(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
4958
+ else res = await call(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
4959
+ } catch (err) {
4960
+ log.error(`Couldn't reach the API at ${apiUrl}.`);
4961
+ log.dim(` ${String(err)}`);
4962
+ process.exitCode = 1;
4963
+ return;
4964
+ }
4965
+ if (!res.ok) {
4966
+ if (await printedStructuredError(res)) {
4967
+ process.exitCode = 1;
4968
+ return;
4969
+ }
4970
+ const body = await res.json().catch(() => null);
4971
+ if (res.status === 503) {
4972
+ log.error("Custom domains aren't available on this Genex environment yet.");
4973
+ } else if (res.status === 404) {
4974
+ log.error(sub === "list" || sub === "add" ? "That game wasn't found on this account." : `${hostname} isn't connected to this game.`);
4975
+ } else {
4976
+ log.error(body?.message ?? body?.error ?? `Request failed (${res.status}).`);
4977
+ }
4978
+ process.exitCode = 1;
4979
+ return;
4980
+ }
4981
+ const data = await res.json();
4982
+ if (opts.json) {
4983
+ log.plain(JSON.stringify(data, null, 2));
4984
+ return;
4985
+ }
4986
+ if (sub === "list") {
4987
+ const rows = data.domains ?? [];
4988
+ if (rows.length === 0) {
4989
+ log.plain("No domains connected to this game.");
4990
+ log.dim(` ${c.cyan("genex domain add play.yourdomain.com")} to connect one.`);
4991
+ return;
4992
+ }
4993
+ for (const d of rows) log.plain(` ${d.hostname.padEnd(34)} ${statusWord(d)}`);
4994
+ return;
4995
+ }
4996
+ if (sub === "add") {
4997
+ if (data.supported === false) {
4998
+ log.plain(String(data.message ?? "That domain's DNS host doesn't support one-click setup."));
4999
+ const manual = data.manual;
5000
+ const records = Array.isArray(manual?.records) ? manual.records : [];
5001
+ if (records.length > 0) {
5002
+ log.plain("");
5003
+ log.plain(" Add these two records at your DNS host:");
5004
+ log.plain("");
5005
+ for (const r of records) {
5006
+ log.plain(` ${c.cyan(String(r.type ?? ""))} ${String(r.name ?? "")}`);
5007
+ log.plain(` ${String(r.value ?? "")}`);
5008
+ }
5009
+ log.plain("");
5010
+ if (manual?.apexHint) {
5011
+ log.dim(" At a root domain your host may call the first one ALIAS or ANAME.");
5012
+ }
5013
+ log.dim(` Then run: genex domain verify ${String(data.hostname ?? "")}`);
5014
+ log.dim(" DNS can take a few minutes to spread.");
5015
+ return;
5016
+ }
5017
+ log.dim(" Your game stays reachable at its usual address.");
5018
+ return;
5019
+ }
5020
+ const applyUrl = String(data.applyUrl ?? "");
5021
+ const providerName = String(data.providerName ?? "your DNS host");
5022
+ log.success(`${data.hostname} can be connected through ${providerName}.`);
5023
+ log.plain("");
5024
+ log.plain(` Approve the DNS change here: ${c.cyan(applyUrl)}`);
5025
+ log.plain("");
5026
+ log.dim(" One click writes the records. Nothing to copy or paste.");
5027
+ if (!applyUrl.startsWith("https://")) {
5028
+ log.warn("That DNS host returned a setup link we could not verify \u2014 not opening it.");
5029
+ } else if (!opts.noOpen) {
5030
+ openBrowser(applyUrl, () => log.dim(" (Couldn't open a browser \u2014 use the link above.)"));
5031
+ }
5032
+ log.dim(` Then ${c.cyan(`genex domain verify ${String(data.hostname)}`)} once you have approved it.`);
5033
+ return;
5034
+ }
5035
+ if (sub === "verify") {
5036
+ const status = String(data.status ?? "");
5037
+ if (status === "active") log.success(`${hostname} is live.`);
5038
+ else if (status === "failed") log.error(`${hostname} is stopped \u2014 its verification record is missing.`);
5039
+ else log.plain(`${hostname} isn't verified yet \u2014 DNS changes can take a few minutes to spread.`);
5040
+ return;
5041
+ }
5042
+ log.success(`${hostname} disconnected.`);
5043
+ }
5044
+
5045
+ // src/commands/shop.ts
5046
+ var SUBS = ["list", "add", "set", "remove"];
5047
+ function money(cents) {
5048
+ return cents === void 0 ? "" : ` ($${(cents / 100).toFixed(2)})`;
5049
+ }
5050
+ async function runShop(opts) {
5051
+ const log = createLogger({ quiet: opts.quiet });
5052
+ const sub = opts.name?.trim() || "list";
5053
+ if (!SUBS.includes(sub)) {
5054
+ log.error(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS.join(", ")}.`);
5055
+ process.exitCode = 1;
5056
+ return;
5057
+ }
5058
+ const meta = await readProject();
5059
+ if (!meta?.id) {
5060
+ log.error("This folder isn't linked to a game.");
5061
+ log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
5062
+ process.exitCode = 1;
5063
+ return;
5064
+ }
5065
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
5066
+ let token = opts.token ?? await readUserToken(opts.envPath);
5067
+ if (!token) {
5068
+ if (opts.noAuth) {
5069
+ log.error("Not signed in. Re-run without --no-auth to connect.");
5070
+ process.exitCode = 1;
5071
+ return;
5072
+ }
5073
+ log.plain("Not signed in \u2014 connecting\u2026");
5074
+ try {
5075
+ token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
5076
+ log,
5077
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
5078
+ });
5079
+ } catch (err) {
5080
+ if (err instanceof AuthPendingError) {
5081
+ printAuthHandoff(log, err);
5082
+ log.dim(`Then re-run ${c.cyan(`genex shop ${sub}`)}.`);
5083
+ return;
5084
+ }
5085
+ log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
5086
+ process.exitCode = 1;
5087
+ return;
5088
+ }
5089
+ await writeUserToken(token, opts.envPath);
5090
+ }
5091
+ const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
5092
+ const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
5093
+ const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
5094
+ async function fail3(res2, what) {
5095
+ if (printedStructuredError(res2)) {
5096
+ process.exitCode = 1;
5097
+ return;
5098
+ }
5099
+ process.exitCode = 1;
5100
+ const body = await res2.json().catch(() => ({}));
5101
+ if (res2.status === 404 && body.error === "not_found") {
5102
+ log.error("In-game purchases aren't enabled on this environment yet.");
5103
+ return;
5104
+ }
5105
+ log.error(body.message ?? `Couldn't ${what} (HTTP ${res2.status}).`);
5106
+ if (body.error === "price_off_grid") {
5107
+ log.dim(` See the full list any time: ${c.cyan("genex shop list")}`);
5108
+ }
5109
+ }
5110
+ if (sub === "list") {
5111
+ const res2 = await call(projectSkus);
5112
+ if (!res2.ok) return fail3(res2, "read your shop");
5113
+ const body = await res2.json();
5114
+ if (opts.json) {
5115
+ log.plain(JSON.stringify(body, null, 2));
5116
+ return;
5117
+ }
5118
+ if (body.items.length === 0) {
5119
+ log.plain("This game sells nothing yet.");
5120
+ log.dim(` Add something: ${c.cyan('genex shop add "Iron Key" --price 100')}`);
5121
+ log.dim(` Prices available: ${body.priceGrid.join(", ")} coin`);
5122
+ return;
5123
+ }
5124
+ log.plain(`${body.items.length} item${body.items.length === 1 ? "" : "s"}:`);
5125
+ for (const s of body.items) {
5126
+ const state = s.active ? "" : c.dim(" (retired)");
5127
+ log.plain(
5128
+ ` ${c.cyan(s.id)} ${s.name} \u2014 ${s.priceCoins} coin${money(s.priceDisplayUsdCents)} ${c.dim(s.type)}${state}`
5129
+ );
5130
+ }
5131
+ log.dim(`Prices available: ${body.priceGrid.join(", ")} coin`);
5132
+ return;
5133
+ }
5134
+ if (sub === "add") {
5135
+ const name = opts.hostname?.trim();
5136
+ if (!name) {
5137
+ log.error(`\`genex shop add\` needs a name, e.g. ${c.cyan('genex shop add "Iron Key" --price 100')}.`);
5138
+ process.exitCode = 1;
5139
+ return;
5140
+ }
5141
+ if (!opts.price) {
5142
+ log.error(`\`genex shop add\` needs ${c.cyan("--price")}, in coin.`);
5143
+ process.exitCode = 1;
5144
+ return;
5145
+ }
5146
+ const res2 = await call(projectSkus, {
5147
+ method: "POST",
5148
+ body: JSON.stringify({
5149
+ name,
5150
+ priceCoins: opts.price,
5151
+ type: opts.type === "durable" ? "durable" : "consumable",
5152
+ ...opts.icon ? { iconUrl: opts.icon } : {}
5153
+ })
5154
+ });
5155
+ if (!res2.ok) return fail3(res2, "add that item");
5156
+ const sku = await res2.json();
5157
+ if (opts.json) {
5158
+ log.plain(JSON.stringify(sku, null, 2));
5159
+ return;
5160
+ }
5161
+ log.success(`Added ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
5162
+ log.plain(` id: ${c.cyan(sku.id)}`);
5163
+ log.dim(` Use it in the game: buy({ skuId: "${sku.id}" })`);
5164
+ return;
5165
+ }
5166
+ const skuId = opts.hostname?.trim();
5167
+ if (!skuId) {
5168
+ log.error(`\`genex shop ${sub}\` needs an item id \u2014 see ${c.cyan("genex shop list")}.`);
5169
+ process.exitCode = 1;
5170
+ return;
5171
+ }
5172
+ const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
5173
+ if (sub === "set") {
5174
+ const patch = {};
5175
+ if (opts.rename) patch.name = opts.rename;
5176
+ if (opts.price) patch.priceCoins = opts.price;
5177
+ if (opts.icon) patch.iconUrl = opts.icon;
5178
+ if (Object.keys(patch).length === 0) {
5179
+ log.error(`Nothing to change. Pass ${c.cyan("--rename")}, ${c.cyan("--price")} or ${c.cyan("--icon")}.`);
5180
+ process.exitCode = 1;
5181
+ return;
5182
+ }
5183
+ const res2 = await call(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
5184
+ if (!res2.ok) return fail3(res2, "update that item");
5185
+ const sku = await res2.json();
5186
+ log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
5187
+ return;
5188
+ }
5189
+ const res = await call(skuUrl, { method: "DELETE" });
5190
+ if (!res.ok) return fail3(res, "remove that item");
5191
+ log.success("Removed from the shop.");
5192
+ log.dim(" Players who already bought it keep it.");
5193
+ }
5194
+
4898
5195
  // src/lib/promote.ts
4899
5196
  async function promoteBuild(apiUrl, projectId, token, log) {
4900
5197
  let res;
@@ -6383,10 +6680,7 @@ function printHint(kind, view, files, log) {
6383
6680
  const url = files[0]?.url ?? "";
6384
6681
  const hint = {
6385
6682
  model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
6386
- // Unreachable while the lane is paused (see src/index.ts) kept so a
6387
- // restore is one deletion. Games that already ship a panorama still load
6388
- // it this way.
6389
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment. url = "${url}"`,
6683
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
6390
6684
  sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
6391
6685
  music: `Load with AudioLoader into a non-positional THREE.Audio: setLoop(true), setVolume(0.30), start on the first user gesture \u2014 and the settings screen MUST have Music + SFX volume sliders \u2014 see genex-ai-music. url = "${url}"`,
6392
6686
  voice: `Load with AudioLoader into a THREE.Audio (or PositionalAudio at the speaker); voice lines are content \u2014 keep them short, SUBTITLED, and skippable \u2014 see genex-ai-voice. url = "${url}"`,
@@ -19108,7 +19402,17 @@ ${c.bold("Usage")}
19108
19402
  genex publish [options] Build + push + make live, then list it in the gallery.
19109
19403
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
19110
19404
  private source onto a public managed genex repo.
19405
+ genex domain <sub> [host] Play this game on a domain you own:
19406
+ add | list | verify | remove. "add" opens a
19407
+ one-click approval at your DNS host \u2014 no
19408
+ records to copy or paste.
19409
+ genex shop <sub> [name|id] What this game sells: list | add | set | remove.
19410
+ "add" prints the item id your game passes to
19411
+ buy({ skuId }) \u2014 the platform owns the catalog,
19412
+ so this is the only way one exists. Prices come
19413
+ off a fixed grid, printed on any refusal.
19111
19414
  genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
19415
+ genex skybox "<prompt>" [options] Generate a skybox (equirect); prints a public asset URL.
19112
19416
  genex sfx "<prompt>" [options] Generate a sound effect (mp3); prints a public asset URL.
19113
19417
  genex music "<prompt>" [options] Generate an instrumental music track (mp3); prints a
19114
19418
  public asset URL. --duration 10-300s (default 90).
@@ -19183,8 +19487,12 @@ ${c.bold("Usage")}
19183
19487
  Pure local work (no API); the PNG tools read
19184
19488
  a file or URL, audit scans the project.
19185
19489
 
19186
- ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `image` `video`)")}
19490
+ ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
19187
19491
  --terrain (texture) seamless tiling surface for terrain/ground.
19492
+ --raw (skybox) send the prompt exactly as written \u2014 skip the
19493
+ appended environment-only clause AND the refusal on
19494
+ prompts that name structures/objects (a sky is sky only;
19495
+ landforms and weather always pass).
19188
19496
  --duration <sec> (sfx, video, music) target clip length in seconds;
19189
19497
  (character/creature animate) 3-6 s of reference video for
19190
19498
  verbs that route to AI video (default 3) \u2014 longer gives a
@@ -19348,8 +19656,15 @@ ${c.bold("Examples")}
19348
19656
  genex publish
19349
19657
  genex publish --categories games,vfx
19350
19658
  genex make-remixable
19659
+ genex domain add play.mygame.com
19660
+ genex domain list
19661
+ genex shop list
19662
+ genex shop add "Iron Key" --price 100 --type durable
19663
+ genex shop set sku_123 --price 200
19664
+ genex shop remove sku_123
19351
19665
  genex publish --no-push --title "My Game"
19352
19666
  genex model "weathered wooden barrel with iron bands"
19667
+ genex skybox "golden hour over a misty mountain range"
19353
19668
  genex sfx "punchy laser zap" --duration 2
19354
19669
  genex music "brooding orchestral battle loop, seamless loop, no intro or outro" --no-wait
19355
19670
  genex voice "You shall not pass!" --voice gruff
@@ -19399,6 +19714,10 @@ function parseArgs(argv) {
19399
19714
  "--name",
19400
19715
  "--repo",
19401
19716
  "--remixed-from",
19717
+ "--price",
19718
+ "--icon",
19719
+ "--rename",
19720
+ "--type",
19402
19721
  "--title",
19403
19722
  "--description",
19404
19723
  "--categories",
@@ -19470,6 +19789,9 @@ function parseArgs(argv) {
19470
19789
  case "--no-auth":
19471
19790
  parsed.options.noAuth = true;
19472
19791
  break;
19792
+ case "--no-open":
19793
+ parsed.options.noOpen = true;
19794
+ break;
19473
19795
  case "--no-push":
19474
19796
  parsed.options.noPush = true;
19475
19797
  break;
@@ -19604,6 +19926,18 @@ function parseArgs(argv) {
19604
19926
  } else {
19605
19927
  (parsed.options.selectors ??= []).push(arg);
19606
19928
  }
19929
+ } else if (parsed.command === "shop") {
19930
+ if (!parsed.options.hostname) parsed.options.hostname = arg;
19931
+ else {
19932
+ parsed.error = `Unexpected argument: ${arg}`;
19933
+ return parsed;
19934
+ }
19935
+ } else if (parsed.command === "domain") {
19936
+ if (!parsed.options.hostname) parsed.options.hostname = arg;
19937
+ else {
19938
+ parsed.error = `Unexpected argument: ${arg}`;
19939
+ return parsed;
19940
+ }
19607
19941
  } else if (parsed.command === "animations") {
19608
19942
  parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
19609
19943
  } else {
@@ -19774,6 +20108,27 @@ function applyValueFlag(options, flag, value) {
19774
20108
  options.duration = n;
19775
20109
  break;
19776
20110
  }
20111
+ case "--price": {
20112
+ const n = Number(value);
20113
+ if (!Number.isInteger(n) || n <= 0) {
20114
+ throw new Error(`Invalid --price value: ${value} (whole coin, e.g. 100)`);
20115
+ }
20116
+ options.price = n;
20117
+ break;
20118
+ }
20119
+ case "--icon":
20120
+ options.icon = value;
20121
+ break;
20122
+ case "--type": {
20123
+ if (value !== "consumable" && value !== "durable") {
20124
+ throw new Error(`Invalid --type value: ${value} (consumable or durable)`);
20125
+ }
20126
+ options.type = value;
20127
+ break;
20128
+ }
20129
+ case "--rename":
20130
+ options.rename = value;
20131
+ break;
19777
20132
  case "--aspect":
19778
20133
  options.aspect = value;
19779
20134
  break;
@@ -19913,21 +20268,6 @@ async function main() {
19913
20268
  ci: !!process.env.CI
19914
20269
  });
19915
20270
  try {
19916
- if (parsed.command === "skybox") {
19917
- log.error("The generated-sky lane is paused \u2014 `genex skybox` is not available.");
19918
- log.plain(
19919
- [
19920
- ` ${c.cyan("\u2192")} Build the sky in code instead \u2014 your call how: a background colour with`,
19921
- ` fog, a gradient dome, or three.js's own ${c.cyan("Sky")} shader with a real sun. It costs`,
19922
- " no credits, no download and no phone memory, and the sun stays in sync with",
19923
- " your lighting.",
19924
- ` ${c.cyan("\u2192")} A distant skyline or landmark was never a sky: build it as geometry, or`,
19925
- ` generate it with ${c.cyan("npx genex model")}.`
19926
- ].join("\n")
19927
- );
19928
- process.exitCode = 1;
19929
- return;
19930
- }
19931
20271
  if (GEN_KINDS.has(parsed.command)) {
19932
20272
  await runGenerate(parsed.command, {
19933
20273
  ...parsed.options,
@@ -19954,6 +20294,12 @@ async function main() {
19954
20294
  case "make-remixable":
19955
20295
  await runMakeRemixable(parsed.options);
19956
20296
  break;
20297
+ case "shop":
20298
+ await runShop(parsed.options);
20299
+ break;
20300
+ case "domain":
20301
+ await runDomain(parsed.options);
20302
+ break;
19957
20303
  case "controller":
19958
20304
  await runController({ ...parsed.options, kind: parsed.options.name });
19959
20305
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.4.2",
3
+ "version": "1.5.0-dev.389",
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": {
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: genex-ai-skybox
3
+ description: Generate a real 360° skybox (equirectangular panorama) from a text prompt with `npx genex skybox`, then load it as the scene background + environment lighting in Three.js. Use when the user wants a specific photoreal/painted sky or backdrop ("sunset over the ocean", "alien purple nebula", "foggy pine forest") as an image, rather than a procedural/shader sky.
4
+ ---
5
+
6
+ # Genex AI · Skybox
7
+
8
+ Turn a prompt into a 360° **equirectangular** panorama that wraps the scene and
9
+ also lights it (image-based lighting).
10
+
11
+ ## When to use this vs. a procedural sky
12
+
13
+ - **Use `npx genex skybox`** for a specific, describable ENVIRONMENT — "golden
14
+ hour over misty mountains", "rolling thunderstorm, towering cumulonimbus",
15
+ "alien purple nebula", "hazy dusk sky over a distant sea horizon". You get a
16
+ real image.
17
+ - Build a procedural, animated, time-of-day sky directly in the game's existing
18
+ scene/runtime only when that is the requested deliverable; this image lane
19
+ owns static generated panoramas.
20
+
21
+ **A skybox is environment only — never content.** Buildings, trees, ruins,
22
+ towers, or any object painted into the sky render at infinite distance: they
23
+ never get closer as the player moves, they sit at the wrong parallax against
24
+ the real 3D world, and they read as broken the moment the camera strafes.
25
+ Structures belong in the scene as geometry (`npx genex model`); the sky
26
+ carries atmosphere — light, weather, clouds, haze, stars.
27
+
28
+ **Writing "no cathedral" does not rescue a prompt that also says "cathedral".**
29
+ Image models are steered by the nouns they ARE given; a ban loses to any
30
+ positive mention of the same thing in the same prompt. A real pilot asked for
31
+ a *"cinematic dark anime gothic atmosphere, ruined cathedral courtyard mood,
32
+ **no buildings or structures**"* — and got a full gothic cathedral with a
33
+ courtyard balustrade and a baked ground plane. Five negations in that prompt,
34
+ zero effect. So never name a structure in a skybox prompt **even to exclude
35
+ it**: describe only light, colour, cloud, weather and time of day.
36
+
37
+ The CLI enforces this both ways. It appends a closed-positive environment
38
+ clause to every prompt (you'll see `↳ environment-only guard applied`; the
39
+ full final prompt is stored with the generation), and it **refuses outright,
40
+ before spending any credits**, when the prompt itself names a structure or
41
+ object. Landforms are fine — "golden hour over misty mountains", "a distant
42
+ sea horizon", "towering cumulonimbus" all pass. If you genuinely need content
43
+ baked into the sky — e.g. a space station panorama for a scene with no world
44
+ geometry — pass `--raw` to send your prompt exactly as written.
45
+
46
+ ## Run
47
+
48
+ ```bash
49
+ npx genex skybox "<prompt>"
50
+ ```
51
+
52
+ Blocks until ready, then prints its public URL:
53
+
54
+ ```
55
+ https://assets.genex.technology/generations/<id>/skybox-equirect
56
+ ```
57
+
58
+ The image lives in Genex storage (R2) and loads straight from that URL — you don't
59
+ download it and nothing is committed to your repo. The URL is permanent (works the same
60
+ in local dev, the published game, and remixes).
61
+
62
+ ## Load it as background + environment
63
+
64
+ Load the equirect JPG through the quality tier's rung ladder, mark it
65
+ equirectangular, and use it for both the visible background and the lighting.
66
+ The bare URL is an 8192×4096 original — ~178 MB decoded, over half a phone's
67
+ GPU budget in one texture — so phones must load the downscale rung the platform
68
+ stores next to every skybox (`$genex-threejs-adaptive-quality`):
69
+
70
+ ```ts
71
+ import * as THREE from "three";
72
+ import { detectTier } from "./controllers/quality/tier.ts";
73
+ import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
74
+
75
+ // the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
76
+ const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
77
+ const tier = detectTier(); // reuse the boot tier if you already have it
78
+ const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
79
+ new THREE.TextureLoader().loadAsync(u),
80
+ );
81
+ texture.mapping = THREE.EquirectangularReflectionMapping;
82
+ texture.colorSpace = THREE.SRGBColorSpace;
83
+
84
+ scene.background = texture; // visible sky
85
+ scene.environment = texture; // image-based lighting on PBR materials
86
+ ```
87
+
88
+ Desktop gets the original; phones get the `@2048`/`@4096` rung; a missing rung
89
+ falls back to the original automatically — never a broken boot.
90
+
91
+ For sharper reflections/lighting, pre-filter it with `PMREMGenerator`:
92
+
93
+ ```ts
94
+ const pmrem = new THREE.PMREMGenerator(renderer);
95
+ const envMap = pmrem.fromEquirectangular(texture).texture;
96
+ scene.environment = envMap;
97
+ scene.background = texture; // keep the raw texture for the visible sky
98
+ // Do NOT dispose `texture` here — it IS the visible background. Dispose it only
99
+ // if you later stop using it as the sky.
100
+ ```
101
+
102
+ ## Publish checklist
103
+
104
+ - Load it from the **URL** the command printed — absolute and permanent, so it resolves
105
+ the same in local dev, the published game, and remixes. Nothing to commit.
106
+ - Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
107
+
108
+ ## Options
109
+
110
+ - `--raw` — send the prompt exactly as written, skipping the appended
111
+ environment-only guard (for the rare sky that must carry content).
112
+ - `--no-wait` — enqueue and return immediately with the generation id; pick
113
+ the result up later with `npx genex wait <id>` (safe to re-run — it attaches
114
+ to the SAME generation).
115
+ - `--api-url <url>` — override the API base (local dev).
116
+
117
+ ## Troubleshooting
118
+
119
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
120
+ - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
121
+ this skybox generation. Tell the user the facts the CLI printed: their balance, this
122
+ generation's cost, and when their credits refill. Then offer to continue the build
123
+ with a procedurally-coded placeholder (a gradient/procedural sky or a plain
124
+ `scene.background` color) and mark the spot with
125
+ `// TODO(genex): regenerate when credits refill` so the real asset is one command
126
+ away later. Do not stop the session over this.
127
+ - **"Email not verified" (`email_verification_required`)** — generation credits
128
+ unlock after the account's email is verified. Give the user the verify link the
129
+ CLI printed, wait for them to confirm, then re-run the command.
130
+ - **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
131
+ `scene.environment` influence via material `envMapIntensity`.
132
+ - **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
133
+ away from looking straight up/down, or use PMREM for the lighting.
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
225
225
 
226
226
  ## Troubleshooting
227
227
 
228
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
228
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
229
229
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
230
230
  this texture generation. Tell the user the facts the CLI printed: their balance,
231
231
  this generation's cost, and when their credits refill. Then offer to continue the
@@ -151,7 +151,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
151
151
 
152
152
  ## Troubleshooting
153
153
 
154
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
154
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
155
155
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
156
156
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
157
157
  - **Nothing plays / black surface** — the first `video.play()` must run inside a user
@@ -112,8 +112,8 @@ Genex asset and runtime lanes are:
112
112
 
113
113
  - `npx genex model "<prompt>"`—a static GLB → `$genex-ai-model`
114
114
  - `npx genex texture "<prompt>"`—a generated surface → `$genex-ai-texture`
115
- - sky, horizon, time of day, weather mood—built in code, never generated; no
116
- command and no skill owns it, so pick what this game needs
115
+ - `npx genex skybox "<prompt>"`—a 360° environment-only sky
116
+ `$genex-ai-skybox`
117
117
  - `npx genex sfx "<prompt>"` / `music` / `voice` →
118
118
  `$genex-ai-sfx`, `$genex-ai-music`, `$genex-ai-voice`
119
119
  - `npx genex image "<prompt>"` / `video`—2D and motion art →
@@ -146,10 +146,9 @@ Assets row.
146
146
  The request sets the floor for the asset plan, never the ceiling: a world
147
147
  should feel dressed and alive, so plan the detail its genre implies — and pick
148
148
  the more efficient engine per object. Paid generation buys hero pieces (the
149
- player's character, key props, music); procedural code is the
149
+ player's character, key props, the skybox, music); procedural code is the
150
150
  unlimited detail engine for structures, buildings, modular kits, and repeated
151
- or varied dressing (`$genex-threejs-procedural-assets` owns that lane) — and
152
- for the sky, which is never generated at all. Mixing
151
+ or varied dressing (`$genex-threejs-procedural-assets` owns that lane). Mixing
153
152
  both in one scene is the normal way to build, never a fallback. In focused
154
153
  work, stay inside the touched scope. Run independent planned generations with
155
154
  `--no-wait`, scaffold while they land, and preserve their IDs, URLs, and
@@ -54,11 +54,12 @@ copy demo architecture.
54
54
  | fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
55
55
  | a static generated GLB for a concrete prop, vehicle, building, or non-rigged object | `$genex-ai-model` |
56
56
  | generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
57
- | sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
57
+ | environment-only 360° sky | `$genex-ai-skybox` |
58
58
  | poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
59
59
  | in-world motion art or another requested video | `$genex-ai-video` |
60
60
  | sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
61
61
  | 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` |
62
+ | 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` |
62
63
  | cinematic menu/title/pause/victory/defeat/lobby/credits video treatment | `$genex-ai-menu` |
63
64
  | drawn HUD chrome the game's style wants—one element or a matched set of frames, masks, and icons | `$genex-ai-hud` |
64
65
  | the game works but feels flat, floaty, or unresponsive: input response, camera, impacts, cooldowns, difficulty, fail/retry | `$genex-threejs-game-feel` |
@@ -40,6 +40,7 @@ the asset lives in Genex storage (R2), not your repo, so there's nothing to comm
40
40
 
41
41
  ```bash
42
42
  npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
43
+ npx genex skybox "golden hour over mountains" # a 360° sky + lighting
43
44
  npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
44
45
  npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
45
46
  npx genex image "vintage travel poster" # a picture (poster/sign/sprite/decal)
@@ -50,11 +51,8 @@ npx genex video "waterfall mist drifting" --loop # a video clip (screen/bac
50
51
  `npx genex` resolve to the right CLI.)
51
52
 
52
53
  Each has a focused skill with the exact loader code — `$genex-ai-model`,
53
- `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
54
- `$genex-ai-video`. The sky is not on that list: there is no sky command, so
55
- every sky, horizon, and time of day is built in code — your call how, from a
56
- background colour and fog to an atmospheric sky shader with a real sun. Two
57
- more workflow skills draw UI art on the same commands,
54
+ `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
55
+ `$genex-ai-video`. Two more workflow skills draw UI art on the same commands,
58
56
  for games whose style wants it: `$genex-ai-menu` (a cinematic menu — looping
59
57
  video backdrop + DOM buttons) and `$genex-ai-hud` (a HUD sprite set with
60
58
  masked fills). A restrained interface in clean CSS is a finished UI too —
@@ -161,7 +159,7 @@ and re-link the clone to the same live game:
161
159
  ```bash
162
160
  git clone <the game's repo url> my-game && cd my-game
163
161
  npm install
164
- npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
162
+ npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
165
163
  ```
166
164
 
167
165
  Don't know the slug? **`npx genex list`** prints every game on your account —
@@ -188,7 +186,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
188
186
  and your own files are never touched:
189
187
 
190
188
  ```bash
191
- npx @genex-ai/cli-demo@latest init
189
+ npx @genex-ai/cli-demo@dev init
192
190
  ```
193
191
 
194
192
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -0,0 +1,251 @@
1
+ ---
2
+ name: genex-monetization
3
+ description: Build an in-game shop that sells for platform coin — item catalog, purchase flow, delivery, and the per-game soft-currency economy a purchase attaches to. Use when the player asks to sell things, add a shop, monetize, or make the game earn. Carries the hard rules: no paid randomness, no gambling in coin, no donation mechanics, and a real-money price beside every coin price.
4
+ ---
5
+
6
+ # Genex Monetization
7
+
8
+ Games on Genex can sell things for **coin**, the platform currency. The player
9
+ buys coin with real money once; spending it inside a game is a ledger movement
10
+ the game never touches. You design what is for sale; the platform owns the
11
+ wallet, the confirmation, and the money.
12
+
13
+ Load this when the game should sell something. Ask first if it should — a game
14
+ with no loop worth monetizing is better without a shop (see §1).
15
+
16
+ ## The hard rules, and the test that generalizes them
17
+
18
+ Before you build ANY purchasable thing, run this test:
19
+
20
+ > **Does the player pay?** (with coin, or with anything bought with coin —
21
+ > directly or indirectly, including a per-game token or key that coin bought.)
22
+ > **Is the outcome uncertain when they pay?**
23
+ > **Is there a prize** — an item, currency, or advantage they wanted?
24
+ >
25
+ > **All three yes = paid randomness. Build the deterministic version instead.**
26
+
27
+ That triad is the test used by every app store to identify gambling, and it
28
+ catches mechanics that do not exist yet — which a list of banned names cannot.
29
+
30
+ Four things are never built, whatever the request:
31
+
32
+ 1. **No paid randomness.** No loot boxes, gacha, mystery boxes, crates, packs,
33
+ prize wheels, raffles, "spin for a bonus", "chance to double your coins".
34
+ Directly or indirectly.
35
+ 2. **No gambling in coin.** No wagering, staking, betting, coinflips, casino or
36
+ slot mechanics denominated in coin or in anything coin buys.
37
+ 3. **No donation or begging mechanics.** No "donate to me" prompts, tip jars, or
38
+ player-to-player coin transfers. Coin buys goods; it never just moves.
39
+ 4. **No pressure.** No countdown timers, "ends in", "limited time", "only N
40
+ left", or stock counters anywhere in the shop.
41
+
42
+ **Randomness the player EARNS by playing is gameplay, not commerce, and is
43
+ completely fine**: an enemy dropping a random item, a chest you found in the
44
+ level, a procedural layout, a critical-hit roll, a shuffled deck. The line is
45
+ what triggered the roll — play, or payment. Build those freely.
46
+
47
+ Genex refuses paid randomness outright rather than allowing it with disclosed
48
+ odds. That is stricter than any app store, and it is why no Genex game needs an
49
+ odds table, an age gate, or a per-country check.
50
+
51
+ ### When a request crosses a line
52
+
53
+ Answer in exactly three parts, then build:
54
+
55
+ 1. **Name it.** "A loot crate is paid randomness — the player pays before
56
+ knowing what they get."
57
+ 2. **Why.** One sentence. "Genex doesn't sell chance; it's a purchase the player
58
+ can't price, and it's what regulators fine studios over."
59
+ 3. **Offer the alternative,** concretely enough to start on, and build that.
60
+
61
+ Never build the banned version "as an option", never build a partial one, and
62
+ never ask the user to confirm they want it. If they insist, restate the rule
63
+ once and build the compliant version. There is no escalation path.
64
+
65
+ **What to build instead:**
66
+
67
+ | They asked for | Build |
68
+ | --- | --- |
69
+ | Loot box, crate, mystery box, card pack | A direct-purchase shop: every item listed at a fixed price, contents visible. For the collecting feel, add a **visible catalog with a completion track** — any purchase advances a meter to a stated milestone reward. |
70
+ | Gacha, banner, pull, summon | A **deterministic unlock**: the character costs a fixed price, or unlocks at a stated number of runs. Coin may buy a stated, visible number of those points. |
71
+ | Prize wheel, spin-to-win, slot machine | A **free spin earned by finishing a run** (never bought), or a **"pick one of three"** screen where all three are visible and the player chooses. Keeps the moment, drops the wager. |
72
+ | Casino game, blackjack, poker, roulette | The same game with **chips that are granted free each session, reset on restart, cannot be bought and cannot become coin**. It becomes a card game. Sell cosmetics — table felt, card backs — for coin. |
73
+ | Coinflip, double-or-nothing, wager my coins | A **skill-based risk/reward inside the run**: a harder route with a bigger payout, staking the run's own score, which was never purchasable. |
74
+ | Betting on matches, PvP wagers | **Leaderboards with a fixed cosmetic reward for placement**, paid by the game. Nobody's balance goes down. |
75
+ | Donate button, tip jar, "pls donate" | A **gift that is a purchase**: they buy a specific item at a stated price and give it. Or a **supporter cosmetic** — a badge or aura at a normal price, where what's delivered is visible. |
76
+ | Pay to remove a wait / energy gate | **Delete the gate** and sell a permanent upgrade or a cosmetic. Pace with difficulty, not with a timer. |
77
+ | Limited-time offer, flash sale | A **permanent tiered ladder** — the value comes from volume, not from a clock. |
78
+ | Pay-to-win stat boost in a competitive game | **Cosmetics**, or a boost that only applies in single-player content. |
79
+
80
+ ## Designing a shop worth buying from
81
+
82
+ Nine checks. Each one is answerable about your actual design.
83
+
84
+ 1. **The shop attaches to a progression that already exists.** Name the screen
85
+ it opens from and the meter a purchase moves. Build the loop first; a shop in
86
+ a game with nothing to want is furniture.
87
+ 2. **A boost shortens a grind the player has already felt.** State it in one
88
+ sentence: "this skips the ore-gathering they've done four times." If you
89
+ can't, it isn't a boost, it's a number.
90
+ 3. **Nothing sold invalidates the core loop.** If a paying and a non-paying
91
+ player both reach the end, the payer must not have skipped the part that IS
92
+ the game.
93
+ 4. **No manufactured friction.** If the annoyance wouldn't exist without the
94
+ shop, remove the annoyance instead of selling the cure.
95
+ 5. **Everything sold is reachable free.** Spending is a shortcut or a
96
+ decoration, never the only path.
97
+ 6. **Prices land on the grid.** Item prices use 50 / 100 / 200 / 500 / 1000 coin
98
+ so every coin pack divides evenly into them and nobody is left holding change
99
+ they cannot spend.
100
+ 7. **Every price shows real money next to it.** The server sends
101
+ `priceDisplayUsdCents` with every item — render it. `250 coins ($2.49)`.
102
+ 8. **One currency layer between money and goods.** Coin buys items. A per-game
103
+ earned currency buys per-game upgrades. They never convert into each other.
104
+ 9. **Purchases never expire and survive a reinstall.** Entitlements live on the
105
+ server; the game re-reads them on every boot.
106
+
107
+ For a per-game earned currency, the load-bearing number is **minutes of play per
108
+ unit earned**. Set it, then price the cheapest meaningful item at one to three
109
+ sessions of earning. Everything else follows. Spend sinks come in three kinds —
110
+ permanent upgrades, refills, cosmetics — and cosmetics are what absorbs late-game
111
+ currency without touching balance.
112
+
113
+ ## Stocking the shop
114
+
115
+ Items live on the platform, not in the game's code. You create them with the CLI,
116
+ and the game names them by id — which is what stops a game inventing its own
117
+ items or repricing them.
118
+
119
+ ```bash
120
+ npx genex shop add "Iron Key" --price 100 --type durable
121
+ # → id: sku_a1b2c3 ← what the game passes to buy()
122
+
123
+ npx genex shop list # what this game sells, and the valid prices
124
+ npx genex shop set sku_a1b2c3 --price 200
125
+ npx genex shop remove sku_a1b2c3 # retires it; players who bought it keep it
126
+ ```
127
+
128
+ `--type consumable` (default) is spent on use; `durable` is owned permanently.
129
+
130
+ **Prices come off a fixed grid** — `genex shop list` prints it, and an off-grid
131
+ price is refused. The grid exists so every coin pack divides evenly by the
132
+ cheapest item, which is what stops a player being left holding change too small
133
+ to spend. Pick the nearest grid price rather than working around it.
134
+
135
+ Record the ids in `DESIGN.md` next to what each item does. They are the one
136
+ thing the game's code cannot regenerate for itself.
137
+
138
+ ## The API
139
+
140
+ From `@genex-ai/embed-sdk`, already installed. `initEmbed()` must have run.
141
+
142
+ ```ts
143
+ import { getShop, buy, getEntitlements, consumeEntitlement } from '@genex-ai/embed-sdk';
144
+
145
+ const items = await getShop();
146
+ // [{ id, type, name, iconUrl, priceCoins, priceDisplayUsdCents }]
147
+ ```
148
+
149
+ Render `name`, `iconUrl`, `priceCoins` **and** `priceDisplayUsdCents`. Never
150
+ hardcode a price: the server charges what its own catalog says, so a hardcoded
151
+ number can silently disagree with what the player is charged.
152
+
153
+ ### Buying
154
+
155
+ ```ts
156
+ buyButton.addEventListener('click', async () => { // must be a real click
157
+ const result = await buy({ skuId: item.id });
158
+ if (result.status === 'canceled') return; // normal — say nothing
159
+ if (result.status !== 'succeeded') {
160
+ showMessage(result.message ?? 'That did not go through.');
161
+ return;
162
+ }
163
+ await deliverPending();
164
+ });
165
+ ```
166
+
167
+ **Call `buy()` synchronously from the click handler.** On the game's own origin
168
+ the confirmation is a popup, and browsers only allow one while a user gesture is
169
+ live — an `await` before it loses the gesture and nothing opens.
170
+
171
+ `buy()` resolves when the SERVER says what happened, not when a window closes.
172
+ Statuses: `succeeded`, `canceled`, `expired`, `insufficient_balance`, `failed`.
173
+
174
+ The player confirms on a Genex-drawn surface — your game does not render the
175
+ price sheet, cannot skin it, and cannot complete a purchase itself. That is
176
+ deliberate: it is what lets a player trust a purchase in a game they have never
177
+ played before.
178
+
179
+ ### Delivering
180
+
181
+ ```ts
182
+ async function deliverPending() {
183
+ for (const e of await getEntitlements({ excludeConsumed: true })) {
184
+ const { alreadyConsumed } = await consumeEntitlement(e.id);
185
+ if (alreadyConsumed) continue; // someone got there first
186
+ applyItem(e.skuId); // AFTER the consume
187
+ await savePlayerState(currentSave());
188
+ }
189
+ }
190
+ ```
191
+
192
+ **Consume first, apply second, and run `deliverPending()` on every boot.**
193
+
194
+ That order is not stylistic. If the game dies between consuming and applying,
195
+ the player loses one item — a support ticket. If you apply first and die before
196
+ consuming, every boot re-delivers it forever — an exploit. Re-listing on boot is
197
+ what makes a purchase survive a crash, a refresh, or a closed tab.
198
+
199
+ `consumable` items are used up. `durable` items are owned permanently — consume
200
+ them once, then record ownership in the player's save.
201
+
202
+ ## Checklist
203
+
204
+ - [ ] Items exist (`npx genex shop list`) before the shop UI is written
205
+ - [ ] The game has a loop and a progression before it has a shop
206
+ - [ ] Every item price is on the 50/100/200/500/1000 grid
207
+ - [ ] Every price renders `priceDisplayUsdCents` beside the coin figure
208
+ - [ ] `buy()` is called synchronously inside a click/tap handler
209
+ - [ ] `canceled` is silent; only real failures show a message
210
+ - [ ] `deliverPending()` runs on every boot, before the player can act
211
+ - [ ] `consumeEntitlement()` is awaited BEFORE the effect is applied
212
+ - [ ] `alreadyConsumed: true` skips the effect
213
+ - [ ] Durable purchases are written to the player's save
214
+ - [ ] No timer, "limited", "ends in", or stock counter anywhere
215
+ - [ ] Nothing sold is unreachable without paying
216
+ - [ ] No paid randomness, no coin wagering, no donation prompt
217
+
218
+ ## Troubleshooting
219
+
220
+ **`buy()` returns `failed` with "the confirmation window was blocked"** — `buy()`
221
+ was not called inside a user gesture, or an `await` ran before it. Move it to the
222
+ first line of the click handler.
223
+
224
+ **The purchase succeeded but the player got nothing** — the game applied the
225
+ effect without consuming, or never ran `deliverPending()` on boot. The
226
+ entitlement is still there; re-list it.
227
+
228
+ **The player got the item twice** — the effect was applied before consuming, or
229
+ `alreadyConsumed` was ignored. Both are the same bug.
230
+
231
+ **`unauthorized` from `getShop()`** — no player identity yet. `initEmbed()` must
232
+ have run and `waitForPlayer()` resolved. See `$genex-threejs-embed-auth`.
233
+
234
+ **`guest_no_wallet`** — guests play but hold no wallet. Show the shop as
235
+ sign-in-to-buy rather than hiding it.
236
+
237
+ **`staging_no_purchase`** — a `genex preview` build cannot spend real coin. Test
238
+ the shop's layout on staging; test a purchase after `genex promote`.
239
+
240
+ **`getShop()` returns nothing** — the game has no items yet. `npx genex shop add
241
+ "<name>" --price <coin>` and use the id it prints.
242
+
243
+ **`price_off_grid`** — that price isn't on the platform's grid. `npx genex shop
244
+ list` prints the valid ones; pick the nearest.
245
+
246
+ **Everything coin-related 404s** — in-game purchases aren't enabled on this
247
+ environment. Nothing to fix in the game; say so and build the rest.
248
+
249
+ **Purchases do nothing in local testing** — local test mode has no wallet and no
250
+ server. `buy()` returns `failed` immediately by design. Test purchases on a
251
+ preview or published build.
@@ -7,7 +7,7 @@ description: Make a Genex Three.js game phone-survivable with the vendored adapt
7
7
 
8
8
  Phones enforce a hard GPU-memory ceiling desktops don't have: iOS silently
9
9
  kills the page when a game allocates too much, and the kill arrives at BOOT —
10
- exactly when textures, models, and the post stack all decode at once. This
10
+ exactly when a skybox, models, and the post stack all decode at once. This
11
11
  skill wires the vendored quality kit so the game boots conservatively on
12
12
  phones, steps quality UP when the device proves smooth, and never gets uglier
13
13
  on desktop. **You can recover from ugly; you cannot recover from a killed
@@ -115,27 +115,22 @@ reads; pause your own loop and audio on `visibilitychange` too.
115
115
 
116
116
  ## Generated assets: load through the rungs
117
117
 
118
- A generated texture original can be several thousand pixels square a handful
119
- of them decode into more memory than a phone has. Every generated image asset
120
- ships with downscale rungs; phones must load through them:
118
+ Generated skyboxes are 8192×4096 about 178 MB decoded, over half a phone's
119
+ whole budget in one texture. Every generated image asset ships with downscale
120
+ rungs; phones must load through them:
121
121
 
122
122
  ```ts
123
123
  import { detectTier } from "./controllers/quality/tier.ts";
124
124
  import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
125
125
 
126
- const texture = await loadTextureWithFallback(TEXTURE_URL, tier, (u) =>
126
+ const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
127
127
  new THREE.TextureLoader().loadAsync(u),
128
128
  );
129
129
  ```
130
130
 
131
- Desktop loads the original, phones the small rung, and a missing rung falls
132
- back to the original — never a broken boot. The `$genex-ai-texture` skill
133
- shows the wiring in place.
134
-
135
- The sky costs nothing here: it is built in code, so there
136
- is no panorama to download, no rung ladder, and no boot-time decode. Games
137
- carrying a generated equirect sky predate that and still load through the
138
- `skybox-equirect` rungs the kit resolves for them.
131
+ Desktop loads the original, phones the `@2048` rung (~11 MB), and a missing
132
+ rung falls back to the original — never a broken boot. The `$genex-ai-skybox`
133
+ and `$genex-ai-texture` skills show the wiring in place.
139
134
 
140
135
  ## Generated models: load through the rungs
141
136
 
@@ -175,7 +170,7 @@ source, fetched only as the fallback of last resort. Meshy characters take
175
170
  the same ladder through
176
171
  `loadMeshyCharacter(manifestUrl, { loader: gltfLoader.loader, modelUrlCandidates: (u) => [pickModel(u, tier, { ktx2: gltfLoader.ktx2 }), pickModel(u, tier), u] })`.
177
172
  KTX2-capable games can also pass `ktx2Load` to `loadTextureWithFallback` so
178
- textures use their `.ktx2` variants.
173
+ skyboxes/textures use their `.ktx2` variants.
179
174
 
180
175
  ## Quality picker in settings
181
176
 
@@ -224,10 +219,9 @@ runtime).
224
219
  (antialias) are locked wrong for the session. Tier first, renderer second.
225
220
  - Governor wired to context-creation flags → no-op at best. Runtime knobs
226
221
  only: DPR, post toggles, distances, frame cap.
227
- - A generated texture loaded with a bare `TextureLoader.loadAsync(url)` on a
228
- game that targets phones → the full-size original decoded; route it through
229
- `loadTextureWithFallback`. (Same for the equirect sky of a game that predates
230
- the procedural sky lane — the `skybox-equirect` rungs still resolve.)
222
+ - Skybox loaded with a bare `TextureLoader.loadAsync(SKYBOX_URL)` on a game
223
+ that targets phones → ~178 MB decoded; route it through
224
+ `loadTextureWithFallback`.
231
225
  - Quality stepping on every spike → shader compiles read as slowness. The
232
226
  governor requires SUSTAINED slow windows; do not shorten them.
233
227
  - Testing quality tiers by resizing the desktop window → tiers key off touch +
@@ -7,7 +7,7 @@ when, or teaching the governor a game-specific step.
7
7
 
8
8
  The phone budget is a hard ceiling that includes GPU memory (textures,
9
9
  framebuffers), and the OS kill arrives with no catchable event. Boot is the
10
- danger window: big textures + models + post targets decode together. So phone tiers
10
+ danger window: skybox + models + post targets decode together. So phone tiers
11
11
  START one notch below what the heuristics suggest and the governor steps UP
12
12
  after ~20 smooth seconds. The cost of guessing low is moments of softness; the
13
13
  cost of guessing high is a dead page.
@@ -67,7 +67,7 @@ type.
67
67
 
68
68
  **3. Never stall the build on art.** Scaffold, boot wiring, the core loop,
69
69
  and the gameplay-logic modules keep moving in parallel whatever the UI is
70
- doing — and the core asset set (hero model, ground texture, sky, sfx) is
70
+ doing — and the core asset set (hero model, ground texture, skybox, sfx) is
71
71
  prompted from the game IDEA, so it can launch up front behind placeholders.
72
72
  Nothing ready to build waits on an art answer: decide, say the decision in
73
73
  one line, keep going.
@@ -134,9 +134,13 @@ everything twice.
134
134
  visible; the lock may only ever engage from the Play/Resume click or a
135
135
  gameplay canvas click (the phase binding `setPaused(phase !== "playing")` is
136
136
  what guarantees this — check it rides `setPhase`, not the render loop).
137
- Headless caveat: `requestPointerLock` throws in headless Chromium —
138
- assert the wiring and the unlocked cue in a screenshot, and say plainly that
139
- the lock itself needs one manual click (do the both-axes look check there).
137
+ Headless caveat, measured on Chromium 151: `requestPointerLock` does NOT
138
+ throw it locks, with or without a user gesture, so the lock and the
139
+ unlocked cue ARE yours to assert headless. What does not survive is the
140
+ both-axes look check: synthesised mouse movement cancels to a net zero
141
+ delta, so turning right then left proves nothing about direction. Assert the
142
+ wiring and the cue in a screenshot, and say plainly that confirming which way
143
+ the view turns needs one manual pass with a real mouse.
140
144
  7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
141
145
  once, in the same browser you already have open.
142
146
 
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```