@chaindaddy/apps
v2.14.1
Published
Beta. Crown Apps SDK and runner framework — build React widgets and webhook-driven runners for Chain Daddy token pages.
Maintainers
Readme
Crown Apps
Beta. The SDK, the CLI and the developer program are in beta: interfaces may change between releases. Install with the
betatag.
Crown Apps are third-party extensions for Chain Daddy token pages. This repository is the SDK and runner framework you build them with: a TypeScript contract for widgets (React components rendered on a token page) and a zero-dependency helper library for runners (headless services driven by webhooks).
It is for external developers who want to ship something onto a token page — a chart, a swap box, a poll — or run a backend that reacts to platform and on-chain events.
| Flavor | What it is | Where it runs | |--------|------------|---------------| | Widget | A React component rendered on a token page, fed structured data through props | The visitor's browser, inside the host page | | Runner | A headless service that receives platform events by webhook and calls back through the Action API | Your own infrastructure |
The step-by-step walkthrough is Getting started on the docs site. This README is the reference.
Install
npm install @chaindaddy/apps@betaRequires Node.js 20+. React 18 is a peer dependency for widget development.
import type { AppWidgetProps } from '@chaindaddy/apps/types/widget'; // widget contract
import { createWebhookHandler } from '@chaindaddy/apps/runner'; // runner SDKYou do not need this package to ship an app. A project made by chaindaddy app init carries its own copy of the widget types (src/widget-types.ts), and its runner checks webhook signatures with node:crypto.
Quick Start: Widget
A widget is one React component plus a capp.json manifest. It receives the token's data through props. The quickest
start is a template: chaindaddy app init --template game|interactive|community copies a complete app from
templates/.
import type { AppWidgetProps } from '@chaindaddy/apps/types/widget';
interface MySettings {
showVolume?: boolean;
}
export function MyWidget({
appData,
settings,
theme,
onAction,
}: AppWidgetProps<MySettings>) {
return (
<div style={{ background: theme.cardBackground, color: theme.textPrimary }}>
<h3>{appData.symbol} - {appData.tokenName}</h3>
<p>Price: ${appData.market.price?.toFixed(6) ?? 'N/A'}</p>
{settings.showVolume && (
<p>24h Vol: ${appData.market.volume24h?.toLocaleString() ?? 'N/A'}</p>
)}
<button onClick={() => onAction({ type: 'toast', message: 'Hello!', variant: 'success' })}>
Toast
</button>
</div>
);
}Export a plain named function, as above. The host loads the export named by entryComponent (or the default export if there is none) and calls it as a function component. A component wrapped in React.memo, forwardRef or lazy does not load, and neither does export default defineWidget({...}).
Pair it with a manifest that names the export:
{
"schemaVersion": "1",
"id": "your-developer-id/my-widget",
"name": "My Widget",
"version": "1.0.0",
"author": { "name": "Your Name", "developerId": "your-developer-id" },
"description": "One-line description of what the widget does.",
"type": "widget",
"entryComponent": "MyWidget",
"bundleUrl": "",
"bundleHash": "",
"bundleSize": 0,
"category": "utility",
"requiredData": ["profile:basic", "market:price"],
"permissions": [],
"minPlatformVersion": "1.0.0",
"license": "MIT"
}schemaVersionis required.- Always set
type. Without it the schema applies the widget and the runner rules together. author.developerIdmust be the developer ID shown on your portal Account page, the one you submit with.- Leave the three bundle fields empty.
chaindaddy app packfills in the hash and size, and the operator sets the URL when it hosts the bundle.
Then build a single-file bundle and submit it; see Publishing.
Full working example: examples/hello-widget/.
Quick Start: Runner
A runner is an HTTP endpoint that verifies a webhook signature, handles the event, and returns a result. The SDK supplies signature verification, idempotency, and an Action API client.
import {
createWebhookHandler,
createActionClient,
withIdempotency,
MemoryStore,
} from '@chaindaddy/apps/runner';
const client = createActionClient({
apiBaseUrl: process.env.API_BASE_URL!,
appId: process.env.APP_ID!,
secret: process.env.WEBHOOK_SECRET!,
});
export const handle = createWebhookHandler({
secret: process.env.WEBHOOK_SECRET!,
onEvent: withIdempotency(async (event) => {
switch (event.eventType) {
case 'token.claimed':
await client.storage.set(event.crownId, 'claimedAt', event.timestamp);
break;
case 'market.price.threshold':
await client.storage.set(event.crownId, 'lastAlert', Date.now().toString());
break;
}
return { status: 'ok' };
}, new MemoryStore()),
});MemoryStore is for development. In production, back idempotency with durable storage — the examples use DynamoDB and Cloudflare KV.
Deployable templates: examples/express-runner/, examples/lambda-runner/, examples/cloudflare-runner/.
Working in This Repository
Clone it to build the bundled widgets, run the SDK tests, or develop a widget against the source types.
npm install # install dependencies
npm run build # build all widget bundles + the runner SDK
npm test # run the runner SDK test suite
npm run typecheck # type-check the widget sources
npm run lint # eslint, zero warnings tolerated
npm run new:widget # scaffold a new widget under apps/
npm run validate:examples # validate every capp.json against the schemanpm run build writes widget bundles to dist/{appId}/, the compiled runner SDK to dist/runner/, and a manifest index to dist/index.json. It reports each bundle's size against its tier cap, so an oversized bundle fails loudly.
Manifest Format (capp.json)
Every app must include a capp.json in its directory root, validated against schema/capp.schema.json. The server validates every submission against the same schema and refuses one that fails (PACKAGE_MANIFEST_INVALID).
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| schemaVersion | string | yes | Always "1" |
| id | string | yes | name, or <developerId>/<name> where developerId is your developer ID. Each part is lowercase letters, digits and hyphens, 3-64 characters. chaindaddy app init uses <developerId>/<dir> |
| name | string | yes | Human-readable display name |
| version | string | yes | Semantic version (major.minor.patch) |
| author | object | yes | { name, url?, developerId }. developerId must equal the developer ID you submit with, or the server refuses the package (PACKAGE_DEVELOPER_MISMATCH) |
| description | string | yes | Short description of widget functionality |
| type | string | set it | widget, headless or full. Without it the schema requires both the widget fields and runner |
| entryComponent | string | widget, full | Named export from the bundle that is the root React component |
| bundleUrl | string | widget, full | URL of the JS bundle. The operator rewrites it to its hosted copy; leave it empty in a package |
| bundleHash | string | widget, full | SHA-256 hex hash of the bundle. Rewritten to the hosted copy's hash |
| bundleSize | number | widget, full | Bundle size in bytes, at most 2,000,000. Rewritten to the hosted copy's size |
| runner | object | headless, full | Webhook endpoint, auth type, events and actions (see Runner SDK) |
| icon | string | no | URL to widget icon for the dashboard |
| category | string | yes | Where the widget picker lists it. 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) |
| requiredData | array | yes | Data scopes the widget needs (see below) |
| configSchema | object | no | JSON Schema (draft-07) for widget-specific settings |
| permissions | array | yes | Platform permissions requested (see below) |
| minPlatformVersion | string | yes | Minimum platform version (semver) |
| supportedChains | array | no | Chain keys supported. Omit for all-chain support |
| tags | array | no | Freeform tags for search/filtering |
| repositoryUrl | string | no | URL to widget source repository |
| license | string | yes | SPDX license identifier |
Data Scopes
Declare only what your widget reads. requiredData is shown to the creator when they install the app and is checked in review. The host does not filter appData by it today.
| 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 | Owner address, status, verification |
| token:metadata | Metadata extensions, highlights URI |
| token:identity | ENS/SNS, social identities |
| chain:info | Chain ID, chain name, explorer URL |
Permissions
| 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 within the platform |
| ui:fullscreen | Ask the host to show the app fullscreen (fullscreen action) |
| iap:purchase | Sell items from the token's store (iap-purchase action); see In-app purchases |
| iap:consume | Spend the signed-in viewer's own consumable, with no server (iap-consume action): on their tap, and only items the store's owner lets apps spend. See In-app purchases |
| notify:user | Send the signed-in user a notification, in-app and push (notify action): at most 5 a day, the user can mute the app, never by text or email |
| net:connect | Connect to your own server: the wss:///https:// origins in network.origins (at most 8, exact hostnames), nothing else. Requests carry Origin: null; authenticate the visitor with the session-token action. See docs.chaindaddy.io › Your own server |
| wallet:sign | Ask the viewer's wallet to sign EIP-712 typed data (sign-typed-data action); the host shows its own confirmation sheet first |
Widget API
Props
Every widget receives AppWidgetProps:
interface AppWidgetProps<TSettings = Record<string, unknown>> {
appData: AppDataContext; // Token profile data (symbol, market, social, etc.)
settings: TSettings; // Widget-specific settings from configSchema
widgetId: string; // Widget identifier matching manifest id
isOwner: boolean; // Whether connected wallet owns this crown
connectedAddress: string | null; // Connected wallet address
chainKey: string; // Chain key for the current profile (e.g., "arb")
theme: AppWidgetTheme; // Host page theme colors
onAction: (action: AppWidgetAction) => void; // Dispatch actions
breakpoint?: 'compact' | 'standard' | 'wide'; // Declared; the host does not pass it yet
}appData
symbol,tokenName,description,tags— basic profile infochainKey,chainId,chainName— chain infotokenAddress,owner,status,tokenId— on-chain state (ownerandstatusalso arrive ascrownOwnerandcrownStatus)market—{ price, volume24h, marketCap, holders, totalSupply, circulatingSupply }socialLinks—{ website, xdotcom, telegram, discord, github, whitepaper }verifications— per-platform verification recordsmetadata— extensible key-value store
Theme
interface AppWidgetTheme {
mode: 'light' | 'dark';
accentColor: string;
backgroundColor: string;
cardBackground: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
borderColor: string;
}Actions
Widgets dispatch actions to the host via onAction():
type AppWidgetAction =
| { type: 'navigate'; path: string } // Client-side navigation
| { type: 'clipboard'; text: string } // Copy to clipboard (needs clipboard:write)
| { type: 'track'; event: string; data?: Record<string, unknown> } // Analytics
| { type: 'toast'; message: string; variant: 'success' | 'error' | 'info' }; // NotificationThe full union also covers api-call, connect-wallet, sign-typed-data, transaction, quote, open-settings, fullscreen, iap-purchase, iap-consume and paid-file. Every shape is declared in types/widget.ts.
iap-purchase sells an item from the token's store: the host opens its own purchase sheet and resolves { status: 'confirmed' | 'paid' | 'cancelled' | 'failed', orderId?, receipt? }. The app never touches the wallet or the amounts. Reading the catalog, the viewer's items and verifying receipts on a game server are in In-app purchases.
Lifecycle Hooks
Widgets can optionally implement lifecycle hooks:
interface AppWidgetLifecycleHooks {
onWidgetMount?(context: WidgetLifecycleContext): void | Promise<void>;
onWidgetUnmount?(context: WidgetLifecycleContext): void;
onDataUpdate?(prevData: AppDataContext, nextData: AppDataContext): void;
onSettingsChange?(prevSettings: unknown, nextSettings: unknown): void;
onVisibilityChange?(visible: boolean): void;
}The lifecycle context provides widgetId, appData, scopedStorage, and an abortController for async cleanup.
Widgets can also export renderStatic() to support server-side rendering.
Settings
Widgets declare configurable settings with configSchema in the manifest — a standard JSON Schema (draft-07) object. Crown managers adjust these through the widget dashboard, and the values arrive as the settings prop.
{
"configSchema": {
"type": "object",
"properties": {
"greeting": {
"type": "string",
"description": "Custom greeting prefix",
"default": "Hello"
},
"showMarketData": {
"type": "boolean",
"description": "Whether to display market data",
"default": true
}
}
}
}In your component, read settings.greeting and settings.showMarketData, typed against your own settings interface.
Runner SDK
Exports
| Export | Description |
|--------|-------------|
| verifyWebhook(secret) | Express middleware for signature verification |
| verifySignature(secret, body, sig) | Sync HMAC verification (Node.js) |
| verifySignatureAsync(secret, body, sig) | Async HMAC verification (all runtimes) |
| createWebhookHandler(options) | Framework-agnostic webhook handler |
| createActionClient(options) | Action API client factory |
| withIdempotency(handler, store) | Idempotency wrapper for event handlers |
| MemoryStore | In-memory idempotency store (development) |
| validateManifest(manifest) | Runtime manifest validation |
| signRequest(secret, body) | Sign outgoing Action API requests |
| isRateLimited(response), getWaitTime(response) | Rate-limit helpers for Action API responses |
Event and action payload types ship alongside them: AppEventType, AppEventEnvelope, AppEvent, ActionScope, and the per-event interfaces.
Event Names and Delivery
The full event list (and payload types) is exported as APP_EVENT_TYPES / AppEventType. App-lifecycle events use the wire names token.app.enabled, token.app.disabled, and token.app.configured; the short forms app.enabled, app.disabled, and app.configured are also accepted when declaring runner.events in a manifest and map to the same wire events.
The backend delivers an event only to runners whose manifest subscribes to it. An empty or absent subscription list receives everything, and test.ping from runner verification is always delivered regardless of the list. Filtering on event.eventType in your handler, as the switch above does, remains good defensive practice.
Apps
The apps in apps/ are the ones Chain Daddy serves to every token page, free on every plan. Each is a normal
Crown App, built and checked the same way as yours, so they double as working references.
| App | Folder | What it is |
|-----|--------|------------|
| Gallery | core-gallery | Images, video and audio the creator hosts, with optional holders-only pieces |
| Community Polls | core-poll | Token-weighted polls; each vote is an EIP-712 signature from the voter's wallet |
| Audio Player | core-anthem | A compact player for an uploaded track or any audio URL, falling back to the token's anthem |
| Video Player | core-intro-video | An uploaded video or any video URL, inline or full screen |
| Activity Feed | core-broadcasts | The token's recent broadcasts, highlights, media and polls |
| HTML | core-html | Rich text, images and embeds, with a sanitizer between the owner and the page |
| Price History | core-lightweight-charts | A self-hosted chart on TradingView's open-source Lightweight Charts |
| TradingView Chart | core-tradingview | TradingView's chart widgets: advanced chart, mini chart, ticker tape and more |
| 3D Viewer | 3d-viewer | Animated 3D scenes in the token's colour, from a set of presets |
| CHAP Catch | chap-catch | A Canvas 2D arcade game by og_foundry: catch coins, dodge rugs |
| Neon Breaker | neon-breaker | A WebGL brick breaker by og_foundry whose colours follow the token |
| Engine | core-engine | Boots Godot and Unity games; not an app anyone adds |
FLUX GP, our multiplayer racing game, lives in its own repository, flux-gp-kit, because its client ships with the game server that runs its races, store and prizes.
Examples
Widgets
- hello-widget: minimal reference widget covering
appData,theme,settings,breakpoint, andonAction - price-alert-widget:
storage:localpermission and theonDataUpdatelifecycle hook - iap-store: a white-label item store, in your own game (a server holding a store key) and as a Crown App (
iap:purchase), with a copyableiap-client.tshelper - 3d-viewer: bundling a third-party rendering library (Three.js) inside a widget
- godot-game: a Godot engine app that saves the player's progress, signs them in and sells the token's store items, through
window.chaindaddy - unity-bridge: the same for a Unity WebGL game: a
.jsliband a C# script
Runners
- express-runner: Express + Docker
- lambda-runner: AWS Lambda + API Gateway + DynamoDB
- cloudflare-runner: Cloudflare Worker + KV
Each runner example has its own README with deployment steps and a .env.example.
Publishing
The walkthrough is Getting started. In short:
- Join the developer program at https://chaindaddy.io/_developer with the wallet you will submit from. Your developer ID is on the portal's Account page.
- Test the bundle locally.
- Submit it, with the CLI or as an open-source app from the portal.
- When the automatic checks pass, send it for review.
Ship with the CLI
Your source stays on your machine; the operator hosts the built bundle. Install the CLI (beta):
npm install -g @chaindaddy/cli@beta
chaindaddy app login # sign in with the wallet that joined the developer program
chaindaddy app init widget yield-tracker # id defaults to <your developer id>/yield-tracker
cd yield-tracker
npm install
npm run build && npm run pack && npm run sign && npm run submitnpm run submit waits for the automatic checks. When they pass it prints chaindaddy app request-publish <id>; run that to send the build for review. chaindaddy app submit --request-review does both in one step.
The individual steps:
chaindaddy app validate
chaindaddy app build
chaindaddy app scan # the checks the server's automated scan runs
chaindaddy app pack # dist/<name>-<version>.capp
chaindaddy app sign --in-place # signs the newest package in dist/
chaindaddy app submit --watch
chaindaddy app request-publish <id>The operator extracts the package, runs the automated scan, hosts the bundle on its apps CDN with immutable cache headers, and rewrites bundleUrl, bundleHash and bundleSize to the hosted copy.
- CLI reference: every
chaindaddy appcommand .capppackage format: what gets uploaded- Agentic usage guide: CI and coding-agent recipes
- In-app purchases: sell items from a token's store and verify receipts
Submit an open-source app
On the developer portal: Apps → New app → Open-source app. Give the manifest, a public bundle URL and a public repository URL. The server fetches the bundle, checks it against bundleHash, and hosts its own copy. Token pages load bundles only from the apps CDN, so the URL you give is only where the server fetches from.
Review and going live
A submission is pending while the automatic checks run, and auto_failed if they fail. request-publish moves a passing build to in_review, and a reviewer sets it to approved or rejected.
Approval makes that build the live version, and the app appears in the app list on every token page, served under its slug. The previous approved build stays live until the next one is approved. Creators install apps from the Apps tab on their token page; a third-party app needs the token owner on Pro+ or above.
Building a Bundle Yourself
chaindaddy app build does this for you. With your own bundler, the bundle must:
- Be one ESM file, with no code splitting
- Export the component named by
entryComponentas a plain named function - Leave
react,react/jsx-runtimeandreact-domexternal. The host provides them, and they are the only bare imports allowed - Be at most 2,000,000 bytes
- Not use
eval(),new Function(),sessionStorageordocument.cookie
import { build } from 'esbuild';
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
await build({
entryPoints: ['src/index.tsx'],
bundle: true,
format: 'esm',
outfile: 'dist/bundle.js',
external: ['react', 'react-dom', 'react/jsx-runtime'],
jsx: 'automatic',
minify: true,
});
// Generate integrity hash
const bundle = readFileSync('dist/bundle.js');
console.log(`Bundle hash: ${createHash('sha256').update(bundle).digest('hex')}`);
console.log(`Bundle size: ${bundle.length} bytes`);Testing locally
Open https://chaindaddy.io/_dev/widgets and use "Test your own bundle": pick dist/bundle.js and capp.json. It renders the widget through the real loader and sandbox with sample data, and nothing is uploaded. Check both theme modes and your actions there. More in Test locally.
Security Model
Widgets are real React components rendered in the host page, not in iframes. The host checks each bundle's SHA-256 against the manifest before it runs it, and the token page's CSP loads bundles only from the apps CDN.
These are the sandbox rules. Today the DOM rule is enforced live on the page; the others are checked by the automated scan at submission and in review. The name in parentheses is what a breach is reported as.
| Area | Rule |
|------|------|
| DOM | An app may not leave a node in <body> or <head> that it added with a direct DOM call. One still there when the task ends is removed and reported (dom_injection). A node added and removed in the same task, such as a measuring probe, is fine. replaceChild / replaceChildren on <body> or <head> are refused. <style> and <link rel="stylesheet"> in <head> are allowed, for CSS-in-JS. For floating UI, render a React portal (createPortal) from your own tree; no marker attribute is honoured. |
| Network | With fetch:self-api, only the platform API: /api/ on the page origin or the API origin (unauthorized_fetch). credentials: 'include' is refused (credentialed_request); calls go with credentials: 'omit' and carry X-App-ID. Authorization, Cookie, Proxy-Authorization, X-Forwarded-For, X-Forwarded-Host and X-Real-IP are stripped (blocked_header). |
| Storage | localStorage is scoped to app-<id>- (every instance of the app) and app-<id>@<instanceId>- (one instance). |
| Globals | sessionStorage, eval and Function are blocked (blocked_global). document.cookie reads '' and writes are dropped (cookie_access). window.location is read-only (location_set). |
A report names exactly one app: the one whose own code, or a library it bundled, made the call. Nodes from the host, a browser extension or another app are never reported. First-party apps run under the same rules, but their reports never count toward disabling. Reports reach you on the Violations tab of your app's page on the portal. An app reported by many distinct visitors can be disabled automatically, and then drops off token pages.
The docs site has the same rules: Sandbox rules. Read SECURITY.md for runner authentication, permission scopes, approval gates, rate limits, storage isolation and audit logging.
Repository Structure
chaindaddy-apps/
types/widget.ts # Widget contract — props, theme, actions, lifecycle
runner/ # Runner SDK (headless app helpers)
src/
types/ # Event, action, manifest types
crypto/ # HMAC signing and verification
client/ # Action API client
idempotency/ # Event deduplication
middleware/ # Express + generic webhook handlers
validation/ # Manifest validator
__tests__/ # SDK tests
apps/ # Our apps, served to every token page (core-engine boots engine apps)
templates/ # Starter apps for `chaindaddy app init --template` (shared/ + sync script)
examples/ # Reference widgets and deployable runner templates
schema/
capp.schema.json # JSON Schema for capp.json validation
docs/ # CLI reference, package format, agentic usage, verifying tokens (+ vectors/)
scripts/ # Build, scaffold, and validation toolingContributing
Bug reports and changes to the SDK, schema, examples and docs are welcome — see CONTRIBUTING.md and CODE_OF_CONDUCT.md. Apps are submitted with the CLI or from the portal, not by merge request. To report a security issue, follow the disclosure process in SECURITY.md.
License
Apache-2.0
