Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.walletconnect.com/llms.txt

Use this file to discover all available pages before exploring further.

The simple WalletConnect Pay flow signs an EIP-3009 transferWithAuthorization typed-data payload — the gasless transfer used by USDC and similar stablecoins. USDT does not implement EIP-3009, so WalletConnect Pay routes it through the Permit2 contract instead. The first time a user pays a given token on a given chain, the wallet must approve Permit2 on-chain. Subsequent payments on the same token only require the typed-data signature.

The two-action flow

When an option needs a Permit2 approval, getRequiredPaymentActions returns two actions — in the order they must execute:
  1. eth_sendTransaction — ERC-20 approve(Permit2, amount) on the token contract
  2. eth_signTypedData_v4 — Permit2 typed-data payload authorizing the transfer
signatures[] passed to confirmPayment must match actions[] in order and length — the approve tx hash first, then the Permit2 signature.

Detecting an approval-required option

Inspect the action list for an eth_sendTransaction entry. In standalone SDK flows, this means option.actions; in WalletKit flows, this means the actions returned by getRequiredPaymentActions. If one is present, the option is a Permit2 flow and your wallet should prepare for an on-chain step.
// WalletKit: inspect the actions returned by getRequiredPaymentActions
fun requiresApproval(requiredActions: List<Wallet.Model.RequiredAction>?): Boolean =
    requiredActions
        ?.filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()
        ?.any { it.action.method == "eth_sendTransaction" }
        ?: false

Showing the approval gas estimate

The approve tx costs native gas (e.g. POL on Polygon, ETH on mainnet) — separate from the token amount the user is paying. Estimate it and show the user the expected fee before they confirm so they aren’t surprised by a wallet prompt for a one-time on-chain cost. This can live wherever your wallet collects user intent — a review screen, a confirmation sheet, or inline on the option row.
suspend fun estimateApprovalFee(
    chainId: String,
    txParams: JSONObject,
): BigInteger? = runCatching {
    val gasLimit = rpcEstimateGas(chainId, txParams)
    val maxFeePerGas = fetchFeeData(chainId).maxFeePerGas
    gasLimit.multiply(maxFeePerGas)
}.getOrNull()
If estimation fails (RPC error, missing fields, etc.), don’t block the flow. Fall back to a generic message such as “Network fee set by wallet” — the wallet’s signing prompt will still surface the actual cost when the user confirms.

Executing the actions in order

Because the Permit2 typed data signs against the token allowance the approve tx grants, the approve must be mined before you sign.
  1. Submit eth_sendTransaction (action 1) and wait for the receipt.
  2. Parse the typed data from eth_signTypedData_v4 (action 2) and sign it.
  3. Push both results into signatures[] in the order the actions were returned.
  4. Call confirmPayment(signatures).
val signatures = mutableListOf<String>()

for (action in actions.filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()) {
    signatures += when (action.action.method) {
        "eth_sendTransaction" -> {
            val txHash = wallet.sendTransaction(action.action.chainId, action.action.params)
            wallet.awaitReceipt(action.action.chainId, txHash)
            txHash
        }
        "eth_signTypedData_v4" ->
            wallet.signTypedData(action.action.chainId, action.action.params)
        else -> error("Unsupported method: ${action.action.method}")
    }
}
EIP-712 library quirks. EIP-712 signing libraries disagree on whether types.EIP712Domain must be present in the payload. The Permit2 typed data returned by WalletConnect Pay omits it.
  • Libraries that require EIP712Domain (e.g. eth_sig_util_plus on Flutter, some Android libraries): synthesize an EIP712Domain entry in types from the fields actually present in domain before signing.
  • Libraries that reject EIP712Domain (e.g. ethers v5 _signTypedData): strip it from types before signing.
  • Yttrium’s signTypedData is currently hardcoded to ERC-3009 (from/to/value/validAfter/validBefore/nonce) and rejects Permit2. Wallets using Yttrium need a generic EIP-712 hasher — see EIP712TypedData.swift for a reference implementation.
If your signing library throws on a Permit2 typed-data payload, normalize the payload before retrying.

Loader UX

The two-action flow has two distinct user-visible steps that can each take several seconds:
  1. The approve tx is broadcast and you wait for the receipt.
  2. The user is prompted to sign the Permit2 typed data.
Show different loader copy for each step so the user understands what is happening — especially during the on-chain wait, which is much slower than a typed-data signature on its own. Pick whatever wording fits your wallet’s voice. Follow these patterns to provide the best user experience when implementing USDT and Permit2-based payments:

Pre-load fee estimates

Preload per-option fee estimates before user selection. This allows users to make informed decisions by comparing realistic gas costs across different payment options.

Display one-time setup messaging

When approval actions are present (first-time Permit2 setup), clearly communicate to users that this is a one-time on-chain approval. Subsequent payments with the same token will only require a signature.

Separate gas cost display

Explain native-token gas costs separately from the token amounts being paid. Users should understand that:
  • The token amount goes to the merchant
  • The gas fee (in the chain’s native token) goes to network validators

Remember user preferences

Retain and auto-select the last-paid token when it’s available in future payments. This reduces friction for repeat users who prefer specific payment methods.

Guard against stale data

Implement safeguards against stale data from rapid option switching and expired payment sessions. Always validate that payment data is current before executing actions.

Validate your integration

Before shipping your USDT support implementation, verify these scenarios work correctly:
1

First-time ERC-20 with Permit2

Test a payment with a token requiring Permit2 approval for the first time. The flow should:
  • Present the approval transaction first
  • Wait for on-chain confirmation
  • Then prompt for the Permit2 signature
  • Complete successfully with both results in signatures[]
2

Repeat ERC-20 payment

After completing the first-time flow, make another payment with the same token. This should:
  • Skip the approval transaction entirely
  • Only require a single Permit2 signature
  • Complete faster than the first-time flow
3

Native token payment

Test a payment using the chain’s native token (e.g., ETH on Base). Verify that:
  • The transaction hash is correctly included in signatures[]
  • The payment completes successfully
4

Rapid option switching

Quickly switch between payment options and verify:
  • No stale data leaks between options
  • Fee estimates update correctly for each option
  • The correct actions execute for the final selected option
5

Expired payment handling

Test behavior when a payment session expires:
  • Before any signatures are submitted
  • The wallet should handle expiration gracefully
  • Users should receive clear feedback about the expiration

Sample wallets

The four reference wallets below all implement this two-action flow and are good starting points to copy from.

React Native

wallets/rn_cli_wallet — see PaymentUtil.ts, PaymentTransactionUtil.ts, PaymentStore.ts.

Kotlin

sample/wallet — see PaymentUtil.kt, PaymentTransactionUtil.kt, PaymentViewModel.kt.

Swift

Example/WalletApp — see PaymentUtil.swift, PayTransactionService.swift, PayPresenter.swift, EIP712TypedData.swift.

Flutter

packages/reown_walletkit/example — see evm_service.dart, wcp_payment_details.dart, wcp_confirming_payment.dart.