# Feature matrix (/docs/feature-matrix) This is the answer to *"does this work on Unreal?"* The [Platform API](/docs/platform-api) is identical across runtimes by design β€” the differences are confined to rendering, input, and networking. We track them here rather than scattering "N/A on this runtime" notes through the reference. The Web SDK is the contract. A βœ… in the Web column means the method ships in `@hypersoniclabs/helix-sdk`. The Unreal column is read from the plugin's generated method table (`HelixGenMethodTable.cpp` in `helix-unreal-runtime`), which marks each method Available, Conditional, CorrectionRequired or Unavailable. Anything short of Available is shown as 🟑 with the reason. APIs that are specified but not shipped on any runtime are marked Planned. ## Platform API [#platform-api] | Capability | Web | Unreal | | -------------------------------------------------------------------------- | :-----: | :--------------------------------------------------------------------------------------: | | Authentication & Identity (`Helix.auth`) | βœ… | βœ… | | Wallet balance (`Helix.wallet.getBalance`) | βœ… | 🟑 not yet: must read the authoritative spendable balance first | | Purchases and entitlements (`Helix.marketplace`, `Helix.purchases`) | βœ… | βœ… | | Data Store (`Helix.dataStore`) | βœ… | βœ… client and listen host (not from a managed server) | | Leaderboards and achievements (`Helix.leaderboard`, `Helix.achievements`) | βœ… | βœ… | | Memory Store (`Helix.memoryStore`) | Planned | Planned | | Inventory reads (`Helix.inventory.hasItem` / `getQuantity` / `getMyItems`) | βœ… | βœ… | | Inventory equip (`Helix.inventory.equipItem`) | βœ… | 🟑 unavailable: needs an instance-equip contract | | Social graph (`Helix.social`) | Planned | Planned | | Rich presence (`Helix.presence`) | βœ… | 🟑 not yet: needs runtime-scoped presence authority | | Chat (`Helix.chat`) | Planned | Planned | | Voice (`Helix.voice`) | βœ… | 🟑 controls are available; `join` is not yet (shell-owned voice contract) | | Avatars (`Helix.avatar`) | βœ… | 🟑 equipped avatars are available; `getLoadout` / `updateLoadout` only through the shell | | Analytics (`Helix.analytics.track`) | βœ… | 🟑 not yet: the platform field is hard-coded to web | ## Runtime-specific [#runtime-specific] | Capability | Web | Unreal | | ------------------------------------------------------ | :-----------------------: | :-------------: | | Instant play from a link (no install) | βœ… | β€” | | HELIX multiplayer & networking (`Helix.multiplayer`) | βœ… | β€” | | Engine-native networking (listen host, managed server) | β€” | βœ… | | Rendering / scene graph | Web stack (Three.js etc.) | Unreal | | Languages | TypeScript / JS | C++ Β· Blueprint | Legend: βœ… available Β· 🟑 partial or not yet available Β· β€” not applicable to this runtime. No CI check compares the two runtimes yet. This table was checked by hand on 2026-10-20 against `@hypersoniclabs/helix-sdk` 0.1.11-helix3.145 and `helix-unreal-runtime` main (2026-09-19). The Unreal plugin has no Lua binding. {/* docs-check: planned-api β€” rows marked Planned name unshipped namespaces */} # HELIX Documentation (/docs) HELIX is an **engine-agnostic virtual-world platform**. You build a world once against a single SDK and HELIX gives you identity, the LIX economy, storage, inventory, social, voice, and multiplayer β€” then runs it on any device from a single link. These docs are organized around one idea: The **Platform API** is engine-agnostic and documented **once**. The **Web SDK** (TypeScript) is the canonical contract; the **Unreal SDK** mirrors it 1:1 in C++, Blueprint, and Lua. The same function names mean the same thing everywhere. ## Start here [#start-here] ## The map [#the-map] ## Built for humans and agents [#built-for-humans-and-agents] Every page in these docs is available as clean Markdown for LLMs β€” append `.md`-style content routes, or pull the whole corpus: * [`/llms.txt`](/llms.txt) β€” a structured index of every page. * [`/llms-full.txt`](/llms-full.txt) β€” the entire documentation as one Markdown file. * Use the **Copy Markdown** button at the top of any page to grab that page for an AI agent. We expect most worlds to be built by AI coding agents calling this SDK. The docs are written to be read by both. # Connect Claude Desktop (/docs/cli/connect-claude-desktop) Connect HELIX once and Claude Desktop can use the same discovery, validation, and publishing capabilities as the [HELIX CLI](/docs/cli), exposed as typed [MCP tools](/docs/cli/mcp). ## Before you start [#before-you-start] * Install [Claude Desktop](https://support.claude.com/en/articles/10065433-install-claude-desktop). * Install Node.js 20 or newer; `node -v` should print `v20` or higher. * Create a HELIX account. The npm package below is a **local** MCP server. It is available to Claude Desktop conversations, but a local server configured in `claude_desktop_config.json` is not available inside Cowork or claude.ai. Cowork requires a publicly reachable remote MCP connector. HELIX does not currently publish a remote connector URL, so do not paste a localhost URL into Cowork. ## Add HELIX [#add-helix] In Claude Desktop, open **Settings β†’ Developer β†’ Edit Config**. Add the `helix` entry to the top-level `mcpServers` object and leave any existing servers intact: ```json { "mcpServers": { "helix": { "command": "npx", "args": ["-y", "@hypersoniclabs/helix-mcp@latest"] } } } ``` On Windows, use `cmd` if Claude cannot launch `npx` directly: ```json { "mcpServers": { "helix": { "command": "cmd", "args": ["/c", "npx", "-y", "@hypersoniclabs/helix-mcp@latest"] } } } ``` Save the file, fully quit Claude Desktop, and reopen it. Open the connector picker beside the chat box and confirm HELIX tools appear. ## Sign in [#sign-in] Ask Claude to check your HELIX login. If needed, it calls `start_login` and gives you a browser link. Complete sign-in yourself, then Claude calls `check_login`. The token is written locally to `~/.helix/credentials.json`; it is not returned to Claude. For an SSH-only or headless machine, use [token login](/docs/cli#account) before starting the MCP server. Do not wait for a browser callback that cannot reach the remote host. ## Verify the connection [#verify-the-connection] Ask Claude: > Use HELIX to list the available world templates, then tell me which one you recommend. Do not > publish anything. A result from `list_templates` confirms the server loaded and can reach HELIX. If the tool is missing, check that the config is valid JSON, that `node -v` reports 20 or newer, and that you fully restarted the desktop app. ## Claude in Chrome [#claude-in-chrome] [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is a separate browser-control connector. Install the Chrome extension, then enable it from Claude Desktop under **Settings β†’ Connectors β†’ Claude in Chrome**. It lets Claude test a world in the browser, but it does not replace the HELIX MCP server: enable both in the same desktop conversation to build with HELIX tools and verify the result in Chrome. The Chrome side-panel chat does not load local `claude_desktop_config.json` servers. Start the task from Claude Desktop when it needs both HELIX and browser control. # HELIX CLI (/docs/cli) `@hypersoniclabs/helix-cli` is the front door for building on HELIX. It scaffolds world projects, resolves the systems a world pins, validates a bundle against the exact rules the server enforces, and publishes worlds (play-from-a-link browser games), Packages and Items. ```bash npm install -g @hypersoniclabs/helix-cli # or, always current: npx -y @hypersoniclabs/helix-cli@latest ``` Install the scoped package, because a bare `npx helix` resolves to an unrelated package on npm. A global install goes stale. `npx -y @hypersoniclabs/helix-cli@latest` is always current; leave off `@latest` and npx reuses whatever copy is already installed globally. `helix doctor` tells you when anything in the toolchain is behind. ## The complete command reference is generated [#the-complete-command-reference-is-generated] That reference is produced by loading the published CLI and walking the command tree it registers, so it cannot list a command that does not exist or miss one that does. The CLI's own help is the authority for the build you have installed: ```bash helix --help helix --help ``` ## The main workflows [#the-main-workflows] | I want to | Start here | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Build and publish a world | [Quickstart](/docs/introduction/quickstart): `helix init`, `helix install`, `helix validate`, `helix publish` | | Publish a 3D item from a GLB | [Publish an item from a GLB](/docs/cli/publish-a-wearable): `helix continuum publish-glb`, `helix continuum make-item` | | Sell something in a world | [Sell something for LIX](/docs/guides/sell-item-for-lix): `helix product register` | | Author a device or a vehicle | [`helix device`](/docs/cli/reference/device), [`helix vehicle`](/docs/cli/reference/vehicle) | | Import from another engine | [`helix bridge`](/docs/cli/reference/bridge) | | Check my setup | `helix doctor`, `helix whoami` | ## Account [#account] ```bash helix login # opens the browser and stores a client token helix login --no-browser # print the sign-in URL instead helix login --token # non-interactive, for CI helix whoami helix logout ``` The default login finishes through a callback on the machine that runs the CLI, so it cannot complete from an SSH-only host, a cloud runner or a container. Create a client token in [Account β†’ API keys](https://helixgame.com/account/api-keys), then sign that machine in explicitly: ```bash helix login --token "$HELIX_CREATOR_TOKEN" ``` Keep the token in your secret manager, not in source control or shell history. The CLI validates it before writing `~/.helix/credentials.json`. The MCP server reads the same file, so run this once before starting MCP on a headless host. ## Source code is not uploaded by default [#source-code-is-not-uploaded-by-default] Every `helix publish` creates an immutable, playable build. It uploads your project **source** only when you ask for it: ```bash helix publish dist --upload-source --source-dir . ``` Without `--upload-source`, HELIX keeps the built bundle that runs the world, not the editable project that produced it, and the CLI prints that outcome after every publish. Keep the project in version control and back it up: a published, optimised build cannot be turned back into your source. ## For AI agents [#for-ai-agents] Agents drive the same code paths through the [MCP server](/docs/cli/mcp), which registers one typed tool per workflow. Its [tool reference](/docs/cli/mcp-tools) is generated from the running server the same way. # MCP server for AI agents (/docs/cli/mcp) The **MCP server** (`@hypersoniclabs/helix-mcp`) gives an agent a structured, typed interface to the platform instead of scraping CLI output. It is a thin layer over the [CLI](/docs/cli): tool schemas route arguments into the same code, so a tool call and a terminal command behave the same. The server registers more than a hundred tools. The [MCP tool reference](/docs/cli/mcp-tools) is generated by starting the published server and listing its tools, so names, descriptions and argument schemas match exactly what an agent sees. ## Connect it [#connect-it] For a complete Claude Desktop walkthrough, including Windows setup and troubleshooting, see [Connect Claude Desktop](/docs/cli/connect-claude-desktop). Add the server to your agent host's MCP configuration (Claude Desktop, Cursor, Codex and others): ```json { "mcpServers": { "helix": { "command": "npx", "args": ["-y", "@hypersoniclabs/helix-mcp@latest"] } } } ``` Discovery tools work signed out. Signing in stays human: the agent calls `start_login`, the human finishes the browser sign-in, and the agent calls `check_login`. The token is stored locally and never enters the conversation. `start_login` uses a callback on the machine that runs MCP. On CI, an SSH-only server or a container, create a client token in [Account β†’ API keys](https://helixgame.com/account/api-keys), run `helix login --token "$HELIX_CREATOR_TOKEN"` on that host, then start MCP. The CLI and the MCP server share `~/.helix/credentials.json`. `publish_world` uploads the playable build by default. Set `uploadSource: true` only when the human also wants HELIX to keep the editable project for the website editor. Otherwise the project must live in version control: an optimised published bundle is not a source backup. ## Start with `get_started` [#start-with-get_started] An agent should begin with `get_started`. It returns the project recipe for what is being built (`kind` is `character`, `multiplayer` or `scene` for worlds, or one of the item and asset kinds) and points to the right docs and skills. From there: * `read_doc` and `read_template` serve the bundled platform documentation and the worked multiplayer templates. * `read_skill` serves bundled skills for avatars, vehicles, assets, modelling, multiplayer and QA. * `validate_world`, `inspect_world` and `capture_world_screenshot` check a built world before `publish_world`. * `publish_continuum_package` and `publish_owned_item` publish Packages and move existing Items onto them. The server carries its own versioned documentation, so for agents it is the best source for the exact version they run. This site exists for people, for search, and for the concepts that do not belong inside a tool description. ## For language models reading this site [#for-language-models-reading-this-site] * [`/llms.txt`](/llms.txt) is a structured index of every page. * [`/llms-full.txt`](/llms-full.txt) is the entire corpus in one file. * Every page is available as clean Markdown (the **Copy Markdown** button, and the content routes). # Publish an item from a GLB (/docs/cli/publish-a-wearable) Authored content enters HELIX as a **Package**: an immutable, content-addressed bundle. A Package Version is sealed once and never changes. An **Item** is an optional product made from an exact Package Version, and a **Marketplace offer** is an explicit, separate step on top. This replaces the old loose-mesh upload (`helix item publish`), which no longer exists: an Item is no longer created {/* docs-check: ignore */} from a bare file. The three steps below were run end to end against the live platform with `@hypersoniclabs/helix-cli` 0.1.27. This is the path for **props**: a GLB that is decor, furniture or any static or animated object. Wearables, avatars and other character add-ons use the character tools and carry an [early access](/docs/cli/mcp-tools/avatars) label. See [Wearables and character add-ons](#wearables-and-character-add-ons) below. ## 1. Seal a Package [#1-seal-a-package] The commands use a sample model, a small wall-mounted whiteboard. Download it, or substitute any `.glb` of your own: ```bash curl -fLO https://docs.helixgame.com/samples/whiteboard.glb ``` `whiteboard.glb` is the Wall-Mounted Whiteboard from the Hyperspace HQ world, about 27 KB. It publishes as a private prop with no warnings, which makes it a safe thing to practise on. ```bash helix continuum publish-glb whiteboard.glb --title "Whiteboard" --private ``` ```text βœ” Sealed Package e679e0c7-2b28-4ef1-a3e8-494eea31f624@1.0.0 intent: 83d67468-286e-4c69-a91a-9835b8c774a9 ``` `--private` seals the Package on its own, with no public offer. `--package-type character` seals a character instead of a prop. Every run is identified by an **intent id**. If the connection drops, run the same command with `--intent-id ` and the CLI resumes instead of publishing twice. Add `--json` to get the full receipt, including the exact manifest CID you need next: ```bash helix continuum publish-glb whiteboard.glb --title "Whiteboard" --private \ --intent-id 83d67468-286e-4c69-a91a-9835b8c774a9 --json ``` ```json { "intentId": "83d67468-286e-4c69-a91a-9835b8c774a9", "package": { "packageId": "e679e0c7-2b28-4ef1-a3e8-494eea31f624", "version": "1.0.0", "manifestCid": "cid:sha256:0fcbc36ae35f868833a776428ca5e36d65488dc23ddc5ace66e0ddffb24cca8a", "type": "asset", "visibility": "private", "created": false }, "item": null, "vaultListing": { "state": "skipped", "reason": "requested_none" }, "verification": { "ok": true, "warnings": [] } } ``` `created: false` shows the resumed run found the existing Package. `verification.warnings` carries measurements to check against your intent; they are advice, not failures. ## 2. Make it an Item [#2-make-it-an-item] ```bash helix continuum make-item \ --package-id e679e0c7-2b28-4ef1-a3e8-494eea31f624 \ --package-version 1.0.0 \ --manifest-cid cid:sha256:0fcbc36ae35f868833a776428ca5e36d65488dc23ddc5ace66e0ddffb24cca8a \ --content-rating everyone \ --title "Whiteboard" \ --personal ``` ```text βœ” Package-backed Item 19041ba6-f8f0-4f67-8e55-41b5df03a873 package: e679e0c7-2b28-4ef1-a3e8-494eea31f624@1.0.0 ``` `--personal` keeps the Item private and without an offer. `--content-rating` is your attestation: `everyone`, `teen`, `mature` or `adult`. ## 3. Open an offer (optional) [#3-open-an-offer-optional] Offers are explicit. Pass `--offer` to `make-item`, or to `publish-glb` to do steps 1 to 3 in one command: ```bash helix continuum publish-glb whiteboard.glb --title "Whiteboard" \ --content-rating everyone --offer marketplace --price-lix 1200 ``` * `--price-lix 0` opens a free **Claim**. * `--offer world --world-id --distribution-key ` opens a route inside one World. * `--starts-at` and `--ends-at` schedule the offer, and `--collectible-supply ` makes a fixed-supply collectible instead of a Standard item. * Publishing to the Marketplace can charge a publish fee in [LIX](/docs/platform-api/lix-economy). The server states the exact fee and the minimum price in its answer. ## Look after it afterwards [#look-after-it-afterwards] | Task | Command | | ---------------------------------------------------- | -------------------------------------------------------- | | Ship a corrected mesh to the same Item, in place | [`helix item publish-version`](/docs/cli/reference/item) | | Move an Item to another sealed version, or roll back | [`helix item set-version`](/docs/cli/reference/item) | | Render and check the Item headless | [`helix item visual-qa`](/docs/cli/reference/item) | | Stop selling it (owners keep their copies) | [`helix item delist`](/docs/cli/reference/item) | | Download the source of a Package you own | [`helix continuum pull`](/docs/cli/reference/continuum) | Catalog thumbnails are rendered by HELIX from the Item's active Package Version and cannot be replaced. To show your own pictures, add them to the Item page. ## Wearables and character add-ons [#wearables-and-character-add-ons] A wearable, hat, hairstyle or other character add-on is a Package of the character kind that declares where it sits on the body and which body it fits. Preparing one uses the character tools (`helix character fit`, `helix character derive-hide`, `helix character fix-rig`, `helix character check`; see the [reference](/docs/cli/reference/character)) and the same Package publication pipeline. This area is in **early access** and changes quickly, so this site does not walk through it step by step. The authoritative, versioned guide is served by the [MCP server](/docs/cli/mcp): call `get_started` with a character or wearable kind, and `read_skill` for `helix-avatars`. ## From an AI agent [#from-an-ai-agent] The MCP server exposes `publish_continuum_package` for sealing Packages and `publish_owned_item` for moving an existing creator-owned Item onto an exact sealed Package Version. It deliberately has no tool that creates an Item from a loose mesh. See the [MCP tool reference](/docs/cli/mcp-tools/continuum). # Build your first multiplayer world (/docs/guides/first-multiplayer-world) This extends the project from the [Quickstart](/docs/introduction/quickstart). You will add a "Claim bonus" button whose result the **server** decides, which is the [server authority](/docs/introduction/how-helix-works#server-authority--the-golden-rule) rule in its smallest form. Every snippet here was added to a real `helix init --kind multiplayer` project, built, type-checked and validated with the CLI. HELIX multiplayer is **server-authoritative and declarative**. A platform-owned room holds shared state and runs the rules you declare in `public/helix.json` at a fixed 20 Hz. You write no server code. Clients read synced state and send *intents* (actions); the room validates them and applies the effects. ## 1. What the scaffold already does [#1-what-the-scaffold-already-does] `helix init my-world --kind multiplayer` gives you presence out of the box. One call, `CharacterMultiplayer.create`, signs the player in, loads their equipped avatar, joins the room (falling back to single player for guests) and renders everyone else with a nameplate and proximity voice: ```ts title="src/main.ts" const character = await CharacterMultiplayer.create({ helix: Helix, renderer, scene, camera, body, input, domElement: renderer.domElement, assetBase: SYSTEM_ASSET_BASE, transcoderPath: TRANSCODER_PATH, spawn: SPAWN, join: true, }); ``` `character.room` is the live room handle. It is `null` when the player is not in a room (guest, standalone, or join failed), so single player always works. Its full type is `HelixRoom` in the [Web SDK reference](/docs/web-sdk/api). ## 2. Declare state and a rule [#2-declare-state-and-a-rule] Add per-player state, an action the client may send, and one rule that handles it. Everything goes under the existing `multiplayer` block of `public/helix.json`: ```json title="public/helix.json" "multiplayer": { "authoritative": true, "state": { "playerVars": { "coins": { "type": "number", "default": 0, "min": 0, "integer": true }, "bonusClaimed": { "type": "boolean", "default": false } } }, "actions": { "claimBonus": { "args": {} } }, "rules": [ { "when": { "on": "action", "name": "claimBonus" }, "if": { "op": "==", "a": { "var": "self.bonusClaimed" }, "b": false }, "then": [ { "do": "add", "target": "self.coins", "by": 50 }, { "do": "set", "target": "self.bonusClaimed", "to": true } ] } ] } ``` When a client sends `claimBonus`, the room fires the rule with `self` bound to that player. The condition runs on the server, so a player who edits their client cannot claim twice. If you misspell a verb or a variable, `helix validate` rejects it and suggests the right name. The complete grammar (entities, zones, timers, state machines, collections) is in the multiplayer logic reference that the [MCP server](/docs/cli/mcp) serves as `read_doc({ name: "multiplayer-logic" })`. ## 3. Ask from the client [#3-ask-from-the-client] The client never changes `coins`. It asks, then reads the synced value: ```ts title="src/main.ts" const bonusButton = document.createElement('button'); bonusButton.style.cssText = 'position:fixed;left:16px;bottom:16px;z-index:10'; bonusButton.onclick = () => character.room?.sendAction('claimBonus', {}); document.body.append(bonusButton); // inside your render loop, read state fresh each frame bonusButton.textContent = `Claim bonus (${character.room?.me.num('coins') ?? 0} coins)`; ``` `room.me`, `room.vars` and `room.player(id)` are guarded live views over the synced state with safe defaults, so there is nothing to cast and no "first patch has not landed yet" check. If the client could grant its own bonus, players would script infinite coins. Movement and cosmetics may be client-driven; anything of value is decided by a declared rule. This is the same rule as for [inventory and LIX](/docs/platform-api/inventory#execution-tiers). ## 4. Check and publish [#4-check-and-publish] ```bash npm run build helix validate dist helix publish dist ``` Open the play link in two browser windows. You will see both players, and each can claim the bonus once. ## Next [#next] # Guides (/docs/guides) Concept pages explain *what* something is; guides show *how* to build a specific thing end to end. Each one names the [Platform API](/docs/platform-api) surfaces it uses and, where it matters, shows the same task on both runtimes. This section grows with the platform. Proximity voice, avatars, and matchmaking guides are next. If you're an AI agent, the [`/llms-full.txt`](/llms-full.txt) bundle includes every guide in one file. # Persist player data (/docs/guides/persist-player-data) HELIX gives a world two durable places to keep things. Pick by asking **who is allowed to write it**. | Data | Use | Written by | | ------------------------------------------------- | ----------------------------------------------------- | -------------------- | | Progress the rules own (level, coins, unlocks) | **Persistent player variables** and the `save` effect | The room, from rules | | The player's own blob (settings, a draft, a note) | [`Helix.dataStore`](/docs/platform-api/data-store) | The client | | World-wide totals and rankings | Counters and leaderboards | The room, from rules | ## Progress the server owns [#progress-the-server-owns] Mark a scalar player variable `"persistent": true` and write it with the `save` effect at the moment that matters. Extend the example from [Build your first multiplayer world](/docs/guides/first-multiplayer-world): ```json title="public/helix.json" "multiplayer": { "authoritative": true, "state": { "playerVars": { "coins": { "type": "number", "default": 0, "min": 0, "integer": true, "persistent": true }, "bonusClaimed": { "type": "boolean", "default": false, "persistent": true } } }, "actions": { "claimBonus": { "args": {} } }, "rules": [ { "when": { "on": "action", "name": "claimBonus" }, "if": { "op": "==", "a": { "var": "self.bonusClaimed" }, "b": false }, "then": [ { "do": "add", "target": "self.coins", "by": 50 }, { "do": "set", "target": "self.bonusClaimed", "to": true }, { "do": "save", "player": "self" } ] } ] } ``` * Saved values are loaded before join rules run, so a `playerJoin` rule already sees the restored values. * Fire `save` on the event that changed something worth keeping (a level up, a checkpoint, a purchase), never on a tick. Saves are debounced and the room also flushes on leave and disconnect. * Only scalar variables (`number`, `string`, `boolean`, `vec3`) can persist, at most 16 per world and 8 KiB per player. * A stored value is re-validated against your **current** manifest on every load. Renaming a variable resets it, so treat persistent names as a schema. ## The player's own data [#the-players-own-data] `Helix.dataStore` is a durable key-value store scoped to **your world**. It is the right place for settings and anything the player authored. Key it by player yourself (for example `player:`). Reads return `null` for an unset key. Writes can carry the version you last read, so two tabs cannot silently overwrite each other, and `0` asserts the key does not exist yet: ```ts type Settings = { musicVolume: number }; const key = `player:${user.id}`; // user comes from Helix.init() or Helix.auth.getUser() const entry = await Helix.dataStore.getEntry(key); // { value, version } | null const next = { ...(entry?.value ?? { musicVolume: 0.8 }), musicVolume: 0.5 }; await Helix.dataStore.set(key, next, { expectedVersion: entry?.version ?? 0 }); const { keys, nextCursor } = await Helix.dataStore.list('player:'); // keys only, never values ``` When a world is opened outside the HELIX shell (a plain `vite preview`), `dataStore.set()` does nothing and says so in the console. A clean preview run is never evidence that a save works; run `helix dev` to exercise it. Treat anything the client writes as untrusted. Coins, items and scores that other players or the economy depend on belong in rules (above) or in the [economy](/docs/guides/sell-item-for-lix). ## Next [#next] # Sell something for LIX (/docs/guides/sell-item-for-lix) A world sells things through **World Products**: world-local currencies, passes and consumables that you register once and the player buys with [LIX](/docs/platform-api/lix-economy). The rule that shapes everything: **your world names a product and reads entitlements back. The shell raises the confirmation, charges the wallet and settles the sale.** A world cannot set a price or write an entitlement. A World Product never mints a universal item. To sell something the player keeps across worlds, publish it as an item and open an offer for it with [`helix continuum publish-glb`](/docs/cli/publish-a-wearable), then buy it from world code with `Helix.marketplace.purchaseDistributionKey`, `purchaseDistribution` or `purchaseListing`. ## 1. Register the product [#1-register-the-product] Register before you write the code that buys it. Your world must already exist and be published (`helix publish`). The `key` is permanent, and so is the `grants` list, so choose both once. ```bash helix product register my-world \ --key coin-pack \ --title "Coin Pack" \ --price 25 \ --grants '[{"kind":"currency","code":"coin","amount":500,"display":"wallet"}]' \ --dry-run ``` `--dry-run` validates everything locally, works while logged out, and prints exactly what would be sent: ```text βœ” Dry run β€” nothing sent. POST /api/v1/iwp/products (worldId of "my-world") body: { "key": "coin-pack", "title": "Coin Pack", "priceLix": 25, "grants": [{ "kind": "currency", "code": "coin", "amount": 500, "display": "wallet" }] } ``` Drop `--dry-run` to register for real. The product is live as soon as the command returns. A `--price` of `0` registers a free claim. An empty `--grants '[]'` makes a tip jar. The grant shapes are `currency` and `pass` (optionally with a duration), up to 8 per product. ## 2. Ask to buy [#2-ask-to-buy] ```ts const result = await Helix.marketplace.purchaseProduct('coin-pack'); if (result.completed) refresh(); else showToast(result.reason ?? result.status); ``` `purchaseProduct` shows the shell's confirmation, charges the wallet and settles. Branch on `result.completed`, not on a single status, so a free claim and an `AlreadyOwned` answer are not mistaken for failures. A `Pending` status is not final: re-read it with `Helix.marketplace.getPurchase(idempotencyKey)`. ## 3. Read what the player owns [#3-read-what-the-player-owns] ```ts const e = await Helix.purchases.getEntitlements(); const coins = e.balances['coin'] ?? 0; const hasPass = e.passes['vip']?.active === true; // spend from a server-held balance const spent = await Helix.purchases.consume('coin', 100); if (!spent.applied) showToast('Not enough coins'); ``` Entitlements live on the server and survive across sessions. For gating inside a multiplayer room, the declarative rules can react to a settled purchase directly, so the room never trusts the client's word about what it owns. ## Try it without spending anything [#try-it-without-spending-anything] `helix dev` serves your built world under a simulated shell with no login and no backend. Purchases, saves and entitlements are answered locally, and a debug menu forces them to fail so you can test the unhappy paths. ## Related [#related] # Choose your runtime (/docs/introduction/choose-your-runtime) You write your world logic against the engine-agnostic [Platform API](/docs/platform-api) either way. The runtime decides how it renders, how it networks, and which languages you write in. ## Quick guide [#quick-guide] ## Side by side [#side-by-side] | | Web | Unreal | | ---------------- | -------------------------------------------------------- | --------------------------------------- | | **Languages** | TypeScript / JavaScript | C++, Blueprint, Lua | | **Distribution** | Instant, from a link, any device | Engine-native build | | **Multiplayer** | HELIX networking ([web-only](/docs/web-sdk/multiplayer)) | Engine-native dedicated servers | | **Platform API** | βœ… identical | βœ… identical | | **Status** | Canonical contract, available first | Mirrors the web contract 1:1 | | **Best for** | Reach, speed, AI-built worlds | Fidelity, large maps, existing UE teams | You are **not** locked in. Because both runtimes call the same Platform API, world logic written for web ports to Unreal with minimal change β€” the differences are rendering, input, and networking, not identity/economy/storage. ## Still deciding? [#still-deciding] Start on **Web**. It's the canonical surface, it's the fastest path to a playable link, and everything you learn transfers. Move to Unreal when you need engine-level fidelity or you already have an Unreal team. Continue to the [Quickstart](/docs/introduction/quickstart). # How HELIX works (/docs/introduction/how-helix-works) HELIX has a small, deliberate vocabulary. Learn these five nouns and the rest of the docs read cleanly. **World Β· Build Β· Instance Β· Player Β· Event.** These are the only words the SDK and docs use for the runtime. Provider-specific terms (rooms, matches, servers) never appear in the public API β€” they're implementation details HELIX hides behind the SDK. ## The nouns [#the-nouns] ## The lifecycle [#the-lifecycle] What happens when someone opens your world from a link: **Launch.** A player opens your World. HELIX authenticates them (or spins up an instant guest) and issues a short-lived **Launch Ticket**. **Init.** Your client boots and calls `Helix.init()`. The SDK completes the handshake with the shell, validates the session and exposes the player's identity, wallet, and inventory. **Join an Instance.** The player joins a running Instance of the current Build (or HELIX creates one). The platform issues an **Instance Ticket** that the runtime verifies before admitting them. **Play.** Clients send intents and the room applies your declared rules ([Multiplayer](/docs/web-sdk/multiplayer)). The server is authoritative for anything that touches wallet, inventory, or ownership; the client is trusted only for visuals and local gameplay. **Persist.** State that must survive the session is written through [Data Store](/docs/platform-api/data-store). Transient cross-instance state (leaderboards, matchmaking) uses [Memory Store](/docs/platform-api/memory-store). ## Server authority β€” the golden rule [#server-authority--the-golden-rule] If an action affects **inventory, currency, or ownership**, the **server is authoritative**. If it only affects **visuals or local gameplay**, the client can be trusted. This single rule decides where every piece of your logic belongs β€” see the [Universal Item Contract](/docs/platform-api/inventory). ## Where this lives in the SDK [#where-this-lives-in-the-sdk] You orchestrate all of this from the [Web SDK](/docs/web-sdk) in the client, and by declaring game logic in `public/helix.json` for the server-authoritative room. The platform pieces β€” auth, wallet, storage, inventory β€” are the [Platform API](/docs/platform-api), identical across runtimes. # What is HELIX? (/docs/introduction) HELIX is a **virtual-world platform**. It is not a game engine and it is not a single game β€” it is the layer that sits **on top of** a rendering engine and provides everything a multiplayer virtual world needs but a renderer doesn't: * **Identity** β€” one account across every world, with instant guest play. * **Economy** β€” a real currency (**LIX**), a marketplace, and creator payouts. * **Persistence** β€” durable Data Store and volatile Memory Store. * **Items & inventory** β€” universal items that work across worlds and runtimes. * **Social** β€” friends, presence, invites, chat, and proximity voice. * **Multiplayer** β€” instances, networking, and server-authoritative state. * **Hosting & distribution** β€” your world is playable instantly, on any device, from a link. You reach all of it through **one SDK**. ## One platform, two runtimes [#one-platform-two-runtimes] This is the core idea, and it shapes the whole of these docs.
Platform (engine-agnostic)

Identity, LIX, storage, inventory, marketplace, social, analytics, moderation. The same backend serves every runtime. Documented once, in the Platform API.

Runtime (engine-specific)

Where your world actually renders and runs. Web (TypeScript, the canonical contract) and Unreal (C++, Blueprint, Lua) both call the same Platform API.

The **Web SDK is the canonical, frozen contract.** The Unreal runtime mirrors it 1:1 β€” every function, including character and world functions. If you learn the API on one runtime, you know it on the other. See [Platform vs Engine SDK](/docs/introduction/platform-vs-engine) for the full mental model. ## Who HELIX is for [#who-helix-is-for] * **Creators** shipping multiplayer web-3D experiences who don't want to build identity, economy, or netcode from scratch. * **Studios and teams** that want their world on web today and on Unreal later β€” without rewriting game logic against a different API. ## Next [#next] # Platform vs Engine SDK (/docs/introduction/platform-vs-engine) Everything in the HELIX SDK falls into one of two buckets. Knowing which bucket you're in tells you where to find the docs, whether the code is portable, and what to expect on another runtime. ## Platform API β€” engine-agnostic [#platform-api--engine-agnostic] The **Platform API** is the shared layer. It has nothing to do with rendering. It's the same behavior, the same guarantees, and the same function names on every runtime: | Capability | Namespace | Docs | | --------------------------- | ----------------------------------- | --------------------------------------------------- | | Authentication & identity | `Helix.auth` | [Authentication](/docs/platform-api/authentication) | | LIX & economy | `Helix.wallet`, `Helix.marketplace` | [LIX & economy](/docs/platform-api/lix-economy) | | Durable storage | `Helix.dataStore` | [Data Store](/docs/platform-api/data-store) | | Volatile storage (planned) | `Helix.memoryStore` | [Memory Store](/docs/platform-api/memory-store) | | Items & inventory | `Helix.inventory` | [Inventory](/docs/platform-api/inventory) | | Social & presence (planned) | `Helix.social` | [Social](/docs/platform-api/social) | Write against the Platform API and your logic is **portable**: it behaves identically whether your world runs on the web or inside Unreal. ## Engine-specific runtime [#engine-specific-runtime] A **runtime** is where your world actually renders and executes. Runtimes add the things only an engine can provide β€” scene graph, input, physics, rendering β€” plus engine-flavored conveniences. * **[Web SDK](/docs/web-sdk)** β€” TypeScript. This is the **canonical contract**. It also ships a full **multiplayer & networking** API (HELIX's own implementation) that is web-specific. * **[Unreal SDK](/docs/unreal-sdk)** β€” the same Platform API exposed to **C++**, **Blueprint**, and **Lua**, plus engine-specific helpers like replication wrappers. Unreal worlds use the engine's own networking rather than the web stack. The Web TypeScript SDK is frozen first and treated as the canonical API surface. The Unreal runtime **mirrors it 1:1** β€” every function, including character and world functions. When the two ever differ, the web signature wins and the difference is documented in the [feature matrix](/docs/feature-matrix). ## The same call, five ways [#the-same-call-five-ways] Because the Platform API is identical across runtimes, the *same* operation looks like idiomatic code in each language. Here's reading the current player's LIX balance: ```ts const wallet = await Helix.wallet.getBalance(); console.log(`You have ${wallet.lix} LIX`); ``` ```cpp UHelixWallet* Wallet = UHelix::Get()->Wallet(); Wallet->GetBalance(FOnBalance::CreateLambda([](const FHelixBalance& B) { UE_LOG(LogHelix, Display, TEXT("You have %lld LIX"), B.Lix); })); ``` ```text Helix β†’ Wallet β†’ Get Balance (latent node) ↳ On Success β†’ Break Helix Balance β†’ "Lix" β†’ Print String ``` ```lua local wallet = Helix.wallet.getBalance() UE.Log(string.format("You have %d LIX", wallet.lix)) ``` This `groupId` is shared across the docs: pick **Web (TS)** once and every runtime-tabbed example on every page follows you. ## How to use this distinction [#how-to-use-this-distinction] * Reading about a **concept** (auth, LIX, storage)? You're in the [Platform API](/docs/platform-api). It applies to every runtime. * Reading about **rendering, input, networking, or engine conveniences**? You're in a runtime section β€” [Web SDK](/docs/web-sdk) or [Unreal SDK](/docs/unreal-sdk). {/* docs-check: planned-api β€” rows marked "(planned)" describe unshipped namespaces */} # Quickstart (/docs/introduction/quickstart) This builds and publishes a multiplayer world on the **Web** runtime, the fastest path to a playable link. Every command on this page was run end to end against the live platform; the [command reference](/docs/cli/reference) lists everything else the CLI can do. Unreal setup lives in the [Unreal SDK](/docs/unreal-sdk) section. ## Prerequisites [#prerequisites] * Node.js 20 or newer * A HELIX account ([helixgame.com](https://helixgame.com)) * A Chromium-based browser available to Playwright. Publishing renders your world once to check it and to capture a cover image. If none is installed, the CLI tells you to run `helix screenshot --install`. ## 1. Install the CLI and sign in [#1-install-the-cli-and-sign-in] ```bash npm install -g @hypersoniclabs/helix-cli helix login ``` The binary is `helix`, the package is `@hypersoniclabs/helix-cli`. A bare `npx helix` runs an unrelated package. To skip the global install, prefix every command with `npx -y @hypersoniclabs/helix-cli@latest`. Keep the `@latest`: without it, npx runs a copy that is already installed globally, even an old one, instead of fetching the current release. `helix login` opens your browser and stores a client token locally. On a machine without a browser, `helix login --no-browser` prints the sign-in URL instead, and `helix login --token ` signs in non-interactively for CI. Check the result with `helix whoami`. Browser login needs a callback on the machine that runs the CLI. On a headless or remote host, create a client token in [Account β†’ API keys](https://helixgame.com/account/api-keys) and run `helix login --token "$HELIX_CREATOR_TOKEN"` before publishing. Keep the token in your secret manager. See the [CLI account guide](/docs/cli#account). ## 2. Create a world [#2-create-a-world] ```bash helix init my-world --kind multiplayer --title "My World" cd my-world npm install helix install ``` * `helix init` scaffolds a complete Vite project. `--kind` is `character` (single player, the default), `multiplayer` (players see and hear each other) or `scene`. * `helix install` resolves the platform systems your world pins (the character and engine packages), downloads their code into `public/helix_modules/`, and wires the hosted runtime into your import map. ## 3. Build and check it [#3-build-and-check-it] ```bash npm run build helix validate dist ``` `helix validate` runs the same rules the server enforces at publish time, so a problem found here is a problem you do not meet later. Fix everything it reports before moving on. The scaffold's comments say it too: test with `npm run build` and `npm run preview`. The dev server rewrites bare imports and hides whether the import map is correct. To try saves, purchases and entitlements without a login or a backend, run `helix dev` from the project folder. It serves the built bundle under a simulated HELIX shell. ## 4. Publish [#4-publish] ```bash helix verify-subpath dist helix publish dist ``` `helix verify-subpath` serves your bundle from a nested path, the way a published world is served, and fails on root-absolute 404s that every other local check misses. `helix publish` runs it again, uploads the files, finalizes an immutable build, captures a cover image and prints the play link: ```text βœ” Published "My World" build 1 play: https://helixgame.com/play/my-world ``` Open the link in two browser windows to see two players in the same space. ## Make something happen [#make-something-happen] The scaffold already gives you a character, an avatar, an emote wheel and, for `--kind multiplayer`, presence and proximity voice. To add shared game state, declare it in `public/helix.json`. HELIX multiplayer is declarative and server-authoritative, so there is no server code to write. The [first multiplayer world](/docs/guides/first-multiplayer-world) guide adds a server-validated action to exactly this project. ## Where to go next [#where-to-go-next] # Apps & the manifest (/docs/phone/app-model) A Helix Phone app is a **web bundle** (HTML/CSS/JS) described by a **manifest**. The manifest is the contract between your app and the phone: it declares the app's identity, where its code lives, which [permissions](/docs/phone/permissions) it needs, and any in-app purchases. ## The manifest [#the-manifest] ```ts type PhoneAppManifest = { appId: PhoneManifestAppId; // unique id (built-in slug, or "vendor.app") name: string; // display name version?: string; // semver, e.g. "0.1.0" subtitle: string; // short tagline on the store card icon: string; // icon token (e.g. "solar:gallery-bold") accent: string; // brand hex, e.g. "#ff4f9a" entry?: string; // where the app loads from (see below) allowedOrigins?: string[]; // sandbox origin allow-list (e.g. ["self"]) permissions: PhonePermissionScope[]; // scopes the app may request contentRating: "Everyone" | "13+" | "16+" | "18+"; iap?: PhoneIapProduct[]; // optional in-app purchase catalog }; ``` A real third-party manifest (the bundled sandbox demo): ```ts { appId: "studio.example", name: "Studio Example", version: "0.1.0", subtitle: "Sandbox demo app", icon: "solar:code-square-bold", accent: "#f2f2f2", entry: "/phone-apps/studio-example/index.html", allowedOrigins: ["self"], permissions: [ "account.basic", "storage.app", "media.pick", "camera.capture", "wallet.read", "presence.world", "payments", "notifications.push", "messages.send_with_consent", "voice.calls", "social.discovery", "social.connections.read", ], contentRating: "16+", iap: [ { sku: "demo_boost_1", name: "Demo Boost", priceLix: 150, type: "consumable" }, { sku: "demo_theme_pack", name: "Demo Theme Pack", priceLix: 500, type: "non_consumable" }, ], } ``` ### App IDs [#app-ids] * **Built-in apps** use a bare slug: `helixgram`, `messages`, `camera`, … * **Third-party apps** use a namespaced `vendor.app` form (e.g. `studio.example`, `acme.notes`). The dot-separated shape keeps third-party IDs from ever colliding with first-party slugs. ### `entry` and the sandbox [#entry-and-the-sandbox] `entry` tells the phone where to load the app: | Value | Meaning | | ----------------------------- | -------------------------------------------------------------------------- | | `helix://apps/` | A **built-in** app rendered by the phone shell itself (native React view). | | `/phone-apps//index.html` | A bundle served by Helix. | | an `https://…` URL | A bundle hosted by the developer (must be listed in `allowedOrigins`). | Third-party apps load inside a **sandboxed iframe** and talk to the phone through the [bridge](/docs/phone/runtime-and-bridge). `allowedOrigins: ["self"]` restricts the bundle to its own origin. ### Content rating [#content-rating] `contentRating` is one of `Everyone` / `13+` / `16+` / `18+`. It's declared per app and surfaced in the store so players (and the platform's age gates) know what to expect. ## Built-in apps [#built-in-apps] The phone ships with these **first-party** apps, always installed: | App ID | Name | What it does | | ------------ | ---------- | ------------------------------------------- | | `helixgram` | Helixgram | Photo & video feed (Instagram-style) | | `h` | H | Public square / short posts (Twitter-style) | | `messages` | Messages | Universal DMs across worlds | | `phone` | Phone | Calls & the dialer | | `contacts` | Contacts | Friends, backed by the Helix social graph | | `wallet` | Wallet | LIX & Coins balances and history | | `album` | Album | Captured photos & videos | | `camera` | Camera | Capture from the current world | | `settings` | Settings | Phone preferences, permissions, appearance | | `calculator` | Calculator | Quick math | | `weather` | Weather | Forecast | | `browser` | Browser | In-phone web | | `notes` | Notes | Jot things down | | `app-store` | App Store | Discover & install apps | Built-in apps are special only in two ways: they can render as native shell views (`helix://`), and they're granted their declared permissions by default. Otherwise they call the **same Phone SDK** third-party apps use β€” so the SDK reference applies to both. Because the social feeds are first-party, the SDK exposes them directly as [`phone.helixgram`](/docs/phone/reference#feeds-helixgram--h) and [`phone.h`](/docs/phone/reference#feeds-helixgram--h) β€” any app with `social.discovery` can read and post to them. ## Install state [#install-state] A player's relationship to an app is an **install record**: `installed` or `uninstalled`. Built-in apps are pre-installed and can't be uninstalled. Third-party apps are installed from the [app store](/docs/phone/app-store) and can be removed (which also revokes their permissions and clears their notifications). # The App Store (/docs/phone/app-store) The **App Store** app is how players discover and install third-party phone apps. This page is the developer's side: how an app gets into the store, how installs work, and what the platform guarantees. ## What an entry is [#what-an-entry-is] A store entry is a [manifest](/docs/phone/app-model) plus a hosted bundle. The manifest provides the store card (name, subtitle, icon, accent, content rating), the permission list the player will see, and any [in-app purchases](/docs/phone/in-app-purchases). ## Install lifecycle [#install-lifecycle] **Discover.** The store lists every catalog app from `GET /api/v1/phone/apps`. Each card shows the declared permissions and content rating up front. **Install.** `POST /api/v1/phone/apps/:appId/install` creates an install record (`status: "installed"`) and grants only `account.basic`. The app now appears on the home screen. **Grant as needed.** The player grants additional [scopes](/docs/phone/permissions) on first use or in Settings. **Uninstall.** `DELETE /api/v1/phone/apps/:appId/install` removes the app, **revokes its permissions, and clears its notifications**. Built-in apps can't be uninstalled. ## REST surface (first-party / server) [#rest-surface-first-party--server] Apps themselves use the [SDK](/docs/phone/reference); these endpoints back the store and tooling: | Method | Path | Purpose | | -------- | --------------------------------------- | ------------------------------------------ | | `GET` | `/api/v1/phone/apps` | List the catalog (built-in + third-party). | | `GET` | `/api/v1/phone/apps/:appId` | A single manifest. | | `GET` | `/api/v1/phone/installs` | The player's installed apps. | | `POST` | `/api/v1/phone/apps/:appId/install` | Install. | | `DELETE` | `/api/v1/phone/apps/:appId/install` | Uninstall. | | `GET` | `/api/v1/phone/permissions` | Effective grants per app. | | `PUT` | `/api/v1/phone/apps/:appId/permissions` | Replace an app's grants. | | `GET` | `/api/v1/phone/permissions/audit` | Recent permission events. | ## Publishing today [#publishing-today] Today the catalog is **first-party curated** β€” an app's manifest is registered in the backend app registry (`phone-app-registry.ts`), and bundles are served from Helix. There is no self-serve upload or automated review queue yet. To ship a third-party app now, you work with the Helix team to register the manifest and host the bundle. ## Publishing, proposed [#publishing-proposed] The self-serve developer flow is designed and tracked on the [Proposed & in-progress](/docs/phone/proposed#app-store-self-serve-publishing) page: * A **developer portal** + `helix phone publish` CLI to submit a manifest and bundle. {/* docs-check: ignore */} * A **review queue** with states (`draft β†’ in_review β†’ approved β†’ published`), content-rating checks, and permission justification. * **Versioned releases** and staged rollout. * **Revenue share** on IAP, mirroring the platform's creator economy. Until that lands, build and test against the local sandbox (the bundled `studio.example` app and your own bundle under `public/phone-apps/`), and use the manifest format above β€” it won't change when self-serve publishing ships. ## Distribution guarantees [#distribution-guarantees] Regardless of how an app is published, the platform guarantees: * **Sandboxing.** Third-party bundles run in an isolated iframe and reach the platform only through the permission-checked [bridge](/docs/phone/runtime-and-bridge). * **Consent.** No scope beyond `account.basic` is active without the player granting it, and any scope is revocable. * **Auditability.** Every permission use is recorded; abusive apps can be cut off per-scope. # Example app: World Postcard (/docs/phone/example-app) Let's build a real app end to end. **World Postcard** lets a player turn a moment from the world they're in into a shareable postcard. Along the way it uses **media**, **camera**, **presence**, **storage**, **payments (IAP)**, **notifications**, **Helixgram**, and the **UI** affordances β€” a broad cross-section of the [SDK](/docs/phone/reference). `media.pickFromAlbum` Β· `camera.mockCapture` Β· `presence.getCurrentWorld` Β· `storage` Β· `payments.purchase` Β· `helixgram.createPost` Β· `notifications.push` Β· `ui.shareSheet` Β· `ui.setBadge` Β· `lifecycle` ## 1. The manifest [#1-the-manifest] Declare exactly the scopes the features below use β€” nothing more. ```ts { appId: "acme.postcard", name: "World Postcard", version: "1.0.0", subtitle: "Turn this moment into a postcard", icon: "solar:postcard-bold", accent: "#f5d90a", entry: "/phone-apps/world-postcard/index.html", allowedOrigins: ["self"], permissions: [ "account.basic", "media.pick", "camera.capture", "presence.world", "storage.app", "payments", "notifications.push", "social.discovery" // to post to Helixgram ], contentRating: "Everyone", iap: [ { sku: "gold_frame", name: "Gold Frame", priceLix: 200, type: "non_consumable" } ] } ``` ## 2. Boot the SDK and restore a draft [#2-boot-the-sdk-and-restore-a-draft] ```ts title="app.ts" import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "acme.postcard" }); type Draft = { assetId?: string; caption: string; frame: "plain" | "gold" }; let draft: Draft = (await phone.storage.get("draft")) ?? { caption: "", frame: "plain", }; const me = await phone.account.getCurrentUser(); phone.ui.toast(`Hi ${me?.displayName ?? "traveler"} β€” make a postcard!`); // Persist the draft whenever the app is backgrounded. phone.lifecycle.on("suspend", () => phone.storage.set("draft", draft)); ``` ## 3. Get the photo β€” pick or capture [#3-get-the-photo--pick-or-capture] Let the player choose an existing album photo, or capture a new one from the world. ```ts async function choosePhoto() { const ok = await phone.permissions.request(["media.pick", "camera.capture"]); if (!ok.includes("media.pick")) { phone.ui.toast("Photo access is needed to make a postcard."); return; } const useCamera = await phone.ui.confirm({ title: "New photo?", body: "Capture from this world, or pick from your album.", confirmLabel: "Capture", cancelLabel: "Album", }); const asset = useCamera ? await phone.camera.mockCapture({ caption: "Postcard" }) // real capture proposed : await phone.media.pickFromAlbum(); if (asset) { draft.assetId = asset.id; render(asset); } } ``` ## 4. Stamp it with the current world [#4-stamp-it-with-the-current-world] Postcards say where you are. Read presence and use the world name as the location. ```ts async function currentLocation(): Promise { const w = await phone.presence.getCurrentWorld(); return w?.worldName ?? "Somewhere in Helix"; } ``` ## 5. Sell a premium frame (IAP) [#5-sell-a-premium-frame-iap] A "Gold Frame" is a one-time, non-consumable unlock. Check ownership, then offer it. ```ts async function applyGoldFrame() { const owned = await phone.payments.getEntitlements(); let hasGold = owned.some((e) => e.sku === "gold_frame"); if (!hasGold) { const buy = await phone.ui.confirm({ title: "Gold Frame β€” 200 LIX", body: "Unlock a premium gold frame for your postcards.", confirmLabel: "Buy", }); if (!buy) return; const ent = await phone.payments.purchase("gold_frame", { idempotencyKey: `gold_frame:${me?.userId}`, }); hasGold = Boolean(ent); } if (hasGold) { draft.frame = "gold"; render(); } } ``` ## 6. Post, notify, share [#6-post-notify-share] Compose the postcard, post it to **Helixgram**, push a **notification**, bump the app **badge**, and offer the **share sheet**. ```ts async function postPostcard() { if (!draft.assetId) return phone.ui.toast("Pick a photo first."); const asset = (await phone.media.listAlbum()).find((a) => a.id === draft.assetId); if (!asset) return; const location = await currentLocation(); // 1) Post to the Helixgram feed (needs social.discovery + media.pick) const post = await phone.helixgram.createPost({ asset, caption: draft.caption || "Wish you were here ✨", location, }); // 2) Notify the player it's live (deep-links back into this app) await phone.notifications.push({ title: "Postcard posted", body: `Your ${location} postcard is live on Helixgram.`, deepLink: "acme.postcard/posted", }); // 3) Badge the home-screen icon await phone.ui.setBadge(1); // 4) Offer to share it out await phone.ui.shareSheet({ items: [{ title: "My World Postcard", text: `A postcard from ${location}`, url: post?.id ? `https://helixgame.com/g/${post.id}` : undefined, }], }); // 5) Clear the draft draft = { caption: "", frame: "plain" }; await phone.storage.set("draft", draft); } ``` ## 7. Handle the deep link [#7-handle-the-deep-link] When the player taps the notification, the app re-opens with a launch context β€” route to the "posted" screen and clear the badge. ```ts const ctx = await phone.runtime.launchContext(); if (ctx?.source === "notification" && ctx.path.endsWith("/posted")) { showPostedScreen(); await phone.ui.setBadge(0); await phone.notifications.markAppRead(); } ``` ## What this demonstrates [#what-this-demonstrates] | Step | SDK surface | Scope | | ----------------- | --------------------------------------------------------- | ----------------------------------- | | Greet + toast | `account`, `ui.toast` | `account.basic` | | Pick / capture | `media.pickFromAlbum`, `camera.mockCapture`, `ui.confirm` | `media.pick`, `camera.capture` | | Location stamp | `presence.getCurrentWorld` | `presence.world` | | Draft persistence | `storage`, `lifecycle` | `storage.app` | | Premium frame | `payments` | `payments` | | Publish | `helixgram.createPost` | `social.discovery` (+ `media.pick`) | | Notify + badge | `notifications.push`, `ui.setBadge` | `notifications.push` | | Share | `ui.shareSheet` | β€” | | Deep link | `runtime.launchContext` | β€” | Drop the bundle under `public/phone-apps/world-postcard/` and register the manifest (see [the App Store](/docs/phone/app-store)). In a sandboxed bundle you don't pass a token β€” the SDK uses the [bridge](/docs/phone/runtime-and-bridge) automatically, so the exact code above runs unchanged. The bundled `studio.example` app is a minimal reference that exercises every bridge method. ## Next [#next] # In-app purchases (/docs/phone/in-app-purchases) Phone apps monetize with **in-app purchases (IAP)** priced in [LIX](/docs/platform-api/lix-economy). You declare a product catalog in your manifest; players buy with `phone.payments`. ## Declare products [#declare-products] ```ts // in the app manifest iap: [ { sku: "demo_boost_1", name: "Demo Boost", priceLix: 150, type: "consumable" }, { sku: "demo_theme_pack", name: "Demo Theme Pack", priceLix: 500, type: "non_consumable" }, { sku: "pro_monthly", name: "Pro", priceLix: 999, type: "subscription", period: "P1M" }, ] ``` | Type | Meaning | | ---------------- | ---------------------------------------------------------------------------------------------- | | `consumable` | Can be bought repeatedly (boosts, currency packs). Each purchase is a new entitlement. | | `non_consumable` | Bought once, owned forever (a theme, an unlock). Re-purchase returns the existing entitlement. | | `subscription` | Recurring access with an `expiresAt`. | ## Purchase [#purchase] The client **requests**; the server validates against the manifest, applies idempotency, and grants: ```ts const ent = await phone.payments.purchase("demo_theme_pack", { idempotencyKey: crypto.randomUUID(), }); if (ent) unlockTheme(ent.sku); ``` Networks retry. With a stable `idempotencyKey`, a retried purchase returns the *same* entitlement instead of charging twice. Generate it once per purchase attempt. ## Read what the player owns [#read-what-the-player-owns] ```ts const owned = await phone.payments.getEntitlements(); const hasPro = owned.some(e => e.sku === "pro_monthly" && !isExpired(e)); // on a fresh device: await phone.payments.restore(); ``` An entitlement: ```ts type PhoneIapEntitlement = { sku: string; productName: string; productType: "consumable" | "non_consumable" | "subscription"; priceLix: number; expiresAt: string | null; // future date for subscriptions mock: boolean; // true until LIX settlement ships }; ``` ## Settlement status [#settlement-status] Purchases currently **grant entitlements but do not debit LIX** β€” they're recorded with `mock: true` (`settlement: pending_lix_purchase_sheet`). When LIX settlement ships, the same `phone.payments.purchase(...)` call will present the platform purchase sheet and debit the player's wallet server-side; **your code won't change**. Build against this API now. Tracked on [Proposed & in-progress](/docs/phone/proposed#real-iap-settlement). ## Server authority [#server-authority] As everywhere in Helix, **value is server-authoritative**: the client can't grant itself an entitlement. The server checks the `payments` scope, verifies the `sku` exists in your manifest, applies idempotency, and (soon) charges LIX. Mirror the [platform economy rules](/docs/platform-api/lix-economy) β€” never trust a client-reported purchase. # The Helix Phone (/docs/phone) Every Helix player carries a **phone**. It isn't part of any one world β€” it travels with the player everywhere, the same way your real phone follows you from place to place. It has a home screen, a set of **built-in apps** (messages, camera, wallet, a photo feed, and more), and an **app store** where third-party developers publish their own apps built on the **Helix Phone SDK**. The Helix Phone is a tiny operating system that lives on top of the Helix platform. Apps are sandboxed web bundles; the **Phone SDK** is how they reach the player's identity, wallet, media, contacts, notifications, and the world they're currently in β€” always gated by permissions. ## Why a phone? [#why-a-phone] A world is a *place*. A phone is *personal* and *portable*. Putting social, payments, messaging, and capture behind a familiar phone metaphor means: * **Players** get one consistent place for their messages, photos, wallet, and friends β€” no matter which world they're in. * **Developers** get a ready-made distribution surface (the app store) and a high-level SDK, instead of rebuilding identity, payments, and media for every world. ## How it fits together [#how-it-fits-together]
The shell

The phone OS: home screen, app switcher, notifications, permissions, and the runtime that hosts apps.

Apps

Built-in (first-party) apps and third-party apps from the store. Each is a web bundle described by a manifest.

The Phone SDK

One object β€” createHelixPhoneSdk() β€” exposing notifications, media, wallet, contacts, messages, storage, and more.

## A taste of the SDK [#a-taste-of-the-sdk] Every app boots the SDK with its own `appId`, then calls high-level namespaces. Reading the player and posting a notification is just: ```ts import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "studio.example" }); const me = await phone.account.getCurrentUser(); await phone.notifications.push({ title: "Welcome", body: `Hi ${me?.displayName ?? "there"} πŸ‘‹`, }); ``` That call only works if the app **declared** `notifications.push` in its manifest and the player **granted** it β€” see [Permissions & consent](/docs/phone/permissions). ## Start here [#start-here] The Phone SDK is shipping. A handful of capabilities are wired end-to-end against **mocked** backends today (camera capture, voice calls, IAP settlement, live world presence) β€” these are flagged inline and tracked on the [Proposed & in-progress](/docs/phone/proposed) page. Everything else (messages, contacts, notifications, media/album, app storage, permissions, social feeds) is real. # Keeping these docs in sync (/docs/phone/keeping-docs-in-sync) The Phone SDK reference is meant to be **comprehensive**, which only works if it can't silently fall behind the code. This repo ships a small pipeline that compares the **live SDK surface** against a committed **snapshot of what the docs cover**, and flags drift automatically. ## How it works [#how-it-works] **Snapshot.** `content/docs/phone/_meta/sdk-surface.json` records every `namespace.method` the docs account for. It's generated from the real `lib/phone/sdk.ts`: ```bash node scripts/extract-phone-sdk-surface.mjs /lib/phone/sdk.ts --json \ > content/docs/phone/_meta/sdk-surface.json ``` The extractor reads the stable bridge method strings (`bridgeRequest("media.pickFromAlbum", …)`) plus a small allow-list of non-bridge methods β€” so any method an app can actually call is captured. **Detect.** `scripts/check-phone-sdk-sync.mjs` extracts the live surface and diffs it against the snapshot, reporting three buckets: * **βž• In the SDK, missing from docs** β†’ document it (and move it off [Proposed](/docs/phone/proposed)). * **βž– In docs, gone from the SDK** β†’ prune or mark removed. * **πŸ†• New namespaces** β†’ likely a whole new doc section. It exits non-zero on drift, so CI can act on it. ```bash PHONE_SDK_PATH=/lib/phone/sdk.ts node scripts/check-phone-sdk-sync.mjs # βœ… In sync β€” 74 methods across 18 namespaces. ``` **Act.** The `phone-sdk-sync` workflow (`.github/workflows/phone-sdk-sync.yml` in the docs repository) runs the checker when the monorepo signals a change, on a weekly safety-net schedule, and on demand. On drift it opens (or updates) a **`phone-sdk-drift` issue** listing exactly what changed. ## Wiring the trigger on the `helix3` branch [#wiring-the-trigger-on-the-helix3-branch] The docs repo can't see the private monorepo, so the monorepo **pushes** a signal when the SDK changes. Add this workflow to the Helix monorepo on the **`helix3`** branch β€” it fires only when `lib/phone/sdk.ts` (or the phone types) change, and sends the SDK file inline so no cross-repo token is needed: ```yaml title=".github/workflows/notify-docs-phone-sdk.yml (in the monorepo, on helix3)" name: Notify docs of Phone SDK change on: push: branches: [helix3] paths: - "lib/phone/sdk.ts" - "lib/phone/types.ts" jobs: notify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Dispatch to helix3-docs env: GH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }} # PAT with repo scope on helix3-docs run: | SDK_B64=$(base64 -w0 lib/phone/sdk.ts) gh api repos/hypersonic-laboratories/helix3-docs/dispatches \ -f event_type=phone-sdk-updated \ -F client_payload[sdkBase64]="$SDK_B64" ``` That's the whole loop: **SDK changes on `helix3` β†’ docs repo checks drift β†’ drift opens an issue β†’ the reference and [Proposed](/docs/phone/proposed) page are reconciled β†’ snapshot regenerated.** If you'd rather not add the monorepo trigger yet, set a `PHONE_SDK_URL` repo secret (a raw URL to `lib/phone/sdk.ts` on `helix3`, with a token if private) and the weekly scheduled run will catch drift on its own. ## Scope & limits [#scope--limits] This pipeline tracks the **shape** of the SDK (namespaces and methods) β€” the thing most likely to drift and the thing the reference must mirror. It intentionally doesn't diff parameter types or prose; those are reconciled by a human (or an agent) when an issue is opened. It pairs with the main SDK's [generated CLI, MCP and Web SDK reference](/docs/cli/reference) β€” same philosophy, applied to the phone. # Permissions & consent (/docs/phone/permissions) Every sensitive Phone SDK call is gated by a **permission scope**. An app can only *use* a scope if it (1) **declared** it in its [manifest](/docs/phone/app-model) and (2) the player **granted** it. Calls without permission return empty/`null` rather than throwing β€” so your app degrades gracefully. ## The scopes [#the-scopes] ## How granting works [#how-granting-works] **Declare** the scopes in your manifest's `permissions` array. Requesting a scope you didn't declare is an error. **Default grants.** `account.basic` is always granted. Built-in apps get all their declared scopes by default. **Third-party apps start with only `account.basic`** β€” everything else needs the player's explicit consent. **Request at point of use.** Ask for a scope when the feature is first used, and check the result: ```ts const granted = await phone.permissions.request("media.pick"); if (!granted.includes("media.pick")) { phone.ui.toast("Media access is needed to attach a photo."); return; } ``` **Players manage grants** in Settings β†’ the app's permission list, and can revoke any scope except `account.basic` at any time. ## Checking permissions [#checking-permissions] ```ts await phone.permissions.list(); // β†’ granted scopes for this app await phone.permissions.has("wallet.read"); // β†’ boolean ``` Because ungranted calls return empty values, you can also just *try* and handle the empty result β€” but checking first lets you show better UI. ## Consent-gated messaging [#consent-gated-messaging] `messages.send_with_consent` is deliberately named: sending messages on a player's behalf is sensitive, so every send is **audited** and the player can revoke the scope to immediately cut an app off. Build messaging features assuming the player is watching who they message. ## The audit trail [#the-audit-trail] Every permission-relevant event is recorded β€” grants, revocations, denied calls, and calls to **undeclared** scopes: ```ts type PhonePermissionAudit = { appId: string; scope: PhonePermissionScope; outcome: | "granted" | "denied" | "undeclared" | "consent_granted" | "consent_revoked" | "consent_denied"; reason: string | null; createdAt: string; }; ``` Players can review this in Settings; it's also what powers abuse investigations. For developers, the takeaway is simple: **declare only what you use, and request it in context** β€” over-asking shows up in the audit log and erodes trust (and store standing). # Proposed & in-progress (/docs/phone/proposed) Everything on this page is **Proposed** or **In-progress**. APIs here may change name, shape, or be cut. The shipping surface is the [SDK reference](/docs/phone/reference); build production apps against that. This page exists so the picture is complete and so feedback lands before the API freezes. We split this into two buckets: * **In-progress** β€” the method exists in the SDK today but runs against a **mock** backend. * **Proposed** β€” a gap we found while documenting; a new surface we think the Phone should expose. *** ## In-progress (mocked today) [#in-progress-mocked-today] These ship in the SDK now and are marked mock in the reference. The method signatures are expected to stay; only the backend becomes real. ### Real camera capture [#real-camera-capture] `camera.mockCapture` produces a placeholder asset. **Proposed:** a real `camera.capture()` that grabs the actual framebuffer of the world the player is in (and, where available, device camera), writing a true photo/video into the album. ### Real voice calls [#real-voice-calls] `calls.*` simulate call state (`provider: "mock"`, `status: "pending_voice_api"`). **In-progress:** a real voice stack so `calls.start(contactId)` opens a live call, with ringing/connected/ended driven by the platform rather than `updateStatus`. ### Real IAP settlement [#real-iap-settlement] `payments.purchase` grants entitlements with `mock: true` and does **not** debit LIX yet. **In-progress:** the platform purchase sheet + server-side LIX settlement. The call signature is final β€” only the money movement is being added. ### Live world presence [#live-world-presence] `presence.getCurrentWorld` returns a fixed world. **In-progress:** real presence wired to the player's actual world/instance, plus a proposed `presence.subscribe(cb)` for live updates as they travel. ### Real social-discovery matching [#real-social-discovery-matching] `social.recordSwipe` always returns `matched: false`. **In-progress:** real mutual-match detection and a `social.getMatches()` surface. ### Placeholder wallet history [#placeholder-wallet-history] The Wallet app's transaction list is deterministic synthetic data until the economy ledger is wired in; shape is final. *** ## Proposed new surfaces [#proposed-new-surfaces] Gaps we found while documenting the SDK. Each would be additive. ### `phone.intents` β€” app-to-app deep links Proposed [#phoneintents--app-to-app-deep-links-proposed] Today apps deep-link into **themselves** via notifications. Proposed: a way to hand off to *another* app with the player's consent β€” e.g. a game opening the Camera, or any app opening a Helixgram composer. ```ts // Proposed await phone.intents.open("camera", { action: "capture", returnTo: "acme.postcard" }); const result = await phone.intents.request("media.pick"); // system picker, returns an AlbumAsset ``` ### `phone.location` β€” in-world position Proposed [#phonelocation--in-world-position-proposed] Presence gives the *world*; some apps want coordinates (a map, a geocache, a "near me"). Proposed: a coarse, permissioned `location.getPosition()` scoped to the current instance, behind a new `location.read` scope. ### `phone.ui.haptic` & `phone.ui.clipboard` Proposed [#phoneuihaptic--phoneuiclipboard-proposed] Small but expected affordances: ```ts // Proposed phone.ui.haptic("success"); // light | success | warning | error await phone.ui.clipboard.write("gg"); const text = await phone.ui.clipboard.read(); // consent-gated ``` ### Blob storage beyond 32 KB Proposed [#blob-storage-beyond-32-kb-proposed] `storage` is a 32 KB KV store. Proposed: `phone.files` for larger per-app blobs (saved games, exports) with signed URLs, distinct from the album. ### Server-initiated & scheduled push Proposed [#server-initiated--scheduled-push-proposed] `notifications.push` is client-initiated while the app runs. Proposed: a server endpoint so an app's backend can notify a player who isn't currently in the app, plus `notifications.schedule(at, …)` for reminders. ### Sensors & audio Proposed [#sensors--audio-proposed] Proposed read-only `phone.sensors` (accelerometer/orientation for motion-controlled apps) and a small `phone.audio` for SFX/looping clips, each behind its own scope. ### Contacts write Proposed [#contacts-write-proposed] `contacts.list()` is read-only. Proposed: app-created, app-scoped contacts (e.g. a guild app adding guildmates) β€” never touching the player's real social graph without consent. ### App Store self-serve publishing Proposed [#app-store-self-serve-publishing] The catalog is first-party curated today. Proposed developer flow: * A **developer portal** + `helix phone publish` CLI to submit a manifest + bundle. {/* docs-check: ignore */} * A **review queue**: `draft β†’ in_review β†’ approved β†’ published`, with content-rating and permission-justification checks. * **Versioned releases**, staged rollout, and **IAP revenue share** mirroring the [creator economy](/docs/platform-api/lix-economy). * **Widgets / live activities** on the home and lock screen as a later phase. *** ## Have an opinion? [#have-an-opinion] This list is where the Phone SDK is going β€” if something you need is missing or mis-shaped, that's the most useful feedback you can give before these freeze. The [sync pipeline](/docs/phone/keeping-docs-in-sync) will automatically move items off this page and into the [reference](/docs/phone/reference) as they ship. # Phone SDK reference (/docs/phone/reference) This is the **complete** surface of the Helix Phone SDK as it ships today (v0.1.0). Each method lists its required [permission scope](/docs/phone/permissions); calls without a granted scope return `null`/`[]` rather than throwing. Methods marked mock are wired against a mocked backend until the real integration lands β€” see [Proposed & in-progress](/docs/phone/proposed). ## Creating the SDK [#creating-the-sdk] ```ts import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "studio.example", // required β€” your manifest appId accessToken?: string, // first-party/server only; omit for sandboxed apps manifest?: PhoneAppManifest, bridgeNonce?: string, // usually read from ?helixBridgeNonce automatically }); ``` `createHelixPhoneSdk` returns an object with the namespaces below. It auto-selects the [bridge or direct transport](/docs/phone/runtime-and-bridge#two-execution-modes). *** ## runtime [#runtime] | Method | Returns | Notes | | ------------------------- | ------------------------------- | --------------------------------------------------------------------- | | `runtime.bootstrap()` | `PhoneBootstrapItem \| null` | Account, installed apps, permissions, launch context, runtime config. | | `runtime.manifest()` | `PhoneAppManifest \| null` | This app's manifest. | | `runtime.launchContext()` | `PhoneAppLaunchContext \| null` | How the app was opened (`web` / `notification` / `shell`). | ## account [#account] | Method | Returns | Scope | | -------------------------- | ---------------------- | -------------------------------- | | `account.getCurrentUser()` | `PhoneAccount \| null` | `account.basic` (always granted) | ```ts const me = await phone.account.getCurrentUser(); // { userId, username, displayName, avatarUrl, phoneNumber, bio } ``` ## permissions [#permissions] | Method | Returns | Notes | | ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | | `permissions.list()` | `PhonePermissionScope[]` | Scopes currently granted to this app. | | `permissions.has(scope)` | `boolean` | Check a single scope. | | `permissions.request(scopes)` | `PhonePermissionScope[]` | Request one scope or an array; returns granted scopes. Throws if a scope wasn't declared in the manifest. | ## storage [#storage] Per-app key–value storage. Keys match `^[a-zA-Z0-9._:-]{1,128}$`; values are JSON up to **32 KB**. Writes are atomic upserts. **Scope: `storage.app`.** | Method | Returns | | ---------------------------- | ---------------------------------- | | `storage.get(key)` | `T \| null` | | `storage.set(key, value)` | `PhoneAppStorageRecord \| null` | | `storage.delete(key)` | `void` | ```ts await phone.storage.set("draft", { caption: "gm", assetId }); const draft = await phone.storage.get<{ caption: string; assetId: string }>("draft"); ``` ## notifications [#notifications] Create and manage **this app's** notifications. Push is rate-limited to \~12/hour per app. **Scope: `notifications.push`.** | Method | Returns | Notes | | ------------------------------------------------ | --------------------------- | -------------------------------------------------------------- | | `notifications.list()` | `PhoneNotification[]` | This app's notifications. | | `notifications.push({ title, body, deepLink? })` | `PhoneNotification \| null` | `title` ≀120, `body` ≀500. `deepLink` opens the app at a path. | | `notifications.markRead(notificationId)` | `PhoneNotification \| null` | | | `notifications.markAppRead(appId?)` | `void` | Marks all of this app's notifications read. | ```ts await phone.notifications.push({ title: "Render ready", body: "Your postcard finished exporting.", deepLink: "studio.example/exports", }); ``` ## media [#media] The player's **album** (photos/videos). Upload accepts a `File`. **Scope: `media.pick`.** | Method | Returns | Notes | | ---------------------------------------------- | -------------------- | ------------------------------------------------------------- | | `media.listAlbum()` | `AlbumAsset[]` | All album assets, newest first. | | `media.pickFromAlbum()` | `AlbumAsset \| null` | The most recent asset (the picker UI returns the chosen one). | | `media.upload(file, { caption?, worldName? })` | `AlbumAsset \| null` | Upload an image/video into the album. | ```ts const photo = await phone.media.pickFromAlbum(); if (photo) attach(photo); ``` ## camera [#camera] Capture into the album. mock β€” `mockCapture` currently produces a placeholder asset (or copies `sourceAssetId`); real world/device capture is [proposed](/docs/phone/proposed). **Scope: `camera.capture`.** | Method | Returns | | --------------------------------------------------------------------- | -------------------- | | `camera.mockCapture({ sourceAssetId?, caption?, worldName?, type? })` | `AlbumAsset \| null` | ## wallet [#wallet] Read-only balances. **Scope: `wallet.read`.** | Method | Returns | | --------------------- | ---------------------------- | | `wallet.getBalance()` | `PhoneWalletBalance \| null` | ```ts const w = await phone.wallet.getBalance(); // w.lix.totalBalance, w.coins.balance β€” read-only ``` ## payments [#payments] In-app purchases priced in **LIX**, from the app's manifest `iap` catalog. Purchases are idempotent. mock β€” entitlements are granted but LIX isn't debited yet (see [In-app purchases](/docs/phone/in-app-purchases)). **Scope: `payments`.** | Method | Returns | Notes | | --------------------------------------------- | ----------------------------- | -------------------------------------------------------- | | `payments.getProducts()` | `PhoneIapProduct[]` | The app's product catalog. | | `payments.purchase(sku, { idempotencyKey? })` | `PhoneIapEntitlement \| null` | Pass an `idempotencyKey` so retries can't double-charge. | | `payments.getEntitlements()` | `PhoneIapEntitlement[]` | What the player owns. | | `payments.restore()` | `PhoneIapEntitlement[]` | Re-fetch entitlements (e.g. on a new device). | ```ts const ent = await phone.payments.purchase("demo_theme_pack", { idempotencyKey: crypto.randomUUID(), }); ``` ## contacts [#contacts] The player's contacts, backed by the Helix social graph. **Scope: `social.connections.read`.** | Method | Returns | | ----------------- | ---------------- | | `contacts.list()` | `PhoneAccount[]` | ## messages [#messages] Threads and sending, consent-gated. **Scope: `messages.send_with_consent`.** | Method | Returns | Notes | | ----------------------------------------------------- | ----------------------- | ---------------------------------------------- | | `messages.threads()` | `MessageThread[]` | Threads with unread counts. | | `messages.send({ participantId, body, attachment? })` | `Message \| null` | `body` ≀2000; `attachment` is an `AlbumAsset`. | | `messages.markRead(threadId)` | `MessageThread \| null` | | ## calls [#calls] Voice calls and call logs. mock β€” calls are simulated until the voice API lands. **Scope: `voice.calls`.** | Method | Returns | | ------------------------------------------------------ | ----------------- | | `calls.logs()` | `CallLog[]` | | `calls.mockCall(contactId)` | `CallLog \| null` | | `calls.mockIncoming(contactId)` | `CallLog \| null` | | `calls.updateStatus(callId, status, durationSeconds?)` | `CallLog \| null` | ## presence [#presence] The player's current world/instance. mock β€” returns a fixed world until live presence lands. **Scope: `presence.world`.** | Method | Returns | | ---------------------------- | ---------------------------- | | `presence.getCurrentWorld()` | `PhonePresenceWorld \| null` | ## social [#social] Opt-in **discovery** profiles and swiping (the dating/meeting primitive). Swipe matching is mock. **Scope: `social.discovery`.** | Method | Returns | Notes | | ---------------------------------------------------------------------------- | ----------------------------------------- | -------------------------- | | `social.getMyDiscoveryProfile()` | `PhoneSocialDiscoveryProfile \| null` | | | `social.updateDiscoveryProfile({ displayName?, bio?, avatarUrl?, active? })` | `PhoneSocialDiscoveryProfile \| null` | Create/update. | | `social.disableDiscoveryProfile()` | `void` | Sets the profile inactive. | | `social.getDiscoveryFeed({ limit? })` | `PhoneSocialDiscoveryProfile[]` | Opted-in candidates. | | `social.recordSwipe(candidateUserId, "like" \| "pass")` | `PhoneSocialDiscoverySwipeResult \| null` | | ## feeds: helixgram & h [#feeds-helixgram--h] Because Helixgram (photo/video) and H (short posts) are first-party, the SDK exposes them directly. Both require **`social.discovery`**; Helixgram `createPost`/`createStory` also require **`media.pick`**. ### phone.helixgram [#phonehelixgram] | Method | Returns | | -------------------------------------------------------------------------------- | -------------------------- | | `helixgram.feed(page?)` / `helixgram.followingFeed(page?)` | `HelixgramPost[]` | | `helixgram.userPosts(userId, page?)` | `HelixgramPost[]` | | `helixgram.stories(page?)` / `helixgram.userStories(userId, page?)` | `HelixgramStory[]` | | `helixgram.userSocial(userId)` | `HelixgramProfile \| null` | | `helixgram.searchUsers(query, limit?)` | `HelixgramProfile[]` | | `helixgram.follow(userId)` / `helixgram.unfollow(userId)` | result | | `helixgram.createPost({ asset, caption, location })` | `HelixgramPost \| null` | | `helixgram.createStory({ asset, caption, location })` | `HelixgramStory \| null` | | `helixgram.like(postId)` / `helixgram.unlike(postId)` | result | | `helixgram.comment(postId, body)` / `helixgram.deleteComment(postId, commentId)` | result | | `helixgram.deletePost(postId)` / `helixgram.deleteStory(storyId)` | `void` | | `helixgram.report({ subjectType, subjectId, reason, details? })` | result | `page` is `{ limit?, before? }` for cursor pagination. ### phone.h [#phoneh] | Method | Returns | | ---------------------------------------------------------- | ------------------ | | `h.feed(page?)` / `h.followingFeed(page?)` | `HPost[]` | | `h.trends({ limit? })` | `HTrend[]` | | `h.userPosts(userId, page?)` | `HPost[]` | | `h.replies(postId, page?)` | `HPost[]` | | `h.userSocial(userId)` | `HProfile \| null` | | `h.createPost(body)` / `h.reply(postId, body)` | `HPost \| null` | | `h.repost(postId)` / `h.like(postId)` / `h.unlike(postId)` | result | | `h.follow(userId)` / `h.unfollow(userId)` | result | | `h.report({ subjectType, subjectId, reason, details? })` | result | ## ui [#ui] Native phone UI affordances. No scope required (UI-only). | Method | Returns | Notes | | ----------------------------------------------------------- | -------------------- | ------------------------------------------------------ | | `ui.toast(body)` | `void` | Transient toast. | | `ui.confirm({ title, body?, confirmLabel?, cancelLabel? })` | `boolean` | Native confirm dialog. | | `ui.shareSheet({ items })` | `PhoneUiShareResult` | OS share sheet; `items` are `{ title?, text?, url? }`. | | `ui.setBadge(count)` | `void` | Set this app's home-screen badge count. | ## lifecycle [#lifecycle] | Method | Returns | Notes | | ---------------------------------- | ------------ | -------------------------------------------------------------------------------------------- | | `lifecycle.on(event, handler)` | `() => void` | Subscribe; returns an unsubscribe fn. Events: `foreground` / `suspend` / `resume` / `close`. | | `lifecycle.emit(event, snapshot?)` | `void` | Mostly internal. | See [Runtime & the bridge](/docs/phone/runtime-and-bridge#lifecycle-events) for the lifecycle model. *** This page covers the **entire** shipping SDK surface. A [drift-detection pipeline](/docs/phone/keeping-docs-in-sync) compares it against `lib/phone/sdk.ts` on the `helix3` branch and flags any namespace or method that lands or changes, so the reference can't silently fall behind the code. # Runtime & the bridge (/docs/phone/runtime-and-bridge) The Phone SDK is the same object everywhere, but **how** it talks to the phone depends on who's running it. Understanding this makes the whole SDK click. ## Two execution modes [#two-execution-modes] When you call `createHelixPhoneSdk({ appId })`, the SDK picks one of two transports automatically: Your app runs inside a **sandboxed iframe** in the phone shell. It has no access token. The SDK routes every call through the **bridge** β€” a `postMessage` channel to the phone shell, which performs the call on your behalf and enforces permissions. This is the mode third-party apps use. ```ts // No token β€” the SDK detects it's framed and uses the bridge. const phone = createHelixPhoneSdk({ appId: "studio.example" }); ``` A first-party context (or a server-side caller) passes an **access token**. The SDK calls the phone REST API (`/api/v1/phone/...`) directly with that token. ```ts const phone = createHelixPhoneSdk({ appId: "wallet", accessToken }); ``` The SDK decides with one rule: if it's running inside another window (`window.parent !== window`) and has **no** access token, it uses the bridge. Otherwise it calls the API directly. **Your app code is identical in both modes** β€” you never branch on it. ## The bridge protocol [#the-bridge-protocol] The bridge is a request/response protocol over `postMessage`, secured by a per-session **`helixBridgeNonce`** (passed to the iframe as a query param). The shape: ```ts // app β†’ shell window.parent.postMessage({ source: "helix-phone-app", appId, bridgeNonce, requestId, method: "notifications.push", // namespaced SDK method params: { title, body }, }, "*"); // shell β†’ app { source: "helix-phone", bridgeNonce, requestId, ok: true, result: { /* … */ }, // or: ok:false, error: "scope_not_granted" } ``` Requests time out after **8 seconds**. The nonce ensures one app can't impersonate another or read another app's responses. You normally never write this by hand β€” the SDK does it β€” but it's why every SDK method maps to a stable `method` string like `media.pickFromAlbum` or `storage.set`. Third-party code never gets the player's token, never sees other apps' data, and can't call the platform directly. The shell is the only thing holding credentials; it brokers every call and checks the app's granted [permissions](/docs/phone/permissions) first. That's what makes running untrusted apps safe. ## Bootstrap β€” one snapshot of everything [#bootstrap--one-snapshot-of-everything] On launch, an app reads a **bootstrap** snapshot that contains the account, installed apps, effective permissions, the launch context, and runtime config: ```ts const runtime = await phone.runtime.bootstrap(); // runtime.account β†’ the signed-in PhoneAccount // runtime.apps β†’ installed app manifests // runtime.permissions β†’ { [appId]: PhonePermissionScope[] } // runtime.launchContext β†’ how this launch happened ``` Convenience accessors wrap it: ```ts const me = await phone.account.getCurrentUser(); // runtime.account const ctx = await phone.runtime.launchContext(); // see below const manifest = await phone.runtime.manifest(); // this app's manifest ``` ### Runtime config [#runtime-config] The bootstrap's runtime block tells you the environment you're in: ### Launch context [#launch-context] How did the app open? Deep link from a notification, the home screen, or a web URL? ```ts type PhoneAppLaunchContext = { appId: string; url: string; path: string; source: "web" | "notification" | "shell"; receivedAt: string; }; ``` Use it to route β€” e.g. a notification with a `deepLink` lands the user on the right screen. ## Lifecycle events [#lifecycle-events] The phone is **foreground-only**: only the active app runs. Subscribe to transitions so you can pause timers, save drafts, or refresh on resume: ```ts const off = phone.lifecycle.on("suspend", () => saveDraft()); phone.lifecycle.on("resume", () => refresh()); phone.lifecycle.on("close", () => flush()); // later off(); // unsubscribe ``` | Event | Fires when | | ------------ | ----------------------------------------------------------- | | `foreground` | The app becomes the active app. | | `suspend` | The app is backgrounded (another app opened, phone closed). | | `resume` | The app returns to the foreground. | | `close` | The app is being torn down. | Because backgrounded apps are suspended, write important state to [`phone.storage`](/docs/phone/reference#storage) on `suspend`/`close` rather than relying on periodic saves. # Unreal SDK (/docs/unreal-sdk) The Unreal SDK brings HELIX to **Unreal Engine**. It exposes the exact same [Platform API](/docs/platform-api) as the web β€” identity, LIX, storage, inventory, social β€” and adds engine-native rendering, physics, and networking. Native worlds are the high-fidelity path. The Unreal SDK **mirrors the [Web SDK](/docs/web-sdk) exactly** β€” every function, including character and world functions. If a method exists on `Helix.*` in TypeScript, it exists here with the same name and meaning. The web signature is canonical; differences are tracked in the [feature matrix](/docs/feature-matrix). ## Three languages, one API [#three-languages-one-api] The HELIX Unreal runtime ships with multiple scripting environments layered on native Unreal. All of them call the **same** SDK: See [Languages](/docs/unreal-sdk/languages) for setup and the same call written in each. ## Getting started [#getting-started] **Install the HELIX plugin** into your Unreal project (`HELIX` plugin, UE 5.x). It bundles the C++ SDK, the Blueprint nodes, and the Lua runtime. **Initialize** the subsystem with the world id and launch ticket the platform provides at boot: ```cpp FHelixInitParams Params; Params.WorldId = WorldId; Params.LaunchTicket = LaunchTicket; UHelix::Get()->Init(Params); ``` **Call the Platform API.** Auth, wallet, inventory, data store β€” identical to web. Pick your language in [Languages](/docs/unreal-sdk/languages). ## What's different from web [#whats-different-from-web] * **Networking.** Unreal worlds use the engine's own dedicated-server networking, with HELIX [wrappers](/docs/unreal-sdk/networking) for replication β€” *not* the web multiplayer stack. * **Rendering & input** are the engine's. HELIX doesn't abstract the scene graph. * **The Platform API is unchanged.** That's the whole point. # Languages (/docs/unreal-sdk/languages) The HELIX Unreal runtime exposes the same Platform API to three environments. Choose by team and task β€” they're interoperable within a project, and they all hit the identical backend. ## One operation, three languages [#one-operation-three-languages] Granting nothing, just reading: fetch the player's profile and LIX balance. ```cpp UHelix* H = UHelix::Get(); H->Profile()->GetMyProfile(FOnProfile::CreateLambda([](const FHelixProfile& P) { UE_LOG(LogHelix, Display, TEXT("Hi %s"), *P.DisplayName); })); H->Wallet()->GetBalance(FOnBalance::CreateLambda([](const FHelixBalance& B) { UE_LOG(LogHelix, Display, TEXT("%lld LIX"), B.Lix); })); ``` ```text Event BeginPlay β†’ Helix Β· Profile Β· Get My Profile (latent) On Success β†’ Break Profile β†’ "Display Name" β†’ Print String β†’ Helix Β· Wallet Β· Get Balance (latent) On Success β†’ Break Balance β†’ "Lix" β†’ Print String ``` Every SDK call is a node. Async calls are **latent** nodes with `On Success` / `On Error` exec pins. ```lua local me = Helix.profile.getMyProfile() -- docs-check: ignore local wallet = Helix.wallet.getBalance() UE.Log(string.format("Hi %s - %d LIX", me.displayName, wallet.lix)) ``` ## Choosing a language [#choosing-a-language] | Language | Reach for it when | | ------------- | ----------------------------------------------------------------------------------------------- | | **C++** | You want the lowest-level binding, maximum performance, or you're already in C++ gameplay code. | | **Blueprint** | Designers/artists wire logic visually; rapid iteration without compiling. | | **Lua** | You prefer Lua, or you're bringing across Lua-based logic. | ## Async conventions [#async-conventions] * **C++** uses typed delegates (`FOnX::CreateLambda`). * **Blueprint** uses latent nodes with `On Success` / `On Error`. * **Lua** calls are synchronous-looking (the binding yields under the hood). # Networking (/docs/unreal-sdk/networking) Unreal worlds **do not** use the web [multiplayer stack](/docs/web-sdk/multiplayer). They use Unreal's own networking β€” dedicated servers, actor replication, RPCs β€” and HELIX adds thin **wrappers** that make identity, tickets, and authoritative value checks ergonomic inside that model. The web runtime needs a networking layer because the browser has none; HELIX provides one. Unreal already has battle-tested networking, so forcing the web stack on it would be a downgrade. The [Platform API](/docs/platform-api) stays identical β€” only the transport differs. ## What HELIX wraps [#what-helix-wraps] * **Instance tickets β†’ connection.** Players arrive with a platform-issued Instance Ticket; the HELIX wrapper verifies it during Unreal's login/connection handshake so only authorized players join. * **Identity on the server.** Replicated player state is tied to the HELIX `Player` identity, not a raw connection β€” so `Helix.*` calls on the server know who they're for. * **Authoritative value checks.** Helpers to keep wallet/inventory/ownership decisions on the server, mirroring the [item execution tiers](/docs/platform-api/inventory#execution-tiers). ## The model [#the-model] Use Unreal replication as you normally would: * Server is authoritative; clients send input/RPCs. * Movement and cosmetic state replicate via Unreal's replication graph. * **Value-bearing actions** (grant item, charge LIX, award reward) go through the server-side [Platform API](/docs/platform-api), never a client RPC you trust blindly. ```cpp // server-side authoritative grant, same Platform API as web void AArenaGameMode::AwardWin(const FHelixPlayer& Player) { UHelix::Get()->Inventory()->ConsumeAndGrant(/* atomic, idempotent */); } ``` If it affects inventory, currency, or ownership, it's decided on the **server** via the Platform API β€” regardless of runtime. Unreal RPCs from clients are input, not authority. ## Scale [#scale] Unreal dedicated servers scale through Unreal's standard tooling and HELIX's hosting. Large-map, high-population worlds are a primary reason to choose Unreal over Web. # Reference (/docs/unreal-sdk/reference) This page is a hand-written sample of the intended shape. It is **not** generated from the Unreal plugin and has not been verified against it. The pipeline described in earlier versions of this page (Doxygen headers converted to Markdown) does not run. Treat signatures here as illustrative until the plugin reference is generated. ## `UHelixWallet` [#uhelixwallet] Concept: [LIX & Economy](/docs/platform-api/lix-economy). Web equivalent: [`Helix.wallet`](/docs/web-sdk/api). ### `GetBalance` [#getbalance] ```cpp void GetBalance(const FOnHelixBalance& OnComplete); ``` Reads the current player's balances and invokes `OnComplete` with an `FHelixBalance`. **Bindings** | Runtime | Call | | --------- | ----------------------------------------------- | | C++ | `UHelix::Get()->Wallet()->GetBalance(Delegate)` | | Blueprint | `Helix Β· Wallet Β· Get Balance` (latent) | | Lua | `Helix.wallet.getBalance()` | *** Because every binding is generated from the same C++ surface, the three languages can't drift from each other β€” and the whole surface is kept aligned with the canonical [Web SDK](/docs/web-sdk/reference) by the [feature matrix](/docs/feature-matrix) check in CI. # Analytics event contract (/docs/platform-api/analytics) `Helix.analytics` is the platform event contract for user journeys, world sessions, creator monetization, social loops, purchases, safety events, and retention. The contract is shared across web, mobile wrappers, Unreal/native clients, workers, and server code. HELIX owns event capture, validation, raw storage, rollups, creator-facing aggregates, and future exports. PostHog is an internal analytics surface for funnels, cohorts, timelines, experiments, feature flags, and sampled web session replay. PostHog is not the source of truth. ## Sending events from a world [#sending-events-from-a-world] `Helix.analytics.track(eventName, input?)` posts one client event to the HELIX collector. The SDK fills the envelope itself (`event_id`, `occurred_at`, user or anonymous id, `platform: 'web'`, `source: 'client'`, `world_id`), so a world only names the event and adds context: ```ts await Helix.analytics.track('world_loaded', { instanceId, // optional: instance, item, invite, referrer, eventId properties: { loadMs: 1840, quality: 'high' }, }); ``` World code may send only these ten events: `world_entered`, `world_loaded`, `world_load_failed`, `world_exited`, `world_heartbeat`, `instance_joined`, `instance_left`, `creator_product_viewed`, `client_error_seen` and `api_error_seen`. Property values are strings, numbers, booleans or `null`; the SDK drops keys that look sensitive (password, token, secret, email, payment and similar) and truncates strings to 256 characters. The promise rejects if the collector answers with an error. ## Architecture split [#architecture-split] * **HELIX collector** accepts single events and batches, validates the envelope, links anonymous and authenticated identities, deduplicates by `event_id`, and returns quickly. * **Raw event store** keeps append-only normalized events indexed by user, anonymous user, session, world, creator, event name, and timestamp. * **PostHog forwarder** sends approved, redacted events downstream and can be disabled without breaking HELIX event capture. * **Rollups** aggregate hourly and daily data by world, creator, user, session, invite, FTUE step, item, product, and platform. * **Staff tools** use rollups and controlled timelines for support, product, safety, and creator operations. PostHog remains the deeper internal exploration tool in v1. Heavy analysis reads rollups or an external analytics surface. Hot product paths must not query raw events. ## Event envelope [#event-envelope] Every event uses the same envelope. Fields that do not apply should be omitted or set to `null`, not filled with placeholder values. ## MVP taxonomy [#mvp-taxonomy] The collector accepts a closed list of event names; `GET /api/v1/analytics/event-names` returns the live list (it is longer than the core taxonomy below). One unknown name rejects its whole batch. Identity and acquisition: * `user_signed_up` * `user_logged_in` * `anonymous_user_identified` * `invite_sent` * `invite_opened` * `invite_accepted` * `referral_source_captured` FTUE: * `ftue_started` * `ftue_step_viewed` * `ftue_step_completed` * `ftue_step_skipped` * `ftue_completed` * `ftue_abandoned` Sessions and worlds: * `session_started` * `session_ended` * `world_entered` * `world_loaded` * `world_load_failed` * `world_exited` * `world_heartbeat` * `instance_joined` * `instance_left` Social: * `friend_request_sent` * `friend_request_accepted` * `party_created` * `party_joined` * `share_clicked` Economy and creator: * `item_viewed` * `item_purchased` * `item_equipped` * `home_visited` * `creator_product_viewed` * `creator_product_purchased` * `creator_earned` * `lix_spent` * `coins_spent` Safety and platform health: * `report_submitted` * `moderation_action_created` * `content_rating_changed` * `trust_state_changed` * `client_error_seen` * `api_error_seen` ## Authority rules [#authority-rules] * Server events are authoritative for purchases, ownership, inventory, grants, creator earnings, moderation actions, bans, trust changes, reports, and content-rating changes. * Client events are useful for journeys, UI behavior, FTUE, gameplay flow, performance context, and client errors. They are not trusted for value-bearing state. * Event ingestion is append-only. Corrections are new events, never in-place edits. * Every source uses the shared envelope so the vendor can change without changing the HELIX contract. ## Privacy and creator boundary [#privacy-and-creator-boundary] Sensitive data must not be captured by default. Do not put raw chat, raw voice, payment details, tokens, secrets, exact private messages, or unnecessary PII in analytics events. Use event and property allowlists instead of arbitrary property capture. Web session replay, when enabled in PostHog, must be sampled, controlled, web-only, and disabled for sensitive flows by default. Creators and world owners see aggregate analytics for their own worlds: * visits and unique visitors * new vs returning users * average session length and total playtime * retention summaries * invite and referral sources * product and item sales * creator earnings * FTUE or world-specific drop-off points * top products and items Creators do not get raw per-user journey timelines in v1. Staff timelines are internal support, product, safety, and operations tools. ## Runtime responsibilities [#runtime-responsibilities] Web frontend: * Wrap event capture behind a shared analytics client. * Capture route context, FTUE steps, invites, world entry and exit, session lifecycle, client errors, and selected UI events. * Identify users on login or signup and link anonymous history. Mobile wrapper: * Use the same envelope. * Prefer the shared web bridge when the app is a web wrapper. * Add native forwarding only for native-only events. Unreal and native clients: * Use a lightweight batch HTTP client that submits the same envelope to the HELIX collector. * Start with session, world, instance, gameplay milestone, error, and economy events. * Do not talk directly to PostHog in v1. Server and workers: * Emit authoritative events for money, inventory, ownership, invites, reporting, moderation, trust, content rating, and creator earnings. * Forward only approved, redacted events to PostHog. * Keep analytics non-blocking so capture never slows gameplay, checkout, ownership, grants, or backend authority paths. ## What exists today [#what-exists-today] The backend (`/api/v1/analytics`) ships: * **Capture:** `POST /events` (one event) and `POST /events/batch` (1–100 events), public and rate-limited per IP and per anonymous id; `GET /event-names`. * **Identity linking and PostHog forwarding.** Forwarding runs only when it is switched on and configured; capture works without it. * **Staff tools (admins only):** journeys, FTUE / invite / open-alpha funnels, actor detail, an event export and an AI insight scaffold. * **Creator rollups (signed-in owners):** `GET /creator/rollup`, `GET /creator/worlds/:worldId/rollup` and `GET /creator/homes/:homeId/rollup`. Not done yet: the Unreal adapter (the plugin marks `analytics.track` as needing a correction because its platform is hard-coded to web) and a mobile-native adapter. ## Deferred from v1 [#deferred-from-v1] * Full custom analytics query builder. * Public user-level analytics. * Raw creator access to user timelines. * Native mobile SDK beyond the wrapper bridge unless native-only events require it. * Unreal dashboard parity. Unreal only needs event submission first. * Voice analytics or moderation analytics beyond event hooks. * Real-time warehouse streaming unless raw storage and rollups prove insufficient. # Authentication & Identity (/docs/platform-api/authentication) Every player has **one HELIX identity**. There is no per-world signup: when a player opens a world, they're already signed in (or they play instantly as a guest and can upgrade later). Your world never handles passwords β€” it receives a verified identity from the platform. ## The launch flow [#the-launch-flow] When a player opens a world, the HELIX shell asks the backend for a short-lived, world-scoped **world session token** (`POST /api/v1/instant-worlds/:worldId/session`) and hands it to the SDK in the `helix:init` handshake. It is not the player's platform access token: it only carries the scopes the world's manifest declares. The SDK exposes the player from it. ```ts const { embedded, world, user } = await Helix.init(); // no arguments; the shell supplies the session if (Helix.auth.isAuthenticated()) { const me = await Helix.auth.getUser(); // { id, username, displayName } } ``` ```cpp // The generated Unreal service mirrors Helix.auth. Requests are objects you dispatch. UHelixAuthService* Auth = UHelixSdkServices::GetAuthService(this); UHelixAuthGetUserRequest* Request = Auth->GetUser(); Request->OnFinished.AddDynamic(this, &AMyActor::HandleUser); // UFUNCTION void HandleUser(UHelixRequest*) Request->Dispatch(); // In HandleUser: Cast(R)->HasResult() / GetResult() ``` When a player later **joins a multiplayer room**, the backend mints a separate, short-lived **room credential** (a JWT with a different audience, so a world session token can't be replayed to join a room). The room verifies it before admitting the player. You don't handle either token by hand: the SDK and shell wire them up. ## `Helix.auth` (Web SDK) [#helixauth-web-sdk] `Helix.init()` resolves with `{ embedded, world, user, runtimeFidelity, customizerSubject, customizerSource }`. When the world is opened outside the shell, `embedded` is `false` and identity returns `null`. ## Guests [#guests] A visitor who opens your link can play **immediately** as a guest. In the Web SDK a guest has no session: `isAuthenticated()` is `false` and `getUser()` resolves `null`. Guests can still join multiplayer rooms, because `multiplayer` is the only scope the backend grants a guest session; account-bound scopes (profile, voice, camera) are never minted for one. Call `requestLogin()` to let a guest sign in without reloading the world. Eligible new accounts get a one-time starter grant of [LIX Credits](/docs/platform-api/lix-economy#lix-credits), which pay for platform services. ## Permissions [#permissions] A world declares the scopes it needs in the `permissions` array of its manifest (`helix.json`). The known scopes are `auth.profile`, `multiplayer`, `voice.room`, `voice.proximity` and `camera.capture`. The session the platform mints carries the declared scopes intersected with that list; there is no per-player consent prompt. Request only what you use: a room join needs `multiplayer`, and a route that requires a scope the session lacks answers `401 Insufficient world scopes`. HELIX is an adults platform. Content rating gates discovery platform-wide, and the platform enforces it; your world doesn't implement age checks. See [Content rating](/docs/platform-api/content-rating). ## Reference [#reference] * Web SDK β†’ [`Helix.auth`](/docs/web-sdk/api) * REST β†’ `GET /api/v1/users/me` (the signed-in account, with a platform access token) # Collectibles (/docs/platform-api/collectibles) A **collectible** is a special kind of universal item: it exists in a **fixed supply** and each copy a player owns is **serialized** (numbered `#1…#N`). Copies are first issued through a creator's **distribution** until the supply runs out, then trade **peer-to-peer** between players for LIX. Nothing mints a copy beyond the supply. A world can't register a collectible as a world product. It can do two things: * **Primary drop.** Sell or give away copies through a distribution (`Helix.marketplace.purchaseDistributionKey(key)` or `purchaseDistribution(id)`). Each settled purchase issues one serialized copy; once the supply is exhausted the drop is no longer offered. * **Resale.** Feature one exact secondary listing with `Helix.marketplace.purchaseListing(listingId)`. A listing is one serial, so the quantity is always 1, and only active fixed-price listings (or offer listings that allow instant buy) can be bought this way. ## Template vs instance [#template-vs-instance] A collectible has two layers β€” the **template** (the design + its supply) and the **instances** (the individual numbered copies players own). ### `Collectible` (template) [#collectible-template] ### `CollectibleInstance` (one owned copy) [#collectibleinstance-one-owned-copy] ## How worlds use collectibles [#how-worlds-use-collectibles] Because a collectible (when `isUniversal`) is a universal item, everything on the [inventory API](/docs/platform-api/inventory) works: ```ts // Gate content on owning a collectible β€” same call as any item. if (await Helix.inventory.hasItem('founders_badge_001')) showFoundersLounge(); ``` To let a player **acquire** one, buy from a primary drop or feature a resale listing: ```ts // primary drop registered for this world const drop = await Helix.marketplace.purchaseDistributionKey('founders-badge'); // or a resale: browse, then buy one exact listing const [listing] = await Helix.marketplace.getListings({ category: 'collectible', limit: 10 }); if (listing?.listingId) { const result = await Helix.marketplace.purchaseListing(listing.listingId); // result.serialNumbers, result.resaleEligibleAt } ``` A resale pays the seller minus the platform fee and the creator's royalty; see [Fees & payouts](/docs/platform-api/lix-economy#fees--payouts-platform-policy). ## Collectible vs ordinary item vs world product [#collectible-vs-ordinary-item-vs-world-product] **A standard item sold at a creator's price β†’ a distribution.** **A world-defined pass, currency or consumable β†’ world product.** **Scarce + serialized + resold between players β†’ collectible**, issued through a distribution and resold through listings. ## Reference [#reference] * Items & inventory β†’ [Inventory](/docs/platform-api/inventory) * Economy & marketplace β†’ [LIX & Economy](/docs/platform-api/lix-economy#the-marketplace) # Content rating & maturity (/docs/platform-api/content-rating) HELIX is an adults-allowed platform with **higher creative freedom than most**, which only works with a clear maturity model. Every universal item and every world carries a **content rating**, declared by its creator and enforced platform-wide for discovery. ## The ratings [#the-ratings] `contentRating` is what the **creator declares**. `contentRatingStatus` is where **moderation** stands on that declaration: `pending_review` β†’ `approved` / `flagged` / `rejected`. Discovery uses the declared rating; enforcement can tighten it after review. ## Declaring a rating (publish-time attestation) [#declaring-a-rating-publish-time-attestation] A creator must **attest** to the rating when publishing β€” it isn't inferred. The items publish pipeline rejects a publish that doesn't carry the attestation, and records when + what was declared (attestation version `ugc-maturity-self-rating-v1`). Any value other than the four ratings is a `400`. ## How it gates [#how-it-gates] * **Discovery** β€” listings are filtered to the viewer's maturity cap: their account setting (default `everyone`; anonymous visitors are `everyone`). A `maxContentMaturity` query parameter can only narrow that cap, never widen it. * **The platform ceiling is `mature`.** No age verification exists on the platform yet, so no viewer's cap can reach `adult`: `adult`-rated content is listed to nobody except its own creator. The ceiling rises when real age verification ships. * **Worlds inherit a floor** β€” when a world is published, its rating must be at least the highest rating declared by the Vault assets it uses. A lower declaration is raised, and the assets that caused it are reported. Undeclared (`unrated`) assets never raise the floor. Creator content is reviewed and can be reported by the community. Repeated intentional misrating can flag or permanently ban the creator account. Rate to the strongest content actually present. ## In the SDK [#in-the-sdk] Ratings are platform-enforced β€” worlds don't compute them. An item's `contentRating` and `contentRatingStatus` are part of its [schema](/docs/platform-api/inventory#the-universal-item); discovery filtering happens on the server, not in world code. ## Reference [#reference] * Item schema β†’ [Inventory](/docs/platform-api/inventory#the-universal-item) * Economy β†’ [LIX & Economy](/docs/platform-api/lix-economy) # Data Store (/docs/platform-api/data-store) `Helix.dataStore` is HELIX's **durable** key–value store, scoped per world. It's the right place for anything that must outlive an instance: player progress, settings, world state. If you know Roblox, this is **DataStore**. Data Store is **durable and authoritative** β€” the source of truth. [Memory Store](/docs/platform-api/memory-store) (planned, not shipped) is the volatile, always-expiring counterpart. Leaderboards and matchmaking already ship as `Helix.leaderboard` and `Helix.matchmaking`. When in doubt about where data belongs, ask: *"would losing this be a bug?"* If yes, it's Data Store. ## API [#api] A rejected call throws a `HelixRequestError` whose `code` says why: `'rate-limited'`, `'version-conflict'`, or a key-policy rule such as `'session-sub-key'`. Outside the shell, reads resolve `null` (or no keys) and writes are no-ops. ## Who may write which key [#who-may-write-which-key] A player's own session may write **exactly one key: `player:`**. Put the whole save in that one JSON document. Every other key is written by the game server (the room) or creator tooling, and a session write anywhere else is refused with a `403` naming the rule. | Key | Written by | | ------------------------------------------------------------- | ------------------------------------------------------- | | `player:` | that player's session Β· the room Β· creator tooling | | `player::*` | the room Β· creator tooling | | `world`, `world:*` | the room Β· creator tooling | | `mp:*` | the room (persisted multiplayer vars) Β· creator tooling | | `devices:` | any visitor in the world Β· the room Β· creator tooling | | `leaderboard:` | the leaderboard routes (`Helix.leaderboard`) | | `helix:*` | the platform only | | `mail:*`, `trade:*`, `guild:*`, `party:*`, `index:*`, `log:*` | reserved | | anything else | the room Β· creator tooling | ## Example [#example] ```ts type Save = { level: number; coins: number }; const { user } = await Helix.init(); if (user) { const key = `player:${user.id}`; // load on join const save = (await Helix.dataStore.get(key)) ?? { level: 1, coins: 0 }; // save on change save.level += 1; await Helix.dataStore.set(key, save); } ``` ## Semantics & guarantees [#semantics--guarantees] * **Per-world namespace.** Keys are isolated to your world; you can't read another world's data. * **Authoritative.** Writes are durable once `set` resolves. * **Write from the server for value-bearing data.** Persisting currency/inventory-adjacent state belongs to server-authoritative code β€” never let a client write its own balance. See the [golden rule](/docs/introduction/how-helix-works#server-authority--the-golden-rule). * **No direct database access.** Creator code reaches persistence only through this API, never a raw DB connection. Two instances of the same world can run at once. For counters or state edited by many players, read with `getEntry` and pass its `version` back as `expectedVersion`, so a conflicting write is rejected with `'version-conflict'` instead of clobbering. For counters many players bump (community totals, votes, boss HP), the game server uses the atomic increment route (`POST /api/v1/data-store/:key/increment`); a player session can't. ## Limits & quotas [#limits--quotas] Data Store is **free for every world** β€” no subscription required. Generous per-world quotas keep one world from degrading the service (or running up the bill); they're modelled on Roblox DataStore norms and can be raised for your world if you outgrow them: Exceeding a storage quota fails the write with a clear error; exceeding the write rate returns **`RateLimited`** β€” back off and retry. Reads aren't rate-limited at the API. Batch writes where you can, and keep hot, high-frequency counters in the room rather than writing them every tick. A player save should be event-driven (on level up, on purchase), not a heartbeat β€” 30/min is plenty for that. The quotas above govern the **`Helix.dataStore` API**. Declared multiplayer state β€” vars marked `persistent: true` in the world manifest β€” is a separate system with its own, much smaller **publish-time** ceilings: **8 KB** per player's persisted document (`persistedPlayerBytes`), **32 KB** per persisted room document (`persistedRoomBytes`), and at most **6** persisted room documents (`persistentRoomKeys`). Those are static worst-case caps computed from the declared types and enforced by the manifest validator when a build is published β€” deliberately far under the 256 KB value limit here, because persisted documents flush through the same shared write budget. Hitting a manifest ceiling is a publish-time declaration problem, not a Data Store quota β€” and raising a Data Store quota won't move it. ## Reference [#reference] * Web SDK β†’ [`Helix.dataStore`](/docs/web-sdk/api) * REST β†’ `GET/PUT/DELETE /api/v1/data-store/:key`, `POST /api/v1/data-store/:key/increment` and `GET /api/v1/data-store?prefix=` β€” the world is identified by the **world-session token**, never a path parameter, so a world can only ever reach its own namespace. * Guide β†’ [Persist player data](/docs/guides/persist-player-data) # Platform API overview (/docs/platform-api) The Platform API is the part of HELIX that has nothing to do with rendering: identity, economy, storage, inventory and social. The Web SDK defines it, and the Unreal plugin mirrors the same method names from a generated contract table. Not every method is available on Unreal yet; the [feature matrix](/docs/feature-matrix) lists the gaps. These pages describe the **semantics**: what each capability does, what the server guarantees, and what the error cases are. Each Platform API page is **language-neutral**. Code samples use the runtime tabs you've seen β€” pick your runtime once and it sticks. The Web (TS) signature is canonical; other runtimes mirror it. ## The surface [#the-surface] ## The `Helix` namespace [#the-helix-namespace] On the Web runtime the platform is reached through a single `Helix` object. These members ship in `@hypersoniclabs/helix-sdk` today: ```ts Helix.auth Helix.avatar Helix.emotes Helix.locomotion Helix.world Helix.multiplayer Helix.voice Helix.party Helix.matchmaking Helix.listen Helix.wallet Helix.inventory Helix.marketplace Helix.purchases Helix.dataStore Helix.leaderboard Helix.achievements Helix.analytics Helix.presence Helix.notify Helix.prompts Helix.device Helix.camera Helix.materials Helix.customizer Helix.trial Helix.debug ``` plus the top-level `Helix.init()`, `Helix.getSessionToken()`, `Helix.hasLeft()` and `Helix.onLeave()`. The exact signatures are in the generated [Web SDK API reference](/docs/web-sdk/api). The contract also specifies `Helix.memoryStore`, `Helix.social`, `Helix.chat`, `Helix.moderation`, `Helix.ui` and `Helix.assets`. Those are **not shipped yet**; their pages are marked as planned. Initialize once at startup. `init()` takes no arguments, because the shell supplies the session: ```ts const { embedded, world, user } = await Helix.init(); // also on the result: runtimeFidelity, customizerSubject, customizerSource ``` ## Two rules that run through everything [#two-rules-that-run-through-everything] Anything affecting **inventory, currency, or ownership** is server-authoritative. Clients are trusted only for visuals and local gameplay. See the [Universal Item Contract](/docs/platform-api/inventory#execution-tiers). **Data Store** is durable and authoritative, the source of truth. **Memory Store** (planned) is volatile, fast, and always requires a TTL, never the source of truth. Don't reach for one when you mean the other. {/* docs-check: planned-api β€” the paragraph above names the unshipped namespaces explicitly */} # Live wearable and Unreal contract (/docs/platform-api/interactive-items-live-contract) # Live wearable and Unreal contract [#live-wearable-and-unreal-contract] This page records the **currently deployed** Universal Item transport behavior. It was audited on July 29, 2026 against the helix3 API and validator worker. The broader [Interactive Universal Items handoff](/docs/platform-api/interactive-items) remains the executable target for shared behavior, authoritative state, runtime adapters, and default-on scrubbed Vault publication. Those target features are not implied by the live upload API described here. ## Item-first upload [#item-first-upload] Create one product, upload each source rendition, finalize each source asset, then wait for every rendition the runtime may consume to become ready: 1. `POST /api/v1/universal-items` once. 2. PUT the web GLB and every Unreal file to the returned presigned URLs. 3. Finalize the web asset once and the Unreal asset once. 4. Poll the item until the required assets report `status: "ready"`. 5. Download only from the public URLs in the read response. One dual-format wearable has **one Universal Item ID and two creator-uploaded source asset rows**: | Source row | Shape | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rendition: "web"` | One GLB declared with `filename`, `mimeType`, and `sizeBytes`. | | `rendition: "unreal"` | Cooked packaged outputβ€”normally the matching `.pak`, `.utoc`, and `.ucas` filesβ€”declared through `files[]`, plus `engineVersion` and `targetPlatforms`. | Do not create separate items for web and Unreal. A generated `thumbnail` asset row may appear asynchronously after processing. It is a derived rendition, not a third source upload, so consumers must not assume the final `assets` array has exactly two rows. Wearables also require the live Character Creator fields: * `kind: "wearable"` * `usage: "wear"` * one valid `slot`, such as `Cosmetic.Slot.Clothing.Top` * `genders: ["male"]`, `["female"]`, or `["male", "female"]` The generic asset DTO can represent mixed fields, but the canonical integration shape keeps web and Unreal on separate rows. Unreal-only engine and platform claims cannot truthfully live on the web row. ## Finalize and readiness [#finalize-and-readiness] Create returns presigned PUT URLs without waiting for bytes. Finalize operates on one **asset**, not one file. It checks every Unreal file that declared a SHA-256, runs the applicable moderation gate, records `metadata.pipeline.finalizedAt`, and moves the asset from `awaiting_upload` to `pending`. Finalize means verified and queued; it does not mean usable. ```text awaiting_upload β†’ pending β†’ processing β†’ ready β†˜ rejected | failed ``` The CDN URLs are allocated before upload. During the deployed probe, all three uploaded objects were downloadable while both source assets still reported `awaiting_upload`. Never load or mount a rendition merely because its URL exists or returns HTTP 200. Require `asset.status === "ready"`. Fresh deployed requests returned **HTTP 201** from both asset finalize calls. The deployed OpenAPI document currently describes the same success response as **HTTP 200**. This is an unresolved contract discrepancy, tracked in backend task **CH-1162**. Until that task settles the stable status code, record 201 as observed runtime behavior and 200 as the published OpenAPI metadata; neither response code makes an asset ready. ## Read and select renditions [#read-and-select-renditions] The public item read returns the item and all of its current asset facts. Runtimes own selection: * Web may use only a ready `web` rendition. * Unreal should prefer a ready `unreal` rendition whose engine version matches the running client and whose `targetPlatforms` includes `Win64`. * Unreal may fall back to a ready web GLB when the client supports that path. * A `thumbnail` row is presentation metadata, never a runtime replacement for either source. `cdnUrl` and `files[].url` are public CDN URLs. `uploadUrl` is temporary and appears only in create/update responses. The public URLs are durable and deterministic, but they are not immutable Vault versions. `PATCH /:itemId/assets/:assetId` can re-declare files and overwrite the same object key. Invalidate caches when status or build metadata changes. Pinned immutable URLs belong to the target Vault version contract. ## Live Unreal metadata and integrity [#live-unreal-metadata-and-integrity] | Field | Deployed behavior | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `engineVersion` | The Unreal version used to cook the package and the runtime compatibility version consumers must match. It is stored per Unreal asset; `x-engine-version` wins over the body and omission defaults to `5.7`. HELIX currently targets Unreal 5.7, but 5.7 is a HELIX target/defaultβ€”not a requirement of the `.pak`/`.utoc`/`.ucas` format or transport API. | | `targetPlatforms` | Current Unreal runtime support is **Win64 only**. New Unreal assets must declare `["Win64"]`; all other target values are unsupported. | | `files[].sha256` | Optional, normalized to lowercase. When supplied, finalize reads the object, rejects missing or mismatched bytes with HTTP 422 `sha256_mismatch`, and corrects stored size to the measured length. | | Asset revision | Not available. There is no immutable revision, build ID, release note, or asset history on a Universal Item asset. | HELIX does not currently support native Unreal packages for any target other than Win64. Cook, publish, select, and mount the `.pak`/`.utoc`/`.ucas` set for Win64, and keep `targetPlatforms: ["Win64"]` on the asset. Unreal 5.5 packages are not compatible with the current HELIX 5.7 runtime. Keep an existing 5.5 package row labeled `engineVersion: "5.5"`; changing only its metadata does not change the cooked bytes. Re-cook the package with Unreal 5.7, publish the new `.pak`/`.utoc`/`.ucas` set with `engineVersion: "5.7"`, and wait for that asset to become ready before selecting it. Explicit patch values such as `5.7.1` are accepted when they reflect the exact engine used for the cook. Upload cooked packaged output, not loose Content Browser `.uasset` files. For an Io Store build, keep the matching `.pak`, `.utoc`, and `.ucas` files together and send a SHA-256 for **every** file. The live platform validates the envelopeβ€”file-key syntax, the 512-file cap, engine/platform syntax, and optional checksumsβ€”but it does not parse or mount the package, prove the declared `engineVersion` can load it, validate cook compatibility or dependencies, require a manifest, enforce a complete expected file set, or validate Unreal naming conventions. Unreal binaries are treated as non-visual and skipped by media moderation. The current multipart endpoint, `POST /api/v1/universal-items/upload`, is web-only. Publish both formats through the presigned JSON create flow, or add the Unreal source row afterward with `POST /:itemId/assets`. ## Live catalogue mode versus target Vault publication [#live-catalogue-mode-versus-target-vault-publication] The live `libraryContribution` field contributes a public, free Universal Item to the catalogue. It is **not** the target scrubbed Vault-definition publication. The backend has `universal_items.vault_asset_id` and immutable Vault versions, but Universal Item asset finalize does not yet create or link that definition automatically. The target remains: * one immutable definition shared by Web and Unreal presentation adapters; * separate owned, placement, session, and private state partitions; * one authoritative function/event choke point with validation, permissions, idempotency, capabilities, and replication filtering; * a first-party `ItemInteractionAbility` for abstract input, sockets, animation, hands, camera, and movement channels; * default-on publication of a scrubbed definition to Vault, respecting the creator preference and an explicit per-publish opt-out. The publish transform keeps manifests, clean defaults, functions/events, sockets, logic, compatibility, and pinned assets. It removes owner/inventory identifiers, live owned values, placement/world identifiers and transforms, bindings, occupants, scores, ammunition, cooldowns, timers, private values, runtime handles, and session state. Installing creates fresh IDs and revision-zero state. None of this target runtime/package surface is shipped by the rendition API today. ## Verification and sources [#verification-and-sources] The deployed transport round trip was run against a dual-format test item (id `807b0662-5fdb-4a59-b61d-c25e254d7c74`). It returned ready web and Unreal source assets, matching downloaded SHA-256 values, and a generated thumbnail row after processing. That fixture proves the deployed byte-integrity flow only; it does not prove a cooked package mounts. The sendable test below requires real `.pak`, `.utoc`, and `.ucas` cooking outputs for that compatibility check. * [Sendable Unreal test](/docs/platform-api/unreal-wearable-test) # Interactive Universal Items (/docs/platform-api/interactive-items) # Interactive Universal Items [#interactive-universal-items] An Interactive Universal Item is a portable, ownable object whose definition includes its assets, behavior, interaction surface, and state schema. A shower, pinball machine, weapon, vehicle, or appliance is published once, works in many worlds, and keeps the same public functions and events while each owned copy and world placement keeps its own state. The product/storage seams below are live, and the prototype proves the interaction model. The canonical package, production item runtime, SDK surface, and Web/Unreal adapters described after the status table are the contract to implement. They are **not shipped APIs yet**. For the deployed upload/finalize/read behavior, use the [live wearable and Unreal contract](/docs/platform-api/interactive-items-live-contract). To run the same dual-format probe against real cooked `.pak`, `.utoc`, and `.ucas` package outputs, send the [Unreal wearable test](/docs/platform-api/unreal-wearable-test) to the integration engineer. ## Current live surface versus target [#current-live-surface-versus-target] | Surface | Current live or proven | Production work still required | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Product and ownership | `universal_items` is the product; every `user_inventory_items` row is an owned instance with JSON state. | Validate owned state against the item definition and add revisioned writes. | | Placement | Home placements already store an inventory-instance link, transform, placement state, and metadata. | Apply the same placement overlay contract to every World/Instance runtime. | | Vault | Vault assets have durable IDs, immutable numeric versions, compatibility, typed metadata, and usage tracking. A Universal Item can reference one through `vault_asset_id`. | Publish the item definition and dependency-complete package through this path for every reusable item. | | Default publication | `users.vault_auto_publish_generated_assets` is `true` by default and generation respects the preference. | Make Universal Item finalization use the same default-on policy and accept an explicit per-publish opt-out. | | Character system | The humanoid runtime has abilities, channel arbitration, named sockets, clip registration, and layered animation. | Ship a first-party `ItemInteractionAbility` and permit safe runtime clip/layer contribution. | | Interactive behavior | The prototype proves typed vars, functions/events, sockets, declarative authority, replication filtering, capabilities, takeover input, cues, sandboxing, and call budgets. | Move the contract into a versioned shared schema and run it in the production Instance authority. | | Runtime parity | The prototype renders on the Web path. | Web and Unreal must consume one manifest and one authoritative state protocol; only presentation adapters differ. | The production boundary is deliberate: do not expose `Helix.items` publicly until the conformance {/* docs-check: ignore */} plan at the end passes on both runtimes. ## Non-negotiable model [#non-negotiable-model] Keep these layers separate: 1. **Definition** β€” immutable manifest, state schema/defaults, functions, events, sockets, logic, compatibility, and pinned Vault dependencies. 2. **Owned instance** β€” ownership plus portable persistent state, keyed by inventory instance ID. 3. **Placement** β€” world-local transform, approved overrides, bindings, and placement state. 4. **Session** β€” authoritative live state replicated to players in the current Instance. 5. **Private authority state** β€” secrets and anti-cheat data that are never sent to clients. Definitions reference durable Vault asset IDs and pinned versions. They never persist CDN URLs. Installation creates fresh owned/placement IDs and state from defaults; it never copies another owner's state. ## Canonical package [#canonical-package] The package extension is `.hlxi`. Version 1 remains a deterministic container, but production packages contain a small manifest/logic graph and pinned Vault references rather than duplicating large media blobs. ```text item.hlxi β”œβ”€β”€ item.manifest.json β”œβ”€β”€ logic/authority.hlx.json # engine-neutral, executed by Instance authority β”œβ”€β”€ logic/presentation.web.mjs # optional, untrusted Web presentation β”œβ”€β”€ logic/presentation.unreal.json # optional, declarative Unreal presentation mapping β”œβ”€β”€ migrations/1-to-2.hlx.json # deterministic state migration, when needed └── checksums.json # SHA-256 for every packaged file ``` The canonical identity has three independent versions: * `schemaVersion` β€” item-manifest contract version, beginning at `1.0`. * `definition.version` β€” semantic version of the item behavior and public surface. * Vault dependency `version` β€” immutable integer version of each referenced Vault asset. The published bundle and every dependency are content-addressed. A world may follow the latest definition for authoring, but a published World Build pins exact item and asset versions. ## Manifest v1 [#manifest-v1] This is the normative shape. JSON Schema should be generated from these types and published at `/schemas/interactive-item-manifest/1.0`. ```ts type JsonSchema = Record; type StatePartition = 'owned' | 'placement' | 'session' | 'private'; type Replication = 'none' | 'owner' | 'instance'; type ItemAuthority = 'platform' | 'instance'; type ItemAuth = 'anyone' | 'owner' | 'world-admin' | 'linked-item' | 'authority'; type VaultRef = { assetId: string; // vault_assets.id UUID; never a URL version: number; // immutable vault_asset_versions.version kind: | 'prop' | 'character' | 'animation' | 'audio' | 'texture' | 'material' | 'environment' | 'terrain' | 'vfx' | 'decal' | 'sky' | 'gaussian_splat' | 'scene' | 'unreal_package'; role: string; // model.primary, animation.reload, audio.fire, ... required: boolean; }; type ItemStateField = { schema: JsonSchema; default: unknown; authority: ItemAuthority; persistence: 'portable' | 'placement' | 'transient' | 'private'; replication: Replication; writableBy: 'authority' | 'owner' | 'world-admin'; }; type ItemFunction = { input: JsonSchema; output?: JsonSchema; auth: ItemAuth; mutates: StatePartition[]; idempotency: 'required' | 'optional' | 'forbidden'; rateLimit?: { calls: number; perSeconds: number }; doc: string; }; type ItemEvent = { payload: JsonSchema; delivery: 'reliable' | 'ephemeral'; audience: Replication; }; type InteractiveItemManifestV1 = { schemaVersion: '1.0'; definition: { id: `helix:item:${string}`; version: `${number}.${number}.${number}`; title: string; description: string; kind: 'home_item' | 'prop' | 'wearable' | 'weapon' | 'vehicle' | 'appliance'; implements: string[]; // e.g. interactive_item, weapon.ranged }; compatibility: { engines: Array<'web' | 'unreal'>; systems: string[]; // e.g. humanoid-character@^0.2.8 skeleton?: 'helix-humanoid@1'; }; assets: Record; physical: { sizeMeters: [number, number, number]; pivot: 'footprint-center'; collision: { mode: 'bounds' | 'vault-ref' | 'none'; asset?: string }; }; sockets: Record; state: Record>; functions: Record; events: Record; cues: Record; character?: { ability: 'item-interaction'; channels: Record; input: Partial>; }; capabilities: string[]; logic: { authoritative: { format: 'helix-declarative-v1'; entry: string }; presentation?: Partial>; }; authoring: { portableOverridePaths: string[]; worldBindings: Array<{ event: string; target: string; function: string }>; }; publication: { vault: 'default' | 'never'; // default obeys the creator preference; never is explicit opt-out primaryAsset: string; // key in assets }; migrations?: Array<{ from: string; to: string; entry: string }>; }; ``` An interactive item is not a new Vault media kind. Its primary render resource retains its real kindβ€”usually `prop`β€”and is searchable with `implements: ["interactive_item", ...]`. The item package is attached as the target `definition.item_manifest` related artifact. Implementing that role requires widening the current material-only related-artifact gate. This preserves the distinction between a reusable resource and an ownable product while making interactive behavior discoverable. ### Vault dependency contracts [#vault-dependency-contracts] Item dependencies use the settled typed Vault formats: | Kind | Canonical descriptor or artifact | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `material` | `.helix-material.json` β€” `application/vnd.helix.material+json`; maps use `source.albedo`, `source.normal`, `source.roughness`, and optional `runtime.ktx2.albedo`, `runtime.ktx2.normal`, `runtime.ktx2.roughness` roles | | `scene` | `.helix-scene.json` β€” `application/vnd.helix.scene+json`; v1 nodes reference a Vault asset UUID plus position/rotation/scale, with optional runtime bundles | | `sky` | `.helix-sky.json` β€” `application/vnd.helix.sky+json`; pins HDR/EXR source and generated lighting/backdrop renditions | | `terrain` | `.helix-terrain.json` β€” `application/vnd.helix.terrain+json` | | `vfx` | `.helix-vfx.json` β€” `application/vnd.helix.vfx+json`; adapters map the portable graph to Web effects or Niagara | | `gaussian_splat` | `.spz` β€” `model/vnd.gaussian-splat.spz`; `gaussianSplat.scope` is `object`, `fragment`, or `environment` | The current related-artifact input is `Array<{ role, artifact: { filename, buffer, contentType? } }>` and the resolved read shape is `{ role, url, mimeType, sizeBytes, checksumSha256 }`. Immutable version metadata stores `{ role, storageKey, storageTarget, mimeType, sizeBytes, checksumSha256 }`. Materials currently identify as `pbr-material-preview`, report preview-beta PBR/tileability capability, and declare either `portable-source-only` or `portable-source-plus-ktx2`. Every final-mesh item thumbnail also carries `metadata.canonicalThumbnail`: source `validated-final-mesh`, renderer version `final-mesh-textured-v1`, transparent background, black UI composite, camera preset, and source SHA-256. Provider/reference images remain provenance-only and must never become the canonical item thumbnail. ## State, revision, and idempotency [#state-revision-and-idempotency] | Partition | Stored on | Authority | Persistence | Replication | Publish behavior | | ----------- | ------------------ | --------------------------------- | --------------------------- | --------------------------------- | ---------------------------------------------------------- | | `owned` | inventory instance | platform/Instance command service | Travels with the owned copy | Owner or Instance, field-by-field | Values reset to manifest defaults | | `placement` | world placement | Instance authority | Stays with that placement | Instance, field-by-field | Values removed; allowlisted authoring overrides may remain | | `session` | live Instance | Instance authority | Never persisted | Instance or owner | Always removed | | `private` | authority store | platform/Instance authority | As explicitly required | Never | Always removed, including defaults marked secret | Every state partition has an integer revision. Every mutation carries expected revisions and returns the revisions it committed. A conflict returns `RevisionConflict`; clients refetch and retry from the new snapshot instead of overwriting concurrent changes. Every function call carries a caller-generated `idempotencyKey`. The authority stores the terminal result for functions marked `required`; the same caller/key/function/instance tuple returns the recorded result without running twice. This is mandatory for durability, inventory, currency, rewards, score finalization, ammunition consumption, and ownership effects. ```ts type ItemCallRequest = { instanceId: string; function: string; args: T; idempotencyKey: string; expectedRevisions?: Partial>; }; type ItemCallResult = { callId: string; value: T; revisions: Record; emitted: Array<{ sequence: number; event: string; payload: unknown }>; }; ``` Hydration order is fixed: definition defaults β†’ owned state β†’ placement state/overrides β†’ session defaults β†’ `onSpawn`. No client renders a default state and then replaces it with persistent state. ## Lifecycle [#lifecycle] The authoritative runtime owns this sequence: ```text validate β†’ install β†’ hydrate β†’ place β†’ spawn β†’ activate ↕ calls / input / events / ticks deactivate β†’ despawn β†’ unplace transfer: flush owned state β†’ revoke old authority β†’ attach new owner upgrade: snapshot β†’ deterministic migration β†’ validate β†’ commit atomically ``` Only `onSpawn`, `onCall`, `onInput`, `onTick`, and `onDespawn` execute item logic. `install`, `place`, `transfer`, publication, and migration are platform commands. Logic cannot forge ownership or bypass them. ## SDK and runtime surface [#sdk-and-runtime-surface] The public Web API is asynchronous and authority-backed: ```ts interface HelixItems { inspect(instanceId: string): Promise; getActions(instanceId: string): Promise; call(request: ItemCallRequest): Promise>; subscribe(instanceId: string, listener: (change: ItemChange) => void): () => void; bind(placementId: string, binding: WorldBinding): Promise; unbind(placementId: string, bindingId: string): Promise; } ``` The authoritative module sees a narrow, capability-checked context: ```ts interface ItemRuntimeContext { readonly self: { definitionId: string; inventoryInstanceId: string; placementId: string }; readonly actor: { userId: string; characterId: string } | null; readonly state: { get(partition: StatePartition, key: string): T; patch(partition: StatePartition, values: Record): void; }; emit(event: string, payload: unknown): void; cue(name: string): void; callItem(targetInstanceId: string, fn: string, args: unknown): Promise; character: { occupy(socket: string, options?: { animation?: string; channels?: string[] }): void; release(): void; play(animation: string, options?: { loop?: boolean; layer?: string }): void; attach(asset: string, socket: string): void; }; world: { invoke(bindingTarget: string, fn: string, payload: unknown): void; raycast?(origin: number[], direction: number[], options: { maxDistance: number }): { targetId: string } | null; applyDamage?(targetId: string, amount: number, cause: string): void; consumeInventory?(inventoryInstanceId: string, quantity: number): Promise; }; now(): number; // authority clock after(seconds: number, fn: string, args?: unknown): void; } interface InteractiveItemModule { onSpawn(ctx: ItemRuntimeContext): void | Promise; onCall(ctx: ItemRuntimeContext, fn: string, args: unknown): unknown | Promise; onInput?(ctx: ItemRuntimeContext, frame: ItemInputFrame): void; onTick?(ctx: ItemRuntimeContext, dt: number): void; onDespawn?(ctx: ItemRuntimeContext): void | Promise; } ``` The Unreal plugin exposes the same operations as latent Blueprint/C++ calls and receives the same snapshot/event envelopes. It does not run a separate item protocol. ## Character integration [#character-integration] Items never bind keys or manipulate the character controller directly. They request abstract actions and character channels through the first-party `ItemInteractionAbility`. * `movement` and `camera-control` are exclusive and priority-arbitrated. * `upper-body`, animation layers, hands, sockets, and input actions are shared. * `character.occupy("seat")` moves the character to an item socket; this is intentionally distinct from attaching a prop to a character bone. * Actor clips target `helix-humanoid@1`, are validated/grounded during publication, and are registered under the item definition/version namespace. * A weapon may own aim, upper-body, hands, and fire/reload actions while locomotion continues. * A shower or pinball takeover may temporarily own movement and camera until release. Late placement must be supported: adding an item after the ability manager starts must still register its clips and contribute its animation layer safely. ## Authority and security [#authority-and-security] One command choke point validates every call: 1. Resolve immutable item definition and instance ownership. 2. Validate function name and JSON-Schema arguments. 3. Check `auth`, world capability grants, and content/runtime compatibility. 4. Check expected revisions and idempotency. 5. Enforce depth **8**, **200 calls per item per second**, declared function limits, memory, and execution-time budgets. 6. Execute engine-neutral authority logic in a sandbox with no network, filesystem, wall clock, randomness, or host references. 7. Validate each requested effect against capabilities. 8. Atomically commit state, idempotency result, and reliable events. 9. Replicate only fields and events the recipient may read. Presentation scripts can play cues and simulate non-valuable motion. They cannot write authoritative state, award score, apply damage, consume inventory, grant items, or emit reliable events. Action labels and enabled states are UI hints; the call gate always re-checks authority. ## Default-on Vault publication [#default-on-vault-publication] Universal Item finalization publishes to Vault unless either: * the creator preference `vaultAutoPublishGeneratedAssets` is false; or * this publish explicitly sets `publication.vault` to `never`. `default` means β€œobey the preference,” not β€œforce public.” The server records whether publication was automatic or explicitly disabled. Current generated-job responses expose `vaultAssetId` and `vaultAutoPublish: "published" | "disabled"`; the per-item `publication.vault` override remains part of this target contract, not a live field. The deterministic publish transform is: ```ts function toVaultDefinition(source: AuthoringItem): PublishedItemDefinition { return { definition: canonicalize(source.definition), compatibility: source.compatibility, assets: pinVaultVersions(source.assets), physical: source.physical, sockets: source.sockets, stateSchema: source.stateSchema, stateDefaults: resetToPublishableDefaults(source.stateSchema), functions: source.functions, events: source.events, cues: source.cues, character: source.character, capabilities: source.capabilities, logic: verifiedLogic(source.logic), authoringDefaults: pick(source.placementOverrides, source.authoring.portableOverridePaths), migrations: source.migrations, }; } ``` It preserves: * definition metadata, schema, clean defaults, logic, functions/events, sockets, capabilities; * pinned Vault dependencies and runtime renditions; * animations, VFX, audio, materials, collision, and compatibility; * only explicitly allowlisted portable authoring overrides. It removes or resets: * owned state values, durability, scores, contents, ammunition, preferences, and usage counters; * placement IDs, transforms, world IDs, local links, binding IDs, and non-allowlisted overrides; * occupants, cooldowns, playback positions, active balls/projectiles, timers, and session handles; * owner/user/inventory-instance IDs and acquisition/commerce history; * private state, anti-cheat seeds, secrets, credentials, runtime handles, and idempotency records. Installing the result creates new IDs and revision `0` for every partition. A changed reusable definition becomes a new immutable version or derivative; it never mutates existing pinned worlds. ## Runtime adapters [#runtime-adapters] The Instance authority executes one manifest and one logic graph for every runtime. Adapters only translate presentation: | Contract | Web adapter | Unreal adapter | | ------------ | ----------------------------------------------- | --------------------------------------- | | Geometry | Resolve Web GLB rendition | Resolve cooked Unreal rendition | | Animation | Register `helix-humanoid@1` GLB clips | Retarget/use matching UE skeleton clips | | VFX | Map portable graph to Web renderer | Map the same graph to Niagara | | Audio | Web Audio spatial source | Unreal Audio Component | | State/events | Shared snapshots, revisions, and event sequence | Same protocol | | Authority | Instance service | Same Instance service | An engine-specific presentation feature may degrade or be absent. The declared functions, authority rules, state, and reliable events may not diverge. ## Example: shower booth [#example-shower-booth] The package owns the booth behavior. A world may bind `finished` to cleanliness, a quest, or lighting without changing the shower. ```json { "schemaVersion": "1.0", "definition": { "id": "helix:item:shower-booth", "version": "1.0.0", "title": "Shower Booth", "description": "Portable interactive shower with door, water, temperature and actor poses.", "kind": "appliance", "implements": ["interactive_item", "appliance.shower"] }, "compatibility": { "engines": ["web", "unreal"], "systems": ["humanoid-character@^0.2.8"], "skeleton": "helix-humanoid@1" }, "assets": { "model": { "assetId": "10000000-0000-4000-8000-000000000001", "version": 3, "kind": "prop", "role": "model.primary", "required": true }, "wash": { "assetId": "10000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.wash", "required": true }, "rinse": { "assetId": "10000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.rinse", "required": true }, "water": { "assetId": "10000000-0000-4000-8000-000000000004", "version": 2, "kind": "audio", "role": "audio.water_loop", "required": true }, "waterVfx": { "assetId": "10000000-0000-4000-8000-000000000005", "version": 1, "kind": "vfx", "role": "vfx.water", "required": true }, "doorAudio": { "assetId": "10000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.door", "required": true } }, "physical": { "sizeMeters": [1.1, 2.18, 1.1], "pivot": "footprint-center", "collision": { "mode": "bounds" } }, "sockets": { "stall": { "type": "stand", "position": [0, 0, 0] }, "approach": { "type": "stand", "position": [0, 0, 1.1], "rotation": [0, 3.14159, 0] }, "camera": { "type": "camera", "position": [0, 1.65, 0.2] }, "showerhead": { "type": "effect", "position": [0, 1.9, -0.1] }, "door": { "type": "hand", "position": [-0.43, 1.1, 0.49] } }, "state": { "owned": { "preferredTemp": { "schema": { "type": "number", "minimum": 18, "maximum": 45 }, "default": 38, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "owner" }, "minutesUsed": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "authority" } }, "placement": { "spray": { "schema": { "enum": ["rain", "jet", "mist"] }, "default": "rain", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" } }, "session": { "doorOpen": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "running": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "occupant": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" } }, "private": {} }, "functions": { "open": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Open the door when unoccupied." }, "close": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Close the door." }, "enter": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Occupy the booth." }, "start": { "input": { "type": "object", "properties": { "temperature": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "anyone", "mutates": ["owned", "session"], "idempotency": "required", "doc": "Start water and the wash pose." }, "stop": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Stop water and release the actor." }, "setTemperature": { "input": { "type": "object", "required": ["value"], "properties": { "value": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned"], "idempotency": "optional", "doc": "Save the owner's preferred temperature." } }, "events": { "doorChanged": { "payload": { "type": "object", "required": ["open"], "properties": { "open": { "type": "boolean" } } }, "delivery": "reliable", "audience": "instance" }, "started": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" }, "finished": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" } }, "cues": { "doorOpen": { "itemAnimation": "door.open", "audio": "doorAudio", "at": "door" }, "doorClose": { "itemAnimation": "door.close", "audio": "doorAudio", "at": "door" }, "waterOn": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": true }, "waterOff": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": false }, "washing": { "actorAnimation": "wash", "loop": true } }, "character": { "ability": "item-interaction", "channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } }, "input": { "cancel": "stop" } }, "capabilities": ["actor.occupy", "actor.animate", "audio.play", "vfx.play"], "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } }, "authoring": { "portableOverridePaths": ["placement.spray"], "worldBindings": [{ "event": "finished", "target": "world", "function": "cleanliness.applyShower" }] }, "publication": { "vault": "default", "primaryAsset": "model" } } ``` ```ts export const shower: InteractiveItemModule = { onSpawn(ctx) { if (ctx.state.get('session', 'running')) ctx.state.patch('session', { running: false, occupant: null }); }, async onCall(ctx, fn, args: any) { const occupant = ctx.state.get('session', 'occupant'); if (fn === 'open' && occupant === null) { ctx.state.patch('session', { doorOpen: true }); ctx.cue('doorOpen'); ctx.emit('doorChanged', { open: true }); } else if (fn === 'close') { ctx.state.patch('session', { doorOpen: false }); ctx.cue('doorClose'); ctx.emit('doorChanged', { open: false }); } else if (fn === 'enter' && occupant === null && ctx.actor) { ctx.state.patch('session', { occupant: ctx.actor.userId, doorOpen: false }); ctx.character.occupy('stall', { channels: ['movement', 'camera-control'] }); } else if (fn === 'start' && ctx.actor?.userId === occupant) { ctx.state.patch('session', { running: true }); ctx.state.patch('owned', { preferredTemp: args.temperature }); ctx.character.play('wash', { loop: true }); ctx.cue('waterOn'); ctx.emit('started', { actorId: occupant }); ctx.after(9, 'stop'); } else if (fn === 'stop' && (ctx.actor === null || ctx.actor?.userId === occupant)) { const used = ctx.state.get('owned', 'minutesUsed'); ctx.state.patch('owned', { minutesUsed: used + 1 }); ctx.state.patch('session', { running: false, occupant: null, doorOpen: true }); ctx.cue('waterOff'); ctx.character.release(); ctx.emit('finished', { actorId: occupant }); } else if (fn === 'setTemperature') { ctx.state.patch('owned', { preferredTemp: args.value }); } } }; ``` Publishing preserves the clean `preferredTemp: 38` default and allowlisted `spray` authoring value. It removes the current owner's preference and usage count, placement transform, occupant, running water, door/session state, world bindings added outside the manifest, and every runtime handle. ## Example: networked pinball machine [#example-networked-pinball-machine] Physics may be predicted by the playing client, but the Instance authority awards score. Spectators receive replicated ball transforms; the machine's own high-score table is portable owned state. ```json { "schemaVersion": "1.0", "definition": { "id": "helix:item:pinball-neon", "version": "1.0.0", "title": "Neon Pinball", "description": "Networked pinball machine with per-copy high scores.", "kind": "appliance", "implements": ["interactive_item", "arcade.pinball"] }, "compatibility": { "engines": ["web", "unreal"], "systems": ["humanoid-character@^0.2.8"], "skeleton": "helix-humanoid@1" }, "assets": { "model": { "assetId": "20000000-0000-4000-8000-000000000001", "version": 5, "kind": "prop", "role": "model.primary", "required": true }, "playPose": { "assetId": "20000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.play", "required": true }, "arcadeAudio": { "assetId": "20000000-0000-4000-8000-000000000003", "version": 2, "kind": "audio", "role": "audio.cues", "required": true }, "scoreVfx": { "assetId": "20000000-0000-4000-8000-000000000004", "version": 1, "kind": "vfx", "role": "vfx.score", "required": true } }, "physical": { "sizeMeters": [0.8, 1.9, 1.7], "pivot": "footprint-center", "collision": { "mode": "bounds" } }, "sockets": { "player": { "type": "stand", "position": [0, 0, 1.2], "rotation": [0, 3.14159, 0] }, "camera": { "type": "camera", "position": [0, 1.75, 2.2], "rotation": [0.22, 3.14159, 0] }, "table": { "type": "effect", "position": [0, 1.0, 0] } }, "state": { "owned": { "highScores": { "schema": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["userId", "score"], "properties": { "userId": { "type": "string" }, "score": { "type": "integer", "minimum": 0 } } } }, "default": [], "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "authority" } }, "placement": { "difficulty": { "schema": { "enum": ["easy", "standard", "hard"] }, "default": "standard", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" } }, "session": { "playerId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "score": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "balls": { "schema": { "type": "integer", "minimum": 0, "maximum": 3 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "ballPose": { "schema": { "type": ["object", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" } }, "private": { "gameEpoch": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" } } }, "functions": { "play": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Claim the machine and begin a three-ball game." }, "input": { "input": { "type": "object", "required": ["action", "phase"], "properties": { "action": { "enum": ["leftFlipper", "rightFlipper", "launch", "nudge"] }, "phase": { "enum": ["pressed", "released"] } }, "additionalProperties": false }, "auth": "anyone", "mutates": [], "idempotency": "forbidden", "rateLimit": { "calls": 120, "perSeconds": 1 }, "doc": "Submit transient controls from the current player." }, "reportBall": { "input": { "type": "object", "required": ["pose"], "properties": { "pose": { "type": "object" } } }, "auth": "anyone", "mutates": ["session"], "idempotency": "forbidden", "rateLimit": { "calls": 30, "perSeconds": 1 }, "doc": "Relay a predicted pose; never awards score." }, "scoreTarget": { "input": { "type": "object", "required": ["targetId", "gameEpoch"], "properties": { "targetId": { "type": "string" }, "gameEpoch": { "type": "integer", "minimum": 1 } } }, "auth": "authority", "mutates": ["session"], "idempotency": "required", "doc": "Authority-verified target hit." }, "finishBall": { "input": { "type": "object", "additionalProperties": false }, "auth": "authority", "mutates": ["owned", "session", "private"], "idempotency": "required", "doc": "Advance or end the game and commit a record." }, "leave": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Release the machine." } }, "events": { "started": { "payload": { "type": "object", "required": ["playerId"], "properties": { "playerId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" }, "scored": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" }, "gameOver": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" }, "newRecord": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" } }, "cues": { "playing": { "actorAnimation": "playPose", "loop": true }, "score": { "audio": "arcadeAudio", "vfx": "scoreVfx", "at": "table" } }, "character": { "ability": "item-interaction", "channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } }, "input": { "look": "aim", "primary": "leftFlipper", "secondary": "rightFlipper", "tertiary": "launch", "cancel": "leave" } }, "capabilities": ["actor.occupy", "actor.animate", "physics.predict", "audio.play", "vfx.play"], "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } }, "authoring": { "portableOverridePaths": ["placement.difficulty"], "worldBindings": [{ "event": "newRecord", "target": "world", "function": "arcade.onRecord" }] }, "publication": { "vault": "default", "primaryAsset": "model" } } ``` ```ts export const pinball: InteractiveItemModule = { onSpawn(ctx) { ctx.state.patch('session', { playerId: null, score: 0, balls: 0, ballPose: null }); ctx.state.patch('private', { gameEpoch: 0 }); }, async onCall(ctx, fn, args: any) { const playerId = ctx.state.get('session', 'playerId'); if (fn === 'play' && playerId === null && ctx.actor) { const gameEpoch = ctx.state.get('private', 'gameEpoch') + 1; ctx.state.patch('session', { playerId: ctx.actor.userId, score: 0, balls: 3 }); ctx.state.patch('private', { gameEpoch }); ctx.character.occupy('player', { animation: 'playPose', channels: ['movement', 'camera-control'] }); ctx.emit('started', { playerId: ctx.actor.userId }); } else if (fn === 'input' && ctx.actor?.userId === playerId) { ctx.world.invoke('physics', 'pinball.input', args); } else if (fn === 'reportBall' && ctx.actor?.userId === playerId) { ctx.state.patch('session', { ballPose: args.pose }); } else if (fn === 'scoreTarget') { if (args.gameEpoch !== ctx.state.get('private', 'gameEpoch')) return; const points: Record = { bumper: 10, ramp: 25, jackpot: 100 }; const score = ctx.state.get('session', 'score') + (points[args.targetId] ?? 0); ctx.state.patch('session', { score }); ctx.cue('score'); ctx.emit('scored', { score }); } else if (fn === 'finishBall') { const balls = ctx.state.get('session', 'balls'); if (balls > 1) { ctx.state.patch('session', { balls: balls - 1, ballPose: null }); return; } const score = ctx.state.get('session', 'score'); const highScores = ctx.state.get>('owned', 'highScores'); const next = [...highScores, { userId: playerId!, score }] .sort((a, b) => b.score - a.score).slice(0, 10); const record = next[0]?.userId === playerId && next[0]?.score === score; ctx.state.patch('owned', { highScores: next }); ctx.state.patch('session', { playerId: null, balls: 0, ballPose: null }); ctx.emit('gameOver', { score }); if (record) ctx.emit('newRecord', { score }); ctx.character.release(); } else if (fn === 'leave' && ctx.actor?.userId === playerId) { ctx.state.patch('session', { playerId: null, ballPose: null }); ctx.character.release(); } } }; ``` Publishing keeps an empty `highScores` default and the allowlisted difficulty. It removes the actual leaderboard from the source copy, player, score, balls, ball pose, nonce, camera takeover, transform, and world-added bindings. A platform-wide leaderboard is a separate service, not owned-item state. ## Example: universal weapon [#example-universal-weapon] The weapon composes with locomotion through upper-body/hands channels. Damage and ammunition are authoritative world/platform operations; recoil, muzzle flash, shell motion, and hit markers are presentation. ```json { "schemaVersion": "1.0", "definition": { "id": "helix:item:carbine-mk1", "version": "1.0.0", "title": "Carbine MK1", "description": "Portable ranged weapon with shared equip, aim, fire and reload behavior.", "kind": "weapon", "implements": ["interactive_item", "weapon.ranged"] }, "compatibility": { "engines": ["web", "unreal"], "systems": ["humanoid-character@^0.2.8"], "skeleton": "helix-humanoid@1" }, "assets": { "model": { "assetId": "30000000-0000-4000-8000-000000000001", "version": 4, "kind": "prop", "role": "model.primary", "required": true }, "aim": { "assetId": "30000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.aim", "required": true }, "fire": { "assetId": "30000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.fire", "required": true }, "reload": { "assetId": "30000000-0000-4000-8000-000000000004", "version": 2, "kind": "animation", "role": "animation.reload", "required": true }, "shot": { "assetId": "30000000-0000-4000-8000-000000000005", "version": 1, "kind": "audio", "role": "audio.fire", "required": true }, "reloadAudio": { "assetId": "30000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.reload", "required": true }, "muzzleFlash": { "assetId": "30000000-0000-4000-8000-000000000007", "version": 1, "kind": "vfx", "role": "vfx.muzzle", "required": true } }, "physical": { "sizeMeters": [0.18, 0.34, 0.86], "pivot": "footprint-center", "collision": { "mode": "bounds" } }, "sockets": { "grip": { "type": "hand", "position": [0, 0, 0], "node": "Grip_R" }, "support": { "type": "hand", "position": [0, 0, 0], "node": "Grip_L" }, "muzzle": { "type": "effect", "position": [0, 0.05, -0.83], "node": "Muzzle" } }, "state": { "owned": { "durability": { "schema": { "type": "number", "minimum": 0, "maximum": 100 }, "default": 100, "authority": "platform", "persistence": "portable", "replication": "owner", "writableBy": "authority" }, "finish": { "schema": { "type": "string", "maxLength": 64 }, "default": "factory", "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "owner" } }, "placement": {}, "session": { "wielderId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }, "magazine": { "schema": { "type": "integer", "minimum": 0, "maximum": 30 }, "default": 30, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" }, "reserve": { "schema": { "type": "integer", "minimum": 0, "maximum": 180 }, "default": 90, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" }, "cooldownUntil": { "schema": { "type": "number", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" } }, "private": { "shotSequence": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" } } }, "functions": { "equip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Attach and activate weapon channels." }, "unequip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Release channels and attachment." }, "aim": { "input": { "type": "object", "required": ["active"], "properties": { "active": { "type": "boolean" } }, "additionalProperties": false }, "auth": "owner", "mutates": [], "idempotency": "forbidden", "doc": "Set aim presentation state." }, "fire": { "input": { "type": "object", "required": ["origin", "direction"], "properties": { "origin": { "type": "array", "minItems": 3, "maxItems": 3 }, "direction": { "type": "array", "minItems": 3, "maxItems": 3 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned", "session", "private"], "idempotency": "required", "rateLimit": { "calls": 10, "perSeconds": 1 }, "doc": "Validate a shot, consume ammunition, and apply an authoritative hit." }, "reload": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Move reserve ammunition into the magazine." } }, "events": { "equipped": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" }, "fired": { "payload": { "type": "object", "required": ["actorId", "sequence"], "properties": { "actorId": { "type": "string" }, "sequence": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" }, "hit": { "payload": { "type": "object", "required": ["targetId", "damage"], "properties": { "targetId": { "type": "string" }, "damage": { "type": "number" } } }, "delivery": "reliable", "audience": "instance" }, "depleted": { "payload": { "type": "object", "additionalProperties": false }, "delivery": "reliable", "audience": "owner" } }, "cues": { "aim": { "actorAnimation": "aim", "loop": true }, "fire": { "actorAnimation": "fire", "audio": "shot", "vfx": "muzzleFlash", "at": "muzzle" }, "reload": { "actorAnimation": "reload", "audio": "reloadAudio" } }, "character": { "ability": "item-interaction", "channels": { "upper-body": { "priority": 30, "mode": "shared" }, "hands": { "priority": 30, "mode": "shared" }, "camera-control": { "priority": 10, "mode": "shared" } }, "input": { "look": "aim", "primary": "fire", "secondary": "aim", "tertiary": "reload" } }, "capabilities": ["actor.animate", "actor.attach", "combat.raycast", "combat.damage", "audio.play", "vfx.play"], "logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } }, "authoring": { "portableOverridePaths": ["owned.finish"], "worldBindings": [{ "event": "hit", "target": "world", "function": "combat.onWeaponHit" }] }, "publication": { "vault": "default", "primaryAsset": "model" } } ``` ```ts export const weapon: InteractiveItemModule = { onSpawn(ctx) { ctx.state.patch('session', { wielderId: null, magazine: 30, reserve: 90, cooldownUntil: 0 }); ctx.state.patch('private', { shotSequence: 0 }); }, async onCall(ctx, fn, args: any) { const wielderId = ctx.state.get('session', 'wielderId'); if (fn === 'equip' && ctx.actor) { ctx.state.patch('session', { wielderId: ctx.actor.userId }); ctx.character.attach('model', 'hand_r.grip'); ctx.emit('equipped', { actorId: ctx.actor.userId }); } else if (fn === 'fire' && ctx.actor?.userId === wielderId) { const now = ctx.now(); const magazine = ctx.state.get('session', 'magazine'); if (magazine <= 0) return ctx.emit('depleted', {}); if (now < ctx.state.get('session', 'cooldownUntil')) return; const sequence = ctx.state.get('private', 'shotSequence') + 1; const durability = ctx.state.get('owned', 'durability'); ctx.state.patch('session', { magazine: magazine - 1, cooldownUntil: now + 100 }); ctx.state.patch('owned', { durability: Math.max(0, durability - 0.02) }); ctx.state.patch('private', { shotSequence: sequence }); const hit = ctx.world.raycast?.(args.origin, args.direction, { maxDistance: 250 }); if (hit) { ctx.world.applyDamage?.(hit.targetId, 20, ctx.self.definitionId); ctx.emit('hit', { targetId: hit.targetId, damage: 20 }); } ctx.cue('fire'); ctx.emit('fired', { actorId: wielderId, sequence }); } else if (fn === 'aim' && ctx.actor?.userId === wielderId) { if (args.active) ctx.character.play('aim', { loop: true, layer: 'upper-body' }); } else if (fn === 'reload' && ctx.actor?.userId === wielderId) { const magazine = ctx.state.get('session', 'magazine'); const reserve = ctx.state.get('session', 'reserve'); const moved = Math.min(30 - magazine, reserve); ctx.state.patch('session', { magazine: magazine + moved, reserve: reserve - moved }); ctx.cue('reload'); } else if (fn === 'unequip' && ctx.actor?.userId === wielderId) { ctx.state.patch('session', { wielderId: null }); ctx.character.release(); } } }; ``` Publishing keeps durability/finish schema and clean defaults, but removes the owner's current durability/finish unless the finish is deliberately passed as an allowlisted derivative override. It always removes ammunition, cooldown, wielder, shot sequence, hit data, runtime attachments, and world combat bindings added outside the package. ## Migration and compatibility [#migration-and-compatibility] * Definitions and Vault versions are immutable. Editing creates a new version. * Patch versions may change presentation without changing state/function/event schemas. * Minor versions may add optional fields, functions, or events. * Major versions require deterministic migrations and cannot silently upgrade pinned World Builds. * Migrations receive only the old validated state document and declared defaults. They have no I/O, clock, randomness, user lookup, or runtime context. * Migration is atomic: validate source β†’ transform β†’ validate target β†’ commit all partitions and revisions, or keep the old version untouched. * An unsupported runtime rendition fails installation before the world starts; it never falls back silently to an incompatible asset. ## Ordered implementation and conformance plan [#ordered-implementation-and-conformance-plan] 1. Publish `interactive-item-manifest@1.0` types, JSON Schema, validator, canonicalizer, package reader/writer, and golden fixtures for these three examples. 2. Add the item definition version and pinned Vault related-artifact records without duplicating media blobs. 3. Add owned/placement/session/private revision envelopes and idempotent item call storage. 4. Implement the authoritative command service, capability gate, replication filter, reliable event sequence, call-depth/rate/memory/time budgets, and deterministic timers. 5. Port the proven declarative interpreter; reject authority scripts for cross-engine definitions. 6. Ship `ItemInteractionAbility`, late clip/layer registration, sockets, abstract input, and channel arbitration in the character runtime. 7. Implement Web and Unreal adapters against the same recorded protocol fixtures. 8. Make Universal Item finalization run the exact Vault publish transform by default, with preference and per-publish opt-out tests. 9. Add SDK/CLI/MCP operations for inspect, place, configure, bind, call, validate, publish, install, upgrade, and usage tracking; MCP remains an instruction layer over the CLI. 10. Run a two-client, two-runtime conformance suite: publish each example, install it into another creator's world, exercise functions/events/state/replication, reconnect, transfer/replace, migrate, republish, and prove source state did not leak. The acceptance test is simple: one creator publishes an interactive object, another creator or agent places it in a different world, and its standard behavior survives while its typed events and functions safely participate in that world's custom logic. ## Source map [#source-map] Current claims in this handoff are grounded in: * Backend owned state: `helix-backend-api/src/database/entities/user-inventory-item.entity.ts` * Backend placement state: `helix-backend-api/src/database/entities/home-item-placement.entity.ts` * Product-to-Vault reference: `helix-backend-api/src/database/entities/universal-item.entity.ts` * Default-on preference: `helix-backend-api/src/database/entities/user.entity.ts` * Vault resources/versions/contracts: `helix-backend-api/src/vault` and `src/database/entities/vault-asset*.ts` * Character abilities/channels/sockets: `helix-web-engine-new/docs/character-architecture.md` * Proven item manifest/runtime: `helixgame.com/prototypes/interactive-items/src/runtime` * Proven examples and character bridge findings: `helixgame.com/prototypes/interactive-items/items` and `SPEC-CHANGES.md` # Inventory & Items (/docs/platform-api/inventory) Items in HELIX are **universal**: an item a player owns exists across worlds and renders on every runtime (each item ships per-runtime renditions β€” e.g. a web GLB and an Unreal static mesh). `Helix.inventory` is how a world reads and uses what a player owns. ## Items, products, listings & collectibles β€” what's what [#items-products-listings--collectibles--whats-what] A **universal item** is the *thing a player owns*. A **distribution** and a **world product** are two different ways to *sell* something. A **collectible** is a scarce, serialized edition of an item that resells peer-to-peer. They're easy to conflate β€” this table is the difference. | Concept | What it is | Bought with | Price comes from | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------- | | **Universal item** | The asset itself β€” placeable / wearable / tradeable, owned across every world & runtime. The noun everything else points at. An item id is never a purchase reference. | β€” (you *own* it) | its distributions | | **Distribution** | A creator's primary offer of an item: price in LIX or Coins, per-player cap, claim cap, optional window. Registered for one world (a `dkey`) or public. | `Helix.marketplace.purchaseDistributionKey(key)` / `purchaseDistribution(id)` | the distribution, server-side | | **World product** | A purchasable a *world* registers (Roblox "developer product"): passes, world currencies and consumables. Never grants a universal item. | `Helix.marketplace.purchaseProduct(productKey)` | the product's `priceLix`, server-side | | **Collectible** | A scarce, *serialized* edition of an item. First issued through a distribution up to its fixed supply, then resold **peer-to-peer** on the Marketplace. | a distribution, then a listing (`purchaseListing(listingId)`) | the distribution, then the seller's listing | ## The universal item [#the-universal-item] The shape every owned item, marketplace listing, and grant target shares. Items are created through the publish pipeline (CLI / MCP / `POST /api/v1/universal-items`) and read by worlds via `Helix.inventory`. ### Field enums [#field-enums] `wearable` Β· `home_item` Β· `home_shell` Β· `avatar` Β· `emote` Β· `prop` Β· `bundle` Β· `achievement` Β· `add_on` Β· `vehicle` Β· `weapon` **Wearable:** `jackets` `tops` `bottoms` `footwear` `accessories` `hair` `makeup` `body` β€” **Home:** `furniture` `decor` `lighting` `structure` `utility` β€” **Interactive:** `interactable` `device` `vehicle` `weapon` β€” plus `wearable`, `collectible` and `misc`. `wear` (equip on an avatar) Β· `place` (in a home/world) Β· `hold` Β· `consume` Β· `display` Β· `inhabit` (a home shell, always) Β· `install` (an add-on, always) `draft` Β· `published` (live + visible) Β· `delisted` Β· `suspended` Β· `hidden` Β· `rejected` Β· `archived` `grant` Β· `purchase` Β· `gift` Β· `collectible` Β· `world_reward` Β· `admin` The same owned item works in every world and renders on every runtime via its per-runtime **assets**. `hasItem(id)` is true regardless of which world or creator asks β€” that's what makes VIP / season passes work across creators. ## Reading inventory [#reading-inventory] Outside the shell (a world opened directly) every read resolves empty: `false`, `0` or `[]`. ```ts if (await Helix.inventory.hasItem('vip_hat_001')) { await Helix.inventory.equipItem('vip_hat_001'); } ``` ## Execution tiers [#execution-tiers] The most important thing to understand about items is **who is allowed to run their logic**. HELIX reasons about it in four tiers. They are a design model, not a field: no item carries a tier value. **If it affects inventory, currency, or ownership, the server is authoritative. If it only affects visuals or local gameplay, the client can be trusted.** Tier 3 logic runs on the server, full stop. ## Consuming an item (Tier 3) [#consuming-an-item-tier-3] The Web SDK's `Helix.inventory` is read and equip only: `hasItem`, `getQuantity`, `getMyItems`, `equipItem`. Consuming a universal item is a designed contract, not a shipped route: neither the SDK nor the backend exposes it today. For world-local balances use `Helix.purchases.consume`. Consumption is server-authoritative and **idempotent** β€” a retried network call can't double-spend: ```ts // designed contract only: not in the SDK or the backend today const result = await Helix.inventory.consumeItem({ itemId: 'health_potion_001', quantity: 1, idempotencyKey: requestId, }); // result.mintResults describes any items/currency atomically granted in return ``` ## Item behaviour [#item-behaviour] Interactive behaviour (state, interactions, abilities with `onEquip` / `onUnequip`) is specified in [Interactive Universal Items](/docs/platform-api/interactive-items), which separates what is live today from the target contract. ## Reference [#reference] * Web SDK β†’ [`Helix.inventory`](/docs/web-sdk/api) * Server authority β†’ [How HELIX works](/docs/introduction/how-helix-works#server-authority--the-golden-rule) * Economy β†’ [LIX & Economy](/docs/platform-api/lix-economy) # LIX & Economy (/docs/platform-api/lix-economy) HELIX has a real, platform-wide economy. Two currencies: * **Coins** β€” soft currency, earned and spent across worlds. * **LIX** β€” hard currency, bought with real money. **100 LIX β‰ˆ $1** (bundles can discount). LIX pays for premium items, collectibles, platform services, and in-world purchases. Creator earnings land in the creator's withdrawable LIX balance; a cash-out flow is not part of the public API today. ### LIX Credits [#lix-credits] **LIX Credits** are a restricted kind of LIX, for example the one-time open-alpha starter grant. They pay for platform services (Dreamer, publish and mint fees, hosting, storage) but never for another creator's item or product, and they can't be transferred or cashed out. A purchase that would pay a creator only counts your other LIX, so a player holding only Credits gets `InsufficientFunds`. Every LIX-affecting action is settled on the server. Clients **request** purchases; they never grant currency or items. All in-world purchase charges deduct LIX server-side. Treat any client-reported balance or grant as untrusted. ## The wallet [#the-wallet] `Helix.wallet` reads balances. Purchases go through `Helix.marketplace` (below). ```ts const { lix, coins } = await Helix.wallet.getBalance(); ``` ## In-world purchases (IWP) [#in-world-purchases-iwp] To sell something, the **client requests** and the **server authorizes and settles**. The SDK persists an idempotency key per attempt so a retry can't double-charge, and the shell owns the Buy / Claim confirmation. ```ts // client: request a purchase of a product your world registered with key "golden-key" const result = await Helix.marketplace.purchaseProduct('golden-key'); if (result.completed) { // Granted, Claimed or AlreadyOwned: charged and fulfilled server-side } ``` `result.status` is one of `Pending`, `Granted`, `Claimed`, `AlreadyOwned`, `InsufficientFunds`, `MaxPerUserReached`, `ProductInactive`, `ProductNotFound`, `NotInWorld`, `Unauthorized`, `RateLimited`, `Cancelled`, `Failed`, `Timeout`, `NotFound` or `PriceChanged`. `completed` is the success convenience. After a reload or a dropped response, recover with `Helix.marketplace.getPurchase(idempotencyKey, ref)`. World code never grants or charges anything itself. A consumable is a world product with a currency grant that the world later spends with the server-atomic `Helix.purchases.consume`. See the [Inventory](/docs/platform-api/inventory) execution tiers and the [Sell an item for LIX](/docs/guides/sell-item-for-lix) guide. ### What a world can sell [#what-a-world-can-sell] A purchase names exactly one server-owned route. An item definition on its own is **not** a purchase reference: the server rejects `item:` refs and the SDK rejects raw item UUIDs. | Route | World call | Ref on the wire | Price and limits come from | | ------------------------------------------------------------- | -------------------------------------- | ------------------- | -------------------------- | | **World product** your world registered | `purchaseProduct(productKey)` | `pkey:` | the product | | **Distribution** registered for this world | `purchaseDistributionKey(key)` | `dkey:` | the distribution | | Any creator's active public **distribution** | `purchaseDistribution(distributionId)` | `distribution:` | the distribution | | One **collectible listing** (a single serial, secondary sale) | `purchaseListing(listingId)` | `listing:` | the seller's listing | A **distribution** is how a creator sells a universal item: a primary offer with its own price (LIX or Coins), per-player cap, claim cap and optional start/end window. A distribution or product priced `0` is a free **Claim** (still tracked and idempotent). `quantity` can be 1 to 100, except listings, which are always 1. | You want to… | Calls | | ----------------------------------------------------------------- | --------------------------------------------------------------------------- | | Run a brand store: products on pedestals, walk up and **Buy** | `purchaseDistributionKey` or `purchaseDistribution` on interact | | Hand out free souvenirs | a distribution priced `0`; the popup shows **Claim** | | Sell a world-defined product (a "Golden Key" that unlocks a door) | `purchaseProduct` β†’ gate on the pass in `purchases.getEntitlements()` | | Grant VIP / backstage access, even across other creators' worlds | `inventory.hasItem(vipPassItemId)` | | Sell a consumable (extra life, refill) and spend it later | `purchaseProduct` with a currency grant β†’ `purchases.consume(code, amount)` | | Feature a resale collectible | `marketplace.getListings` β†’ `purchaseListing(listingId)` | | Build your own confirm UI | `marketplace.getPurchaseContext(ref)` | | Show a LIX-aware UI (disable Buy when short) | `wallet.getBalance` + `onBalanceChanged` | Items players own are **placeable, tradeable, equippable** things β€” home dΓ©cor, wearables, collectibles. They are sold through a **distribution**, never a world product. Pure gameplay effects (an extra life, a speed boost, a VIP pass for this world) are **world product** purchases β€” they are not universal items. Players list and trade on the Marketplace; a world can feature one listing with `purchaseListing`. ## World products [#world-products] A **world product** is a purchasable a world registers β€” the Roblox "developer product" analog β€” for world-local effects: passes, named currency balances and consumables. It can't hand over a universal item: the server rejects a product with an `itemId` or an item grant and points you at a distribution instead. The price lives on the product **server-side**; world code names the product by its `key` (`purchaseProduct('golden-key')` sends `pkey:golden-key`), never by a price or a UUID. Register a product before prompting it (see [Sell an item for LIX](/docs/guides/sell-item-for-lix)). Fulfilment is recorded as the player's **entitlements** in this world, which `Helix.purchases` reads and spends: Limits per world: 512 products, 16 currency codes and 16 pass keys. Both settle through the **same** chargeβ†’grant core and return the same `PurchaseResult`. A **distribution** sells an existing [universal item](/docs/platform-api/inventory#the-universal-item) at the distribution's price and pays the **item's** creator. A **world product** is registered by your world, pays the **world's** creator, and is the only option for passes, world currencies and pure gameplay **consumables**. ### Managing products over HTTP [#managing-products-over-http] Registering and managing products is a **creator / server** action β€” there's no world-runtime SDK call for it (worlds only *prompt* purchases). Call these with the creator's session; only a world's owner may manage its products. | Method Β· path | Body | Returns | | -------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------- | | `POST /api/v1/iwp/products` | `{ worldId, key?, title, description?, priceLix, grants?, type?, maxPerUser? }` | the created product | | `PATCH /api/v1/iwp/products/:id` | `{ title?, description?, priceLix?, maxPerUser?, active? }` | the updated product | | `POST /api/v1/iwp/products/:id/active` | `{ active: boolean }` | the product | | `GET /api/v1/iwp/products/:id` | β€” | the product | | `GET /api/v1/iwp/creator/worlds/:worldId/products` | β€” | your world's products | ```bash curl -X POST "$API/api/v1/iwp/products" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"worldId":"…","key":"golden-key","title":"Golden Key","priceLix":250,"grants":[{"kind":"pass","passKey":"golden-key"}]}' ``` The **purchase** surface is shell-mediated (worlds call the SDK; the platform shell settles), but the endpoints exist for completeness β€” all require the **player's** session: | Method Β· path | Purpose | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `GET /api/v1/iwp/context?ref=&worldId=&quantity=` | product/listing + live balance + eligibility (for the popup) | | `POST /api/v1/iwp/purchase` | `{ ref, worldId?, idempotencyKey, quantity? }` β†’ a [`PurchaseResult`](/docs/web-sdk/api#purchaseresult) | | `GET /api/v1/iwp/purchase/:idempotencyKey` | status of a purchase (resume after a dropped connection) | `ref` is `pkey:`, `dkey:`, `distribution:` or `listing:`; anything else is `ProductNotFound`. The server always resolves price + payee itself β€” a client-supplied price is never trusted. A player is limited to 20 purchase attempts a minute. ## The marketplace [#the-marketplace] `Helix.marketplace` browses listings and initiates every in-world purchase. Rare collectibles trade **peer-to-peer** for LIX on the Marketplace; a world can feature a single listing with `purchaseListing`. These are distinct concepts that today share the `Helix.marketplace` namespace: * **In-world purchases** (`purchaseProduct`, `purchaseDistributionKey`, `purchaseDistribution`) are a **creator selling to a player** β€” a store/checkout. The analog is iOS **StoreKit** or Android **Play Billing** (and Roblox **developer products**): billing, not a storefront you browse. * **The Marketplace** (`getListings`, `purchaseListing`) is the **platform-wide, peer-to-peer exchange** for collectibles β€” players trading with players. `options` is `{ quantity?, idempotencyKey? }` (or a bare quantity number). Omit `idempotencyKey` and the SDK persists one for you. Outside the shell, purchases resolve `{ status: 'Unauthorized', completed: false }` and reads resolve empty or `null`. ## Fees & payouts (platform policy) [#fees--payouts-platform-policy] * **Collectible resales** carry a seller fee of 10% by default (5% on Plus, 2.5% on Gold, 0% on Diamond) plus a 5% royalty to the original creator when the seller isn't the creator. The buyer pays no surcharge. Rates are platform-owned, snapshotted on each trade, and creators can't override them. * **Publishing** an item can cost a one-off, non-refundable LIX fee by kind, unless the platform has free uploads switched on. Read the live state from `GET /api/v1/universal-items/publish-config` rather than assuming it. * LIX Credits can't pay a creator, and nothing in the public API cashes LIX out today. These are platform-enforced β€” you don't implement them. The figures are the backend's defaults and can be changed by platform configuration. Payment processors and ledger internals are never part of the public SDK. You call `Helix.wallet` and `Helix.marketplace`; HELIX handles settlement. ## Reference [#reference] * Web SDK β†’ [`Helix.wallet` / `Helix.marketplace` / `Helix.purchases`](/docs/web-sdk/api) * Guide β†’ [Sell an item for LIX](/docs/guides/sell-item-for-lix) # Memory Store (/docs/platform-api/memory-store) Memory Store (`Helix.memoryStore`) is part of the designed Platform API contract but is **not** in `@hypersoniclabs/helix-sdk` today, so the signatures below are a specification, not something you can call. The shipped surface is listed in the [Web SDK overview](/docs/web-sdk). {/* docs-check: planned-api */} `Helix.memoryStore` is HELIX's **volatile** store: fast, shared across every running instance of your world, and **always expiring**. It's for live state that's cheap to lose. If you know Roblox, this is **MemoryStore**. [Data Store](/docs/platform-api/data-store) (durable storage) is live. Ranked boards ship as `Helix.leaderboard` (`submit`, `top`) and queueing as `Helix.matchmaking` (`enqueue`, `cancel`, `status`, `onMatch`). For a hot counter, keep it in the room's state and flush it, or have the game server use the Data Store increment route. Every Memory Store entry **requires a TTL** and may vanish when it expires or under pressure. It has **no economic authority** β€” it cannot grant items or LIX. For anything that must survive, write to [Data Store](/docs/platform-api/data-store). ## API [#api] ## Live leaderboard example [#live-leaderboard-example] ```ts // record a score (atomic, ranked, shared across instances) const board = Helix.memoryStore.sortedMap('round-scores'); await board.set(playerId, score, 300); // 5-minute TTL // read the top 10 const top = await board.getRange({ limit: 10, descending: true }); ``` ## Hot counter example [#hot-counter-example] ```ts // count concurrent players in a zone without hammering Data Store const inZone = await Helix.memoryStore.increment(`zone:${zoneId}`, 1, 60); ``` ## When to use which [#when-to-use-which] | Need | Use | | -------------------------------------- | ------------------------------------------------------------- | | Player progress, settings, owned state | [Data Store](/docs/platform-api/data-store) | | Live leaderboard for a round | `Helix.leaderboard` today; Memory Store `sortedMap` (planned) | | Matchmaking / cross-instance handoff | `Helix.matchmaking` today; Memory Store `queue` (planned) | | Hot counter (players in a zone) | room state today; Memory Store `increment` (planned) | | Anything you'd be upset to lose | [Data Store](/docs/platform-api/data-store) | ## Reference [#reference] * Not in the Web SDK or the backend yet. Shipped neighbours: [`Helix.dataStore`, `Helix.leaderboard`, `Helix.matchmaking`](/docs/web-sdk/api). # Social & Presence (/docs/platform-api/social) The friend graph and chat (`Helix.social`, `Helix.chat`) are part of the designed Platform API contract but are **not** in `@hypersoniclabs/helix-sdk` today, so the signatures in the Planned section are a specification, not something you can call. Presence, parties and matchmaking do ship; they come first below. {/* docs-check: planned-api */} ## What ships today [#what-ships-today] Three social surfaces are in the Web SDK now: Friends and blocking exist as account routes on the backend (`/api/v1/users/me/friends`, `/api/v1/users/:id/friend-request`, `/api/v1/users/:id/block`, `/api/v1/users/me/friends/presence`), used by the HELIX site with the player's own account token. Worlds can't call them: the friend graph is not exposed to world code until `Helix.social` ships. ## Planned: `Helix.social` [#planned-helixsocial] `Helix.social` exposes the player's social graph: who their friends are, who's online, and how to pull them into your world. Because identity is platform-wide, friendships and presence work across every world automatically. ### Invite a friend into your world (planned) [#invite-a-friend-into-your-world-planned] ```ts const friends = await Helix.social.getFriends(); const online = []; for (const f of friends) { const p = await Helix.social.getPresence(f.id); if (p.online) online.push(f); } // pull an online friend into this instance if (online[0]) await Helix.social.inviteToInstance(online[0].id); ``` ### Friend presence (planned) [#friend-presence-planned] Presence tells you whether a friend is online and, when permitted, what world they're in β€” the basis for "join friend" buttons and for surfacing active sessions. Respect the player's privacy settings; the platform enforces visibility. Real-time chat (`Helix.chat`) is planned too. Spatial and proximity voice ships as `Helix.voice` (see the [guides](/docs/guides)). ## Reference [#reference] * Web SDK β†’ [`Helix.presence`, `Helix.party`, `Helix.matchmaking`](/docs/web-sdk/api) # Universal Avatar (/docs/platform-api/universal-avatar) Universal Avatar is the player-owned character layer that travels across HELIX runtimes. The same loadout should be readable by the Web game, the Web SDK, and the Unreal runtime, even when an item ships different renderable assets per runtime. This page is the shared contract for the first Character Creator implementation. ## Loadout model [#loadout-model] The platform stores one loadout per player: ```ts type AvatarBaseShape = 'male' | 'female' | 'custom'; type UniversalAvatarBody = | { mode: 'preset'; baseMeshId: 'helix:humanoid:male' | 'helix:humanoid:female'; } | { mode: 'customMesh'; avatarItemId: string; inventoryItemId: string; glbUrl: string | null; skeleton: string | null; }; type AvatarLoadout = { version: 1; body: UniversalAvatarBody; baseShape: AvatarBaseShape; customAvatar: EquippedAvatar; // { source: 'equipped' | 'auto' | 'default' | 'trial', itemId, glbUrl, skeleton, … } equippedSlots: Array<{ slot: string; // a Cosmetic.Slot.* tag itemId: string; inventoryItemId: string; socket?: string | null; inventoryItem: InventoryItem; }>; inventory: InventoryItem[]; updatedAt: string; }; ``` Presets reference humanoid meshes shipped with the runtime, so they need no fetch. `custom` uses an owned avatar item and points to its web-compatible GLB when available. Character body editing is locked while a custom full mesh is active. `baseShape` is **derived, not stored**: it is `custom` when a custom avatar is equipped and `male` otherwise. There is no female base mesh yet, so a patch with `baseShape: 'female'` is rejected with a `400`, and the preset body always resolves to `helix:humanoid:male`. Change the body through `customAvatarInventoryItemId`. ## Equipment slots [#equipment-slots] Avatar equipment is instance-based. Equip by inventory instance id, not universal item id, so duplicate owned copies can be selected independently. An item can only be equipped into the slot it declares. Slot keys are Character Creator tags (`Cosmetic.Slot.*`), the same vocabulary as an item's own `slot` field, and responses always return them. The older camelCase names are still accepted on a patch (deprecated) and translated one way. `headPreset`, `faceType` and `clothingPreset` are body presets with no tag and are stored as-is. | Legacy name | Slot tag | Web engine default socket | | ---------------- | ----------------------------------------- | ------------------------- | | `headPreset` | β€” (preset) | `head` | | `faceType` | β€” (preset) | `head` | | `eyebrows` | `Cosmetic.Slot.Appearance.Eyes.Eyebrows` | `head` | | `eyelashes` | `Cosmetic.Slot.Appearance.Eyes.Eyelashes` | `head` | | `irises` | `Cosmetic.Slot.Appearance.Eyes.Iris` | `head` | | `hair` | `Cosmetic.Slot.Appearance.Hair.Main` | `head` | | `clothingPreset` | β€” (preset) | `chest` | | `top` | `Cosmetic.Slot.Clothing.Top` | `chest` | | `outfit` | `Cosmetic.Slot.Clothing.Set` | `chest` | | `bottom` | `Cosmetic.Slot.Clothing.Bottoms` | `hips_l` | | `underwear` | `Cosmetic.Slot.Clothing.Underwear.Bottom` | `hips_l` | | `socks` | `Cosmetic.Slot.Clothing.Socks` | `hips_l` | | `shoes` | `Cosmetic.Slot.Clothing.Shoes` | `hips_l` | | `bag` | `Cosmetic.Slot.Clothing.Backpack` | `back` | | `hat` | `Cosmetic.Slot.Accessory.Head.Hat` | `head` | | `mask` | `Cosmetic.Slot.Accessory.Face.Mask` | `head` | | `eyewear` | `Cosmetic.Slot.Accessory.Face.Eyewear` | `head` | | `necklace` | `Cosmetic.Slot.Accessory.Neck.Necklace` | `chest` | | `nails` | `Cosmetic.Slot.Accessory.Hands.Nails` | `hand_r.grip` | | `gloves` | `Cosmetic.Slot.Accessory.Hands.Gloves` | `hand_r.grip` | The backend reports a slot's `socket` from the inventory instance's state, then its metadata, then the item's `properties.universalAvatar.socket`, and `null` otherwise; the web engine then falls back to the default socket above. ## Platform API [#platform-api] Read the current player's loadout: ```http GET /api/v1/universal-items/avatar/loadout/me Authorization: Bearer ``` Patch the current player's loadout: ```http PATCH /api/v1/universal-items/avatar/loadout/me Authorization: Bearer Content-Type: application/json { "customAvatarInventoryItemId": null, "equippedSlots": { "Cosmetic.Slot.Clothing.Top": "inventory-instance-id", "Cosmetic.Slot.Clothing.Shoes": null } } ``` `null` unequips a slot (or, for `customAvatarInventoryItemId`, clears the custom avatar). Unknown slots, an item equipped into a slot it doesn't declare, and non-wearable inventory instances are rejected. The old placeholder-seed route is retired and answers `410 Gone`. ## Web SDK and shell behavior [#web-sdk-and-shell-behavior] The Character Creator is a shell overlay, not a standalone destination. It can be raised from any home or world that allows Universal Avatars. Worlds can request the shell to open the creator and can read or patch loadouts: ```ts await Helix.avatar.openCreator(); const loadout = await Helix.avatar.getLoadout(); await Helix.avatar.updateLoadout({ equippedSlots: { bag: backpackInventoryInstanceId, }, }); const off = Helix.avatar.onAvatarChanged(async () => { applyAvatar(await Helix.avatar.getLoadout()); // the callback carries no payload: re-read }); ``` The shell remains authoritative for authenticated API calls. Direct local world runs may return an empty/default loadout until they are embedded in the HELIX shell. When `avatar.updateLoadout` succeeds, the shell emits `avatar-changed` and `equipment-changed` to the active world, which fire `onAvatarChanged`. For rendering, `Helix.avatar.getEquipped()` resolves the local player's configured avatar and `getEquippedForUser(userId)` another joined player's public one, for replicas. `projectResource` and `readResource` read authorized avatar bytes through the shell. ## Engine runtime [#engine-runtime] The Web engine uses a humanoid equipment service over the existing socket service: 1. Resolve the item slot. 2. Resolve the socket from `properties.universalAvatar.socket`, item metadata, or the default slot map. 3. Attach the item's renderable mesh to that socket. 4. Use a generated primitive placeholder when the item has no web-compatible mesh yet. Placeholder meshes must still follow the shared skeleton/socket contract. They are temporary renderables, not a separate ownership or equipment model. When a world uses the HELIX humanoid as the player character and Universal Avatars are enabled, the runtime should read `Helix.avatar.getLoadout()` during player setup and apply the returned loadout to the humanoid by default. Worlds can opt into additional scripting later, but the baseline humanoid path should not require custom per-world avatar code. ## Unreal compatibility [#unreal-compatibility] The Unreal runtime should consume the same loadout fields and slot ids. Runtime-specific mesh assets may differ, but slot names, inventory instance ids, base shape semantics, and custom-avatar lock behavior should not diverge. Today the Unreal plugin's `avatar.getEquipped`, `getEquippedForUser`, `openCreator` and `onAvatarChanged` are available; `getLoadout` and `updateLoadout` work only through the shell until the runtime token is accepted. # Unreal dual-format wearable test (/docs/platform-api/unreal-wearable-test) # Unreal dual-format wearable test [#unreal-dual-format-wearable-test] Send this page to the Unreal integration engineer. It tests the [live transport contract](/docs/platform-api/interactive-items-live-contract), not the target interactive-item runtime. Use `https://helix-backend-api-helix3.up.railway.app` with a helix3 Bearer token. The backend's Swagger UI is not public; the request and response shapes you need are on this page. ## 0. Cook and package the Unreal rendition [#0-cook-and-package-the-unreal-rendition] Package the wearable for the current Win64 runtime before publishing it. For an Unreal Io Store build, use the matching Win64 packaged outputs: ```text /Content/Paks/HelixWearable.pak /Content/Paks/HelixWearable.utoc /Content/Paks/HelixWearable.ucas ``` Do not upload loose Content Browser `.uasset` files. The package files are a set: publish the matching `.pak`, `.utoc`, and `.ucas` produced by the same Win64 cook. Record the exact Unreal version that performed the cook. That value becomes `engineVersion` and is how a consuming runtime decides whether the rendition is compatible. HELIX currently targets and defaults this field to Unreal **5.7**. That is a HELIX runtime target, not a requirement of the package extensions or upload format. Unreal 5.5 packages are not compatible with the HELIX 5.7 runtime. Do not relabel an existing 5.5 cook as 5.7. Re-cook with Unreal 5.7, publish the newly cooked package set, and keep the old row labeled 5.5. Explicit patch values such as `5.7.1` are accepted when they reflect the exact engine used for the cook. Cook and publish only a Win64 package. HELIX does not currently support native Unreal packages for other targets. Every Unreal asset in this test must declare `targetPlatforms: ["Win64"]`. Before creating the item, calculate the size and SHA-256 of all three files: ```bash wc -c HelixWearable.pak HelixWearable.utoc HelixWearable.ucas shasum -a 256 HelixWearable.pak HelixWearable.utoc HelixWearable.ucas ``` ## 1. Create one item with two source rows [#1-create-one-item-with-two-source-rows] Use **one Universal Item with two creator-uploaded source rows**: one `web` GLB and one `unreal` packaged build. Replace every angle-bracketed value below with the actual filename, byte count, or 64-character SHA-256 from the cook before sending the request. ```http POST /api/v1/universal-items Authorization: Bearer Content-Type: application/json ``` ```json { "slug": "ue-dual-wearable-", "title": "UE Dual Wearable Test", "description": "Private web + cooked Unreal package contract test", "kind": "wearable", "category": "jackets", "slot": "Cosmetic.Slot.Clothing.Top", "genders": ["male", "female"], "usage": "wear", "contentRating": "everyone", "contentRatingAttestationAccepted": true, "keepPersonal": true, "assets": [ { "rendition": "web", "filename": "wearable.glb", "mimeType": "model/gltf-binary", "sizeBytes": }, { "rendition": "unreal", "engineVersion": "5.7", "targetPlatforms": ["Win64"], "files": [ { "key": "Wearable/Win64/Content/Paks/HelixWearable.pak", "size": , "mimeType": "application/octet-stream", "sha256": "" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.utoc", "size": , "mimeType": "application/octet-stream", "sha256": "" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.ucas", "size": , "mimeType": "application/octet-stream", "sha256": "" } ] } ] } ``` Expected create result: HTTP 201; item `status` is `draft`; both source assets are `awaiting_upload`; the Unreal asset reads `engineVersion: "5.7"` and `targetPlatforms: ["Win64"]`. The response has one web `uploadUrl` and one `files[].uploadUrl` for each of the three package files. Select both source rows from `response.item.assets` by `rendition`; do not treat an asset row as the top-level response. Representative Unreal response shape: ```json { "item": { "assets": [ { "id": "", "rendition": "unreal", "status": "awaiting_upload", "engineVersion": "5.7", "targetPlatforms": ["Win64"], "files": [ { "key": "Wearable/Win64/Content/Paks/HelixWearable.pak", "size": , "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.pak", "uploadUrl": "" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.utoc", "size": , "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.utoc", "uploadUrl": "" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.ucas", "size": , "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.ucas", "uploadUrl": "" } ] } ] } } ``` If `x-engine-version` is also sent, it overrides the `engineVersion` value in the body. Prefer one truthful value unless explicitly testing that header behavior. ## 2. Upload the packaged outputs [#2-upload-the-packaged-outputs] There is no upload JSON after create. PUT each file's bytes to its own returned `uploadUrl`. Do not send the HELIX Bearer token to R2. ```http PUT ``` Repeat this request for the `.pak`, `.utoc`, and `.ucas` URLs: ```http PUT ``` Each PUT should return HTTP 200. ## 3. Finalize each source asset [#3-finalize-each-source-asset] Finalize each source asset once: ```http POST /api/v1/universal-items//assets//finalize Authorization: Bearer Content-Type: application/json {"sizeBytes": } ``` ```http POST /api/v1/universal-items//assets//finalize Authorization: Bearer Content-Type: application/json {} ``` Fresh deployed tests returned HTTP **201** with `status: "pending"` from each finalize call. Finalize means verified and queued, not ready. The deployed runtime returned HTTP 201, while the deployed OpenAPI document currently documents HTTP 200. Backend task **CH-1162** owns the reconciliation. This page reports the observed 201 without claiming the OpenAPI discrepancy is fixed. In either case, readiness comes only from the asset status. ## 4. Poll until ready [#4-poll-until-ready] ```http GET /api/v1/universal-items/ ``` Do not load either source rendition until its asset reports `status: "ready"`. Select the Unreal row from `response.item.assets` by rendition, engine version, and Win64 target. The ready row must still identify the exact cook/runtime compatibility and all three package files: ```json { "item": { "assets": [ { "id": "", "rendition": "unreal", "status": "ready", "engineVersion": "5.7", "targetPlatforms": ["Win64"], "files": [ { "key": "Wearable/Win64/Content/Paks/HelixWearable.pak", "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.pak" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.utoc", "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.utoc" }, { "key": "Wearable/Win64/Content/Paks/HelixWearable.ucas", "sha256": "", "url": "https:///unreal/Wearable/Win64/Content/Paks/HelixWearable.ucas" } ] } ] } } ``` A generated `thumbnail` asset row may be appended asynchronously after the two source rows become ready. It is expected derived output, not a third upload. Do not assert `assets.length === 2`; select the ready `web` or `unreal` row by rendition and compatibility. ## 5. Download, verify, and mount [#5-download-verify-and-mount] Download every selected `files[].url`, recompute SHA-256 locally, and require it to match `files[].sha256` before mounting. Require the complete matching `.pak`/`.utoc`/`.ucas` set, then select only a rendition whose `engineVersion` matches the running client and whose `targetPlatforms` includes `Win64`. The live backend does not inspect Unreal package internals or prove runtime compatibility. A ready status means the declared bytes passed transport checksβ€”not that Unreal can mount the package. Mount the downloaded set in the matching Unreal runtime and exercise the wearable there. If no compatible native rendition exists, Unreal may fall back to the ready web GLB only when that path is supported. Web must never select an Unreal-only rendition. Public `cdnUrl` and `files[].url` values are durable, mutable pointersβ€”not immutable Vault versions. An asset update may overwrite the same object key, so invalidate caches when status or build metadata changes. ## 6. Run one checksum-negative check [#6-run-one-checksum-negative-check] Create a disposable Unreal asset that declares the complete cooked `.pak`/`.utoc`/`.ucas` set. Declare the real size and digest for two files, make exactly one digest intentionally wrong, then upload the matching bytes for all three files. Its finalize must return HTTP 422: ```json { "message": "Upload integrity check failed for 1 file(s); the asset was NOT finalized. Re-upload and finalize again.", "reason": "sha256_mismatch", "files": [ "BadDigest.pak: declared , stored object is ( bytes)" ] } ``` Delete that disposable asset after the check. ## Report back [#report-back] The server currently verifies transport integrity; it does not parse or mount Unreal packages, validate cook/dependencies, require a complete manifest, or provide immutable asset revisions. Please report: * whether the downloaded complete package set mounts successfully in Unreal 5.7 on Win64; * the exact Unreal cook version and confirmation that the target was Win64; * the item and asset IDs used; * the final asset statuses; * the recomputed SHA-256 result for the `.pak`, `.utoc`, and `.ucas`; * any engine-side load, dependency, naming, or compatibility failure. # World media (thumbnail, preview image, preview video) (/docs/platform-api/world-media) Every world has a **thumbnail** (its card artwork across discovery) and an optional **preview** image plus an optional **preview video** that appear as extra slides in the world detail carousel. Until a creator uploads one, a world's `thumbnailUrl` is a deterministic auto-generated `data:image/svg+xml` placeholder β€” it is **never null**. Check `hasCustomThumbnail` to tell a real upload from the placeholder; testing `thumbnailUrl` for null will always say "set". ## The media slots [#the-media-slots] The preview slot accepts **images only**. `video/mp4` and `video/webm` uploads are rejected with a `400`. Use the YouTube endpoint below instead β€” YouTube handles transcoding, adaptive streaming, moderation and bandwidth, and the world detail embeds it privacy-first via `youtube-nocookie`. ## Uploading an image [#uploading-an-image] Two steps, mirroring the build-upload flow. Both are owner-gated. ```bash # 1. Presign. `kind` is "thumbnail" (default) or "preview". curl -X POST "$API/api/v1/instant-worlds/$WORLD_ID/media/upload" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"contentType":"image/png","size":267583,"kind":"preview"}' # β†’ { "item": { "key": "...", "url": "", "contentType": "...", "cacheControl": "..." } } ``` The presigned URL is bound to the exact `contentType` and byte `size` you declared β€” send the same bytes you measured, or the PUT is rejected. ```bash # 2. PUT the bytes straight to storage, then finalize with the key you were given. curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/png" --data-binary @preview.png curl -X POST "$API/api/v1/instant-worlds/$WORLD_ID/media/finalize" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"key":"'"$KEY"'","kind":"preview"}' # β†’ the updated world ``` Accepted image types: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/avif`. Moderation runs **asynchronously** β€” you never wait on a verdict. If a scan later flags the image, the slot is cleared and the world falls back to its generated thumbnail. ## Setting a preview video [#setting-a-preview-video] Paste any normal YouTube link. It is normalised server-side to a video id and stored; nothing is downloaded or re-hosted. ```bash curl -X POST "$API/api/v1/instant-worlds/$WORLD_ID/media/youtube" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=30s"}' ``` All of these resolve to the same id β€” extra params like `&t=` and `&list=` are discarded: Anything that is not a YouTube reference β€” another host, a `javascript:` URL, a malformed id β€” is rejected with a `400`. There is no best-effort fallback. The world then carries both a raw id and a ready-made embed URL: Embedding `previewYoutubeUrl` keeps you on the nocookie host and matches what the HELIX world detail does. If you build your own player, load it **on click** rather than on page load β€” an eagerly mounted YouTube iframe pulls \~1MB of player JS into your page's critical path. ## Clearing a slot [#clearing-a-slot] ```bash # kind = thumbnail (reverts to the generated SVG) | preview | youtube curl -X DELETE "$API/api/v1/instant-worlds/$WORLD_ID/media?kind=youtube" \ -H "Authorization: Bearer $TOKEN" ``` ## Reading media back [#reading-media-back] Every world response carries the resolved URLs: ```ts const world = await fetch(`${API}/api/v1/instant-worlds/${slug}`).then((r) => r.json()); world.item.thumbnailUrl; // always set (custom upload or generated SVG) world.item.hasCustomThumbnail; // false β†’ it's the generated placeholder world.item.previewMediaUrl; // preview image, or null world.item.previewYoutubeId; // video id, or null world.item.previewYoutubeUrl; // nocookie embed URL, or null ``` Image URLs are served through the CDN's image transform, so they arrive already resized and in a modern format for the requesting browser. ## How the carousel assembles slides [#how-the-carousel-assembles-slides] The world detail builds its carousel in this order, skipping anything unset and de-duplicating identical URLs: 1. **Thumbnail** β€” always the cover, and the page's LCP image. 2. **Preview image** β€” if set and different from the thumbnail. 3. **Screenshots** β€” the gallery a creator arranges in the editor, in their order. 4. **Preview video** β€” last, as a click-to-play slide. The carousel de-duplicates identical URLs, but it cannot tell that two *different* URLs hold the same picture. Uploading a copy of your thumbnail as the preview gives players a carousel that shows the same image twice. Make the preview earn its slide: a different area, a different moment, something the cover doesn't already show. # Web SDK (/docs/web-sdk) The Web SDK is HELIX's **TypeScript** runtime. A world is a static bundle that runs in a sandboxed iframe inside the HELIX shell; the SDK performs the handshake with the shell and exposes the [Platform API](/docs/platform-api) as the `Helix` namespace, plus the web-only multiplayer and voice stack. ## Install [#install] ```bash npm install @hypersoniclabs/helix-sdk ``` `helix init` already adds it to a new project. There is no separate server SDK: multiplayer game logic is declared in `public/helix.json` and runs on a platform-owned room (see [Multiplayer](/docs/web-sdk/multiplayer)). ## Start up [#start-up] ```ts import { Helix } from '@hypersoniclabs/helix-sdk'; const { embedded, world, user, runtimeFidelity } = await Helix.init(); // call once, at startup if (Helix.auth.isAuthenticated()) { const me = await Helix.auth.getUser(); // { id, username, displayName } } ``` `Helix.init()` takes no arguments. The shell hands the world a short-lived, world-scoped session token and the SDK wraps that handshake. When the world is opened directly (a plain `vite preview`), `init()` resolves with `embedded: false`, identity returns `null`, and saves, purchases and entitlements degrade to neutral, so the same build runs both ways. Use `helix dev` to exercise the platform calls offline. ## What is on `Helix` [#what-is-on-helix] | Namespace | What it does | | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `Helix.auth` | Current user, login prompt, auth change events | | `Helix.wallet` | LIX balance and balance change events | | `Helix.dataStore` | Durable per-world key-value storage with optimistic versions | | `Helix.inventory` | Items the player owns, equip | | `Helix.marketplace`, `Helix.purchases` | Buy products, items and listings; read entitlements | | `Helix.leaderboard`, `Helix.achievements` | Rankings and badges | | `Helix.multiplayer`, `Helix.voice`, `Helix.party`, `Helix.matchmaking` | Rooms, proximity voice, parties | | `Helix.avatar`, `Helix.emotes`, `Helix.locomotion` | The player's equipped avatar, emotes and movement pack | | `Helix.notify`, `Helix.prompts`, `Helix.device`, `Helix.camera`, `Helix.presence` | Shell UI: notifications, interaction prompts, tablet, photos, activity | The complete signature of every member, generated from the published package, is in the [API reference](/docs/web-sdk/api). ## Get started [#get-started] # Multiplayer & Networking (/docs/web-sdk/multiplayer) This stack is part of the **Web** runtime. Unreal worlds use the engine's own dedicated-server networking, see [Unreal networking](/docs/unreal-sdk/networking). The [Platform API](/docs/platform-api) (auth, LIX, storage, inventory) is shared; *this page is not.* HELIX multiplayer is **server-authoritative and declarative**. You do not write or deploy a server. A platform-owned room holds shared state, validates what clients send, and runs the rules you declare in the `multiplayer` block of `public/helix.json` at a fixed 20 Hz. The transport is an implementation detail; the vocabulary stays World / Build / Instance / Player. ## Two layers [#two-layers] 1. **Presence.** Players see each other move and animate, wearing their avatars, with nameplates and (by default) proximity voice. This is one call, `CharacterMultiplayer.create`, and on its own makes a complete hangout. `helix init --kind multiplayer` writes it for you. 2. **Declared game logic.** Shared scores and teams, server-driven entities, zones, timers, a state machine, collections, ownership, persistence. You declare them as data; rules of the form `when` an event, `if` a condition, `then` effects change the state, and the state syncs to every client. Every world must also work **single player**: guests and standalone runs never join a room, and `character.room` is simply `null`. ## Opt in [#opt-in] The manifest needs `maxPlayers` above 1, the `multiplayer` permission, and a `multiplayer` block: ```json title="public/helix.json" { "helixVersion": "0.3", "maxPlayers": 8, "permissions": ["auth.profile", "multiplayer", "voice.proximity"], "multiplayer": { "authoritative": true } } ``` `helix validate` enforces the cross-field rules and rejects an unknown verb or variable with a suggestion. A room locks at `maxPlayers` and spills extra players into a fresh instance; the platform also caps concurrent players per room. ## Talking to the room [#talking-to-the-room] The room handle is `HelixRoom` (`character.room`, or `Helix.multiplayer.joinRoom()` if you manage joins yourself). Clients **never write shared state**. They send intents and read synced state: | Call | Purpose | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `room.sendAction(name, args)` | Ask the room to run a declared `action`. Arguments are validated server-side. | | `room.vars`, `room.me`, `room.player(id)` | Guarded live views over room, own and other players' variables (`.num`, `.str`, `.bool`, `.vec3`, `.list`) | | `room.phase()`, `room.isMyTurn()` | State machine phase and turn order | | `room.onMessage(event, cb)` | Receive a declared server broadcast | | `room.onStateChange(cb)` | React to any synced change | Read state fresh each frame instead of caching it. The exact signatures are in the [API reference](/docs/web-sdk/api). ```ts // ask character.room?.sendAction('claimBonus', {}); // read const coins = character.room?.me.num('coins') ?? 0; ``` [Build your first multiplayer world](/docs/guides/first-multiplayer-world) walks through the full loop with a working example. ## The networking model [#the-networking-model] * **Movement** is client-authoritative and server-relayed (about 10 Hz, interpolated), with a server plausibility gate. The server rejects implausible position jumps, so respawns and teleports must be server rules (the `respawn` and `teleport` effects), not client-side moves. * **Value** (wallet, inventory, rewards, scores) is owned by rules and the economy, never by client events. A client that moves its own body across the map freezes its avatar for everyone else, and a client that grants itself coins can script infinite coins. Put both behind declared actions and rules. This mirrors the [item execution tiers](/docs/platform-api/inventory#execution-tiers). ## Voice [#voice] Multiplayer worlds ship proximity voice by default (`voice.proximity` in the manifest). The join is four lines after the room connects, and the player controls their own microphone through the shell. Details and channel tuning are in the `voice` section of the multiplayer reference that the [MCP server](/docs/cli/mcp) serves as `read_doc({ name: "multiplayer-world" })`. ## Reference [#reference] * [API reference: HelixRoom, Helix.multiplayer, Helix.voice](/docs/web-sdk/api) * [Build your first multiplayer world](/docs/guides/first-multiplayer-world) # Reference (/docs/web-sdk/reference) The Web SDK reference is **generated** from the type declarations of the published [`@hypersoniclabs/helix-sdk`](https://www.npmjs.com/package/@hypersoniclabs/helix-sdk) package on every build of this site, at the version recorded in `reference/sources.json`. There is no hand-written signature table to go stale. # Avatars, characters and animation (/docs/cli/mcp-tools/avatars) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `capture_gesture` [#capture_gesture] LOOK at a pose-DSL gesture: renders the clip as a contact sheet β€” front and side, evenly spaced across the clip β€” and returns it as an image. This is the CLOSING check, not the inner loop: measure with inspect\_gesture until the numbers are right (it is milliseconds), then capture once before handing the clip to a human, because "how it reads" is the one thing measurement cannot tell you. DO NOT CALL THIS FIRST β€” a clip that still has compile warnings is not ready to look at, and the render cannot tell you what those warnings would have. Each call costs a headless browser launch, a few seconds. Always renders the DEFAULT character: a clip replicates by NAME and plays on whatever body each player has, so a pose is authored against the canonical body every custom character is conformed to β€” the sheet is a reference render, not a preview of any particular player's view. Needs a world that has been npm-installed and install\_world\_packages'd. WHAT THE IMAGE CAN AND CANNOT ANSWER: trust it for silhouette, which limb is where, joint bend direction and gross orientation β€” the 'does this read as a person doing X' judgement no measurement can make. Do NOT read distances or which body part moved off it. That is measurably unreliable: an elbow sitting 0.20m FORWARD was read as 'tucked against the ribs', and a wind-up where the TORSO had leaned 0.19m back was read as 'the arms are behind the head'. Depth magnitude is the weak axis and the three-quarter view does not fix it β€” it fixes in-front-versus-behind ORDERING only. Natural-range violations are invisible in a render entirely. So when the image raises a question, answer it with inspect\_gesture, never by looking harder. | Argument | Type | Required | Description | | ---------------- | --------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clip` | `string` | yes | Absolute path to the gesture clip .json file | | `world` | `string` | yes | Absolute path to the world project directory (the one holding public/helix\_modules β€” NOT dist/) | | `frames` | `integer` | no | How many evenly spaced sample times across the clip (default 5 β€” 0/25/50/75/100%) (min 2, max 10) | | `views` | `("front" \| "back" \| "left" \| "right" \| "three-quarter")[]` | no | Up to two camera angles. left/right are the CHARACTER's OWN sides, matching the readback's character-left/character-right β€” not the viewer's; three-quarter is 40 degrees off the front, on the side the clip uses. YOU RARELY NEED THIS: the default is chosen per mode β€” a time sequence gets front + the side the clip actually keys (derived from its bones and mask), which is the ORTHOGONAL pair that makes 'moves side-to-side, not front-to-back' readable; a single moment (at) gets front + three-quarter, which lifts a limb clear of the torso instead of hiding it behind. Override for a reach behind the body. Do not pair front with back, or left with right: each looks down ONE axis, so neither view shows depth. | | `at` | `number \| string` | no | Capture ONE moment instead of a sequence β€” seconds (0.6) or a percentage ('60%'). The two views then sit SIDE BY SIDE at roughly 1.8x the size, which is what you want when a human asks you to fix one specific thing: the whole image budget goes to that instant instead of a fifth of it. Omit for the time sequence. | | `out` | `string` | no | Where to write the PNG (default: alongside the clip as \.sheet.png) | | `attach` | `string` | no | Absolute path to a prop .glb to render attached while the clip plays. MEASURE BEFORE PIXELS: converge the numbers with inspect\_gesture and the same attach params first β€” the render shows how the held prop reads, not where it is. | | `attachSocket` | `string` | no | Socket to attach to (default: the preset's, then hand\_r.grip) | | `attachPreset` | `string` | no | Held-item category (pistol, rifle, sword, mug, flashlight, torch, phone) contributing default socket + offsets | | `attachOffset` | `number[]` | no | Socket-local position offset in meters (XYZ) β€” overrides the preset | | `attachRotation` | `any` | no | Socket-local rotation in degrees (XYZ) β€” overrides the preset | ## `capture_npc` [#capture_npc] LOOK at a world's NPCs: a filmstrip of the running world β€” a canvas frame at each of several moments β€” where every frame is CAPTIONED with the NPC sample taken at that same instant (position, locomotion state, arrived, nav path, your own world state). Returns preview images plus the full list of files and captions. THE CLOSING CHECK, NEVER THE FIRST CALL: run inspect\_npc until the numbers are right, then capture once. A frame answers what measurement cannot β€” is the NPC actually there, is it wearing the right body, is it T-posing, is it standing inside the counter β€” and answers nothing about distance, progress or whether a chase is closing; take all of that from inspect\_npc, which is why the caption rides along. NO CAMERA CONTROL: the inspect lane exposes no way to aim a camera at an NPC, so these are plain full-view frames of whatever the world itself is showing. If the NPC is off-screen in the world's own view, that is what you will get β€” frame the world's camera in world code, or read the numbers instead. Same requirement as inspect\_npc: the world must pass npcs to world.ready(...) and have been rebuilt since. | Argument | Type | Required | Description | | --------------- | ----------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the BUILT world bundle directory (the same dir inspect\_world and publish\_world take β€” usually \/dist) | | `npc` | `string` | no | Caption only this NPC id (the frames themselves are the whole view either way) | | `frames` | `integer` | no | How many frames across the window (default 6) (min 1, max 24) | | `seconds` | `number` | no | The window the frames are spread over, in seconds (default 6) β€” frames are evenly spaced across it (min 0, max 60) | | `settleMs` | `integer` | no | Settle time between world-ready and the first frame (default 750 ms) (min 0) | | `out` | `string` | no | Directory to write the PNGs into (default: a fresh temp dir, whose path is reported) | | `browser` | `"bundled" \| "chrome" \| "msedge"` | no | Diagnostic override: force this one launch candidate, no fallback. | | `previewImages` | `integer` | no | How many downscaled preview image blocks to return (default 3). Pass 0 to skip images entirely β€” every written file and its caption is listed in the text block either way. (min 0) | ## `check_avatar` [#check_avatar] Judge an avatar GLB BEFORE it is sealed (executes the bundled `helix character check`; free, no login, nothing uploaded; about 2-3 minutes). It plays the platform's own walk, run and idle clips on the avatar in the production viewer beside Base Female with the hosted gate's own math, and writes the tool-owned inspection set (`inspection/index.json` and the sheets in the check directory, one LOOK question each: walk left/right, front, back, run, idle turnaround, head, hands and feet close-ups). Exit 0 = no blocker (a FLOOR: it says nothing about likeness, colour, the face or the vision critic; verdict pass, or verdict warn with warnings\[] that never block); 1 = a blocking assert failed (each blocker prints its measured value, its fixHint and the tool that fixes it); 2 = could not measure (never a pass). Pass glbPath, or jobId / itemId of an avatar you generated (its mesh is downloaded first). Failing asserts are fixed by the table, never by hand-patching the GLB and never by re-running hoping. Answers NOT AVAILABLE when the bundled CLI has no `character check`. RULES: a generated avatar comes ONLY from create\_avatar (never hand-patch triangles, skin weights, the bind pose or bones of a generated GLB: the importer normalises the arm bind, head/neck and the static hair/tail/cloth weights, and a hand patch is how the Cream Hoodie shipped broken); judge it ONLY by the tool-owned inspection sheets (never from the front or rest views) and open and answer EVERY sheet; a failing assert is fixed by the tool the fix table names, never by re-running hoping and never by an override (there is none for avatars); only a failed technical spec check or content safety holds an avatar; the visual check is advisory and never blocks (default visibility public; notes release it privately with one equippable creator copy; a spec failure holds it with one free retry), so never tell the human a visual check blocks, gates or holds anything and never ask whether to make it public or private; block until the tool returns, never end your turn while a generation runs; create\_avatar ships hair, tail and cloth STATIC (skinned to the body), but physics bones are NOT refused: when the creator asks for moving hair, tail or cloth the job view reports followUp: 'dynamics' and you author them AFTER the avatar exists (helix-avatars reference dynamics; the cloud agent has an add-dynamics tool for it), then publish a new version, and you say plainly if any asked-for part could not be made to move; a WARN verdict is not a failure (see BLOCK AND WARN below), so never stop and ask the human about one. | Argument | Type | Required | Description | | --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `glbPath` | `string` | no | Absolute path to the avatar GLB (a helix-humanoid\@1 character, normally \*.web.glb). | | `jobId` | `string` | no | An Asset Generation job id: its mesh is downloaded and checked. | | `itemId` | `string` | no | An avatar item id you generated: its job is found and its mesh checked. | | `out` | `string` | no | Directory for the output. With glbPath the check writes straight into it (default \.check: inspection/index.json, check.json, the sheets). With jobId/itemId the mesh is downloaded into it and the check goes in \/check (default ./avatar-check-\). | ## `create_avatar` [#create_avatar] THE ONE PATH for a generated avatar: a playable character body made from a description and, optionally, a reference image or a four-view sheet. A thin client of the Asset Generation job driver: it EXECUTES the bundled CLI (`helix assets start character`, then `helix assets resume`) and BLOCKS until the platform has finished building it (typically 10 to 30 minutes: reference, mesh, texture, rig and the seconds-long technical spec check; the rendered visual check is advisory and never gates anything: never end your turn, never "come back later"; progress is streamed). The PLATFORM owns the policy: it draws the A-pose turnaround from your prompt, holds it at a review gate (a person approves or revises it; it auto-approves only after referenceReviewSeconds, which an unattended caller such as an agent must set, 25 is right), redraws or retries on its own, builds mesh, texture, rig and LODs, publishes the avatar and releases it as the visibility you asked for unless the technical spec check fails (visible only to you while held); a visual-check finding is a note, never a block. This tool then downloads the exact GLB into `out`, runs `helix character check` on it (exit 0 = no blocker: verdict pass, or verdict warn with generator-limit warnings only; a floor not a verdict; 1 = a blocking assert failed; 2 = could not measure, never a pass) and reports. Returns the item id and one state: RELEASED\_\ (passed), RELEASED\_PRIVATE (a request for private that passed), RELEASED\_PRIVATE\_WITH\_WARNINGS (QA warn: released privately to you, every warning with its fixHint, exactly one creator copy granted and equippable from your inventory), HELD\_QA\_BLOCKED (QA blocked it; the platform's one free retry is offered first, and once it is spent the avatar stays HELD and the LIX refund is stated), HELD\_QA\_PENDING (QA has no verdict yet: NOT a failure, never regenerate over it; continue with resumeJobId), GENERATION\_FAILED (the message states failure.escalate: true means a different route could finish it) or HANDOFF (the router says this prompt needs an agent route; nothing ran). It also prints creatorCopy, the LIX spent and refunded, the features status (fingers, lipSync, blink, gaze), the tool-owned inspection sheets with a LOOK question each, and `Next:` lines. Nothing in it is a question for the human. RELEASE POLICY (the platform decides, never you): the default visibility is PUBLIC. Only a failed TECHNICAL spec check (an unusable file; it runs in seconds) or content safety holds an avatar; the rendered visual check is ADVISORY: its findings come back as notes and never block publishing, listing or availability. An avatar with notes (generator limits only, or a visual check that did not pass) is released PRIVATELY to its creator with them, and exactly ONE creator copy is granted at once and is equippable from the creator's inventory. A spec failure holds the avatar (nothing public) and the platform offers ONE free retry (a redraw at no net charge; result.freeRetryUsed says whether it was spent) and then offers the LIX refund. A lost executor or provider stall is retried by the platform itself and never escalates to the paid agent; only failure.escalate: true hands a job to a different route. Spends about 125-135 LIX (guard: maxLix, default 200; the CLI refuses to start over it and charges nothing). Pass resumeJobId to CONTINUE a job (a wait that timed out, a pending QA verdict): nothing is bought twice. Asset Generation CANNOT read a single reference image: look at the image yourself and describe the character in `prompt` in detail (hair colour and style, eyes, every outfit piece and its colour, accessories, species features such as ears and tail, build, age, expression, the art style), and pass referenceImagePath so the image is kept beside the results for the likeness check. Pass referenceSheetPath only for a real FRONT/BACK/LEFT/RIGHT sheet (2x2, 1x4 or 4x1, one figure per view, mid-grey background, arms straight and clear of the body); a sheet that fails the pose gate is refused before any spend (this sheet route still uses the CLI's older loop). RULES: a generated avatar comes ONLY from create\_avatar (never hand-patch triangles, skin weights, the bind pose or bones of a generated GLB: the importer normalises the arm bind, head/neck and the static hair/tail/cloth weights, and a hand patch is how the Cream Hoodie shipped broken); judge it ONLY by the tool-owned inspection sheets (never from the front or rest views) and open and answer EVERY sheet; a failing assert is fixed by the tool the fix table names, never by re-running hoping and never by an override (there is none for avatars); only a failed technical spec check or content safety holds an avatar; the visual check is advisory and never blocks (default visibility public; notes release it privately with one equippable creator copy; a spec failure holds it with one free retry), so never tell the human a visual check blocks, gates or holds anything and never ask whether to make it public or private; block until the tool returns, never end your turn while a generation runs; create\_avatar ships hair, tail and cloth STATIC (skinned to the body), but physics bones are NOT refused: when the creator asks for moving hair, tail or cloth the job view reports followUp: 'dynamics' and you author them AFTER the avatar exists (helix-avatars reference dynamics; the cloud agent has an add-dynamics tool for it), then publish a new version, and you say plainly if any asked-for part could not be made to move; a WARN verdict is not a failure (see BLOCK AND WARN below), so never stop and ask the human about one. Failing asserts are fixed by the table (assert -> cause -> tool): bind\_pose, arms\_clear, surface\_closed, silhouette, posture, no\_strands, grounded, limbs\_untwisted, rest\_frames, skin\_intact, no\_emissive\_washout, materials\_resolved, colour\_matches\_albedo, inspection.complete. read\_skill(\{ name: "helix-avatars" }) holds the whole table. BLOCK vs WARN: the verdict is pass, warn, fail or error. BLOCK findings are the serious defects (arms or limbs in the body, torn or see-through surface, bind pose, T-pose or not animating, washout, missing textures, detached or floating parts, not grounded, limb twist or skin tear, wrong scale), reported as advisory notes with fix hints: the visual check never holds an avatar (only a failed technical spec check or content safety does). WARN findings (defect.hair\_volume, defect.hair\_shape, defect.cloth\_clipping, defect.silhouette\_minor, defect.likeness, wear.walk.no\_poke\_through) are generator limits that never block: the avatar is RELEASED PRIVATELY to its owner with the warnings (state RELEASED\_PRIVATE\_WITH\_WARNINGS), is usable now, and they are advisory notes (never a gate), and are never a reason to stop and ask the human (improve once or accept). For a VRM, a game model or a robot use bridge\_import / import\_character (helix-avatars skill); for a prop use generate\_asset. | Argument | Type | Required | Description | | ------------------------ | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `prompt` | `string` | no | What the character is, in detail: written from your own look at the reference image. Required unless resumeJobId continues an existing job. (min length 1, max length 3000) | | `referenceImagePath` | `string` | no | Absolute path to the reference picture the prompt describes. Copied into `out` as reference.\; used for the likeness check, not read by Asset Generation. | | `referenceSheetPath` | `string` | no | Absolute path to a trusted FRONT/BACK/LEFT/RIGHT sheet. Skips the job's own sheet drawing; it must pass the sheet layout and pose gates. | | `visibility` | `"private" \| "unlisted" \| "public"` | no | Omit it to take the platform's one default (public; only a failed technical spec check or content safety holds an avatar, the visual check never does). Passed to the job only when you set it, and fixed when the job starts. | | `name` | `string` | no | Title of the avatar item. (min length 1, max length 100) | | `out` | `string` | no | Absolute directory for the results (default ./avatar-\): avatar.glb, raw-rig.glb, sheet.png and views, reference.\*, check/ (the inspection set), create-avatar.json. | | `maxLix` | `number` | no | Refuse to start when the up-front estimate exceeds this (default 200). | | `referenceReviewSeconds` | `integer` | no | Seconds the reference sheet waits for a look before it auto-approves (0-120). Set 25 whenever no person is watching (any cloud or coding agent): without it the sheet waits for a person for ever and this call never returns. Passed to the job only when you set it; fixed when the job starts. (min 0, max 120) | | `resumeJobId` | `string` | no | Continue THIS Asset Generation job instead of starting one (after a timed-out wait or a pending QA verdict). Do not pass visibility with it for a platform-driven job: it was fixed at start. | | `reviseGuidance` | `string` | no | With resumeJobId, for a job whose reference sheet is still open at its review gate (the platform auto-approves it after a short look, so this rarely applies): guidance for ONE fresh redraw. SPENDS LIX. (max length 800) | | `retryFailedJob` | `boolean` | no | With resumeJobId, only for a job from before the platform driver: retry from the stage that stopped (may spend LIX). A driver job retries on its own. | | `timeoutMinutes` | `integer` | no | How long to wait for the whole run, hosted QA included (default 120; the platform alone waits up to 75 minutes for QA). (min 5, max 240) | ## `fix_avatar_rig` [#fix_avatar_rig] Repair an avatar GLB's rig with the IMPORTER'S OWN stages instead of a hand script (executes the bundled `helix character fix-rig`, then re-runs `helix character check` on the result): hair, tail and loose cloth are skinned STATIC to head/neck/spine (no arm or clavicle weight in back hair, residual cross-limb weight removed, trouser legs sided), then the arms are re-bound onto the platform A-pose (plus an allowance for thick sleeves) and a hanging head is lifted, identically on every LOD. Use it for the failing asserts the check\_avatar table sends here: bind\_pose, arms\_clear, surface\_closed, silhouette, posture, skin\_intact. It writes a NEW GLB (default \.fixed.glb) and never edits the input; seal it with publish\_avatar\_version. Never cuts triangles. Answers NOT AVAILABLE when the bundled CLI has no `character fix-rig`. RULES: a generated avatar comes ONLY from create\_avatar (never hand-patch triangles, skin weights, the bind pose or bones of a generated GLB: the importer normalises the arm bind, head/neck and the static hair/tail/cloth weights, and a hand patch is how the Cream Hoodie shipped broken); judge it ONLY by the tool-owned inspection sheets (never from the front or rest views) and open and answer EVERY sheet; a failing assert is fixed by the tool the fix table names, never by re-running hoping and never by an override (there is none for avatars); only a failed technical spec check or content safety holds an avatar; the visual check is advisory and never blocks (default visibility public; notes release it privately with one equippable creator copy; a spec failure holds it with one free retry), so never tell the human a visual check blocks, gates or holds anything and never ask whether to make it public or private; block until the tool returns, never end your turn while a generation runs; create\_avatar ships hair, tail and cloth STATIC (skinned to the body), but physics bones are NOT refused: when the creator asks for moving hair, tail or cloth the job view reports followUp: 'dynamics' and you author them AFTER the avatar exists (helix-avatars reference dynamics; the cloud agent has an add-dynamics tool for it), then publish a new version, and you say plainly if any asked-for part could not be made to move; a WARN verdict is not a failure (see BLOCK AND WARN below), so never stop and ask the human about one. | Argument | Type | Required | Description | | ----------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------- | | `glbPath` | `string` | yes | Absolute path to the imported avatar GLB (helix-humanoid\@1). | | `out` | `string` | no | Output GLB path (default \.fixed.glb). | | `sleeveAllowance` | `number` | no | Extra outward arm angle in degrees over the A-pose; omit for auto (from the measured sleeve thickness). | | `headPitch` | `number` | no | Head lift in degrees (positive = chin up); omit for auto; 0 = off. | ## `generate_animation` [#generate_animation] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Explicit capability boundary: HELIX does not yet generate animation clips. Use an already published animation from the shared library. Direct byte-first Vault uploads are retired; new clips must use the approved sealed Package workflow when supported. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------- | | `prompt` | `string` | yes | (min length 1, max length 3000) | ## `gesture_axis_table` [#gesture_axis_table] The bone axis reference for authoring pose-DSL gestures: per bone group, what each Euler axis DOES in human terms, plus the advisory natural range per axis (outside it the compiler warns, it does not fail). Call this BEFORE picking rotation values β€” guessing costs a revise cycle per bone. Read live from the humanoid-character version THIS world pinned, which is why it takes a world dir and why no doc reproduces the table. Meanings hold FROM REST ONLY: rotations chain, so once a parent has rotated use inspect\_gesture with axes:true instead β€” or state a hand-position goal as a compile-time ik key (β‰₯ 0.2.15) and skip joint-space search entirely. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------------------------------------------ | | `world` | `string` | yes | Absolute path to the world project directory (the one holding public/helix\_modules β€” NOT dist/) | ## `get_look` [#get_look] THE DRESSED CHARACTER BEHIND ONE LOOK, in a single public call: the base model plus every add-on, each with its pinned Continuum Package, model URL/CID, and the fit data the add-on composer needs (slot type, attach/skin mode, sealed fits/ops) β€” enough to reproduce the dressed character without any other call. Distinct from get\_item\_build, which lists parts and PRICES; this returns the ASSETS to render. Works signed out β€” public endpoint, no login required. `merged` is non-null only when the backend has composed a single merged Runtime Artifact (a stretch capability); when null, assemble base + addOns yourself with the engine's add-on composer. Identify the Look by its Universal Item id (from search\_items / search\_vault), or by its exact Package id and version. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ---------------------------------------------------------------------------------------------- | | `itemId` | `string` | no | Universal item id of the Look | | `packageId` | `string` | no | Continuum Package id of the Look (use with version) | | `version` | `string` | no | Package version; omitted defaults to the latest sealed version. Only meaningful with packageId | ## `import_character` [#import_character] Convert an external biped character GLB (a Meshy AI export, or any Mixamo-named rig) into a helix-humanoid\@1 character asset. Conforms the mesh onto the canonical skeleton AT ITS OWN PROPORTIONS (the platform’s shared locomotion clips then drive it), generates the LOD chain (micro-chart baked atlases are auto-rebaked: weld β†’ re-unwrap β†’ closest-point texture transfer), and KTX2-encodes the textures β€” all in-process, no native tools, no extra files (the canonical helix-humanoid\@1 skeleton is bundled; a world’s installed humanoid-character pack or `canonical` overrides it). Outputs \/\.web.glb (multi-scene: scene i = LOD i; the head is sliced into a FaceMesh node per level so first-person mode can hide it, like the stock face part), plus \/anims/\.glb when `anim` is given. Use when the human supplies a custom character model instead of the stock bodies. A GENERATED avatar never comes through here: use create\_avatar (this importer is for a VRM-less game model, a robot, or re-importing an Asset Generation raw rig that a failing check\_avatar assert sends you back to; defaults only, the importer normalises arm bind, head/neck and static hair/cloth weights itself). For a PUBLISHED AVATAR item (any source: VRM, generated, game model, robot) follow the helix-avatars skill β€” get\_started(\{kind:"avatar"}) β€” which covers rigging, budgets over all LODs, hair/cloth physics and the QA that catches what every gate misses. FACES: the import prints a `face:` line β€” read it. Detection only recognizes exact established morph names (viseme\_*, vrc.v\_*, aa/ih/ou/ee/oh, jawOpen, blinkLeft/Blink\_L), so a rig whose shapes are named anything else β€” a VRoid model exported as GLB rather than VRM (Fcl\_MTH\_A, Fcl\_EYE\_Close\_L, …) is the common case β€” imports as `face: none` and that avatar is PERMANENTLY MUTE, with no error anywhere. Pass `faceMap` to map the semantics onto the morphs the asset actually has. A .vrm source should go through bridge\_import instead, which carries the vowels, blinks and expressions across on its own. read\_doc(\{name:"avatar-face"}) has the mapping file and the capability ladder. Validated example β€” a Meshy AI character with its walk animation, everything else default: \{ "body": "C:/downloads/Meshy\_AI\_Warrior\_biped\_Character\_output.glb", "anim": "C:/downloads/Meshy\_AI\_Warrior\_biped\_Animation\_Walking\_withSkin.glb", "name": "warrior", "output": "./characters" } β†’ characters/warrior.web.glb (\~3MB, LOD tris β‰ˆ10000/5000/1500 at 1024/512/256px KTX2, UASTC up close + ETC1S far) + characters/anims/locomotion.warriorWalk.glb. | Argument | Type | Required | Description | | ------------ | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `body` | `string` | yes | Absolute path to the source biped character GLB (skinned body) | | `anim` | `string` | no | Optional animation GLB on the SAME source rig β€” retargeted onto the canonical skeleton | | `clipName` | `string` | no | Clip name for `anim` (default 'locomotion.warriorWalk') | | `canonical` | `string` | no | Canonical helix body GLB override; omit to use the world’s installed humanoid-character pack when present, else the bundled helix-humanoid\@1 skeleton | | `worldDir` | `string` | no | World directory searched for the installed pack and used as the base for a relative `output` (default: cwd) | | `output` | `string` | no | Output directory (default './character-out') | | `name` | `string` | no | Output basename β†’ \.web.glb (default: derived from the body file name) | | `texSize` | `integer` | no | Texture longest-edge cap, halved per LOD level (default 1024) | | `lodTris` | `integer[]` | no | Triangle target per LOD level, highβ†’low (default \[10000, 5000, 1500]) | | `faceMap` | `string` | no | Absolute path to an explicit helix/avatar-face\@1 JSON mapping (β†’ --face-map). Required whenever the source's blend shapes are not named one of the established aliases, or the character imports mute. The importer verifies every declared morph/bone exists after conversion and fails loudly if one does not. | | `boneMap` | `string` | no | Absolute path to a bone-map JSON \{ name, version, map: \{ sourceBone: helixBone } } (β†’ --map). Omit for Meshy/Mixamo/MetaHuman rigs (auto-detected); pass an identity map when you authored the rig with helix bone names. | | `fit` | `"source" \| "canonical"` | no | 'source' (default) keeps the model's own proportions and embeds the canonical skeleton at its joints; 'canonical' re-poses it to canonical proportions. | | `bake` | `boolean` | no | false β†’ --no-bake: keep the source UVs instead of the automatic weld/unwrap/rebake a micro-chart atlas triggers. The rebake smeared a generated face (mouth into chin) and turned hair strands into blocks; pass false whenever the source atlas is usable. | | `lockBorder` | `boolean` | no | β†’ --lock-border: lock open mesh borders while simplifying LODs, so seams between separate parts do not open. | | `ktxMode` | `string` | no | Per-LOD KTX2 modes, e.g. 'uastc,etc1s' (default) or 'etc1s'. Normal maps need UASTC; ETC1S normals read as hammered foil. | | `provenance` | `string` | no | Absolute path to a provenance JSON \{ sourceUrl, license (SPDX), licenseUrl, attribution?, author?, model?, version? } (β†’ --provenance), pinned into the import receipt. | ## `inspect_gesture` [#inspect_gesture] MEASURE a pose-DSL gesture clip without a browser (same code path as `helix gesture inspect`). Runs the world's OWN installed character system, so what it reports is what the world renders. Use it after writing or editing ANY gesture clip, BEFORE asking a human to look: it is the only way to tell a good pose from a broken one without eyes. Reports (a) compile errors and natural-range warnings, (b) per-bone landmark readback in plain English at any time ("hand\_r: at head height, 0.34m to the character-right, palm faces forward"), (c) with motion:true, what the clip DOES over time β€” extent per axis, dominant direction, purity, reversals, (d) with axes:true, what a +10Β° edit to each keyed axis actually does AT THE POSED CHAIN, which the rest-frame axis table cannot tell you once a parent has rotated, (e) for clips with compile-time ik keys, the ik-goals report: target vs actual landing (cm error) + the resolved joint eulers β€” a clip using ik on a world whose installed system predates IK (added in 0.2.15) fails with a version message; fix via install\_world\_packages. Needs a world that has been npm-installed and install\_world\_packages'd. | Argument | Type | Required | Description | | ---------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clip` | `string` | yes | Absolute path to the gesture clip .json file | | `world` | `string` | yes | Absolute path to the world project directory (the one holding public/helix\_modules β€” NOT dist/) | | `t` | `(number \| string)[]` | no | Readback times: seconds (0.35) or percentages of duration ("50%"). Default \[0] β€” pass the moments that matter, e.g. the peak of the pose. | | `bones` | `string[]` | no | Bones to report (default clavicle\_r, upperarm\_r, lowerarm\_r, hand\_r, head, spine\_03). hand\_l/hand\_r additionally report palm + finger facing, self-calibrated from the rig. | | `motion` | `boolean` | no | Report what the clip does over time. Pair with motionWindow β€” otherwise the restβ†’poseβ†’rest travel dominates every number and hides the gesture. | | `motionWindow` | `number[]` | no | Restrict motion analysis to t=\[a,b] β€” the sustained part of the clip | | `axes` | `boolean` | no | Report what a +10Β° edit to each keyed axis does at the posed chain β€” use this when a value that "should" work does not | | `probe` | `string` | no | Which bone axes measures the effect on (default: the last bones entry that exists) | | `rig` | `string` | no | Measure against a custom character GLB instead of the canonical helix-humanoid skeleton (conform aims bones at each character's own joints, so extremity frames differ by up to \~20Β°) | | `attach` | `string` | no | Absolute path to a prop .glb to measure attached to the pose. Adds (e): the prop bbox in the grip-socket frame ("0.06m behind the grip point"), a unit verdict (the runtime rejects props spanning over 2 m), and where the prop lands at each t. Measure here FIRST; capture\_gesture with the same attach params is the closing look. | | `attachSocket` | `string` | no | Socket to attach to (default: the preset's, then hand\_r.grip). Grip sockets are semantic held-item frames: +z = where the item points, +y = the item's up β€” a prop authored grip-at-origin, +z out, +y up, in meters needs no offsets. | | `attachPreset` | `string` | no | Held-item category (pistol, rifle, sword, mug, flashlight, torch, phone) contributing default socket + offsets | | `attachOffset` | `number[]` | no | Socket-local position offset in meters (XYZ) β€” overrides the preset | | `attachRotation` | `any` | no | Socket-local rotation in degrees (XYZ) β€” overrides the preset | ## `inspect_npc` [#inspect_npc] MEASURE what a world's NPCs actually DO, as numbers (same code path as the CLI): boots the built world headlessly, watches it run for a few seconds, and reports per NPC β€” distance actually walked vs distance actually GAINED (the progress ratio: 1.00 is a straight line, 0.1 is a shuffle), closest approach and final distance to a target you name, STUCK WINDOWS using the driver's own rule (under 0.35 m of ground made in 1.5 s β€” the same stall its sideways commit fires on), the arrived edges with timestamps, how much of the run held a planned nav path at all, and the locomotion + world-state timelines. THIS IS THE INNER LOOP: measure here until the numbers are right, THEN capture\_npc once to look. The order is a rule, not a preference β€” behaviour bugs on this system are SILENT and invisible in a still frame: an NPC that chases perfectly and never attacks (arrived read after the seek instead of before), one that grinds into a corner forever (re-seeking every frame, which disables the stuck commit), one on the default stand body that can never walk at all, and a NavGrid that degraded to direct steering without a word. Each of those has a signature in this report and none of them has one in a screenshot. REQUIRES the world to hand its NPC pool to the inspect helper β€” world.ready(\{ scene, body, spawn, npcs, npcState }) β€” and to have been REBUILT since; a world that is inspect-wired but passes no npcs fails here with the exact wiring line. read\_doc(\{ name: "npc-world" }) has the recipe. | Argument | Type | Required | Description | | ----------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the BUILT world bundle directory (the same dir inspect\_world and publish\_world take β€” usually \/dist, NOT the project root the gesture tools take) | | `seconds` | `number` | no | How long to watch, in seconds (default 8). Long enough for the behaviour to complete a cycle: a chase that takes 12 s to cross the level reads as "never arrived" in a 4 s window. (min 0.5, max 60) | | `sampleHz` | `integer` | no | Samples per second (default 10). 10 Hz resolves the 1.5 s stuck window and every state change worth naming; raise it only to time a fast edge. (min 1, max 60) | | `npc` | `string` | no | Report only this NPC id. Every id the world reported still appears in the roster line, so a typo is obvious rather than silent. | | `target` | `number[]` | no | World-space point \[x, y, z] the NPC is supposed to reach β€” adds start/closest/final distance and how much ground it CLOSED. This is the measurement that answers "is the chase actually working", which no position dump does. | | `settleMs` | `integer` | no | Settle time between world-ready and the first sample (default 750 ms). Raise it for worlds whose NPCs spawn behind an await. (min 0) | | `browser` | `"bundled" \| "chrome" \| "msedge"` | no | Diagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct. | | `json` | `boolean` | no | Return the raw inspection JSON instead of the rendered report β€” every sample-derived number, for programmatic follow-up. The rendered report is the right default; it names what each number means. | ## `list_cosmetic_slots` [#list_cosmetic_slots] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Print the Character-Creator cosmetic slot and gender vocabulary through the HELIX creator CLI. No arguments. ## `list_voices` [#list_voices] List or search the authenticated HELIX text-to-speech voice catalog before generate\_audio. Executes the packaged HELIX CLI and returns a COMPACT row per voice β€” id, name, category, short description, traits, and the deduped languageCodes you must pair the id with β€” plus a RECOMMENDED default that is guaranteed to have at least one language code. Per-language preview URLs are omitted (they were the bulk of the payload); pass detail:true for the raw catalog. Provider credentials, cloning, and voice administration are never exposed. | Argument | Type | Required | Description | | ----------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------ | | `search` | `string` | no | Search voice name, description, labels, and category (max length 100) | | `category` | `"premade" \| "professional"` | no | Safe voice category | | `pageSize` | `integer` | no | Voices per page (default 20) (min 1, max 100) | | `pageToken` | `string` | no | Opaque next-page token returned by the preceding page (min length 1, max length 512) | | `detail` | `boolean` | no | Return the raw catalog response, including every per-language preview URL. Large. | ## `publish_avatar_version` [#publish_avatar_version] Seal a CHECKED GLB as a new Package Version of an avatar item you already made (executes `helix character check`, then `helix item publish-version`, then waits for the hosted visual-QA verdict). REFUSES to seal when the local check does not pass, because every sealed version is immutable: nothing failing is ever uploaded to "see what the check says". The item keeps its id and owner; the visual check is advisory and never holds the new version (only a failed technical spec check or content safety does), and a WARN verdict is reported with an improve-or-accept next step. Use it after fix\_avatar\_rig or a re-import (import\_character on out/raw-rig.glb) to ship a repaired GLB. Returns the same state and inspection sheets as create\_avatar. RULES: a generated avatar comes ONLY from create\_avatar (never hand-patch triangles, skin weights, the bind pose or bones of a generated GLB: the importer normalises the arm bind, head/neck and the static hair/tail/cloth weights, and a hand patch is how the Cream Hoodie shipped broken); judge it ONLY by the tool-owned inspection sheets (never from the front or rest views) and open and answer EVERY sheet; a failing assert is fixed by the tool the fix table names, never by re-running hoping and never by an override (there is none for avatars); only a failed technical spec check or content safety holds an avatar; the visual check is advisory and never blocks (default visibility public; notes release it privately with one equippable creator copy; a spec failure holds it with one free retry), so never tell the human a visual check blocks, gates or holds anything and never ask whether to make it public or private; block until the tool returns, never end your turn while a generation runs; create\_avatar ships hair, tail and cloth STATIC (skinned to the body), but physics bones are NOT refused: when the creator asks for moving hair, tail or cloth the job view reports followUp: 'dynamics' and you author them AFTER the avatar exists (helix-avatars reference dynamics; the cloud agent has an add-dynamics tool for it), then publish a new version, and you say plainly if any asked-for part could not be made to move; a WARN verdict is not a failure (see BLOCK AND WARN below), so never stop and ask the human about one. | Argument | Type | Required | Description | | --------- | -------- | -------- | ---------------------------------------------------------- | | `itemId` | `string` | yes | The avatar item to give a new version. | | `glbPath` | `string` | yes | Absolute path to the repaired avatar GLB. | | `reason` | `string` | yes | What was fixed (lands in the audit ledger). (min length 3) | | `notes` | `string` | no | Optional creator-facing changelog note. | | `out` | `string` | no | Directory for the check output (default \.check). | ## `save_look` [#save_look] SAVE LOOK: freeze an inventory instance's CURRENT composition into a Look (a reusable, shareable, never-owned preset others can apply β€” never a copy others can own). Wraps the same authority as compose: never charges a publish fee, never grants a copy of any part to anyone, never creates a distribution. `visibility: 'private'` keeps it visible only to you; `'public'` seals a public Package plus Vault and Marketplace listings so others can discover and apply it (refused, with a readable reason, if any part lacks a Continuum pin or is not itself public). Pass `lookItemId` for a Look you already created to advance it to a new version instead of creating a new one β€” `created` in the result tells you which happened. | Argument | Type | Required | Description | | ----------------- | ----------------------- | -------- | ------------------------------------------------------------------- | | `inventoryItemId` | `string` | yes | Inventory instance whose current composition becomes the Look | | `title` | `string` | yes | (min length 1, max length 80) | | `description` | `string` | no | (max length 500) | | `visibility` | `"private" \| "public"` | no | (default "private") | | `lookItemId` | `string` | no | An existing Look you created β€” advances it to a new Package Version | ## `scaffold_locomotion_pack` [#scaffold_locomotion_pack] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the CLI command that writes a STARTER locomotion pack directory β€” anims/clip-meta.json seeded with one example walk entry plus a README carrying the per-clip contract (the full clip vocabulary lives in read\_doc(\{name:"locomotion-clip-spec"})). A locomotion pack is the set of replacement movement clips a player equips as a character add-on (a female walk, a zombie shamble): data, not code, in the `character.locomotion@1` slot. Then drop your authored GLBs beside the meta file (file name = the entry name + ".glb", exactly one animation per file, arriving in-place on canonical helix-humanoid bones) and add an entry per clip this pack ships, each with its MEASURED duration and strideSpeed β€” a single-clip pack is a legitimate product, and every name it omits falls back to the platform clip. Read the spec doc first; follow with validate\_locomotion\_pack. Item-first pack publishing is retired. | Argument | Type | Required | Description | | ----------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dir` | `string` | yes | Directory for the new pack; the starter files land in \/anims/. | | `overwrite` | `boolean` | no | Replace starter files that already exist. Default: existing files are KEPT and reported as skipped, so re-scaffolding never clobbers an authored clip-meta.json. | ## `validate_locomotion_pack` [#validate_locomotion_pack] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the CLI command that validates a locomotion pack directory OFFLINE and reports EVERY problem in one pass, not just the first: the pack format (anims/clip-meta.json present, readable and an array; per-entry name/loop/duration/strideSpeed types), the GLB cross-checks (a file per surviving entry, exactly one animation in it, its real length against the declared duration), the gait-only clip vocabulary (deaths, get-ups and sit/lie poses are refused in a pack), the strideSpeed rules β€” null on the non-travelling clips, the per-clip band around the platform reference on the travelling ones β€” and the walk \< run \< sprint ladder across the gaits the pack actually contains. A ring clip with no direction, or a direction off the fixed grid, is reported as a warning: it loads but never joins the strafe ring. Run it after any hand edit to clip-meta.json. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------- | | `dir` | `string` | yes | Path to the pack directory (the one containing anims/). | # Import bridge (/docs/cli/mcp-tools/bridge) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `bridge_detect` [#bridge_detect] Fingerprint an external file or engine-project directory through the CLI-owned HELIX Bridge and return helix.bridge-detection/1 JSON. This is read-only and runs no converter or plugin executable. | Argument | Type | Required | Description | | -------- | -------- | -------- | --------------------------------------------------------------------------- | | `source` | `string` | yes | Absolute path to the external asset, archive, mod export, or engine project | ## `bridge_doctor` [#bridge_doctor] List built-in/installed Bridge Plugins and optional Unreal, Unity, Godot, and Blender adapter configuration. | Argument | Type | Required | Description | | ------------ | ---------- | -------- | ----------- | | `pluginDirs` | `string[]` | no | | ## `bridge_import` [#bridge_import] Execute the CLI-owned HELIX Bridge pipeline: detect, plan, convert, package, and write checksummed provenance. For every command-backed capability, including a packaged official adapter, allowPermissions must contain every permission named by bridge\_plan and expectedPlanKey must equal that reviewed plan deterministicKey; never add permissions merely to make a blocked plan run. Only an in-process built-in-only route with no command needs no additional acknowledgement. | Argument | Type | Required | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `source` | `string` | yes | Absolute path to the external source | | `output` | `string` | yes | Absolute path to a new output directory that does not already exist; ignored by dryRun | | `type` | `"auto" \| "asset" \| "character" \| "addon" \| "scene" \| "animation" \| "extract"` | no | | | `target` | `"web" \| "unreal" \| "mobile" \| "canonical"` | no | | | `plugins` | `string[]` | no | | | `pluginDirs` | `string[]` | no | | | `headTexture` | `string` | no | MetaHuman baked head texture (required for a detected MetaHuman face) | | `eyeTexture` | `string` | no | MetaHuman eye bake override; omit to use the PR #99 canonical bake packaged in the CLI | | `canonical` | `string` | no | Canonical helix-humanoid body override | | `boneMap` | `string` | no | Custom skeleton/bone map JSON | | `inputs` | `object` | no | Additional named input paths required by the selected plugin plan | | `expectedPlanKey` | `string` | no | Reviewed bridge\_plan deterministicKey; mandatory for non-dry-run command-backed execution | | `allowPermissions` | `("filesystem.read-source" \| "filesystem.write-output" \| "unrestricted-host-execution" \| "network" \| "spawn-process" \| "unreal-editor" \| "unity-editor" \| "blender" \| "gpu")[]` | no | Exact bridge\_plan permissions approved for command-backed execution; unrestricted-host-execution authorizes a local process, not a sandbox | | `dryRun` | `boolean` | no | Plan only; execute and write nothing | ## `bridge_inspect` [#bridge_inspect] Inspect an external source through HELIX Bridge and return structural evidence plus every viable installed-plugin plan. Read-only: it does not install or execute plugins. | Argument | Type | Required | Description | | ------------ | ---------- | -------- | ----------------------------------------------------------------- | | `source` | `string` | yes | Absolute path to the external source | | `pluginDirs` | `string[]` | no | Additional local plugin directories to inspect without installing | ## `bridge_plan` [#bridge_plan] Resolve an external source into a deterministic helix.bridge-plan/1 capability graph. Always call this before granting permissions to any command-backed capability, including a packaged official adapter, or when the source is unfamiliar. Read-only and safe to use on untrusted plugins. | Argument | Type | Required | Description | | ------------- | ------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------ | | `source` | `string` | yes | Absolute path to the external source | | `type` | `"auto" \| "asset" \| "character" \| "addon" \| "scene" \| "animation" \| "extract"` | no | Desired content domain; auto uses detection evidence | | `target` | `"web" \| "unreal" \| "mobile" \| "canonical"` | no | Runtime representation target (default web) | | `plugins` | `string[]` | no | Installed plugin ids to include explicitly | | `pluginDirs` | `string[]` | no | Additional local plugin directories to inspect without installing | | `headTexture` | `string` | no | MetaHuman baked head texture to fingerprint into the reviewed plan | | `eyeTexture` | `string` | no | MetaHuman eye bake override to fingerprint; omit for the CLI-packaged canonical bake | | `canonical` | `string` | no | Canonical helix-humanoid body override to fingerprint | | `boneMap` | `string` | no | Custom skeleton/bone map JSON to fingerprint | | `inputs` | `object` | no | Additional named input paths to fingerprint into the reviewed plan | ## `bridge_plugin_create` [#bridge_plugin_create] Scaffold a new local Bridge Plugin directory with bridge-plugin.json and an executable JSON-stdin handler. | Argument | Type | Required | Description | | ----------- | -------- | -------- | -------------------------------------- | | `directory` | `string` | yes | Absolute path to a new empty directory | | `id` | `string` | yes | Stable lowercase plugin id | | `name` | `string` | no | | ## `bridge_plugin_inspect` [#bridge_plugin_inspect] Validate and inspect an installed Bridge Plugin id or an uninstalled local plugin directory without running it. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------- | | `plugin` | `string` | yes | Installed id or absolute plugin directory | ## `bridge_plugin_install` [#bridge_plugin_install] Validate and atomically install a local Bridge Plugin directory. Installation makes it discoverable but does not grant execution permissions. Use force only when replacing the same installed id is part of the requested task. | Argument | Type | Required | Description | | ----------- | --------- | -------- | ------------------------------------------------------- | | `directory` | `string` | yes | Absolute plugin directory containing bridge-plugin.json | | `force` | `boolean` | no | | ## `bridge_plugin_remove` [#bridge_plugin_remove] Remove one installed community/local Bridge Plugin. Built-in HELIX plugins cannot be removed. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------- | | `id` | `string` | yes | Installed plugin id | ## `bridge_plugin_test` [#bridge_plugin_test] Validate a local Bridge Plugin manifest, file bounds, and declared executable handlers without executing conversion capabilities. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ------------------------- | | `directory` | `string` | yes | Absolute plugin directory | ## `bridge_plugins_list` [#bridge_plugins_list] List built-in, installed, and optionally local Bridge Plugins with capabilities, versions, trust, permissions, and roots. | Argument | Type | Required | Description | | ------------ | ---------- | -------- | ----------- | | `pluginDirs` | `string[]` | no | | ## `bridge_validate` [#bridge_validate] Validate a Bridge output, provenance record, or combined base-avatar GLB through the CLI. Set baseAvatar for the PR #99 three-LOD + textured-eye publication gate. | Argument | Type | Required | Description | | ------------ | --------- | -------- | ------------------------------------------------------- | | `artifact` | `string` | yes | Absolute path to the artifact or bridge-provenance.json | | `baseAvatar` | `boolean` | no | | # Continuum and packages (/docs/cli/mcp-tools/continuum) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `dry_run_continuum_canary` [#dry_run_continuum_canary] Validate and compose the deterministic Pacifica/material/vehicle/character canary through the Creator library without backend mutation. Before preparing the descriptor, convert structurally proven actors to native building/level/space/wall/floor/ceiling/door/window/stairs/foliage/light/portal/gameplay-anchor elements; preserve exact mesh objects and material bindings wherever conversion is unproven; and compose one dependency-linked Package graph rather than independent per-actor payloads. | Argument | Type | Required | Description | | -------- | -------- | -------- | -------------------------------------------------------------------- | | `input` | `string` | yes | Absolute path to the Continuum canary JSON descriptor (min length 1) | ## `get_continuum_publication_receipt` [#get_continuum_publication_receipt] Retrieve the sanitized durable Creator CLI receipt for a completed Continuum publication job. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------ | | `jobId` | `string` | yes | Publication job SHA-256 returned by a Continuum publish tool | ## `get_package_manifest` [#get_package_manifest] Fetch the full manifest for a published system/ability/asset-pack β€” the contract to read before pinning OR configuring it. For a SYSTEM, the payload carries `config` (the complete tunable surface) + `capabilities`. For an ABILITY, ability.json with its configSchema. Always includes the code + asset CDN base URLs. Defaults to the latest version. Read this instead of guessing config keys. | Argument | Type | Required | Description | | --------- | -------- | -------- | -------------------------------------------------------- | | `slug` | `string` | yes | Package slug (e.g. fly, humanoid-character) | | `version` | `string` | no | Semver version; defaults to the latest published version | ## `install_world_packages` [#install_world_packages] Resolve a world's helix.json system/ability pins against the platform catalog, download their code into public/helix\_modules/, and wire the hosted three runtime (import map + src/helix.runtime.ts). Run it in the world project after `npm install` and after every pin edit, before `npm run build`. Replays the committed helix.lock.json when present; pass update:true to re-resolve ranges to newer versions and rewrite the lock β€” also REQUIRED after adding a new pin (e.g. visual) to a world that already has a lock. Same code path as the human `helix install`, but resolution happens in-process against THIS toolchain's environment β€” no PATH ambiguity. | Argument | Type | Required | Description | | ----------- | --------- | -------- | -------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the world project directory (the one containing helix.json or public/helix.json) | | `update` | `boolean` | no | Re-resolve pins against the live catalog and rewrite helix.lock.json (default: replay the existing lock) | ## `publish_continuum_canary` [#publish_continuum_canary] Resume and publish a Creator-validated Continuum canary as one exact-CID Package graph. Structural actors must already be native Scene elements; unproven conversions must retain their exact mesh and material bindings. This MCP tool accepts only private Packages with private/unlisted Vault and Marketplace Listings. It cannot expose or synthesize CLI public-listing confirmation. | Argument | Type | Required | Description | | -------- | -------- | -------- | -------------------------------------------------------------------------------- | | `input` | `string` | yes | Absolute path to a publish-ready Continuum canary JSON descriptor (min length 1) | ## `publish_continuum_package` [#publish_continuum_package] Publish or resume one exact immutable Package Version descriptor, including a frozen Build, through Creator CLI object adoption/ingestion, lease, fencing, seal and optional policy-authorized Listing. No content or publication policy is implemented in MCP. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------------------------------------- | | `input` | `string` | yes | Absolute path to the Package publication JSON descriptor (min length 1) | ## `publish_continuum_scratch_world` [#publish_continuum_scratch_world] Construct a World from semantic primitives through the same importer, Scene compiler, exact-CID Package and resumable WorldRelease pipeline used for source maps. MCP cannot silently authorize a public Listing. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------------------------------------------- | | `input` | `string` | yes | Absolute path to the scratch-world publication JSON descriptor (min length 1) | ## `publish_continuum_source_map` [#publish_continuum_source_map] Convert an inventoried source map into hierarchical native Scene elements, exact-CID Packages and a WorldRelease through the resumable Creator CLI pipeline. The descriptor may request only an existing owner/editor project publication policy; MCP cannot grant public Vault/Marketplace authority. | Argument | Type | Required | Description | | -------- | -------- | -------- | ---------------------------------------------------------------------------- | | `input` | `string` | yes | Absolute path to the source-world publication JSON descriptor (min length 1) | ## `pull_continuum_package` [#pull_continuum_package] Download an authorized exact Package Version creator source bundle through Creator CLI, verify its CID and safely extract its self-contained files into a new directory. Backend package visibility and allowlists remain authoritative; MCP does not bypass access or execute source hooks. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------- | | `packageId` | `string` | yes | Exact Continuum Package UUID | | `version` | `string` | yes | Exact immutable Package Version (min length 1, max length 64) | | `output` | `string` | yes | New output directory for editable source (min length 1) | # Devices (/docs/cli/mcp-tools/devices) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `create_device` [#create_device] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. The only Device authoring entrypoint. Prepares the Creator CLI command that wraps an existing GLB in a declarative `helix.device/1` project. Devices are MCP/CLI-authored; there is intentionally no website workbench. After running it, edit the generated device.json through normal agent file tools, then call validate\_device and test\_device. | Argument | Type | Required | Description | | ----------- | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path for the new Device project | | `id` | `string` | yes | Stable lowercase Device slug (min length 3, max length 50) | | `title` | `string` | yes | Player-facing Device title (min length 1, max length 80) | | `model` | `string` | yes | Absolute path to the source GLB | | `template` | `"standing-desk" \| "clock" \| "television"` | yes | Starting behavior/state contract. Television includes a permissioned Screen/App, universal text input, audio playback, and typed power/volume/audio ports. | ## `test_device` [#test_device] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the Creator CLI Device simulation command. Use mode=both before publication: the exact same self-contained Device definition is exercised as an owned Item Instance and as a World-owned package instance; single covers every control/graph path and durable remount, multiplayer uses two independent clients against one authority state and refuses divergence. | Argument | Type | Required | Description | | ----------- | ------------------------------------- | -------- | ----------------------------------------------------- | | `directory` | `string` | yes | Absolute Device project directory or device.json path | | `mode` | `"single" \| "multiplayer" \| "both"` | no | Default both | | `json` | `boolean` | no | Return machine-readable test receipts | ## `validate_device` [#validate_device] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the Creator CLI validation command for a Device project. It fails closed on malformed definitions, invalid GLB bytes, missing named-node bindings, invalid typed state or Universal Action Surface controls, undeclared capabilities, and missing Home/World compatibility. | Argument | Type | Required | Description | | ----------- | --------- | -------- | ----------------------------------------------------- | | `directory` | `string` | yes | Absolute Device project directory or device.json path | | `json` | `boolean` | no | Return a machine-readable validation report | # Generation (/docs/cli/mcp-tools/generation) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `generate_asset` [#generate_asset] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. FOR AN AVATAR (a playable character body) DO NOT USE THIS: create\_avatar is the one path (it runs, blocks, checks and judges for you; this tool only hands back a command that needs the CLI on PATH and leaves the draft unjudged). LAST PLATFORM RUNG: generate a new prop or character only after list\_materials and search\_assets found no suitable resource. For a character, referenceSheetPath may supply an approved local FRONT/BACK/LEFT/RIGHT PNG/JPEG/WebP; the backend then skips image generation but preserves validation, mesh, texture, rig, LOD, thumbnail, billing, and Vault publication. This spends the signed-in creator’s LIX, and the up-front estimate is not a cap β€” `helix jobs` shows what each job actually cost. THIS COMMAND BLOCKS (default up to 20 minutes) waiting for the job β€” mesh generation on a complex prop can genuinely take a while. A "timed out" error is this wait giving up CLIENT-SIDE, not the job failing server-side: the printed `helix assets resume ` command checks on or continues that SAME job (never start a new one after a timeout). Pass timeoutMs to wait longer up front, or pollIntervalMs to poll less often. | Argument | Type | Required | Description | | -------------------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"prop" \| "character"` | yes | | | `prompt` | `string` | yes | (min length 1, max length 3000) | | `title` | `string` | no | (min length 1, max length 100) | | `additionalPrompt` | `string` | no | (max length 800) | | `targetPolycount` | `integer` | no | (min 1000) | | `visibility` | `"public" \| "unlisted" \| "private"` | no | | | `referenceSheetPath` | `string` | no | Character-only local path to a trusted FRONT/BACK/LEFT/RIGHT PNG, JPEG, or WebP sheet. Its exact bytes are checksum-bound and reused; no image generation is invoked. (min length 1) | | `timeoutMs` | `integer` | no | How long the CLI waits for the job to finish before giving up (client-side only β€” the job keeps running server-side either way). Default 1200000ms/20min. Raise this if you expect a slow mesh stage instead of re-running the command after a timeout. | | `pollIntervalMs` | `integer` | no | How often the CLI checks job status while waiting. Default 2000ms. | ## `generate_audio` [#generate_audio] Generate a sound effect, music track, or spoken line through the Asset Generation API, running the job to completion in ONE call. This executes the packaged HELIX CLI, which owns authentication, mode-specific validation, the long-running job, LIX accounting, provider access, and default-on Vault publication. Returns the CLI JSON receipt with the durable job/Vault ids and cost evidence. Sound effect is the default mode; music allows up to 4,100 prompt characters and 3-600 seconds; text\_to\_speech uses text + voiceId instead of prompt. NEVER use it for a vehicle: vehicle sound (engine, exhaust, horn, doors, tyres, any one-shot) is sourced from real recordings via import\_vehicle\_audio, never generated, and the vehicle gates refuse generated audio. PREFER start\_asset\_generation for anything slow: it also covers all three audio modes and hands back a job id, so a timeout or dropped connection cannot lose the job. AUDIO IS METERED β€” the charge settles on actual usage and can land above the estimate; there is no automatic refund. Check what it cost with `helix jobs`. | Argument | Type | Required | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `audioMode` | `"sound_effect" \| "music" \| "text_to_speech"` | no | sound\_effect (default), music, or text\_to\_speech | | `prompt` | `string` | no | Required for sound\_effect/music; omit for text\_to\_speech. The CLI/backend enforce the mode-specific combined prompt limit before charging. (min length 1, max length 4100) | | `additionalPrompt` | `string` | no | (max length 800) | | `title` | `string` | no | (min length 1, max length 100) | | `model` | `"eleven_text_to_sound_v2" \| "music_v2" \| "music_v1" \| "eleven_multilingual_v2" \| "eleven_flash_v2_5" \| "eleven_v3"` | no | Allowlisted model for the selected mode; invalid mode/model pairs fail before generation. | | `outputFormat` | `"mp3_44100_128" \| "mp3_44100_192" \| "mp3_22050_32"` | no | Portable MP3 rendition; defaults to mp3\_44100\_128 | | `loop` | `boolean` | no | Sound-effect only: request a seamless loop | | `durationSeconds` | `number` | no | Sound effect: 0.5-30 seconds; music: 3-600 seconds (min 0.5, max 600) | | `promptInfluence` | `number` | no | Sound-effect only: prompt adherence from 0 to 1 (min 0, max 1) | | `forceInstrumental` | `boolean` | no | Music only: omit vocals | | `text` | `string` | no | Text-to-speech only: spoken text; the selected model may impose a lower limit (min length 1, max length 40000) | | `voiceId` | `string` | no | Text-to-speech only: required HELIX voice id returned by list\_voices (min length 1, max length 64) | | `languageCode` | `string` | no | Text-to-speech only: lowercase ISO 639-1 code, e.g. en | | `seed` | `integer` | no | (min 0, max 4294967295) | | `voiceSettings` | `{ stability?: number, similarityBoost?: number, style?: number, speed?: number, speakerBoost?: boolean }` | no | Text-to-speech only: bounded voice controls | | `pronunciationDictionaryIds` | `string[]` | no | Text-to-speech only: up to three unique provider dictionary ids | | `projectId` | `string` | no | | | `sessionId` | `string` | no | | ## `generate_environment_splat` [#generate_environment_splat] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. EXPERIMENTAL: generate a World Labs Gaussian splat in SPZ format through the Asset Generation API. Declare whether it is a placeable object or full environment. Successful default-on runs publish a typed Gaussian-splat resource to Vault; consumers need an SPZ-capable renderer. | Argument | Type | Required | Description | | ------------------ | --------------------------- | -------- | ------------------------------- | | `prompt` | `string` | yes | (min length 1, max length 3000) | | `additionalPrompt` | `string` | no | (max length 800) | | `title` | `string` | no | (min length 1, max length 100) | | `projectId` | `string` | no | | | `sessionId` | `string` | no | | | `scope` | `"object" \| "environment"` | no | | ## `generate_image` [#generate_image] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Generate a standalone image through the Asset Generation API (creator/client-token auth). NOT for an avatar's turnaround sheet: the image is auto-published PUBLIC to the creator's Vault by default (a privacy surprise on a private request), and Asset Generation cannot condition a character on it unless it is a clean four-view sheet β€” for an avatar call create\_avatar, which draws the sheet inside the job. This is NOT material generation. A successful default-on run includes a Vault asset id; explicit creator opt-out is returned as vaultAutoPublish="disabled". Spends the signed-in creator’s LIX β€” the up-front estimate is not a cap; `helix jobs` shows what it actually cost. | Argument | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------- | | `prompt` | `string` | yes | (min length 1, max length 3000) | | `title` | `string` | no | (min length 1, max length 100) | | `additionalPrompt` | `string` | no | (max length 800) | | `projectId` | `string` | no | | | `sessionId` | `string` | no | | ## `generate_material` [#generate_material] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Generate a validated portable PBR material bundle through the Asset Generation API and auto-publish it to Vault by default. Source PNG maps are retained; a KTX2 rendition is reported only when a real encoder produced it. 1K is the web/mobile default; 2K is explicit. | Argument | Type | Required | Description | | ------------------ | -------------- | -------- | ------------------------------- | | `prompt` | `string` | yes | (min length 1, max length 3000) | | `title` | `string` | no | (min length 1, max length 100) | | `additionalPrompt` | `string` | no | (max length 800) | | `resolution` | `1024 \| 2048` | no | | | `projectId` | `string` | no | | | `sessionId` | `string` | no | | ## `list_materials` [#list_materials] FIRST ASSET RUNG: search the versioned HELIX platform material pack. Returns the offered PBR texture materials (photographed or scanned; procedural glass/water are not offered and are named under `unlisted`) and the immutable CDN base as JSON. Use before Vault search, generation, or external/CC0 sources. Apply one with use\_material. | Argument | Type | Required | Description | | ------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | `string` | no | Free-text material search over id, name, category, and tags | | `category` | `string` | no | Exact category filter, e.g. brick, wood, metal, glass | | `limit` | `integer` | no | Palette budget for this result (default 12; the global catalog remains open-ended) (min 1, max 50) | | `resolution` | `string` | no | texture resolution, e.g. 1k or 2k; omit for the material's default. Keeps only materials carrying it. The CLI errors with the valid values rather than substituting a different resolution. | ## `list_systems` [#list_systems] List the runtime systems published on HELIX Instant (e.g. humanoid-character). A system is a versioned framework that hosts abilities; a world pins one. Returns slug\@version + title. No arguments. ## `poll_asset_generation` [#poll_asset_generation] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Poll ONE known job without starting or advancing a duplicate. Successful asset-pipeline jobs report vaultAssetId plus vaultAutoPublish; Asset Generation prop/character jobs report the universal item id and Vault receipt after publish. A JOB THAT IS NOT DONE IS NOT NECESSARILY DEAD: a job can be PAUSED AT A GATE β€” waiting on the creator, or holding a concept the server itself rejected β€” and is still live and advanceable. When that is the case the status output carries the advance actions THE SERVER OFFERS for that job; read them from the response and use those. Never assume a fixed menu of recovery verbs and never infer one from this description β€” the server is the authority on what can advance a job, and it changes. DO NOT HAVE A JOB ID, OR WANT TO KNOW WHAT YOU SPENT? Run `helix jobs` β€” it lists your generation jobs with each one’s state and actual LIX cost, and prints the exact command that advances each parked job. `helix jobs --state unfinished` or `--state awaiting_approval` narrows it to what is stuck. That is the answer to "what did I spend" and "what is still waiting on me"; this tool is for a job you can already name. | Argument | Type | Required | Description | | ---------- | ---------------------- | -------- | ---------------------------------------------------------- | | `jobId` | `string` | yes | Job id returned by start\_asset\_generation | | `pipeline` | `"dreamer" \| "asset"` | yes | dreamer for prop/character; asset for all standalone kinds | ## `start_asset_generation` [#start_asset_generation] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. FOR AN AVATAR USE create\_avatar INSTEAD (it blocks until the job is done, then checks and judges the result; a job started here and left is how an avatar run ended with LIX spent and nothing published). THE DURABLE ASYNC ROUTE β€” prefer it for every other generation: it returns a job id immediately, so a timeout or a dropped connection never loses the job (poll it with poll\_asset\_generation instead of starting a duplicate). Covers all five generation surfaces. Prop/character route through the Asset Generation job lane (legacy `pipeline: 'dreamer'`); image/material/audio/Gaussian splat use the standalone asset lane; animation delegates to the CLI’s explicit no-charge unavailable boundary. AUDIO: audioMode selects sound\_effect (default), music, or text\_to\_speech β€” speech takes text + voiceId + languageCode from list\_voices instead of a prompt. NEVER for vehicle sound (engine, exhaust, horn, doors, tyres, any one-shot): that is sourced from real recordings via import\_vehicle\_audio, not generated. COST IS NOT CAPPED BY THE ESTIMATE. The up-front quote is an estimate, and metered generations (audio in particular) settle on ACTUAL usage β€” the charge can land ABOVE or below it, and there is no automatic refund of a difference. A real music job quoted \~1.2 LIX settled at 1.4. To bound the spend, pass maxLix: the CLI refuses to start when the ESTIMATE exceeds it β€” that is a pre-flight guard on the quote, still not a guarantee about the final charge. Check what anything actually cost with `helix jobs`. | Argument | Type | Required | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"prop" \| "character" \| "image" \| "material" \| "audio" \| "gaussian_splat" \| "animation"` | yes | | | `prompt` | `string` | no | The generation prompt. Required for every kind EXCEPT audio in text\_to\_speech mode, which uses text instead. (min length 1, max length 3000) | | `referenceSheetPath` | `string` | no | kind "character" only: absolute path to a trusted FRONT/BACK/LEFT/RIGHT sheet (2x2, 1x4 or 4x1, one figure per view, arms straight and clear of the body). The CLI uploads it (`helix assets start-reference`); the backend skips image generation, runs its layout, orientation and pose gates before any spend, and builds the character from exactly that sheet. For avatars prefer create\_avatar. (min length 1) | | `title` | `string` | no | (min length 1, max length 100) | | `additionalPrompt` | `string` | no | (max length 800) | | `targetPolycount` | `integer` | no | (min 1000) | | `projectId` | `string` | no | | | `sessionId` | `string` | no | | | `loop` | `boolean` | no | audio, sound\_effect only: request a seamless loop | | `durationSeconds` | `number` | no | audio duration β€” sound\_effect: 0.5-30s; music: 3-600s (min 0.5, max 600) | | `resolution` | `1024 \| 2048` | no | | | `repeatPerMeter` | `number` | no | material only: tile repeats per metre | | `splatScope` | `"object" \| "environment"` | no | | | `maxLix` | `number` | no | Refuse to start if the up-front ESTIMATE exceeds this many LIX. A guard on the quote, not a cap on the final metered charge. | | `audioMode` | `"sound_effect" \| "music" \| "text_to_speech"` | no | audio only: sound\_effect (default), music, or text\_to\_speech | | `model` | `"eleven_text_to_sound_v2" \| "music_v2" \| "music_v1" \| "eleven_multilingual_v2" \| "eleven_flash_v2_5" \| "eleven_v3"` | no | audio only: allowlisted model for the selected mode | | `outputFormat` | `"mp3_44100_128" \| "mp3_44100_192" \| "mp3_22050_32"` | no | audio only: portable MP3 rendition; defaults to mp3\_44100\_128 | | `promptInfluence` | `number` | no | audio, sound\_effect only: prompt adherence from 0 to 1 (min 0, max 1) | | `forceInstrumental` | `boolean` | no | audio, music only: omit vocals | | `text` | `string` | no | audio, text\_to\_speech only: the spoken text (used INSTEAD of prompt) (min length 1, max length 40000) | | `voiceId` | `string` | no | audio, text\_to\_speech only: a voice id returned by list\_voices β€” never invent one (min length 1, max length 64) | | `languageCode` | `string` | no | audio, text\_to\_speech only: one of THAT voice's supported language codes | | `seed` | `integer` | no | (min 0, max 4294967295) | | `stability` | `number` | no | audio, text\_to\_speech only (min 0, max 1) | | `similarityBoost` | `number` | no | audio, text\_to\_speech only (min 0, max 1) | | `style` | `number` | no | audio, text\_to\_speech only (min 0, max 1) | | `speed` | `number` | no | audio, text\_to\_speech only (min 0.7, max 1.2) | | `speakerBoost` | `boolean` | no | audio, text\_to\_speech only | | `pronunciationDictionaryIds` | `string[]` | no | audio, text\_to\_speech only: up to three unique provider dictionary ids | ## `use_material` [#use_material] Resolve one HELIX platform material id to its immutable CDN map URLs, its `assetBaseUrl` and its authored rendering defaults, at the texture resolution you pick. APPLY IT with the visual system, never by hand: pin `"visual": "^0.1"` in helix.json (then install\_world\_packages with update: true), `const materials = await loadMaterialLibrary(assetBaseUrl, { renderer })` from @helix/visual, `materials.apply(mesh, id)` β€” exact per-axis tiling for boxes, planes, spheres, cylinders and capsules; pass `{ size: [w, h] }` (metres the 0..1 UV span covers) for imported meshes β€” then `await materials.ready()`. Details: read\_doc(\{ name: "world-look" }). The output states which resolution was resolved and which ones the material offers, under `resolution`. | Argument | Type | Required | Description | | ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | yes | Material id returned by list\_materials | | `resolution` | `string` | no | texture resolution, e.g. 1k or 2k; omit for the material's default. A resolution the material does not carry is an error naming the ones it does β€” it is never silently swapped for another. | # Getting started and account (/docs/cli/mcp-tools/getting-started) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `check_for_updates` [#check_for_updates] Check whether the HELIX toolchain AND a world are current. Toolchain: compares the MCP / SDK / manifest against the latest published on npm and returns the exact update command for anything behind. The CLI is reported as BUNDLED β€” this MCP always runs its own packaged copy, so a global CLI install cannot change what it reports and is not the remedy for it. World (pass projectDir): reads the manifest system pins + helix.lock.json and compares them against the platform catalog β€” reporting stale in-range versions (safe to re-install), out-of-range pins, missing recommended capabilities (a world without the engine-core system cannot use screenshots, world inspection, or the photo camera), and a runtime baked behind the platform three line (three 0.185.1 today β€” an older world still publishes, with a warning, and already-published worlds keep running), with the applicable upgrade steps inlined. Items: also audits EVERY item the world uses β€” multiplayer.weaponItems pins against the vault (a definition pinned below its current published version, a pin that does not resolve on this backend, or the two config copies disagreeing), installed Vault assets (public/helix.assets.json) against their current Vault versions, and installed interactables (helix\_interactables/installed.json) against their Marketplace references. Item updates are OPTIONAL and consented separately from system updates β€” "update my systems" does not cover items; ask the user specifically per item before re-pinning or reinstalling anything. | Argument | Type | Required | Description | | ------------ | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `projectDir` | `string` | no | Absolute path to a world/ability project β€” also checks its pinned @helix dependencies AND its helix.json system pins against the catalog | ## `check_login` [#check_login] Check the outcome of a start\_login flow: pending (the human is still signing in β€” wait and re-check), done (logged in; credentials saved), or failed (timeout/denied β€” start\_login again to retry). No arguments. ## `get_started` [#get_started] REQUIRED FIRST STEP before building any HELIX Instant world. Returns the complete project recipe. Pass kind="character" for a world with a player character in ANY camera framing β€” all built on the humanoid-character system. Pass kind="multiplayer" for a world where players share a space and/or a game β€” presence PLUS declarative game logic (scores, teams, entities, physics, turns); returns the multiplayer HUB that routes to 15 worked templates + the multiplayer-logic DSL (you write no server code). Pass kind="scene" for a bare scene with no character. Omit kind to get the decision guide. If the world SELLS anything (a shop, currency, passes, cosmetics, tips, unlocks), that is in-world purchases and layers onto any of these β€” see read\_doc(\{name:"purchases"}) and read\_template(\{name:"world-shop"}). | Argument | Type | Required | Description | | -------- | ------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `kind` | `"character" \| "multiplayer" \| "scene" \| "vehicle" \| "avatar" \| "device"` | no | "character" = humanoid-character world; "multiplayer" = shared world (presence + declarative game logic; the hub + 15 templates + the DSL); "scene" = bare world; "vehicle" = make, convert or publish a drivable VEHICLE item (car, kart, truck, motorcycle, bicycle β€” from a real-world model name, a game mod, a downloaded model, a concept image, or from scratch) or its add-ons; "avatar" = make, convert, fix or publish an AVATAR item (a playable character body from a VRM, a generated character, a game model or a robot), including hair/cloth/tail physics bones; "device" = author a declarative GLB-backed Device (Early access); omit for the decision guide | ## `model_build` [#model_build] Build a CODE-MODELED prop: run a `.model.js` program (read\_skill helix-modeling, read its references/program.md for the format) and write \.glb (+ \.collider.glb) with no Blender and no network. Returns errors with line numbers, findings (floating parts, not on y=0, collision undeclared, budgets), per-part triangles and bounds, and a rendered 4-view sheet (hero, front, side, top) as an image. LOOK at the sheet and iterate 3 to 6 times. Not platform visual QA: publishing still needs the helix-gauntlet loop. | Argument | Type | Required | Description | | -------- | --------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | `source` | `string` | yes | Absolute path of the program file, \.model.js | | `out` | `string` | no | Absolute output folder (default: a build/ folder next to the program) | | `style` | `"realistic" \| "hand-painted" \| "low-poly"` | no | Budget the build for an art style (default realistic: 60k tris; hand-painted 20k; low-poly 6k) | | `look` | `boolean` | no | Render the 4-view sheet (default true) | ## `model_search` [#model_search] Find helpers for CODE MODELING (read\_skill helix-modeling): a .model.js program builds a prop from geometry helpers (primitives, lathe, sweep, loft, booleans, arrays, deformers). No arguments lists every helper by family plus the authoring conventions; query ranks helpers for a need ("curved tube", "hole in a wall"); ids returns full contracts for those helpers. Read-only. Search instead of guessing names. | Argument | Type | Required | Description | | -------- | ---------- | -------- | ------------------------------------------------------------------------- | | `query` | `string` | no | Free-text need, e.g. "curved tube" (max length 500) | | `ids` | `string[]` | no | Helper ids whose full contracts you want, e.g. \["lathe","pipeAlongPath"] | | `limit` | `integer` | no | Most hits for a query (default 8) (min 1, max 50) | ## `read_doc` [#read_doc] Read a HELIX Instant reference document: "bridge" (the plugin-driven external content import workflow), "continuum" (AI-native semantic Scenes, exact-CID Packages/Builds, resume and publication policy), "character-world", "character-animation" (authoring CUSTOM character gestures as JSON keyframe poses β€” for SHORT SIMPLE upper-body-led motion only: player actions, stances, one-shot accents, additive overlays. NOT dances, fight moves or acrobatics β€” no root motion, foot planting or weight shift. Carries the axis table, compile-time ik keys for hand-position goals + runtime reach/gaze IK (β‰₯ 0.2.15), why rotations chaining breaks joint-space posing, and the headless pose evaluator), "avatar-face" (WHETHER A CHARACTER CAN MOVE ITS MOUTH AT ALL β€” the helix/avatar-face\@1 manifest must ride the GLB at IMPORT time; nothing at runtime can add one later and an avatar without it is permanently mute with no error raised anywhere. The viseme15 > vowel5 > jaw > none capability ladder, the receiver-local boundary β€” no facial weight is ever networked β€” why mp.attachVoice() is the entire world-side integration, the automatic VRM path, when a faceMap is REQUIRED (a VRoid model exported as GLB rather than VRM imports mute), bone-vs-morph jaw targets, why add-ons never carry faces, and the honest limits: Face V1 is 0.3-line only, and the stock Base Female ships with NO face channels), "locomotion-clip-spec" (LOCOMOTION PACKS: authoring the replacement movement clip set a player equips as a character add-on β€” the pack format (anims/clip-meta.json + one GLB per clip), the gait-only clip vocabulary (the core nine, the strafe rings, turn-in-place), strideSpeed as a MEASUREMENT with its per-clip bands and the walk \< run \< sprint ladder, the canonical skeleton contract, and the loader's per-clip failure behaviour; partial packs down to one clip are valid products), "shooter-worlds" (GUNS: the gun-control ability contract, weapon profiles, FX/hit systems + the frame-order contract, health/death/ragdoll, replica stances, published gun definitions, and explosive weapons β€” rockets and grenades as published items plus the world rules their blast needs β€” read it for ANY world where players shoot), "npc-world" (NPCS: a shopkeeper, quest giver, guard or a monster that hunts the player β€” an NPC is a Character whose driver is your behaviour instead of the keyboard. NpcScene and the stand/physics body tiers, AIDriver behaviours and the two ordering rules whose failures are silent, the attack doctrine, NavGrid pathfinding and its honest v1 limits, InteractionSpots prompts, Vault characters as NPC bodies, and the multiplayer handoff to humanoidEntities), "multiplayer-world" (shared-presence worlds: the CharacterMultiplayer facade, typed state accessors + the single-playerβ†’multiplayer conversion guide), "multiplayer-logic" (the declarative game-logic DSL: declared state, when/if/then rules, entities, zones, timers, state machine, collections, ownership, the weaponDamage op), "bring-your-world" (adopt HELIX in an EXISTING three.js project, staged: become a world β†’ adopt the character β†’ go multiplayer), "lighting-world" (the platform lighting path β€” the visual runtime that owns the renderer/sky/sun/grade, the public/helix.visuals.json contract, the baked-vs-dynamic decision tree, the publish-time bake via bake\_world\_lighting, quality tiers, converting a hand-lit world, and the opt-out for worlds with their own visual style), "world-look" (making a raw world LOOK good β€” fix defects before taste: the asset-truth import checklist for scale/metalness/normals/colorspaces/alpha, geometry-first sun aim, consuming platform materials correctly with per-axis repeat and multiplier semantics, texture hygiene β€” mips inside the KTX2, minFilter, textures-before-AA β€” scene assembly, composition, and judging your own captures), "scene-performance" (first-build and final measurable optimization: exact Scene resource scope, authored versus submitted geometry, material/Palette and delivery counters, estimated reference-budget utilization, receipt limits, and matched fullscreen visual QA), "sdk" (full @hypersoniclabs/helix-sdk API), "items" (Standard/Collectible definitions, Marketplace/World distributions, shell-mediated Claim/Buy and resale policy), "purchases" (SELLING world-local consumables/effects β€” registration, entitlements + consume, reload recovery and room-side rules), "manifest" (every helix.json field + bundle rules), "publishing" (pipeline, login, error semantics), "screenshots" (declaring hero shots, the capture/inspect/attach workflow, the thumbnail/preview slots), "world-inspect" (measuring world layout as data: the world.ready integration, the placement checks, measure-then-look, findings as measurements not verdicts), "upgrades" (existing-world migration ledger: stale pins, missing capabilities, step-by-step fixes), "catalog" (platform systems/abilities/asset-packs), "world-recipe" (bare-scene worlds with no character), "terrain" (GROUND for a scene world: the --terrain scaffold flag, generating or importing a heightfield, the six presets, inspect before you publish, and the lighting policy), "vehicles" (the HELIX VEHICLE CONTRACT for making or converting a drivable vehicle item and its add-ons: mesh parts and naming, pivots/axes/units, seats, materials, budgets, the landV2 physics fields and how to derive them from real specs, audio, add-on anchoring, publish and the visual verification it requires β€” get\_started(\{kind:"vehicle"}) for the workflow). | Argument | Type | Required | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | | `name` | `"bridge" \| "continuum" \| "character-world" \| "character-animation" \| "character-attachments" \| "avatar-face" \| "locomotion-clip-spec" \| "shooter-worlds" \| "npc-world" \| "multiplayer-world" \| "multiplayer-logic" \| "bring-your-world" \| "lighting-world" \| "world-look" \| "scene-performance" \| "sdk" \| "items" \| "purchases" \| "manifest" \| "publishing" \| "screenshots" \| "world-inspect" \| "upgrades" \| "catalog" \| "world-recipe" \| "terrain" \| "vehicles"` | yes | | ## `read_skill` [#read_skill] Read a first-party HELIX skill β€” the WORKFLOW and judgment for a kind of job, where read\_doc holds the contract. Skills: helix-anime-movement, helix-assets, helix-avatar-qa, helix-avatars, helix-city-convert, helix-gauntlet, helix-modeling, helix-multiplayer, helix-vehicles, helix-world-build, helix-world-director, helix-world-qa. Omit reference to read the skill's SKILL.md (it lists its references); pass reference to read one reference file, e.g. read\_skill(\{ name: "helix-vehicles", reference: "source-beamng" }). | Argument | Type | Required | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------ | | `name` | `"helix-anime-movement" \| "helix-assets" \| "helix-avatar-qa" \| "helix-avatars" \| "helix-city-convert" \| "helix-gauntlet" \| "helix-modeling" \| "helix-multiplayer" \| "helix-vehicles" \| "helix-world-build" \| "helix-world-director" \| "helix-world-qa"` | yes | Skill name. | | `reference` | `string` | no | A reference file stem listed in the skill (no extension, no path). | ## `start_login` [#start_login] Begin the HELIX browser sign-in for the production environment ([https://helixgame.com](https://helixgame.com)): opens the human's browser to the sign-in page and returns the URL β€” relay it if the browser did not open. The human completes the sign-in on the website; the minted token is captured and saved locally by this process (never returned to you). Poll check\_login for the outcome. No-ops when already logged in to this environment. | Argument | Type | Required | Description | | ------------- | --------- | -------- | ------------------------------------------------------------------------------------------- | | `openBrowser` | `boolean` | no | Open the human's browser automatically (default true); false = only return the URL to relay | | `force` | `boolean` | no | Sign in again even when already logged in to this environment (switch accounts) | ## `unreal_doctor` [#unreal_doctor] Diagnose a HELIX Unreal (UE 5.8) project for World publishing. Two layers, same implementations the human CLI uses: a static publish-readiness validation (helix.unreal.json + project shape, no editor needed), and optionally the SDK’s in-editor Project Doctor commandlet (engine=true; needs UE\_ROOT on this machine) which can also APPLY every automatable fix (fix=true β€” previewed, surgical, guesses are skipped with reasons). Findings come back machine-readable with a who-must-act verdict: pass / fixable / creator-action / unsupported. | Argument | Type | Required | Description | | --------- | --------- | -------- | ------------------------------------------------------------------------------------ | | `project` | `string` | yes | project directory holding the .uproject (or the .uproject path for the static layer) | | `engine` | `boolean` | no | also run the in-editor SDK Doctor commandlet (requires UE\_ROOT) | | `fix` | `boolean` | no | apply automatable in-editor fixes, then re-scan (implies engine) | ## `whoami` [#whoami] Check HELIX login state. Publishing requires a logged-in creator account; if not logged in, drive the browser sign-in with start\_login (the human completes it on the website). No arguments. # MCP tool reference (/docs/cli/mcp-tools) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} The HELIX MCP server registers 124 tools. This reference is generated by starting `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` and listing its tools, so names, descriptions and argument schemas are exactly what an agent sees. ### [Import bridge](/docs/cli/mcp-tools/bridge) [#import-bridge] Detect, plan and run imports from other engines and file formats. 12 tools: `bridge_detect`, `bridge_doctor`, `bridge_import`, `bridge_inspect`, `bridge_plan`, `bridge_plugin_create`, `bridge_plugin_inspect`, `bridge_plugin_install`, `bridge_plugin_remove`, `bridge_plugin_test`, `bridge_plugins_list`, `bridge_validate`. ### [Vehicles](/docs/cli/mcp-tools/vehicles) [#vehicles] Check, simulate, preview and publish vehicles and vehicle add-ons. 11 tools: `check_vehicle_addon`, `check_vehicle_audio`, `check_vehicle_cabin`, `check_vehicle_host`, `get_vehicle_reference_package`, `import_vehicle_audio`, `preview_vehicle`, `publish_vehicle`, `publish_vehicle_addon`, `simulate_vehicle`, `validate_vehicle`. ### [Devices](/docs/cli/mcp-tools/devices) [#devices] Create, validate and test interactive devices. 3 tools: `create_device`, `test_device`, `validate_device`. ### [Avatars, characters and animation](/docs/cli/mcp-tools/avatars) [#avatars-characters-and-animation] Avatars, NPCs, looks, gestures, animation and locomotion packs. 17 tools: `capture_gesture`, `capture_npc`, `check_avatar`, `create_avatar`, `fix_avatar_rig`, `generate_animation`, `gesture_axis_table`, `get_look`, `import_character`, `inspect_gesture`, `inspect_npc`, `list_cosmetic_slots`, `list_voices`, `publish_avatar_version`, `save_look`, `scaffold_locomotion_pack`, `validate_locomotion_pack`. ### [Continuum and packages](/docs/cli/mcp-tools/continuum) [#continuum-and-packages] Publish, pull and inspect Continuum packages and receipts. 9 tools: `dry_run_continuum_canary`, `get_continuum_publication_receipt`, `get_package_manifest`, `install_world_packages`, `publish_continuum_canary`, `publish_continuum_package`, `publish_continuum_scratch_world`, `publish_continuum_source_map`, `pull_continuum_package`. ### [Items and distribution](/docs/cli/mcp-tools/items) [#items-and-distribution] Search, publish, delist and distribute universal items. 23 tools: `check_listed_items`, `create_item_distribution`, `delist_item`, `disable_item_distribution`, `get_asset`, `get_item_build`, `inspect_item_definition`, `install_asset`, `item_thumbnail`, `list_asset_versions`, `list_item_distributions`, `publish_owned_item`, `scaffold_item_definition`, `search_assets`, `search_items`, `search_vault`, `track_asset`, `update_asset_metadata`, `update_item_distribution`, `validate_item_definition`, `visual_qa_item`, `visual_qa_item_hosted`, `visual_qa_item_status`. ### [Generation](/docs/cli/mcp-tools/generation) [#generation] Generate assets, images, audio, materials and environments. 10 tools: `generate_asset`, `generate_audio`, `generate_environment_splat`, `generate_image`, `generate_material`, `list_materials`, `list_systems`, `poll_asset_generation`, `start_asset_generation`, `use_material`. ### [Worlds](/docs/cli/mcp-tools/worlds) [#worlds] Scaffold, validate, publish and measure worlds, terrain, placements, achievements and products. 29 tools: `analyze_scene_performance`, `bake_world_lighting`, `capture_world_screenshot`, `clear_world_thumbnail`, `create_world`, `inspect_world`, `list_abilities`, `list_achievements`, `list_my_worlds`, `list_templates`, `list_world_products`, `placements_bake`, `placements_inspect`, `publish_unreal_world`, `publish_world`, `read_template`, `register_achievement`, `register_world_product`, `scaffold_world`, `set_achievement_icon`, `set_preview_video`, `set_world_thumbnail`, `terrain_generate`, `terrain_import`, `terrain_inspect`, `update_achievement`, `update_world_product`, `validate_world`, `world_metrics`. ### [Getting started and account](/docs/cli/mcp-tools/getting-started) [#getting-started-and-account] Docs, skills, sign-in and environment checks. 10 tools: `check_for_updates`, `check_login`, `get_started`, `model_build`, `model_search`, `read_doc`, `read_skill`, `start_login`, `unreal_doctor`, `whoami`. # Items and distribution (/docs/cli/mcp-tools/items) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `check_listed_items` [#check_listed_items] \[Experimental] Re-judge LISTED vehicles against TODAY's gates (executes the bundled `helix vehicle recheck-listed`). A listed item is never re-judged when a gate is added, so a car can be on sale on a visual-QA record that predates the gate that would fail it β€” the F40 was listed before the posture and hands-while-steering asserts existed, and sold with its driver lying back, hands off the wheel and fingers twitching. For each car it reads the passing record of its current version and reports CURRENT, STALE (names every current blocking gate the record never ran) or FAIL (names the gates it failed). With rerun: true it renders each car again NOW (preview\_vehicle on the published car: every seat occupied, the full gauntlet inspection set and its sheets) and reports that verdict and the failing asserts. Run it before and after you update a listed car; with no itemIds it re-checks every listed vehicle (or one creator's). STALE or FAIL is ADVICE, never a block: it does not stop a listing or a sale. To improve such a car, run the helix-gauntlet loop, stage a new version of the SAME item and read the visual-check notes (advisory). Exit 0 = every car current (and passing today), 1 = a car has notes worth reading. | Argument | Type | Required | Description | | ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `itemIds` | `string[]` | no | Vehicle item ids (default: every listed vehicle in the marketplace catalogue). | | `creatorId` | `string` | no | Without itemIds: only this creator's listed vehicles. | | `rerun` | `boolean` | no | Render each car again now with today's gates (several minutes per car). Needs a `helix login` (a QA account). | | `out` | `string` | no | With rerun: directory for each car's inspection (default ./.helix-recheck). | | `json` | `boolean` | no | | ## `create_item_distribution` [#create_item_distribution] Runs the bundled CLI (`helix item distribution create`) and returns its output: creates one creator-authorized primary Claim/Buy route for a published item. This does not republish the definition or charge another publish fee. World routes require the owned world slug plus a stable key; Marketplace routes carry neither. LISTING IS SHIPPING, so look first: the helix-gauntlet loop on the published item is strongly advised (for a vehicle: preview\_vehicle(\{ item }) with every inspection sheet looked at, and check\_listed\_items(\{ itemIds: \[itemId] })), but it is advice, not a requirement. The visual check never gates a listing: for a vehicle whose visual-QA record is missing, stale or failed, the CLI prints advisory notes and lists it anyway. Only an unusable file or content safety refuses (the error says why). | Argument | Type | Required | Description | | -------------- | -------------------------- | -------- | ----------------------------------------------------------------------- | | `itemId` | `string` | yes | Published universal item definition id. | | `channel` | `"marketplace" \| "world"` | yes | | | `priceLix` | `number` | yes | 0 = Claim; positive = Buy and must meet the server-owned floor. (min 0) | | `world` | `string` | no | Owned world slug, required for channel=world. | | `key` | `string` | no | Stable world code key, required for channel=world. | | `maxPerPlayer` | `integer \| "unlimited"` | no | | | `maxClaims` | `any` | no | | | `startsAt` | `string` | no | | | `endsAt` | `string` | no | | | `dryRun` | `boolean` | no | | ## `delist_item` [#delist_item] Runs the bundled CLI (`helix item delist`). Unlist a published item DEFINITION the creator owns (CH-1671). Existing owners keep what they already have; the item stops appearing on browse/marketplace and nobody new can acquire it through ANY of its distributions. This is not a delete: owned item definitions remain so current owners keep their content. Different from disable\_item\_distribution, which only turns off ONE acquisition route. Not currently reversible (no relist). | Argument | Type | Required | Description | | -------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `itemId` | `string` | yes | The universal item id to delist β€” the UUID from the item detail page/API or an approved Creator publication receipt. | | `reason` | `string` | no | Optional note for the creator's own record; not shown to anyone else. | ## `disable_item_distribution` [#disable_item_distribution] Runs the bundled CLI (`helix item distribution disable`): disables an acquisition route without deleting its history or item definition. | Argument | Type | Required | Description | | ---------------- | --------- | -------- | ----------- | | `distributionId` | `string` | yes | | | `dryRun` | `boolean` | no | | ## `get_asset` [#get_asset] Read one HELIX Vault asset, including immutable artifact URL, MIME type, version, provenance, visibility, and world-usage receipts. | Argument | Type | Required | Description | | --------- | -------- | -------- | ---------------- | | `assetId` | `string` | yes | Vault asset UUID | ## `get_item_build` [#get_item_build] WHAT IS IN A LOOK (API identifier: `build`): its base, every add-on installed on it, which of them you already own, and what the parts cost together. Call it on any item whose `buildKind` is set β€” search\_items marks those β€” before deciding to use a preset, because a Look is a RECIPE and not a bundle: it is never owned itself, and its parts are still owned and bought individually, from their own listings, so `totalPriceLix` is the SUM of the parts rather than a price the Look itself charges. Answers 404 for an ordinary item, which is the normal case and not an error worth reporting. A part whose Continuum Package Version is not pinned to a marketplace item yet comes back with `item: null` and is counted in `unresolvedPartCount` β€” that part is genuinely in the Look and genuinely unavailable, so never read the remaining parts as the whole Look. For the dressed character itself β€” model URLs and fit data, ready to render, not just prices β€” use get\_look instead. | Argument | Type | Required | Description | | -------- | -------- | -------- | --------------------------------------------------- | | `itemId` | `string` | yes | Universal item id of the build (from search\_items) | ## `inspect_item_definition` [#inspect_item_definition] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the CLI command that resolves a PUBLISHED item definition and prints what an author tunes against β€” a weapon cadence, ammo, damage, spread and sounds; an emote its playback mode and pinned clip, plus the joiner clip and offset when it is a paired one. Read-only. | Argument | Type | Required | Description | | --------- | --------- | -------- | ------------------------------------------------------------------- | | `assetId` | `string` | yes | Asset ID of an already-published legacy weapon or emote definition. | | `version` | `integer` | no | Pin a specific version. Default: the latest. (min 1) | ## `install_asset` [#install_asset] Resolve one durable Vault id, download the exact immutable version, verify server SHA-256 and byte count, then write public/helix.assets.json provenance. Material installs default to the lean runtime KTX2 family; request source for editable PNG maps or all only when both families are genuinely needed. The CLI owns rendition selection and installation. For reproducible installs, supply version, checksumSha256, and sizeBytes together. Any mismatch fails before a receipt is written. | Argument | Type | Required | Description | | -------------------- | -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the world project root | | `assetId` | `string` | yes | Vault asset UUID from search\_assets | | `version` | `integer` | no | Exact immutable Vault version (min 1) | | `checksumSha256` | `string` | no | Expected SHA-256 for that version | | `sizeBytes` | `integer` | no | Expected byte size for that version (min 1) | | `materialRenditions` | `"runtime" \| "source" \| "all"` | no | Material map family: runtime KTX2 only (default), source PNG only, or all. Non-material assets are unchanged. (default "runtime") | ## `item_thumbnail` [#item_thumbnail] Request a fresh server render of an ALREADY-PUBLISHED item's catalog thumbnail (avatar, wearable, add-on, vehicle, ...) you own, and get back the preview URL β€” runs in-process, no CLI step. The catalog card is rendered by HELIX with the standard renderer from the item's ACTIVE Package Version, with one fixed camera and lighting preset per item type; creators CANNOT upload or override it (action=set is gone and is refused). Use this after a corrected Package Version, or when the card still shows an older mesh; pass dryRun first to see what would change without writing, and force to re-render when the server says it is already current. To correct a Package-backed mesh, seal a new Package Version and move the item onto it, then request the render here. A creator-owned item only (403 otherwise); an older backend answers 'does not support thumbnail refresh yet'. Not for worlds: use set\_world\_thumbnail. | Argument | Type | Required | Description | | -------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | | `action` | `"refresh" \| "set"` | no | Default and only working action: refresh (server re-render). "set" is refused: creators cannot override the catalog card. | | `itemId` | `string` | yes | Published universal item id (UUID) from the item detail page/API or an approved Creator publication receipt. | | `force` | `boolean` | no | Re-render even when the stored thumbnail is already current | | `dryRun` | `boolean` | no | Report what would change, write nothing | ## `list_asset_versions` [#list_asset_versions] List immutable versions of one Vault asset, including URL, MIME type, byte count, SHA-256, notes, and creation time. | Argument | Type | Required | Description | | --------- | -------- | -------- | ---------------- | | `assetId` | `string` | yes | Vault asset UUID | ## `list_item_distributions` [#list_item_distributions] Runs the bundled CLI (`helix item distribution list --json`) and returns every active or disabled primary distribution drawing from one item definition. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------- | | `itemId` | `string` | yes | | ## `publish_owned_item` [#publish_owned_item] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Move an existing creator-owned Universal Item onto one exact sealed Continuum Package Version using the approved Publications workflow. This does not create an Item, add an offer/distribution, or upload loose bytes. The Package must be public, sealed, current, and match the Item. A content-rating attestation is required; no backend receipt shape is defined by MCP. | Argument | Type | Required | Description | | ------------------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------- | | `itemId` | `string` | yes | Existing creator-owned Universal Item UUID | | `packageId` | `string` | yes | Exact sealed Continuum Package UUID | | `packageVersion` | `string` | yes | Exact immutable Package Version (min length 1, max length 64) | | `manifestCid` | `string` | yes | Exact sealed Package manifest CID (min length 1) | | `contentRating` | `"everyone" \| "teen" \| "mature" \| "adult"` | yes | | | `intentId` | `string` | no | Optional UUIDv4 idempotency intent for safe retries; the CLI creates one when omitted | | `acceptRatingAttestation` | `true` | yes | Explicitly attest that the selected content rating is accurate | ## `scaffold_item_definition` [#scaffold_item_definition] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the CLI command that authors a COMPLETE, valid interactive item definition β€” no hand-written descriptor JSON. Two item types, and the type decides which arguments apply. type:"weapon" (the default) builds it from a mesh plus a category (Rifle | Pistol | SMG | Shotgun | Sniper | LMG | Launcher | Grenade), which resolves the engine spread and reload tables, the grip preset and the shell class; with no sounds directory it copies category-default shot sounds next to the descriptor. The two explosive categories also carry the fire kind: a Launcher fires a projectile round, a Grenade is THROWN and is never rack-equipped β€” the blast itself (damage fanout, the fuse, the FX) stays world-authored, read\_doc(\{name:"shooter-worlds"}) section 9. Mesh convention: grip point at the ORIGIN, +y up, +z out the barrel, meters. type:"emote" builds it from an animation clip GLB instead: the performance IS the item, so there is no mesh, no category and no gun config, and the clip has a hard duration ceiling β€” 10s as playback "oneshot", 60s as playback "loop". A PAIRED emote (one another player joins in on) adds a second clip, where that player stands, and an optional offerAtS β€” the pose the initiator holds while waiting, as seconds into their own clip. Either playback pairs. The scaffold prints the exact roles block to paste in, or pass joinerClip + joinerOffset + offerAt to the descriptor. An emote may also carry TRANSITIONS: introClipPath is the enter clip played into the body clip, outroClipPath the exit clip played out of it (a sit-down before a sit loop, a stand-up after it). Each caps at 5s, and on playback "oneshot" intro + clip + outro together cap at the same 10s β€” the player is locked for the whole chain. A PAIRED emote takes NEITHER: a joiner arrives on a held offer, so there is no shared instant to chain a transition into. Follow with validate\_item\_definition. This legacy descriptor format has no Item-first publishing route; use the approved sealed-Package publication flow for new content. For the consumption side (installWeaponItem before attach), read\_doc(\{name:"shooter-worlds"}) section 8. | Argument | Type | Required | Description | | --------------- | ------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"weapon" \| "emote"` | no | Item type. Default: weapon. A weapon takes meshPath + category; an emote takes clipPath + playback. | | `meshPath` | `string` | no | WEAPON: path to the prop mesh .glb. Required unless type is "emote". | | `category` | `"Rifle" \| "Pistol" \| "SMG" \| "Shotgun" \| "Sniper" \| "LMG" \| "Launcher" \| "Grenade"` | no | WEAPON: archetype β€” it inherits spread, reload foley, grip preset and shell class, and for Launcher/Grenade the projectile or thrown fire kind. Required unless type is "emote"; an emote has no category. | | `soundsDir` | `string` | no | WEAPON: directory of your own .ogg shot sounds (mono opus-in-ogg; a file named *dry* becomes the dry-fire click). Omit for the category defaults. | | `clipPath` | `string` | no | EMOTE: path to the animation .glb the emote plays. Required when type is "emote". | | `playback` | `"oneshot" \| "loop"` | no | EMOTE: how the clip plays. Default: oneshot. A oneshot clip caps at 10 seconds, a loop at 60 β€” validate MEASURES the clip and refuses one that overruns its mode. | | `audioPath` | `string` | no | EMOTE: one .ogg the performance carries (mono opus-in-ogg). Optional. | | `introClipPath` | `string` | no | EMOTE: path to the ENTER transition .glb, played once into the body clip (e.g. the sit-down before a sitting loop). Caps at 5s, and on playback "oneshot" it counts against the 10s ceiling for intro + clip + outro. Not allowed on a paired emote. | | `outroClipPath` | `string` | no | EMOTE: path to the EXIT transition .glb, played once out of the body clip (e.g. the stand-up after a sitting loop). Same 5s cap and the same "oneshot" chain ceiling, and likewise not allowed on a paired emote. | | `title` | `string` | no | Item title, also the source of the item id. Default: the mesh/clip basename. | | `outDir` | `string` | no | Output directory for the descriptor plus copied files. Default: the current directory. | ## `search_assets` [#search_assets] SECOND ASSET RUNG: typed search over reusable HELIX Vault resources β€” call it before generating anything, and before looking at external/CC0 sources. THE INSTALLABLE TABLE: hits here are the ones install\_asset takes (props, models, materials, animations); for sealed Continuum Packages β€” Scenes, vehicles, weapons, devices, Looks β€” search\_vault is the different table. Finding is not using: install what fits and place it, or say why you passed. Executes the packaged HELIX CLI and returns a COMPACT row per hit (id, title, kind, short description, format, size, performance tier, key dimensions, reuse counts, artifact + thumbnail URLs) β€” enough to choose. Bulk per-item `metadata` and `selectionExplanation` are omitted; get the full record for ONE asset with get\_asset, its versions and checksums with list\_asset\_versions, or pass detail:true for the raw payload (large β€” it truncates). Prefer origin="generated" for trusted pipeline provenance and engine="web" for browser worlds. Ranking signals are independent β€” lexical match, hard filters, measured performance, observed reuse β€” never one magic score. Character rows carry skeleton (and skeletonVerification where the publish gate measured it): the browser runtime plays humanoid rigs, so a verified humanoid skeleton is what makes a character web-playable. | Argument | Type | Required | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | `string` | no | Full-text search over title, description, and tags | | `match` | `"recall" \| "narrow"` | no | What the query DOES to the result set. "narrow" makes it a hard FILTER β€” use it to ask "does the Vault have X?", where an empty result is the honest answer. "recall" (the backend default when omitted) makes it a RANKING input over the whole library, so you always get the nearest candidates β€” use it to ask "show me candidates near X". Omitting this keeps recall, which never returns empty and will hand back confidently irrelevant rows when nothing real matches. | | `kinds` | `("prop" \| "character" \| "animation" \| "audio" \| "texture" \| "material" \| "environment" \| "terrain" \| "vfx" \| "decal" \| "sky" \| "gaussian_splat" \| "scene" \| "interactive_item" \| "unreal_package")[]` | no | Typed Vault kinds to include | | `subtype` | `string` | no | Typed subtype, e.g. static-prop or pbr-material (max length 64) | | `scope` | `"object" \| "environment"` | no | Gaussian-splat reuse scope | | `format` | `string` | no | Primary format, e.g. glb, spz, hdr, or helix-material (max length 64) | | `performanceTier` | `"mobile" \| "desktop" \| "cinematic" \| "unknown"` | no | Measured runtime-cost tier | | `rank` | `"relevance" \| "performance" \| "reuse"` | no | Ranking strategy; explanations remain independent | | `skeleton` | `string` | no | Required standard skeleton | | `engine` | `string` | no | Runtime filter; usually "web" for Instant Worlds | | `license` | `string` | no | License filter | | `source` | `string` | no | Publish-source filter | | `origin` | `string` | no | Trust/provenance filter; "generated" is platform generated | | `official` | `boolean` | no | TRUE narrows to the platform-vouched (official) tier β€” calibrated, convention-correct assets; try it FIRST for weapons and other interactive\_item definitions, and widen only when it has no fit. FALSE narrows to community assets; omit for everything. A preference filter, never a gate. | | `limit` | `integer` | no | (min 1, max 100) | | `detail` | `boolean` | no | Return the CLI payload untrimmed, including per-item metadata and selectionExplanation. Large enough to be truncated at the default limit β€” prefer get\_asset for one asset. | ## `search_items` [#search_items] THE MARKETPLACE SHELF, not the Vault stockroom β€” and the only way to find a LOOK. `search_assets` searches raw creator RESOURCES (the meshes, clips and materials a world is built out of); this searches published PRODUCTS: what a creator has already finished, priced and listed. Two different tables answering two different questions, so reach for this one whenever the question is "has somebody already made this?" rather than "what parts exist?". A LOOK (API identifier: `build`) is a published, dressed-character (or vehicle/weapon/device) preset β€” a base plus the exact add-ons installed on it, pinned as an immutable Continuum Build Package β€” and it exists precisely so you do not have to reassemble it: for a police world, prefer a finished police character over sourcing a base, finding a uniform and proving the fit yourself. A LOOK IS NEVER OWNED ITSELF: it is a free, always-available preset, and its parts are still acquired individually from their own listings β€” applying one never grants, buys, or moves anything you do not already own. A Look is NOT its own category (a character Look IS an avatar, a vehicle Look IS a vehicle), so `builds` defaults to `include` and a plain query for "police" returns finished Looks alongside the parts. Every row carries `buildKind` when it is a Look β€” that field is the ONLY thing distinguishing a preset from the base it was assembled from. Inspect one with get\_item\_build for its part list and prices, or get\_look for the dressed character (base + add-ons with model URLs) ready to render. | Argument | Type | Required | Description | | ------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `query` | `string` | no | Free text over title, slug and tags | | `kind` | `string` | no | Universal item kind: avatar, wearable, vehicle, weapon, add\_on, home\_item, home\_shell, emote, prop (max length 32) | | `category` | `string` | no | Marketplace category (max length 32) | | `subCategory` | `string` | no | Form factor beneath the category, e.g. car / plane / rifle / art (max length 64) | | `tag` | `string` | no | Exact tag (max length 64) | | `hostKind` | `string` | no | Host class β€” vehicle / character / weapon. Answers both "add-ons for this class" and "things that TAKE add-ons". (max length 24) | | `builds` | `"include" \| "only" \| "exclude"` | no | How much of the result is BUILDS. Omitted = `include` (the default): builds come back alongside the bases and parts, which is what makes a plain topical search find a finished preset. `only` narrows to presets β€” use it when you want something already assembled. `exclude` returns BASES to build on, which is what you want before publishing a build of your own. | | `limit` | `integer` | no | (min 1, max 100) | ## `search_vault` [#search_vault] THE CONTINUUM VAULT CATALOG β€” a different table from search\_assets (which holds the props install\_asset installs), and the ONLY discovery tool that finds LOOKS and Scene Packages (packageType "scene": a ready-made place to pin as a world's `scene` instead of building one). `search_assets` hits the legacy raw-resource table, which has no Looks and never will; this hits `/api/v1/vault/catalog`, the table backing the website's Vault page, which lists every sealed, listed Continuum Package: Looks (pass packageType: "look"), characters, vehicles, weapons, devices, add-ons, materials. A LOOK (API identifier: `build`) is a free, never-owned dressed-character preset β€” its parts are still acquired individually from their own listings. Public: works signed out. Each hit that is a Look carries a compact `build` summary (its base and add-on slots); read the dressed character with model URLs and fit data via get\_look, or the parts and prices via get\_item\_build. | Argument | Type | Required | Description | | ------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | `string` | no | Free-text search over title and tags | | `packageType` | `string` | no | "look" is an alias for the API identifier "build" β€” use it to find Looks. Other Continuum Package types: character, vehicle, weapon, device, addon, material, animation, scene, gameplay, runtime-module, unreal-world, asset. Omit to search every type. (max length 32) | | `sort` | `"recent" \| "name"` | no | Sort order; omit for the backend default | | `cursor` | `string` | no | Pagination cursor from a previous nextCursor | | `limit` | `integer` | no | (min 1, max 100) | ## `track_asset` [#track_asset] Read the backend-measured usage receipt for one durable Vault asset id: current/lifetime world usage, last use, and published worlds. Unobserved usage remains zero/null rather than a fabricated popularity score. | Argument | Type | Required | Description | | --------- | -------- | -------- | ---------------- | | `assetId` | `string` | yes | Vault asset UUID | ## `update_asset_metadata` [#update_asset_metadata] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Update creator-owned Vault annotations only. Immutable artifact bytes, kind, skeleton, compatibility, and version are never mutated. Byte changes must enter through a sealed Continuum Package; direct Vault byte-first version uploads are retired. | Argument | Type | Required | Description | | ------------------------- | --------------------------------------------- | -------- | ------------------------------ | | `assetId` | `string` | yes | Creator-owned Vault asset UUID | | `title` | `string` | no | (min length 1, max length 150) | | `description` | `string \| null` | no | | | `tags` | `string[]` | no | | | `contentRating` | `"everyone" \| "teen" \| "mature" \| "adult"` | no | | | `acceptRatingAttestation` | `boolean` | no | | ## `update_item_distribution` [#update_item_distribution] Runs the bundled CLI (`helix item distribution update`): reprices or changes schedule/caps on an existing distribution. Item, channel, world and stable key are immutable. Pass unlimited to clear a cap and none to clear a schedule boundary. | Argument | Type | Required | Description | | ---------------- | ------------------------ | -------- | ----------------------------- | | `distributionId` | `string` | yes | | | `priceLix` | `number` | no | (min 0) | | `maxPerPlayer` | `integer \| "unlimited"` | no | | | `maxClaims` | `any` | no | | | `startsAt` | `string` | no | ISO-8601, or "none" to clear. | | `endsAt` | `string` | no | ISO-8601, or "none" to clear. | | `dryRun` | `boolean` | no | | ## `validate_item_definition` [#validate_item_definition] \[Experimental] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Prepare the CLI command that validates an item descriptor OFFLINE: the manifest authority validator, the vendored rules for the descriptor type, referenced-file existence, and the mesh precheck. A weapon is checked against the engine gun-config rules; an emote is checked against its own β€” the emote block and a resolvable clip, no gun config, and the MEASURED clip duration against its playback ceiling (10s oneshot, 60s loop). A PAIRED emote (an emote.roles.joiner block) is checked the same way on BOTH clips, plus the joiner offset bounds and the offer instant, which must land inside the measured initiator clip. Enter/exit TRANSITIONS (emote.intro / emote.outro) are measured too β€” 5s each, and on a "oneshot" intro + clip + outro together against the 10s ceiling β€” and are refused outright on a paired emote. Run it after any hand edit. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------------- | | `file` | `string` | yes | Path to the .helix-interactive.json descriptor. | ## `visual_qa_item` [#visual_qa_item] Runs the bundled CLI (`helix item visual-qa`) and returns its output and exit code: renders a published item's CURRENT package version headless in the real HELIX runtime (test-grounds world for vehicles AND whole avatars β€” the avatar is tried on as the player and walked β€”, the customiser for add-ons and wearables on every declared host/base, the item viewer for props, the item page for emotes, the Home walk-through for scenes), measures it (wheels at hubs, arch clearance, socket placement, on-body bounds, scale, NaN/invisible, textured; for avatars also materials resolved, no emissive wash-out, lit colour vs its own base colour, limb twist and skin tearing while walking), and with --submit records the backend's verdict β€” numeric asserts plus a vision critic β€” against that exact version, with a contact sheet. The visual check is ADVISORY: it records notes with fix hints and never gates a listing, a distribution, a version move or a publish (only a technical failure, an unusable file reported with its plain reason, or content safety blocks). Run it after every publish or new version, read the notes and the contact sheet, fix what you agree with, and re-run. Exit code 0 = pass (or warn: advisory notes only), 1 = fail (notes, never a gate), 2 = harness error (never a pass). To have the platform render it instead of this machine, use visual\_qa\_item\_hosted. | Argument | Type | Required | Description | | ---------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `itemId` | `string` | yes | Universal item id to render and judge. | | `submit` | `boolean` | no | Record the verdict on the backend (default true). False = local dry run only. | | `localCritic` | `boolean` | no | Also run the vision critic locally (needs ANTHROPIC\_API\_KEY or OPENROUTER\_API\_KEY in the environment). | | `storageState` | `string` | no | Playwright storageState JSON of a signed-in QA account; Try Now and the customiser need one (default $HELIX\_VQA\_STORAGE\_STATE). | | `hostIds` | `string[]` | no | Only fit onto these host/base item ids (add-ons, wearables). Default: every declared host/base. | | `packageVersion` | `string` | no | Judge this candidate Package Version instead of the current one β€” a version move on an already-offered item must pass here BEFORE the move. | | `vehiclePackage` | `string` | no | With packageVersion, for a vehicle: path to the candidate vehicle-package JSON the version will carry. | ## `visual_qa_item_hosted` [#visual_qa_item_hosted] Runs the bundled CLI (`helix item visual-qa --hosted`) and returns its output and exit code (create\_avatar and publish\_avatar\_version already wait for this verdict for you): asks the PLATFORM to render and judge an item's current package version on its hosted GPU runner β€” no Chrome, no GPU, no QA login on your machine. It queues one hosted run per (item, package version, manifest), waits, and exits 0 = passed (an avatar may be `verdict: warn`: advisory warnings\[], terminal, nothing else to wait for), 1 = failed (advisory notes: reasons and blockers\[] printed; never a gate), 2 = error / timed out, printing the contact sheet URL. Use this instead of visual\_qa\_item when this machine has no suitable browser (CI, a sandbox, a remote agent), or after a deferred Marketplace publish, which can queue a hosted run for you (the response carries hostedRun: \{ jobId, status }; follow it with visual\_qa\_item\_status). The hosted notes are advisory, the same as a local --submit run: nothing waits on them. A freshly sealed model can lag the CDN; the runner waits for it (\<=10 min) and a lag never uses an attempt. Retry (requeue a fresh job) after a failed/errored/exhausted run only once the item is fixed or the failure was infrastructure (retry:true). | Argument | Type | Required | Description | | ---------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `itemId` | `string` | yes | Universal item id to render and judge on the hosted runner. | | `retry` | `boolean` | no | REQUEUE this exact version after a failed, errored or exhausted hosted run: a fresh job whose attempts start again (waits out the server cooldown, \~10 min after the last run, up to timeoutMinutes). Use after infrastructure/mirror failures or once a fix is published as a new version. | | `wait` | `boolean` | no | Wait for the verdict (default true). False = queue it and return; read it later with visual\_qa\_item\_status. | | `timeoutMinutes` | `integer` | no | How long to wait for the verdict (default 30). | ## `visual_qa_item_status` [#visual_qa_item_status] Runs the bundled CLI (`helix item visual-qa --status`, read-only) and returns an item's hosted visual-QA state: the latest hosted job for its CURRENT package version (queued / rendering / passed / failed / error, attempts, reasons, error), the verdict (pass, warn, fail or error: an avatar `passed` job with verdict warn is TERMINAL: advisory warnings\[], nothing else to wait for) and its warnings\[] / blockers\[], the contact sheet URL, and whether the current version has a passing run (advisory; nothing waits on it). Needs no login and changes nothing. Exit code 0 = passed (or warn) or still in flight/none, 1 = failed, 2 = error. Use it to follow a hostedRun from a deferred publish. | Argument | Type | Required | Description | | -------- | --------- | -------- | ------------------------------------------------------- | | `itemId` | `string` | yes | Universal item id whose hosted visual-QA state to read. | | `json` | `boolean` | no | Machine-readable output (the hosted job record). | # Vehicles (/docs/cli/mcp-tools/vehicles) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `check_vehicle_addon` [#check_vehicle_addon] \[Experimental] Prove a vehicle add-on will fit and LAND WHERE IT SHOULD before it is published (executes the bundled `helix vehicle addon check`). Directory: addon.json \{ title, fits, ops, description?, tags?, version?, provenance? } + model.glb (tune-only parts ship none). Checks: the fits/ops contract (at most 32 ops; 64 for a clip-set), the model's scene.extras.helixMeshFrame (a socket-frame part must be centred on its mount β€” the hub for wheels), and per host: it is a vehicle, its drawn wheel origins are AT THE HUBS, every hide glob matches a host node, the host board GRANTS the hide (the standard board grants only **stock** globs, so a wheel hiding rim\_\* fits ZERO hosts until the host grants it) and EXPOSES every tunable path the part requires; then the backend's own fit dry run reports how many hosts it really fits; and for `tune` ops the DRIVE EFFECT on each host, measured with the live runtime's own physics and add-on fold (0-100, top speed, quarter mile, skidpad g and body roll, 60-0 at full and 30% pedal, handbrake), each op judged on a figure it can move β€” a part that changes nothing is an error, an op that cancels another a warning. A performance-slot part (engine, ECU, gearbox, exhaust, brakes, suspension, anti-roll bars, intake) with NO tune op and no sound mount is refused up front (round 5's "competition exhaust" did nothing). It also runs the web customizer's own meshless guard on the exact add-on definition publish would seal: a part with no model.glb must be a verified tune-only (or sound-only) definition, otherwise the customizer refuses to equip or save it and the check reports ADDON\_CUSTOMIZER\_REFUSES with the customizer's exact message (round 8 sold four tune-only parts that every older check called fit). The drive effect is measured ungoverned (speedLimiterKph is ignored; no add-on can add or remove a speed limiter). Exit 0 = fits every named host and does something. | Argument | Type | Required | Description | | ----------- | --------- | -------- | ------------------------------------------------------------------------------ | | `dir` | `string` | yes | Absolute path to the add-on directory. | | `glb` | `string` | no | Absolute path to the add-on model when the directory holds more than one .glb. | | `noMeasure` | `boolean` | no | Skip the drive-effect measurement of tune ops. | | `json` | `boolean` | no | | ## `check_vehicle_audio` [#check_vehicle_audio] \[Experimental] Prove every sound a vehicle declares will actually play, is SOURCED (real recordings with provenance, never synthesised) AND sounds like an engine β€” the silent-car check visual QA never makes (executes the bundled `helix vehicle audio-check`). PROVENANCE: it prints, for every slot and bank, its source β€” sim, mod, url, source file, sha256, extractor β€” and REFUSES synthesised or generated audio (AUDIO\_SYNTHESISED), FAILS a car without sourced engine audio, i.e. one that would fall back to the engine's built-in synthesised engine (AUDIO\_ENGINE\_UNSOURCED), and FAILS any EMPTY slot and any slot that would fall back to a platform default (the generated default sound pack, synthetic starter, runtime tyre/skid/handbrake noise) β€” all 31 slots must be sourced (AUDIO\_SLOT\_UNSOURCED); the only permitted empty slots are a component the real car physically lacks (turbo slots on an NA car, reverseBeep, clutch on an automatic/CVT, EV combustion slots and, on a single-speed drive, shiftUp/shiftDown; transmissionWhine stays required; an EV is detected from physics, its motor ladder is a recording, and audio.drivetrain:"electric" FAILS as AUDIO\_SYNTHESISED), declared consumed:false, absent:"not-fitted" plus a reason, and exhaustLadder absent:"combined-with-engine" beside a sourced engine ladder. Also refused: AUDIO\_MIX\_FIELDS\_MISSING (engineGainDb/exhaustGainDb missing or non-finite, or minLoadMix/maxLoadMix/muffling outside 0..1 β€” a car shipped without them froze in Try Now), AUDIO\_PROVENANCE\_MISSING (a recording that says nothing about its origin) and AUDIO\_SLOT\_FITMENT\_MISMATCH (a not-fitted claim the physics contradicts, or turbo sounds on a non-turbo car). The fix is import\_vehicle\_audio from the most accurate simulator mod (then the BeamNG base game, another sim, real recordings), never synthesis. It selects samples exactly as the engine does (every rung of the engineBlend/exhaustBlend ladders, and each slot asset named by its buffers) and requires each to resolve (pre-publish: a WAV beside the package; published: HTTP 200 with the exact declared bytes and sha256), to be PCM WAV, and to be audible rather than digital silence. A cid-only row, a 404, a re-encoded file or an MP3 each fail. Three further gates: every ladder rung's measured fundamental must match its rpm label against the firing fundamental for the declared sampleCylinderCount/strokes/crankLayout (a rung an octave or more off is refused β€” the round-7 octave-low ladders), engine and exhaust must be separate sourced recordings (a mod with ONE combined recording omits exhaustBlend and declares exhaustLadder absent:"combined-with-engine"; both blends on one sourced recording is only a warning, AUDIO\_ENGINE\_EXHAUST\_SHARED\_RECORDING), and each rung must be a real broadband engine sound, not a near-pure synthetic tone (measured by autocorrelation and spectral noise floor; a beep passes pitch but fails this). CONTENT GATES (thresholds calibrated so the 993, recorded horns and field recordings pass): AUDIO\_CLIPPED (flat-topped runs at full scale or at the file's own peak), AUDIO\_DC\_OFFSET, AUDIO\_LADDER\_FIXED\_FORMANTS (one resonator stack stamped on every rung), AUDIO\_LADDER\_FIXED\_TONE (a fixed-frequency line in most rungs), AUDIO\_LADDER\_RUNG\_COPY (a rung resampled from its neighbour) and AUDIO\_HORN\_SYNTHETIC (an exactly periodic horn, warn) β€” read\_skill helix-vehicles reference "audio", "Content gates". Also refused: two roles that are one recording (shift up = shift down, horn = an engine rung, an exhaust rung = an engine rung resampled), and a rev-limiter sample longer than 0.35 s (the runtime re-fires it every 60 ms, so a long clip plays as a held tone). Pass `reference` (a recording of the real car idling) to compare your idle voice with it. Pass a package directory/JSON before publishing, or a published vehicle item id to read back what the runtime gets. Exit 0 = every sample plays, is sourced and is a real engine sound; 1 = a sample will not play, a slot is empty or unsourced, audio is synthesised, or a gate fails. | Argument | Type | Required | Description | | ----------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `string` | yes | Absolute path to the package directory or vehicle-package.json (pre-publish), or a published vehicle item UUID (read-back). | | `reference` | `string[]` | no | Recordings of the REAL car to compare the idle voice with: an absolute path to a WAV (mp3/m4a/ogg need ffmpeg) of the engine idling, or "slot=/abs/path" for a named slot (horn=, startup=, ...). Several clips: the median is compared. Spectral centroid and firing roughness; warning beyond x2.5, error beyond x10. | | `json` | `boolean` | no | | ## `check_vehicle_cabin` [#check_vehicle_cabin] \[Experimental] Judge every declared seat of an UNPUBLISHED (or published) car's cabin OFFLINE, in about a second, with no browser and no sign-in (executes the bundled `helix vehicle cabin-check`). It fits the cabin with the platform runtime's own fitCabinToBody and seats the runtime's canonical legs at the fitted pose, measured against every triangle of the model, then reports per seat β€” any seat count (2, 2+2, 4, 5, 7) β€” H-point height over the floor, how far the runtime had to move the seat, head clearance to the roof's inner surface, knee, shin and foot clearance, the legs against the seat ahead and the tunnel/console, the rear footwell under the front seats (free height over the foot) and the driver's legs against the wheel, each PASS / WARN / FAIL with millimetres and what it is against. Iterate rear seats with this (seconds), then confirm once with preview\_vehicle(\{ occupants: true }), which boards every seat. A WARN within a few millimetres of the seat ahead is a likely live clip (a real avatar's legs run longer than the canonical ones): clear it. Exit 0 = no FAIL (warnings may remain), 1 = a seat FAILs, 2 = could not run. validate\_vehicle with cabin: true runs the same check. | Argument | Type | Required | Description | | --------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `target` | `string` | no | Absolute path to the vehicle package directory (vehicle-package.json + model.glb) or the package JSON beside its model. | | `glb` | `string` | no | Absolute path to the GLB when target is a package JSON (or the directory holds several). | | `itemId` | `string` | no | Check a PUBLISHED vehicle item's stored package and drawn model instead (public read), e.g. a control car. | | `offline` | `boolean` | no | Use only the cached engine runtime; never fetch it. | | `json` | `boolean` | no | Machine-readable report. | ## `check_vehicle_host` [#check_vehicle_host] \[Experimental] Read a published vehicle's add-on surface the way the customizer sees it (public reads, no sign-in): its slot board (host kind, measured sockets / nodes / tunable paths, hide grants) and every add-on that FITS it, grouped by slot type with price, against the required minimum parts set (wheels, exhaust, ECU, gearbox, suspension, brakes, anti-roll bars; spoiler where the real car has one). Also runs the web customizer's own add-on guard on EVERY fitting add-on's exact Package pin (the CLI's vendored copy of the customizer's meshless guard, public reads): a part the customizer would refuse to equip or save ("Add-on definition declares visual geometry but has no public web GLB.", a visual mount with no GLB, a wrong root) is named with the customizer's message. Also fails when the car's GLB lacks the nodes a host-frame add-on binds to (HOST\_BIND\_ROOT\_MISSING: no vehicle\_root node; HOST\_BIND\_MOUNT\_NODE\_MISSING: an offered boot/bonnet socket with no node; HOST\_BIND\_ROOT\_NOT\_OWNER: the lid is outside vehicle\_root), and when any published add-on's bind root is absent from the car: the message names the node, the slot and the add-on and says to add an empty node of that name over the car's top-level nodes. Fails when the car is not a host (its GLB had no asset.extras.HELIX\_vehicle at mint), when the board has no sockets or no tunable paths, when a required part is missing or not for sale, or when any fitting add-on would be refused by the customizer. Run it after the car is published and after each of its add-ons. | Argument | Type | Required | Description | | -------- | -------- | -------- | -------------------- | | `itemId` | `string` | yes | The vehicle item id. | ## `get_vehicle_reference_package` [#get_vehicle_reference_package] \[Experimental] Copy the reference vehicle package β€” the complete helix.vehicle-package/2 of the live Porsche 911 (993), which passes visual QA and drives live β€” to a path you name, and return a short summary of its blocks. It is about 75k characters of JSON, too large for any tool output, so it is never returned inline: read the copied file block by block. Its notes are stripped of the 993's own provenance; replace EVERY number, citation and slot note with your car's. Its AUDIO block is shape only: the 993's sounds are not your car's, so replace the whole block with your car's sound from its most accurate simulator mod (import\_vehicle\_audio, never synthesised) and give every slot its provenance. What to keep, change or delete: read\_skill(\{ name: "helix-vehicles", reference: "reference-package" }). | Argument | Type | Required | Description | | ----------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `out` | `string` | yes | Absolute path of the file to write (…/vehicle-package.json), or a directory to write reference-package.json into. | | `overwrite` | `boolean` | no | Replace an existing file. Off by default, so your edited package is never clobbered. | ## `import_vehicle_audio` [#import_vehicle_audio] \[Experimental] AUDIO STEP ONE for every vehicle, whatever its mesh source: extract the car's real recorded sound from the most accurate simulator mod (executes the bundled `helix vehicle audio-import`; minutes, FMOD bank decoding). HARD RULE: NEVER generate or synthesise vehicle sound (no resynthesis, no generate\_audio, not even for a slot nothing provides, and never rely on the platform's default sounds: the synthesised fallback engine, the generated default sound pack, runtime noise); ALWAYS source real recordings, from BeamNG mods as much as possible, and a car ships a sourced recording for EVERY one of the 31 runtime slots (the only empty slots are a component the real car physically lacks, declared consumed:false, absent:"not-fitted" plus a reason, and exhaustLadder absent:"combined-with-engine" beside a sourced engine ladder; doors are required). Audio step one, whatever the mesh source: find the most accurate simulator mod of that exact car (BeamNG first; Assetto Corsa/ACC, rFactor 2, Automobilista 2 where theirs is more accurate), choose it by MEASURING candidates against real recordings of the car (firing-fundamental pitch track vs rpm, spectral centroid, roughness, idle character, limiter cadence), and extract it with import\_vehicle\_audio. Source: a BeamNG mod directory or .zip, an Assetto Corsa car directory or .bank, or a single .bank. It decodes the mod's engine and exhaust rpm ladders (row 0 = off-throttle/low load, row 1 = on-throttle) and its one-shots (startup, shutdown, idle, revLimiter, shiftUp/shiftDown, turboSpool, blowOff, backfire, tyreSqueal, doorOpen/doorClose, horn, intake, transmissionWhine, clutch, impacts, suspension, reverseBeep) to 16-bit PCM WAV under \/audio/ and writes \/audio.json β€” an `audio` block (bank + slots) to merge into vehicle-package.json β€” where EVERY consumed slot and bank carries provenance \{ kind: "sim-mod" | "sim-official" | "recording", sim, mod, modUrl?, sourceFile, sourceSha256, event?, extractor }. map is a slot-map JSON that builds one car from several sources (one mod's ladder, another mod's horn; stock events by exact take, trim, concat, real recordings, absent:"not-fitted") β€” read\_skill helix-vehicles reference "audio-import". EVERY one of the 31 slots must come out sourced (engineLadder, exhaustLadder, idle, startup, shutdown, shiftUp, shiftDown, clutch, transmissionWhine, turboSpool, blowOff, intake, revLimiter, backfire, suspension, impactLight, impactMedium, impactHeavy, tyreSqueal, tyreScrub, tyreSpin, tyreRollSlow, tyreRollFast, indicatorTick, skid, brakeSqueal, handbrake, reverseBeep, horn, doorOpen, doorClose); a slot the best mod lacks is filled, in order, from another BeamNG mod of the same engine or car family, the BeamNG base game's stock banks and vehicles/common sounds, another simulator's mod, real recordings of the car β€” never another car's sound, never synthesised, never a platform default. The only permitted empty slots are a component the real car physically lacks (turboSpool/blowOff without a turbo, reverseBeep, clutch on an automatic/CVT, EV combustion slots), declared consumed:false, absent:"not-fitted" plus a reason, and exhaustLadder absent:"combined-with-engine" beside a sourced engine ladder when the mod records one combined set. Doors are required. HOW TO PICK THE MOST ACCURATE MOD: measure every candidate against real recordings of the car β€” firing-fundamental pitch track vs rpm, spectral centroid, roughness/harmonic balance, idle character, limiter cadence β€” with the commands in read\_skill(\{ name: "helix-vehicles", reference: "audio" }), never by reputation. Writes \/audio.json (all 31 slots, plus the five mix fields the runtime reads: from the map, basePackage, the mod's soundConfig, else engine defaults) and \/audio-import.json (the report: every slot's decision, source, alternatives and reason, and the unmapped source sounds). Then run check\_vehicle\_audio. Exit 0 = every slot written with provenance; 1 = the source could not be read, yielded no engine audio, or left slots unfilled (the output lists them). Answers NOT AVAILABLE when the bundled CLI has no audio-import command (it never improvises a synthesised substitute). | Argument | Type | Required | Description | | --------------- | ---------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `source` | `string` | yes | Absolute path to the mod: a BeamNG mod directory or .zip, an Assetto Corsa car directory, or an FMOD .bank. | | `out` | `string` | yes | Absolute path of the vehicle package directory to write audio/\*.wav and audio.json into. | | `sim` | `"beamng" \| "ac" \| "auto"` | no | Which simulator the source is from (default auto-detect). | | `vehicle` | `string` | no | BeamNG vehicle id inside the mod (when it holds several). | | `config` | `string` | no | BeamNG configuration (.pc file) whose soundConfig is wanted; a trim can use a different engine blend. | | `map` | `string` | no | Absolute path to a slot-map JSON merging slots from several sources into one car. | | `beamngRoot` | `string` | no | Absolute path to the BeamNG.drive install (or set HELIX\_BEAMNG\_ROOT), so stock FMOD banks and engine blends a mod refers to by name (art\_sound.zip, audio.zip, vehicles/common.zip) resolve and slots no part names get what the game itself plays. | | `cylinders` | `integer` | no | The RECORDED engine's cylinder count (the ladder pitch model). Derived from a BeamNG engine part when it says; REQUIRED for an Assetto Corsa car and for a BeamNG mod whose count would only be a guess. (min 1, max 16) | | `prefer` | `"exterior" \| "interior"` | no | Microphone set to prefer when the source has both (default exterior). | | `acNativeRungs` | `boolean` | no | Assetto Corsa: keep only rungs recorded at their own reference rpm (no FMOD auto-pitch varispeed). | | `basePackage` | `string` | no | Absolute path to an existing vehicle-package.json, its directory, or an audio.json: its audio block supplies the mix (engineGainDb, exhaustGainDb, minLoadMix, maxLoadMix, muffling) so re-importing a PUBLISHED car keeps its current mix. Use it whenever the car already exists. | | `modUrl` | `string` | no | Where the mod was fetched from; recorded in every slot's provenance. | | `json` | `boolean` | no | | ## `preview_vehicle` [#preview_vehicle] \[Experimental] Render an UNPUBLISHED vehicle (a package directory, or vehicle-package.json + GLB, that exists only on disk) headless in the REAL HELIX runtime and measure it (executes the bundled `helix vehicle preview`; several minutes), before the first mint locks groundToBodyOrigin forever. Nothing is uploaded or recorded. Returns a per-wheel stance table and PASS/FAIL per check β€” ride height vs your declared groundToBodyOrigin (with the value to declare instead), wheels at hubs, arch clearance, spin, steering, panels, seats, rolling radius vs wheelRadius β€” and writes fixed appearance poses (front, side, rear, top, front34, rear34, cockpit under \/poses/, plus poses.jpg) and a contact sheet you must LOOK at beside your reference photos. Pass referencePaint (the median body-paint colour of the reference photos, #rrggbb) to also gate the RENDERED paint as Try Now daylight draws it: the body paint alone from six cameras with the world's own sun, exposure, tone mapping and local lights, judged on hue, saturation, lightness and highlight wash (vehicle.paint.*; a red that renders salmon or a bonnet that blows to white fails; white/silver/grey/black are judged on tint and lightness). Pass referenceWheel (a face-on close-up photo of the real wheel; optional spokeCount, finish, centreCap, crop) to also render the candidate's wheel square-on and fail vehicle.wheels.design.* on spoke count, finish or missing centre cap. It also presses every lamp key and fails vehicle.lamps.each\_function\_lights naming a lens that stays dark. occupants: true seats an avatar in EVERY declared seat, rear rows included (a seat the stand-in room has none of is granted in the preview's own page, so a preview is single-client), prints the CABIN FIT (where the runtime actually put each seat, and fails when it moved one more than 30 mm or refused it) and measures the cabin the way visual QA will (head clearance, hips, feet, hands on the rim, penetration). It never runs the vision critic: only a submitted or hosted visual QA is a gate result. Seats board through the candidate's OWN seat ids. OCCUPANTS AND THE GAUNTLET INSPECTION SET ARE ON BY DEFAULT: every seat is occupied and the hands are measured over time (blocking: hands\_on\_wheel\_steering β€” both driver hands on the rim at centre, left lock and right lock; hands\_follow\_wheel β€” they travel round with the wheel; hands\_at\_nine\_and\_three; fingers\_still and hands\_still β€” no twitching or grasping; plus posture torso\_angle, knees\_up, head\_in\_wings), and \/inspection/ gets small contact sheets, one LOOK question each: exterior 8 angles day and night, each lamp lit at night, wheels, panels open, every seat from first person / side glass / over the shoulder / windscreen, the driver steering left/centre/right, and still-hands frames (preview\.inspection.complete fails a set with a hole). OPEN EVERY SHEET beside the reference photos (read\_skill helix-gauntlet). With item and no target and no addon it inspects a PUBLISHED car exactly as the runtime serves it. With addon: \[\...] it previews UNPUBLISHED add-ons instead, fitted by the engine's own fold to the candidate car or to a published car (item), and measures the DRAWN part in world space against its anchor: wheels within 1 cm of each hub, socket-framed parts on their socket, host-framed parts inside the car and not at its origin (the classic part-at-the-car-centre bug); a tune-only add-on has its ops read back from the live tuning and the v2 driveline. A host-frame add-on whose bind root/socket the car's GLB lacks is refused in under a second, before any browser (HOST\_BIND\_\*); a runtime composition failure ('\[helix-vehicle] composition failed') or page error before the car appears fails immediately with its message instead of a 120 s timeout. Needs a `helix login` (a QA account). Exit 0 pass, 1 fail, 2 could not measure. | Argument | Type | Required | Description | | ---------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `string` | no | Absolute path to the vehicle package directory (vehicle-package.json + one .glb) or to the package JSON. Omit only with addon + item to fit add-ons to a published car. | | `glb` | `string` | no | Absolute path to the GLB when target is a package JSON (or the directory holds several). | | `out` | `string` | no | Directory for frames, poses/, poses.jpg, contact-sheet.jpg and preview\.json (default \/.helix-preview). | | `occupants` | `boolean` | no | Default true: every seat occupied, posture, hands while steering, still fingers, cabin fit. false only for a quick iteration (never a gate result). | | `inspection` | `boolean` | no | Default true: render the gauntlet inspection set and write the contact sheets under \/inspection/. false only for a quick iteration (never a gate result). | | `referencePaint` | `string` | no | The reference body paint colour (#rrggbb): the median of the body-paint pixels across several reference photos. When set, the body paint is rendered alone from six cameras in the Try Now daylight (the world's own sun, exposure, tone mapping and lights) and judged on hue, saturation, lightness and highlight wash (white, silver, grey and black are judged on tint and lightness instead). Blocking asserts vehicle.paint.hue\|saturation\|lightness\|glare; a failure names the paint material's values and the fix (linear baseColorFactor, metallic, roughness; never exposure). | | `paintTolerance` | `number` | no | Maximum circular hue distance in degrees the rendered body may sit from a chromatic referencePaint (default 12). | | `referenceWheel` | `object` | no | Reference wheel close-up. When set, the candidate's front-left wheel is rendered square-on (wheelface/candidate.jpg) and judged on spoke count, finish and centre cap against the photo: blocking vehicle.wheels.design.spokes\|centre\_cap and .finish (a neighbouring finish only warns). Look at wheelface/candidate.jpg beside the photo. | | `addon` | `string[]` | no | Absolute paths of UNPUBLISHED add-on directories (addon.json + model.glb, or no model for a tune-only part) to fit to the car and measure. | | `item` | `string` | no | No target: the published vehicle item β€” alone, inspect it as the runtime serves it (the gauntlet on a listed car); with addon, the car to fit the add-ons to. | | `toleranceCm` | `number` | no | With addon: how far a drawn part may sit from its anchor (default: wheels 1 cm, other parts 3 cm). | | `json` | `boolean` | no | | ## `publish_vehicle` [#publish_vehicle] \[Experimental] Publish a vehicle end to end from a package directory, Package-first (executes the bundled `helix vehicle publish`; minutes, not seconds) and return its output and exit code. Directory: vehicle-package.json, model.glb carrying asset.extras.HELIX\_vehicle, the WAVs beside it or in audio/, and publish.json \{ title, description, tags, version, contentRating, provenance: \{ kind: "authored"|"imported"|"generated", source }, hostGrantExtensions: \{ "whl.wheel": \{ "hide": \["rim\_\*"] }, … } }. It validates (package, GLB, host manifest, the drive against targets, every audio sample and its provenance), REFUSES synthesised or generated audio (AUDIO\_SYNTHESISED) a car without sourced engine audio (AUDIO\_ENGINE\_UNSOURCED β€” the engine's built-in synthesised fallback engine is not an engine sound for a published car; run import\_vehicle\_audio first) and any empty or default-falling-back slot (AUDIO\_SLOT\_UNSOURCED β€” every one of the 31 slots needs a sourced recording; platform defaults are never an answer), uploads the GLB and WAVs as public objects, builds the root (mesh.visual pinned to the uploaded GLB, audio rows pinned, the hide grants sealed in), seals a public `vehicle` Package, makes the Item from that exact Version, and reads everything back (pin, package identity, mesh, audio, the add-on host board). It opens no offer. With itemId + version + reason it seals a STAGED version into the same car's Package and moves the item (or stops at staged-awaiting-visual-qa on a listed car) β€” the only way to change a car that exists; never mint a second copy. ACTIVATE a new version of a LISTED car only by re-running this same call after visual QA passes (pin and properties.vehiclePackage move together); NEVER `helix item set-version` for a vehicle (it moves the pin but not properties.vehiclePackage, so the car keeps serving its old package). Resumable: re-run the same call after any failure (ledger in \/.helix-publish). Exit 0 = published and verified (or staged awaiting visual QA); 1 = a check failed; 2 = could not run. Run dryRun first. Answers NOT AVAILABLE when the bundled CLI has no vehicle mint. | Argument | Type | Required | Description | | --------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `dir` | `string` | yes | Absolute path to the vehicle package directory. | | `title` | `string` | no | Display title (default: publish.json title, then the package displayName). | | `description` | `string` | no | | | `tags` | `string[]` | no | | | `contentRating` | `"everyone" \| "teen" \| "mature" \| "adult"` | no | | | `glb` | `string` | no | Absolute path to the drawn model when the directory holds more than one .glb. | | `itemId` | `string` | no | Publish a new version of this existing vehicle item instead of minting one. | | `version` | `string` | no | Package Version to seal (default 1.0.0; required with itemId). | | `reason` | `string` | no | With itemId: why the version moves (1–500 chars). | | `noSimulate` | `boolean` | no | Skip the drive check against targets. | | `dryRun` | `boolean` | no | Validate, check audio (provenance and sourcing included) and run the seal-side validation (provenance.source \<= 256 chars, contentRating, Package shape); nothing is uploaded or charged. | | `json` | `boolean` | no | | ## `publish_vehicle_addon` [#publish_vehicle_addon] \[Experimental] Publish a vehicle add-on end to end from its directory, Package-first (executes the bundled `helix vehicle addon publish`; minutes): the full check\_vehicle\_addon (including the drive-effect measurement) β†’ upload the model (none for a tune-only part) β†’ seal a public `addon` Package β†’ make the Item from that exact Version (pinned at birth, unlisted, no offer) β†’ read back (pinned, the backend lists every host as a fit, it appears on each host's add-on rail). A hide the host has not granted stops it: the host car must seal the grant (hostGrantExtensions in its publish.json). Sell it with create\_item\_distribution once visual QA passes. Resumable. Exit 0 = published and verified. itemId + version publishes a changed part as a NEW VERSION of the same item (staged, then moved; a listed meshed part is held by visual QA: stage, run visual QA on that version, repeat the same call to finish). dryRun also runs the seal's refusals (provenance.source \<= 256 chars, contentRating, title/tag limits, Package shape, version clash). Answers NOT AVAILABLE when the bundled CLI has no add-on mint. | Argument | Type | Required | Description | | --------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dir` | `string` | yes | Absolute path to the add-on directory. | | `contentRating` | `"everyone" \| "teen" \| "mature" \| "adult"` | no | | | `glb` | `string` | no | Absolute path to the add-on model when the directory holds more than one .glb. | | `noMeasure` | `boolean` | no | Skip the drive-effect measurement of tune ops. | | `itemId` | `string` | no | Publish a NEW VERSION of this existing add-on item instead of minting one (needs version). | | `version` | `string` | no | Package Version to seal, MAJOR.MINOR.PATCH; with itemId, the next version of that part (e.g. 1.0.1). A version name already sealed with different bytes is refused. | | `reason` | `string` | no | With itemId: the audited version-move reason, 1-500 chars (default names the version). | | `dryRun` | `boolean` | no | Run the check and the seal-side validation only; nothing is uploaded. | | `json` | `boolean` | no | | ## `simulate_vehicle` [#simulate_vehicle] \[Experimental] Drive an UNPUBLISHED vehicle package offline on the platform's own vehicle runtime (executes the bundled `helix vehicle simulate`; it fetches and caches the live humanoid-character runtime) and return 0–100 km/h, 0–60 mph, quarter mile, top speed, ungoverned top speed, 100–0 km/h and 60–0 mph braking, skidpad lateral g, turbo lag, the gears used and the rest pose (grounded, pitch and roll ≀ 3Β°), each compared with the package's `targets` (a row passes when |simulated βˆ’ target| / target ≀ its tolerance fraction). KEYBOARD LAUNCH: it also launches the car the way a keyboard player does (W 0β†’1 in one tick, A/D taps with 100 ms reactions holding a lane; W+A/D is normalised to 0.707 throttle; a land-v2 car has traction control only when its package authors `simulation.land.tractionControl`, which this models exactly as live and reports on a `traction control:` line) and nudges the yaw 0.2 rad/s hands-off at 20/40/60/100 km/h and as each upshift re-engages: keyboard 0–100 vs the instrument, wheelspin per gear, post-shift yaw, lane offset. A nudge that grows into a spin FAILS "not drivable on a keyboard" (validate\_vehicle reports it as the warning SIMULATED\_KEYBOARD\_LAUNCH\_UNDRIVABLE; tune only with sourced levers, or author traction control (the real system, or a declared playability aid) β€” physics-from-specs "Drivable on a keyboard" and "Traction control"). Exit 0 = every compared figure passes and the keyboard launch does not FAIL; 1 = a figure is out of tolerance, the rest pose is wrong or the car is not drivable on a keyboard; 2 = could not simulate. validate\_vehicle runs the same check. It also reports the SHIFT SCHEME: a gearbox with `auto.enabled` false cannot shift itself, so the figures cannot be reached and the live car will pin 1st gear unless the player uses the manual keys β€” and a manual-kind box with auto enabled is reported as "simulated with auto-shift; live manual-shift unverified". Run it after every physics edit and before the first mint: it is the only way to know a car is too quick or stops too short without publishing it. Only grip.peak and suspension.\* (and, for a turbo car, the derated low end of the curve) are yours to tune toward the targets (read\_skill helix-vehicles reference "physics-from-specs"). Also prints the engine-policy warnings SPEED\_LIMITER\_IS\_A\_MARKET\_GOVERNOR (speedLimiterKph below 0.9 Γ— the ungoverned top speed: the platform car runs ungoverned and the live solver never applies a limiter; null it and set targets.topSpeedKph to the ungoverned figure), TORQUE\_CURVE\_EXCEEDS\_DECLARED\_CRANK\_FIGURES (a torque curve or peak field more than 5 % above the maker's declared crank figures: put losses in drivetrainEfficiency and drag, never inflate the curve) and AWD\_DRIVE\_BIAS\_\* (driveBiasFront must be strictly between 0 and 1 and equal centre.torqueSplit on an AWD car) β€” see physics-from-specs. | Argument | Type | Required | Description | | ------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `string` | no | Absolute path to the vehicle package directory or its vehicle-package.json. | | `itemId` | `string` | no | Simulate a PUBLISHED vehicle item's stored package instead (public read) β€” for example a control car such as the live 993. | | `spec` | `string` | no | Absolute path to an extra target file to compare against: a package, \{ targets: \{…} }, or a reference spec with perf.\* figures (perf.0\_100\_kmh\_s, perf.braking\_100\_0\_m, …; a leaf is a number or \{ value, tol\_pct }, default Β±10 %). | | `offline` | `boolean` | no | Use only the cached runtime; never fetch it. | | `noKeyboardLaunch` | `boolean` | no | Skip the keyboard launch and its hands-off yaw probe (faster; only while iterating on figures). | | `json` | `boolean` | no | Machine-readable results. | ## `validate_vehicle` [#validate_vehicle] \[Experimental] Run the HELIX vehicle validator (executes the bundled `helix vehicle validate`) on a vehicle package (helix.vehicle-package/2) and, with glb, its model β€” BEFORE any publish β€” and return its report and exit code. It checks the engine's own package validator (a package with an error never spawns, and publish does not catch it), the publish gate (distributable provenance, wheelVisuals, required fields), groundToBodyOrigin = wheelRadius + staticRideHeight (locked after first publish), the GLB host manifest in asset.extras.HELIX\_vehicle (without it the car is no add-on host), the host-frame bind nodes (a car offering a boot/bonnet socket needs the lid's mount node and a vehicle\_root node owning it, or a bound add-on throws in the runtime and the car is never presented: HOST\_BIND\_*), all four wheel nodes with their origins AT THE HUB, frame orientation, the platform budgets (24 draws, 90k triangles, 48 materials, 2048 px), articulation panel nodes and their controls, the specialty componentBuild digest (prints the computed value), and the drive figures against `targets` (simulate\_vehicle; skip with noSimulate). Exit 0 = no errors. It also prints, as warnings, SPEED\_LIMITER\_IS\_A\_MARKET\_GOVERNOR, TORQUE\_CURVE\_EXCEEDS\_DECLARED\_CRANK\_FIGURES and AWD\_DRIVE\_BIAS\_* (the platform car runs ungoverned on the maker's declared crank figures; physics-from-specs). With cabin: true it also runs check\_vehicle\_cabin's per-seat fit (errors CABIN\_CHECK\_FAIL, warnings CABIN\_CHECK\_WARN); a car that declares more than two seats is pointed at it. read\_doc(\{name:"vehicles"}) is the contract it enforces. | Argument | Type | Required | Description | | ---------------- | --------- | -------- | ----------------------------------------------------------------------------------------------- | | `vehiclePackage` | `string` | yes | Absolute path to the vehicle package JSON. | | `glb` | `string` | no | Absolute path to the vehicle GLB to check against the package. Always pass it before a publish. | | `noSimulate` | `boolean` | no | Skip the drive-figure check against targets (structure only). | | `offline` | `boolean` | no | Drive with the last cached engine runtime; never fetch it. | | `cabin` | `boolean` | no | Also run the offline cabin check (every declared seat; FAIL rows are errors). Needs glb. | | `json` | `boolean` | no | Machine-readable issue list. | # Worlds (/docs/cli/mcp-tools/worlds) {/* Generated from `@hypersoniclabs/helix-mcp@0.2.16-helix3.209` by `npm run docs:generate`. Do not edit by hand. */} MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run. ## `analyze_scene_performance` [#analyze_scene_performance] EXECUTES the Creator CLI scene performance analyzer and returns its report to the agent. Call after the first build or imported Scene exists, then again with final snapshot/Package/runtime evidence. It runs canonical Scene/Package validation, and unresolved non-bundle resources keep affected checks unknown. It separates authored mesh-definition triangles from runtime submitted triangles; file-local/slot/unique/source/native/Package/observed Palette materials; declared/GPU texture costs; owned/transitive/cold/warm delivery; and static estimated reference-budget utilization from supplied runtime claims. Runtime proof records compile/stream/GI readiness, grounded route, occupancy, frame count/duration, bundle/dirty-code identity, canvas/DPR, and internal compositor resolution separately. Unknown metrics stay unknown and never improve the result. Read scene-performance for the interpretation and matched visual-QA workflow. | Argument | Type | Required | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute directory containing the built bundle or Scene-owned resources | | `scene` | `string` | no | Exact Scene v2 JSON; supplies authoritative resource roles/integrity, surfaces, and stream groups | | `package` | `string` | no | Exact Continuum Package version JSON; supplies semantic material bindings/dependencies and transitive closure | | `snapshot` | `string` | no | inspect\_world snapshot JSON for visible-scene triangles | | `perf` | `string` | no | Runtime receipt. The report treats its values as supplied claims and reports byte/identity binding separately. | | `profile` | `"web-low-mobile" \| "web-mobile-balanced" \| "web-high-mobile" \| "web-desktop-balanced" \| "web-pc-ultra" \| "web-standalone-vr" \| "web-pc-vr"` | no | Existing Palette Showcase reference profile; default web-desktop-balanced. It is a comparison reference, not a universal Scene guarantee. | ## `bake_world_lighting` [#bake_world_lighting] Bake a world's lighting field from its BUILT bundle and point its visual contract at the artifact (same code path as `helix bake-lighting`): serves the build headlessly, drives the in-page bake driver across sun keyframes plus the practical lights, audits and repairs the probe field, then writes helix.visuals.bake.json and patches helix.visuals.json to mode "baked" in BOTH the source public/ and the built root. USE IT FOR a bounded, static-geometry world whose visual contract declares β€” or should declare β€” mode "baked": interiors, arenas, dioramas, anything whose geometry and lights do not move. A BOUNDED INTERIOR authors probes: \{ center, size } in helix.visuals.json to fit the bake volume to its room BEFORE baking β€” the default volume is sized for open ground, and the bake warns when its volume does not fit the play space (heed that warning by authoring the block and re-baking); an authored center also pins the still-dynamic world's runtime volume to the same box, so author the block at build time, not bake time. Run it AFTER npm run build, on the built world, never on an unbuilt source tree, and expect MINUTES of blocking GPU work (a small world is a minute or two β€” do not interrupt it, and do not call it in a loop). NOTHING IT WRITES SHIPS until the world is re-validated and re-published: build β†’ bake\_world\_lighting β†’ validate\_world β†’ publish\_world, and re-bake whenever the geometry, the lights, or any TRANSPORT knob in helix.visuals.json changes (sun, sky, environment, shadows, probes β€” the bake photographs all of them). LEAVE keys ALONE unless you know better: omitted, the engine plans them β€” ONE key for a static sun (timeOfDay.cycleSeconds 0), 9 for a cycle β€” and the pointer it writes records the count as bakedLighting.keys, which validate\_world re-checks against the world clock. Pass gpu: true to bake on the machine GPU through a system Chrome/Edge when the wait hurts (measured 41 s against 27 minutes on a 45 MiB world); it is not bit-reproducible across machines, so the software renderer stays the default. It fails loudly rather than write a wrong artifact β€” an unbuilt world, a world that never publishes the bake handle (nothing called createVisualRuntime, so it is not runtime-lit and has no probe field), a boot slower than timeoutSeconds, or the donor-occlusion gate β€” and the failure text leads with the page errors, which are the diagnosis. It never installs a browser: a launch failure relays the install instructions for the human. The decision tree (baked vs dynamic vs the opt-out) and the full helix.visuals.json reference: read\_doc(\{name:"lighting-world"}). | Argument | Type | Required | Description | | ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the world project directory (its dist/ is baked and its public/ contract patched) or to the built bundle directory itself | | `keys` | `integer` | no | Sun keyframes to capture. OMITTED = the engine plans them: ONE for a static sun (timeOfDay.cycleSeconds 0 or absent), 9 for a cycle β€” which is what you want, so leave it out. A static world baked at 9 keys pays \~9x the artifact bytes for eight hours nobody plays, and keys: 1 on a CYCLING world is refused outright. (min 1) | | `gpu` | `boolean` | no | Bake on the machine GPU through a system Chrome/Edge instead of the bundled software renderer β€” measured 41 s against 27 minutes on a 45 MiB world. NOT bit-reproducible across machines, which is why software stays the default: use it while iterating, and remember the shipped artifact is whatever the last bake wrote. | | `timeoutSeconds` | `integer` | no | Boot budget in seconds β€” the wait for the bake handle, NOT a cap on the bake itself. Omitted, the bake scales it with the bundle size (20 s per MiB, floor 300 s), which is normally right; raise it when the headless boot is slow for some OTHER reason. A world that never publishes the handle fails with that diagnosis instead. | ## `capture_world_screenshot` [#capture_world_screenshot] Capture headless screenshots of a built world via its in-page screenshot helper (Playwright, in-process β€” same code path as the `helix screenshot` CLI command). Returns a compact preview image per capture PLUS a text block listing every file written. LOOK at the returned image(s) and judge the framing before attaching one with set\_world\_thumbnail. FOR PLACEMENT QUESTIONS USE inspect\_world, NOT this tool β€” do not answer "is it floating / how far apart / does it fit" by looking harder; distances and depth read from an image are measurably unreliable. This is the CLOSING check for how the scene reads (framing, materials, lighting, mood), after the numbers are already right. Prefer a declared shot (world code calls screenshots.register β€” read\_doc "screenshots") over the camera/lookAt/fov override or blindMs, both non-deterministic fallbacks. | Argument | Type | Required | Description | | --------------- | ----------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the world's built bundle directory (same dir publish\_world takes) | | `shot` | `string` | no | Capture only this declared shot (default: every declared shot) | | `camera` | `number[]` | no | Override pose: camera position \[x, y, z] β€” bypasses declared shots, writes custom.png | | `lookAt` | `any` | no | Override pose: look-at point \[x, y, z] (only applied together with camera) | | `fov` | `number` | no | Override pose: field of view in degrees (only applied together with camera) | | `preview` | `boolean` | no | Also capture each shot at 2560x1440 to \.preview\.png | | `blindMs` | `number` | no | Explicit opt-in fallback for worlds WITHOUT the helper: skip the readiness wait, delay this many ms, then screenshot the canvas. Non-deterministic β€” a last resort, never a stable thumbnail source. | | `out` | `string` | no | Output directory (default: a "screenshots" folder next to `directory`) | | `browser` | `"bundled" \| "chrome" \| "msedge"` | no | Diagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct. | | `previewImages` | `integer` | no | How many downscaled preview image blocks to return (default 3). Pass 0 to skip image blocks entirely β€” right when capturing a gallery for the human rather than inspecting framing yourself; every written file path is always listed in the text block either way. (min 0) | ## `clear_world_thumbnail` [#clear_world_thumbnail] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Revert a world thumbnail/preview slot to the auto-generated placeholder. | Argument | Type | Required | Description | | -------- | -------------------------- | -------- | ------------------------------------------------------------- | | `slug` | `string` | yes | World slug | | `kind` | `"thumbnail" \| "preview"` | no | Media slot to clear: thumbnail or preview. Default thumbnail. | ## `create_world` [#create_world] Mint a world's IDENTITY β€” the row, as a Draft with no build β€” so products and achievements can be registered BEFORE the first publish. Both registries live outside the bundle and hang off a world id, so on a brand-new project register\_world\_product and register\_achievement fail with "No world of yours has the slug ..." until this has run; publishing is what adds a build, not what creates the world. Idempotent for a slug you already own β€” the existing world is reported and nothing is sent; a slug someone else owns is refused by the backend. The slug is globally unique and permanent, so choose it once. | Argument | Type | Required | Description | | -------------------------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- | | `slug` | `string` | yes | URL identifier: 3-50 chars, lowercase letters/digits/hyphens, starting alphanumeric. Globally unique and PERMANENT. | | `title` | `string` | yes | Display name players see, max 80 chars | | `description` | `string` | no | Catalog description, max 1000 chars | | `serverCreationPermission` | `"creator" \| "collaborators" \| "anyone"` | no | Who may stand up a persistent server for this world (server default: anyone) | ## `inspect_world` [#inspect_world] Measure the layout of a built world as DATA (same code path as `helix world inspect`): boots it headlessly, snapshots the scene graph β€” world positions, extents, oriented boxes, visibility, physics colliders β€” and runs placement checks: floating/sunk/intersecting geometry, collider-vs-visual divergence (including the classic half-extent/full-size slip), mirrored or degenerate transforms, stray coordinates. THE INNER LOOP FOR PLACEMENT: measure here until the numbers are right (this answers "is the bench 0.4 m in the air" exactly β€” a screenshot measurably cannot), then capture\_world\_screenshot ONCE before handing to a human. FINDINGS ARE MEASUREMENTS, NOT VERDICTS: check each against your intent. Hovering pickups, ramps embedded in the ground, and walls overlapping at their corners all warn on perfectly good worlds β€” a warn that matches what you meant needs no fix; a warn that surprises you is the bug. NEVER edit geometry just to silence a finding. WHAT IT CANNOT ANSWER: whether the scene READS as intended β€” materials, lighting mood, animation, art direction β€” that is the closing screenshot check. Requires the world to integrate @helix/engine-core/inspect (one line at end of boot: world.ready(\{ scene, body, spawn }) β€” read\_doc "world-inspect"; the scaffold recipe shows the wiring). EXISTING worlds scaffolded before this integration usually lack the call β€” check\_for\_updates(\{ projectDir }) reports the wiring state up front, cheaper than a 60s timeout. DENSE HAND-BUILT worlds warn a lot on intended decor (hanging lights read as floating): uniform same-parent runs collapse to one line automatically, and after you adjudicate, writeBaseline persists the verdict so later inspects report only what CHANGED. Object paths are canonicalized (content order) β€” stable across runs; coordinates remain the strongest cross-run handle. | Argument | Type | Required | Description | | --------------- | ------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the built world bundle directory (same dir publish\_world takes) | | `report` | `("anomalies" \| "roster" \| "colliders" \| "zones")[]` | no | Report sections (default \["anomalies"]). "roster" = the grouped object inventory with sizes/positions; "colliders" = the physics roster; "zones" = the declared multiplayer.json trigger volumes with their rest surfaces and touched geometry. Ask for what you need β€” a large world renders long. | | `json` | `boolean` | no | Return the raw \{ snapshot, analysis } JSON instead of the rendered report β€” for programmatic follow-up on exact numbers. CAUTION on large worlds: the raw snapshot is \~350 bytes PER OBJECT (a 5000-object world is \~2 MB) β€” prefer the rendered report (grouped roster, per-rule capped findings) and request json only when you need every number. | | `focusNear` | `number[]` | no | THE SINGLE-OBJECT LENS: report only the object(s) near this world-space point β€” the fastest way to check the thing you just placed, because YOU wrote its coordinates. Returns exact position, size, AABB, FACING (yaw + world direction, with TOWARD / AWAY from / side-on relations to nearby objects β€” how you catch the chair pointing away from its table), touching colliders, and only the findings involving it. Analysis still runs whole-world; this scopes the report. No naming needed. | | `focusRadius` | `number` | no | Radius for focusNear in meters (default 2). | | `focus` | `string` | no | Alternative lens: report only objects whose path contains this substring (case-insensitive) β€” for named objects and imported GLB assets, whose node names survive export. | | `settleMs` | `integer` | no | Settle time between world-ready and the snapshot (default 750 ms). Raise it for worlds whose props animate into place. (min 0) | | `browser` | `"bundled" \| "chrome" \| "msedge"` | no | Diagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct. | | `writeBaseline` | `boolean` | no | Accept ALL current findings into inspect.baseline.json beside the world project β€” after that, inspect reports only what CHANGED (new findings, plus accepted ones that no longer occur). Use AFTER adjudicating the findings against intent (the hanging decor is meant to hang), and tell the user you wrote it. Entries key on rule + world-space anchor, so they survive rebuilds and renumbering. | | `showAccepted` | `boolean` | no | Render baseline-accepted findings too instead of hiding them behind the baseline summary line. | ## `list_abilities` [#list_abilities] List the abilities published on HELIX Instant (e.g. fly, swim). Abilities plug into a system; optionally filter by the host system id. Returns slug\@version + title + target system. | Argument | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------------------------------- | | `system` | `string` | no | Only abilities targeting this system id (e.g. humanoid-character) | ## `list_achievements` [#list_achievements] List a world's registered achievements. Run it BEFORE publishing multiplayer rules to confirm every key an `awardAchievement` effect names actually exists β€” publish cannot check the registry, and a missing key fails silently at runtime. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ----------------- | | `worldSlug` | `string` | yes | Slug of the world | ## `list_my_worlds` [#list_my_worlds] List the worlds owned by the logged-in creator (including drafts), with their slugs and build state. No arguments. ## `list_templates` [#list_templates] List the available multiplayer world TEMPLATES β€” complete, publish-shaped worked worlds, one per capability cluster (presence, server/owned/shared/physics entities, collections, state machine, time, checkpoints, teams, voice channels). After get\_started(\{kind:"multiplayer"}), pick the closest and read it with read\_template(\{name}). No arguments. ## `list_world_products` [#list_world_products] List what a world sells: each registered product, the `pkey:` world code buys it by, its price and whether it is still on sale. Run it BEFORE writing purchase code to confirm the key you are about to name exists β€” an unregistered key fails only at runtime, as ProductNotFound. Omit worldSlug for every PUBLISHED world of the creator's and its registry (that route lists Published worlds only, so name the slug to see a draft world's). | Argument | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------------------- | | `worldSlug` | `string` | no | Slug of the world; omit for all your published worlds and what each sells | ## `placements_bake` [#placements_bake] Bake deterministic instanced placements on authored terrain from a helix.placements-source/1 JSON file: solid records placed exactly, decorative records scattered by rule. The CLI validates prepared prototype LOD/texture closure, masks and stable solid placements, then writes placements.json and pinned binary regions. Local authoring only; this does not publish. Read the terrain recipe first. | Argument | Type | Required | Description | | -------- | -------- | -------- | --------------------------------------- | | `source` | `string` | yes | Absolute path to placements source JSON | | `out` | `string` | yes | Absolute output folder | ## `placements_inspect` [#placements_inspect] Validate a baked placements folder and report prototype LODs, texture families, stable solid counts, region bytes and format caps. Read-only. Run after every bake and before scene publication; inspect runtime residency and visual passes as well. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------ | | `dir` | `string` | yes | Absolute folder containing placements.json | ## `publish_unreal_world` [#publish_unreal_world] Publish a COOKED UNREAL world to HELIX and make it the world's active build. Takes the candidate directory the Unreal HelixWorldPublish commandlet writes (it contains world-build-manifest.json, its .sha256 receipt, cook-plan.json, and artifacts/\/…) β€” NOT an Unreal project directory, and NOT a web bundle. Verifies the candidate against the bytes on disk, registers the Build, uploads every artifact, has the server re-verify and countersign it, then promotes it. This is the ONLY way a cooked Unreal world reaches HELIX. Requires login (check whoami first) and the world must already exist (create\_world). YOU CANNOT COOK FROM HERE: cooking is an Unreal Editor / commandlet step on a Windows machine with UE 5.8 β€” if there is no candidate directory yet, say so and stop rather than looking for one to invent. Web worlds use publish\_world instead; the two are not interchangeable. | Argument | Type | Required | Description | | ---------------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `candidate` | `string` | yes | Absolute path to the cook output directory β€” the one containing world-build-manifest.json. Passing an Unreal project root or a web bundle is refused with the reason. | | `world` | `string` | yes | The HELIX world to publish to: its slug, or its UUID. The world must already exist β€” create\_world mints one. | | `maxPlayers` | `integer` | yes | Players per instance for the entry activity (1-1000). Ask the human if the world does not imply one. (min 1, max 1000) | | `activityId` | `string` | no | Activity id for the entry map. Defaults to 'default'; only set it for a world with several named activities. | | `visibility` | `"private" \| "unlisted" \| "public"` | no | Catalog visibility to set when the build is promoted. Omit to leave the world's current visibility alone. Going 'public' lists it for everyone β€” that is the human's call, not yours. | | `promote` | `boolean` | no | Defaults to true: the uploaded build becomes the active release. Set false to upload and verify WITHOUT changing what players get β€” use this to stage a build for review. | | `verifyDelivery` | `boolean` | no | After promoting, re-read what a client would actually download and check it matches what was uploaded. Cheap; worth it on a first publish. | ## `publish_world` [#publish_world] Publish a validated world bundle to HELIX Instant. Refuses a World whose engine systems are pinned to an exact version (use ^ ranges; there is no agent override β€” only the human can run the CLI with --allow-exact-engine-pin "\"). Automatically captures a ready world thumbnail unless a custom cover is already selected, uploads it with the files, and activates the Build and cover together; returns the public play URL. Capture failure refuses publication. Requires login (check whoami first). Re-publishing the same slug creates the next build of that world. | Argument | Type | Required | Description | | -------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the built bundle directory (e.g. the dist/ folder) | | `thumbnail` | `string` | no | Selected cover image path. Preserves an existing custom cover; otherwise uses this image, thumbnail.\ from the bundle, or automatically captures the ready world. | | `uploadSource` | `boolean` | no | Opt in to uploading this world's source alongside the build so it can later be opened and edited in HELIX's website editor. Defaults to false β€” source is NEVER uploaded unless you set this, and it is the human's decision, not yours: ASK before setting it, because it sends their code to HELIX and some creators deliberately do not want that. The upload excludes node\_modules, .git, dist and .vite, and anything the project's .gitignore excludes. It travels on a separate PRIVATE channel, not as part of the public build, and it does not change the published bundle in any way. Without it the world still publishes and plays exactly the same; it simply cannot be edited on the website afterwards, because nothing but the built bundle was ever sent. | | `sourceDir` | `string` | no | Absolute path of the PROJECT directory to archive when uploadSource is true. Defaults to the parent of `directory`, which is right for the usual layout (publish dist/, source lives one level up). Set it only when the project root is somewhere else. Meaningless without uploadSource β€” passing it alone is refused rather than silently ignored. | ## `read_template` [#read_template] Read a complete, publish-shaped multiplayer world template: its DSL checklist, the full public/helix.json (incl. the multiplayer block), the client src/main.ts delta, and build/publish steps. Pick the name from the hub (get\_started multiplayer) or list\_templates β€” e.g. "hangout". | Argument | Type | Required | Description | | -------- | -------- | -------- | --------------------------------------------------- | | `name` | `string` | yes | Template name, e.g. "hangout" (see list\_templates) | ## `register_achievement` [#register_achievement] Register an achievement on one of the creator's worlds β€” the registry a world's badges live in, which is NOT part of helix.json and cannot be declared in the bundle. REGISTER BEFORE PUBLISHING the rule that awards it: `awardAchievement` with an unregistered key publishes clean and is a silent runtime no-op, the single most common authoring trap. Needs a 2D icon file (required β€” an achievement always has a graphic; generate one with generate\_image if the human has no art, and note that a blank or sub-16px placeholder is refused). Leave unlockMode alone for a badge a room rule awards (the default); declare `signal` + `value` instead for a criteria badge the backend measures with no room at all. Pass dryRun to validate everything locally β€” no network, works logged out. TEST THE UNLOCK FIRST: the same row seeds `.helix/dev-achievements.json` for `helix dev`, where a criteria badge unlocks off simulated evidence as you play β€” the seed uses the STORED nested shape (`criteria: { signal, op, value, key, field, metric }`; this tool flattens `key`/`field` into criteriaKey/criteriaField because `key` is already the badge slug) and needs no icon. Registration takes effect immediately, live worlds included β€” no republish; the backfill pass credits players who already qualify. | Argument | Type | Required | Description | | --------------- | --------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `worldSlug` | `string` | yes | Slug of the world that issues the achievement (the creator owns it) | | `key` | `string` | yes | Stable slug your rules name: 2-80 chars, lowercase a-z0-9\_-, starting alphanumeric. Treat it as a schema β€” renaming needs a fresh registration. | | `name` | `string` | yes | Badge title players see | | `icon` | `string` | yes | Absolute path to the 2D graphic (png/jpg/webp/gif/avif/heic) β€” REQUIRED | | `description` | `string` | no | What the player did to earn it | | `points` | `integer` | no | Gamerscore points, 0-1000 (default 0) (min 0, max 1000) | | `hidden` | `boolean` | no | Show "???" until the player earns it | | `trophy` | `string` | no | Absolute path to a bespoke 3D trophy (.glb); omitted, the icon is baked onto the default plaque | | `unlockMode` | `"room" \| "criteria" \| "platform"` | no | Who may award it β€” the trust marker. Default: `criteria` when a signal is declared, else `room` (an awardAchievement rule). `platform` means FIRST-PARTY code awards it: nothing in a world can, so do not pick it. | | `signal` | `"datastore" \| "iwp" \| "analytics" \| "playtime"` | no | Declaring a signal makes this a `criteria` achievement (the backend measures it, no room needed β€” works single-player). datastore = a numeric field in a per-user document; iwp = in-world purchases; analytics = a platform event count; playtime = seconds in this world. | | `op` | `"gte" \| "lte" \| "eq"` | no | Comparison against `value`. Default gte. | | `value` | `number` | no | Threshold the measured value is compared against. Required with `signal`. (min 0) | | `criteriaKey` | `"player:{userId}" \| "mp:player:{userId}"` | no | datastore only, and this IS the trust decision: "mp:player:\{userId}" is the room's flushed persistent playerVars (room-written β‡’ trustworthy), "player:\{userId}" is the player's own client-written save (they can write it β‡’ self-reported). | | `criteriaField` | `string` | no | datastore only: dot-path to the number (e.g. "stats.summits"). For mp:player:\{userId} it names the persistent playerVar directly. | | `eventName` | `string` | no | analytics only: the PLATFORM analytics event to count (e.g. world\_entered). A world cannot mint its own. | | `metric` | `"count" \| "lix"` | no | iwp only: purchase count (default) or total LIX spent. | | `dryRun` | `boolean` | no | Validate locally and report what would be sent; no network, no login needed | ## `register_world_product` [#register_world_product] Register a product one of the creator's worlds SELLS β€” the registry `Helix.marketplace.purchaseProduct` buys from, which is NOT part of helix.json and cannot be declared in the bundle or minted at runtime. REGISTER BEFORE WRITING the code that buys it: world code names the creator-authored `key` (`purchaseProduct("speed_boost")` buys `pkey:speed_boost`) and the key is IMMUTABLE once registered, so it is chosen here, once. The PRICE lives server-side only β€” a world never sends an amount; the shell raises the confirm popup and settles. priceLix 0 is legitimate: it registers a free "Claim". AUTHOR WHAT IT HANDS OVER as a `grants` effect array β€” pass | currency; several effects = a bundle, an empty array = a tip jar. The backend fulfils it DURABLY at settle. World Products do not mint universal Items; use the Creator approved Publications workflow for those. The effect list is IMMUTABLE once registered. Pass dryRun to validate everything locally β€” no network, works logged out. TEST THE LOOP FIRST: the same product row seeds `.helix/dev-products.json` for `helix dev` β€” buy/entitlements/consume answer locally with nothing charged, and the debug menu forces the failure paths. Registration takes effect on a live world immediately β€” no republish. | Argument | Type | Required | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `worldSlug` | `string` | yes | Slug of the world that sells it (the creator owns it). Registering on a draft world is allowed; a purchase needs a live world. | | `title` | `string` | yes | Name the purchase popup shows the player (max 120 chars) | | `type` | `"consumable"` | no | Legacy world-local effect with no durable grant. Universal-item grant types are not creator-facing. | | `grants` | `({ kind: "pass" \| "currency", passKey?: string, durationSeconds?: integer, code?: string, amount?: integer, display?: "wallet" \| "uses" })[]` | no | The world-local effect array is IMMUTABLE once registered. Max 8 effects; several = a bundle, \[] = a tip jar. Universal items use ItemDistribution instead. | | `priceLix` | `number` | yes | Price in LIX. 0 registers a free "Claim" β€” a confirmation, no spend. This is the ONLY place a price lives. (min 0) | | `key` | `string` | yes | Required stable slug world code buys as `pkey:`: max 64 chars of lowercase a-z0-9\_-, starting alphanumeric. IMMUTABLE β€” renaming would break every build naming it. Internal product UUIDs are never purchase references. (min length 1) | | `description` | `string` | no | What the player is buying (max 2000 chars) | | `maxPerUser` | `integer` | no | Cap on how many one player may own (whole number >= 1). A permanent-pass-only product is capped at 1 and REFUSES a higher value β€” a re-buy would charge again and deliver nothing. (min 1) | | `dryRun` | `boolean` | no | Validate locally and report what would be sent; no network, no login needed | ## `scaffold_world` [#scaffold_world] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. REQUIRED FIRST BUILD STEP when no project exists yet (a workspace that already has package.json and public/helix.json is scaffolded: skip this) β€” the command is `helix init `, and running it writes a complete, build-ready world project: package.json, vite.config.ts (base: "./"), tsconfig.json, index.html with the import-map markers + loading overlay, .gitignore, public/helix.json (v0.2, pinning humanoid-character AND engine-core), src/helix.runtime.ts, src/loading.ts, and a RUNNABLE src/main.ts β€” a real character world that builds and renders, with the world-owned InputService pre-wired (register game keys on it β€” never raw addEventListener), the floor collider, the world.ready() inspect hook, and the ability base resolved against document.baseURI so it survives the sub-path a published world is served from. DO NOT HAND-WRITE THESE FILES from the recipe prose: the recipe is explanation, the scaffold is the source. An agent that typed them out instead got four API calls wrong against the installed .d.ts and shipped a world that built, validated, and rendered nothing. Multiplayer scaffolds include proximity VOICE CHAT by default (the voice.proximity permission + the fail-soft join) β€” remove both only for a deliberately silent world. A scene scaffold can also come with GROUND: terrain: "\" adds `--terrain `, which generates a 4 x 4-tile (1 km x 1 km) heightfield into public/scene/mall/terrain/, paints it from the preset, lifts the starter mall onto the flat spawn plateau, and declares it in the sealed document β€” public/.helixignore already covers terrain/, so those bytes stay a dev override. Scene worlds ONLY; read\_doc(\{name:"terrain"}). Existing files are SKIPPED (safe to run inside an existing project); the CLI never destructively overwrites. Then: npm install β†’ install\_world\_packages β†’ npm run build β†’ validate\_world β†’ `helix verify-subpath dist` β†’ publish\_world. | Argument | Type | Required | Description | | ------------ | ----------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path of the project directory (created if missing; existing files are kept unless overwrite) | | `title` | `string` | yes | World title shown in the tab, loading screen, and catalog (min length 1, max length 80) | | `slug` | `string` | yes | URL-safe world slug (also the npm package name) (min length 3, max length 64) | | `kind` | `"character" \| "multiplayer" \| "scene"` | yes | character = single-player character world; multiplayer = presence world (grow the multiplayer block per the hub/templates); scene = YOUR CHOICE for a genuinely large or open world that cannot load at once (a city, a landscape, kilometres of ground, thousands of objects), or when the creator names a Scene document / Scene Package to stream β€” never for an arena, a room, "a scene", "a bare scene" or "no character", which are the plain recipe of get\_started(\{ kind: "scene" }) or a character/multiplayer world with your own geometry. It is a multiplayer world whose ENVIRONMENT the scene system streams and the visual runtime lights β€” the scaffold seals a starter document into public/scene/, pins it in helix.json, and writes NO floor, props or lights, because the document carries geometry, materials, lights, spawn and collision | | `maxPlayers` | `integer` | no | multiplayer and scene worlds only; default 8. Keep modest β€” the platform clamps per-room (min 2, max 64) | | `terrain` | `"meadow" \| "alpine" \| "desert" \| "coast" \| "forest" \| "volcanic"` | no | scene worlds ONLY: generate a 1 km x 1 km starter terrain from a built-in preset (meadow = gentle rolling grass, alpine = high massifs and snow, desert = dunes and mesas, coast = a shoreline that climbs inland, forest = wooded ridges and glens, volcanic = a crater cone in basalt). Omit for the flat starter scene | | `overwrite` | `boolean` | no | Replace existing files (default false: skip them β€” the into-existing-project mode) | ## `set_achievement_icon` [#set_achievement_icon] Replace a registered achievement's ICON ART in place (HYPR-5142). Same UniversalItem, same key β€” every holder keeps their badge, and the default trophy plaque is re-baked with the new art (a bespoke trophy mesh is left untouched). This is the RIGHT way to change art: do NOT re-register the achievement to swap an icon (that re-keys it and leaves the old one delisted). `image` is a path to a local icon file (png/jpeg/webp); it is validated (a single flat colour is rejected) before anything is uploaded. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------- | | `worldSlug` | `string` | yes | Slug of the world | | `key` | `string` | yes | The achievement key | | `image` | `string` | yes | Path to the replacement icon image file (png/jpeg/webp) | ## `set_preview_video` [#set_preview_video] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Set or clear a published world's YouTube preview. The CLI and the API own what counts as a valid YouTube link β€” the URL is normalised server-side. | Argument | Type | Required | Description | | -------- | --------- | -------- | -------------------------------- | | `slug` | `string` | yes | World slug | | `url` | `string` | no | YouTube URL or video id | | `clear` | `boolean` | no | Clear the existing preview video | ## `set_world_thumbnail` [#set_world_thumbnail] RETURNS A COMMAND, NOT DATA. This tool hands you a `helix …` command string to RUN yourself β€” it executes nothing and returns no results. The answer is in that command’s output. Attach a custom thumbnail or preview image to a published world (fills the generated-placeholder slot). Filling an EMPTY slot is a normal finishing step after a good capture β€” do it without asking. Pass overwrite ONLY when the human explicitly asked to replace an existing image; an occupied slot without overwrite is refused by the CLI (with the current URL intact) so you can relay the situation instead of silently skipping it. | Argument | Type | Required | Description | | ----------- | -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `slug` | `string` | yes | World slug | | `file` | `string` | yes | Absolute path to a local image file (png, jpg/jpeg, or webp) | | `kind` | `"thumbnail" \| "preview"` | no | Media slot to fill: thumbnail (cover everywhere, 1280x720) or preview (one extra detail-carousel slide, 2560x1440). Default thumbnail. | | `overwrite` | `boolean` | no | Replace an already-set slot β€” only on an explicit human request, never automatically | ## `terrain_generate` [#terrain_generate] Generate the GROUND a scene world stands on: runs `helix terrain generate` and writes a terrain source folder (per-tile 16-bit heights, the painted splats, terrain.json). Deterministic β€” the same preset, seed and size always produce the same bytes, so a re-run is free and a diff is real. USE IT for open ground under a scene world: a scene document declares the folder (the `terrain` element of a scene-elements document, or scaffold\_world(\{ kind: "scene", terrain }) which already wired one) and the scene system streams the tiles as the world loads. A tile is 256 cells and this generator always writes the 1 m authoring grid, so a tile is 256 m: the default 4x4 grid is 1 km x 1 km (\~2.4 MiB) and the cap is 16x16 = 4 km x 4 km (\~39 MiB, \~25 s of work). A coarser authoring grid (0.5, 2, 4 or 8 m per sample) is an IMPORT-side choice β€” terrain\_import takes outputMetresPerSample, generate has no spacing flag. Pass tiles OR sizeKm, never both. The preset picks BOTH the landform and the surfaces painted onto it. ALWAYS run terrain\_inspect on the folder before publishing β€” folded layers and over-cap tiles are reported there and nowhere else. Terrain worlds run DYNAMIC lighting: never bake\_world\_lighting a world whose ground is streamed terrain. Full recipe: read\_doc(\{ name: "terrain" }). | Argument | Type | Required | Description | | ---------- | ----------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `out` | `string` | yes | Absolute path of the terrain source folder to write (for a scaffolded scene world: public/scene/\/terrain) | | `preset` | `"meadow" \| "alpine" \| "desert" \| "coast" \| "forest" \| "volcanic"` | yes | Built-in preset: meadow (gentle rolling grass), alpine (massifs, snow above the treeline), desert (dunes and mesas), coast (a shoreline climbing inland), forest (wooded ridges and glens), volcanic (a crater cone in basalt) | | `seed` | `integer` | no | Landform seed; default 1. Change it for a different world of the same character | | `tiles` | `string` | no | Grid size in tiles, e.g. "8x8". A tile is 256 cells β€” 256 m at 1 m per sample, 1 km at 4 m. Default 4x4 (1 km x 1 km at 1 m); the cap is 16x16 | | `sizeKm` | `number` | no | Square extent in kilometres instead of tiles, rounded to whole 256 m tiles (1 km = 4x4) | | `seaLevel` | `number` | no | Flood the landform to this height in metres β€” everything below becomes water-level ground | | `holes` | `string` | no | Absolute path to a greyscale PNG mask; pixels above mid-grey become holes in the ground (the hole is visual only β€” the collider under it stays solid, so do not use it for a pit the player falls through) | ## `terrain_import` [#terrain_import] Turn an EXISTING heightmap β€” a real-world DEM, a sculpted export, ground shaped in another tool β€” into a terrain source folder: runs `helix terrain import`, resamples the file onto the authoring grid and paints it with a preset. Use it instead of terrain\_generate whenever the shape of the ground is already decided somewhere else; everything after this point (inspect, the scene document, publishing, lighting) is identical. TWO SPACINGS, never confuse them: metresPerSample is the SOURCE posting β€” metres between two samples IN THE FILE, taken from the source data and never guessed β€” while outputMetresPerSample is the AUTHORING grid the world is built on, one of 0.5, 1, 2, 4 or 8 m (default 1). Author at 1 m for hand-scale ground a player walks over; go coarser when the extent is the point β€” a tile is 257 samples, so at 4 m one tile spans 1 km and a city-scale landscape costs a sixteenth of the tiles the same ground would at 1 m. HEIGHTS: png16 and r16 store 0..65535, so heightRange says what the darkest and brightest sample mean in metres (e.g. \[0, 900]) and a wrong pair yields a plausible-looking world at the wrong scale; f32 (headerless little-endian float32) already holds absolute metres and REJECTS heightRange. Either way the ground belongs near y = 0 β€” the sky, the height fog and the numeric precision are all built for it, and the absolute altitude is a fact for the docs, not for the world: with png16/r16 shift heightRange (a 1900..3900 m DEM becomes \[0, 2000]), with f32 pass heightOffset, which is added to every decoded height, so a DEM whose sea level reads 507 m re-bases with heightOffset: -507. The extension picks the format when format is omitted (.png, .r16/.raw, .f32). A source posting coarser than the authoring grid is upsampled with a cubic that keeps the slope continuous across the source samples; bilinear (resample) creases along every source row and column and the light draws each one. Three more inputs when the source needs them: origin puts sample (0, 0) at a chosen world position instead of centring the grid, samples gives the row length of a raw r16/f32 file that is not square, and splat + splatRoles paint from an AUTHORED RGBA image covering the grid extent β€” every role bound to a channel there takes its weight from that channel instead of the preset rule, and the roles left unnamed keep theirs. Then run terrain\_inspect. read\_doc(\{ name: "terrain" }). | Argument | Type | Required | Description | | ----------------------- | --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | `string` | yes | Absolute path to the heightmap: a 16-bit PNG, a raw R16, or a headerless float32 (.f32) holding absolute metres | | `out` | `string` | yes | Absolute path of the terrain source folder to write | | `preset` | `string` | yes | Built-in preset id (meadow, alpine, desert, coast, forest, volcanic) to paint it with, or an ABSOLUTE path to a preset JSON file | | `metresPerSample` | `number` | yes | The SOURCE's ground resolution: metres between two samples IN THE FILE (a 30 m SRTM tile is 30). The grid the world is authored on is outputMetresPerSample | | `heightRange` | `number[]` | no | \[MIN, MAX] in metres that the darkest and brightest sample stand for, e.g. \[0, 900] β€” shifted so the lowest ground sits near 0. REQUIRED for png16 and r16; REJECTED for f32, which already holds metres (re-base that one with heightOffset) | | `outputMetresPerSample` | `0.5 \| 1 \| 2 \| 4 \| 8` | no | Metres between two samples of the AUTHORING grid: 0.5, 1, 2, 4 or 8, default 1. A tile is 257 samples, so it spans 256 m at 1 m and 1 km at 4 m β€” stay at 1 m for ground the player walks over, go coarse for a landscape whose extent is the point | | `heightOffset` | `number` | no | Metres added to every decoded height: how an absolute-metres DEM is re-based so its sea level or its floor lands on 0 (a source whose sea level reads 507 m takes -507) | | `origin` | `any` | no | \[X, Z] world metres of sample (0, 0). Omitted, the grid is centred on the world origin | | `samples` | `integer \| string` | no | Samples per row of a raw r16 or f32 file β€” a number, or "WxH" β€” needed only when its byte length is not a square. Ignored for png16 | | `tiles` | `string` | no | Grid size in tiles, e.g. "8x8". A tile is 256 cells β€” 256 m at 1 m per sample, 1 km at 4 m. Default 4x4 (1 km x 1 km at 1 m); the cap is 16x16 | | `format` | `"png16" \| "r16" \| "f32"` | no | Override the format the file extension implies. f32 is headerless little-endian float32 in absolute metres | | `resample` | `"cubic" \| "bilinear"` | no | How a coarser source posting reaches the authoring grid. Default cubic (slope-continuous across the source samples); bilinear creases at every source sample | | `splat` | `string` | no | Absolute path to an AUTHORED RGBA8 PNG covering the grid extent; each channel bound by splatRoles replaces the preset rule for that role. Goes with splatRoles | | `splatRoles` | `string` | no | Which role each splat channel carries, e.g. "R=base,G=low,B=slope". Roles: base, slope, peak, low, cavity, accent β€” the ones left unnamed keep their preset rules. Goes with splat | ## `terrain_inspect` [#terrain_inspect] Report what a terrain source folder would actually ship, as numbers (`helix terrain inspect`): extent and tile count, authored samples, delivered bytes split into heights and splats, the height range, the coarse layer, which material roles the bake had to FOLD into a kept role on which tiles (a tile carries four layers; a preset may declare six), and every warning β€” an over-cap tile, an unbaked or splat-less folder, a grid past 16 x 16. THE GATE BEFORE PUBLISHING: none of this is visible in a screenshot and none of it is reported at publish time, so an unbaked or over-cap terrain otherwise ships silently. Read-only β€” it writes nothing. Returns the human summary followed by the same report as JSON. | Argument | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------------ | | `dir` | `string` | yes | Absolute path of the terrain source folder (it holds terrain.json) | ## `update_achievement` [#update_achievement] Patch a registered achievement's presentation (name/description/points/hidden/active). The key, unlock mode and criteria are FIXED at registration β€” rules name the key, and re-labelling how a badge is earned would rewrite what every already-granted copy claims; register a new achievement instead. To change the ICON ART in place, use set\_achievement\_icon (the key and every holder's badge are preserved β€” do NOT re-register just to swap art). Set active:false to delist one (existing holders keep theirs). | Argument | Type | Required | Description | | ------------- | --------- | -------- | ----------------------------------------- | | `worldSlug` | `string` | yes | Slug of the world | | `key` | `string` | yes | The achievement key | | `name` | `string` | no | | | `description` | `string` | no | | | `points` | `integer` | no | (min 0, max 1000) | | `hidden` | `boolean` | no | | | `active` | `boolean` | no | false delists it β€” nobody new can earn it | ## `update_world_product` [#update_world_product] Patch a registered product's presentation, price or ownership cap, or set active:false to take it off sale (the row stays, so past purchases and the items they granted keep resolving). The key and the type are FIXED at registration β€” world code buys the key, and changing what a purchase hands over would re-label every copy already sold; register a new product and deactivate this one instead. | Argument | Type | Required | Description | | ------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `worldSlug` | `string` | yes | Slug of the world that sells it | | `product` | `string` | yes | The product key (or its uuid, if it was registered without a key) | | `title` | `string` | no | | | `description` | `string` | no | | | `priceLix` | `number` | no | New price in LIX; 0 makes it a free "Claim" (min 0) | | `maxPerUser` | `integer \| null` | no | New per-player ownership cap (whole number >= 1). Refused above 1 on a permanent-pass-only product; pass null to clear it back to the derived cap. | | `active` | `boolean` | no | false takes it off sale β€” existing owners keep theirs, nobody new can buy it | ## `validate_world` [#validate_world] Validate a built world bundle directory (the Vite dist/ folder containing helix.json) against the exact rules the platform enforces at publish. Run this after building and fix every reported problem before publishing. A World whose engine systems are pinned to an exact version (not a ^ range) is refused: use ranges, and promote an engine fix instead of pinning to it. | Argument | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------------- | | `directory` | `string` | yes | Absolute path to the built bundle directory (e.g. the dist/ folder) | ## `world_metrics` [#world_metrics] The static traversal/movement reference from a world’s OWN installed character system β€” read it instead of guessing spatial constraints while building: player capsule (height/radius/crouch), step height (what auto-steps vs blocks), max walkable slope, movement speeds, and the derived jump envelope (air time, max flat-ground gap at run/sprint) plus corridor/ceiling clearances. Values are the system DEFAULTS a world starts from; worlds may override via config. Use it to size gaps, steps, ramps, corridors, and ceilings BEFORE placing geometry, and pair it with inspect\_world to verify what you placed. | Argument | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `projectDir` | `string` | yes | Absolute path to the world project (the dir with helix.json β€” must be npm-installed + install\_world\_packages'd so its pinned system is present) | # helix achievement (/docs/cli/reference/achievement) {/* Generated from `@hypersoniclabs/helix-cli@0.1.27-helix3.551` by `npm run docs:generate`. Do not edit by hand. */} CLI version 0.1.27-helix3.551. Run `helix achievement --help` for the text your installed build prints. ## `helix achievement` [#helix-achievement] Register and manage a world's achievements (the badges `awardAchievement` and criteria unlocks grant) ```bash helix achievement ``` ### `helix achievement register` [#helix-achievement-register] Register a new achievement on one of your worlds β€” one round trip, mints the soulbound badge item ```bash helix achievement register [options] ``` | Argument | Required | Description | | ------------ | -------- | ------------------------------------- | | `world-slug` | yes | the world that issues the achievement | | Option | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `--key ` | stable slug your rules name (2-80 chars, lowercase a-z0-9\_-); treat it as a schema β€” renaming needs a fresh registration (required) | | `--name ` | badge title players see (required) | | `--icon ` | the 2D graphic (png/jpg/webp/gif/avif/heic) β€” REQUIRED, and a blank or tiny placeholder is refused (required) | | `--description ` | what the player did to earn it | | `--points ` | gamerscore points, 0-1000 (default 0) | | `--hidden` | show "???" until the player earns it | | `--trophy ` | bespoke 3D trophy; omitted, your icon is baked onto the default plaque | | `--unlock-mode ` | who may award it: room \| criteria \| platform (default: criteria when a condition is declared, else room) | | `--signal ` | server-measured condition: datastore \| iwp \| analytics \| playtime β€” declaring one makes this a criteria achievement | | `--op ` | comparison: gte \| lte \| eq (default gte) | | `--value ` | threshold the measured value is compared against | | `--criteria-key