Skip to main content
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:
FlowHow it worksBest for
Pay with linkUser pastes a pay.walletconnect.com link, picks a payment option, and signsCheckout links, invoices, payment requests
Pay via QR / WalletConnect sessionUser scans or pastes a wc: URI, establishes a session, and approves transactionsPOS 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

3. Configure the PrivyProvider

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');
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:
ChainChain IDCAIP-2 prefixUSDC address
Ethereum1eip155:10xA... (USDC on Ethereum)
Base8453eip155:84530x... (USDC on Base)
Ethereum Sepolia11155111eip155:111551110x... (USDC on Ethereum Sepolia)
Base Sepolia84532eip155:845320x... (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

1

Create a test payment

Go to the WalletConnect POS demo and create a test payment. Copy the payment link.
2

Log in with Privy

Log in to your app with Privy so an embedded wallet is provisioned.
3

Complete the payment flow

Paste the link into your app and complete the payment flow.

Learn more