Resources
| Resource | Description |
|---|---|
| Pods Docs | Official documentation for Pods widgets, strategies, and tx flows |
| Privy Wallets | Privy wallets, embedded wallets, and smart-wallet documentation |
| EarnWidget Integration | Current SDK widget contract for Earn |
| SwapWidget Integration | Current SDK widget contract for Swap |
| LLM Privy Integration Guide | Canonical 1-shot prompt for AI-assisted host integration |
Verified integration contract
Install the current verified package set:pnpm add [email protected] @deframe-sdk/[email protected] @privy-io/[email protected] [email protected] [email protected] @reduxjs/toolkit@^2 react-redux@^9 redux@^5
Do not install
@privy-io/react-auth/smart-wallets as a separate dependency. Import it from the @privy-io/react-auth package subpath.Environment variables
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:// app/layout.tsx
import '@deframe-sdk/components/styles.css'
import './globals.css'
// src/main.tsx
import '@deframe-sdk/components/styles.css'
import './index.css'
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)),
},
},
})
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
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, anduseSmartWallets. - 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
supportedChainsin sync with every EVM chain that Pods bytecode can target;sendTransaction,switchChain, andgetClientForChain({ 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, andNEXT_PUBLIC_PODS_WEBSOCKET_URLenv names. - Map those env values into the current
DeframeProviderAPI 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:
'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.
// [email protected] 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>
)
}
.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.
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>
)
}
processBytecode expectations
The host still owns execution. Your processBytecode bridge must:
- group legs by
chainIdand submit one batched transaction per chain (the example below usesclient.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
Branding / theme customization
UseDeframeProvider.config.theme as the main theme API:
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',
},
},
},
}
.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;
}
.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
@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 |
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:- authenticate the user
- create or reuse the embedded Privy wallet
- let Privy expose the smart-wallet client for transaction submission
- 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 |
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 |
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'
const { client, getClientForChain } = useSmartWallets()
const smartWalletAddress = client?.account.address
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
Verify whether the smart wallet is deployed
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
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',
},
],
})
In Pods integrations, the smart-wallet address is the address you normally pass into
DeframeProvider.config.walletAddress.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_URLas base URLNEXT_PUBLIC_PODS_API_KEYinx-api-key
List strategies
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
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
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)
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.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 })
}
If you are using sponsorship or a paymaster, make sure it has enough balance before testing the flow.