# Compass Widgets -- LLM Integration Guide

> For AI coding assistants. This file contains everything needed to integrate
> @compass-labs/widgets into a React/Next.js application.
> Package: @compass-labs/widgets (npm)
> Docs: https://docs.compasslabs.ai/v2/Products/Widgets

## Universal activation prompt

If you're an AI agent reading this for the first time, the user typically activates this guide with a prompt like:

> Fetch https://compasslabs.ai/llms-widgets.md and integrate Compass widgets into this project.

After fetching, save this guide to your context (e.g. as a project-level memory or skill file) so you can reference it across the integration without re-fetching. Then proceed with the steps below.

## Quick Reference

- Package: @compass-labs/widgets
- Required peer deps: react 18+, react-dom, @tanstack/react-query 5.x
- Optional peer deps: @nestjs/common, @nestjs/core, reflect-metadata (only if using the NestJS server)
- viem is a regular dependency (auto-installed). Install it explicitly only if you import viem types in your own code.
- Wallet library: @rainbow-me/rainbowkit + wagmi OR @privy-io/react-auth
- Supported widget chains: ethereum, base, arbitrum (default: base). The server handler also supports hyperevm for gas sponsorship, but the React widgets currently render only the three EVM chains above.
- Server export: @compass-labs/widgets/server (Next.js)
- NestJS export: @compass-labs/widgets/server/nestjs
- Styles: @compass-labs/widgets/styles.css (must be imported in layout)

## Integration Steps (Next.js)

1. Install packages
2. Create server-side API route at app/api/compass/[...path]/route.ts
3. Create providers file at components/providers.tsx
4. Import styles and wrap app with Providers in app/layout.tsx
5. Add a widget to a page
6. Configure Tailwind content paths (if using Tailwind)
7. Set environment variables in .env.local

## Complete Code -- Next.js + RainbowKit

### Install

```bash
npm install @compass-labs/widgets @tanstack/react-query viem @rainbow-me/rainbowkit wagmi
```

### File: app/api/compass/[...path]/route.ts

```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;
```

### File: components/providers.tsx

```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>
  );
}
```

### File: app/layout.tsx

```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>
  );
}
```

### File: app/page.tsx

```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>
  );
}
```

### File: .env.local

```bash
# Required
COMPASS_API_KEY=your_compass_api_key

# RainbowKit / WalletConnect
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id

# Optional - gas-sponsored transactions
GAS_SPONSOR_PK=your_gas_sponsor_private_key
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
```

### Tailwind v3: tailwind.config.ts

Add to the content array:

```ts
"./node_modules/@compass-labs/widgets/**/*.{js,mjs,ts,tsx}"
```

### Tailwind v4: app.css

Add after @import "tailwindcss":

```css
@source "../node_modules/@compass-labs/widgets/dist/**/*.{js,mjs}";
```


## Complete Code -- Next.js + Privy

### Install

```bash
npm install @compass-labs/widgets @tanstack/react-query viem @privy-io/react-auth
```

### File: app/api/compass/[...path]/route.ts

```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;
```

### File: components/providers.tsx

```tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyProvider, usePrivy, useWallets } from "@privy-io/react-auth";
import { CompassProvider } from "@compass-labs/widgets";

const queryClient = new QueryClient();

function CompassWithPrivy({ children }: { children: React.ReactNode }) {
  const { login, logout, authenticated } = usePrivy();
  const { wallets } = useWallets();
  const wallet = wallets[0];

  // Privy returns chainId in CAIP-2 format ("eip155:8453"); widgets need numeric.
  const chainId = wallet?.chainId ? Number(wallet.chainId.split(":")[1]) : undefined;

  return (
    <CompassProvider
      defaultChain="base"
      wallet={{
        address: authenticated
          ? ((wallet?.address as `0x${string}` | undefined) ?? null)
          : null,
        chainId: authenticated ? chainId : undefined,
        signTypedData: async (data) => {
          if (!wallet) throw new Error("Wallet not connected");
          const provider = await wallet.getEthereumProvider();
          if (!provider) throw new Error("Ethereum provider unavailable");
          const domainFields: { name: string; type: string }[] = [];
          if (data.domain.name !== undefined) domainFields.push({ name: "name", type: "string" });
          if (data.domain.version !== undefined) domainFields.push({ name: "version", type: "string" });
          if (data.domain.chainId !== undefined) domainFields.push({ name: "chainId", type: "uint256" });
          if (data.domain.verifyingContract !== undefined) domainFields.push({ name: "verifyingContract", type: "address" });
          const signature = await provider.request({
            method: "eth_signTypedData_v4",
            params: [wallet.address, JSON.stringify({
              types: { EIP712Domain: domainFields, ...data.types },
              primaryType: data.primaryType,
              domain: data.domain,
              message: data.message,
            })],
          });
          return signature as string;
        },
        switchChain: async (targetChainId) => {
          if (!wallet) throw new Error("Wallet not connected");
          await wallet.switchChain(targetChainId);
        },
        login: () => login(),
        logout: () => logout(),
      }}
    >
      {children}
    </CompassProvider>
  );
}

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
      config={{
        loginMethods: ["wallet", "email"],
        appearance: { theme: "dark" },
        embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
      }}
    >
      <QueryClientProvider client={queryClient}>
        <CompassWithPrivy>{children}</CompassWithPrivy>
      </QueryClientProvider>
    </PrivyProvider>
  );
}
```

**Note (Privy v3):** the adapter gates `address` on `authenticated` rather than reading directly from `useWallets()`. External wallets like MetaMask don't expose a programmatic disconnect, so they stay in `useWallets()` until the user disconnects from the wallet extension itself. Reading `address` from `authenticated` ensures the widget reflects logout immediately while the wallet extension's own state is left alone.

### File: app/layout.tsx

```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>
  );
}
```

### File: app/page.tsx

```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>
  );
}
```

### File: .env.local

```bash
# Required
COMPASS_API_KEY=your_compass_api_key

# Privy
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id

# Optional - gas-sponsored transactions
GAS_SPONSOR_PK=your_gas_sponsor_private_key
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
```

### Tailwind v3: tailwind.config.ts

Add to the content array:

```ts
"./node_modules/@compass-labs/widgets/**/*.{js,mjs,ts,tsx}"
```

### Tailwind v4: app.css

Add after @import "tailwindcss":

```css
@source "../node_modules/@compass-labs/widgets/dist/**/*.{js,mjs}";
```


## Generic Proxy Pattern (for non-Next.js frameworks)

