Skip to main content
Use Privy when you want Deframe’s EarnWidget to feel like a Web2 product while still keeping wallet custody and transaction execution in your host app.

Resources

ResourceDescription
Deframe DocsOfficial documentation for Deframe widgets, strategies, and tx flows
Privy WalletsPrivy wallets, embedded wallets, and smart-wallet documentation
EarnWidget IntegrationCurrent SDK widget contract for Earn
LLM Privy Integration GuideCanonical 1-shot prompt for AI-assisted host integration

Verified integration contract

Install the current verified package set:
pnpm add deframe-sdk@0.2.0 @deframe-sdk/components@0.1.23 @privy-io/react-auth@2.25.0 permissionless@0.2.57 viem@2.37.7 @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_DEFRAME_URL=https://api.deframe.io
NEXT_PUBLIC_DEFRAME_API_KEY=your_api_key
NEXT_PUBLIC_DEFRAME_WEBSOCKET_URL=wss://api.deframe.io/updates
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id
For Vite hosts, keep the same names by enabling:
import path from 'node:path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  envPrefix: ['VITE_', 'NEXT_PUBLIC_'],
  resolve: {
    alias: {
      'next/link': path.resolve(__dirname, 'src/shims/next-link.tsx'),
    },
  },
})
For non-Next hosts, add the shim used by that alias:
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 { polygon } from 'viem/chains'
import type { ReactNode } from 'react'

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

Host integration pattern

Inside your Earn 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.
  • Pass the smart-wallet address into DeframeProvider.config.walletAddress.
  • Pass these config values:
    • DEFRAME_API_URL from NEXT_PUBLIC_DEFRAME_URL
    • DEFRAME_API_KEY from NEXT_PUBLIC_DEFRAME_API_KEY
    • DEFRAME_WEBSOCKET_URL from NEXT_PUBLIC_DEFRAME_WEBSOCKET_URL
    • 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 {
  DeframeProvider,
  EarnWidget,
  type DeframeProviderProps,
  type TxUpdateEvent,
} from 'deframe-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 DeframeSmartWalletClient = {
  sendTransaction: (input: {
    calls: Array<{ to: string; data: string; value?: bigint }>
  }) => Promise<string>
}

