App Manifest
Every app ships a capp.json manifest. Unknown fields are rejected.
Required Fields
These fields are required for all app types:
| Field | Type | Description |
|---|---|---|
schemaVersion | string | Always "1" |
id | string | <your developer ID>/<name>, each part lowercase letters, digits and hyphens, 3-64 chars. The coin page knows your app by its slug (from name), not by this id |
name | string | Display name, up to 40 characters (an app already listed under a longer name keeps it). No control or text-direction characters |
version | string | Semantic version (major.minor.patch) |
author | object | { name, developerId }. developerId is your developer ID from the portal's Account page; packages naming another are refused |
description | string | Short description (1-512 chars). No control or text-direction characters |
category | string | Where the widget picker and the Apps directory list your app. One of: content (text, images, links, embeds), market (prices, charts, trades, swaps), media (video, streams, music, 3D), games, interactive (interactive media and experiences that are not games), art (visual art, illustration, generative work, collections), social (feeds, tips, community), governance (polls, votes), analytics (holder and on-chain stats), utility (tools that do a job), custom (anything else, listed as Other). Set it with chaindaddy app init --category. Your store listing in the portal can change it without a new build; a build that declares a different category from your last approved one changes it back |
requiredData | array | Data scopes the app needs |
permissions | array | Platform permissions requested |
minPlatformVersion | string | Minimum platform version (semver) |
license | string | SPDX license identifier |
App Type
The type field determines what components your app has:
| Type | Widget | Runner | Default |
|---|---|---|---|
widget | Required | Not allowed | Yes |
headless | Not allowed | Required | No |
full | Required | Required | No |
When type is omitted, the app is a widget.
Widget Fields
Required when type is widget or full:
| Field | Type | Description |
|---|---|---|
entryComponent | string | Named export from the bundle (valid JS identifier) |
bundleUrl | string | HTTPS URL of the bundle. On submission we host a copy and point this at it |
bundleHash | string | SHA-256 of the bundle file, 64 hex characters |
bundleSize | number | Bundle size in bytes (max 2,000,000) |
Runner Section
Required when type is headless or full:
{
"runner": {
"endpointUrl": "https://your-server.example.com/webhook",
"authType": "hmac-sha256",
"events": ["token.profile.updated", "app.configured"],
"actions": ["apps:storage:write"]
}
}| Field | Type | Required | Description |
|---|---|---|---|
endpointUrl | string | Yes | HTTPS URL for webhook delivery |
authType | string | Yes | hmac-sha256, bearer, or custom-header |
events | array | Yes | Event types to subscribe to, from the event types. app.enabled, app.disabled and app.configured are short forms of token.app.enabled, token.app.disabled and token.app.configured: either form subscribes, and deliveries carry the full name |
actions | array | No | Action scopes the runner requires |
Optional Fields
| Field | Type | Description |
|---|---|---|
icon | string | Your icon, a PNG, JPEG or WebP in the package, by its path: "assets/icon.png". See Store listing. A URL is accepted but never shown |
configSchema | object | JSON Schema (draft-07) for the settings a token owner can change. See Configurable settings |
tier | string | The bundle size chaindaddy app scan allows: core up to 50 KB, standard up to 250 KB (the default), rich up to 1 MB, heavy up to 2 MB |
defaultSize | string | The widget's size when it is first placed, in page grid units: "{cols}x{rows}", for example "4x3". On a coin page the tile keeps that height: content taller than the tile scrolls inside the app |
minColSpan, minRowSpan | number | The smallest width and height, in grid columns and rows, an owner can resize the widget to |
supportedChains | array | Chain keys supported (omit for all chains) |
tags | array | Tags for search, up to 5 of up to 24 characters. They seed your store listing when your first build creates the app |
repositoryUrl | string | URL of the source repository: https://, up to 2048 characters |
network | object | With net:connect: { "origins": ["wss://live.example.com", "https://api.example.com"] }, the servers your app connects to itself, and/or { "ownerOrigin": { "title": "Forum server", "description": "…" } }, one https:// server each coin page's owner sets in your app's settings (add "frame": "/embed" to show a page of that server as your app's view: Show a page of the owner's server). See Your own server and A server the coin's owner runs |
engine | object | A finished browser build ("kind": "web", a folder with an index.html) or a single-threaded Godot or Unity web export, in the package's engine/ folder: { "kind": "web", "entry": "index.html", "threads": false }, { "kind": "godot", "entry": "index", "files": [...], "threads": false }. chaindaddy app pack lists files when you leave it out. See Engine Apps |
wallet | object | What the transaction action may reach beyond your token on its own chain: chains (up to 8 more EVM chain ids) and contracts (up to 16 { chainId, address, label?, methods }), each called only in ways that move no value. methods lists every function you call on that contract as its bare Solidity signature, such as "mint(address,uint256)"; a call to any other function is refused. The same contracts are the only ones a sign-typed-data request may be for. Needs wallet:transaction or wallet:sign; reviewed with the app. See transaction |
payments | object | What your app does with money: operations and, with pay, currencies. See Payments |
contentRating | string | general (the default) or nsfw. See Content rating |
contentThemes | string[] | With nsfw, required: sexual, graphic or both. Leave it out otherwise. See Content rating |
access | object | { "tokenGating": { "type": "balance", "minBalance": "10000" } } makes your app usable only by viewers who hold the page's token. See Holders only |
display | object | { "minWidth": 320, "minHeight": 180 }: the smallest size your app works at, in CSS pixels, a floor no code page can size it under. It may also carry aspect ("16:9") with fit (contain or fill), always together |
presentation | object | { "themeTokens": ["--cd-accent", "--cd-text"] }: the page colours your app takes when a code page shows it themed. Leave it out to be shown as you draw it |
embed | boolean | true asks to run on other websites: a coin's owner can then put your app on the coin's own site. Chain Daddy decides whether your app qualifies and shows the reason when it does not. Absent means false. Since schema 1.1.0. See Apps on other sites |
Content rating
Chain Daddy is for adults. Rate your app "contentRating": "nsfw" if it has sexual content or graphic violence, and name which in contentThemes:
| Theme | What it covers |
|---|---|
sexual | Nudity or sexual content |
graphic | Graphic violence or gore |
"contentRating": "nsfw",
"contentThemes": ["sexual", "graphic"]An NSFW app is listed in its own NSFW section and only shown to visitors who choose to see mature content; its category still says what kind of app it is. Some places hide apps with a theme, so list every theme your app shows. Leave both fields out, or set "contentRating": "general", for everything else.
For now, an NSFW app:
- Shows no photographs of sexually explicit content. Illustrated or stylised art is fine.
- Needs a verified identity. Verify your identity in the Developer Portal before you submit it.
- Takes no payments. It declares no
payments.operationsand none of their permissions, carries nopaid/files, and has no install price or items for sale.
A build is turned down, so you can fix it, when its content does not fit its category or it shows a theme its manifest does not declare. The notice names the change to make, and neither counts against you. No rating allows sexual content involving minors or anyone presented as under 18, or sexual content without consent.
Payments
Your app takes and moves money only through Chain Daddy's checkout, and payments says how. Each operation stands for one permission, and the two must match: a money permission without its operation, or an operation without its permission, fails validation.
| Operation | Permission | What it is |
|---|---|---|
sell | iap:purchase | Sell store items through the checkout (iap-purchase) |
spend | iap:consume | Spend a viewer's consumable items (iap-consume) |
pay | iap:pay | Pay wallets through the checkout: prizes, tips, splits (pay) |
swap | wallet:trade | Open the trade sheet (quote) |
contract | wallet:transaction | Propose a transaction on a contract in wallet.contracts (transaction) |
sign | wallet:sign | Ask for a signature (sign-typed-data, or sign-message on a Solana coin) |
currencies lists what you pay in, on the page's chain: token (the page's own token) or usdc (the chain's dollar stable); native (the chain's coin) may be listed for what you sell. The checkout resolves each to its address, so you never name one. It is required with pay; what a store item costs is set by its store.
"permissions": ["iap:purchase", "iap:pay"],
"payments": { "operations": ["sell", "pay"], "currencies": ["usdc", "token"] }The page refuses an action whose operation you did not declare, a pay in a currency you did not declare, and a transaction to a function not in that contract's methods. Each refusal resolves { ok: false, error } naming what to add, and is counted toward disabling the app the way sandbox violations are.
Apps approved before payments and methods existed have until 9 November 2026 to add them. Until then validation warns (chaindaddy app validate, and the declarations line of your build's scan log) and the page holds such an app to its permissions alone; from that date a manifest without them fails validation and the page refuses the undeclared actions. An app that declares is held to its declaration at once.
Holders only
access.tokenGating limits your app to viewers who hold at least minBalance of the token whose page it is installed on:
"access": { "tokenGating": { "type": "balance", "minBalance": "10000" } }type:balancecounts the page's token;nftcounts NFTs held when the page's token is an NFT collection.minBalance: a whole number, as a string. Forbalanceit is whole tokens, as the page shows them ("10000"is 10,000 tokens, whatever the token's decimals); fornftit is a count."1"means any holding.
The gate is enforced by Chain Daddy's servers, on every plan, and always against the token of the page your app is on: you cannot point it at another token. The gate comes from the manifest of your approved version, so changing it takes a new version.
- A viewer who is not signed in, or whose wallets together hold less than
minBalance, does not get your app's settings or its config. The page shows a lock in its place, "Hold at least 10,000 $SYMBOL to use this", with Sign in to unlock for a viewer who is not signed in, and your app does not run. - A signed-in viewer who holds enough gets your app as usual. Wallets linked to the account count together.
- If a balance cannot be read, the app stays locked.
- The page's owner and managers always see it.
- If the owner also limits your app to one of their holder groups, a viewer needs both.
The gate withholds what the page stores for your app. Your bundle is public, so keep anything that must stay private on your own server and check the viewer there.
Data Scopes
Declare what token data your app needs. The creator sees this list before installing.
| Scope | Description |
|---|---|
profile:basic | Symbol, name, description, tags |
profile:social | Social links, verifications |
market:price | Price, volume, market cap |
market:holders | Holder count, holder stats |
token:ownership | Token owner address, registration status |
token:metadata | Token metadata extensions |
token:identity | ENS/SNS, social identities |
chain:info | Chain ID, name, explorer URL |
Widget Permissions
Permissions for browser/platform capabilities. The owner sees each one before installing.
| Permission | Description |
|---|---|
fetch:self-api | Call the platform's own API, without cookies (see sandbox rules) |
storage:local | Keep localStorage between visits, scoped to your app. For a Godot or Unity engine app, also what the engine saves on the device. Without it, what your app writes lasts only until the page closes |
clipboard:write | Write to the visitor's clipboard with the clipboard action, on the visitor's tap in your app (refused without this permission). Shown at install as "Can write to your clipboard" |
navigate:internal | Move the visitor to another page of the site with the navigate action, on their tap in your app |
ui:fullscreen | Show the app fullscreen with the fullscreen action, sent from a tap in your app. requestFullscreen() inside your frame does not work |
iap:purchase | Sell items from this token's store with the iap-purchase action. The page shows its own purchase sheet; your app never sees the wallet or names an amount or a recipient. Shown at install as "Sell items from this token's store". See Selling from an app |
iap:consume | Spend the signed-in visitor's own consumable with the iap-consume action, on their tap, with no server of yours. Only items the store's owner lets apps spend. Shown at install as "Spend items a visitor owns in this token's store, on their tap (only items the store lets apps spend)". See Spending from the app |
iap:pay | Pay up to 3 wallets through the Chain Daddy checkout, in USDC or the page's token, with the pay action. It is how an app moves value: player-to-player trades, tips, payments to you. See Paid Apps |
notify:user | Send the signed-in user a notification with the notify action: in-app and by push, at most 5 a day to one user, who can mute your app. Shown at install as "Send you notifications (you can mute the app)" |
wallet:transaction | Propose a transaction with the transaction action. The page checks it against the manifest's wallet scope and shows it decoded before the viewer's wallet asks. It moves no value. Shown at install as "Can propose transactions (you see each one decoded and confirm it)" |
wallet:trade | Open this token's Buy sheet with the quote action. The viewer chooses and confirms the trade |
wallet:sign | Ask the viewer's wallet to sign a message (EIP-712) with the sign-typed-data action: in your app's own name, or for a contract the wallet scope declares, and never one that grants access to tokens. Shown at install as "Can ask you to sign messages (you see what each one is for and confirm it)" |
net:connect | Connect to your own server: new WebSocket() and fetch() to the origins in network.origins, and nowhere else. Reviewed with each version, and shown at install as "Can connect to api.example.com". Also lets the app ask for a session token, unless it declares network.ownerOrigin, whose server each page's owner chooses (A server the coin's owner runs). See Your own server |
ui:pointer-lock | Engine apps only: lock the pointer for mouse-look, with the engine's own pointer lock call. The browser locks it only after the player clicks the game, and Esc releases it. Reviewed with each version, and shown at install as "Can hide and lock your mouse pointer while you play (Esc releases it)". A widget that declares it gets nothing |
Action Scopes
What a runner may do on the platform, in runner.actions. The only runner actions are storage, under apps:storage:write. The read scopes token:read, market:read and apps:read are accepted, but no action uses them yet.
A complete example
{
"schemaVersion": "1",
"id": "4e82adad-1cf0-4214-a4d6-b324c5f60442/market-chart",
"name": "Market Chart",
"version": "1.0.0",
"author": { "name": "Dev Co", "developerId": "4e82adad-1cf0-4214-a4d6-b324c5f60442" },
"description": "Interactive price chart for token pages",
"type": "widget",
"entryComponent": "MarketChart",
"bundleUrl": "https://cdn.example.com/market-chart/1.0.0/bundle.js",
"bundleHash": "9f2b…64 hex characters…",
"bundleSize": 45000,
"category": "market",
"requiredData": ["market:price", "profile:basic"],
"permissions": [],
"minPlatformVersion": "1.0.0",
"license": "MIT"
}Validation
chaindaddy app validateThe schema is capp.schema.json. The CLI checks against it, and so does the server at every submission, so a manifest the CLI accepts is one the server accepts. It is published at https://docs.chaindaddy.io/schemas/capp.schema.json, and each version, which never changes, at https://docs.chaindaddy.io/schemas/capp/<version>.json: see versions.
Submitting
Pack, sign and submit: Publish your first app. An open-source app is submitted the same way: name its public repository in repositoryUrl and it is listed as open source.