Privy embedded wallets give every user a wallet at signup, with no extensions or seed phrases required. WalletConnect Pay lets merchants generate payment links and QR codes that any wallet can fulfill. This recipe shows how to combine them so users can pay for anything in-app, with a single tap.
Using Privy and WalletConnect Pay together, your app can:
- Let users paste a WalletConnect Pay link and complete the payment with their Privy wallet
- Connect to any dApp via WalletConnect QR code and approve transactions in-app
- Support multiple chains (Ethereum, Base, Polygon, Arbitrum, and more)
- Handle ERC-20 token transfers (like USDC) alongside native ETH payments
This recipe covers two flows:
| Flow | How it works | Best for |
|---|
| Pay with link | User pastes a pay.walletconnect.com link, picks a payment option, and signs | Checkout links, invoices, payment requests |
| Pay via QR / WalletConnect session | User scans or pastes a wc: URI, establishes a session, and approves transactions | POS terminals, dApp interactions, merchant QR codes |
Setup
1. Install dependencies
npm install @privy-io/react-auth @reown/walletkit @walletconnect/core @walletconnect/web3wallet
2. Get project IDs
Your app needs two project IDs:
Add them to your environment:
VITE_PRIVY_APP_ID=your-privy-app-id
VITE_WALLETCONNECT_PROJECT_ID=your-walletconnect-project-id
Wrap your app with PrivyProvider and enable automatic embedded wallet creation. This ensures every user gets a wallet on login.
import {PrivyProvider} from '@privy-io/react-auth';
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{
appearance: {
theme: 'dark'
},
embeddedWallets: {
ethereum: {
createOnLogin: 'users-without-wallets'
}
}
}}
>
<App />
</PrivyProvider>;
Setting createOnLogin to 'users-without-wallets' means Privy automatically provisions an
Ethereum wallet the first time a user logs in. No extra steps needed.
4. Initialize WalletConnect clients
Create a shared module that initializes both the Web3Wallet (for QR/session flow) and WalletKit (for payment link flow). Both share a single Core instance.
import {Core} from '@walletconnect/core';
import {Web3Wallet} from '@walletconnect/web3wallet';
import {WalletKit} from '@reown/walletkit';
let core: InstanceType<typeof Core> | null = null;
let web3wallet: Awaited<ReturnType<typeof Web3Wallet.init>> | null = null;
let walletkit: Awaited<ReturnType<typeof WalletKit.init>> | null = null;
const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID;
const metadata = {
name: 'My Pay App',
description: 'Pay with crypto',
url: window.location.origin,
icons: ['https://your-app.com/icon.png']
};
function getCore() {
if (!core) {
core = new Core({projectId});
}
return core;
}
export async function getWeb3Wallet() {
if (web3wallet) return web3wallet;
web3wallet = await Web3Wallet.init({
core: getCore(),
metadata
});
return web3wallet;
}
export async function getWalletKit() {
if (walletkit) return walletkit;
walletkit = await WalletKit.init({
core: getCore(),
metadata,
payConfig: {
appId: 'your-wc-pay-app-id',
apiKey: 'your-wc-pay-api-key'
}
});
return walletkit;
}
Web3Wallet handles WalletConnect v2 session pairing (QR codes, wc: URIs). WalletKit from @reown/walletkit adds the Pay API for payment links. Both share one Core to avoid duplicate relay connections.
Pay with link
This flow lets users paste a WalletConnect Pay link (like https://pay.walletconnect.com/...) and complete the payment entirely in your app.
1. Get the user’s Privy wallet
Use Privy’s useWallets hook to access the embedded wallet:
import {useWallets} from '@privy-io/react-auth';
const {wallets} = useWallets();
const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy');
2. Validate the payment link
Use isPaymentLink from @reown/walletkit to verify the link before making API calls:
import {isPaymentLink} from '@reown/walletkit';
const uri = 'https://pay.walletconnect.com/...';
if (!isPaymentLink(uri)) {
throw new Error('Not a valid WalletConnect Pay link');
}
3. Fetch payment options
Build the user’s account list from supported chain prefixes and their wallet address, then call getPaymentOptions:
const SUPPORTED_CHAINS = [
'eip155:1', // Ethereum
'eip155:8453', // Base
'eip155:137', // Polygon
'eip155:42161', // Arbitrum
'eip155:10' // Optimism
];
const walletkit = await getWalletKit();
const address = embeddedWallet.address;
const accounts = SUPPORTED_CHAINS.map((prefix) => `${prefix}:${address}`);
const options = await walletkit.pay.getPaymentOptions({
paymentLink: uri,
accounts,
includePaymentInfo: true
});
The response contains:
paymentId — unique identifier for this payment session
options — array of payment methods (different tokens and chains the user can pay with)
info — merchant name, requested amount, and expiry
4. Get required actions and sign
Once the user selects a payment option, fetch the transaction actions and sign each one with the Privy wallet:
const actions = await walletkit.pay.getRequiredPaymentActions({
paymentId: options.paymentId,
optionId: selectedOption.id
});
const provider = await embeddedWallet.getEthereumProvider();
const signatures: string[] = [];
for (const action of actions) {
const {chainId, method, params} = action.walletRpc;
const numericChainId = parseInt(chainId.split(':')[1], 10);
await embeddedWallet.switchChain(numericChainId);
const result = await provider.request({
method,
params: JSON.parse(params)
});
signatures.push(result);
}
Each action includes a walletRpc object with the chain, method (e.g. eth_sendTransaction), and params. The Privy embedded wallet handles signing seamlessly — no pop-ups or confirmations unless your app configures them.
5. Handle data collection (if required)
Some payment options require additional user information (e.g. shipping address). The API provides a collectData URL that your app renders in an iframe:
const collectDataUrl = selectedOption.collectData?.url ?? options.collectData?.url;
if (collectDataUrl) {
// Render the URL in an iframe overlay.
// Listen for postMessage events:
// { type: 'IC_COMPLETE' } → data collected, proceed
// { type: 'IC_ERROR' } → collection failed
}
6. Confirm the payment
Submit all signatures to finalize the payment:
const result = await walletkit.pay.confirmPayment({
paymentId: options.paymentId,
optionId: selectedOption.id,
signatures
});
if (result.status === 'succeeded' || result.status === 'processing') {
// Payment confirmed
} else {
// Payment failed — check result.status
}
Supported chains
Configure which chains your app supports for each flow:
| Chain | Chain ID | CAIP-2 prefix | USDC address |
|---|
| Ethereum | 1 | eip155:1 | 0xA... (USDC on Ethereum) |
| Base | 8453 | eip155:8453 | 0x... (USDC on Base) |
| Ethereum Sepolia | 11155111 | eip155:11155111 | 0x... (USDC on Ethereum Sepolia) |
| Base Sepolia | 84532 | eip155:84532 | 0x... (USDC on Base Sepolia) |
For the Pay with link flow, your app can advertise additional chains (Polygon, Arbitrum, Optimism,
zkSync, Avalanche) since WalletConnect Pay matches available payment options to the chains listed.
Testing
Log in with Privy
Log in to your app with Privy so an embedded wallet is provisioned.
Complete the payment flow
Paste the link into your app and complete the payment flow.
Learn more