Documentation Index
Fetch the complete documentation index at: https://turnkey-0e7c1f5b-walletconnectpay-cookbook.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Overview
WalletConnect Pay is a payment protocol that enables wallet users to pay merchants with crypto by scanning a QR code. The protocol handles payment discovery, transaction construction, gas sponsorship (via 7702 paymaster), and on-chain broadcast.
This cookbook shows how to integrate Turnkey embedded wallets with WalletConnect Pay using the with-wcpay example — a React Native mobile wallet that authenticates users via email OTP, signs EIP-712 payment authorizations with Turnkey, and lets WalletConnect Pay handle the rest.
Each end-user’s wallet is fully self-custodial: Turnkey creates a dedicated sub-organization per user with a 1-of-1 root quorum, meaning only the authenticated user can authorize signing.
Getting started
Before you begin, make sure you’ve followed the Turnkey Quickstart guide.
You should have:
- A Turnkey organization and Auth Proxy Config ID
- A wallet funded with USDC on Base
You’ll also need:
- A WalletConnect Dashboard wallet project with a WalletConnect Pay API key
- Xcode with iOS Simulator (macOS) for running the React Native app
- Node.js v16+
Install dependencies
npm install @turnkey/react-native-wallet-kit @walletconnect/pay @walletconnect/react-native-compat react-native-webview expo-camera
Setting up the Turnkey wallet
We’ll use @turnkey/react-native-wallet-kit to authenticate and manage an embedded wallet. The TurnkeyProvider wraps the app with auth and wallet context:
import { TurnkeyProvider } from "@turnkey/react-native-wallet-kit";
const TURNKEY_CONFIG = {
organizationId: process.env.EXPO_PUBLIC_TURNKEY_ORGANIZATION_ID,
apiBaseUrl: "https://api.turnkey.com",
authProxyConfigId: process.env.EXPO_PUBLIC_TURNKEY_AUTH_PROXY_CONFIG_ID,
passkeyConfig: {
rpId: process.env.EXPO_PUBLIC_TURNKEY_RPID,
},
auth: {
otp: { email: true, sms: false },
passkey: true,
oauth: { appScheme: "wcpaydemo" },
autoRefreshSession: true,
},
};
export default function App() {
return (
<TurnkeyProvider config={TURNKEY_CONFIG}>
{/* Your app screens */}
</TurnkeyProvider>
);
}
Authenticating with email OTP
Users authenticate via email OTP. On first login, Turnkey creates a sub-organization with an Ethereum wallet:
import { useTurnkey } from "@turnkey/react-native-wallet-kit";
const customWallet = {
walletName: "WCPay Wallet",
walletAccounts: [
{
curve: "CURVE_SECP256K1",
pathFormat: "PATH_FORMAT_BIP32",
path: "m/44'/60'/0'/0/0",
addressFormat: "ADDRESS_FORMAT_ETHEREUM",
},
],
};
function LoginScreen() {
const { initOtp, completeOtp } = useTurnkey();
async function handleLogin(email: string, otpCode: string, otpId: string) {
// Step 1: Send OTP
const id = await initOtp({
otpType: "OTP_TYPE_EMAIL",
contact: email,
});
// Step 2: Verify OTP and create wallet (if new user)
await completeOtp({
otpId: id,
otpCode,
otpType: "OTP_TYPE_EMAIL",
contact: email,
createSubOrgParams: { customWallet },
});
// Auth success — user is now logged in with a wallet
}
}
Initializing WalletConnect Pay
Configure the WalletConnect Pay client with your API key:
import { WalletConnectPay } from "@walletconnect/pay";
const client = new WalletConnectPay({
apiKey: process.env.EXPO_PUBLIC_WC_API_KEY,
});
// Build CAIP-10 accounts for all supported chains
function buildAccounts(walletAddress: string): string[] {
return [
`eip155:1:${walletAddress}`, // Ethereum
`eip155:8453:${walletAddress}`, // Base
`eip155:10:${walletAddress}`, // Optimism
`eip155:137:${walletAddress}`, // Polygon
`eip155:42161:${walletAddress}`, // Arbitrum
];
}
Fetching payment options
When a user scans a merchant QR code or enters a payment link, fetch available payment options:
// Normalize payment link format (dashboard URLs use ?pid= query param)
function normalizePaymentLink(link: string): string {
let cleaned = link.replace(/\\/g, "");
const pidMatch = cleaned.match(/[?&]pid=([^&]+)/);
if (pidMatch) {
return "https://pay.walletconnect.com/" + pidMatch[1];
}
return cleaned;
}
const options = await client.getPaymentOptions({
paymentLink: normalizePaymentLink(paymentLink),
accounts: buildAccounts(walletAddress),
includePaymentInfo: true,
});
console.log("Merchant:", options.info?.merchant.name);
console.log("Amount:", options.info?.amount.display.assetSymbol);
console.log("Options:", options.options.length);
Signing with Turnkey
WalletConnect Pay returns RPC actions that the wallet must sign. For USDC payments, this is typically an eth_signTypedData_v4 action containing an ERC-3009 ReceiveWithAuthorization.
The key integration point: Turnkey’s signMessage with PAYLOAD_ENCODING_EIP712 handles the EIP-712 hashing server-side — you pass the raw typed data JSON string directly:
import { useTurnkey } from "@turnkey/react-native-wallet-kit";
async function signWcPayAction(
action: { walletRpc: { method: string; params: string } },
signMessage: Function,
walletAccount: any
): Promise<string> {
const { method, params } = action.walletRpc;
const parsedParams = JSON.parse(params);
if (method === "eth_signTypedData_v4") {
// parsedParams = [signerAddress, typedDataJSON]
const typedDataJson =
typeof parsedParams[1] === "string"
? parsedParams[1]
: JSON.stringify(parsedParams[1]);
// Turnkey handles EIP-712 hashing server-side
const result = await signMessage({
walletAccount,
message: typedDataJson,
addEthereumPrefix: false,
encoding: "PAYLOAD_ENCODING_EIP712",
hashFunction: "HASH_FUNCTION_NO_OP",
});
return assembleSignature(result);
}
if (method === "personal_sign") {
const messageHex = parsedParams[0];
const message = messageHex.startsWith("0x")
? Buffer.from(messageHex.slice(2), "hex").toString("utf8")
: messageHex;
const result = await signMessage({
walletAccount,
message,
});
return assembleSignature(result);
}
throw new Error(`Unsupported RPC method: ${method}`);
}
function assembleSignature(result: { r: string; s: string; v: string }): string {
const r = (result.r.startsWith("0x") ? result.r.slice(2) : result.r).padStart(64, "0");
const s = (result.s.startsWith("0x") ? result.s.slice(2) : result.s).padStart(64, "0");
let v = parseInt(result.v, 10);
if (v < 27) v += 27;
return `0x${r}${s}${v.toString(16).padStart(2, "0")}`;
}
The signMessage parameter names must be encoding and hashFunction — not encodingOverride or hashFunctionOverride. Using the wrong names will silently fall back to default encoding, producing a valid but incorrect signature.
Handling identity verification
Some payments require identity verification for Travel Rule compliance. Check for collectData on the selected payment option and show a WebView if present:
import { WebView } from "react-native-webview";
function IdentityVerification({ url, onComplete, onError }) {
const handleMessage = (event) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === "IC_COMPLETE") onComplete();
if (data.type === "IC_ERROR") onError(data.error);
} catch {}
};
return (
<WebView
source={{ uri: url }}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
/>
);
}
// In your payment flow:
if (selectedOption.collectData?.url) {
// Show WebView, wait for IC_COMPLETE, then proceed to signing
}
Confirming the payment
After signing all actions (and completing identity verification if required), submit the signatures to WalletConnect Pay:
// Get required signing actions
const actions = await client.getRequiredPaymentActions({
paymentId: options.paymentId,
optionId: selectedOption.id,
});
// Sign each action with Turnkey (maintain order)
const signatures = [];
for (const action of actions) {
const sig = await signWcPayAction(action, signMessage, walletAccount);
signatures.push(sig);
}
// Confirm payment — WC Pay handles gas and broadcast
const result = await client.confirmPayment({
paymentId: options.paymentId,
optionId: selectedOption.id,
signatures,
});
if (result.status === "succeeded") {
console.log("Payment confirmed on-chain!");
}
Putting it all together
Here’s the complete payment flow in a single component:
import { useTurnkey, ClientState } from "@turnkey/react-native-wallet-kit";
import { WalletConnectPay } from "@walletconnect/pay";
const client = new WalletConnectPay({
apiKey: process.env.EXPO_PUBLIC_WC_API_KEY,
});
export default function PaymentScreen({ paymentLink }) {
const { wallets, signMessage, clientState } = useTurnkey();
const ethAccount = wallets
?.flatMap((w) => w.accounts || [])
.find((a) => a.addressFormat === "ADDRESS_FORMAT_ETHEREUM");
async function handlePayment() {
// 1. Fetch payment options
const options = await client.getPaymentOptions({
paymentLink: normalizePaymentLink(paymentLink),
accounts: buildAccounts(ethAccount.address),
includePaymentInfo: true,
});
const selectedOption = options.options[0];
// 2. Handle identity verification if required
if (selectedOption.collectData?.url) {
await showIdentityWebView(selectedOption.collectData.url);
}
// 3. Get signing actions
const actions = await client.getRequiredPaymentActions({
paymentId: options.paymentId,
optionId: selectedOption.id,
});
// 4. Sign with Turnkey
const signatures = [];
for (const action of actions) {
const sig = await signWcPayAction(action, signMessage, ethAccount);
signatures.push(sig);
}
// 5. Confirm — WC Pay handles gas + broadcast
const result = await client.confirmPayment({
paymentId: options.paymentId,
optionId: selectedOption.id,
signatures,
});
return result;
}
}
Testing
You can test your integration using WalletConnect Pay’s built-in test flow:
- Go to the WalletConnect Dashboard → your wallet project → WalletConnect Pay tab
- Set a mock merchant receiving address in the Test section
- Generate a test payment link from the Point-of-Sale test app
- Scan or paste the link in your wallet app
Payments will arrive at your configured test address. No real merchant onboarding required.
Summary
✅ You’ve now learned how to:
- Authenticate users with Turnkey via email OTP and create embedded wallets
- Initialize a WalletConnect Pay client and fetch payment options from merchant QR codes
- Sign EIP-712 typed data (
ReceiveWithAuthorization) with Turnkey using PAYLOAD_ENCODING_EIP712
- Handle Travel Rule identity verification via WebView
- Confirm payments through WalletConnect Pay, which handles gas sponsorship and on-chain broadcast
For the full working example, see the with-wcpay repository.