> ## Documentation Index
> Fetch the complete documentation index at: https://docs.calmtreasury.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic provider

> Add `<DynamicCalmProvider>` to a Dynamic app.

## 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`. 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](#generating-the-publishable-key) so the key is bound to your Dynamic
environment.

<Warning>
  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`.
</Warning>

## Generating the publishable key

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.

<Steps>
  <Step title="Open the Calm dashboard">
    Sign in to the [Calm dashboard](https://dashboard.calmtreasury.xyz) and open
    **Publishable keys**.
  </Step>

  <Step title="Fill out &#x22;Generate a new key&#x22;">
    * **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`).

    <Frame caption="Calm dashboard: Generate a new key, Dynamic selected">
      <img src="https://mintcdn.com/calm/6g_rB5dy463sESxc/images/dashboard-generate-dynamic-key.webp?fit=max&auto=format&n=6g_rB5dy463sESxc&q=85&s=154b509f9e0fec7971d85b23da6f9e1b" alt="Calm dashboard generate-key form with the Dynamic wallet provider and a Dynamic environment ID" width="1392" height="700" data-path="images/dashboard-generate-dynamic-key.webp" />
    </Frame>
  </Step>

  <Step title="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>`.

    <Frame caption="Calm dashboard: the new key under Active keys">
      <img src="https://mintcdn.com/calm/6g_rB5dy463sESxc/images/dashboard-active-keys.webp?fit=max&auto=format&n=6g_rB5dy463sESxc&q=85&s=b9df667ddddeb8b22af37123331c0ed7" alt="A generated publishable key listed under Active keys with a copy button" width="1392" height="396" data-path="images/dashboard-active-keys.webp" />
    </Frame>
  </Step>
</Steps>

## Installation

To add Calm to your project, install the required packages.

<CodeGroup>
  ```sh bun theme={null}
  bun add @calm-xyz/react @dynamic-labs/sdk-react-core@^4 @dynamic-labs/ethereum@^4 @tanstack/react-query@^5
  ```

  ```sh npm theme={null}
  npm install @calm-xyz/react @dynamic-labs/sdk-react-core@^4 @dynamic-labs/ethereum@^4 @tanstack/react-query@^5
  ```

  ```sh pnpm theme={null}
  pnpm add @calm-xyz/react @dynamic-labs/sdk-react-core@^4 @dynamic-labs/ethereum@^4 @tanstack/react-query@^5
  ```

  ```sh yarn theme={null}
  yarn add @calm-xyz/react @dynamic-labs/sdk-react-core@^4 @dynamic-labs/ethereum@^4 @tanstack/react-query@^5
  ```
</CodeGroup>

* [Dynamic](https://dynamic.xyz) 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](https://tanstack.com/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`):

```tsx theme={null}
import "@calm-xyz/react/styles.css";
```

or `@import` it from your own CSS file:

```css theme={null}
@import "@calm-xyz/react/styles.css";
```

### Wrap App in `<DynamicCalmProvider>`

Place `<DynamicCalmProvider>` inside `<DynamicContextProvider>` and
`<QueryClientProvider>`.

```tsx app/layout.tsx theme={null}
"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>
  );
}
```

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

### Open the onramp

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`](/sdk/hooks/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.)

<CodeGroup>
  ```tsx app/page.tsx theme={null}
  "use client";
  import { CalmOnramp, useCalm, useSession } from "@calm-xyz/react";

  function DepositButton() {
    const { address } = useCalm();
    const session = useSession({ address });
    const ready = !!session.data && !session.isError;
    if (!ready) return <button type="button" disabled>Loading…</button>;
    return (
      <CalmOnramp>
        <button type="button">Deposit funds</button>
      </CalmOnramp>
    );
  }

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

  ```tsx app/layout.tsx theme={null}
  "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
                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>
    );
  }
  ```
</CodeGroup>

## Props

<ParamField path="calmKey" type="string" required>
  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.
</ParamField>

<ParamField path="currency" type="&#x22;usd&#x22; | &#x22;gbp&#x22; | &#x22;eur&#x22;" required>
  Source fiat currency for the bank-deposit onramp.
</ParamField>

<ParamField path="chain" type="number" default="1337">
  Destination chain id for the delivered USDC. Defaults to HyperCore
  (`1337`).
</ParamField>

<ParamField path="apiUrl" type="string" default="https://api.calmtreasury.xyz">
  Override the Calm API root. Use
  `https://api.sandbox.calmtreasury.xyz` for the sandbox environment.
</ParamField>

<ParamField path="rpcUrls" type="Partial<Record<number, string>>" required>
  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.
</ParamField>

## Errors

The provider's `createSession` throws on any non-2xx response. The
[`useSession`](/sdk/hooks/useSession) hook surfaces the error in
`result.error`. See [Errors](/sdk/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()`](/sdk/hooks/useSession#clear) and `handleLogOut()` before mounting for the new wallet. |
