---
name: build-a-crown-app
description: Build, test and publish a Crown App - an app any Chain Daddy token page can install - as a sandboxed React widget, a headless runner that receives signed events, or both. Covers the CLI path, capp.json manifest and permissions, the sandbox rules, actions like iap-purchase and session-token, local testing, review, and what paid apps can and can't do today. Use when someone wants to put a game, storefront, gallery or tool on a token page.
license: MIT
---

# Build a Crown App

A Crown App runs on token pages. The token's owner adds it from the page's **Apps** tab; every app is free to
install on every plan.

| Type | What it is | Scaffold |
|---|---|---|
| `widget` | A React component, sandboxed in a frame on the page | `chaindaddy app init widget my-app` |
| `headless` | A runner: your HTTPS endpoint that receives the page's signed events | `chaindaddy app init headless my-runner` |
| `full` | A widget and a runner | `chaindaddy app init full my-app` |

Starters with working code: `game` (three.js arcade game with a shop), `interactive` (canvas piece),
`community` (poll, links, holder count): `chaindaddy app init game my-game`.

## Who can publish

- **Testing locally needs no account.** Start there.
- **Publishing** needs any active paid plan (not a trial) and the Developer Beta Program Agreement, signed at
  https://chaindaddy.io/_developer with the wallet that holds the plan. Check with `chaindaddy app whoami`.
- Every build is a signed `.capp` package, checked automatically, then reviewed by a person.

## The path

```bash
npm install -g @chaindaddy/cli@beta           # Node 20+
chaindaddy app init widget my-app && cd my-app && npm install
npm run dev                                   # or chaindaddy app dev: a token page on 127.0.0.1:5180
# when it works:
chaindaddy login                              # the human approves in their browser; no key to paste
npm run validate && npm run build && npm run scan
npm run pack && npm run sign && npm run submit
chaindaddy app submit --request-review        # or press Submit for review on the build in the portal
```

Add `--json --non-interactive` to CLI calls you run as an agent. Exit codes: `0` ok, `1` user error, `2`
system, `3` validation failed, `4` auth, `5` server.

`app dev` runs the widget in the real sandbox at real slot sizes, answers every host action from sample data
(a store, purchases, sign-in, signatures), refuses what a real page refuses, and prints every violation. Fix
those before submitting.

## The widget

- One named export (a plain function component, the name in `entryComponent`), one ESM file, at most
  2,000,000 bytes, with `react`, `react-dom` and `react/jsx-runtime` left external. `chaindaddy app build`
  enforces this.
- Props: `appData` (the token's data), `settings` (the owner's settings, from your `configSchema`), `theme`
  (use its colors), `connectedAddress`, `signedIn`, `isOwner`, `breakpoint`, `onAction`, and
  `subscribeStore` (with `iap:purchase`).
- Everything sensitive goes through `onAction({ type, … })`, which resolves `{ ok, data }` or `{ ok: false, error }`.

## Permissions (`capp.json` → `permissions`)

Declare only what you use; the owner sees each one before installing and the scan checks the bundle matches.

| Permission | Lets the app |
|---|---|
| `iap:purchase` | Sell from this token's store: `onAction({ type: 'iap-purchase', sku, quantity })`. The page shows its own sheet and the viewer's wallet pays; your app never sees a wallet or names an amount |
| `net:connect` | Reach your own servers listed in `network.origins` (exact `https://` / `wss://` hosts), and ask for a `session-token` |
| `fetch:self-api` | Call the platform API (`/api/...`) with no cookies |
| `storage:local` | Keep `localStorage` between visits (512K characters) |
| `notify:user` | Send the signed-in user a notification (5 a day per user) |
| `wallet:trade` | Open the token's Buy sheet |
| `wallet:transaction`, `wallet:sign` | Propose a transaction or an EIP-712 signature, limited to your token and contracts declared in `wallet`; the viewer sees it decoded and confirms |
| `clipboard:write`, `navigate:internal`, `ui:fullscreen` | Clipboard, move to another page, fullscreen - each on a tap |

Data scopes go in `requiredData` (`profile:basic`, `market:price`, `market:holders`, …).

## Selling inside the app

```ts
const res = await onAction({ type: 'iap-purchase', sku: 'credits.100', quantity: 1, appAccountToken: user.id });
// res.data.status: 'confirmed' | 'paid' | 'cancelled' | 'failed'; 'paid' settles later
props.subscribeStore?.((e) => {
  if (e.type === 'store:order' && e.status === 'confirmed') unlock(e.sku, e.receipt);
  if (e.type === 'store:entitlements') refreshInventory();
});
```

Read the catalog and the viewer's items with `api-call` (`/api/v2/iap/public/…`, `/api/v2/iap/me/…`). An app
sells from the store of the token page it is on. The store itself: `sell-items-in-your-app`.

## Your own server

Declare `net:connect` with your origins. Get `onAction({ type: 'session-token' })` → `{ token, expiresAt }`, send
it to your server, and verify it there (`verify-receipts-and-sessions`). To sign a visitor in, send
`onAction({ type: 'sign-in' })` from a button they tap - don't ask their wallet to sign a login of your own.

## Runners

A runner receives signed events (`runner.events` in the manifest) at `runner.endpointUrl`. Verify
`X-App-Signature` (hex HMAC-SHA256 of the raw body, secret hex-decoded) before processing, dedupe by
`eventId`, and answer quickly. Per-token data goes through the Action API (`apps:storage:write`).

## Paid apps

Every app is free to install. Charging for an app itself, with a revenue share for its developer, is in
development and not live: the beta agreement includes no payment to developers. What works today is selling
items for the token from inside the app (above), at 2.5% or 0% on a Chain Daddy launchpad token. Check
https://docs.chaindaddy.io/developer/paid-apps for the current state before promising anything.

## Mistakes to avoid

- `eval`, `new Function` or `document.cookie`: the frame refuses them and the scan fails the build.
- Fetching anything not declared: images and calls to other hosts are blocked and reported as violations;
  enough reports disable the app on every page.
- Setting `location` or following a link in the frame: it takes your app off the page. Use `navigate`.
- Keying signed-in state on `connectedAddress`; use the `signedIn` prop.
- Putting a store key, API key or secret in the bundle. The bundle is public.
- Declaring permissions you don't use: review refuses them, and owners see them.
- An app name another developer uses: names are unique.

## Docs

https://docs.chaindaddy.io/developer/quickstart · widgets · manifest · actions · security · runners ·
submission · build-a-game · engine-apps (Godot and Unity web exports) - all under
https://docs.chaindaddy.io/developer/
