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

# Virtual Debit Cards Powered by Stablecoins

> Issue virtual Visa debit cards funded by USDC using Dynamic embedded wallets, account abstraction, and the Rain Card API.

## What We're Building

A Next.js application that lets users create an embedded wallet via social login, complete a KYC application, and receive a virtual Visa debit card funded by stablecoins (USDC). The complete flow covers:

* Social login and automatic wallet creation with Dynamic
* Gasless transactions via ZeroDev account abstraction
* Multi-step KYC form submitted to the Rain Card API
* Virtual card issuance, balance display, and USDC deposits
* JWT-authenticated API routes using Dynamic's JWKS endpoint

The [full example is on GitHub](https://github.com/dynamic-labs-oss/examples/tree/main/examples/nextjs-stablecoin-card-rain).

***

## Prerequisites

* A [Dynamic](https://app.dynamic.xyz) environment ID and API key
* A [Rain Card](https://rain.com) API key and base URL
* Node.js 18+

***

## Step 1: Install Dependencies

```bash theme={"system"}
npm install @dynamic-labs/sdk-react-core @dynamic-labs/ethereum @dynamic-labs/ethereum-aa \
  viem @tanstack/react-query react-hook-form zod jsonwebtoken jwks-rsa
```

The key packages:

| Package                        | Purpose                                                  |
| :----------------------------- | :------------------------------------------------------- |
| `@dynamic-labs/sdk-react-core` | Embedded wallet UI and hooks                             |
| `@dynamic-labs/ethereum-aa`    | ZeroDev smart wallet connectors for gasless transactions |
| `viem`                         | EVM transaction encoding (ERC-20 deposits)               |
| `@tanstack/react-query`        | Async state management for on-chain reads                |
| `jsonwebtoken` / `jwks-rsa`    | JWT verification for authenticated API routes            |

***

## Step 2: Configure Environment Variables

```bash title=".env.local" theme={"system"}
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-dynamic-environment-id
DYNAMIC_API_KEY=dyn_your_api_key
RAIN_API_KEY=your-rain-api-key
RAIN_API_BASE_URL=https://api.rain.com
GOOGLE_MAPS_API_KEY=your-google-maps-key   # optional, for address autocomplete
```

***

## Step 3: Set Up Dynamic with Account Abstraction

Wrap your app with `DynamicContextProvider`, registering both standard Ethereum connectors and ZeroDev smart wallet connectors. The smart wallet connectors enable gasless USDC deposits from the user's embedded wallet to their card account.

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

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
  DynamicContextProvider,
  EthereumWalletConnectors,
  DynamicUserProfile,
  ZeroDevSmartWalletConnectors,
} from "@dynamic-labs/sdk-react-core";
import { redirect } from "next/navigation";

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
  });

  return (
    <DynamicContextProvider
      theme="light"
      settings={{
        environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
        walletConnectors: [
          EthereumWalletConnectors,
          ZeroDevSmartWalletConnectors,
        ],
        events: {
          onLogout: () => redirect("/"),
        },
      }}
    >
      <QueryClientProvider client={queryClient}>
        {children}
        <DynamicUserProfile />
      </QueryClientProvider>
    </DynamicContextProvider>
  );
}
```

Add this to your root layout:

```tsx title="app/layout.tsx" theme={"system"}
import Providers from "@/lib/providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

***

## Step 4: Authenticate API Routes with Dynamic JWTs

Dynamic issues a JWT for every authenticated user. Protect your server-side API routes by verifying that token against Dynamic's JWKS endpoint. This gives you access to the user's verified credentials (wallet address, email) inside every handler.

```ts title="lib/dynamic/dynamic-auth.ts" theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";

export type AuthenticatedUser = {
  sub: string;
  email?: string;
  metadata?: Record<string, unknown>;
  verified_credentials: Array<{
    address?: string;
    wallet_provider?: string;
  }>;
};

const client = jwksClient({
  jwksUri: "https://app.dynamicauth.com/api/v0/sdk/.well-known/jwks",
});

async function verifyToken(token: string): Promise<AuthenticatedUser> {
  return new Promise((resolve, reject) => {
    jwt.verify(token, (header, callback) => {
      client.getSigningKey(header.kid, (err, key) => {
        callback(err, key?.getPublicKey());
      });
    }, {}, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded as AuthenticatedUser);
    });
  });
}

type AuthHandler = (
  req: NextRequest,
  context: { user: AuthenticatedUser }
) => Promise<NextResponse>;

export function withAuth(handler: AuthHandler) {
  return async (req: NextRequest) => {
    const authHeader = req.headers.get("authorization");
    if (!authHeader?.startsWith("Bearer ")) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    try {
      const user = await verifyToken(authHeader.slice(7));
      return handler(req, { user });
    } catch {
      return NextResponse.json({ error: "Invalid token" }, { status: 401 });
    }
  };
}
```

