Skip to content

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:

FieldTypeDescription
idstringUnique identifier. Lowercase alphanumeric + hyphens, 3-64 chars. Pattern: /^[a-z0-9][a-z0-9-]{2,63}$/
namestringHuman-readable display name (1-128 chars)
versionstringSemantic version (major.minor.patch)
authorobject{ name, url?, developerId }
descriptionstringShort description (1-512 chars)
categorystringOne of: market, social, analytics, utility, media, governance, custom
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, it defaults to widget for backwards compatibility.

Widget Fields

Required when type is widget or full:

FieldTypeDescription
entryComponentstringNamed export from the bundle (valid JS identifier)
bundleUrlstringHTTPS URL to the JS bundle (empty string for built-in)
bundleHashstringSHA-256 hex hash (empty string for built-in)
bundleSizenumberBundle 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"]
  }
}
FieldTypeRequiredDescription
endpointUrlstringYesHTTPS URL for webhook delivery
authTypestringYeshmac-sha256, bearer, or custom-header
eventsarrayYesEvent types to subscribe to (at least one)
healthCheckPathstringNoPath for health checks (e.g., /health)
timeoutnumberNoResponse timeout in seconds (5-300, default 30)
schedulestringNoCron expression for schedule.tick events
retryPolicyobjectNo{ maxRetries, backoffMs[] }
actionsarrayNoAction scopes the runner requires

Optional Fields

FieldTypeDescription
iconstringURL to app icon
configSchemaobjectJSON Schema (draft-07) for configurable settings
supportedChainsarrayChain keys supported (omit for all chains)
tagsarrayFreeform tags for search/filtering
repositoryUrlstringURL 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.

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

Widget Permissions

Permissions for browser/platform capabilities:

PermissionDescription
fetch:self-apiFetch to the platform's own API
storage:localScoped localStorage for the widget
clipboard:writeWrite to system clipboard
navigate:internalClient-side navigation

Action Scopes

Permissions for runner-to-platform actions:

ScopeCategoryApproval
crown:readReadNever
market:readReadNever
apps:readReadNever
crown:metadata:writeWriteConfigurable
apps:config:writeWriteConfigurable
apps:storage:writeWriteNever
notifications:sendWriteNever
crown:transfer:initiateDangerousAlways
crown:apps:manageDangerousAlways

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/packages

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