Skip to main content

Why This Matters

Building DeFi UIs from scratch takes weeks: wallet connections, chain switching, deposit/withdraw flows, position tracking, P&L display. Our React widgets handle all of this. You drop in a component, and your users can start earning yield.

Widget Builder Studio

The Widget Builder Studio lets you pick a widget, choose a theme preset, tweak colors and fonts, and see a live preview. When you’re happy with how it looks, switch to the code tab and copy the generated integration code.

Widget Builder Studio

Customize and preview widgets visually. Copy production-ready code.

How It Works

The widgets are React components that talk to the Compass API through your server. Here’s the flow:
  1. Your user opens your app and connects their wallet
  2. The widget shows available yield opportunities (vaults, Aave markets, Pendle markets)
  3. The user picks a market, enters an amount, and signs a transaction
  4. Your server relays the signed transaction to the Compass API, which handles execution
  5. Gas is sponsored by you (optional) so the user doesn’t need ETH for fees
The widgets create a product account for each user behind the scenes. This account holds the user’s funds and interacts with DeFi protocols on their behalf. The user always stays in control - they sign every transaction with their own wallet.

Installation

npm install @compass-labs/widgets @tanstack/react-query viem
You also need a wallet connection library. Pick one:
npm install @rainbow-me/rainbowkit wagmi

Quick Start

Setting up the widgets takes three steps:
  1. Wrap your app with providers - connects the wallet to the widgets
  2. Add a server-side API route - keeps your API key secure
  3. Drop in a widget - render it on a page

Step 1: Client-side Providers

Create a providers file that connects your wallet library to the CompassProvider. The CompassProvider is the wrapper that gives all widgets access to wallet state, chain selection, and theming.
// components/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider, http, useAccount, useSignTypedData, useSwitchChain, useDisconnect } from "wagmi";
import { base, mainnet, arbitrum } from "wagmi/chains";
import { getDefaultConfig, RainbowKitProvider, useConnectModal } from "@rainbow-me/rainbowkit";
import { CompassProvider } from "@compass-labs/widgets";
import "@rainbow-me/rainbowkit/styles.css";

const config = getDefaultConfig({
  appName: "My App",
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
  chains: [base, mainnet, arbitrum],
  transports: {
    [base.id]: http(),
    [mainnet.id]: http(),
    [arbitrum.id]: http(),
  },
  ssr: true,
});

const queryClient = new QueryClient();

function CompassWithWallet({ children }: { children: React.ReactNode }) {
  const { address, chainId } = useAccount();
  const { signTypedDataAsync } = useSignTypedData();
  const { switchChainAsync } = useSwitchChain();
  const { openConnectModal } = useConnectModal();
  const { disconnectAsync } = useDisconnect();

  return (
    <CompassProvider
      defaultChain="base"
      wallet={{
        address: address ?? null,
        chainId,
        signTypedData: async (data) => {
          return signTypedDataAsync({
            domain: data.domain,
            types: data.types,
            primaryType: data.primaryType,
            message: data.message,
          });
        },
        switchChain: async (targetChainId) => {
          await switchChainAsync({ chainId: targetChainId });
        },
        login: openConnectModal,
        logout: disconnectAsync,
      }}
    >
      {children}
    </CompassProvider>
  );
}

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          <CompassWithWallet>{children}</CompassWithWallet>
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Step 2: Server-side API Route

The widgets call the Compass API through your server. This keeps your API key out of the browser. Create a catch-all API route in your Next.js app:
// app/api/compass/[...path]/route.ts
import { createCompassHandler } from "@compass-labs/widgets/server";

const handler = createCompassHandler({
  apiKey: process.env.COMPASS_API_KEY!,
  gasSponsorPrivateKey: process.env.GAS_SPONSOR_PK,  // optional
  rpcUrls: {
    ethereum: process.env.ETHEREUM_MAINNET_RPC_URL,
    base: process.env.BASE_MAINNET_RPC_URL,
    arbitrum: process.env.ARBITRUM_MAINNET_RPC_URL,
  },
});