<Note>
  The JWKS URI must include your environment ID in production:
  `https://app.dynamicauth.com/api/v0/sdk/{environmentId}/.well-known/jwks`
</Note>

***

## Step 5: Build the KYC Application Flow

The KYC form collects personal, address, and financial information across three steps. On submit, a server action POSTs to your `/api/apply` route, which forwards the data to Rain and automatically creates a card.

### Application API route

```ts title="app/api/apply/route.ts" theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import { withAuth, AuthenticatedUser } from "@/lib/dynamic/dynamic-auth";
import { updateUserMetadata } from "@/lib/dynamic/methods";
import { createUserApplication, createCardForUser } from "@/lib/rain";

export const POST = withAuth(
  async (req: NextRequest, { user }: { user: AuthenticatedUser }) => {
    const body = await req.json();

    // Pull the embedded wallet address from verified credentials
    const walletAddress = user.verified_credentials.find(
      (c) => c.wallet_provider === "embeddedWallet"
    )?.address;

    if (!walletAddress) {
      return NextResponse.json({ error: "No wallet address found" }, { status: 400 });
    }

    if (!user.email) {
      return NextResponse.json({ error: "Email is required" }, { status: 400 });
    }

    // Submit KYC to Rain
    const application = await createUserApplication({
      firstName: body.firstName,
      lastName: body.lastName,
      birthDate: body.birthDate,
      nationalId: body.nationalId.replace(/\D/g, ""),
      countryOfIssue: "US",
      email: user.email,
      phoneCountryCode: "1",
      phoneNumber: body.phoneNumber.replace(/\D/g, ""),
      address: body.address,
      walletAddress,
      ipAddress: body.ipAddress,
      occupation: body.occupation,
      annualSalary: body.annualSalary,
      accountPurpose: body.accountPurpose,
      expectedMonthlyVolume: body.expectedMonthlyVolume,
      isTermsOfServiceAccepted: true,
    });

    // Create a virtual card for the approved application
    const card = await createCardForUser(application.id, {
      type: "virtual",
      limit: {
        frequency: "per30DayPeriod",
        amount: Number(body.expectedMonthlyVolume),
      },
    });

    // Persist card reference in Dynamic user metadata
    await updateUserMetadata(user.sub, { rainCard: card });

    return NextResponse.json({ ok: true, card });
  }
);
```

<Note>
  The Rain API auto-approves applications in test mode. In production, applications go through manual KYC review before a card can be created.
</Note>

### Calling the route from the client

Pass the Dynamic auth token as a Bearer token in every request to your API routes:

```tsx theme={"system"}
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";

function useAuthFetch() {
  const { authToken } = useDynamicContext();

  return async (url: string, options: RequestInit = {}) => {
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${authToken}`,
        "Content-Type": "application/json",
      },
    });
  };
}

// In your form submit handler:
const authFetch = useAuthFetch();
const res = await authFetch("/api/apply", {
  method: "POST",
  body: JSON.stringify(formData),
});
```

***

## Step 6: Display Card Details and Balance

Once a card is created, its reference is stored in the user's Dynamic metadata. Retrieve the card and check the Rain balance from separate API routes, then render them in a card component.

### Balance API route

```ts title="app/api/balance/route.ts" theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import { withAuth, AuthenticatedUser } from "@/lib/dynamic/dynamic-auth";
import { userCreditBalance } from "@/lib/rain/methods";

export const GET = withAuth(
  async (_req: NextRequest, { user }: { user: AuthenticatedUser }) => {
    const rainCard = user.metadata?.rainCard as { userId: string } | undefined;

    if (!rainCard) {
      return NextResponse.json({ error: "No card found" }, { status: 404 });
    }

    const balance = await userCreditBalance(rainCard.userId);
    return NextResponse.json({ balance });
  }
);
```

### Card details (encrypted)

Card numbers, CVVs, and expiry dates come back from Rain encrypted. Retrieve them per session and render client-side with a show/hide toggle:

```ts title="app/api/card-details/route.ts" theme={"system"}
import { NextRequest, NextResponse } from "next/server";
import { withAuth, AuthenticatedUser } from "@/lib/dynamic/dynamic-auth";
import { cardEncryptedData } from "@/lib/rain/methods";

