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

# Privy provider

> Add `<PrivyCalmProvider>` to a Privy app.

## Prerequisites

Calm authenticates Privy users via Privy's **identity tokens** —
short-lived JWTs that carry the user's `sub` and `linked_accounts`
(including the wallet and email). They are not the same as Privy
access tokens. You only need to flip this once per Privy app.

<Steps>
  <Step title="Open the Privy dashboard">
    Sign in at [dashboard.privy.io](https://dashboard.privy.io) and
    pick your app.
  </Step>

  <Step title="Navigate to User Management → Authentication → Advanced">
    <Frame caption="Privy dashboard: User management → Authentication, Advanced tab">
      <img src="https://mintcdn.com/calm/gAx8OVmNgMIw2jlr/images/privy-authentication-page.webp?fit=max&auto=format&n=gAx8OVmNgMIw2jlr&q=85&s=34428cb160dc3d9697e05317513a804c" alt="Privy User Authentication settings panel, Advanced tab selected" width="2484" height="900" data-path="images/privy-authentication-page.webp" />
    </Frame>
  </Step>

  <Step title="Toggle &#x22;Return user data in an identity token&#x22; ON">
    Scroll the Advanced tab until you see it. Once enabled, the user's
    `linked_accounts` (including the linked wallet) ride inside the
    identity token — which is exactly what the SDK forwards to Calm.

    <Frame caption="&#x22;Return user data in an identity token&#x22; toggle, enabled">
      <img src="https://mintcdn.com/calm/gAx8OVmNgMIw2jlr/images/privy-activate-identity-tokens.webp?fit=max&auto=format&n=gAx8OVmNgMIw2jlr&q=85&s=99bfc18d82df9dd1c7f90817afef1344" alt="Return user data in an identity token toggle, enabled" width="1654" height="368" data-path="images/privy-activate-identity-tokens.webp" />
    </Frame>
  </Step>
</Steps>

<Warning>
  If this toggle is off, the SDK can't start a session — the API can't
  see the user's wallet in the JWT claims and returns
  `wallet_not_linked`.
</Warning>

## Generating the publishable key

The `calmKey` you pass to `<PrivyCalmProvider>` is a **publishable key**
minted in the Calm dashboard. A Privy key is bound to your Privy **app
ID**, so Calm only accepts identity tokens minted by that Privy app.

<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 **Privy**.
    * **Privy app ID** — paste your Privy app ID (from the Privy
      dashboard). Required for Privy keys; it binds the key to your Privy
      app.
    * **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, Privy selected">
      <img src="https://mintcdn.com/calm/6g_rB5dy463sESxc/images/dashboard-generate-privy-key.webp?fit=max&auto=format&n=6g_rB5dy463sESxc&q=85&s=7d2dcb6ed7b9dce7f70f4b0007322e86" alt="Calm dashboard generate-key form with the Privy wallet provider and a Privy app ID" width="1392" height="700" data-path="images/dashboard-generate-privy-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 @privy-io/react-auth@^2.25 @tanstack/react-query@^5
  ```

  ```sh npm theme={null}
  npm install @calm-xyz/react @privy-io/react-auth@^2.25 @tanstack/react-query@^5
  ```

  ```sh pnpm theme={null}
  pnpm add @calm-xyz/react @privy-io/react-auth@^2.25 @tanstack/react-query@^5
  ```

  ```sh yarn theme={null}
  yarn add @calm-xyz/react @privy-io/react-auth@^2.25 @tanstack/react-query@^5
  ```
</CodeGroup>

* [Privy](https://privy.io) is the identity provider the SDK reads the user, wallet, and identity token from.
* [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 `<PrivyCalmProvider>`

Place `<PrivyCalmProvider>` inside `<PrivyProvider>` and
`<QueryClientProvider>`.

```tsx app/layout.tsx theme={null}
"use client";
import { PrivyProvider } from "@privy-io/react-auth";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyCalmProvider } from "@calm-xyz/react/privy";

const queryClient = new QueryClient();

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}>
          <QueryClientProvider client={queryClient}>
            {/* <PrivyCalmProvider> must be wrapped in <PrivyProvider>
                and <QueryClientProvider> — it reads the identity token
                from Privy and uses react-query under the hood. */}
            <PrivyCalmProvider
              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}
            </PrivyCalmProvider>
          </QueryClientProvider>
        </PrivyProvider>
      </body>
    </html>
  );
}
```

<Warning>
  Mount `<PrivyCalmProvider>` only once the signed-in user has a connected
  wallet — it reads the wallet address at mount and requires it to be
  **defined**. For email logins Privy provisions the embedded wallet as a
  separate step after authentication, so `authenticated` can be true while
  the wallet address is still `undefined`; mounting in that window throws.
  Gate the mount on `usePrivy().user?.wallet?.address`.
</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 { PrivyProvider } from "@privy-io/react-auth";
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
  import { PrivyCalmProvider } from "@calm-xyz/react/privy";

  const queryClient = new QueryClient();

  export default function RootLayout({
    children,
  }: {
    children: React.ReactNode;
  }) {
    return (
      <html lang="en">
        <body>
          <PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}>
            <QueryClientProvider client={queryClient}>
              <PrivyCalmProvider
                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}
              </PrivyCalmProvider>
            </QueryClientProvider>
          </PrivyProvider>
        </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`           | Privy identity token couldn't be verified — token missing claims, signature invalid, or past `exp`. Check the Privy dashboard toggle above.                                                                                                              |
| `wallet_not_linked`       | The wallet on the session URL isn't in the Privy user's linked accounts. Usually means the identity-token toggle is off and the wallet didn't ride 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 Privy account switch). Call [`useSession({ address }).clear()`](/sdk/hooks/useSession#clear) and `usePrivy().logout()` before mounting for the new wallet. |
