Skip to Content
VXShop2. Implement Shop (Vanilla)

2. Implement Shop (Vanilla / Script Tag)

This guide walks you through implementing VXShop without React — using a <script> tag, vanilla ESM, or CJS.

Using React?

See Implement Shop (React) for the useVXShop hook flow. The VXShop API is identical across both pages — see Import Options for a side-by-side comparison.

Three Steps

  1. Create Purchase UI - Design shop buttons in your game
  2. Integrate VXShop - Connect buttons to VXShop payment dialogs ⭐
  3. Handle Purchase - Update user state on server when purchase completes

Step 2 is the key integration point. Steps 1 and 3 are standard game development.

Step 1: Create Purchase UI

Design your shop interface however you like:

  • HTML/CSS popup menus
  • Game-engine UI (Phaser, Babylon, Three.js, Unity WebGL)
  • Plain DOM buttons inside an iframe

VXShop doesn’t impose any UI requirements. Match your game’s style.

Step 2: Integrate VXShop

Option A: Vibe Coding

Tell Agent8 to connect your button to VXShop:

"Connect the Remove Ads button to VX Shop using the vanilla script-tag API. The product ID is remove-ads."

Product ID Required

Always provide the exact Product ID from your Verse8 VX Shop dashboard.

Option B: Manual Implementation

Review Your Code

Payment logic must always be reviewed manually. Incorrect flows can lead to CPP removal.

Installation

Two paths.

Option 1: Script tag (no build step)

<script src="https://unpkg.com/@verse8/platform/dist/index.global.js"></script>

A jsdelivr mirror is also published — swap unpkg.com for cdn.jsdelivr.net/npm/... if preferred.

Option 2: Bundler ESM/CJS (no React peer)

npm install @verse8/platform
import { VXShop } from '@verse8/platform/vanilla';

The /vanilla subpath is React-free — no react peer dependency is required at install time. (The default @verse8/platform entry re-exports the React hook and therefore needs React installed.)

Basic Usage

<button id="buy-remove-ads">Remove Ads</button> <script> VXShop.onClose(() => VXShop.refresh()); VXShop.init(); document.getElementById('buy-remove-ads').onclick = () => { VXShop.buyItem('remove-ads'); }; </script>

That’s it. VXShop.buyItem() opens the Verse8 payment dialog.

A bare init() works automatically inside the Verse8 host iframe — the host attaches an ?auth=<token> query parameter that carries the user’s verseId and account, and the SDK parses it during init(). See Auth Helpers for reading the same values yourself (e.g. to display the current user’s account).

For other scenarios (local dev, standalone pages, or to override auto-resolution), pass values explicitly:

VXShop.init({ verseId: 'my-verse', account: 'my-account' });

For Vite / Next.js builds you can also set VITE_AGENT8_VERSE / NEXT_PUBLIC_AGENT8_VERSE (and the matching *_ACCOUNT) at build time.

Vanilla ESM tree-shaking

If you prefer named imports they tree-shake fine:

import { init, getItem, buyItem, onClose, refresh } from '@verse8/platform/vanilla';

Advanced: Reactive UI with subscribe()

Without a framework like React, use VXShop.subscribe() to react to state changes (items loading, errors, purchases settling):

<div id="shop"></div> <script> const shopEl = document.getElementById('shop'); VXShop.subscribe(({ items, isLoading, error }) => { if (error) { shopEl.innerHTML = `<p class="error">${error}</p>`; return; } if (isLoading) { shopEl.innerHTML = '<p>Loading shop…</p>'; return; } renderItems(items); }); VXShop.onClose(() => VXShop.refresh()); VXShop.init(); function renderItems(items) { shopEl.innerHTML = items .map((item) => { const disabled = !item.purchasable || item.purchaseLimitReached ? 'disabled' : ''; const label = item.purchaseLimitReached ? 'Purchased' : !item.purchasable ? item.purchaseBlockReason || 'Cannot Buy' : `${item.price} VX`; return `<button data-product-id="${item.productId}" ${disabled}>${label}</button>`; }) .join(''); shopEl.querySelectorAll('button[data-product-id]').forEach((btn) => { btn.onclick = () => VXShop.buyItem(btn.dataset.productId); }); } </script>

subscribe() fires on every state change and returns an unsubscribe function.

Advanced: Handling Purchase Limits

Show different states based on purchase availability:

