Skip to main content
Enable users to access DeFi yield strategies with a Web2-grade experience by combining Privy’s embedded wallets with Deframe’s strategy execution layer. This guide shows how to route user funds into diversified yield strategies — spanning lending, staking, and protocol-native yields — while Deframe handles bytecode generation, protocol routing, and execution. Privy simplifies onboarding and wallet management, allowing users to earn yield across DeFi without interacting directly with contracts or managing complex transaction flows.

Resources

ResourceDescription
Deframe DocsOfficial documentation for Deframe strategies
Privy WalletsPrivy Wallets for helping users interact with DeFi

Install and configure the Vite project

Commands/selections used (as shown in the create-vite wizard):
yarn create vite
  • Project name: deframe-privy-integration
  • Select a framework: React
  • Select a variant: TypeScript + SWC
  • Use rolldown-vite (Experimental)?: No
  • Install with yarn and start now?: Yes
Then, to run locally:
cd deframe-privy-integration
yarn dev

Vite requirement (Privy SDK)

If (and only if) your app is built with Vite, you must add Node polyfills so the Privy SDK works correctly in the browser.

1) Install the polyfills plugin

yarn add --dev vite-plugin-node-polyfills

2) Enable it in vite.config.ts

// vite.config.ts
import { nodePolyfills } from 'vite-plugin-node-polyfills'

export default defineConfig({
  plugins: [
    // ...
    nodePolyfills(),
  ],
})

Privy SDK setup

Below is a minimal setup for Privy and Alchemy provider setup. To customize your Privy provider, follow the instructions in the Privy Quickstart to get your app set up with Privy.
// src/App.tsx
import { PrivyProvider } from '@privy-io/react-auth'
import { SmartWalletsProvider } from '@privy-io/react-auth/smart-wallets';
import YourApp from "your_app_path";

function App() {
  const privyAppId = import.meta.env.VITE_APP_PRIVY_APP_ID || ''

  if (!privyAppId) {
    throw new Error('VITE_APP_PRIVY_APP_ID is not set')
  }

  return (
    <PrivyProvider
      appId={privyAppId}
      config={{
        embeddedWallets: {
            ethereum: {
                createOnLogin: "users-without-wallets",
            },
        },
      }}
    >
      <SmartWalletsProvider>
        <YourApp />
      </SmartWalletsProvider>
    </PrivyProvider>
  )
}

export default App

Configure secrets

This application relies on environment variables to connect to Privy and Deframe. These values are injected at build time by Vite and must be prefixed with VITE_ to be exposed to the client.

Create the .env file

Never commit your .env file to version control. Add it to .gitignore.
Create a .env file at the root of your project and define the following variables:
# .env
VITE_APP_PRIVY_APP_ID=''
VITE_APP_DEFRAME_API_URL=''
VITE_APP_DEFRAME_API_KEY=''
VariableDescription
VITE_APP_PRIVY_APP_IDYour Privy application identifier from the Privy Dashboard
VITE_APP_DEFRAME_API_URLThe base API URL provided by Deframe for executing protocol interactions
VITE_APP_DEFRAME_API_KEYYour Deframe API key for authenticating requests
Both Deframe values can be obtained from the Deframe Dashboard.

Add TypeScript support for environment variables

To ensure full TypeScript support when accessing environment variables via import.meta.env, create the following file:
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_APP_PRIVY_APP_ID: string;
  readonly VITE_APP_DEFRAME_API_URL: string;
  readonly VITE_APP_DEFRAME_API_KEY: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Install and configure Privy SDK

Installation

yarn add @privy-io/react-auth@latest permissionless
To make the UX feel “Web2 smooth” (no wallet popups, no manual gas management), enable Privy Smart Wallets and sponsor gas via a paymaster (use Alchemy or Pimlico).
1

Go to the Privy dashboard

2

Open Smart Wallets settings

Find the Wallet infrastructure / Smart Wallets settings in your app
3

Enable Smart Wallets

Enable Smart Wallets for your target chains (Ethereum, Polygon, Gnosis, Arbitrum, Base and HyperEVM)

Authentication (required)

Before creating wallets or calling Deframe endpoints that depend on a wallet address, users must be logged in and authenticated. At a minimum, your app should provide:
  • Start login: trigger an authentication flow (email OTP, OAuth, etc.)
  • Complete login: confirm the user session is established
  • Check authentication: expose a boolean to decide whether to show the authenticated app or the login screen
  • Logout (recommended): allow the user to end the session

Helpful Privy hooks

usePrivy() (from @privy-io/react-auth)

PropertyDescription
readySDK is ready to be used
authenticatedWhether the user is authenticated
userUser object (profile + linked accounts)
logout()End the session

useLoginWithEmail() (from @privy-io/react-auth)

PropertyDescription
sendCode({ email })Send an OTP to the email
loginWithCode({ code })Complete login with the OTP
state.statusCurrent state (initial, sending-code, awaiting-code-input, submitting-code, done, error)
state.errorError details (when state.status === 'error')
import { usePrivy, useLoginWithEmail } from '@privy-io/react-auth'

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