export const GET = handler;
export const POST = handler;
The createCompassHandler function creates a request handler that:
  • Proxies widget requests to the Compass API with your API key
  • Handles gas sponsorship if you provide a gasSponsorPrivateKey
  • Supports all widget operations (account creation, deposits, withdrawals, swaps, transfers)

Step 3: Environment Variables

# .env.local

# Required
COMPASS_API_KEY=your_compass_api_key

# Wallet provider (pick one)
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id  # for RainbowKit
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id                          # for Privy

# Optional - for gas-sponsored transactions
GAS_SPONSOR_PK=your_gas_sponsor_private_key

# RPC URLs (required for gas-sponsored transactions)
ETHEREUM_MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your_key
BASE_MAINNET_RPC_URL=https://base-mainnet.g.alchemy.com/v2/your_key
ARBITRUM_MAINNET_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/your_key

Step 4: Add Layout and Widgets

Import the widget styles in your layout, then add any widget to a page:
// app/layout.tsx
import { Providers } from "@/components/providers";
import "@compass-labs/widgets/styles.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
// app/page.tsx
"use client";

import { EarnAccount } from "@compass-labs/widgets";

export default function Home() {
  return (
    <main className="max-w-md mx-auto p-8">
      <EarnAccount />
    </main>
  );
}

Step 5: Configure Tailwind (if using)

If your app uses Tailwind CSS, add the widgets package to your content config so Tailwind picks up the classes used by the widgets:
// tailwind.config.ts
const config = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./node_modules/@compass-labs/widgets/**/*.{js,mjs,ts,tsx}",
  ],
  // ...
};

Available Widgets

EarnAccount

A banking-style savings account widget. This is the primary widget - it gives users a single place to deposit funds, choose between variable-rate and fixed-rate markets, and track their earnings. Behind the scenes it creates a product account for the user, handles token approvals, swaps, and deposits across Aave, Morpho vaults, and Pendle. The widget shows:
  • Total balance across all positions and available tokens in the account
  • Market selector with Variable (Aave, Morpho vaults) and Fixed (Pendle) tabs
  • Deposit/Withdraw forms with token selection and auto-swapping
  • Top Up button to transfer tokens from the user’s wallet to their earn account
  • Earnings summary with per-position P&L
<EarnAccount
  title="Savings Account"
  showHeader={true}
  showTopUpButton={true}
  showTrustBadge={true}
  compact={false}
  defaultMarketTab="variable"
  chain="base"
  onDeposit={(market, amount, txHash) => console.log('Deposited:', amount)}
  onWithdraw={(market, amount, txHash) => console.log('Withdrew:', amount)}
/>
PropTypeDefaultDescription
titlestring’Savings Account’Title shown in the header
showHeaderbooleantrueShow the header with title and wallet status
showInterestRatebooleantrueShow the interest rate badge
showTopUpButtonbooleantrueShow the “Top Up” button to transfer from wallet
showTrustBadgebooleantrueShow “Non-custodial” and “Audited protocols” badges
compactbooleanfalseUse compact layout with reduced spacing
defaultMarketTab’variable’ | ‘fixed''variable’Which market tab to show first
allowedVariableMarketsstring[]undefinedFilter which variable markets to show. Undefined shows all.
allowedFixedMarketsstring[]undefinedFilter which fixed markets to show. Undefined shows all.
chainstringundefinedLock to a specific chain. Hides the chain selector when set.
onDepositfunctionundefinedCallback after successful deposit
onWithdrawfunctionundefinedCallback after successful withdraw

SwapWidget

A token swap interface. Users pick a token pair, enter an amount, and get a real-time quote. Transactions are signed with EIP-712 and can be gas-sponsored.
<SwapWidget
  layout="full"
  defaultFromToken="ETH"
  defaultToToken="USDC"
  allowedTokens={['USDC', 'ETH', 'WETH', 'WBTC', 'DAI', 'USDT']}
  showReverseButton={true}
  onSwapSuccess={(fromToken, toToken, fromAmount, toAmount, txHash) => {
    console.log('Swap successful:', txHash);
  }}
  onSwapError={(error) => console.error('Swap failed:', error)}
