A debit card your users fund with stablecoins. Issued in seconds.
Your users sign in with email or social and get an embedded wallet and a virtual Visa card in the same flow. They fund it gaslessly from their own stablecoin balance and spend it anywhere Visa is accepted - issue, fund, and reveal the card in a handful of calls.
@dynamic-labs-sdk/client@dynamic-labs-sdk/react-hooks@dynamic-labs-sdk/evmCreate the wallet
DocsOne Dynamic client per app. The EVM extension gives every user an embedded wallet - the card's funding source and the signer for gasless transfers.
lib/dynamic-client.tsimport { createDynamicClient } from "@dynamic-labs-sdk/client"; import { addEvmExtension } from "@dynamic-labs-sdk/evm"; export const client = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!, }); addEvmExtension(client);Issue the card
On your server - the Rain Api-Key never touches the browser. Submit the KYC application for a userId, then issue a virtual card for that user.
server/issue-card.tsconst RAIN = "https://api-dev.raincards.xyz/v1/issuing"; const auth = { "Api-Key": process.env.RAIN_API_KEY!, "Content-Type": "application/json", }; // POST /v1/issuing/applications/user -> KYC, returns the Rain userId const { userId } = await fetch(`${RAIN}/applications/user`, { method: "POST", headers: auth, body: JSON.stringify(application), }).then((r) => r.json()); // POST /v1/issuing/users/:userId/cards -> the virtual card const card = await fetch(`${RAIN}/users/${userId}/cards`, { method: "POST", headers: auth, body: JSON.stringify({ type: "virtual" }), }).then((r) => r.json());Reveal card details
The plaintext PAN/CVC never touch your server. The browser mints an AES key and passes its SessionId; Rain returns the secrets ENCRYPTED, your server relays them, and the browser decrypts locally.
server/card-secrets.ts// GET /v1/issuing/cards/:cardId/secrets (SessionId = the browser's key) const encrypted = await fetch(`${RAIN}/cards/${cardId}/secrets`, { headers: { "Api-Key": process.env.RAIN_API_KEY!, SessionId: sessionId }, }).then((r) => r.json()); // Relay `encrypted` to the browser, which decrypts with its own key. const pan = await decryptSecret(encrypted.encryptedPan, secretKey); const cvc = await decryptSecret(encrypted.encryptedCvc, secretKey);Read the balance
Spending power comes straight from Rain, keyed by the userId. Read it on your server and surface it to the UI.
server/balance.ts// GET /v1/issuing/users/:userId/balances const balance = await fetch(`${RAIN}/users/${userId}/balances`, { headers: { "Api-Key": process.env.RAIN_API_KEY! }, }).then((r) => r.json()); balance.spendingPower; // what the card can spend right nowGet the deposit address
Each user has a per-chain Rain deposit contract; stablecoins sent to it fund the card. Fetch it (or create it once) for the funding chain.
server/deposit-address.ts// GET /v1/issuing/users/:userId/contracts // (POST the same path with { chainId } to create one the first time) const contracts = await fetch(`${RAIN}/users/${userId}/contracts`, { headers: { "Api-Key": process.env.RAIN_API_KEY! }, }).then((r) => r.json()); const depositAddress = contracts.find( (c) => c.chainId === 84532, // Base Sepolia )?.depositAddress;Fund it gaslessly
DocsBack in the browser: a sponsored stablecoin transfer from the user's embedded wallet to that Rain deposit address - EIP-7702, no native gas, no approval popups.
hooks/use-fund-card.tsimport { sendSponsoredTransaction } from "@dynamic-labs-sdk/evm"; const { transactionHash } = await sendSponsoredTransaction({ walletAccount, calls: [ { target: USDC_ADDRESS, data: encodeTransfer(depositAddress, amount), }, ], });