Build a game with a token economy on Chain Daddy
From nothing to a live game that sells items for its own token, using FLUX GP as the worked example. It's for you, or for the AI agent you point at it: every step says what to do, why, and how to check it worked.
What you'll end up with:
- a token with its own page on Chain Daddy;
- a store on that page selling your game's items, priced in your token or in dollars;
- your game running on the page, where buying an item puts it on your car within seconds;
- if you want multiplayer, a game server that knows who owns what and spends it exactly once.
You don't write a smart contract, handle a private key or touch a payment. Chain Daddy runs the chain side, and your game asks it to.
What you need: TypeScript for the game, and Go (or any language) for the server if you want one. A browser wallet. For your own token, a Chain Daddy plan: see Step 1.
Quick start (15 minutes)
1. Play it. Open the web client and press PLAY. The token page it belongs to is chaindaddy.io/FLUXGP.
2. Run it. You need Docker.
git clone https://gitlab.com/opencrown/flux-gp-kit && cd flux-gp-kit
docker compose -f deploy/docker/compose.yaml up --buildOpen http://localhost:8080 in two tabs, choose ONLINE in both (within 15 seconds of each other), and race yourself. That's the game server and the web client, on one origin.
3. See the store the way a token page runs it. The preview is a stand-in for the page: it answers the app's requests from a copy of the real $FLUXGP store.
cd app && npm ci && npm run build
python3 -m http.server 8766 --bind 127.0.0.1 # then open http://127.0.0.1:8766/preview/Open SHOP and buy something. Add ?iap=owned to start with items in the garage, or ?iap=signedout to see a signed-out visitor.
4. Read four files. They're the whole integration:
| File | What it shows |
|---|---|
economy/catalog.json | What the store sells, and how each item finds its way into the game (metadata). |
app/src/store.tsx | The app reading the catalog and the viewer's items, and buying through the page. |
server/cdapi/cdapi.go | The server checking what a player owns, spending it, and awarding prizes. |
app/capp.json | What the app asks the page for: permissions, and the server it may connect to. |
How the pieces fit
Your token ──── its page on Chain Daddy (chaindaddy.io/YOURTOKEN)
│
├── the store: items for sale; the chain records who owns what
│
└── your app (a Crown App): the game, sandboxed on the page
│ onAction(...) ask the page to show a price, run a purchase, read the store
│
└── your game server (optional): races, ranked results, prizes
│ store key (scoped): read items, spend them, award drops
└── Chain Daddy APIThree rules explain most of the design:
- Money never passes through your code. When a player buys, the page shows its own purchase sheet, and their wallet pays the token's store directly. The chain records the result as an entitlement: this wallet owns this item, this many, until this date. Your game only reads entitlements.
- The app is untrusted, the page is trusted, the server is the authority. The app runs in a sandbox and asks the page for anything sensitive through
onAction. Anything with value is decided by your server with a store key that only it holds: spending a Nitro charge, awarding a prize, a ranked result. - Every write can be retried safely. Each call that changes something carries an idempotency key, so a network retry never spends or awards twice.
Step 1: Your token
Everything hangs off a token page, and the page comes with the token.
- Launch a new token, or claim one you already have: Create a token. To see a launch end to end,
docs/launch/is the real $FLUXGP launch, written as a runbook an AI agent can follow while you review and sign. - Decide what's permanent. The name, symbol, supply and on-chain metadata can't change after launch. The page's description, media and layout can.
- Get a plan. Publishing an app needs an active paid plan on the publishing wallet, and the Developer Beta Program Agreement signed by that wallet (App Development). Installing a third-party app on a page needs its owner on Pro+ or above.
Checkpoint: your token has a page, and you know its chain key (e.g. base) and crown id (the page's registration id; $FLUXGP's is 8).
Step 2: Your store and catalog
Install the CLI (npm install -g @chaindaddy/cli; the command is chaindaddy), give it the page owner's account key (from chaindaddy.io/_api), and open the store:
export CHAINDADDY_API_KEY=cd_live_… # the page owner's key: keep it out of your shell history and repo
chaindaddy iap store open --chain base --crown 8 # prints the store idThen decide what to sell. The store has four kinds of item, and choosing the right one is most of the design:
| Type | Use it for | In FLUX GP |
|---|---|---|
non_consumable | Something you own for good | Liveries, boost trails, tag frames, a car, the numbered Holo Foil livery (maxSupply: 250) |
consumable | Something you use up, or a bundle | Nitro charges; the starter pack (grants a livery, a trail and 3 Nitro); "Light up Mainnet Orbit", where everyone's purchases fill one bar |
timed | Access for a while | A 24-hour car rental; a 30-day season pass (each purchase adds time) |
unlock | Free while a rule holds | The Whale Hauler, while you hold 1,000,000 $FLUXGP (rule: {hold: {min}}); season items, while you own the pass (rule: {owns: {sku}}) |
Price an item in your token (price, in base units) or in dollars (priceUsdMicros), which the page converts to your token at checkout so the price doesn't swing with the chart.
The one contract between your store and your game is each item's metadata:
{ "sku": "livery.whale-blue", "type": "non_consumable", "name": "Whale Blue livery",
"price": "25000000000000000000000", "maxPerWallet": 1,
"metadata": { "game": "flux-gp", "slot": "livery", "id": "whale-blue" } }slot says what kind of thing it is (livery, trail, car, boost…) and id which one. The game reads nothing else, so you can rename, reprice and redraw items in the store without shipping a new version.
Art: 512×512, readable at 96 px, pinned to IPFS (or any HTTPS host) as the item's imageUrl. FLUX GP renders its art with the game's own renderer (app/test/render-items.mjs).
Create the items from the catalog file, as the store's owner. New items start as drafts; the script creates them on sale. Try it dry first:
DRY_RUN=1 ./economy/apply-catalog.sh <storeId>
./economy/apply-catalog.sh <storeId>Checkpoint: your items show on your token page's store, and GET /api/v2/iap/public/stores?chainKey=base&crownId=8 lists them.
Full reference: Store and Rewards.
Step 3: Sell inside your game
A Crown App is a React component plus a manifest, capp.json. The page runs it in a sandbox and hands it onAction, its line to the page. Start from the kit, or scaffold a fresh one with chaindaddy app init widget.
Ask for what you use in capp.json:
"permissions": ["iap:purchase", "fetch:self-api", "storage:local", "ui:fullscreen"]iap:purchase lets the app sell from the token's store; the install screen shows it as "Sell items from this token's store".
Read the catalog and what the viewer owns through the page, with the viewer's own session:
const catalog = await onAction({ type: 'api-call', method: 'GET',
endpoint: `/api/v2/iap/public/stores?chainKey=${chainKey}&crownId=${crownId}` });
const mine = await onAction({ type: 'api-call', method: 'GET',
endpoint: `/api/v2/iap/me/entitlements?storeId=${storeId}` });Turn items into game content with the metadata: an entitlement for livery.whale-blue becomes {slot: 'livery', id: 'whale-blue'}, and the garage offers it. The kit's slotOf in app/src/store.tsx is ten lines.
Buy by asking the page:
const res = await onAction({ type: 'iap-purchase', sku: 'livery.whale-blue' });
// res.data.status: 'confirmed' | 'paid' | 'cancelled' | 'failed'The page shows the price, the buyer approves once in their wallet, and the action resolves. confirmed means the item is theirs: read the entitlements again, and put it on the car. paid means the payment is on chain and still confirming, so show "confirming" and read again when it lands (poll /api/v2/iap/orders/{orderId}, or subscribe with subscribeStore). cancelled charges nothing.
Test it in the preview from the quick start: it plays the page's part, including cancelled and failed purchases (?buy=cancelled, ?buy=failed).
Checkpoint: in the preview, buying an item puts it in the garage, and equipping it changes the car.
Full reference: Actions and App Manifest.
Step 4: Your game server (when you need one)
A single-player game can stop at Step 3. You need a server when something must be trusted beyond one browser: multiplayer, spending a consumable, awarding a prize, a leaderboard. FLUX GP's is one Go binary (server/); the ideas carry to any stack.
Give it a store key, scoped to what it does and created by the store's owner:
chaindaddy iap key create <storeId> --name "game server" \
--scope entitlements:read,entitlements:write,rewards:awardThe secret prints once. Put it straight into the server's secrets (FLUX GP's AWS stack keeps it in Secrets Manager); never in the app.
Know who's playing. In the page, the app asks for a session token (onAction({type: 'session-token'})) and sends it to your server, which checks it against /api/v2/apps/jwks: an Ed25519 signature, then the claims typ app-session+v1 and iss https://chaindaddy.io (the header's typ is plain JWT), your app id as the audience, and the viewer's wallet as the subject. No pop-up, no password. A viewer who isn't signed in to Chain Daddy races as a guest or, with a wallet connected, gets a SIGN IN button that calls the host's sign-in action (one signature), and the session token works after it. Don't ask for a login signature of your own inside the page: it's a second prompt for the same thing. Outside the page, a wallet signs a login message (EIP-712) instead. See app/src/net/online.ts, server/auth and server/cdapi/session.go.
Check what they own before a race starts:
GET /api/v2/iap/stores/{storeId}/entitlements?wallet=0x… Authorization: Bearer cd_iap_…The server decides which car and cosmetics a player may race with. The client only asks.
Spend exactly once. A Nitro charge is spent when the race starts, keyed by race and wallet:
POST /api/v2/iap/stores/{storeId}/consume Idempotency-Key: <room>:<wallet>
{ "wallet": "0x…", "sku": "nitro", "quantity": 1 }Retry it as often as the network makes you: it's applied once.
Award prizes that can't overpay. Create an award-mode drop as the store's owner. FLUX GP's gives the Podium trophy to a ranked race's top three. The server awards it:
POST /api/v2/iap/stores/{storeId}/drops/{dropId}/award Idempotency-Key: <room>:<wallet>:podium
{ "wallet": "0x…" }The drop's caps (per wallet, in total) are enforced by Chain Daddy, so even a compromised server can't hand out more than you funded.
Pay token prizes the same way, from an award-mode airdrop campaign you fund and cap (per award, per wallet, per day, in total). The server uses a second store key, scope airdrops:award, which only the campaign's owner can pin to it, because this one moves tokens:
POST /api/v2/iap/stores/{storeId}/airdrops/{campaignId}/award Idempotency-Key: <room>:<wallet>:place-<n>
{ "wallet": "0x…", "amount": "500000000000000000000", "reason": "1st place, ranked race …" }FLUX GP's Season 0 pays 500 / 250 / 100 $FLUXGP to ranked podiums with at least four people in the race, from a 100,000 $FLUXGP pool its treasury funded (server/cdapi/economy.go).
Hear about purchases with a webhook (chaindaddy iap webhook set <storeId> --url https://…/v1/webhooks/chaindaddy). Verify each call's HMAC signature with the webhook secret, then refresh that wallet's items. Purchase receipts are signed too (Ed25519 JWS, keys at /api/v2/iap/jwks), so the server can trust one without calling back.
Deploy it: docker compose on any host, one CloudFormation stack on AWS (deploy/aws, about US$18 a month; a new version is one parameter), or Vercel for the control plane plus a container for the race rooms (deploy/vercel).
Checkpoint: GET https://<your server>/v1/info shows "economy": {"store": true, …}, and an online race with Nitro spends a charge.
Step 5: Multiplayer on the page
The app is sandboxed, so it may only connect to servers its manifest declares, and review approves:
"permissions": ["iap:purchase", "fetch:self-api", "storage:local", "ui:fullscreen", "net:connect"],
"network": { "origins": ["wss://game.example.com", "https://game.example.com"] }Your server then sees Origin: null from the page's frame: allow it in CORS. FLUX GP's client signs in with the session token, matchmakes over HTTPS and races over a WebSocket. It gets its seat back after a dropped connection, and can watch live races. The wire protocol is docs/game/PROTOCOL.md. Keep the server the authority: it runs the simulation, the client predicts its own car, and results come from the server.
Checkpoint: in the page, ONLINE finds a room, and a second browser joins the same race.
Step 6: Ship it
cd app
npm run build && npm run validate && npm run scan # the bundle, the manifest, the platform's own checks
npm run pack && npm run sign # a signed .capp package, with your developer key
npm run submit # upload; automatic checks, then reviewAutomatic checks cover security, the sandbox policy, accessibility and size; then a reviewer approves the version. Install it from your token page's Apps tab. Each new version goes through review again, and pages update to it when you choose.
Before you tell anyone:
- [ ] Every item's copy is true: what it does, how long it lasts, what "limited" means.
- [ ] Buying, cancelling and a failed purchase all look right on a phone and on desktop.
- [ ] The store key lives only on the server; the app bundle contains no secrets (
npm run scan). - [ ] Your server survives a restart: sessions, rooms and idempotency keys.
- [ ] You've bought one of everything yourself, with a real wallet.
Designing it well
The integration is the easy part. What makes players glad they bought something is the design. What we learned building FLUX GP:
- Sell fun and identity, not power. Liveries, trails, tag frames, a car that's different rather than better. FLUX GP's ranked races are stock: everyone races on the same numbers, so money never buys a ranked win. Faster cars and Nitro live in casual races.
- Make buying feel like part of the game. The item should appear on your car seconds after the wallet confirms. The shop sits behind a paused race, not on a separate page.
- Give every item a route that isn't money. Cars also unlock by level. A player who never pays should still see everything the game has.
- Use the token for more than checkout. A tag from your Chain Daddy identity; a community vote on next week's track; a community build that everyone's purchases light up together; a track drawn from the token's own chart. These are what make it this token's game.
- Holding can unlock, but never earn. Holding the token opens a car and a cosmetic lane. It never pays out, and the game never talks about the token's price going up.
- Keep money away from chance. Nothing you pay for is random: no loot boxes. Prizes come from drops you fund, and racing for them is free, because an entry fee plus a prize is gambling almost everywhere. Ask for more people in a race before a prize pays (FLUX GP asks for four): a prize worth money is worth farming with spare wallets.
- Let the server decide what matters. Clients predict and draw; the server runs the race, checks ownership, spends and awards. Put an idempotency key on every write.
- Price in dollars when you can. A $0.50 rental should cost about $0.50 whatever the chart did today.
- Write copy that stays true. "Only 250 will ever be sold" is fine because the store enforces it. Anything that implies an item or the token will be worth more later isn't.
This isn't legal advice. Rules on prizes, games of chance and tokens differ by country, so check yours before you copy the model. The full reasoning, and 18 integrations in all, are in docs/game/ECONOMY.md.
Where to take it next
The kit leaves these threads open on purpose. Each is marked Extend: in the code where it would go:
- Gifting: buy a car for a friend.
- Leaderboards and ghosts kept on the platform, readable by any app on the token.
- Sponsored liveries: another token's community ships its paint in your game.
- Cheers: spectators tip a racer, shown as fireworks, never as speed.
- Binary snapshots: the biggest bandwidth saving for big races.
Using an AI agent
Point your agent at this guide and at AGENTS.md, which maps the repo, lists the checks that must pass, and says how to add an item, a car or a track. For the token launch, docs/launch/RUNBOOK.md is written for an agent: every command, what to check, and when to stop and ask you. Two rules for any agent: it never handles a private key or seed phrase (you sign), and it never prints a store key or API key.
FLUX GP is MIT-licensed: gitlab.com/opencrown/flux-gp-kit. The same guide ships in the repo as docs/GUIDE.md, next to AGENTS.md for AI agents.