Skip to content

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.
  • localStorage is scoped to your app.

Widget Props ​

Every widget receives AppWidgetProps:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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 it

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

BreakpointDescription
compactNarrow container (sidebar, mobile)
standardDefault width
wideFull-width container

Configurable Settings ​

Define a configSchema in your manifest to let token owners customize your widget:

json
{
  "configSchema": {
    "type": "object",
    "properties": {
      "showVolume": {
        "type": "boolean",
        "description": "Show 24h trading volume",
        "default": true
      }
    }
  }
}

Read them from the settings prop:

tsx
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:

  1. One file, an ES module of at most 2,000,000 bytes. Code splitting produces chunks the page cannot resolve.
  2. A named export matching the manifest's entryComponent, as a plain function component (not React.memo, forwardRef or a default-export wrapper).
  3. Only react, react-dom and react/jsx-runtime as bare imports. The page provides them. Bundle everything else.
  4. No eval(), new Function() or document.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:

html
<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}.

json
{
  "primaryColor": "#FF6B00",
  "logoUrl": "https://your-domain.com/logo.png",
  "customCta": "Buy MYTOKEN",
  "footerText": "Thanks for supporting us"
}