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
- Create Purchase UI - Design shop buttons in your game
- Integrate VXShop - Connect buttons to VXShop payment dialogs ⭐
- 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/platformimport { 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:
verseId | account |
|---|---|
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= query | AGENT8_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
- ✅
subscribelisteners fire oninit,refresh, and afteronClose
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
- 👉 Import Options — choose the right entry point for your stack
- 👉 Premium Subscription Example
- 👉 Gacha System Example
- Game Server Documentation