// Email OTP login (inside LoginScreen):
const { sendCode, loginWithCode, state } = useLoginWithEmail()
await sendCode({ email })
await loginWithCode({ code })
For the next steps in this guide, assume the user is authenticated.

EOA & Smart Wallet usage

Create the EOA (embedded wallet)

useWallets() (from @privy-io/react-auth)

PropertyDescription
readyWallets list is ready
walletsArray of wallets linked/created for the user

useCreateWallet() (from @privy-io/react-auth)

PropertyDescription
createWallet()Creates an embedded wallet for the user (when they don’t have one yet)
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>
}

Create the Smart Wallet

useSmartWallets() (from @privy-io/react-auth/smart-wallets)

PropertyDescription
clientSmart wallet client instance (when available)
client.account.addressThe Smart Wallet address (Account Abstraction)
import { useSmartWallets } from '@privy-io/react-auth/smart-wallets'

const { client } = useSmartWallets()
const smartWalletAddress = client?.account.address
Smart wallets are typically counterfactual: you may have an address even if the contract is not deployed yet on a given chain. Before calling Deframe endpoints that depend on a smart wallet on a specific network, make sure the smart wallet is deployed on that target chain.
  • How to verify deployment: query the chain for contract bytecode (if the result is 0x, it is not deployed).
  • How to deploy: send a minimal “no-op” transaction (e.g. to: smartWalletAddress, value: 0, data: 0x) using the smart wallet client for that chain.
Paymaster note: if you’re using a paymaster for gas sponsorship, it must have sufficient credit/balance; deployment still consumes gas.

Example: 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 })
const isDeployed = !!code && code !== '0x'
console.log({ isDeployed, code })

Example: 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 }) // Polygon
if (!chainClient || !smartWalletAddress) throw new Error('Missing chain client or smart wallet')

await chainClient.sendTransaction({
  to: smartWalletAddress,
  value: 0n,
  data: '0x',
})
The Smart Wallet address is used to request bytecodes from Deframe’s API.

Fetching strategies from Deframe API

All requests below use:
  • Base URL: VITE_APP_DEFRAME_API_URL
  • Auth header: x-api-key: VITE_APP_DEFRAME_API_KEY

1) List strategies

Fetch the available strategies that the user can interact with.
ParameterValue
MethodGET
Path/strategies
Query params (optional)limit: number of items per page, page: page number
const baseUrl = import.meta.env.VITE_APP_DEFRAME_API_URL
const apiKey = import.meta.env.VITE_APP_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)
Each strategy item commonly includes fields like id, protocol, assetName, network, availableActions, logourl, underlyingDecimals, and assetDecimals.

2) Strategy details for a wallet

Fetch specific information for a given strategy, such as apy (protocol UI APY), avgApy (last 30 days), and inceptionApy (since contract deployment). If wallet is passed, the wallet’s spot position is returned.
ParameterValue
MethodGET
Path/strategies/:strategy-id
Query paramswallet: the user’s wallet address
const baseUrl = import.meta.env.VITE_APP_DEFRAME_API_URL
const apiKey = import.meta.env.VITE_APP_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)

3) Generate bytecodes for an action

Request the transaction bytecodes needed to execute a strategy action for a wallet.
ParameterValue
MethodGET
Path/strategies/:strategy-id/bytecode
Query paramsaction (required): one of the strategy availableActions (e.g. lend), wallet (required): the user’s wallet address, amount (required): the amount considering decimals
const baseUrl = import.meta.env.VITE_APP_DEFRAME_API_URL
const apiKey = import.meta.env.VITE_APP_DEFRAME_API_KEY

const strategyId = 'Aave-USDT-polygon'
const action = 'lend'
const wallet = '0xYOUR_WALLET'
const amount = '1500000' // for 1.5 with 6 decimals

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)
Response type:
type DeframeBytecodeResponse = {
  feeCharged: string
  metadata: {
    isCrossChain: boolean
    isSameChainSwap: boolean
    crossChainQuoteId: string
  }
  bytecode: {
    to: string
    value: string
    data: string
    chainId: string
  }[]
}
Ensure amount respects the token decimals. The response is intended to be executed by the user’s wallet.

Executing the bytecodes

After you receive the response from /strategies/:strategy-id/bytecode, you must execute each item in bytecode[] as a smart-wallet transaction on the chain specified by chainId.
  • The smart wallet must be deployed on the target chain (see the Smart Wallet deployment warning above).
  • Use the Smart Wallet client for the target chain via getClientForChain({ id: chainId }).
  • Convert types: to and data must be 0x-prefixed hex strings, value should be a bigint.
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) => {
    return {
      to: b.to as `0x${string}`,
      data: b.data as `0x${string}`,
      value: BigInt(b.value),
    }
  })

  const tx = await chainClient.sendTransaction({ calls })
  console.log({ tx })
}
If you are using a paymaster, ensure it has enough credit/balance; execution consumes gas.

Next Steps