/>
PropTypeDefaultDescription
layout’compact’ | ‘full''full’Widget size. Compact is smaller for sidebars.
defaultFromTokenstring’ETH’Default input token
defaultToTokenstring’USDC’Default output token
allowedTokensstring[][‘USDC’, ‘ETH’, ‘WETH’, ‘WBTC’, ‘DAI’, ‘USDT’, ‘AUSD’, ‘SBC’]Tokens available for swap
showReverseButtonbooleantrueShow button to flip the token pair
showSettingsbooleanfalseShow slippage settings
onSwapSuccessfunctionundefinedCalled after a successful swap with token details and tx hash
onSwapErrorfunctionundefinedCalled when a swap fails

Shared Components

These building-block components are exported for custom layouts. Use them if you want to build your own widget arrangement instead of using the pre-built widgets above.
ComponentWhat it does
ChainSwitcherDropdown to switch between Ethereum, Base, and Arbitrum
WalletStatusShows the connected wallet address and connection state
EarnAccountGuardWraps content that requires an earn account. Prompts account creation if needed.
EarnAccountBalanceShows the earn account token balance with a transfer button
AccountBalancesModalModal showing detailed token breakdown of the earn account
ActionModalGeneric modal for deposit/withdraw/transfer actions
DepositWithdrawFormReusable form for deposit and withdraw inputs
PnLSummaryDisplays profit/loss information
TransactionHistoryShows past deposit and withdraw transactions

Data Hooks

If you want to fetch data without using the pre-built widgets (for example, to build a completely custom UI), these hooks give you access to the same data the widgets use:
HookReturns
useVaultsData()Vault list, loading state, and refetch function
useAaveData()Aave market list, loading state, and refetch function
usePendleData()Pendle market list, loading state, and refetch function
useSwapQuote()Swap quote for a given token pair and amount

Context Hooks

For advanced use cases, you can access the internal context directly:
HookWhat it provides
useCompassWallet()Wallet address, connection state, signTypedData, switchChain, login, logout
useCompassChain()Current chain ID and setter function. Persists to localStorage.
useCompassApi()API client instance
useEarnAccount()Earn account address, deployment state, createAccount(), refetch()

Wallet Adapter

The CompassProvider needs a wallet prop that bridges your wallet library to the widgets. This is how the widgets know the connected address, can request signatures, and can switch chains.
interface WalletAdapter {
  // Connected wallet address, or null if not connected
  address: Address | null;

  // Current chain ID (optional - used to verify the wallet is on the right chain)
  chainId?: number;

  // Sign EIP-712 typed data - required for all transactions
  signTypedData: (data: TypedDataToSign) => Promise<string>;

  // Switch to a different chain (optional - enables cross-chain support)
  switchChain?: (chainId: number) => Promise<void>;

  // Open wallet connection dialog (optional - enables "Connect Wallet" button)
  login?: () => void;

  // Disconnect wallet (optional)
  logout?: () => Promise<void>;
}
The signTypedData function is the most important one. Every transaction (deposit, withdraw, swap, transfer) goes through EIP-712 typed data signing. The Compass API prepares the data, the user signs it in their wallet, and the server executes the transaction (optionally sponsoring gas).

Theming

The widgets support three theming approaches:
  1. Preset name - Use a built-in theme: theme="compass-dark"
  2. Preset with overrides - Start from a preset and customize specific values: theme={{ preset: 'compass-dark', overrides: {...} }}
  3. Full custom theme - Provide a complete theme object with all values

Built-in Theme Presets

