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 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
  • Support multiple chains (Ethereum, Base, Polygon, Arbitrum, Optimism, and more)
  • Handle ERC-20 token transfers (like USDC) alongside native ETH payments
WalletConnect Pay offers interchange revenue for wallets and $WCT cashback for users. See the WalletConnect Pay overview for details on earning revenue through your integration.

Setup

1. Install dependencies

npm install @privy-io/react-auth @reown/walletkit @walletconnect/core

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 WalletKit

Create a shared module that initializes WalletKit for the payment link flow.
import {Core} from '@walletconnect/core';
import {WalletKit} from '@reown/walletkit';

let core: InstanceType<typeof Core> | null = null;
let walletkit: Awaited<ReturnType<typeof WalletKit.init>> | null = null;

const projectId = process.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() as any,
    metadata
  });
  return web3wallet;
}

export async function getWalletKit() {
  if (walletkit) return walletkit;
  walletkit = await WalletKit.init({
    core: getCore() as any,
    metadata,
    payConfig: {
      appId: projectId,
      apiKey: 'your-wc-pay-api-key'
    }
  });
  return walletkit;
}

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:
import {useWallets} from '@privy-io/react-auth';
declare function getWalletKit(): Promise<any>;
declare const uri: string;

const SUPPORTED_CHAINS = [
  'eip155:1', // Ethereum
  'eip155:8453', // Base
  'eip155:137', // Polygon
  'eip155:42161', // Arbitrum
  'eip155:10' // Optimism
];

const {wallets} = useWallets();
const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy');

const walletkit = await getWalletKit();
const address = embeddedWallet.address;

const accounts = SUPPORTED_CHAINS.map((prefix) => `${prefix}:${address}`);

try {
  const options = await walletkit.pay.getPaymentOptions({
    paymentLink: uri,
    accounts,
    includePaymentInfo: true
  });
} catch (error) {
  if (error.message.includes('payment not found')) {
    // The payment link is invalid or has been cancelled
  } else if (error.message.includes('expired')) {
    // The payment has expired
  } else {
    throw error;
  }
}
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 (expiresAt)
Check options.info.expiresAt and warn users when time is running low. Payments expire after a set period, and signing after expiry results in a failed payment.

4. Handle data collection (if required)

Some payment options require additional user information (e.g. shipping address). Check the selected option for a collectData object and render the provided URL in an iframe:
const collectData = selectedOption.collectData;

if (collectData?.url) {
  // Render collectData.url in an iframe.
  // WalletConnect handles the form UI inside the iframe.
  // Listen for postMessage events from the iframe:
  //   { type: 'IC_COMPLETE' } → data collected successfully, proceed to signing
  //   { type: 'IC_ERROR' }   → collection failed, show an error
  // To pre-populate known user data, append a base64-encoded JSON query param:
  // const prefill = btoa(JSON.stringify({ email: '[email protected]' }));
  // const url = `${collectData.url}?prefill=${prefill}`;
}
The iframe submits collected data directly to WalletConnect. Do not pass collectedData to confirmPayment() when using this flow — it is handled automatically.

5. Get required actions and sign

Once the user selects a payment option, fetch the transaction actions and sign them with the Privy wallet. The API can return multiple actions (e.g. a token approval followed by a Permit2 signature), and different RPC methods require different parameter handling:
import {useWallets} from '@privy-io/react-auth';
declare function getWalletKit(): Promise<any>;
declare const options: any;
declare const selectedOption: {id: string};

const {wallets} = useWallets();
const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy');

const walletkit = await getWalletKit();

const actions = await walletkit.pay.getRequiredPaymentActions({
  paymentId: options.paymentId,
  optionId: selectedOption.id
});

const provider = await embeddedWallet.getEthereumProvider();

const signatures = await Promise.all(
  actions.map(async (action) => {
    const {chainId, method, params} = action.walletRpc;
    const parsedParams = JSON.parse(params);

    const numericChainId = parseInt(chainId.split(':')[1], 10);
    await embeddedWallet.switchChain(numericChainId);

    switch (method) {
      case 'eth_sendTransaction':
        return await provider.request({method, params: [parsedParams[0]]});
      case 'eth_signTypedData_v4':
        return await provider.request({method, params: parsedParams});
      case 'personal_sign':
        return await provider.request({method, params: parsedParams});
      default:
        throw new Error(`Unsupported RPC method: ${method}`);
    }
  })
);
The three methods above (eth_sendTransaction, eth_signTypedData_v4, personal_sign) are the most common, but the API can return any wallet RPC method. Add additional cases to the switch statement as needed for your integration.

6. Confirm the payment

Submit all signatures to finalize the payment. The response may indicate the payment is still processing — poll until isFinal is true:
async function confirmAndPoll(
  walletkit: Awaited<ReturnType<typeof WalletKit.init>>,
  paymentId: string,
  optionId: string,
  signatures: string[]
) {
  let result = await walletkit.pay.confirmPayment({
    paymentId,
    optionId,
    signatures
  });

  while (!result.isFinal && result.pollInMs) {
    await new Promise((resolve) => setTimeout(resolve, result.pollInMs));
    result = await walletkit.pay.confirmPayment({
      paymentId,
      optionId,
      signatures
    });
  }

  return result;
}

try {
  const result = await confirmAndPoll(walletkit, options.paymentId, selectedOption.id, signatures);

  if (result.status === 'succeeded') {
    // Payment confirmed
  } else {
    // Payment failed — check result.status for details
    // Possible statuses: 'failed', 'expired', 'cancelled'
  }
} catch (error) {
  // Network error or unexpected failure
}
Key types from the WalletConnect Pay API:
interface PaymentOptionsResponse {
  paymentId: string;
  info?: PaymentInfo;
  options: PaymentOption[];
  collectData?: CollectDataAction;
}

interface PaymentOption {
  id: string;
  amount: PayAmount;
  etaS: number;
  actions: Action[];
  collectData?: CollectDataAction;
}

interface WalletRpcAction {
  chainId: string;
  method: string;
  params: string; // JSON-encoded string
}

interface ConfirmPaymentResponse {
  status: 'requires_action' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'cancelled';
  isFinal: boolean;
  pollInMs?: number;
  info?: PaymentResultInfo;
}
For the full API reference, see the WalletConnect Pay SDK documentation.

Supported chains

Configure which chains your app supports. The SUPPORTED_CHAINS array in step 3 determines which payment options WalletConnect Pay returns.
ChainChain IDCAIP-2 prefixUSDC address
Ethereum1eip155:10xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Base8453eip155:84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Polygon137eip155:1370x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Arbitrum42161eip155:421610xaf88d065e77c8cC2239327C5EDb3A432268e5831
Optimism10eip155:100x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
WalletConnect Pay matches available payment options to the chains your app lists. Add more chains to SUPPORTED_CHAINS to offer users additional payment methods.
For testnet development, use the WalletConnect Dashboard POS tool to create test payments on supported testnets.

Testing

1

Create a test payment

Go to the WalletConnect Dashboard and use the POS tool to 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

WalletConnect Pay documentation

Official WalletConnect Pay integration guide for wallets

WalletConnect Dashboard

Create test payments and manage your WCP ID