function renderRemoveAds() { const item = VXShop.getItem('remove-ads'); const btn = document.getElementById('buy-remove-ads'); if (!item) return; if (item.purchaseLimitReached) { btn.textContent = 'Purchased'; btn.disabled = true; } else if (item.purchasable) { btn.textContent = `${item.price} VX`; btn.disabled = false; } else { btn.textContent = item.purchaseBlockReason || 'Cannot Buy'; btn.disabled = true; } } VXShop.subscribe(renderRemoveAds); VXShop.onClose(() => VXShop.refresh()); VXShop.init();

Advanced: Showing Remaining Purchases

For items with purchase limits, show the remaining count:

function renderEventPack() { const item = VXShop.getItem('event-pack'); const btn = document.getElementById('buy-event-pack'); if (!item) return; if (item.purchaseLimit) { const remaining = item.remainingPurchaseQuantity; btn.textContent = `Buy ${item.price} VX (${remaining}/${item.purchaseLimit})`; } else { btn.textContent = `Buy ${item.price} VX`; } btn.disabled = !item.purchasable || item.purchaseLimitReached; } VXShop.subscribe(renderEventPack); VXShop.onClose(() => VXShop.refresh()); VXShop.init(); document.getElementById('buy-event-pack').onclick = () => VXShop.buyItem('event-pack');

Parameter Resolution Priority

init() resolves verseId and account from several sources. Higher in the list wins:

verseIdaccount
explicit init({ verseId })explicit init({ account })
AGENT8_VERSE env (bundler builds only)Verse8 auth token (?auth= query)
Verse8 auth token (?auth= query)URL ?account= query
URL ?verseId= queryAGENT8_ACCOUNT env (Verse8 hosts only)

The ?auth= token is what the Verse8 host injects in production; the SDK parses it automatically during init(). Build-time env wins for verseId because it’s typically a build-time pin; the auth token wins for account because that’s the runtime user identity.

Step 3: Handle Purchase on Server

Identical to the React flow. When a purchase completes, update user state in your game server’s $onItemPurchased handler:

// server.js async $onItemPurchased({ account, purchaseId, productId, quantity, metadata }) { switch (productId) { case "remove-ads": await $global.updateUserState(account, { adsRemoved: true }); break; case "gold-pack-1000": const userState = await $global.getUserState(account); await $global.updateUserState(account, { gold: (userState.gold || 0) + 1000, }); break; } return { success: true }; }

Server-Side Required

All purchase state changes must happen server-side for security. The client-side onClose callback is informational only — never grant items based on it.

👉 Learn more about state management

Testing Checklist

Before going live:

  • ✅ Purchase buttons open correct products
  • ✅ Payment dialog displays correctly
  • ✅ Purchases update user state
  • ✅ State syncs to client immediately
  • ✅ Purchase limits work correctly
  • ✅ Error messages display properly
  • subscribe listeners fire on init, refresh, and after onClose

Test Thoroughly

Incorrect payment flows can harm user experience and lead to CPP removal.

API Reference

VXShop Namespace

const VXShop = { init(opts?: VXShopInitOptions): void; getItem(productId: string): VXShopItem | undefined; getItems(): VXShopItem[]; buyItem(productId: string): void; refresh(): Promise<void>; onClose(cb: (payload: VXShopDialogClosedPayload) => void): () => void; subscribe(listener: (state: VXShopState) => void): () => void; getState(): VXShopState; };

Verse8 Namespace (auth helpers)

See Auth Helpers for the standalone reference.

VXShopInitOptions

interface VXShopInitOptions { /** Override the verseId. */ verseId?: string; /** Override the account. */ account?: string; /** Set to false to skip the initial fetch. Default: true. */ autoRefresh?: boolean; }

VXShopState

interface VXShopState { items: VXShopItem[]; isLoading: boolean; error: string | null; }

VXShopItem

interface VXShopItem { id: number; verseId: string; productId: string; name: string; price: number; stock: number; imageUrl: string; description: string; metadata: string | null; purchasable: boolean; purchaseBlockReason?: string; remainingPurchaseQuantity: number | null; purchaseLimit: number | null; purchaseConstraints: PurchaseConstraints | null; purchasedCount: number; purchaseLimitReached: boolean; }

VXShopDialogClosedPayload

interface VXShopDialogClosedPayload { purchased: boolean; productId: string; action: "purchased" | "closed"; }

Next Steps

Last updated on