Widget Development
A widget is a React component on a coin page. The token's data arrives through props, and the page does things for the widget (a sale, a signature, a dialog, a platform API call) when it sends an action. To start one and ship it, follow Publish your first app; this page is what a widget is given and the rules its bundle follows.
How a widget runs
- It runs in a sandboxed frame on the coin page, with no access to the page's DOM, cookies or session. What it may do there is in the sandbox rules.
- The page checks the bundle's SHA-256 against the approved manifest before running it, and loads bundles only from the apps CDN, where Chain Daddy hosts every build.
localStorageis scoped to your app.
Widget Props
Every widget receives AppWidgetProps:
interface AppWidgetProps<TSettings = Record<string, unknown>> {
appData: AppDataContext; // Token data for the current page
settings: TSettings; // The owner's settings for your widget (your configSchema)
widgetId: string; // Your widget ID
isOwner: boolean; // Whether the viewer owns this token
connectedAddress: string | null; // Viewer's wallet address
signedIn?: boolean; // Viewer is signed in to Chain Daddy as that wallet
chainKey: string; // Chain key (e.g., "arb")
theme: AppWidgetTheme; // Host page colors
onAction: (action) => Promise<WidgetActionResponse> | void; // Ask the page to do something
breakpoint?: 'compact' | 'standard' | 'wide';
isFullscreen?: boolean; // Your app is showing fullscreen (see the fullscreen action)
subscribeStore?: (listener) => () => void; // Store events, with iap:purchase
}Token Data
The appData object provides structured token data:
interface AppDataContext {
symbol: string; // "DOGE"
tokenName: string | null;
description: string | null;
tags: string[];
chainKey: string; // "arb"
chainId: number | string;
chainName: string; // "Arbitrum One"
tokenAddress: string | null;
crownOwner: string | null; // Token owner's wallet address
crownStatus: string; // "active", "at-risk", "inactive"
tokenId: number | null;
market: {
price: number | null;
volume24h: number | null;
marketCap: number | null;
holders: number | null;
totalSupply: number | null;
circulatingSupply: number | null;
liquidity?: number | null;
txns24h?: number | null;
priceChange1h?: number | null;
priceChange6h?: number | null;
priceChange24h?: number | null;
sparkline7d?: number[] | null;
};
socialLinks: {
website: string | null;
xdotcom: string | null;
telegram: string | null;
discord: string | null;
github: string | null;
whitepaper: string | null;
};
verifications: { ... }; // Per-platform verification records
metadata: Record<string, unknown>;
}The full type definition is src/widget-types.ts in a project scaffolded by chaindaddy app init widget. owner and status carry the same values as crownOwner and crownStatus.
Theme
Match the host page's styling using the theme object:
interface AppWidgetTheme {
mode: 'light' | 'dark';
accentColor: string;
backgroundColor: string;
cardBackground: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
borderColor: string;
}Actions
Send an action with onAction(). An action that answers resolves { ok: true, data } or { ok: false, error }, where error says why in words you can show:
// Navigate within the platform (token pages live at /{symbol}/{chain})
onAction({ type: 'navigate', path: '/doge/arbitrum' });
// A short notice on the page
onAction({ type: 'toast', message: 'Saved', variant: 'success' });
// A dialog with your own text; resolves with the button pressed
const { data } = await onAction({
type: 'modal',
title: 'Save these settings?',
buttons: [{ id: 'no', label: 'Not now' }, { id: 'yes', label: 'Save', style: 'primary' }],
});
// data.button is 'yes', 'no', or null if the visitor dismissed itThe others cover the platform API (api-call), sign-in, sales and payments (iap-purchase, pay), signatures, transactions, the Buy sheet, notifications, uploads and fullscreen. Actions is the full contract: every action's request, answer and refusals, the permission it needs, and whether the visitor must tap. An action your manifest has no permission for may be refused instead of answered, so wrap calls in try/catch.
Saving a user's data
Keep a user's data on your own server: declare net:connect, ask the page for a session token and send it with each request, and key the data by the wallet it names. See Session tokens. An app's frame has no IndexedDB, and anything it keeps in memory is gone on reload.
Responsive Design
Widgets receive a breakpoint prop based on their container width:
| Breakpoint | Description |
|---|---|
compact | Narrow container (sidebar, mobile) |
standard | Default width |
wide | Full-width container |
Configurable Settings
Define a configSchema in your manifest to let token owners customize your widget:
{
"configSchema": {
"type": "object",
"properties": {
"showVolume": {
"type": "boolean",
"description": "Show 24h trading volume",
"default": true
}
}
}
}Read them from the settings prop:
export function MyWidget({ appData, settings }: AppWidgetProps<MySettings>) {
return (
<div>
<p>Price: ${appData.market.price}</p>
{settings.showVolume && <p>Volume: ${appData.market.volume24h}</p>}
</div>
);
}Building your widget
chaindaddy app build compiles src/index.tsx to dist/bundle.js, and fails if the bundle breaks one of these rules. With your own bundler, follow them yourself and pass the file to chaindaddy app dev --bundle and chaindaddy app pack --bundle:
- One file, an ES module of at most 2,000,000 bytes. Code splitting produces chunks the page cannot resolve.
- A named export matching the manifest's
entryComponent, as a plain function component (notReact.memo,forwardRefor a default-export wrapper). - Only
react,react-domandreact/jsx-runtimeas bare imports. The page provides them. Bundle everything else. - No
eval(),new Function()ordocument.cookie: the sandbox rules.
The manifest's tier sets the size chaindaddy app scan allows: standard up to 250 KB by default (Optional fields). Check the result in the real sandbox with Test locally.
Branding on the buy embed
Pro+ and above take Chain Daddy branding off the buy button a token owner embeds on their own site and off their stream overlay. It is the same setting that removes the watermark from the coin page. On the script embed, data-hide-branding drops the "Powered by Chain Daddy" link it places beside the button:
<div data-chaindaddy-buy data-crown-id="DOGE-arbitrum" data-hide-branding></div>Custom embed branding
On the Developer plan, which is in beta and free while it is, or Partner, the token's owner or a manager can set the buy embed's colors, logo, call-to-action text and footer, with PUT /api/v2/payments/branding/{crownId}.
{
"primaryColor": "#FF6B00",
"logoUrl": "https://your-domain.com/logo.png",
"customCta": "Buy MYTOKEN",
"footerText": "Thanks for supporting us"
}