Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
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:
- Quote (
GET /api/autoramps/quote) returns a signed, time-limited rate for the requested direction, amount, and rails.
- 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.
- 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:
- Create an Iron customer
- Complete KYC identity verification (hosted link)
- Sign any required compliance documents
- Register the Dynamic embedded wallet (signed proof of ownership — Travel Rule)
- Add a SEPA bank account
- 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. This guide uses the headless @dynamic-labs-sdk/client (not the React SDK) with the EVM extension.
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).
Install Dependencies
npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @tanstack/react-query @radix-ui/react-select lucide-react
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 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:
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:
"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:
src/app/api/iron/customers/route.ts
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:
src/app/api/iron/customers/[id]/banks/route.ts
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.
src/lib/hooks/useKYCMetadata.ts
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:
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)
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)
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
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