PresetDescription
compass-darkDefault dark theme with indigo/purple accents and glassmorphism
compass-lightLight theme with indigo primary and subtle shadows
minimal-darkFlat dark design with white/gray primary, sharp corners, no shadows
minimal-lightFlat light design with black/gray primary, sharp corners
high-contrast-darkAccessible dark theme with blue primary, bold borders, larger text
high-contrast-lightAccessible light theme with bold colors and larger text
<CompassProvider
  theme="compass-dark"
  defaultChain="base"
>
  {/* widgets */}
</CompassProvider>
Use the Widget Builder Studio to preview all presets and customize them visually before copying the theme config.

Custom Theme Overrides

You only need to override the properties you want to change. Everything else inherits from the preset.
<CompassProvider
  theme={{
    preset: 'compass-dark',
    overrides: {
      colors: {
        brand: {
          primary: '#8b5cf6',
          primaryHover: '#a78bfa',
        },
      },
    },
  }}
  defaultChain="base"
>
  {/* widgets */}
</CompassProvider>

Colors

Brand Colors (colors.brand)

PropertyDescription
primaryPrimary brand color for buttons, links, and accents
primaryHoverHover state for primary color
primaryMutedMuted/subtle variant of primary (for backgrounds, badges)
primaryTextText color on primary-colored backgrounds
secondarySecondary accent color
secondaryHoverHover state for secondary color

Background Colors (colors.backgrounds)

PropertyDescription
backgroundMain page/widget background
surfaceCard and panel backgrounds
surfaceHoverHover state for surfaces
overlayModal and overlay backdrop color

Text Colors (colors.text)

PropertyDescription
textPrimary text color
textSecondarySecondary/muted text
textTertiaryTertiary/disabled text

Border Colors (colors.borders)

PropertyDescription
borderDefault border color
borderHoverBorder hover state
borderFocusFocus ring color for inputs

Semantic Colors (colors.semantic)

Each semantic color has a DEFAULT (main color) and muted (background/subtle) variant:
PropertyDescription
success.DEFAULTSuccess/positive state color
success.mutedSuccess background/subtle color
warning.DEFAULTWarning/caution state color
warning.mutedWarning background/subtle color
error.DEFAULTError/negative state color
error.mutedError background/subtle color
info.DEFAULTInfo/neutral state color
info.mutedInfo background/subtle color

Typography

Font Families (typography)

PropertyDescription
fontFamilyPrimary font stack for all text
fontFamilyMonoMonospace font for code and numbers

Text Scales

Each scale (heading, subheading, body, caption, label) supports:
PropertyDescription
fontSizeFont size (e.g., '1.5rem')
lineHeightLine height (e.g., '1.4')
fontWeightFont weight (e.g., '600')
letterSpacingLetter spacing (e.g., '-0.02em') - optional

Spacing

PropertyDescription
unitBase spacing unit (e.g., '4px')
containerPaddingOuter container padding
cardPaddingPadding inside cards and panels
inputPaddingPadding inside input fields and buttons

Shape

Border Radius (shape.borderRadius)

PropertyDescription
noneNo radius (e.g., '0')
smSmall radius for subtle rounding
mdMedium radius for inputs and buttons
lgLarge radius for cards
xlExtra large radius for modals
fullFully rounded (e.g., '9999px')

Border Width (shape.borderWidth)

PropertyDescription
borderWidthDefault border width (e.g., '1px')
The minimal-dark and minimal-light presets use sharp corners (0 radius). Start from these if you want a flat design.

Effects

Shadows (effects.shadow)

PropertyDescription
smSubtle shadow for hover states
mdMedium shadow for cards
lgLarge shadow for modals and dropdowns

Blur (effects.blur)

PropertyDescription
smSubtle blur for overlays
mdMedium blur for glassmorphism
lgHeavy blur for backgrounds

Transitions (effects.transition)

PropertyDescription
fastQuick interactions (150ms)
normalStandard transitions (200ms)
slowDeliberate animations (300ms)
The minimal-dark and minimal-light presets disable shadows. The compass-dark preset uses glassmorphism with blur effects.

Full Theme Example

