Skip to content

App Manifest ​

Every app ships a capp.json manifest. Unknown fields are rejected.

Required Fields ​

These fields are required for all app types:

FieldTypeDescription
schemaVersionstringAlways "1"
idstring<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
namestringDisplay name, up to 40 characters (an app already listed under a longer name keeps it). No control or text-direction characters
versionstringSemantic version (major.minor.patch)
authorobject{ name, developerId }. developerId is your developer ID from the portal's Account page; packages naming another are refused
descriptionstringShort description (1-512 chars). No control or text-direction characters
categorystringWhere 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
requiredDataarrayData scopes the app needs
permissionsarrayPlatform permissions requested
minPlatformVersionstringMinimum platform version (semver)
licensestringSPDX license identifier

App Type ​

The type field determines what components your app has:

TypeWidgetRunnerDefault
widgetRequiredNot allowedYes
headlessNot allowedRequiredNo
fullRequiredRequiredNo

When type is omitted, the app is a widget.

Widget Fields ​

Required when type is widget or full:

FieldTypeDescription
entryComponentstringNamed export from the bundle (valid JS identifier)
bundleUrlstringHTTPS URL of the bundle. On submission we host a copy and point this at it
bundleHashstringSHA-256 of the bundle file, 64 hex characters
bundleSizenumberBundle size in bytes (max 2,000,000)

Runner Section ​

Required when type is headless or full:

json
{
  "runner": {
    "endpointUrl": "https://your-server.example.com/webhook",
    "authType": "hmac-sha256",
    "events": ["token.profile.updated", "app.configured"],
    "actions": ["apps:storage:write"]
  }
}
FieldTypeRequiredDescription
endpointUrlstringYesHTTPS URL for webhook delivery
authTypestringYeshmac-sha256, bearer, or custom-header
eventsarrayYesEvent 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
actionsarrayNoAction scopes the runner requires

Optional Fields ​

FieldTypeDescription
iconstringYour 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
configSchemaobjectJSON Schema (draft-07) for the settings a token owner can change. See Configurable settings
tierstringThe 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
defaultSizestringThe 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, minRowSpannumberThe smallest width and height, in grid columns and rows, an owner can resize the widget to
supportedChainsarrayChain keys supported (omit for all chains)
tagsarrayTags for search, up to 5 of up to 24 characters. They seed your store listing when your first build creates the app
repositoryUrlstringURL of the source repository: https://, up to 2048 characters
networkobjectWith 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
engineobjectA 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
walletobjectWhat 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
paymentsobjectWhat your app does with money: operations and, with pay, currencies. See Payments
contentRatingstringgeneral (the default) or nsfw. See Content rating
contentThemesstring[]With nsfw, required: sexual, graphic or both. Leave it out otherwise. See Content rating
accessobject{ "tokenGating": { "type": "balance", "minBalance": "10000" } } makes your app usable only by viewers who hold the page's token. See Holders only
displayobject{ "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
presentationobject{ "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
embedbooleantrue 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:

ThemeWhat it covers
sexualNudity or sexual content
graphicGraphic violence or gore
json
"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.operations and none of their permissions, carries no paid/ 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.

OperationPermissionWhat it is
selliap:purchaseSell store items through the checkout (iap-purchase)
spendiap:consumeSpend a viewer's consumable items (iap-consume)
payiap:payPay wallets through the checkout: prizes, tips, splits (pay)
swapwallet:tradeOpen the trade sheet (quote)
contractwallet:transactionPropose a transaction on a contract in wallet.contracts (transaction)
signwallet:signAsk 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.

json
"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:

json
"access": { "tokenGating": { "type": "balance", "minBalance": "10000" } }
  • type: balance counts the page's token; nft counts NFTs held when the page's token is an NFT collection.
  • minBalance: a whole number, as a string. For balance it is whole tokens, as the page shows them ("10000" is 10,000 tokens, whatever the token's decimals); for nft it 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.

ScopeDescription
profile:basicSymbol, name, description, tags
profile:socialSocial links, verifications
market:pricePrice, volume, market cap
market:holdersHolder count, holder stats
token:ownershipToken owner address, registration status
token:metadataToken metadata extensions
token:identityENS/SNS, social identities
chain:infoChain ID, name, explorer URL

Widget Permissions ​

Permissions for browser/platform capabilities. The owner sees each one before installing.

PermissionDescription
fetch:self-apiCall the platform's own API, without cookies (see sandbox rules)
storage:localKeep 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:writeWrite 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:internalMove the visitor to another page of the site with the navigate action, on their tap in your app
ui:fullscreenShow the app fullscreen with the fullscreen action, sent from a tap in your app. requestFullscreen() inside your frame does not work
iap:purchaseSell 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:consumeSpend 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:payPay 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:userSend 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:transactionPropose 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:tradeOpen this token's Buy sheet with the quote action. The viewer chooses and confirms the trade
wallet:signAsk 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:connectConnect 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-lockEngine 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 ​

json
{
  "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 ​

bash
chaindaddy app validate

The 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.