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

# Crypto to fiat onramp and offramp with Iron and Dynamic

> Learn how to integrate Iron Finance with Dynamic's JS SDK embedded wallets to let users onramp fiat to crypto and offramp stablecoins to fiat via SEPA

## What We're Building

A Next.js app that pairs Dynamic's embedded wallets with Iron Finance's payment rails so a user can:

* Complete one-time KYC, register their embedded wallet, and add a SEPA bank account
* **Onramp**: get a live EUR → USDC quote, execute it, and receive a virtual IBAN (vIBAN) to send a SEPA transfer to. USDC is delivered to the embedded wallet automatically.
* **Offramp**: get a live USDC → EUR quote, execute it, and receive a deposit address. Sending USDC to it triggers a SEPA payout to the registered IBAN.
* View prior onramps and offramps with live status

## How It Works

Iron exposes a **quote + execute** model around its `/api/autoramps` endpoint:

1. **Quote** (`GET /api/autoramps/quote`) returns a signed, time-limited rate for the requested direction, amount, and rails.
2. **Execute** (`POST /api/autoramps`) creates an autoramp from that quote. The response includes `deposit_rails` — a vIBAN (for onramps) or a deposit crypto address (for offramps) the user sends funds to.
3. When the deposit arrives, Iron converts and settles automatically to the destination registered on the autoramp (the user's embedded wallet for onramps, their IBAN for offramps).

Before a user can ramp, they complete a six-step onboarding:

1. Create an Iron customer
2. Complete KYC identity verification (hosted link)
3. Sign any required compliance documents
4. Register the Dynamic embedded wallet (signed proof of ownership — Travel Rule)
5. Add a SEPA bank account
6. Done

**3. Deposits trigger automatic conversion** — Each autoramp address/vIBAN is persistent and reusable. Iron handles the conversion and settlement automatically on every deposit.

All Iron API calls are proxied through your own Next.js API routes. Iron credentials never touch the client.

## Building the Application

### Project setup

Scaffold a Next.js app and follow the [JavaScript quickstart](/javascript/reference/quickstart). This guide uses the headless `@dynamic-labs-sdk/client` (not the React SDK) with the EVM extension.

<Info>
  **Dashboard:** Under **Chains & Networks**, enable **Ethereum** and the stablecoin networks you use with Iron (for example Ethereum mainnet for USDC). Under **Sign-in Methods**, enable **Email OTP** and **Google** under **Social Sign-in**. Under **Security → Allowed Origins**, add the origin where the app runs (for example `http://localhost:3000`).
</Info>

### Install Dependencies

<CodeGroup>
  ```bash npm theme={"system"}
  npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @tanstack/react-query @radix-ui/react-select lucide-react
  ```

  ```bash yarn theme={"system"}
  yarn add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @tanstack/react-query @radix-ui/react-select lucide-react
  ```

  ```bash pnpm theme={"system"}
  pnpm add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @tanstack/react-query @radix-ui/react-select lucide-react
  ```

  ```bash bun theme={"system"}
  bun add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @tanstack/react-query @radix-ui/react-select lucide-react
  ```
</CodeGroup>

### Configure Environment Variables

```env .env.local theme={"system"}
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-dynamic-environment-id
IRON_API_KEY=your-iron-api-key
IRON_ENVIRONMENT=sandbox
NEXT_PUBLIC_IRON_ENVIRONMENT=sandbox
```

You can find your Dynamic Environment ID in the [Dynamic dashboard](https://app.dynamic.xyz) under **Developer Settings → SDK & API Keys**.

Contact Iron to obtain your `IRON_API_KEY`. Set `IRON_ENVIRONMENT=sandbox` while testing — in sandbox mode, KYC approvals can be simulated without real documents.

### Initialize Dynamic

Create the Dynamic client with the EVM extension at `src/lib/dynamic.ts`:

```typescript src/lib/dynamic.ts theme={"system"}
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

export const dynamicClient = createDynamicClient({
  environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
  metadata: { name: "Iron Finance Ramp" },
});

addEvmExtension();
```

Extensions must be registered immediately after `createDynamicClient()`. The client auto-initializes on import — no explicit `initializeClient()` call is needed.

### Configure Wallet Context (Providers)

Create a `WalletContext` that tracks the current EVM account and exposes auth/wallet helpers to all components:

```typescript src/lib/providers.tsx theme={"system"}
"use client";

import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
import {
  getWalletAccounts, onEvent, isSignedIn, logout,
  detectOAuthRedirect, completeSocialAuthentication,
} from "@dynamic-labs-sdk/client";
import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { dynamicClient } from "./dynamic";

interface WalletContextValue {
  evmAccount: EvmWalletAccount | null;
  loggedIn: boolean;
  ensureEvmWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  evmAccount: null,
  loggedIn: false,
  ensureEvmWallet: async () => {},
  disconnect: async () => {},
});

export function useWallet() { return useContext(WalletContext); }

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false } },
});

export default function Providers({ children }: { children: ReactNode }) {
  const [evmAccount, setEvmAccount] = useState<EvmWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

  const refresh = useCallback(() => {
    const accounts = getWalletAccounts(dynamicClient);
    setEvmAccount(accounts.find(isEvmWalletAccount) ?? null);
    setLoggedIn(isSignedIn(dynamicClient));
  }, []);

  const disconnect = useCallback(async () => {
    await logout(dynamicClient);
    setEvmAccount(null);
    setLoggedIn(false);
  }, []);

  const ensureEvmWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isEvmWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["EVM"] }, dynamicClient);
      }
    } catch {}
    refresh();
  }, [refresh]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        if (await detectOAuthRedirect({ url }, dynamicClient)) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
          return;
        }
      } catch {}
      refresh();
    };
    handleOAuthRedirect();
    const unsub1 = onEvent({ event: "walletAccountsChanged", listener: () => ensureEvmWallet() }, dynamicClient);
    const unsub2 = onEvent({ event: "logout", listener: () => { setEvmAccount(null); setLoggedIn(false); } }, dynamicClient);
    return () => { unsub1(); unsub2(); };
  }, [refresh, ensureEvmWallet]);

  return (
    <WalletContext.Provider value={{ evmAccount, loggedIn, ensureEvmWallet, disconnect }}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WalletContext.Provider>
  );
}
```

### Create Server-Side Iron API Routes

All calls to Iron are made from Next.js API routes. This keeps your Iron credentials server-side only.

For example, `src/app/api/iron/customers/route.ts`:

```typescript src/app/api/iron/customers/route.ts theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import { ironClient } from "@/lib/services/iron";

export async function POST(req: NextRequest) {
  const body = await req.json();
  const customer = await ironClient.createCustomer(body);
  return NextResponse.json({ data: customer }, { status: 201 });
}
```

Create similar route files for:

* `/api/iron/customers/[id]/wallets` — register a crypto wallet with an Iron customer (required for Travel Rule compliance)
* `/api/iron/customers/[id]/banks` — add a SEPA bank account (fiat address)
* `/api/iron/autoramps` — create an onramp or offramp autoramp
* `/api/iron/fiatcurrencies` — list supported fiat currencies

For example, `src/app/api/iron/customers/[id]/banks/route.ts`:

```typescript src/app/api/iron/customers/[id]/banks/route.ts theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import { ironFetch } from "@/lib/services/iron";

export async function POST(req: NextRequest) {
  const b = await req.json();
  const [given, ...rest] = b.account_holder_name.trim().split(/\s+/);
  const fiatAddress = await ironFetch<{ id: string }>("/api/addresses/fiat", {
    method: "POST",
    idempotent: true,
    body: JSON.stringify({
      customer_id: b.customer_id,
      currency: { code: b.currency },
      bank_details: {
        recipient: {
          type: "Individual",
          given_name: given,
          family_name: rest.join(" ") || given,
        },
        provider_name: b.bank_name,
        provider_country: { code: b.bank_country },
        account_identifier: { type: "SEPA", iban: b.iban },
        address: {
          street: b.street,
          city: b.city,
          state: b.state,
          country: { code: b.country },
          postal_code: b.postal_code,
        },
        is_third_party: false,
      },
      label: b.label,
    }),
  });
  return NextResponse.json({ data: fiatAddress }, { status: 201 });
}
```

Iron's onboarding involves multiple steps. Use Dynamic's `updateUser` to persist progress in the user's metadata across sessions so users don't have to restart if they close the tab.

```typescript src/lib/hooks/useKYCMetadata.ts theme={"system"}
import { onEvent, updateUser } from "@dynamic-labs-sdk/client";
import { useState, useEffect, useCallback } from "react";
import { dynamicClient } from "@/lib/dynamic";

export type OnboardStep = "customer" | "kyc" | "signings" | "wallet" | "bank" | "complete";

export function useKYCMetadata() {
  const [state, setState] = useState({ customerId: "", step: "customer" as OnboardStep, /* ... */ });

  // Load from dynamicClient.user on mount and whenever user changes
  useEffect(() => {
    const loadFromUser = () => {
      const metadata = dynamicClient.user?.metadata as { iron?: { customerId?: string; onboardingStep?: OnboardStep } } | undefined;
      if (metadata?.iron?.customerId) {
        setState((s) => ({ ...s, customerId: metadata.iron!.customerId!, step: metadata.iron!.onboardingStep ?? "customer" }));
      }
    };
    loadFromUser();
    const unsub = onEvent({ event: "userChanged", listener: () => loadFromUser() }, dynamicClient);
    return () => unsub();
  }, []);

  const updateState = useCallback(async (updates: Partial<typeof state>) => {
    const newState = { ...state, ...updates };
    setState(newState);
    // Persist to Dynamic user metadata
    await updateUser(
      { userFields: { metadata: { iron: { customerId: newState.customerId, onboardingStep: newState.step } } } },
      dynamicClient
    );
  }, [state]);

  return { ...state, updateState };
}
```

### Build the Onboarding Flow

The onboarding page (`src/app/onboard/page.tsx`) walks users through six steps. Use `useWallet()` to access the current EVM account for wallet registration and signing.

**Wallet registration requires signing a proof-of-ownership message:**

```typescript theme={"system"}
const { evmAccount } = useWallet();

const handleCreateWallet = async () => {
  if (!evmAccount) throw new Error("No wallet connected.");
  const proofMessage = `I am verifying ownership of ${evmAccount.address} as customer ${customerId}`;
  const signature = await evmAccount.signMessage(proofMessage);

  await fetch("/api/iron/wallets/self-hosted", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ customer_id: customerId, blockchain: "Base", address: evmAccount.address, message: proofMessage, signature }),
  });
};
```

### Build the Ramp Interface

With onboarding complete, the main page lets users create autoramps for either direction. The UI has two tabs: **Offramp** (crypto → fiat) and **Onramp** (fiat → crypto).

**Creating an Offramp Autoramp (Crypto → Fiat)**

```typescript theme={"system"}
const handleCreateOfframpAutoramp = async () => {
  const selectedBank = registeredBanks[selectedBankIndex];
  const iban = selectedBank?.iban || selectedBank?.account_identifier?.iban;

  const res = await fetch("/api/offramp", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      action: "execute",
      customer_id: customerId,
      source_currency: selectedToken,     // e.g. "USDC"
      destination_currency: selectedFiat, // e.g. "EUR"
      bank_account_id: iban,
      blockchain: selectedChain,
    }),
  });

  const data = await res.json();
  // data.payment_instructions contains the deposit address
};
```

**Creating an Onramp Autoramp (Fiat → Crypto)**

```typescript theme={"system"}
const handleCreateOnrampAutoramp = async () => {
  const res = await fetch("/api/onramp", {
    method: "POST",
    idempotent: true,
    body: JSON.stringify({
      action: "execute",
      customer_id: customerId,
      source_currency: selectedFiat,     // e.g. "EUR"
      destination_currency: selectedToken, // e.g. "USDC"
      wallet_address: evmAccount?.address,
      blockchain: selectedChain,
    }),
  });

  const data = await res.json();
  // data.payment_instructions contains the virtual IBAN
};
```

### Load Registered Wallets and Banks

### Transaction History

List prior ramps with `GET /api/iron/customers/[id]/autoramps`. Each item includes `kind`, `status`, the embedded `quote`, and the `deposit_rails` so you can render a detail view without re-fetching.

### Run the Application

<CodeGroup>
  ```bash npm theme={"system"}
  npm run dev
  ```

  ```bash yarn theme={"system"}
  yarn dev
  ```

  ```bash pnpm theme={"system"}
  pnpm dev
  ```

  ```bash bun theme={"system"}
  bun dev
  ```
</CodeGroup>

The app is available at `http://localhost:3000`. Make sure that origin is in **Security → Allowed Origins** in the Dynamic dashboard.

## Conclusion

This integration demonstrates how Dynamic's embedded wallets combine with Iron's payment infrastructure to deliver a compliant onramp and offramp experience:

* **Embedded Wallets** — Dynamic's JS SDK handles wallet creation, authentication, and message signing; no seed phrase management for users
* **KYC & Compliance** — Iron's hosted onboarding flow handles identity verification, document signing, and Travel Rule compliance
* **Offramp** — Users send USDC to an Iron-managed wallet address and receive EUR via SEPA directly in their bank account
* **Onramp** — Users send EUR via SEPA to a virtual IBAN and receive USDC in their Dynamic embedded wallet
* **Persistent Autoramps** — Conversion rules are set up once and reused for every subsequent deposit
* **Persistent State** — Dynamic user metadata (`updateUser`) keeps onboarding progress safe across sessions

### Additional Resources

* [Dynamic JavaScript SDK](/javascript/reference/quickstart)
* [Dynamic Embedded Wallets](/overview/wallets/embedded-wallets/mpc/overview)
* [Iron Documentation](https://docs.iron.xyz)
* [Iron Autoramp API](https://docs.iron.xyz/reference-sandbox/autoramp/create-a-new-autoramp)
* [Stablecoin Accounts Overview](/recipes/stablecoins/quick-start)