The server route is a simple proxy. It:
1. Receives requests at /api/compass/*
2. Forwards them to https://api.compasslabs.ai/* with the API key in the X-API-Key header
3. For gas-sponsored transactions, the handler also signs and relays using the sponsor private key

Any framework that can create HTTP route handlers works. For Vite/Remix/Express/etc., implement the proxy pattern manually:
- Forward GET and POST requests from /api/compass/{path} to https://api.compasslabs.ai/{path}
- Add header: X-API-Key: {your_api_key}
- Pass through the request body and query parameters
- Return the API response to the client


## NestJS Alternative

For NestJS, use the built-in module:

```ts
import { Module } from "@nestjs/common";
import { CompassModule } from "@compass-labs/widgets/server/nestjs";

@Module({
  imports: [
    CompassModule.register({
      apiKey: process.env.COMPASS_API_KEY!,
      gasSponsorPrivateKey: process.env.GAS_SPONSOR_PK,
      rpcUrls: {
        ethereum: process.env.ETHEREUM_MAINNET_RPC_URL,
        base: process.env.BASE_MAINNET_RPC_URL,
        arbitrum: process.env.ARBITRUM_MAINNET_RPC_URL,
      },
    }),
  ],
})
export class AppModule {}
```

For async configuration (e.g. with ConfigModule):

```ts
CompassModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    apiKey: config.get("COMPASS_API_KEY"),
    rpcUrls: {
      base: config.get("BASE_MAINNET_RPC_URL"),
    },
  }),
})
```


## Available Widgets

### EarnAccount

Banking-style savings account widget. Users deposit stablecoins/crypto, choose between variable-rate (Aave, Morpho vaults) and fixed-rate (Pendle) markets, and track earnings. Creates a product account per user. Handles token approvals, swaps, and deposits automatically.

Example:

```tsx
<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)}
/>
```

Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | 'Savings Account' | Title shown in the header |
| showHeader | boolean | true | Show the header with title and wallet status |
| showInterestRate | boolean | true | Show the interest rate badge |
| showTopUpButton | boolean | true | Show the "Top Up" button to transfer from wallet |
| showTrustBadge | boolean | true | Show "Non-custodial" and "Audited protocols" badges |
| compact | boolean | false | Use compact layout with reduced spacing |
| defaultMarketTab | `'variable' \| 'fixed'` | 'variable' | Which market tab to show first |
| allowedVariableMarkets | string[] | undefined | Filter which variable markets to show. Undefined = all. |
| allowedFixedMarkets | string[] | undefined | Filter which fixed markets to show. Undefined = all. |
| tokenSymbols | string[] | undefined | Filter markets by token symbol (e.g. ['USDC', 'USDT']). Case-insensitive. |
| chain | string | undefined | Lock to a specific chain. Hides chain selector. Values: 'ethereum', 'base', 'arbitrum' |
| height | string | '600px' | Fixed height for the widget container |
| minTvlUsd | number | 1000000 | Minimum TVL in USD for vault filtering (server-side) |
| minLiquidityUsd | number | 100000 | Minimum available liquidity in USD for vault filtering |
| minDepositCapUsd | number | 0 | Minimum deposit capacity in USD. Set to 0 to exclude paused vaults. |
| size | `'auto' \| 'sm' \| 'md' \| 'lg'` | 'auto' | Typography density. 'auto' picks a size based on container width. |
| signedOutHero | object | undefined | Override pre-login hero copy: `{ apyPillText?, headline?, subtitle?, livePillText?, livePillValue? }` |
| onDeposit | (market, amount, txHash) => void | undefined | Callback after successful deposit |
| onWithdraw | (market, amount, txHash) => void | undefined | Callback after successful withdraw |

### CreditAccount

Credit widget for borrowing against crypto holdings through DeFi lending protocols.

Example:

```tsx
<CreditAccount
  title="Credit Account"
  showHeader={true}
  chain="base"
/>
```

Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | 'Credit Account' | Title shown in the header |
| showHeader | boolean | true | Show the header with title and wallet status |
| showTopUpButton | boolean | true | Show the "Top Up" button to transfer from wallet |
| compact | boolean | false | Use compact layout with reduced spacing |
| height | string | '100%' | Fixed height for the widget container |
| chain | string | undefined | Lock to a specific chain |
| allowedCollateralTokens | string[] | undefined | Restrict which tokens can be used as collateral |
| allowedDebtTokens | string[] | undefined | Restrict which tokens can be borrowed |
| onBorrow | (txHash) => void | undefined | Callback after successful borrow |
| onRepay | (txHash) => void | undefined | Callback after successful repay |

### CompassEarnWidget

Unified earn interface combining positions view and rebalancing in one tabbed widget.

Example:

```tsx
<CompassEarnWidget
  preset="full"
  enablePositions={true}
  enableRebalance={true}
  defaultTab="positions"
  showHeader={true}
  showPnL={true}
/>
```

Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| preset | 'full' | undefined | Feature preset to enable all features |
| enablePositions | boolean | undefined | Enable positions tab |
| enableRebalance | boolean | undefined | Enable rebalance tab |
| defaultTab | 'positions' \| 'rebalance' | undefined | Default tab to show |
| showHeader | boolean | undefined | Show widget header |
| showPnL | boolean | undefined | Show P&L information |
| onTabChange | (tab: TabId) => void | undefined | Callback when tab changes |

### RebalancingWidget

Portfolio rebalancing widget. Shows current positions across DeFi venues, lets users set target allocations, previews rebalance actions, and executes them atomically.

Example:

```tsx
<RebalancingWidget
  title="Portfolio Manager"
  chain="base"
  showChainSwitcher={true}
  showWalletStatus={true}
  defaultSlippage={0.5}
/>
```

Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | 'Portfolio Manager' | Widget title in the header |
| showChainSwitcher | boolean | true | Show chain switcher in header |
| showWalletStatus | boolean | true | Show wallet status in header |
| showTopUp | boolean | true | Show the "Top Up" button in balance card |
| minRebalanceThresholdUsd | number | 1 | Minimum USD value to include in rebalance actions |
| defaultSlippage | number | 0.5 | Default slippage for swaps (percent) |
| chain | string | undefined | Lock to specific chain (hides chain switcher) |
| height | string | '600px' | Fixed widget height |
| venues | object | undefined | Filter venues: { vaults?: string[], aave?: string[], pendle?: string[] } |
| onRebalance | (plan, txHash) => void | undefined | Callback after successful rebalance |
| onError | (error) => void | undefined | Callback when rebalance fails |

### TraditionalInvestingWidget

Perpetual futures trading widget for stocks, commodities, and forex on-chain via Hyperliquid.

Example:

```tsx
<TraditionalInvestingWidget
  showHeader={true}
  title="Trading"
/>
```

Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showHeader | boolean | undefined | Show widget header with title |
| title | string | undefined | Widget title text |
| height | string | undefined | Widget height |
| onTrade | (asset, side, size, txHash) => void | undefined | Callback when a trade executes |
| onDeposit | (amount, txHash) => void | undefined | Callback when deposit completes |
| onWithdraw | (amount, txHash) => void | undefined | Callback when withdraw completes |


## Theming

The widgets support three theming approaches:
1. Preset name string: theme="compass-dark"
2. Preset with overrides: theme={{ preset: 'compass-dark', overrides: {...} }}
3. Full custom theme object

### Built-in Theme Presets

| Preset | Description |
|--------|-------------|
| compass-dark | Default. Dark theme with indigo/purple accents and glassmorphism effects. |
| compass-light | Light theme with indigo primary and subtle shadows. |
| minimal-dark | Flat dark design. White/gray primary, sharp corners (0 radius), no shadows. |
| minimal-light | Flat light design. Black/gray primary, sharp corners, no shadows. |
| high-contrast-dark | Accessible dark theme. Blue primary, bold 2px borders, larger text sizes. |
| high-contrast-light | Accessible light theme. Dark blue primary, bold borders, larger text. |

### Using a Preset

```tsx
<CompassProvider theme="compass-dark" defaultChain="base">
  {children}
</CompassProvider>
```

### Overriding a Preset

```tsx
<CompassProvider
  theme={{
    preset: 'compass-dark',
    overrides: {
      colors: {
        brand: {
          primary: '#8b5cf6',
          primaryHover: '#a78bfa',
        },
      },
    },
  }}
  defaultChain="base"
>
  {children}
</CompassProvider>
```

### Full Theme Object Reference

All fields are optional when using overrides. Only specify what you want to change.

colors.brand:
  primary         - Primary brand color (buttons, links, accents)
  primaryHover    - Hover state for primary
  primaryMuted    - Muted variant (backgrounds, badges)
  primaryText     - Text on primary backgrounds
  secondary       - Secondary accent color
  secondaryHover  - Secondary hover state

colors.backgrounds:
  background      - Main page/widget background
  surface         - Card and panel backgrounds
  surfaceHover    - Surface hover state
  overlay         - Modal/overlay backdrop

colors.text:
  text            - Primary text
  textSecondary   - Secondary/muted text
  textTertiary    - Tertiary/disabled text

colors.borders:
  border          - Default border color
  borderHover     - Border hover state
  borderFocus     - Focus ring color (inputs)

colors.semantic (each has DEFAULT and muted):
  success         - Positive states
  warning         - Caution states
  error           - Negative states
  info            - Neutral information

typography:
  fontFamily      - Primary font stack (e.g. 'Inter, system-ui, sans-serif')
  fontFamilyMono  - Monospace font stack
  heading         - { fontSize, lineHeight, fontWeight, letterSpacing? }
  subheading      - { fontSize, lineHeight, fontWeight, letterSpacing? }
  body            - { fontSize, lineHeight, fontWeight, letterSpacing? }
  caption         - { fontSize, lineHeight, fontWeight, letterSpacing? }
  label           - { fontSize, lineHeight, fontWeight, letterSpacing? }

spacing:
  unit            - Base spacing unit (e.g. '4px')
  containerPadding - Container padding
  cardPadding     - Card/panel padding
  inputPadding    - Input field padding

shape.borderRadius:
  none            - No radius ('0')
  sm              - Small radius
  md              - Medium radius (inputs, buttons)
  lg              - Large radius (cards)
  xl              - Extra large (modals)
  full            - Pill shape ('9999px')
shape.borderWidth - Default border width (e.g. '1px')

effects.shadow:
  sm              - Subtle (hover states)
  md              - Medium (cards)
  lg              - Large (modals)
effects.blur:
  sm, md, lg      - Blur levels for overlays/glassmorphism
effects.transition:
  fast            - 150ms (hover states)
  normal          - 200ms (most interactions)
  slow            - 300ms (page transitions)

### Full Theme Override Example

```tsx
<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"
>
  {children}
</CompassProvider>
```


## Shared Components

These building-block components are exported for custom layouts:

| Component | What it does |
|-----------|-------------|
| ChainSwitcher | Dropdown to switch between Ethereum, Base, and Arbitrum |
| WalletStatus | Shows connected wallet address and connection state |
| EarnAccountGuard | Wraps content requiring an earn account. Prompts creation if needed. |
| CreditAccountGuard | Wraps content requiring a credit account. Prompts creation if needed. |
| EarnAccountBalance | Shows earn account token balance with a transfer button |
| AccountBalancesModal | Modal showing detailed token breakdown |
| ActionModal | Generic modal for deposit/withdraw/transfer actions |
| DepositWithdrawForm | Reusable form for deposit and withdraw inputs |
| PnLSummary | Displays profit/loss information |
| TransactionHistory | Shows past deposit and withdraw transactions |
| SwapForm | Token swap form with quote display |
| CopyableAddress | Address display with click-to-copy |

## Data Hooks

| Hook | What it returns |
|------|----------------|
| useSwapQuote() | Swap quote for a given token pair and amount |
| useRebalancingData() | Portfolio positions, idle balances, and rebalance plan |
| useRefreshBalances() | Function to trigger a balance refresh across all widgets |

## Context Hooks (advanced)

| Hook | What it provides |
|------|-----------------|
| useCompassWallet() | Wallet address, connection state, signTypedData, switchChain, login, logout |
| useCompassChain() | Current chain ID and setter. Persists to localStorage. |
| useCompassApi() | API client instance for custom requests |
| useEarnAccount() | Earn account address, deployment state, createAccount(), refetch() |
| useCreditAccount() | Credit account address, deployment state, createAccount(), refetch() |
| useTheme() | Resolved theme object (the merged result of preset + overrides), for components that need to read theme values directly |

The SDK also exports `useEmbeddableWallet`, `useChain`, and `useEmbeddableApi` as aliases for `useCompassWallet`, `useCompassChain`, and `useCompassApi` respectively. Prefer the `useCompass*` names in new code.

## WalletAdapter Interface

The CompassProvider requires a `wallet` prop implementing the WalletAdapter interface.
This bridges your wallet library (wagmi, Privy, ethers, etc.) to the widgets.

```ts
interface WalletAdapter {
  ready?: boolean;              // Whether wallet provider has initialized (default: true)
  address: Address | null;      // Connected address, or null
  chainId?: number;             // Current chain ID (for chain verification)
  signTypedData: (data: TypedDataToSign) => Promise<string>;  // REQUIRED: EIP-712 signing
  switchChain?: (chainId: number) => Promise<void>;            // Switch chains
  login?: () => void;           // Open connect dialog
  logout?: () => Promise<void>; // Disconnect
  fundWallet?: (params: FundWalletParams) => Promise<void>;    // Buy crypto via on-ramp
  hasExternalWallet?: boolean;  // false hides Deposit/Withdraw tabs (default: true)
  sendTransaction?: (params: SendTransactionParams) => Promise<string>; // Raw tx sending
}
```

signTypedData is the most important method. Every transaction (deposit, withdraw, swap, transfer)
goes through EIP-712 typed data signing. The Compass API prepares the data, the user signs it,
and the server executes (optionally sponsoring gas).

## Server Handler Config

```ts
interface CompassHandlerConfig {
  apiKey: string;                    // Compass API key (required)
  serverUrl?: string;                // Custom API URL (default: https://api.compasslabs.ai)
  gasSponsorPrivateKey?: string;     // Private key for gas sponsorship
  rpcUrls?: {                        // RPC URLs (required if using gas sponsorship)
    ethereum?: string;
    base?: string;
    arbitrum?: string;
    hyperevm?: string;
  };
}
```

Supported GET routes: earn-account/check, earn-account/balances, swap/quote, token/balance,
token/prices, vaults, aave/markets, pendle/markets, positions, credit-account/check,
credit/positions, credit/balances, tx/receipt, traditional-investing/opportunities,
traditional-investing/positions

Supported POST routes: create-account, deposit/prepare, deposit/execute, withdraw/prepare,
withdraw/execute, transfer/approve, transfer/prepare, transfer/execute, bundle/prepare,
bundle/execute, swap/prepare, swap/execute, rebalance/preview, credit-account/create,
credit/bundle/prepare, credit/bundle/execute, credit/transfer, approval/execute,
traditional-investing/deposit, traditional-investing/deposit/execute,
traditional-investing/withdraw, traditional-investing/market-order,
traditional-investing/limit-order, traditional-investing/cancel-order,
traditional-investing/execute, traditional-investing/approve-builder-fee,
traditional-investing/ensure-leverage, traditional-investing/enable-unified-account

## Environment Variables

Required:
  COMPASS_API_KEY              - Your Compass API key (get one at https://compasslabs.ai)

Wallet provider (pick one):
  NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID  - WalletConnect project ID (for RainbowKit)
  NEXT_PUBLIC_PRIVY_APP_ID              - Privy app ID (for Privy)

Optional (for gas-sponsored transactions):
  GAS_SPONSOR_PK               - Private key of the gas sponsor wallet
  ETHEREUM_MAINNET_RPC_URL     - Ethereum RPC endpoint
  BASE_MAINNET_RPC_URL         - Base RPC endpoint
  ARBITRUM_MAINNET_RPC_URL     - Arbitrum RPC endpoint
  HYPEREVM_MAINNET_RPC_URL     - HyperEVM RPC endpoint (server-side gas sponsorship only)

## Supported Chains

Client-side widgets render these three chains:

| Chain | ID | Chain ID (numeric) |
|-------|----|--------------------|
| Ethereum | 'ethereum' | 1 |
| Base | 'base' | 8453 |
| Arbitrum | 'arbitrum' | 42161 |

Default chain: 'base'

Set via: `<CompassProvider defaultChain="base">`
Lock a widget to one chain: `<EarnAccount chain="base" />` (hides chain selector)

The server handler additionally accepts `hyperevm` in `rpcUrls` for gas sponsorship of HyperEVM transactions, but no React widget currently renders HyperEVM positions — only set this if you're calling the Compass API directly for HyperEVM and want gas sponsored.

## Supported Tokens

USDC, ETH, WETH, WBTC, cbBTC, USDT, DAI, SBC

## Gas Sponsorship

To sponsor gas so users don't need ETH:
1. Set gasSponsorPrivateKey in createCompassHandler config (wallet with ETH on each chain)
2. Set rpcUrls for each chain
When configured, widgets automatically use sponsored transactions.
Users sign EIP-712 typed data, the sponsor wallet pays gas and relays.
