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-walletconnect-pay 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.
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:
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, addEthereumPrefix: true, }); 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"); if (!result.v) throw new Error("Turnkey returned empty v value in signature"); 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.
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}