Sign in with Chain Daddy
Not generally available yet
Sign in with Chain Daddy is being rolled out. Until it is switched on, every endpoint on this page answers 404.
Your site, game or app sends a visitor to Chain Daddy, they approve it on chaindaddy.io, and they come back with a signed statement that says which wallet they control. Your server can then read that wallet's items in your store (GET …/entitlements?wallet=, holder unlocks included) and recognise the same person it sees in your Crown App.
It is standard OpenID Connect: the authorization code flow with PKCE, so any stock library works (openid-client, Auth.js, go-oidc, Authlib, Spring Security, .NET). You get an ID token and nothing else. There are no refresh tokens and no access to the Chain Daddy API on the user's behalf: after sign-in you keep your own session, as with Sign in with Apple.
Endpoints
| Issuer | https://api.chaindaddy.io |
| Discovery | https://api.chaindaddy.io/.well-known/openid-configuration |
| Authorize (the consent page) | https://chaindaddy.io/_oauth/authorize |
| Token | https://api.chaindaddy.io/api/v2/oauth/token |
| Userinfo | https://api.chaindaddy.io/api/v2/oauth/userinfo |
| Keys (JWKS) | https://api.chaindaddy.io/api/v2/oauth/jwks |
Use discovery rather than hard-coding the rest.
Register your site
Registering needs a Developer Beta Program membership and is free. Call it with your signed-in session or a cd_live_ key:
curl -X POST https://api.chaindaddy.io/api/v2/developer/oauth-clients \
-H "Authorization: Bearer $CD_LIVE_KEY" -H "Content-Type: application/json" \
-d '{"name":"FLUX GP","redirectUris":["https://fluxgp.io/auth/callback"],"confidential":true,
"crown":{"chainKey":"base","crownId":8}}'
# → {"id":"cdc_…","name":"FLUX GP","confidential":true,"clientSecret":"cd_oauth_…",…}redirectUris: 1 to 10, matched exactly (no wildcards, no fragments).httpsonly, excepthttp://localhostandhttp://127.0.0.1for development.confidential:truefor a server that can keep a secret. Thecd_oauth_…secret is shown once. Leave itfalsefor a single-page app, a game or a native app: those use PKCE with no secret.name: up to 40 characters. It may not contain "Chain Daddy", "official", "support" or "wallet".crown(optional): your token page. It earns the consent screen's verified mark when you hold or manage that crown AND the redirect URI's host is one of the crown's DNS-verified domains (or a subdomain of one). Without it the screen says "Unverified site".
List, edit and delete with GET, PATCH and DELETE on /api/v2/developer/oauth-clients[/{id}]. Deleting turns the client off at once.
Scopes and claims
| Scope | Adds |
|---|---|
openid (required) | sub, chain, and the standard claims below |
wallets | wallets: every wallet linked to the person's Chain Daddy account, sub first |
profile | identity: the handle they chose to publish (handle, platform, verified, displayName) |
The consent screen shows each scope on its own line. The ID token (ES256, five minutes) carries:
| Claim | |
|---|---|
iss | https://api.chaindaddy.io |
aud | your client id |
typ | always signin+v1. Check it. |
sub | the wallet they signed in with: lowercase hex on EVM, base58 on Solana |
chain | evm or solana |
nonce | the nonce you sent |
iat, exp, auth_time, jti | standard |
There is no email and no balance in the token. A balance goes stale in minutes; check holdings and items live on your server through your store's entitlements, where a holder unlock is the gate.
Quickstart: Node with openid-client
import * as client from 'openid-client';
const config = await client.discovery(
new URL('https://api.chaindaddy.io'),
process.env.CD_CLIENT_ID!,
{ id_token_signed_response_alg: 'ES256' },
client.ClientSecretPost(process.env.CD_CLIENT_SECRET!), // client.None() for a public client
);
// Sign-in: remember verifier, state and nonce in the visitor's session.
const verifier = client.randomPKCECodeVerifier();
const state = client.randomState();
const nonce = client.randomNonce();
const url = client.buildAuthorizationUrl(config, {
redirect_uri: 'https://fluxgp.io/auth/callback',
scope: 'openid wallets',
code_challenge: await client.calculatePKCECodeChallenge(verifier),
code_challenge_method: 'S256',
state,
nonce,
});
// redirect the browser to url
// Callback:
const tokens = await client.authorizationCodeGrant(config, new URL(req.url, 'https://fluxgp.io'), {
pkceCodeVerifier: verifier,
expectedState: state,
expectedNonce: nonce,
});
const claims = tokens.claims()!;
if (claims.typ !== 'signin+v1') throw new Error('not a Chain Daddy sign-in');
const wallet = claims.sub; // start your own session for this walletQuickstart: Auth.js
providers: [{
id: 'chaindaddy',
name: 'Chain Daddy',
type: 'oidc',
issuer: 'https://api.chaindaddy.io',
clientId: process.env.CD_CLIENT_ID,
clientSecret: process.env.CD_CLIENT_SECRET,
authorization: { params: { scope: 'openid wallets' } },
checks: ['pkce', 'state', 'nonce'],
client: { id_token_signed_response_alg: 'ES256' },
profile(p) {
if (p.typ !== 'signin+v1') throw new Error('not a Chain Daddy sign-in');
return { id: p.sub, name: p.identity?.handle ?? p.sub };
},
}]Verifying an ID token yourself
If you receive the ID token outside a library (from your SPA, say), verify it on your server:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://api.chaindaddy.io/api/v2/oauth/jwks'));
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: 'https://api.chaindaddy.io',
audience: process.env.CD_CLIENT_ID,
algorithms: ['ES256'],
});
if (payload.typ !== 'signin+v1') throw new Error('not a Chain Daddy sign-in');
if (payload.nonce !== expectedNonce) throw new Error('replayed sign-in');Crown Apps don't use this
Inside your Crown App, keep using the host's session-token action: an app-session+v1 token verified against /api/v2/apps/jwks. If your site and your Crown App share a server, verify both kinds and key players by sub, which is the same wallet string in both. The two token kinds use different keys and different typ values, so neither ever passes for the other.
Rules the flow enforces
- PKCE (
S256) andstateare required on every request. The response carriesiss(RFC 9207); check it. - A
redirect_urithat is not registered exactly is refused on chaindaddy.io with an error. The visitor is never sent anywhere. - A code lives 60 seconds and redeems once. A code presented with the wrong client, redirect URI or verifier is burned.
- ID tokens and access tokens last five minutes. The access token opens
userinfoand nothing else. - A visitor who has approved your site before signs in again without another click, until they disconnect it under Settings → Connected sites.
- Suspending a developer's program access turns off every one of their clients.