@surf_liquid/surf-widget
Embeddable React widget for Surfliquid — drop DeFi yield into any app in minutes.
What it does
@surf_liquid/surf-widget gives your users a fully self-custodial yield account — backed by Surfliquid's on-chain strategy engine — without leaving your app. Users deposit USDC, the protocol deploys a personal smart contract vault for them, and the agent rebalances across the best yields automatically.
The widget handles everything:
- Smart contract vault deployment (first-time deposit)
- ERC-20 approval + deposit in a single, guided flow
- Real-time APY, balance, and earnings display
- Instant withdrawal
- On-chain activity feed with agent messages
How it fits together
Your App
└── @surf_liquid/core-sdk ← owns wallet connection & all on-chain calls
└── @surf_liquid/surf-widget ← consumes the client, renders the UI
Important: All wallet connections must go through
@surf_liquid/core-sdk. Do not pass a raw ethers signer or wagmi connector directly to the widget. TheSurfClientfrom the core SDK wraps your wallet and exposes the interface the widget depends on.
Installation
# 1. Install the core SDK (required — handles wallet + chain calls)
npm install @surf_liquid/core-sdk
# 2. Install the widget
npm install @surf_liquid/surf-widgetPeer dependencies (install if not already in your project):
npm install react react-domQuick Start
Step 1 — Get your App ID
Register your project at sdk.surfliquid.com to get an appId. This ties your integration to your project and unlocks access to the Surfliquid API.
Step 2 — Initialise and authenticate via core-sdk
import { SurfClient } from '@surf_liquid/core-sdk';
const client = SurfClient.create({
projectName: 'my-app', // identifies your integration in logs
appId: 'YOUR_APP_ID', // from https://sdk.surfliquid.com/
chainId: 8453, // 1 = Ethereum, 8453 = Base (default), 137 = Polygon
autoApprove: true, // handles ERC-20 approval automatically
});
await client.verifyApp(); // validate appId — throws INVALID_APP_ID if rejected
await client.connectWallet('metamask'); // or 'walletconnect', 'coinbase', etc.
await client.authenticate(); // establishes cookie session (token is always null — gate on auth.authenticated)Step 3 — Drop the widget into your React tree
import { SurfWidget } from '@surf_liquid/surf-widget';
export default function App() {
return (
<SurfWidget
appId="YOUR_APP_ID"
client={client}
walletAddress="0xYourAddress"
chainId={8453}
onSuccess={(action, txHash) => console.log(`${action} confirmed:`, txHash)}
onError={(action, err) => console.error(`${action} failed:`, err)}
/>
);
}That's it. No extra providers, no manual CSS imports — styles are fully scoped inside the widget.
SurfWidget Props
| Prop | Type | Default | Description |
|---|---|---|---|
appId |
string |
required | App ID from sdk.surfliquid.com |
client |
ISurfClient |
required | SurfClient instance from @surf_liquid/core-sdk |
walletAddress |
string |
required | Connected wallet address |
chainId |
number |
8453 |
Chain to operate on (1 Ethereum, 8453 Base, 137 Polygon) |
theme |
SurfTheme |
— | Override colors, radius, and font family |
className |
string |
— | Extra CSS class on the root element |
minDeposit |
number |
0.1 |
Minimum deposit amount in USDC |
onSuccess |
(action, txHash) => void |
— | Fired after a confirmed deposit or withdrawal |
onError |
(action, error) => void |
— | Fired when a deposit or withdrawal fails |
Theming
Override any design token via the theme prop. All values are optional — unset tokens fall back to the default Surfliquid palette.
<SurfWidget
client={client}
walletAddress={address}
theme={{
colors: {
primary: '#6366f1', // buttons, active states, progress lines
primaryText: '#ffffff', // text on primary-colored surfaces
background: '#0f0f11', // widget background
cardBackground: '#1a1a1f', // card/modal background
text: '#f4f4f5', // primary text
textSecondary: '#a1a1aa', // labels, descriptions
apy: '#22c55e', // APY percentage highlight
border: '#27272a', // dividers and outlines
success: '#22c55e', // success states
},
borderRadius: '16px',
fontFamily: 'Inter, sans-serif',
}}
/>All tokens are injected as CSS custom properties scoped to [data-surf-widget], so they never affect the rest of your app.
CSS Class Overrides
For deeper customisation, target any .surf-* class in your own stylesheet. Every rule is already scoped to [data-surf-widget], so your overrides need the same scope to take effect:
[data-surf-widget] .surf-card {
border-radius: 20px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
[data-surf-widget] .surf-btn--primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
[data-surf-widget] .surf-balance-amount {
font-size: 28px;
}Key class groups:
| Group | Classes |
|---|---|
| Card | .surf-card, .surf-card--selected |
| Buttons | .surf-btn, .surf-btn--primary, .surf-btn--outline, .surf-btn--soft, .surf-btn--ghost |
| Modal | .surf-modal, .surf-modal-overlay, .surf-modal-body |
| Amount input | .surf-amount-box, .surf-amount-input, .surf-amount-hint |
| Step progress | .surf-step, .surf-step--active, .surf-step-icon, .surf-step-title, .surf-step-desc, .surf-step-separator |
| Tabs | .surf-tabs, .surf-tab, .surf-tab--active |
| Stats | .surf-stat-value, .surf-stat-value--apy, .surf-balance-amount |
| Success screen | .surf-success, .surf-success-title, .surf-success-desc |
| Activity feed | .surf-activity-item, .surf-activity-icon |
Minimum Deposit
Control the minimum deposit amount with the minDeposit prop (default: 0.1 USDC):
<SurfWidget
client={client}
walletAddress={address}
minDeposit={10} // users must deposit at least 10 USDC
/>The widget enforces this in two ways:
- The deposit button stays disabled until the entered amount meets the minimum
- A hint label below the input shows
Min 10 USDC
Supported Chains
| Chain | Chain ID | USDC Address |
|---|---|---|
| Ethereum | 1 |
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Base | 8453 |
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Polygon | 137 |
0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
The widget automatically enforces the correct network. If the connected wallet is on a different chain, the card shows a "Switch to [Chain]" button that calls wallet_switchEthereumChain (and wallet_addEthereumChain if the chain isn't in the wallet yet). No configuration needed — just pass the desired chainId.
Using the Provider directly
For tighter layout control — e.g. rendering the vault card in one place and the deposit button somewhere else — wrap your tree with SurfProvider and consume context with useSurf anywhere inside it:
import { SurfProvider, useSurf } from '@surf_liquid/surf-widget';
function App() {
const containerRef = useRef<HTMLDivElement | null>(null);
return (
<div ref={containerRef}>
<SurfProvider
config={{ appId: 'YOUR_APP_ID', client, walletAddress: address, chainId: 8453, theme: myTheme }}
containerRef={containerRef}
>
<Dashboard />
</SurfProvider>
</div>
);
}
function Dashboard() {
const { walletAddress, chainId } = useSurf();
// ...
}Headless Usage — Hooks
Import individual hooks to build a fully custom UI while relying on Surfliquid's state management and on-chain logic.
All hooks must be called inside a <SurfProvider>.
useVault
Fetches the connected user's vault state.
import { useVault } from '@surf_liquid/surf-widget';
const { vault, isLoading, refetch } = useVault();
vault?.balance // "1234.56" — deposited USDC
vault?.earnings // "12.34" — total yield earned
vault?.apy // 8.7 — current APY (number)
vault?.apy7d // 8.95 — 7-day windowed APY (number | null)
vault?.apy14d // 8.80 — 14-day windowed APY (number | null)
vault?.apy30d // 8.60 — 30-day windowed APY (number | null)
vault?.availableBalance // withdrawable amountuseDeposit
Drives the full deposit flow: vault deploy → ERC-20 approve → deposit.
import { useDeposit } from '@surf_liquid/surf-widget';
const { step, isProcessing, execute, reset } = useDeposit();
// Kick off a deposit
await execute('100', vault);
// step.id progression:
// 'idle' → 'creating_contract' → 'approving' → 'depositing' → 'success' | 'error'useWithdraw
import { useWithdraw } from '@surf_liquid/surf-widget';
const { step, isProcessing, execute, reset } = useWithdraw();
await execute('100', vault);
// step.id: 'idle' | 'closing_position' | 'success' | 'error'useDepositBalance
Returns the wallet's USDC balance formatted for display.
import { useDepositBalance } from '@surf_liquid/surf-widget';
const { balance, isLoading } = useDepositBalance(vault);
// balance: "1500.00"useWithdrawableBalance
Returns the amount currently available to withdraw from the vault.
import { useWithdrawableBalance } from '@surf_liquid/surf-widget';
const { balance, isLoading } = useWithdrawableBalance(vault);useChainGuard
Detects whether the connected wallet is on the correct chain and provides a one-call switch.
import { useChainGuard } from '@surf_liquid/surf-widget';
const { walletChainId, isWrongChain, isSwitching, switchChain } = useChainGuard();
// walletChainId — the chain the wallet is currently on (null if no wallet detected)
// isWrongChain — true when walletChainId !== the chainId passed to SurfWidget
// isSwitching — true while wallet_switchEthereumChain is in flight
// switchChain() — prompts the wallet to switch; adds the chain if not present yet<SurfWidget> calls this internally — the "Switch to [Chain]" UI on the card is automatic. Use this hook only if you need the chain-guard state in your own custom UI.
useAgentMessages
Fetches paginated agent activity for the connected wallet.
import { useAgentMessages } from '@surf_liquid/surf-widget';
const { activities, isLoading, refetch } = useAgentMessages();
// activities[0].description — human-readable description
// activities[0].type — "deposit" | "withdraw"
// activities[0].protocol — e.g. "Surf Agent v2"
// activities[0].timestamp — Unix ms
// activities[0].txHash — on-chain transaction hash
// activities[0].chainId — chain where the tx occurred
// activities[0].amount — number | null
// activities[0].token — token symbol | null
// activities[0].fromVault — VaultRef | null ({ name, address, apy })
// activities[0].toVault — VaultRef | null
// activities[0].apyBefore — APY before the action | null
// activities[0].apyAfter — APY after the action | null
// activities[0].signal — strategy signal | nullHeadless Usage — Components
Use individual pre-built components inside your own layout:
import {
VaultCard,
DepositModal,
WithdrawModal,
VaultActivityModal,
ManageDropdown,
} from '@surf_liquid/surf-widget';UI Primitives
import {
Button,
Modal,
Tabs,
StepProgress,
TokenIcon,
} from '@surf_liquid/surf-widget';StepProgress
Renders a vertical step tracker — useful for any multi-step transaction flow.
import { StepProgress } from '@surf_liquid/surf-widget';
<StepProgress
baseExplorerUrl="https://basescan.org/tx/"
steps={[
{
id: 'create',
title: 'Surf Account Creation',
description: 'Deploy your self-custodial smart contract',
status: 'completed',
txHash: '0xabc...',
},
{
id: 'deposit',
title: 'Deposit USDC',
description: 'Fund your Surf Account',
status: 'active',
},
]}
/>Step status values: 'pending' | 'active' | 'completed' | 'error'
TypeScript
All types are exported from the package root:
import type {
// Config & theming
SurfWidgetProps,
SurfConfig,
SurfTheme,
// Domain models
VaultInfo,
VaultActivity,
VaultDeposit,
TokenInfo,
ChainInfo,
SurfVaultInfo,
SurfVaultAsset,
SurfSupportedAsset,
VaultRef,
VaultChainAddress,
// State machines
DepositStep,
WithdrawStep,
// Agent messages
AgentMessage,
AgentMessagesResult,
// Client interface — implement this to mock or extend
ISurfClient,
} from '@surf_liquid/surf-widget';ISurfClient
The widget depends on this interface, not on a specific SDK version. You can implement it yourself for testing:
import type { ISurfClient } from '@surf_liquid/surf-widget';
const mockClient: ISurfClient = {
getVault: async () => ({ exists: false, userVaultAddress: null }),
deployVault: async () => ({ vaultAddress: '0x...', transactionHash: '0x...', salt: '0x...' }),
deposit: async () => ({ hash: '0x...', wait: async () => {} }),
withdraw: async () => ({ hash: '0x...', wait: async () => {} }),
getSupportedAssets: async () => [],
getTokenBalance: async () => 0n,
getWithdrawableAmount: async () => 0n,
getAgentMessages: async () => ({ page: 1, limit: 10, total: 0, pages: 0, messages: [] }),
verifyApp: async () => {},
refreshSession: async () => ({ expiresAt: new Date().toISOString() }),
on: () => {},
off: () => {},
};Demo App
A fully working reference integration is available at:
https://github.com/Dev-zkCross/demo-sdk-surfliquid
It demonstrates:
- Connecting MetaMask via
@surf_liquid/core-sdk - Passing the
clientto<SurfWidget> - Handling
onSuccess/onErrorcallbacks - Custom theming
Wallet Support
The widget is wallet-agnostic — it works with any wallet you connect through @surf_liquid/core-sdk:
| Wallet | Support |
|---|---|
| MetaMask | |
| WalletConnect | |
| Coinbase Wallet | |
| Any EIP-1193 injected wallet | |
| Safe (Gnosis) |
Reminder: Always initialise
SurfClientfrom@surf_liquid/core-sdkfirst, then pass the resultingclientinstance to the widget. Never bypass the core SDK by passing a raw signer.
License
MIT Surfliquid