Skip to main content

Prerequisites

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

Installation

To add Calm to your project, install the required packages.
bun add @calm-xyz/react @dynamic-labs/sdk-react-core@^4 @dynamic-labs/ethereum@^4 @tanstack/react-query@^5
  • Dynamic is the identity provider the SDK reads the user, wallet, and JWT from.
  • @dynamic-labs/ethereum registers the EVM connectors so the user’s wallet rides inside the JWT’s verified_credentials.
  • TanStack Query is an async state manager that handles requests, caching, and more.

Import the stylesheet

Import the Calm stylesheet once at your app root (Next.js layout.tsx, React main.tsx):
import "@calm-xyz/react/styles.css";
or @import it from your own CSS file:
@import "@calm-xyz/react/styles.css";

Wrap App in <DynamicCalmProvider>

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"
            >
              {children}
            </DynamicCalmProvider>
          </QueryClientProvider>
        </DynamicContextProvider>
      </body>
    </html>
  );
}

Open the onramp

The provider always renders its children. Wrap any trigger element in <CalmOnramp> to open the deposit modal, and use useReady() to gate the trigger on whether the Calm context is live (i.e. Dynamic reports a signed-in user with an EVM wallet).
"use client";
import { CalmOnramp } from "@calm-xyz/react";
import { useReady } from "@calm-xyz/react/dynamic";

function DepositButton() {
  const ready = useReady();
  if (!ready) return <button type="button" disabled>Loading…</button>;
  return (
    <CalmOnramp>
      <button type="button">Deposit funds</button>
    </CalmOnramp>
  );
}

export default function Page() {
  return <DepositButton />;
}

Props

calmKey
string
required
Your publishable key — calm_public_(live|dev)_<32 hex>. Identifies the Calm tenant. Sent to the API on every session creation via the X-Calm-Publishable-Key header.
currency
"usd" | "gbp" | "eur"
required
Source fiat currency for the bank-deposit onramp.
initialChain
1 | 8453 | 42161 | 999
default:"999"
Initial destination chain for auto-converted USDC. Optional — defaults to 999 (HyperEVM). Once mounted the SDK owns the chain; switch it at runtime via useCalm().setChain(...).
The destination chain defaults to HyperEVM (999) with mode set to hypercore. It’s independent of the Dynamic wallet’s current chain — to deliver USDC elsewhere, override initialChain, or call useCalm().setChain(...) at runtime (and setMode(...) when staying on chain 999).
initialMode
"hyperevm" | "hypercore"
Initial Hyperliquid execution layer. Optional — defaults to "hypercore" when initialChain is 999. Silently ignored on every other chain because mode only carries meaning on Hyperliquid (the SDK exposes mode: null off 999). Switch at runtime via useCalm().setMode(...), which throws when called with chain !== 999.
baseUrl
string
default:"https://api.calmtreasury.xyz"
Override the Calm API root. Use https://api.sandbox.calmtreasury.xyz for the sandbox environment.

useReady

import { useReady } from "@calm-xyz/react/dynamic";

const ready = useReady();
Returns boolean. false until Dynamic is ready, the user is authenticated, the JWT is issued, and an EVM wallet is connected; true once <DynamicCalmProvider> has a configured Calm context. Safe to call before any provider mounts — defaults to false. Use it to render your own loading UI in place of the silent null the provider used to return.

Errors

The provider’s createSession throws on any non-2xx response. The useSession hook surfaces the error in result.error. Common codes:
error.codeMeaning
invalid_tokenDynamic JWT couldn’t be verified — token missing claims, signature invalid, or past exp.
wallet_not_linkedThe 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_keycalmKey is malformed, unknown, or revoked. Check your Calm dashboard.
publishable_key_missingNo X-Calm-Publishable-Key header reached the API — usually a bundler stripping the env var. Check process.env.NEXT_PUBLIC_CALM_KEY.
origin_not_allowedYour page’s Origin isn’t on the publishable key’s allowlist. Live keys require HTTPS; add the origin in the dashboard.
refresh_wallet_mismatchThe calm_refresh cookie is bound to a different wallet than the one signing in now (common after a Dynamic account switch). Call useCalm().logout() before mounting the provider for the new wallet.