# Pay with WalletConnect Pay

Build a checkout flow that pays [WalletConnect Pay](https://docs.walletconnect.com/payments/wallets/overview) merchant links with an Openfort embedded wallet. The user pastes a payment link, picks a stablecoin option, and the embedded wallet signs the required actions — no extension, no seed phrase, and no backend.

With Openfort and WalletConnect Pay together, your app can:

* Let users paste a WalletConnect Pay link and complete the payment with their embedded wallet
* Pay across multiple chains — Base, Ethereum, Polygon, Arbitrum, Optimism, and BNB Smart Chain
* Settle in stablecoins (USDC, USDT, EURC, PYUSD, USDG); the gateway picks the source chain from the wallet's balances

:::info
WalletConnect Pay shares interchange revenue with wallets and pays `$WCT` cashback to users. See the [WalletConnect Pay overview](https://docs.walletconnect.com/payments/wallets/overview) for how to earn from your integration.
:::

:::tip
You'll build a React + Vite app where users sign in with an Openfort embedded wallet (passkey recovery), paste a WalletConnect Pay link, review the merchant and amount, and complete the payment by signing each action with their wallet.
:::

## How it works

WalletConnect Pay returns each payment step as a raw wallet RPC request — `eth_sendTransaction`, `eth_signTypedData_v4`, or `personal_sign`. The Openfort embedded wallet exposes a standard EIP-1193 provider through the wagmi connector, so each action is forwarded straight to the wallet: Openfort signs, WalletConnect Pay broadcasts and settles.

The flow has two phases, split by the user pressing **Pay**. Everything before it is read-only and reversible; everything after commits the chosen option.

```text
  Your app                          Openfort wallet        WalletConnect Pay
 ─────────────────────────────────────────────────────────────────────────────

 PHASE 1 · Browse  (read-only — nothing is committed)
 ───────────────────────────────────────────────────
   paste link
        │
        ▼
   pay.getPaymentOptions({ paymentLink, accounts }) ───────────────▶ gateway
        │                                            ◀─ options + expiry
        ▼
   user reviews options and picks one
        │
        ▼
   (optional) merchant data-collection iframe

 ══════════ user presses "Pay" · point of no return ══════════

 PHASE 2 · Execute  (runs once, for the chosen option)
 ───────────────────────────────────────────────────
   pay.getRequiredPaymentActions({ paymentId, optionId }) ─────────▶ gateway
        │                                              ◀─ N raw wallet RPCs
        ▼                                                 (e.g. approve, transfer)
   for each action:
        switch chain ─▶ provider.request(walletRpc) ─▶ Openfort signs
        │
        ▼
   pay.confirmPayment({ paymentId, optionId, signatures }) ────────▶ gateway
        │                                              ◀─ poll until isFinal
        ▼
   status === "succeeded"
```

## Getting started

:::note
* Node.js 22+
* [Openfort account](https://dashboard.openfort.io) with Shield credentials
* A [WalletConnect project](https://dashboard.walletconnect.com) — project ID and a **Pay** API key
* A passkey-capable browser (for wallet recovery)
:::

::::steps
### Install the dependencies

Add the SDKs to your React + Vite app:

```bash
pnpm add @openfort/react @walletconnect/pay wagmi viem @tanstack/react-query
```

This integration uses **passkey** wallet recovery, so — unlike most embedded-wallet setups — it needs no backend for Shield encryption sessions.

### Configure your Openfort credentials

Set up your project in the [Openfort Dashboard](https://dashboard.openfort.io):

1. Create an account or sign in
2. Get your **Publishable Key** (`pk_test_...`) from **Developers → API Keys**
3. Copy your **Shield Publishable Key** from the **Shield** section
4. (Optional) Create a **Gas Policy** to sponsor the on-chain payment and note the **gas sponsorship ID** (`pol_...`)

### Get your WalletConnect Pay credentials

In the [WalletConnect Dashboard](https://dashboard.walletconnect.com):

1. Copy your **Project ID**
2. In the **Pay** section, copy your **Pay API key** (`wcp_...`)

:::tip
The **Project ID** identifies your WalletConnect project (the same ID the wagmi WalletConnect connector uses) for attribution. The **Pay API key** authorizes calls to the Pay gateway — it is what lets `getPaymentOptions` and `confirmPayment` succeed. You provide both.
:::

### Configure your environment

Create a `.env` file in your project root with:

```bash
VITE_OPENFORT_PUBLISHABLE_KEY=pk_test_...
VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY=your-shield-publishable-key
VITE_OPENFORT_FEE_SPONSORSHIP_ID=pol_...        # Optional — sponsors the payment gas
VITE_WALLET_CONNECT_PROJECT_ID=your-walletconnect-project-id
VITE_WALLETCONNECT_PAY_API_KEY=wcp_...
```

### Build the payment flow

Configure the Openfort provider with Shield and passkey recovery — no encryption-session endpoint, so no backend:

```tsx
// src/Providers.tsx
<OpenfortProvider
  publishableKey={import.meta.env.VITE_OPENFORT_PUBLISHABLE_KEY}
  walletConfig={{
    shieldPublishableKey: import.meta.env.VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY,
    ethereum: {
      ethereumFeeSponsorshipId: import.meta.env.VITE_OPENFORT_FEE_SPONSORSHIP_ID || undefined,
    },
  }}
  uiConfig={{
    walletRecovery: {
      allowedMethods: [RecoveryMethod.PASSKEY],
      defaultMethod: RecoveryMethod.PASSKEY,
    },
  }}
>
  {children}
</OpenfortProvider>
```

A single WalletConnect Pay client talks to the gateway. `appId` is your project ID; `apiKey` is the Pay key:

```ts
// src/lib/pay.ts
import { WalletConnectPay } from '@walletconnect/pay'

const pay = new WalletConnectPay({
  appId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID,
  apiKey: import.meta.env.VITE_WALLETCONNECT_PAY_API_KEY,
})
```

First fetch the options and present them. `getPaymentOptions` is read-only, so nothing is committed yet — render `options` and let the user choose the stablecoin and chain they want to pay with, then keep that choice in state:

```ts
const { paymentId, options } = await pay.getPaymentOptions({ paymentLink, accounts })
if (options.length === 0) {
  throw new Error('No payment options for this wallet on the supported chains.')
}
// Render `options`; store the one the user selects. Don't request actions yet.
```

When the user commits by pressing **Pay**, request the actions for the selected option *once*. `getRequiredPaymentActions` is the point of no return — it locks in the option, so call it only after the user confirms, never while they're still browsing. Each action it returns is a raw wallet RPC: forward it to the Openfort wallet's EIP-1193 provider (from the wagmi connector), switching chains when an action targets a different network, then confirm:

```ts
// Runs once, when the user presses "Pay" on their selected option.
async function submitPayment(option: PaymentOption) {
  const actions = await pay.getRequiredPaymentActions({ paymentId, optionId: option.id })

  const provider = await connector.getProvider()
  const signatures: string[] = []
  for (const { walletRpc } of actions) {
    // walletRpc.method is eth_sendTransaction | eth_signTypedData_v4 | personal_sign
    await switchChainAsync({ chainId: chainIdFromCaip2(walletRpc.chainId) })
    const signature = await provider.request({
      method: walletRpc.method,
      params: JSON.parse(walletRpc.params),
    })
    signatures.push(signature as string)
  }

  let result = await pay.confirmPayment({ paymentId, optionId: option.id, signatures })
  while (!result.isFinal) {
    await new Promise((r) => setTimeout(r, result.pollInMs ?? 2500))
    result = await pay.confirmPayment({ paymentId, optionId: option.id, signatures })
  }
  if (result.status !== 'succeeded') {
    throw new Error(`Payment ${result.status}.`)
  }
}
```

Here `connector` is your active wagmi connector, `switchChainAsync` comes from wagmi's `useSwitchChain`, and `chainIdFromCaip2` is a small helper that parses the numeric chain ID out of a CAIP-2 string like `eip155:8453`. A payment can span more than one action — for example an ERC-20 approval followed by the transfer — and those actions may live on different chains, which is why each iteration switches the wallet to `walletRpc.chainId` before signing. Wrap `getPaymentOptions` and the signing loop in `try/catch` so you can show a failure state for an invalid or expired link, no options, a rejected signature, or any non-`succeeded` terminal status.

:::warning
Payment links expire. `getPaymentOptions` returns `info.expiresAt`; signing after it passes makes `confirmPayment` fail. Surface the deadline and prompt the user to request a fresh link when time runs low.
:::

### Handle merchant data collection (optional)

Some merchants require extra details (for example a shipping address) before the payment can proceed. When the selected option carries a `collectData.url`, render it in an iframe — WalletConnect hosts the form and reports the outcome over `postMessage`:

```tsx
// src/components/CollectDataFrame.tsx
useEffect(() => {
  const onMessage = (event: MessageEvent) => {
    const data = event.data as { type?: string; message?: string }
    if (data?.type === 'IC_COMPLETE') onComplete() // proceed to signing
    else if (data?.type === 'IC_ERROR') onError(data.message ?? 'Verification failed.')
  }
  window.addEventListener('message', onMessage)
  return () => window.removeEventListener('message', onMessage)
}, [onComplete, onError])

return <iframe title="Merchant data collection" src={collectData.url} />
```

The iframe submits the collected data directly to WalletConnect, so once `IC_COMPLETE` arrives you continue to `getRequiredPaymentActions` and sign as usual.

### Test the flow

Start your dev server, sign in with the embedded wallet, then paste a WalletConnect Pay link. Don't have one? Create a test payment with the **WalletConnect Dashboard POS tool**.
::::

## Supported chains

WalletConnect Pay settles in stablecoins (USDC, USDT, EURC, PYUSD, USDG) and picks the source chain from the wallet's balances. You choose which chains to offer: define a `SUPPORTED_CHAINS` constant, advertise the connected wallet as a CAIP-10 account (`eip155:<chainId>:<address>`) on each, and pass that list as `accounts` to `getPaymentOptions`.

Common EVM chains use their standard IDs — for example Base (`eip155:8453`), Ethereum (`eip155:1`), Polygon (`eip155:137`), Arbitrum (`eip155:42161`), Optimism (`eip155:10`), and BNB Smart Chain (`eip155:56`). WalletConnect Pay supports many more networks and tokens than any single integration enables.

For the full, authoritative list, see WalletConnect's [Token and chain coverage](https://docs.walletconnect.com/payments/token-and-chain-coverage).

## Payment types

The fields you read from `@walletconnect/pay` responses:

```ts
// Returned by getPaymentOptions
interface PaymentOptionsResponse {
  paymentId: string
  info?: PaymentInfo // info.merchant.name, info.amount, info.expiresAt
  options: PaymentOption[]
}

interface PaymentOption {
  id: string
  amount: PayAmount // amount.display.networkName, amount.display.iconUrl
  etaS: number // estimated settlement time, seconds
  collectData?: { url: string } // present when the merchant needs extra details
}

// Returned by getRequiredPaymentActions — one entry per signature
interface Action {
  walletRpc: {
    chainId: string // CAIP-2, e.g. "eip155:8453"
    method: string // eth_sendTransaction | eth_signTypedData_v4 | personal_sign
    params: string // JSON-encoded argument array
  }
}

// Returned by confirmPayment
interface ConfirmPaymentResponse {
  status: 'requires_action' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'cancelled'
  isFinal: boolean
  pollInMs?: number
  info?: { txId?: string }
}
```

## Next steps

* [WalletConnect Pay documentation](https://docs.walletconnect.com/payments/wallets/overview)
* [Embedded Wallet Guide](/docs/products/embedded-wallet)
* [Wallet Recovery](/docs/products/embedded-wallet)
* [Gas Sponsorship](/docs/configuration/gas-sponsorship)