export const POST = withAuth(
  async (req: NextRequest, { user }: { user: AuthenticatedUser }) => {
    const { sessionId } = await req.json();
    const rainCard = user.metadata?.rainCard as { cardId: string } | undefined;

    if (!rainCard) {
      return NextResponse.json({ error: "No card found" }, { status: 404 });
    }

    const data = await cardEncryptedData(rainCard.cardId, sessionId);
    return NextResponse.json(data);
  }
);
```

***

## Step 7: Fund the Card with USDC

Users deposit USDC from their embedded wallet to the card's on-chain account. Use viem's `writeContract` to call the ERC-20 `transfer` function, with ZeroDev sponsoring gas so users don't need ETH.

```tsx title="hooks/useDepositTokens.ts" theme={"system"}
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { parseUnits, erc20Abi } from "viem";

const RUSDC_ADDRESS = "0x..."; // rUSDC contract on your target network
const CARD_DEPOSIT_ADDRESS = "0x..."; // Rain card account address

export function useDepositTokens() {
  const { primaryWallet } = useDynamicContext();

  const deposit = async (amountUsd: number) => {
    if (!primaryWallet || !isEthereumWallet(primaryWallet)) {
      throw new Error("EVM wallet not connected");
    }

    const walletClient = await primaryWallet.getWalletClient();
    const publicClient = await primaryWallet.getPublicClient();

    // USDC has 6 decimals
    const amount = parseUnits(String(amountUsd), 6);

    const hash = await walletClient.writeContract({
      address: RUSDC_ADDRESS,
      abi: erc20Abi,
      functionName: "transfer",
      args: [CARD_DEPOSIT_ADDRESS, amount],
    });

    await publicClient.waitForTransactionReceipt({ hash });
    return hash;
  };

  return { deposit };
}
```

Offer preset amounts ($5, $10, \$25) and a custom input so users can quickly top up their card:

```tsx title="components/FundCard.tsx" theme={"system"}
import { useState } from "react";
import { useDepositTokens } from "@/hooks/useDepositTokens";

const PRESETS = [5, 10, 25];

export function FundCard() {
  const [amount, setAmount] = useState<number>(10);
  const [loading, setLoading] = useState(false);
  const { deposit } = useDepositTokens();

  const handleDeposit = async () => {
    setLoading(true);
    try {
      await deposit(amount);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <div>
        {PRESETS.map((p) => (
          <button key={p} onClick={() => setAmount(p)} aria-pressed={amount === p}>
            ${p}
          </button>
        ))}
      </div>
      <input
        type="number"
        value={amount}
        onChange={(e) => setAmount(Number(e.target.value))}
        min={1}
      />
      <button onClick={handleDeposit} disabled={loading}>
        {loading ? "Depositing..." : `Deposit $${amount}`}
      </button>
    </div>
  );
}
```

***

## Complete User Flow

```
1. User visits the app → sees a "Get Started" prompt
2. Social login (email, Google, Discord) → Dynamic creates an embedded wallet
3. User navigates to /apply → fills out 3-step KYC form
4. POST /api/apply → Rain API creates application + virtual card
5. Card reference saved to Dynamic user metadata
6. User navigates to /card → sees card details, wallet balance, card balance
7. User deposits USDC → ERC-20 transfer from wallet to card account (gasless via ZeroDev)
8. User spends on the virtual Visa card → transactions appear in history
```

***

## Supported Networks

| Network                    | Chain ID |
| :------------------------- | :------- |
| Ethereum Sepolia (testnet) | 11155111 |
| Base Sepolia (testnet)     | 84532    |

***

## Related

* [Building with Stablecoin Accounts](/recipes/stablecoins/quick-start)
* [Sending USDC](/recipes/stablecoins/sending-usdc)
* [Embedded Wallets Overview](/overview/wallets/embedded-wallets/mpc/overview)
* [ZeroDev Smart Wallets](/react/smart-wallets/add-smart-wallets)
* [Working with User Metadata](/react/users/displaying-updating#working-with-user-metadata)
