> ## 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 Integration

> Build Pods widget hosts with Privy embedded smart wallets

Use Privy when you want Pods widgets to feel like a Web2 product while still keeping wallet custody and transaction execution in your host app.

## Resources

| Resource                                                   | Description                                                       |
| ---------------------------------------------------------- | ----------------------------------------------------------------- |
| [Pods Docs](https://docs.pods.finance/)                    | Official documentation for Pods widgets, strategies, and tx flows |
| [Privy Wallets](https://docs.privy.io/wallets)             | Privy wallets, embedded wallets, and smart-wallet documentation   |
| [EarnWidget Integration](/widgets/earn-widget)             | Current SDK widget contract for Earn                              |
| [SwapWidget Integration](/widgets/swap-widget)             | Current SDK widget contract for Swap                              |
| [LLM Privy Integration Guide](/llms-privy-integration.txt) | Canonical 1-shot prompt for AI-assisted host integration          |

## Verified integration contract

Install the current verified package set:

```bash theme={null}
pnpm add pods-sdk@0.2.87 @deframe-sdk/components@0.1.91 @privy-io/react-auth@3.33.1 permissionless@0.2.57 viem@2.52.0 @reduxjs/toolkit@^2 react-redux@^9 redux@^5
```

<Note>
  Do not install `@privy-io/react-auth/smart-wallets` as a separate dependency. Import it from the `@privy-io/react-auth` package subpath.
</Note>

## Environment variables

```bash theme={null}
NEXT_PUBLIC_PODS_API_URL=https://api.pods.finance
NEXT_PUBLIC_PODS_API_KEY=your_api_key
NEXT_PUBLIC_PODS_WEBSOCKET_URL=wss://api.pods.finance/updates
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id
```

## Styles

Import the widget stylesheet before your host CSS.

For Next.js App Router:

```tsx theme={null}
// app/layout.tsx
import '@deframe-sdk/components/styles.css'
import './globals.css'
```

For Vite React:

```tsx theme={null}
// src/main.tsx
import '@deframe-sdk/components/styles.css'
import './index.css'
```

For Vite hosts, keep the same names by enabling:

```ts theme={null}
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  envPrefix: ['VITE_', 'NEXT_PUBLIC_'],
  resolve: {
    alias: {
      'next/link': fileURLToPath(new URL('./src/shims/next-link.tsx', import.meta.url)),
    },
  },
})
```

For non-Next hosts, add the shim used by that alias:

```tsx theme={null}
import type { AnchorHTMLAttributes, PropsWithChildren } from 'react'

type NextLinkShimProps = PropsWithChildren<
  AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }
>

export default function Link({ href, children, ...props }: NextLinkShimProps) {
  return (
    <a href={href} {...props}>
      {children}
    </a>
  )
}
```

## Provider setup

```tsx theme={null}
import { PrivyProvider } from '@privy-io/react-auth'
import { SmartWalletsProvider } from '@privy-io/react-auth/smart-wallets'
import { defineChain, type Chain } from 'viem'
import { arbitrum, base, baseSepolia, bsc, gnosis, mainnet, optimism, polygon } from 'viem/chains'
import type { ReactNode } from 'react'

const hyperEvm = defineChain({
  id: 999,
  name: 'HyperEVM',
  nativeCurrency: {
    decimals: 18,
    name: 'HYPE',
    symbol: 'HYPE',
  },
  rpcUrls: {
    default: {
      http: ['https://rpc.hyperliquid.xyz/evm'],
    },
  },
  blockExplorers: {
    default: {
      name: 'HyperEVM Explorer',
      url: 'https://hyperevmscan.io',
    },
  },
})

const PODS_SUPPORTED_CHAINS: Chain[] = [
  mainnet,
  optimism,
  bsc,
  polygon,
  base,
  baseSepolia,
  arbitrum,
  gnosis,
  hyperEvm,
]

const PODS_DEFAULT_CHAIN =
  PODS_SUPPORTED_CHAINS.find((chain) => chain.id === polygon.id) ?? PODS_SUPPORTED_CHAINS[0]

export function PrivyProviders({ children }: { children: ReactNode }) {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID ?? ''}
      config={{
        embeddedWallets: {
          ethereum: {
            createOnLogin: 'all-users',
          },
        },
        supportedChains: PODS_SUPPORTED_CHAINS,
        defaultChain: PODS_DEFAULT_CHAIN,
        appearance: {
          walletChainType: 'ethereum-only',
        },
      }}
    >
      <SmartWalletsProvider>{children}</SmartWalletsProvider>
    </PrivyProvider>
  )
}
```

## Host integration pattern

Inside your widget host component:

* Use `usePrivy`, `useWallets`, `useCreateWallet`, `getEmbeddedConnectedWallet`, and `useSmartWallets`.
* Reuse the current embedded wallet when it already exists.
* If the user is authenticated but still has no smart wallet, show a “Create smart wallet” action before mounting the widget.
* Keep Privy `supportedChains` in sync with every EVM chain that Pods bytecode can target; `sendTransaction`, `switchChain`, and `getClientForChain({ id })` fail when the target chain is not supported.
* Pass the smart-wallet address into `DeframeProvider.config.walletAddress`.
* Use the branded `NEXT_PUBLIC_PODS_API_URL`, `NEXT_PUBLIC_PODS_API_KEY`, and `NEXT_PUBLIC_PODS_WEBSOCKET_URL` env names.
* Map those env values into the current `DeframeProvider` API config fields.
* Set `enableCrossChainInvestments: true`.

## Complete `EarnWidgetHost` reference implementation

If you want a full working host example instead of just the smaller integration fragments, use this as a reference implementation:

```tsx theme={null}
'use client'

import { useCallback, useMemo, useState } from 'react'
import {
  PodsSwapFormView,
} from '@deframe-sdk/components'
import {
  DeframeProvider,
  EarnWidget,
  type DeframeProviderProps,
  type TxUpdateEvent,
} from 'pods-sdk'
import {
  getEmbeddedConnectedWallet,
  useCreateWallet,
  usePrivy,
  useWallets,
} from '@privy-io/react-auth'
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'

const ROUTE_TO_STATE: Record<string, string> = {
  history: 'History',
  wallet: 'Wallet',
  swap: 'Swap',
  earn: 'Earn',
  overview: 'Overview',
  details: 'Details',
  deposit: 'Deposit',
  withdraw: 'Withdraw',
  'investment-details': 'Investment Details',
  'history-deposit-details': 'Deposit History Details',
  'history-withdraw-details': 'Withdraw History Details',
  'history-swap': 'Swap History',
  'history-swap-details': 'Swap History Details',
}

type ProcessBytecode = NonNullable<DeframeProviderProps['processBytecode']>

type RawBytecode = {
  chainId?: number | string
  to: string
  data: string
  value: string
  gasLimit?: string
}

type PayloadShape = {
  clientTxId: string
  bytecodes: RawBytecode[]
  simulateError?: boolean
}

type Eip1193Provider = {
  request: (args: { method: string; params?: unknown[] }) => Promise<unknown>
}

type PodsSmartWalletClient = {
  sendTransaction: (input: {
    calls: Array<{ to: string; data: string; value?: bigint }>
  }) => Promise<string>
}

type SmartWalletData = {
  client?: { account?: { address?: string } } & PodsSmartWalletClient
  getClientForChain?: (
    input: { id: number },
  ) => PodsSmartWalletClient | Promise<PodsSmartWalletClient | null | undefined> | null | undefined
}

type ActiveWalletShape = {
  chainId?: string | number
  address?: string
  walletClientType?: string
  switchChain?: (chainId: number) => Promise<void>
  getEthereumProvider?: () => Promise<Eip1193Provider | undefined>
  getProvider?: () => Promise<Eip1193Provider | undefined>
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function normalizeWalletChainId(chainId?: string | number): number | undefined {
  if (typeof chainId === 'number' && Number.isInteger(chainId) && chainId > 0) {
    return chainId
  }

  if (typeof chainId === 'string') {
    const [scope, raw] = chainId.split(':')
    if (scope === 'eip155' && raw) {
      const parsed = Number.parseInt(raw, 10)
      return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
    }
    const parsed = Number.parseInt(raw ?? chainId, 10)
    return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
  }

  return undefined
}

function toBigIntValue(value?: string): bigint | undefined {
  if (!value) return 0n

  const normalized = value.trim()
  if (!normalized || normalized === '0') return 0n

  try {
    return BigInt(normalized)
  } catch {
    return undefined
  }
}

function isSignatureRejected(error: unknown): boolean {
  const maybeError = error as { code?: number | string; message?: string }
  const code = maybeError?.code
  const message = String(maybeError?.message || '').toLowerCase()
  return code === 4001 || code === 'ACTION_REJECTED' || message.includes('rejected')
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

async function waitForReceipt(
  provider: Eip1193Provider,
  txHash: string,
): Promise<{ status?: string | number }> {
  const startedAt = Date.now()
  while (Date.now() - startedAt < 120_000) {
    const receipt = await provider.request({ method: 'eth_getTransactionReceipt', params: [txHash] })
    if (isRecord(receipt) && receipt.status != null) {
      return receipt as { status?: string | number }
    }
    await sleep(1500)
  }
  throw new Error('Timed out waiting for transaction confirmation')
}

async function resolveWalletProvider(wallet: ActiveWalletShape): Promise<Eip1193Provider | undefined> {
  if (wallet.getEthereumProvider) {
    const provider = await wallet.getEthereumProvider()
    if (provider) return provider
  }

  if (wallet.getProvider) {
    return wallet.getProvider()
  }

  return undefined
}

export function EarnWidgetHost() {
  const { ready, authenticated, login, logout, user } = usePrivy()
  const { wallets } = useWallets()
  const smartWalletData = useSmartWallets() as SmartWalletData
  const { createWallet } = useCreateWallet()
  const { client: smartWalletClient, getClientForChain } = smartWalletData

  const [routeName, setRouteName] = useState('Overview')
  const [txLogs, setTxLogs] = useState<string[]>([])
  const [isCreatingSmartWallet, setIsCreatingSmartWallet] = useState(false)

  const embeddedWallet = getEmbeddedConnectedWallet(wallets)
  const activeWallet = embeddedWallet ?? wallets[0]
  const smartWalletAddress = smartWalletClient?.account?.address
  const hasEmbeddedWallet = Boolean(embeddedWallet?.address)
  const hasSmartWalletSession = Boolean(smartWalletAddress && smartWalletClient)
  const podsApiUrl = process.env.NEXT_PUBLIC_PODS_API_URL
  const podsApiKey = process.env.NEXT_PUBLIC_PODS_API_KEY
  const podsWebSocketUrl = process.env.NEXT_PUBLIC_PODS_WEBSOCKET_URL

  const routeNavigate = useCallback((route: string) => {
    setRouteName((current) => {
      const mapped = ROUTE_TO_STATE[route] ?? route
      return current === mapped ? current : mapped
    })
  }, [])

  const logEvent = useCallback((entry: string) => {
    const timestamp = new Date().toLocaleTimeString()
    setTxLogs((previous) => [`${timestamp} ${entry}`, ...previous].slice(0, 24))
  }, [])

  const createSmartWallet = useCallback(async () => {
    if (!createWallet) {
      logEvent('Create wallet flow unavailable: missing createWallet hook.')
      return
    }

    if (hasEmbeddedWallet) {
      logEvent('Embedded wallet already exists. Waiting for the smart-wallet session.')
      return
    }

    setIsCreatingSmartWallet(true)
    try {
      await createWallet()
      logEvent('Smart wallet creation requested.')
    } catch (error) {
      const msg = String((error as { message?: string })?.message || 'Failed to create smart wallet')
      logEvent(`Create wallet failed: ${msg}`)
    } finally {
      setIsCreatingSmartWallet(false)
    }
  }, [createWallet, hasEmbeddedWallet, logEvent])

  const processBytecode = useCallback<ProcessBytecode>(async (payload: PayloadShape, ctx) => {
    const rawTransactions: RawBytecode[] = payload.bytecodes

    const emit = (event: TxUpdateEvent): void => {
      ctx.updateTxStatus(event)

      const detail =
        'txHash' in event && event.txHash
          ? ` tx=${String(event.txHash).slice(0, 10)}...`
          : ''
      const extra =
        'code' in event && event.code
          ? ` code=${String(event.code)}`
          : ''
      const message =
        'message' in event && event.message
          ? ` message=${String(event.message)}`
          : ''
      logEvent(`${event.type}${detail}${extra}${message}`)
    }

    if (!ready || !authenticated || !activeWallet || !hasSmartWalletSession) {
      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: 'NO_SMART_WALLET_SESSION',
        message: 'No Privy smart-wallet session is ready.',
      })
      return
    }

    if (activeWallet?.walletClientType !== 'privy') {
      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: 'UNSUPPORTED_WALLET',
        message: 'This integration only accepts Privy embedded smart-wallet sessions.',
      })
      return
    }

    if (payload.simulateError) {
      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: 'SIMULATED_ERROR',
        message: 'simulateError=true',
      })
      return
    }

    let hasSubmittedTx = false

    try {
      const provider = await resolveWalletProvider(activeWallet)
      if (!provider) {
        emit({
          type: 'SIGNATURE_ERROR',
          clientTxId: payload.clientTxId,
          code: 'NO_PROVIDER',
          message: 'No wallet provider available for signatures.',
        })
        return
      }

      if (!smartWalletClient && !getClientForChain) {
        emit({
          type: 'SIGNATURE_ERROR',
          clientTxId: payload.clientTxId,
          code: 'UNSUPPORTED_WALLET',
          message: 'No smart wallet client available.',
        })
        return
      }

      if (!smartWalletAddress) {
        emit({
          type: 'SIGNATURE_ERROR',
          clientTxId: payload.clientTxId,
          code: 'UNSUPPORTED_WALLET',
          message: 'No smart wallet address available for this session.',
        })
        return
      }

      emit({ type: 'HOST_ACK', clientTxId: payload.clientTxId })

      const grouped = new Map<number, RawBytecode[]>()
      const fallbackChainId = normalizeWalletChainId(activeWallet.chainId)

      for (const tx of rawTransactions) {
        const chainId = normalizeWalletChainId(tx.chainId) ?? fallbackChainId
        if (!chainId) {
          emit({
            type: 'SIGNATURE_ERROR',
            clientTxId: payload.clientTxId,
            code: 'INVALID_CHAIN',
            message: 'Missing or invalid chainId in processBytecode payload.',
          })
          return
        }

        const current = grouped.get(chainId) ?? []
        current.push(tx)
        grouped.set(chainId, current)
      }

      for (const [chainId, transactions] of grouped) {
        if (activeWallet.switchChain) {
          const currentChain = normalizeWalletChainId(activeWallet.chainId)
          if (currentChain !== chainId) {
            await activeWallet.switchChain(chainId)
          }
        }

        const client = await Promise.resolve(
          typeof getClientForChain === 'function'
            ? getClientForChain({ id: chainId })
            : smartWalletClient,
        )

        if (!client) {
          emit({
            type: 'SIGNATURE_ERROR',
            clientTxId: payload.clientTxId,
            code: 'UNSUPPORTED_WALLET',
            message: `Smart wallet client is not initialized for chainId=${chainId}.`,
          })
          return
        }

        const calls = transactions.map((tx) => {
          const normalizedValue = toBigIntValue(tx.value)
          return {
            to: tx.to,
            data: tx.data,
            ...(normalizedValue && normalizedValue > 0n ? { value: normalizedValue } : {}),
          }
        })

        for (const _tx of transactions) {
          emit({
            type: 'SIGNATURE_PROMPTED',
            clientTxId: payload.clientTxId,
          })
        }

        const txHash = await client.sendTransaction({ calls })
        hasSubmittedTx = true

        for (const _tx of transactions) {
          emit({
            type: 'TX_SUBMITTED',
            clientTxId: payload.clientTxId,
            chainId,
            txHash,
          })
        }

        const receipt = await waitForReceipt(provider, txHash)
        if (receipt.status === '0x0' || receipt.status === 0) {
          for (const _tx of transactions) {
            emit({ type: 'TX_REVERTED', clientTxId: payload.clientTxId })
          }
          return
        }

        for (const _tx of transactions) {
          emit({ type: 'TX_CONFIRMED', clientTxId: payload.clientTxId })
        }
      }

      emit({ type: 'TX_FINALIZED', clientTxId: payload.clientTxId })
    } catch (error) {
      if (isSignatureRejected(error)) {
        emit({ type: 'SIGNATURE_DECLINED', clientTxId: payload.clientTxId })
        return
      }

      const maybeError = error as { code?: number | string; message?: string }

      if (hasSubmittedTx) {
        emit({
          type: 'TX_FAILED',
          clientTxId: payload.clientTxId,
          code: String(maybeError.code ?? 'UNKNOWN_ERROR'),
          message: String(maybeError.message ?? 'Failed to execute smart-wallet transaction'),
        })
        return
      }

      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: String(maybeError.code ?? 'UNKNOWN_ERROR'),
        message: String(maybeError.message ?? 'Failed to execute smart-wallet transaction'),
      })
    }
  }, [activeWallet, authenticated, getClientForChain, hasSmartWalletSession, logEvent, ready, smartWalletAddress, smartWalletClient])

  const config = useMemo(
    () => ({
      // Next.js client example. In Vite, replace process.env with import.meta.env.
      // pods-sdk@0.2.87 still expects these provider field names.
      DEFRAME_API_URL: podsApiUrl,
      DEFRAME_API_KEY: podsApiKey,
      DEFRAME_WEBSOCKET_URL: podsWebSocketUrl,
      walletAddress: smartWalletAddress,
      userId: user?.id,
      globalCurrency: 'USD' as const,
      globalCurrencyExchangeRate: 1,
      enableCrossChainInvestments: true,
      components: {
        SwapFormView: PodsSwapFormView,
      },
      theme: {
        mode: 'dark' as const,
        preset: 'default' as const,
      },
      debug: process.env.NODE_ENV === 'development',
    }),
    [podsApiKey, podsApiUrl, podsWebSocketUrl, smartWalletAddress, user?.id],
  )

  const missingConfig = !podsApiUrl || !podsApiKey
  const showWalletAction = ready && authenticated && !hasSmartWalletSession
  let walletActionLabel = 'Create smart wallet'
  if (isCreatingSmartWallet) {
    walletActionLabel = 'Creating smart wallet...'
  }
  if (hasEmbeddedWallet && !hasSmartWalletSession) {
    walletActionLabel = 'Smart wallet session loading...'
  }

  const walletActionDisabled = isCreatingSmartWallet || hasEmbeddedWallet
  let widgetGateMessage: string | null = null
  if (!ready) {
    widgetGateMessage = 'Loading Privy...'
  } else if (!authenticated) {
    widgetGateMessage = 'Connect with Privy to load the widget.'
  } else if (missingConfig) {
    widgetGateMessage = 'Please provide NEXT_PUBLIC_PODS_API_URL and NEXT_PUBLIC_PODS_API_KEY.'
  } else if (!hasSmartWalletSession) {
    widgetGateMessage = hasEmbeddedWallet
      ? 'Waiting for the Privy smart-wallet session...'
      : 'Create a smart wallet before loading the widget.'
  }

  return (
    <section className="pods-host-shell">
      <header className="pods-host-header">
        <h1>EarnWidget integration</h1>
        <div>
          {ready && authenticated ? (
            <button onClick={() => logout()} type="button">
              Sign out
            </button>
          ) : null}
          {ready && !authenticated ? (
            <button onClick={() => login()} type="button">
              Connect with Privy
            </button>
          ) : null}
          {showWalletAction ? (
            <button
              onClick={() => void createSmartWallet()}
              disabled={walletActionDisabled}
              type="button"
            >
              {walletActionLabel}
            </button>
          ) : null}
        </div>
      </header>

      {widgetGateMessage ? (
        <div>{widgetGateMessage}</div>
      ) : (
        <section className="deframe-widget-card">
          <DeframeProvider config={config} processBytecode={processBytecode}>
            <EarnWidget autoHeight onRouteChange={routeNavigate} />
          </DeframeProvider>
        </section>
      )}

      <section className="pods-log-card" aria-live="polite">
        {txLogs.length === 0 ? (
          <p>No tx status updates yet.</p>
        ) : (
          txLogs.map((entry, index) => <p key={`${entry}-${index}`}>{entry}</p>)
        )}
      </section>

      <section className="pods-state-card">Current route: {routeName}</section>
    </section>
  )
}
```

Suggested host CSS for that example:

```css theme={null}
.pods-host-shell {
  width: min(1180px, 100%);
  max-width: 100%;
  margin: 0 auto;
  overflow-x: hidden;
}

.deframe-widget-card {
  width: min(760px, 100%);
  max-width: 100%;
  margin: 0 auto;
  overflow-x: hidden;
}

.deframe-widget-card > * {
  width: min(760px, 100%);
  max-width: 100%;
  min-width: 0;
}
```

## Add Swap to the Same Privy Host

When your app exposes both Invest and Swap, keep the Privy wallet logic, `config`, and `processBytecode` in one shared provider component, register the Pods Wallet-style swap view in that provider config, then mount either widget inside it.

```tsx theme={null}
import {
  PodsSwapFormView,
} from '@deframe-sdk/components'
import {
  DeframeProvider,
  EarnWidget,
  SwapWidget,
  type DeframeProviderProps,
} from 'pods-sdk'
import { useMemo, type ReactNode } from 'react'

type ProcessBytecode = NonNullable<DeframeProviderProps['processBytecode']>

type PrivyDeframeProviderProps = {
  children: ReactNode
  config: DeframeProviderProps['config']
  processBytecode: ProcessBytecode
}

function PrivyDeframeProvider({
  children,
  config,
  processBytecode,
}: PrivyDeframeProviderProps) {
  const providerConfig = useMemo(() => ({
    ...config,
    components: {
      ...(config.components ?? {}),
      SwapFormView: PodsSwapFormView,
    },
  }), [config])

  return (
    <DeframeProvider config={providerConfig} processBytecode={processBytecode}>
      {children}
    </DeframeProvider>
  )
}

type SharedWidgetRouteProps = {
  config: DeframeProviderProps['config']
  processBytecode: ProcessBytecode
}

export function InvestRoute({
  config,
  processBytecode,
  onRouteChange,
}: SharedWidgetRouteProps & {
  onRouteChange: (route: string) => void
}) {
  return (
    <PrivyDeframeProvider config={config} processBytecode={processBytecode}>
      <EarnWidget autoHeight onRouteChange={onRouteChange} />
    </PrivyDeframeProvider>
  )
}

export function SwapRoute({
  config,
  processBytecode,
  fromTokenAddress,
  fromChainId,
  toTokenAddress,
  toChainId,
}: SharedWidgetRouteProps & {
  fromTokenAddress?: string
  fromChainId?: number
  toTokenAddress?: string
  toChainId?: number
}) {
  return (
    <PrivyDeframeProvider config={config} processBytecode={processBytecode}>
      <SwapWidget
        autoHeight
        fromTokenAddress={fromTokenAddress}
        fromChainId={fromChainId}
        toTokenAddress={toTokenAddress}
        toChainId={toChainId}
      />
    </PrivyDeframeProvider>
  )
}
```

Keep the provider mounted only after the Privy smart-wallet session is ready. Use the [SwapWidget guide](/widgets/swap-widget) for URL prefill and remount-key handling.

## `processBytecode` expectations

The host still owns execution. Your `processBytecode` bridge must:

* **group legs by `chainId` and submit one batched transaction per chain** (the example below uses
  `client.sendTransaction({ calls })` — do not send legs individually)
* emit `HOST_ACK`
* emit `SIGNATURE_PROMPTED`
* submit transactions through the Privy smart-wallet session
* emit `TX_SUBMITTED`
* wait for confirmation through the wallet provider
* emit `TX_CONFIRMED`
* emit `TX_FINALIZED`
* map user cancellations to `SIGNATURE_DECLINED`
* map other pre-submit failures to `SIGNATURE_ERROR`

For the full transaction lifecycle contract, see [processBytecode Contract](/widgets/process-bytecode).

## Branding / theme customization

Use `DeframeProvider.config.theme` as the main theme API:

```tsx theme={null}
theme: {
  mode: 'dark',
  preset: 'default',
  overrides: {
    dark: {
      colors: {
        brandPrimary: '#2DD881',
        brandSecondary: '#8EF0B0',
        bgDefault: '#0B0F14',
        bgSubtle: '#121820',
        bgMuted: '#1B2330',
        bgRaised: '#161E29',
        textPrimary: '#F6F8FB',
        textSecondary: '#AAB6C5',
        textDisabled: '#64748B',
      },
    },
  },
}
```

You can also apply widget-scoped CSS variables from the host shell:

```css theme={null}
.pods-host-shell .deframe-widget {
  --deframe-widget-color-brand-primary: #2dd881;
  --deframe-widget-color-bg-primary: #0b0f14;
  --deframe-widget-color-bg-raised: #161e29;
  --deframe-widget-color-text-primary: #f6f8fb;
  --deframe-widget-color-text-secondary: #aab6c5;
  --deframe-widget-size-radius-lg: 18px;
}
```

Keep those overrides scoped to `.deframe-widget`, never global on `body` or `:root`.

## Authentication flow

Before creating wallets or rendering an authenticated Earn host, users must be logged in.

At a minimum, your app should provide:

* a way to start login
* a way to complete login
* a boolean gate for authenticated vs unauthenticated UI
* a logout path

Helpful hooks from `@privy-io/react-auth`:

### `usePrivy()`

| Property        | Description                       |
| --------------- | --------------------------------- |
| `ready`         | SDK is ready to be used           |
| `authenticated` | Whether the user is authenticated |
| `user`          | User object with linked accounts  |
| `login()`       | Starts login flow                 |
| `logout()`      | Ends the session                  |

### `useLoginWithEmail()`

| Property                  | Description                    |
| ------------------------- | ------------------------------ |
| `sendCode({ email })`     | Sends an OTP to the email      |
| `loginWithCode({ code })` | Completes login with the OTP   |
| `state.status`            | Current login state            |
| `state.error`             | Error details when login fails |

```tsx theme={null}
import { useState } from 'react'
import { usePrivy, useLoginWithEmail } from '@privy-io/react-auth'

export function EmailLoginForm() {
  const { ready, authenticated } = usePrivy()
  const { sendCode, loginWithCode, state } = useLoginWithEmail()
  const [email, setEmail] = useState('')
  const [code, setCode] = useState('')

  const handleSendCode = async () => {
    await sendCode({ email })
  }

  const handleLoginWithCode = async () => {
    await loginWithCode({ code })
  }

  if (!ready) return null
  if (authenticated) return null

  return (
    <form>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <button type="button" onClick={handleSendCode}>
        Send code
      </button>
      <input value={code} onChange={(event) => setCode(event.target.value)} />
      <button type="button" onClick={handleLoginWithCode}>
        Log in
      </button>
      {state.error ? <p>{state.error.message}</p> : null}
    </form>
  )
}
```

## Embedded wallet + smart wallet flow

The practical flow is:

1. authenticate the user
2. create or reuse the embedded Privy wallet
3. let Privy expose the smart-wallet client for transaction submission
4. pass the smart-wallet address to Pods

### Create or reuse the embedded wallet

#### `useWallets()`

| Property  | Description                         |
| --------- | ----------------------------------- |
| `ready`   | Wallet list is ready                |
| `wallets` | Linked/created wallets for the user |

#### `useCreateWallet()`

| Property         | Description                                          |
| ---------------- | ---------------------------------------------------- |
| `createWallet()` | Creates the embedded wallet for the user when needed |

```tsx theme={null}
import { useCreateWallet, useWallets } from '@privy-io/react-auth'

const { ready, wallets } = useWallets()
const { createWallet } = useCreateWallet()

if (ready && wallets.length === 0) {
  return <button onClick={() => void createWallet()}>Create wallet</button>
}
```

### Access the smart-wallet client

#### `useSmartWallets()`

| Property                    | Description                                  |
| --------------------------- | -------------------------------------------- |
| `client`                    | Smart-wallet client instance when available  |
| `client.account.address`    | Smart-wallet address                         |
| `getClientForChain({ id })` | Returns a chain-specific smart-wallet client |

```tsx theme={null}
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'

const { client, getClientForChain } = useSmartWallets()
const smartWalletAddress = client?.account.address
```

<Warning>
  Smart wallets are commonly counterfactual. You may have an address before the contract is deployed on a specific chain.

  Before depending on a smart wallet on a target network:

  * check whether the contract is deployed on that chain
  * if needed, trigger deployment with a minimal transaction
  * ensure your paymaster or sponsorship setup has enough balance
</Warning>

### Verify whether the smart wallet is deployed

```tsx theme={null}
import { createPublicClient, http } from 'viem'
import { polygon } from 'viem/chains'

const smartWalletAddress = '0xYOUR_SMART_WALLET_ADDRESS'

const publicClient = createPublicClient({
  chain: polygon,
  transport: http(),
})

const code = await publicClient.getCode({ address: smartWalletAddress as `0x${string}` })
const isDeployed = !!code && code !== '0x'

console.log({ isDeployed, code })
```

### Trigger deployment with a minimal transaction

```tsx theme={null}
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'

const { client, getClientForChain } = useSmartWallets()
const smartWalletAddress = client?.account.address

const chainClient = await getClientForChain({ id: 137 })
if (!chainClient || !smartWalletAddress) throw new Error('Missing chain client or smart wallet')

await chainClient.sendTransaction({
  calls: [
    {
      to: smartWalletAddress,
      value: 0n,
      data: '0x',
    },
  ],
})
```

<Note>
  In Pods integrations, the smart-wallet address is the address you normally pass into `DeframeProvider.config.walletAddress`.
</Note>

## Fetching strategies and bytecode directly from Pods API

Even when you use the widget, it can still help to understand the underlying API calls for debugging or custom host flows.

All examples below use:

* `NEXT_PUBLIC_PODS_API_URL` as base URL
* `NEXT_PUBLIC_PODS_API_KEY` in `x-api-key`

### List strategies

```tsx theme={null}
const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL
const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY

const url = new URL('/strategies', baseUrl)
url.searchParams.set('limit', '100')

const res = await fetch(url.toString(), {
  method: 'GET',
  headers: {
    'x-api-key': apiKey,
  },
})

const json = await res.json()
console.log(json)
```

### Get strategy details for a wallet

```tsx theme={null}
const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL
const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY

const strategyId = 'Aave-USDT-polygon'
const wallet = '0xYOUR_WALLET'

const url = new URL(`/strategies/${strategyId}`, baseUrl)
url.searchParams.set('wallet', wallet)

const res = await fetch(url.toString(), {
  method: 'GET',
  headers: {
    'x-api-key': apiKey,
  },
})

const json = await res.json()
console.log(json)
```

### Generate strategy bytecode

```tsx theme={null}
const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL
const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY

const strategyId = 'Aave-USDT-polygon'
const action = 'lend'
const wallet = '0xYOUR_WALLET'
const amount = '1500000'

const url = new URL(`/strategies/${strategyId}/bytecode`, baseUrl)
url.searchParams.set('action', action)
url.searchParams.set('wallet', wallet)
url.searchParams.set('amount', amount)

const res = await fetch(url.toString(), {
  method: 'GET',
  headers: {
    'x-api-key': apiKey,
  },
})

const json = await res.json()
console.log(json)
```

Typical response shape:

```ts theme={null}
type PodsBytecodeResponse = {
  feeCharged: string
  bytecode: Array<{
    to: string
    value: string
    data: string
    chainId: string
  }>
}
```

## Executing strategy bytecodes with Privy

After you receive strategy bytecodes, execute them with the smart-wallet client for the target chain.

```tsx theme={null}
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'

const { getClientForChain } = useSmartWallets()

async function executePodsBytecodes(resp: PodsBytecodeResponse) {
  const first = resp.bytecode[0]
  if (!first) throw new Error('Empty bytecode array')

  const chainId = Number(first.chainId)
  const chainClient = await getClientForChain({ id: chainId })
  if (!chainClient) throw new Error('Chain client not found')

  const calls = resp.bytecode.map((b) => ({
    to: b.to as `0x${string}`,
    data: b.data as `0x${string}`,
    value: BigInt(b.value),
  }))

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

<Tip>
  If you are using sponsorship or a paymaster, make sure it has enough balance before testing the flow.
</Tip>

## Recommended references

* [EarnWidget Integration](/widgets/earn-widget)
* [LLM Privy Integration Guide](/llms-privy-integration.txt)
* [Widget Integration Overview](/widget-integration)