type SmartWalletData = {
  client?: { account?: { address?: string } } & DeframeSmartWalletClient
  getClientForChain?: (
    input: { id: number },
  ) => DeframeSmartWalletClient | Promise<DeframeSmartWalletClient | 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 existingSmartWalletAddress =
    user?.smartWallet?.address ?? embeddedWallet?.address ?? smartWalletAddress
  const hasExistingSmartWallet = Boolean(existingSmartWalletAddress)
  const isSmartWalletSession =
    activeWallet?.walletClientType === 'privy' &&
    !!smartWalletAddress

  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 (hasExistingSmartWallet) {
      logEvent(`Smart wallet already exists: ${existingSmartWalletAddress}`)
      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, existingSmartWalletAddress, hasExistingSmartWallet, 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) {
      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: 'NO_WALLET',
        message: 'No Privy wallet is connected.',
      })
      return
    }

    if (activeWallet?.walletClientType !== 'privy') {
      emit({
        type: 'SIGNATURE_ERROR',
        clientTxId: payload.clientTxId,
        code: 'UNSUPPORTED_WALLET',
        message: hasExistingSmartWallet
          ? 'This integration only accepts Privy embedded smart-wallet sessions. A smart wallet exists, but current wallet is external.'
          : '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, hasExistingSmartWallet, logEvent, ready, smartWalletAddress, smartWalletClient])

  const config = useMemo(
    () => ({
      DEFRAME_API_URL: process.env.NEXT_PUBLIC_DEFRAME_URL,
      DEFRAME_API_KEY: process.env.NEXT_PUBLIC_DEFRAME_API_KEY,
      DEFRAME_WEBSOCKET_URL: process.env.NEXT_PUBLIC_DEFRAME_WEBSOCKET_URL,
      walletAddress: smartWalletAddress,
      userId: user?.id,
      globalCurrency: 'USD' as const,
      globalCurrencyExchangeRate: 1,
      enableCrossChainInvestments: true,
      theme: {
        mode: 'dark' as const,
        preset: 'default' as const,
      },
      debug: process.env.NODE_ENV === 'development',
    }),
    [smartWalletAddress, user?.id],
  )

  const missingConfig = !config.DEFRAME_API_URL || !config.DEFRAME_API_KEY

  return (
    <section className="deframe-host-shell">
      <header className="deframe-host-header">
        <h1>EarnWidget integration</h1>
        <div>
          {ready && (
            authenticated ? (
              <>
                <button onClick={() => logout()} type="button">
                  Sign out
                </button>
                {!isSmartWalletSession ? (
                  <button
                    onClick={() => void createSmartWallet()}
                    disabled={isCreatingSmartWallet || hasExistingSmartWallet}
                    type="button"
                  >
                    {isCreatingSmartWallet
                      ? 'Creating smart wallet...'
                      : hasExistingSmartWallet
                        ? 'Smart wallet already exists'
                        : 'Create smart wallet'}
                  </button>
                ) : null}
              </>
            ) : (
              <button onClick={() => login()} type="button">
                Connect with Privy
              </button>
            )
          )}
        </div>
      </header>

      {missingConfig ? (
        <div>Please provide NEXT_PUBLIC_DEFRAME_URL and NEXT_PUBLIC_DEFRAME_API_KEY.</div>
      ) : null}

      <section className="deframe-widget-card">
        <DeframeProvider config={config} processBytecode={processBytecode}>
          <EarnWidget autoHeight onRouteChange={routeNavigate} />
        </DeframeProvider>
      </section>

      <section className="deframe-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="deframe-state-card">Current route: {routeName}</section>
    </section>
  )
}
Suggested host CSS for that example:
.deframe-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;
}

processBytecode expectations

The host still owns execution. Your processBytecode bridge must:
  • 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.

Branding / theme customization

Use DeframeProvider.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',
      },
    },
  },
}
You can also apply widget-scoped CSS variables from the host shell:
.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()

PropertyDescription
readySDK is ready to be used
authenticatedWhether the user is authenticated
userUser object with linked accounts
login()Starts login flow
logout()Ends the session

useLoginWithEmail()

PropertyDescription
sendCode({ email })Sends an OTP to the email
loginWithCode({ code })Completes login with the OTP
state.statusCurrent login state
state.errorError details when login fails
import { usePrivy, useLoginWithEmail } from '@privy-io/react-auth'

const { ready, authenticated } = usePrivy()
if (!ready) return null
if (!authenticated) return <LoginScreen />

const { sendCode, loginWithCode, state } = useLoginWithEmail()
await sendCode({ email })
await loginWithCode({ code })

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 Deframe

Create or reuse the embedded wallet

useWallets()

PropertyDescription
readyWallet list is ready
walletsLinked/created wallets for the user

useCreateWallet()

PropertyDescription
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()

PropertyDescription
clientSmart-wallet client instance when available
client.account.addressSmart-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 Deframe integrations, the smart-wallet address is the address you normally pass into DeframeProvider.config.walletAddress.

Fetching strategies and bytecode directly from Deframe 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_DEFRAME_URL as base URL
  • NEXT_PUBLIC_DEFRAME_API_KEY in x-api-key

List strategies

const baseUrl = import.meta.env.NEXT_PUBLIC_DEFRAME_URL
const apiKey = import.meta.env.NEXT_PUBLIC_DEFRAME_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_DEFRAME_URL
const apiKey = import.meta.env.NEXT_PUBLIC_DEFRAME_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_DEFRAME_URL
const apiKey = import.meta.env.NEXT_PUBLIC_DEFRAME_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:
type DeframeBytecodeResponse = {
  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 executeDeframeBytecodes(resp: DeframeBytecodeResponse) {
  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.