Reference (Web SDK)
The auto-generated API reference for @helix/sdk. This page is generated from the SDK's TypeScript source on every merge.
Shipped in @hypersoniclabs/helix-sdk v0.2
Helix.wallet, Helix.marketplace, Helix.inventory, Helix.dataStore, and Helix.camera below are live
(shell-mediated over postMessage). They're hand-documented here; TypeDoc auto-generation is wired in
next. Other namespaces (memoryStore, social, …) are planned.
Two rules that shape every call here
- Server-authoritative. The world only ever requests; the platform charges LIX and grants the item. A client-reported price or balance is never trusted.
- Graceful when signed out. Opened directly (no shell — local
vite dev) every read returns an empty default and a purchase resolvesUnauthorized— never a thrown error, never a fake success. Embedded but signed out, a purchase resolvesUnauthorizedand the shell raises the login overlay.
Shared types
type Balance = { lix: number; coins: number; updatedAt?: string };
type PurchaseStatus =
| 'Granted' | 'Claimed' | 'AlreadyOwned' // success (completed === true)
| 'InsufficientFunds' | 'MaxPerUserReached'
| 'ProductInactive' | 'ProductNotFound' | 'NotInWorld'
| 'Unauthorized' | 'RateLimited' | 'Cancelled' | 'Failed' | 'Timeout' | 'Pending' | 'NotFound';
type PurchaseResult = {
status: PurchaseStatus;
completed: boolean; // true for Granted | Claimed | AlreadyOwned — the player owns it now
reason?: string;
itemId?: string | null;
balanceAfterLix?: number;
shortfallLix?: number; // on InsufficientFunds
};Purchase statuses
Every purchase resolves to exactly one of these. The backend is authoritative — render from status;
never invent a success or guess a failure. completed is the shortcut for "the player owns it now."
| Status | completed | Meaning | What to do |
|---|---|---|---|
Granted | ✅ | Paid + item granted. | Unlock / equip. |
Claimed | ✅ | Free item granted (no charge). | Unlock / equip. |
AlreadyOwned | ✅ | Player already owns it (non-consumable / cap). | Treat as owned; don't re-charge. |
InsufficientFunds | — | Balance < price. Carries shortfallLix. | Show shortfall + a link to top up. |
MaxPerUserReached | — | Per-user cap hit. | Show "limit reached". |
ProductInactive / ProductNotFound | — | Not for sale / unknown. | "No longer available", close. |
NotInWorld | — | Product isn't from this world (or session lapsed). | Prompt rejoin. |
Unauthorized | — | Not signed in. | The shell raises login; offer retry after. |
RateLimited | — | Too many attempts. | Brief cooldown, then allow retry. |
Cancelled | — | Player dismissed the popup. | Do nothing. |
Timeout | — | Network dropped before the result was confirmed. | The SDK polls; surface a Retry that reuses the key. |
Failed | — | Unexpected server error. Money + inventory stay consistent (rolled back). | Friendly error + Retry. |
Pending | — | In flight (only seen when polling a resumed purchase). | Keep polling. |
Helix.wallet
Concept: LIX & Economy.
getBalance(): Promise<Balance>
The current player's LIX + Coins balances. Returns { lix: 0, coins: 0 } when not embedded.
const { lix, coins } = await Helix.wallet.getBalance();
hud.setLix(lix);onBalanceChanged(cb: (b: Balance) => void): Unsubscribe
Fires after the wallet changes (e.g. a purchase settles). Returns an unsubscribe function.
const off = Helix.wallet.onBalanceChanged(({ lix }) => hud.setLix(lix));
// later: off();Helix.marketplace
purchaseItem(itemId: string): Promise<PurchaseResult>
Request a server-settled purchase of a catalog (marketplace) item. The shell raises the confirm
popup and settles; a free item (price 0) resolves Claimed. Check result.completed.
const result = await Helix.marketplace.purchaseItem('premium_sword_001');
if (result.completed) equip('premium_sword_001'); // Granted | Claimed | AlreadyOwned
else if (result.status === 'InsufficientFunds') showTopUp(result.shortfallLix);
else showToast(result.status); // Cancelled, RateLimited, …purchaseProduct(productId: string): Promise<PurchaseResult>
Same, for a product registered on the world (item_grant / non_consumable / consumable).
const res = await Helix.marketplace.purchaseProduct('golden_key');
if (res.completed && (await Helix.inventory.hasItem('golden_key_item'))) openVault();getListings(query?): Promise<Listing[]>
Browse marketplace listings ({ kind?, category?, search?, limit? }). [] when not embedded.
const chairs = await Helix.marketplace.getListings({ kind: 'home_item', search: 'chair' });
chairs.forEach((l) => addShelfItem(l.itemId, l.title, l.priceLix, l.isFree));getPurchaseContext(ref: string): Promise<PurchaseContext | null>
Product/listing metadata + the player's live balance + eligibility, in one call — for building a
custom confirm UI. ref is an itemId or item:<itemId>. The built-in popup uses this internally.
const ctx = await Helix.marketplace.getPurchaseContext('item:premium_sword_001');
// { title, priceLix, isFree, balanceLix, balanceAfterLix, owned, eligible, eligibilityStatus }
if (ctx && !ctx.eligible) disableBuyButton(ctx.eligibilityStatus);Idempotency is built in
The SDK mints a stable idempotency key per purchaseItem / purchaseProduct attempt and the shell
reuses it across the popup's retries — a dropped network can never double-charge. A repeated request
returns the stored result, not a second charge.
Helix.inventory
Concept: Inventory & Items.
hasItem(itemId: string): Promise<boolean>
Whether the player owns at least one. Works across worlds and creators — the basis for VIP /
season-pass gating. false when not embedded.
// Gate backstage on a VIP pass sold in another creator's world.
if (await Helix.inventory.hasItem('vip_pass_001')) openBackstage();getQuantity(itemId: string): Promise<number>
How many the player owns. 0 when not embedded.
const potions = await Helix.inventory.getQuantity('health_potion_001');getMyItems(): Promise<Item[]>
All items the current player owns. [] when not embedded.
const mine = await Helix.inventory.getMyItems();
mine.filter((i) => i.kind === 'wearable').forEach(renderInLocker);equipItem(itemId: string): Promise<void>
Equip an owned cosmetic (visual). No-op when not embedded.
if (await Helix.inventory.hasItem('vip_hat_001')) await Helix.inventory.equipItem('vip_hat_001');onInventoryChanged(cb: () => void): Unsubscribe
Fires when the player's inventory changes (e.g. after a purchase). Returns an unsubscribe function.
const off = Helix.inventory.onInventoryChanged(() => refreshOwnedDoors());
// later: off();Helix.dataStore
Durable per-world key–value storage — the Roblox DataStore analog. Concept:
Data Store. Free for every world (quotas + a per-world write rate
limit apply). Scoped to the current world; namespace per-player data yourself (player:${userId}).
get<T>(key: string): Promise<T | null>
Read a durable value. null if unset (or when not embedded).
const progress = (await Helix.dataStore.get<Progress>(`player:${userId}`)) ?? { level: 1 };set<T>(key: string, value: T): Promise<void>
Write durably; overwrites any existing value. Subject to the world's quotas + write rate limit — a
rejected write throws (e.g. RateLimited). No-op when not embedded.
await Helix.dataStore.set(`player:${userId}`, progress);delete(key: string): Promise<void>
Remove a key. No-op when not embedded.
list(prefix?: string): Promise<string[]>
List stored keys, optionally filtered by prefix (keys only, like Roblox ListKeysAsync). [] when
not embedded.
const players = await Helix.dataStore.list('player:');Helix.camera
The universal in-engine camera → phone Gallery. Players open a photo-mode inside any world, frame a shot, and it's saved to their account — appearing in the phone's Photos app on every device, tagged with the world it was taken in. The camera UI + capture run inside the world (they own the canvas); this SDK surface is the cloud side.
Requires the camera.capture permission in the world manifest. The save is authenticated by the
world-session token and enforced server-side: the backend derives the owner and the world name
from the signed session, so the scope can't be bypassed and "Taken in World" can't be spoofed.
available(): boolean
true when running inside a HELIX shell (so a save can reach the account). false in local preview —
capture still works, but savePhoto no-ops.
capture(canvas, opts?): Promise<Blob>
Encode an already-rendered canvas to a Blob, centre-cropped to opts.aspect
('portrait' | 'landscape' | 'square' | 'free', default 'free'). Pure capture — no upload. Render
into the canvas in the same tick before calling (WebGL clears its buffer between frames). Also exported
as captureCanvas; cropRectForAspect(w, h, aspect) gives the crop rectangle.
const blob = await Helix.camera.capture(renderer.domElement, { aspect: 'portrait' });savePhoto(image, meta?): Promise<SavedPhoto | null>
Persist a captured photo (Blob | ArrayBuffer | Uint8Array) to the player's account. Resolves the
saved asset, or null in preview mode (no shell) — never a fake success. Rejects if the world lacks
the camera.capture permission.
const photo = await Helix.camera.savePhoto(blob, { caption: 'gg', aspect: 'portrait' });
// { assetId, url, thumbnailUrl, worldName, createdAt } — worldName is set by the platform.Most worlds don't call these directly: the first-party humanoid-character engine ships a diegetic
photo-mode (WorldCamera) that drives the character camera and calls savePhoto for you. See the
character-world recipe.