Here is a complete example showing all customizable properties:
<CompassProvider
  theme={{
    preset: 'compass-dark',
    overrides: {
      colors: {
        brand: {
          primary: '#6366f1',
          primaryHover: '#818cf8',
          primaryMuted: 'rgba(99, 102, 241, 0.15)',
          primaryText: '#ffffff',
          secondary: '#8b5cf6',
          secondaryHover: '#a78bfa',
        },
        backgrounds: {
          background: '#050507',
          surface: '#0c0c10',
          surfaceHover: '#12121a',
          overlay: 'rgba(0, 0, 0, 0.7)',
        },
        text: {
          text: '#f4f4f5',
          textSecondary: '#a1a1aa',
          textTertiary: '#71717a',
        },
        borders: {
          border: 'rgba(255, 255, 255, 0.08)',
          borderHover: 'rgba(255, 255, 255, 0.12)',
          borderFocus: '#6366f1',
        },
        semantic: {
          success: { DEFAULT: '#10b981', muted: 'rgba(16, 185, 129, 0.15)' },
          warning: { DEFAULT: '#f59e0b', muted: 'rgba(245, 158, 11, 0.15)' },
          error: { DEFAULT: '#ef4444', muted: 'rgba(239, 68, 68, 0.15)' },
          info: { DEFAULT: '#6366f1', muted: 'rgba(99, 102, 241, 0.15)' },
        },
      },
      typography: {
        fontFamily: 'Inter, system-ui, sans-serif',
        fontFamilyMono: 'JetBrains Mono, monospace',
        heading: { fontSize: '1.5rem', lineHeight: '1.3', fontWeight: '700' },
        subheading: { fontSize: '1.125rem', lineHeight: '1.4', fontWeight: '600' },
        body: { fontSize: '0.9375rem', lineHeight: '1.5', fontWeight: '400' },
        caption: { fontSize: '0.8125rem', lineHeight: '1.4', fontWeight: '400' },
        label: { fontSize: '0.75rem', lineHeight: '1.3', fontWeight: '500' },
      },
      spacing: {
        unit: '4px',
        containerPadding: '1.5rem',
        cardPadding: '1.25rem',
        inputPadding: '0.75rem 1rem',
      },
      shape: {
        borderRadius: {
          none: '0',
          sm: '6px',
          md: '8px',
          lg: '12px',
          xl: '16px',
          full: '9999px',
        },
        borderWidth: '1px',
      },
      effects: {
        shadow: {
          sm: '0 1px 2px rgba(0, 0, 0, 0.1)',
          md: '0 4px 12px rgba(0, 0, 0, 0.15)',
          lg: '0 8px 24px rgba(0, 0, 0, 0.2)',
        },
        blur: { sm: '4px', md: '8px', lg: '16px' },
        transition: { fast: '150ms ease', normal: '200ms ease', slow: '300ms ease' },
      },
    },
  }}
  defaultChain="base"
>
  {/* widgets */}
</CompassProvider>

Supported Chains

ChainID
Ethereumethereum
Basebase
Arbitrumarbitrum
Set the default chain in the provider:
<CompassProvider defaultChain="base">
Each widget includes a built-in chain switcher. If you use the chain prop on EarnAccount, the chain selector is hidden and the widget is locked to that chain.

Gas Sponsorship

To sponsor gas so your users don’t need ETH for transaction fees:
  1. Set gasSponsorPrivateKey in the server handler (a wallet with ETH on each chain you support)
  2. Set rpcUrls for each chain
When configured, the widgets automatically use sponsored transactions. Users sign with EIP-712 and the sponsor wallet pays the gas. See Gas Sponsorship for details.

Use Cases

Embedded Savings Account: Drop in the EarnAccount widget to give users a bank-like savings experience. They deposit stablecoins, pick a yield market, and start earning - all within your app. Wallet Integration: Add an “Earn” tab to your wallet app. Users see their positions, P&L, and can manage deposits without leaving your app. DEX Integration: Add the SwapWidget to let users swap tokens directly within your app with gas-sponsored transactions.

Next Steps