# Build my own browser game store on Crystallize, like Troll Universe

I want a storefront for my business that works like **Troll Universe** (https://troll.superfast.shop), a demo store built on Crystallize: A 3D adventure game in the browser where the shop is part of the world: access to the game, cosmetics, gear and bundles are Crystallize products, sold by merchants in five worlds of Norwegian nature and folklore and bought through real carts and orders.

The content model and the wiring below are proven in that store: keep them for the features I want, and rename things to fit my trade. If a feature doesn't fit my business, leave its parts out rather than forcing it. Everything I see is mine to decide: brand, colours, typography, layout, copy and pages. Don't copy the demo's look; build my brand.

What the demo does: digital goods, sale price lists. It is B2C, in English, Norwegian, Swedish; my markets and languages may differ.

## How to work with me

- You need to run commands, edit files and use MCP tools. If you can't (for example in a plain chat), tell me, and suggest I paste this prompt into Claude Code, Cursor or another coding agent instead.
- Ask your questions a few at a time (three to five), in the order of the steps below, and suggest a sensible default for each so I can just say yes. Ask each thing once, even where two lists overlap, and ask the ones that change the build first. Don't start a step before the one before it is done.
- The technical detail below is for you. Talk to me in plain words, and explain a choice only when I need to make it.
- Keep the folder empty until step 5, apart from `.env.local`.
- Never ask me to paste an access token secret into the chat, and tell me not to paste a filled-in command or its output either. Tell me which command to run or which file to put it in, and I'll do it.
- Before you create or change anything in Crystallize, show me the plan (shapes, pieces, topics, price variants, markets) and wait for my yes. Before an import, show me a sample of five items.
- Use the Crystallize skills (the `skills` MCP tool, or the use-crystallize plugin) for how-to: `content-model`, `information-architecture`, `taxonomy`, `data-creation`, `mass-operations`, `pricing`, `query`, `mutation`, `js-api-client`, `responsive-images`, `payments`, and when this store needs them `subscriptions`, `bookable-resources`, `vector-ranking` and `permissions`. Where this prompt and a skill disagree, the skill wins. Check the live schemas with the MCP instead of guessing field names.
- When something I asked for isn't possible in Crystallize the way I pictured it, say so and offer the closest thing that is.
- **The storefront never reads from the Core API on customer-facing pages.** Products, public prices, stock and content come from the Discovery API and the Catalogue API; confidential prices (negotiated B2B price lists) from the Catalogue API on the server.
- **Use the Shop API for everything about customers:** carts, checkout, customers, orders (including orders created directly, such as renewals), order meta, pipeline stages, payments and subscription contracts. Take the customer identifier from the server session, never from the browser. The Core API is for setup scripts, back-office tools, scheduled jobs and what the Shop API can't do, always on the server and never once per page view.

## Step 1: Crystallize tenant, tokens and MCP

Ask me:
1. Do I have a Crystallize account and a tenant for this store? If not, I sign up for free at https://app.crystallize.com/signup and create an empty tenant. Ask for its **tenant identifier**.
2. Do I have access tokens? If not, tell me: in the Crystallize App, go to **Settings → Access Tokens → Generate a new token**, name it, and keep the ID and the secret (the secret is shown once). A token acts with its user's role, so while we build, that user needs to be Tenant Admin (or have a role that can write shapes, pieces, topics, items and prices and run mass operations). Once the store is built, switch the MCP to a token from a read-only user.
3. Is the Crystallize MCP connected, with write access? Check that you have its write tools (`run-mass-operation`, `mutate-core`), not only the read tools (`tenant-overview`, `fetch-content-model`): with only the read tools, it was added without `?exposeWrite=true`. If it's missing, have me run this in my terminal with my own token, then restart you:
   ```bash
   npx add-mcp "https://mcp.crystallize.com/mcp?exposeWrite=true" \
     --header "X-Crystallize-Access-Token-Id: <token id>" \
     --header "X-Crystallize-Access-Token-Secret: <token secret>"
   ```
   If the MCP can't be connected in my tool, say so and fall back to scripts that call the Crystallize APIs with the tokens from `.env.local`.
   In Claude Code, also suggest the Crystallize skills: `/plugin marketplace add crystallizeapi/ai`, then `/plugin install use-crystallize@crystallize-ai`. The plugin brings its own read-only Crystallize MCP entry with placeholder credentials: keep using the one added above (and add `&exposeSkills=false` to its URL once the plugin's skills are installed).
4. Confirm the connection with `tenant-overview`, and tell me what's already in the tenant. If it isn't empty, ask whether to build next to what's there or in a fresh tenant.

The storefront needs the tenant identifier and its own credentials: an access token pair for the Shop API (server only), and a static auth token if Discovery and the Catalogue API are secured. Don't reuse the admin build token in production. Put them in `.env.local` (have me fill in the secrets) and make sure `.env*` is in `.gitignore`.

## Step 2: my business and my brand

Ask me:
- What I sell, who buys it, and what makes us different. My brand name and a one-line pitch.
- My markets, currencies and languages, and my VAT/tax setup.
- Where my product data lives today: a spreadsheet or CSV, an export from Shopify, WooCommerce or another platform, a PIM or ERP, my current website, or nothing yet. Where my product images are.
- My brand: logo (SVG if I have it), colours, typefaces, tone of voice, and two or three sites whose look I like. If I have no brand guide, propose a direction from my answers and show it before building pages.
- Where it should run: my own domain, and hosting (suggest Vercel for Next.js).

Then ask what is particular to this kind of store:

- What kind of game is it, and what do I sell: access to the game as a one-time purchase, optional items inside it (cosmetics, gear, skills that change the gameplay, emotes), bundles, or all of these?
- My rules for selling inside a game. The demo's are: a fixed price on everything, no in-game currency, never random packs, and the game is complete without the optional items. Do I keep them?
- Which items come in variants (a beanie or a sweater in several colours, each with its own price and image)?
- How do I mark down: weekly deals on single items, bundles priced below the sum of their parts, or both?
- Who sells in the game: one shop behind a key, or merchants standing in the world who each carry part of the range?
- May players buy for each other (a gift to a player nearby)?
- My currency or currencies, VAT for digital goods per market, and languages.
- Is there an age limit (the demo is 18+ and asks for confirmation at sign-up)?
- Where do accounts, saves and multiplayer live? Crystallize keeps the customers, carts and orders; it is not the game's account system. The demo uses Supabase for sign-in by magic link, saves and realtime.
- My payment provider. The demo's payment is a mock: the carts and orders are real, but no money is charged.
- Where do the product images come from? The demo renders them from the game's own models and effects, so what you buy is what you get.

## Step 3: the content model

Create this in my tenant with the MCP, renamed to fit my trade, after I have approved the plan. Keep the structure: it is what the storefront's features depend on.

Build in dependency order, since mass operations run in sequence with no rollback: pieces and shapes (empty first, then their components), topic maps (at most 30 topics per `topic/create`), price variants, markets and VAT types, the folder tree (two to four levels, five to twelve per level), and only then items. Use lowercase-hyphen identifiers. Decide now which fields are translated and which are discoverable. Restrict every item relation with `acceptedShapeIdentifiers`. Never add price or stock components to a shape: variants have them built in.

- **folder** (folder): `description` (rich text, translated). The tree is the game → Access, and the shop → one folder per category (lanterns, companions, outfits, trails, campfire, fishing, boat, headwear, gear, camp, emotes, bundles). Each folder has an external reference `<game>/<key>`, and each product its SKU, so the setup can be run again without making duplicates.
- **digital-item** (product), one shape for everything sold:
  - `summary` (single line, translated, up to 200 characters).
  - `category` (selection, exactly one): access, lantern, companion, outfit, trail, campfire, fishing, boat, hat, gear, camp, emote, bundle.
  - `image` (one image, with alt text per language).
  - `game-effect` (single line): JSON the game reads to know what the item does, such as `{"type":"lantern","color":"#4dffc8","glow":"#58d8ff"}`. An item with variants adds `variants: { <sku>: {…} }` with what differs per colour.
  - `merchants` (selection, multiple): which merchants in the world carry the item.
  - `includes` (item relations to `digital-item` variants, up to 12 SKUs): what a bundle contains.
- Variants: one per colour on the items that have colours, each with its own SKU, price, image and a `color` attribute. Everything else has one variant.
- Pricing: gross prices, two price variants in one currency: `usd` (the regular price, always set) and `usd-sale` (set only on weekly deals and on bundles). No stock: the goods are digital and owned once.
- Languages: English (default), Norwegian and Swedish; names, summaries and alt texts in all three.
- Customers: the e-mail address is the identifier, with meta for the game account's id and the age confirmation.
- Orders: type standard, one line per item (type `digital`, with image and discount), and cart meta for where it was bought (the landing page, the shop, a named merchant), the language, and the recipient when it is a gift.

## Step 4: my data

Import my products and content with mass operations, mapping my data onto the model: validate each file with `build-mass-operation`, run it with `run-mass-operation` and follow it with `get-mass-operation-status`. Read the operation logs, not just the task status: a task can finish as complete with failed operations, and there is no rollback. Give every upsert a `resourceIdentifier` so a re-run updates instead of duplicating, and chunk large imports. Start with a small batch, check it in the Crystallize App with me, then run the rest.

Publish every imported item in each language: the storefront only sees published versions (in a mass operation, `item/publish` needs real `itemId`s). Import images from URLs with `copyRemoteAsset`, or upload them (presigned upload, then `registerImage`). Renditions are generated in a queue: after a large import, republish once they exist, and have the storefront fall back to the original image URL while `variants` is empty. Before building pages, query Discovery once; if the tenant isn't ignited yet, run `igniteDiscoApi` (with `stacks: opensearch` if we use vector ranking), wait for the task to complete, and allow a few minutes. If I have no data yet, create a small, clearly labelled placeholder set (ten or so products) so we can build the pages, and remind me to replace it.

- The demo is one game, Troll Universe: five worlds of Norwegian nature and folklore, access as a one-time purchase, and a shop of cosmetics (lanterns, companions, outfits, headwear, trails), gear, a camp, emotes, three skills that change the gameplay (a campfire, a fishing rod, a rowboat) and two bundles. The product list lives as one file in the repo and seeds the tenant.
- Product images are 1024 × 1024 WebP, one per variant, rendered on a small page of their own with the game's real models and effects, uploaded once and remembered by key.
- The game's own models are built by scripts in Blender and compressed with gltf-transform; textures are CC0 from Poly Haven and ambientCG; all sound is generated in the browser.
- From me: the game, the list of what I sell with prices and what each item does in the game, which merchant carries what, the bundles, and an image per item and colour.

## Step 5: the storefront

- A static front end (the demo is Three.js and Vite; any game engine that runs in the browser works) with server functions beside it. Every Crystallize call is made on the server: the browser never holds a Crystallize token, and price, total, discount and ownership are never decided in the browser.
- **The catalogue (Catalogue API or Discovery):** one public server route returns the whole shop in the player's language, flattened to what the game needs per item: SKU, name, price, compare-at price, currency, category, summary, effect, image with its ready-made sizes, merchants, bundle contents and variants. The price shown is the sale price when it is set and lower than the regular one; the regular price is then the compare-at. Cache it for a minute in memory and at the CDN. The landing page lists the same items from the same route.
- **The shop in the game:** categories, search, filters (owned, not owned, on sale), sorting, product pages with the colour choice, a basket of up to 12 items, "This week's offers", "Others also bought" and "My purchases". A merchant in the world opens the same shop filtered to the items whose `merchants` selection names them.
- **Try on:** the product page puts the item on the player's own character from its `game-effect` before anything is bought, and takes it off again.
- **Checkout (Shop API):** the server checks the player's session, that every SKU exists in the catalogue, that it isn't already owned and that a bundle's contents aren't partly owned; upserts the customer; then `hydrate` with lines `{ sku, quantity: 1, type: 'digital' }` and `context.price { currency, decimals: 2, pricesHaveTaxesIncludedInCrystallize: true, selectedVariantIdentifier: 'usd-sale', fallbackVariantIdentifiers: ['usd'], compareAtVariantIdentifier: 'usd' }`, so Crystallize computes the discount per line and on the total. Check that every line made it in and the total is above zero, then `place`. The payment window shows exactly what came back.
- **Order (Shop API):** a JWT from `/auth/token` on the server (scopes `cart`, `order`), then `/order` `createFromCart` with the payment. The order id is the cart id: read the order first, create it, and on failure read again, so one cart never becomes two orders. Before that, check that the cart's customer and its meta both belong to the signed-in player and that the cart is placed.
- **From order to inventory:** the SKUs on the order, plus the contents of any bundle, are added to the player's entitlements in the game's account store, where only the server can write. The game equips an owned item per slot and interprets its `game-effect`; a skill that changes gameplay is checked on the server where it matters (a catch is only accepted from a player who owns the fishing rod).
- **Gifts:** the buyer picks a player nearby; the recipient is named by an encrypted code, never a raw account id. The cart carries the recipient in its meta, the order belongs to the buyer, and the entitlement goes to the recipient.
- **My purchases:** the player's orders from the Shop API by customer identifier, read on the server with the identifier from the session: date, lines with image, price, discount and whether it was a gift.
- **Others also bought:** counted from recent orders (which SKUs the same customers bought) in a cached server job on the Core API, never per page view.
- **Setup:** one script that can be run again creates the languages, price variants, shapes, folders, products and variants on the Core API, uploads the images, sets them on both the component and the variant (so cart and order lines carry an image), and publishes every item in every language. After setup, Crystallize is the source of truth for names, prices and content.
- With a real payment provider: create the payment session from the placed cart, and let the provider's verified webhook create the order and grant the entitlements. The browser must never be able to trigger an entitlement.
- Env: `CRYSTALLIZE_TENANT_IDENTIFIER`, `CRYSTALLIZE_TENANT_ID`, `CRYSTALLIZE_ACCESS_TOKEN_ID`, `CRYSTALLIZE_ACCESS_TOKEN_SECRET`, on the server only, plus those of the account system and the e-mail sender.

Build my design, not a demo's: my typefaces, colours and tone, with pages that suit my products. Show me the home page, a category and a product page early, and adjust the design with me before building the rest.

Every page is built for search engines, answer engines and AI agents from the start, not added at the end:
- **SEO:** server-rendered HTML, one `<h1>`, a unique title and description, canonical URLs, `hreflang` per language, a sitemap and `robots.txt`, and clean, translated paths.
- **Structured data:** JSON-LD on every page that has a subject: `Organization` and `WebSite` (with a `SearchAction`) on the home page, `Product` with `Offer` (price, currency, availability) on product pages, `BreadcrumbList` on categories and products, `Article` or `Recipe` on content. Generate it from the same data as the page.
- **GEO, for answer engines and agents:** `/llms.txt` (an H1, a summary, links to every section), `/llms-full.txt`, and a Markdown twin of every page (its URL plus `.md`, or `Accept: text/markdown`), with prices, stock and facts written out in plain sentences.
- **Agents can act, not only read:** register the store's actions (search, add to cart, check availability) with WebMCP (`navigator.modelContext.registerTool`, or annotated forms), and keep the accessibility tree clean: semantic HTML, labelled controls, real buttons and links.
- **Fast and stable:** responsive images from Crystallize, no layout shift (reserve space for images and late content), little client JavaScript. Aim for 100 in every Lighthouse category (Performance, Accessibility, Best Practices, SEO; heavy 3D or video pages may score lower on performance) and pass every Agentic Browsing check.

## Step 6: run it, then ship it

Run it locally and walk me through each feature above with my own data. Fix what I find. Before you call it done, run Lighthouse on the home page, a category and a product page (`npx lighthouse <url> --only-categories=performance,accessibility,best-practices,seo,agentic-browsing`), validate the JSON-LD (https://validator.schema.org), open `/llms.txt` and a page's `.md` twin, and fix what falls short. Deploy only when I ask: set the same environment variables on the host, point my domain, and give me the live URL.

## Watch out for

- Keep ownership out of the browser and out of the save file: a purchase is an order in Crystallize and an entitlement only the server can write.
- A bundle grants its contents, not itself: resolve `includes` when the order is created, and refuse a bundle when the player already owns part of it.
- Make every step safe to repeat: the same cart paid twice must give one order and no new entitlements.
- Weekly deals are a sale price variant, not a promotion: the product page can show was and now, and the cart computes the same discount.
- Never leave the regular price empty: it is the fallback and the compare-at price.
- Set the image on the variant as well as on the component, or cart and order lines come back without one.
- Paths differ per language, so find folders by shape or external reference rather than by path.
- Publish every item in every language, then the tree; a language published without content gives items with no name.
- A purchase announced to other players is only a message. What they see on a character must come from the server-confirmed profile.
- Don't invent facts about my business (prices, stock, certifications, delivery promises). Ask, or leave a clearly marked placeholder.

And for every Crystallize store, from the skills:
- **Orders:** create and change them through the Shop API `/order` (`createFromCart`, `create`, `setMeta` with `merge: true`, `addToStage`, `setPayments`). Editing an order in Core adds a duplicate to the Shop API's list. Read a customer's orders with the identifier from the session, `limit: 100`, grouped by `coreId` (keep the newest). Seed order history through `/order` `create`, not Core `registerOrder`. The order id is the cart id; don't call `fulfill` after `createFromCart`.
- **Payments:** create the order from the payment provider's verified webhook, never from the browser, once per cart.
- **Cart meta** goes in `hydrate(input: { meta })`; `hydrate` is the whole cart, so send every line each time.
- **Images:** serve them straight from Crystallize (not through an image optimizer such as `next/image` in its default setup): a WebP `srcset` from the returned `variants` with an accurate `sizes`, falling back to the original URL while renditions are missing. Replace an image by uploading a new one, not with `registerImageRevision`.
- **Ranking (if the store ranks per shopper):** check that `__type(name: "RankByInput")` exists; pass `context` as a variable, with `userTaste` entries of `{ vocabulary, weights, magnitude }` (magnitude = the square root of the sum of squared weights); `tieBreaker` is required; don't combine `sorting` with ranking, and page with `skip` and `limit` inside the rerank window; introspect `TenantRankByField` for field names; leave `context` out when there's no taste; use `explain: true` only for tuning.
- **Bookings (if the store books):** durations are in seconds; the role needs the `bookingPolicies` permission; put the customer on the cart before `bookSkuItem`; every `hydrate` must resend each booked line with its window and unit, or the booking is cancelled; remove a booking by re-hydrating without it.

## Done when

- My tenant has the content model above with my products, prices and images in it.
- The storefront runs on my domain in my brand, and every feature I chose works with my data: we have clicked through each one together.
- Lighthouse scores close to 100 in every category and every Agentic Browsing check passes on the home, category and product pages.
- I know how to add a product, change a price and publish, in the Crystallize App.
