HELIX Docs
Web SDK

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

  1. Server-authoritative. The world only ever requests; the platform charges LIX and grants the item. A client-reported price or balance is never trusted.
  2. Graceful when signed out. Opened directly (no shell — local vite dev) every read returns an empty default and a purchase resolves Unauthorized — never a thrown error, never a fake success. Embedded but signed out, a purchase resolves Unauthorized and 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."

StatuscompletedMeaningWhat to do
GrantedPaid + item granted.Unlock / equip.
ClaimedFree item granted (no charge).Unlock / equip.
AlreadyOwnedPlayer already owns it (non-consumable / cap).Treat as owned; don't re-charge.
InsufficientFundsBalance < price. Carries shortfallLix.Show shortfall + a link to top up.
MaxPerUserReachedPer-user cap hit.Show "limit reached".
ProductInactive / ProductNotFoundNot for sale / unknown."No longer available", close.
NotInWorldProduct isn't from this world (or session lapsed).Prompt rejoin.
UnauthorizedNot signed in.The shell raises login; offer retry after.
RateLimitedToo many attempts.Brief cooldown, then allow retry.
CancelledPlayer dismissed the popup.Do nothing.
TimeoutNetwork dropped before the result was confirmed.The SDK polls; surface a Retry that reuses the key.
FailedUnexpected server error. Money + inventory stay consistent (rolled back).Friendly error + Retry.
PendingIn 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.

On this page