> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pods.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Privy Wrapper

> Iframe wrapper using Privy authentication with embedded smart wallets and multi-chain support

This guide covers building an iframe wrapper with **Privy** for authentication (email OTP), embedded wallets, and multi-chain smart wallet support across 6 chains.

<Info>
  For the complete one-shot LLM prompt, see [`/llms-iframe-privy-multichain.txt`](/llms-iframe-privy-multichain.txt).
</Info>

***

## Tech Stack

* **React** (19+) with TypeScript
* **Vite** (6+) as build tool
* **Tailwind CSS v4** for styling
* **@privy-io/react-auth** — authentication, embedded wallets, and smart wallets
* **permissionless** `"^0.3.3"` — required by Privy's smart wallet internals
* **viem** — Ethereum interactions
* **vite-plugin-node-polyfills** — Privy requires Node.js polyfills in Vite

***

## Critical Import Paths

| What                  | Import path                          | Exported name                         |
| --------------------- | ------------------------------------ | ------------------------------------- |
| Smart wallet provider | `@privy-io/react-auth/smart-wallets` | `SmartWalletsProvider`                |
| Smart wallet hook     | `@privy-io/react-auth/smart-wallets` | `useSmartWallets`                     |
| Per-chain client      | `@privy-io/react-auth/smart-wallets` | `useSmartWallets().getClientForChain` |
| Auth provider         | `@privy-io/react-auth`               | `PrivyProvider`                       |
| usePrivy              | `@privy-io/react-auth`               | `usePrivy`                            |
| Email login           | `@privy-io/react-auth`               | `useLoginWithEmail`                   |
| Create wallet         | `@privy-io/react-auth`               | `useCreateWallet`                     |

***

## Privy Provider Setup

```tsx theme={null}
import { PrivyProvider } from '@privy-io/react-auth'
import { SmartWalletsProvider } from '@privy-io/react-auth/smart-wallets'
import { ethereum, polygon, base, arbitrum, gnosis, hyperEvm } from './config/chains'

<PrivyProvider
  appId={PRIVY_APP_ID}
  config={{
    appearance: { theme: 'dark' },
    embeddedWallets: {
      showWalletUIs: false,
      ethereum: {
        createOnLogin: 'users-without-wallets',
      },
    },
    defaultChain: polygon,
    supportedChains: [ethereum, polygon, base, arbitrum, gnosis, hyperEvm],
  }}
>
  <SmartWalletsProvider>
    {children}
  </SmartWalletsProvider>
</PrivyProvider>
```

***

## Configuration Gotchas

### 1. `showWalletUIs: false`

Privy's default shows a transaction simulation modal before every `sendTransaction`. Disable it when the wrapper handles the full tx flow.

### 2. Chain RPC URLs must use authenticated providers

Privy uses the chain's `rpcUrls.default` during Safe initialization. If it rate-limits, the smart wallet client **silently stays `undefined`**. Use Alchemy endpoints for supported chains.

### 3. Smart wallets are counterfactual

The address is valid before the contract exists on-chain. Do **not** call `getCode()` or send dummy transactions — Privy deploys atomically on first real tx.

### 4. `useCreateWallet()` fallback

`createOnLogin` only fires during the login flow. Use `useCreateWallet()` as a runtime fallback for existing users who don't have an embedded wallet yet.

### 5. Use `getClientForChain` for cross-chain tx

<Warning>
  **This is the #1 multi-chain runtime bug.** The `client` from `useSmartWallets()` is bound to `defaultChain` (Polygon). Calling `smartWalletClient.sendTransaction()` directly **always routes through the Polygon bundler**, regardless of the bytecode's `chainId`.
</Warning>

```typescript theme={null}
const { client: smartWalletClient, getClientForChain } = useSmartWallets()

// For every transaction, resolve the chain-specific client:
const chainId = bytecodes[0]?.chainId ?? 137
const chainClient = await getClientForChain({ id: chainId })
const txHash = await chainClient.sendTransaction({ calls })
```

### 6. Privy Dashboard configuration

Each chain must have a bundler URL and Policy ID configured in the Privy Dashboard. Without this, `getClientForChain` returns `undefined`. See the [Privy Integration guide](/external-integrations/privy-integration) for detailed dashboard setup instructions.

***

## processBytecode Implementation

```typescript theme={null}
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'
import { createPublicClient, http } from 'viem'
import { getChainById, getRpcUrl } from '@/config/chains'

const { client: smartWalletClient, getClientForChain } = useSmartWallets()

async function processBytecode(clientTxId: string, bytecodes: BytecodeTransaction[]) {
  if (!smartWalletClient) return

  sendToIframe({ type: 'HOST_ACK', clientTxId })
  sendToIframe({ type: 'SIGNATURE_PROMPTED', clientTxId })

  // Determine chain from first bytecode
  const chainId = bytecodes[0]?.chainId ?? 137

  // CRITICAL: use getClientForChain, not smartWalletClient directly
  const chainClient = await getClientForChain({ id: chainId })
  if (!chainClient) {
    sendToIframe({
      type: 'SIGNATURE_ERROR', clientTxId,
      code: 'CHAIN_NOT_CONFIGURED',
      message: `Chain ${chainId} is not configured in the Privy Dashboard.`,
    })
    return
  }

  const calls = bytecodes.map((bc) => ({
    to: bc.to as `0x${string}`,
    data: bc.data as `0x${string}`,
    value: bc.value ? BigInt(bc.value) : 0n,
  }))

  const txHash = await chainClient.sendTransaction({ calls })

  sendToIframe({ type: 'SIGNED', clientTxId })
  sendToIframe({ type: 'TX_SUBMITTED', clientTxId, chainId, txHash })

  // Wait for confirmation using chain-specific public client
  const publicClient = createPublicClient({
    chain: getChainById(chainId)!,
    transport: http(getRpcUrl(chainId)),
  })
  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash })

  sendToIframe({
    type: 'TX_CONFIRMED', clientTxId,
    txHash: receipt.transactionHash,
    blockNumber: Number(receipt.blockNumber),
    confirmations: 1,
  })
  sendToIframe({ type: 'TX_FINALIZED', clientTxId, txHash: receipt.transactionHash })
}
```

***

## Environment Variables

```env theme={null}
VITE_WEBVIEW_URL=https://iframe.pods.finance
VITE_ALCHEMY_RPC_ID=your_alchemy_api_key
VITE_ALCHEMY_POLICY_ID=your_alchemy_gas_policy_id
VITE_PODS_API_URL=https://api.pods.finance
VITE_PODS_API_KEY=your_pods_api_key
VITE_ALLOWED_ORIGINS=*
VITE_APP_PRIVY_APP_ID=your_privy_app_id
```

***

## Documentation References

* [Iframe Overview](/iframe/overview) — Protocol spec, query params, postMessage types
* [wagmi + viem Wrapper](/iframe/wagmi-viem) — Alternative wrapper with browser wallets
* [Privy Integration (Widgets)](/external-integrations/privy-integration) — Privy with React SDK widgets
* [Privy docs](https://docs.privy.io)
* [viem docs](https://viem.sh)
