Appearance
App Manifest
Every app is defined by a capp.json manifest file. This document is the complete reference for all manifest fields.
Required Fields
These fields are required for all app types:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier. Lowercase alphanumeric + hyphens, 3-64 chars. Pattern: /^[a-z0-9][a-z0-9-]{2,63}$/ |
name | string | Human-readable display name (1-128 chars) |
version | string | Semantic version (major.minor.patch) |
author | object | { name, url?, developerId } |
description | string | Short description (1-512 chars) |
category | string | One of: market, social, analytics, utility, media, governance, custom |
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, it defaults to widget for backwards compatibility.
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 to the JS bundle (empty string for built-in) |
bundleHash | string | SHA-256 hex hash (empty string for built-in) |
bundleSize | number | Bundle size in bytes (max 512,000) |
Runner Section
Required when type is headless or full:
json
{
"runner": {
"endpointUrl": "https://your-server.example.com/webhook",
"authType": "hmac-sha256",
"events": ["token.claimed", "market.price.threshold"],
"healthCheckPath": "/health",
"timeout": 30,
"schedule": "0 */6 * * *",
"retryPolicy": {
"maxRetries": 3,
"backoffMs": [1000, 5000, 30000]
},
"actions": ["apps:storage:write", "notifications:send"]
}
}| 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 (at least one) |
healthCheckPath | string | No | Path for health checks (e.g., /health) |
timeout | number | No | Response timeout in seconds (5-300, default 30) |
schedule | string | No | Cron expression for schedule.tick events |
retryPolicy | object | No | { maxRetries, backoffMs[] } |
actions | array | No | Action scopes the runner requires |
Optional Fields
| Field | Type | Description |
|---|---|---|
icon | string | URL to app icon |
configSchema | object | JSON Schema (draft-07) for configurable settings |
supportedChains | array | Chain keys supported (omit for all chains) |
tags | array | Freeform tags for search/filtering |
repositoryUrl | string | URL to source repository |
Data Scopes
Declare what token data your app needs. Scope names keep the crown: prefix on the wire so existing platform code accepts the same manifests, the scopes describe data about the token registration.
| 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 |
crown:ownership | Token owner address, registration status |
crown:metadata | Token metadata extensions |
crown:identity | ENS/SNS, social identities |
chain:info | Chain ID, name, explorer URL |
Widget Permissions
Permissions for browser/platform capabilities:
| Permission | Description |
|---|---|
fetch:self-api | Fetch to the platform's own API |
storage:local | Scoped localStorage for the widget |
clipboard:write | Write to system clipboard |
navigate:internal | Client-side navigation |
Action Scopes
Permissions for runner-to-platform actions:
| Scope | Category | Approval |
|---|---|---|
crown:read | Read | Never |
market:read | Read | Never |
apps:read | Read | Never |
crown:metadata:write | Write | Configurable |
apps:config:write | Write | Configurable |
apps:storage:write | Write | Never |
notifications:send | Write | Never |
crown:transfer:initiate | Dangerous | Always |
crown:apps:manage | Dangerous | Always |
Complete Examples
json
{
"id": "market-chart",
"name": "Market Chart",
"version": "1.0.0",
"author": { "name": "Dev Co", "developerId": "dev-123" },
"description": "Interactive price chart for token pages",
"type": "widget",
"entryComponent": "MarketChart",
"bundleUrl": "https://cdn.example.com/market-chart/1.0.0/bundle.js",
"bundleHash": "abc123def456...",
"bundleSize": 45000,
"category": "market",
"requiredData": ["market:price", "profile:basic"],
"permissions": [],
"minPlatformVersion": "1.0.0",
"license": "MIT"
}json
{
"id": "price-alerter",
"name": "Price Alerter",
"version": "1.0.0",
"author": { "name": "Dev Co", "developerId": "dev-123" },
"description": "Sends notifications when price thresholds are crossed",
"type": "headless",
"category": "analytics",
"requiredData": ["market:price"],
"permissions": [],
"minPlatformVersion": "1.0.0",
"license": "MIT",
"runner": {
"endpointUrl": "https://runner.example.com/webhook",
"authType": "hmac-sha256",
"events": ["market.price.threshold", "app.enabled"],
"healthCheckPath": "/health",
"actions": ["apps:storage:write", "notifications:send"]
}
}Subscription names vs. delivered event names
The manifest schema's runner.events enum uses the short names app.enabled, app.disabled, and app.configured — these are what chaindaddy app validate and chaindaddy app pack accept (token.app.enabled is rejected by the schema). The envelope delivered to your endpoint, however, carries the full event name in its eventType field: token.app.enabled, token.app.disabled, token.app.configured. Subscribe with the short names; match on the full names in your handler. See the Event Reference.
Validation
Validate your manifest using the SDK:
typescript
import { validateManifest } from '@chaindaddy/apps/runner';
const manifest = JSON.parse(fs.readFileSync('capp.json', 'utf8'));
const result = validateManifest(manifest);
if (!result.valid) {
console.error('Manifest errors:', result.errors);
}The same JSON Schema is used by chaindaddy app validate and can be fetched from the API at GET /api/v2/developer/schema/app-manifest (authenticated).
Submitting Your App
The supported submission path is the chaindaddy CLI. Do not hand-POST capp.json to a backend endpoint — the modern submit flow takes a signed .crown archive (the manifest + bundle + signature) via multipart upload, not raw manifest JSON.
bash
chaindaddy app validate # local schema check against the JSON Schema above
chaindaddy app pack # builds <slug>.crown (manifest + bundle + META.json)
chaindaddy app sign # signs the package with your developer key
chaindaddy app submit <pkg> # uploads to POST /api/v2/developer/apps/packageschaindaddy app submit reads the field names from your capp.json as written in this document (camelCase: entryComponent, minPlatformVersion, author.developerId, repositoryUrl). The backend's package handler reads those same camelCase fields directly from the embedded manifest — no rewriting required.
Legacy JSON endpoint
The older POST /api/v2/developer/apps/submit JSON endpoint still exists for the E2E test harness and pre-CLI integrations. It parses a snake_case subset of fields (entry_point, min_crown_version, author.wallet) into its internal validator. New integrations should use the CLI / /api/v2/developer/apps/packages path documented above — the legacy endpoint is not maintained against the JSON Schema and will reject manifests authored to this document.