Calm authenticates Dynamic users via the JWT Dynamic mints on
sign-in. The JWT carries the user’s sub, verified_credentials
(including the linked EVM wallet), and the Dynamic environment id —
which is exactly what the SDK forwards to Calm.The only setup is making sure Dynamic is configured for EVM wallets
so an Ethereum address appears in verified_credentials. Register the
Ethereum connectors in your Dynamic config (see “Wrap App” below) —
no Dynamic dashboard toggle is required.You’ll also need your public Dynamic environment ID — the same
environmentId you pass to <DynamicContextProvider>, found in the
Dynamic dashboard. Add it when you generate your Calm publishable
key so the key is bound to your Dynamic
environment.
If no EVM connector is registered, the SDK can’t start a session — the
API can’t see an EVM wallet in the JWT claims and returns
wallet_not_linked.
The calmKey you pass to <DynamicCalmProvider> is a publishable key
minted in the Calm dashboard. A Dynamic key is bound to your Dynamic
environment ID, so Calm only accepts JWTs minted by that Dynamic
environment.
1
Open the Calm dashboard
Sign in to the Calm dashboard and open
Publishable keys.
2
Fill out "Generate a new key"
Environment — Sandbox to start, Live for production. Each
environment issues its own key (calm_public_sandbox_… /
calm_public_live_…).
Wallet provider — select Dynamic.
Dynamic environment ID — paste your Dynamic environment ID (from
the Dynamic dashboard). Required for Dynamic keys; it binds the key
to your Dynamic environment.
Allowed origin(optional) — the origin your app is served
from. Live keys require an https:// origin; in Sandbox you can
leave it blank to skip the Origin check (e.g. for
http://localhost).
Calm dashboard: Generate a new key, Dynamic selected
3
Generate and copy the key
Click Generate. The new key appears under Active keys — copy
it and pass it as calmKey. It looks like
calm_public_sandbox_<32 hex>.
Place <DynamicCalmProvider> inside <DynamicContextProvider> and
<QueryClientProvider>.
app/layout.tsx
"use client";import { DynamicContextProvider,} from "@dynamic-labs/sdk-react-core";import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { DynamicCalmProvider } from "@calm-xyz/react/dynamic";const queryClient = new QueryClient();export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body> <DynamicContextProvider settings={{ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!, walletConnectors: [EthereumWalletConnectors], }} > <QueryClientProvider client={queryClient}> {/* <DynamicCalmProvider> must be wrapped in <DynamicContextProvider> and <QueryClientProvider> — it reads the JWT from Dynamic and uses react-query under the hood. */} <DynamicCalmProvider calmKey={process.env.NEXT_PUBLIC_CALM_KEY!} currency="usd" rpcUrls={{ 1: "https://your-ethereum-rpc", 8453: "https://your-base-rpc", 999: "https://rpc.hyperliquid.xyz/evm", }} > {children} </DynamicCalmProvider> </QueryClientProvider> </DynamicContextProvider> </body> </html> );}
Mount <DynamicCalmProvider> only once primaryWallet is set — it reads
the wallet address at mount and requires it to be defined. Gate the
mount on useDynamicContext().primaryWallet?.address; mounting before
the wallet resolves throws.
The provider always renders its children. Wrap any trigger element in
<CalmOnramp> to open the deposit modal, and gate it on the Calm session
being ready via useSession — the trigger only
enables once the session is live, so the modal’s data loads immediately
on open. (A connected wallet alone isn’t enough; the session is what the
modal’s requests are authenticated with.)
Your publishable key — calm_public_(live|sandbox)_<32 hex>. Identifies
the Calm tenant. Sent to the API on every session creation via
the X-Calm-Publishable-Key header.
Per-chain RPC URLs (keyed by chain id) used to read transaction
receipts when confirming a swap. The receipt is read from the chain the
swap transaction lands on (e.g. Base 8453), so each source chain needs
a reliable RPC — reading through the wallet’s own endpoint can stall the
confirm step. Provide an entry for every chain you support. Pass a
stable (memoized) object.
The provider’s createSession throws on any non-2xx response. The
useSession hook surfaces the error in
result.error. See Errors for the full code table;
the most common from this shell:
error.code
Meaning
invalid_token
Dynamic JWT couldn’t be verified — token missing claims, signature invalid, or past exp.
wallet_not_linked
The wallet on the session URL isn’t an EVM entry in the Dynamic user’s verified_credentials. Usually means EthereumWalletConnectors isn’t registered in your Dynamic config so no EVM wallet rides inside the JWT.
invalid_publishable_key
calmKey is malformed, unknown, or revoked. Check your Calm dashboard.
publishable_key_missing
No X-Calm-Publishable-Key header reached the API — usually a bundler stripping the env var. Check process.env.NEXT_PUBLIC_CALM_KEY.
origin_not_allowed
Your page’s Origin isn’t on the publishable key’s allowlist. Live keys require HTTPS; add the origin in the dashboard.
refresh_invalid
The calm_refresh cookie is missing, expired, or bound to a different wallet (common after a Dynamic account switch). Call useSession({ address }).clear() and handleLogOut() before mounting for the new wallet.