JavaScript SDK
@chaindaddy/js puts Chain Daddy on any website. Today it does Sign in with Chain Daddy: a button, a popup, and a callback page. Every approval happens on chaindaddy.io, in the popup; your page only receives the result.
From a script tag
<script src="https://js.chaindaddy.io/v1/cd.js"></script>It defines window.ChainDaddy and the <cd-signin-button> element.
| URL | Caching | Use |
|---|---|---|
https://js.chaindaddy.io/v1/cd.js | 5 minutes | the current 1.x build: fixes reach your site without a change |
https://js.chaindaddy.io/v<version>/cd.js | forever | one exact version, for Subresource Integrity |
https://js.chaindaddy.io/v<version>/cd.js.sri | forever | its sha384-… integrity value |
https://js.chaindaddy.io/v1/cd.json | 5 minutes | {"version","integrity"} of the current build, to print a pinned tag |
To pin a version with Subresource Integrity, read the .sri file next to it and add both attributes:
<script src="https://js.chaindaddy.io/v0.1.0/cd.js"
integrity="sha384-…" crossorigin="anonymous"></script>Every file is served with Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin, so it loads under SRI and on pages that send COEP.
From npm
npm install @chaindaddy/jsimport { signIn, handleCallback, exchangeCode } from '@chaindaddy/js/signin';
import { defineSignInButton } from '@chaindaddy/js';The package is ESM with type declarations, and safe to import where there is no DOM (a server render). Each release is published with npm provenance from gitlab.com/opencrown/chaindaddy-js.
The API
signIn(options) opens the Chain Daddy popup and resolves with { code, codeVerifier, state, nonce, issuer, clientId, redirectUri } once the person approves. Call it from a click handler, so the browser lets the popup open. If the popup is blocked, the page goes to Chain Daddy instead and signIn resolves null; your callback page then finishes.
| Option | |
|---|---|
clientId | your client id: the URL of your client.json, or a registered cdc_ id |
redirectUri | your callback page, exactly as your client lists it, on the same origin as the page with the button |
scope | default openid; add wallets, profile |
claims | an OpenID Connect claims request (holdings of your coin, crowns), as an object |
signal | an AbortSignal to stop waiting. Closing the popup does not cancel: a browser can report a popup closed when it is not |
handleCallback() runs on your callback page. In the popup it passes the answer back to your page and closes the popup. After a redirect it returns the same result signIn would have.
exchangeCode(result) redeems the code at the token endpoint from the browser (a public client: no secret) and returns { id_token, … }.
Your server must verify the ID token before it trusts anything in it: ES256, keys at https://api.chaindaddy.io/api/v2/oauth/jwks, and check iss, aud (your client id), typ (signin+v1), exp and the nonce. See Verifying an ID token yourself. A token checked only in the browser proves nothing.
API types
The package also carries TypeScript types for the public API contract: every stable operation, with its parameters and responses. They are types only, so they add nothing to your bundle. Call the API with fetch.
import type { paths, components } from '@chaindaddy/js/api';
type Order = components['schemas']['IapOrder'];
type Sites = paths['/api/v2/public/crowns/{chain}/{crownId}/sites']['get']['responses'][200]['content']['application/json'];The button
<cd-signin-button
client-id="https://you.example/client.json"
redirect-uri="https://you.example/callback"
scope="openid wallets"
theme="light"
text="signin"></cd-signin-button>
<script>
document.querySelector('cd-signin-button').addEventListener('cd-signin', async (e) => {
const tokens = await ChainDaddy.exchangeCode(e.detail);
await fetch('/session', { method: 'POST', body: tokens.id_token }); // your server verifies it
});
</script>It fires cd-signin with the result as detail, or cd-signin-error with { message } (access_denied when the person cancels). theme is light or dark; text is signin ("Sign in with Chain Daddy") or continue ("Continue with Chain Daddy"). The text and the crown are fixed, and the button is at least 40 pixels tall: please don't restyle it into something that reads differently.
The callback page
<script src="https://js.chaindaddy.io/v1/cd.js"></script>
<script>
ChainDaddy.handleCallback().then(async (result) => {
if (!result) return; // the popup handed it back and closed
const tokens = await ChainDaddy.exchangeCode(result); // after a redirect: finish here
await fetch('/session', { method: 'POST', body: tokens.id_token });
location.replace('/');
});
</script>A coin's apps on its own site
The same script defines three elements. Put a link inside each one: it shows until the app is ready, and stays if the app can't load, so the page never has a hole in it.
<script async src="https://js.chaindaddy.io/v1/cd.js"></script>
<!-- One of the coin's apps -->
<cd-app crown="123" chain="base" app="slug"><a href="https://chaindaddy.io/SYM/base">$SYM on Chain Daddy</a></cd-app>
<!-- The coin's card: name, price, market cap -->
<cd-coin crown="123" chain="base"><a href="https://chaindaddy.io/SYM/base">$SYM on Chain Daddy</a></cd-coin>
<!-- A button that opens the coin's page in a small window -->
<cd-buy><a href="https://chaindaddy.io/SYM/base">Buy $SYM</a></cd-buy>| Attribute | On | Meaning |
|---|---|---|
crown | cd-app, cd-coin | The coin's id on Chain Daddy |
chain | cd-app, cd-coin | Its chain: a key (base) or a CAIP-2 id (eip155:8453) |
app | cd-app | The app's slug. It must be installed on the coin and allowed on other sites |
mode | cd-app, cd-coin | light or dark. Left out, it follows the visitor's system setting |
- Where it works.
cd-coin,cd-buyand read-only apps work on any https site. An app that uses a wallet works only on the coin's verified sites; anywhere else the link stays. - Events. Each element fires
cd:readywhen its app is showing andcd:unavailablewhen it falls back to the link. Nothing else comes back to your page: no wallet address, no sign-in and no result. - Colours. Set
--cd-accent,--cd-surface,--cd-text,--cd-text-mutedand--cd-borderon the element or any parent. An app takes the ones its developer chose to take;cd-buyuses them all. - Size. The element is a block as wide as its container. An app is as tall as its tile on a coin page (from its default size), or the minimum height it declares if that is more; the coin's card is as tall as its content.
- Content Security Policy. If your site sends one, allow
script-src https://js.chaindaddy.ioandframe-src https://embed.chaindaddy.io. - From npm.
import { defineEmbedElements } from '@chaindaddy/js/embed', then calldefineEmbedElements()once in the browser.
Check a store payment on your server
@chaindaddy/js/verify is the one part of the package made for a server, not a page. It checks on chain that a store order was paid, without asking Chain Daddy: it reads the checkout contract through a JSON-RPC endpoint you choose. It is not in the script-tag build.
import { verifyPayment, VerifyError, NATIVE_TOKEN } from '@chaindaddy/js/verify';
const result = await verifyPayment(order.verification, {
rpcUrl: process.env.RPC_URL,
expect: { orderId, chainId: 'eip155:8453', token: COIN_ADDRESS, payee: PAYOUT_ADDRESS, amount: 975000n },
});
// { paid, payer, depth, checkout, blockNumber, orderHash, chainId }| Option | |
|---|---|
expect | Required. What you sold, from your own records: orderId, chainId, token (NATIVE_TOKEN for the chain's own coin), payee (your payout address) and amount (the least it must receive; a bigint or a decimal string) |
rpcUrl | Your JSON-RPC endpoint for the order's chain |
request | In place of rpcUrl: a function shaped like ({ method, params }) => result, such as a viem client's request |
fetch | Used with rpcUrl in place of the global fetch |
confirmations | Wait deeper. The helper already reads at the deeper of what the order asks and what our own server waits on that chain (MIN_CONFIRMATIONS); this can only raise it |
signal | An AbortSignal for the requests made through rpcUrl |
It resolves paid: true only when the contract says that exact order was paid, depth blocks deep, and paid: false when it was not (yet). Anything it cannot vouch for is a VerifyError with a code: a block that is not what expect says, a checkout that is not one of ours on that chain, an RPC endpoint on another chain or one that fails. It checks EVM orders; for a Solana order it throws unsupported. Verify a payment has a full example, every code, and the same check by hand.