Skip to main content
Use EarnWidget when you want Deframe yield UX in your app while keeping wallet custody and transaction execution in your infrastructure.

What You Own vs What Deframe Owns

AreaOwner
Wallet connection, signatures, tx submissionYour app
Strategy data, quote/bytecode generation, protocol routingDeframe API
UI state and yield screens (overview, details, deposit, withdraw)EarnWidget

Verified integration contract

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.

Styles and Theme

Import the published component stylesheet in the host entrypoint before your app CSS:
import '@deframe-sdk/components/styles.css'
import './index.css'
This is required for the full widget visual system. The SDK injects part of the widget styling, but the component-library stylesheet still needs to be present in the host. Treat .deframe-widget as a style boundary owned by the SDK:
  • Avoid global resets like * { ... } or broad element resets that can leak into the widget.
  • Scope host app styles to your own containers.
  • Use DeframeProvider.config.theme as the source of truth for widget theming.
  • Use widget-scoped CSS variables only as host-side branding extensions.
.host-shell {
  /* Host app styles */
}

.widget-slot {
  /* Layout/spacing around widget */
}

.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;
}

Environment Variables

Canonical client env names:
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
If your host uses Vite and you want to keep these same names, configure:
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>
  )
}

Step 1: Implement processBytecode

processBytecode is the host bridge between widget actions and wallet execution.
import type { UpdateTxStatus } from 'deframe-sdk'

type TxIntent = {
  clientTxId: string
  bytecodes: Array<{
    chainId?: number
    to: string
    data: string
    value: string
    gasLimit?: string
  }>
}

export async function processBytecode(
  payload: TxIntent,
  ctx: { updateTxStatus: UpdateTxStatus }
) {
  const { clientTxId, bytecodes } = payload

  try {
    ctx.updateTxStatus({ type: 'HOST_ACK', clientTxId })

    for (const tx of bytecodes) {
      ctx.updateTxStatus({ type: 'SIGNATURE_PROMPTED', clientTxId })

      const txHash = await sendTransactionWithYourWallet(tx)
      ctx.updateTxStatus({ type: 'TX_SUBMITTED', clientTxId, chainId: tx.chainId, txHash })

      const receipt = await waitForReceipt(txHash)
      if (receipt.status !== 1) {
        ctx.updateTxStatus({ type: 'TX_REVERTED', clientTxId, txHash, reason: 'Execution reverted' })
        return
      }

      ctx.updateTxStatus({ type: 'TX_CONFIRMED', clientTxId, txHash, blockNumber: receipt.blockNumber })
    }

    ctx.updateTxStatus({ type: 'TX_FINALIZED', clientTxId })
  } catch (error: any) {
    if (error?.code === 4001 || error?.code === 'ACTION_REJECTED') {
      ctx.updateTxStatus({ type: 'SIGNATURE_DECLINED', clientTxId })
      return
    }

    ctx.updateTxStatus({
      type: 'SIGNATURE_ERROR',
      clientTxId,
      code: String(error?.code ?? 'UNKNOWN_ERROR'),
      message: String(error?.message ?? 'Failed to process transaction')
    })
  }
}
For the complete Privy + smart-wallet host flow, see:

Step 2: Mount DeframeProvider + EarnWidget

import { DeframeProvider, EarnWidget } from 'deframe-sdk'
import { useState } from 'react'

export function EarnWidgetScreen({ walletAddress }: { walletAddress: string }) {
  const [routeName, setRouteName] = useState('overview')

  return (
    <DeframeProvider
      config={{
        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,
        language: 'EN',
        enableCrossChainInvestments: true,
        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',
              }
            }
          }
        }
      }}
      processBytecode={processBytecode}
    >
      <div className="host-shell">
        <section className="widget-slot">
          <EarnWidget autoHeight onRouteChange={setRouteName} />
        </section>
      </div>
    </DeframeProvider>
  )
}

Route Names Exposed by onRouteChange

Common values: overview, details, deposit, withdraw, investment-details, history, history-deposit-details, history-withdraw-details. Use these to adapt host shell layout, breadcrumbs, and analytics tags.

API Endpoints Used in Earn Flows

  • GET /strategies for strategy discovery
  • GET /strategies/{strategyId}/bytecode for deposit/withdraw transaction data
  • GET /wallets/{wallet} for portfolio state
  • GET /wallets/{wallet}/history/{strategyId} for investment activity
Guides:

Customer Integration Checklist

  1. Create customer + API key and store mapping in your backend.
  2. Pass the connected wallet address into DeframeProvider.config.walletAddress.
  3. Implement processBytecode with your wallet stack.
  4. Emit all required tx lifecycle statuses back to the widget.
  5. Configure DEFRAME_WEBSOCKET_URL so the widget can recover realtime cross-chain state.
  6. Track completed transactions in your own ledger for reconciliation/support.