Examples use React with @dynamic-labs-sdk/react-hooks. The underlying JS SDK is framework-agnostic — swap the hooks for direct calls against @dynamic-labs-sdk/client to adapt this guide to vanilla JS, Vue, Svelte, or any other framework.
The JavaScript SDK is headless, so receiving USDC always comes down to surfacing the user’s wallet address in some shape your UI can render. This guide walks through four ways to do that.
Setup
Follow the React Quickstart (JS SDK) to scaffold a Vite + React app and wire <DynamicProvider> with the EVM extension.
Subdomain (Global Identity)
If you enable Global Identity (configured in the dashboard, framework-agnostic), each EVM wallet gets a unique subdomain (e.g. alice.dyn.eth) that anyone can use to send funds. The subdomain lives on the wallet’s verified credential as nameService.name:
import { useUser, useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
export function Subdomain() {
const { data: user } = useUser();
const { data: accounts = [] } = useGetWalletAccounts();
const evmAccount = accounts.find(isEvmWalletAccount);
const subdomain = user?.verifiedCredentials.find(
(vc) => vc.address?.toLowerCase() === evmAccount?.address.toLowerCase()
)?.nameService?.name;
if (!subdomain) return null;
return <p>Send USDC to: <strong>{subdomain}</strong></p>;
}
QR code
Generate a QR for the raw address with any QR library (e.g. qrcode.react):
import { useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { QRCodeSVG } from "qrcode.react";
export function ReceiveQR() {
const { data: accounts = [] } = useGetWalletAccounts();
const address = accounts.find(isEvmWalletAccount)?.address;
if (!address) return null;
return <QRCodeSVG value={address} size={192} />;
}
Wallet address
For copy-to-clipboard flows, read address straight off the wallet account:
import { useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
export function CopyAddress() {
const { data: accounts = [] } = useGetWalletAccounts();
const address = accounts.find(isEvmWalletAccount)?.address;
if (!address) return null;
return (
<button onClick={() => navigator.clipboard.writeText(address)}>
Copy {address.slice(0, 6)}…{address.slice(-4)}
</button>
);
}
Payment links
A “payment link” is just a URL with the recipient, amount, and chain encoded as query params. The sender lands on a page in your app that reads the params and triggers a USDC transfer from their wallet.
Generating the link
src/components/PaymentLinkGenerator.tsx
import { useState } from "react";
import { useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { getActiveNetworkId } from "@dynamic-labs-sdk/client";
export function PaymentLinkGenerator() {
const { data: accounts = [] } = useGetWalletAccounts();
const walletAccount = accounts.find(isEvmWalletAccount);
const [amount, setAmount] = useState("10");
const [link, setLink] = useState("");
const generate = async () => {
if (!walletAccount) return;
const { networkId } = await getActiveNetworkId({ walletAccount });
const params = new URLSearchParams({
recipient: walletAccount.address,
amount,
token: "USDC",
chainId: networkId,
});
setLink(`${window.location.origin}/pay?${params}`);
};
return (
<>
<input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} step="0.01" />
<button onClick={generate} disabled={!walletAccount}>Generate link</button>
{link && (
<>
<input readOnly value={link} />
<button onClick={() => navigator.clipboard.writeText(link)}>Copy</button>
</>
)}
</>
);
}
getActiveNetworkId returns the wallet’s current chain id (e.g. "8453" for Base) so the sender lands on the right chain. See Getting Active Network.
Processing the link
The payment page reads the URL params, switches the sender’s wallet to the correct chain, then runs an ERC-20 transfer:
import { useState } from "react";
import { useUser, useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import {
getActiveNetworkId,
switchActiveNetwork,
NetworkNotAddedError,
addNetwork,
} from "@dynamic-labs-sdk/client";
import { useSearchParams } from "react-router-dom";
import { parseUnits, erc20Abi } from "viem";
const USDC_BY_CHAIN: Record<string, `0x${string}`> = {
"1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum
"137": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // Polygon (native)
"42161": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum (native)
"10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism (native)
"8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
};
export function Pay() {
const [params] = useSearchParams();
const { data: user } = useUser();
const { data: accounts = [] } = useGetWalletAccounts();
const walletAccount = accounts.find(isEvmWalletAccount);
const recipient = params.get("recipient") as `0x${string}` | null;
const amount = params.get("amount");
const targetChainId = params.get("chainId");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hash, setHash] = useState<`0x${string}` | null>(null);
if (!recipient || !amount || !targetChainId) return <p>Invalid payment link</p>;
if (!user) return <p>Sign in to complete this payment</p>;
const handlePay = async () => {
if (!walletAccount) return setError("Connect an EVM wallet");
const usdc = USDC_BY_CHAIN[targetChainId];
if (!usdc) return setError(`USDC not supported on chain ${targetChainId}`);
setError(null);
setPending(true);
try {
const { networkId } = await getActiveNetworkId({ walletAccount });
if (networkId !== targetChainId) {
try {
await switchActiveNetwork({ walletAccount, networkId: targetChainId });
} catch (err) {
if (err instanceof NetworkNotAddedError) {
await addNetwork({ walletAccount, networkData: err.networkData });
await switchActiveNetwork({ walletAccount, networkId: targetChainId });
} else throw err;
}
}
const walletClient = await createWalletClientForWalletAccount({ walletAccount });
const tx = await walletClient.writeContract({
address: usdc,
abi: erc20Abi,
functionName: "transfer",
args: [recipient, parseUnits(amount, 6)],
});
setHash(tx);
} catch (err: any) {
setError(err.message ?? "Payment failed");
} finally {
setPending(false);
}
};
if (hash) return <p>Paid! Tx: {hash}</p>;
return (
<>
<p>Pay {amount} USDC to {recipient.slice(0, 8)}…{recipient.slice(-6)}</p>
<button onClick={handlePay} disabled={pending}>{pending ? "Processing…" : "Pay"}</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</>
);
}
For richer payment requests (description, reference, expiry) just add more query params before generating the link:
const params = new URLSearchParams({
recipient: walletAccount.address,
amount,
token: "USDC",
chainId: networkId,
description: "Coffee",
reference: "order-2026-0042",
expiresAt: String(Date.now() + 60 * 60 * 1000), // 1 hour
});
Always validate expiresAt on the payment page before accepting the transfer.
Security considerations
- Validate params server-side if your app touches the recipient address or amount in any backend flow — the URL is user-controlled.
- Rate limit the page that processes links so a leaked URL can’t drain a wallet via auto-clicking.
- HTTPS only — never serve payment URLs over plain HTTP.
- Bound amounts — reject anything outside a sane min/max for your product.