surfliquid
v0.1.1
Published
Framework-agnostic Surfliquid SDK for wallet auth, vault deployment, deposits, and withdrawals.
Maintainers
Readme
surfliquid
Framework-agnostic TypeScript SDK for Surfliquid — wallet authentication, vault deployment, deposits, withdrawals, portfolio queries, and activity feeds.
Table of Contents
- Installation
- Quick Start
- Initialization
- Step-by-Step Integration
- API Reference
- Events
- Types
- Error Handling
- Examples
Installation
npm install surfliquid ethersethers v6 is a required peer dependency.
Quick Start
import { SurfClient } from "surfliquid";
const client = SurfClient.create({
projectName: "my-app",
appId: "your-app-id",
autoApprove: true,
});
await client.connectWallet("metamask");
await client.authenticate();
const vault = await client.getVault();
if (!vault.exists) await client.deployVault();
const tx = await client.deposit({
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
amount: "10.0",
});
await tx.wait();Initialization
SurfClient.create(config) — recommended
const client = SurfClient.create({
projectName: "my-app", // Required — sent as X-Surf-Project-Name header
appId: "your-app-id", // Required — sent as X-App-ID header on first user login
environment: "mainnet", // "mainnet" | "testnet" — default: "mainnet"
chainId: 8453, // Default: Base (8453) on mainnet
autoApprove: true, // Auto-approve ERC20 spend before deposit — default: false
apiBaseUrl: "https://api.surfliquid.com", // Optional — uses default if omitted
});SurfClient.builder() — fluent API
Use this when you need to register custom chains, tokens, or wallet adapters at build time.
const client = SurfClient.builder()
.setProject("my-app", "your-app-id")
.setEnvironment("mainnet")
.setChain(8453)
.setAutoApprove(true)
.build();Config options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| projectName | string | Yes | — | Your project name |
| appId | string | Yes | — | Your app / project ID |
| environment | "mainnet" \| "testnet" | No | "mainnet" | Network environment |
| chainId | number | No | 8453 | Active chain ID |
| autoApprove | boolean | No | false | Auto-approve ERC20 allowance before deposits |
| rpcUrl | string | No | Chain default | Custom RPC endpoint |
| apiBaseUrl | string | No | https://api.surfliquid.com | Custom API base URL |
| factoryAddress | `0x${string}` | No | Chain default | Override vault factory address |
Supported chains
| Chain | Chain ID | Environment |
|-------|----------|-------------|
| Base | 8453 | mainnet |
| Polygon | 137 | mainnet |
| Base Sepolia | 84532 | testnet |
Step-by-Step Integration
1. Create the client
const client = SurfClient.create({
projectName: "my-app",
appId: "your-app-id",
autoApprove: true,
});2. Register event listeners
Set up listeners before connecting so no events are missed.
client.on("wallet:connected", ({ address, chainId }) => {
console.log("Connected:", address, "on chain", chainId);
});
client.on("auth:authenticated", ({ token }) => {
console.log("JWT:", token);
});
client.on("vault:deployed", ({ vaultAddress }) => {
console.log("Vault at:", vaultAddress);
});
client.on("deposit:completed", ({ asset, amount, txHash }) => {
console.log(`Deposited ${amount} of ${asset} — tx: ${txHash}`);
});
client.on("error", ({ code, message }) => {
console.error(`[${code}] ${message}`);
});3. Connect wallet
// Supported: "metamask" | "trust" | "coinbase" | "rabby" | "phantom" | "walletconnect" | "injected"
const state = await client.connectWallet("metamask");
console.log(state.address); // "0x..."
console.log(state.chainId); // 84534. Authenticate
const auth = await client.authenticate();
console.log(auth.authenticated); // true
console.log(auth.token); // "eyJ..."5. Check or deploy vault
const vault = await client.getVault();
if (!vault.exists) {
const result = await client.deployVault();
console.log("Deployed at:", result.vaultAddress);
} else {
console.log("Vault:", vault.userVaultAddress);
console.log("Total value: $" + vault.totalValueUSD);
console.log("APY:", vault.apyBreakdown?.currentAPY + "%");
}6. Deposit
const tx = await client.deposit({
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
amount: "10.0", // human-readable, not wei
});
await tx.wait();7. Check portfolio
const portfolio = await client.getPortfolioSummary();
console.log("Active assets:", portfolio.activeCount);
portfolio.assets.forEach((addr, i) => {
console.log(addr, "→ profit:", portfolio.profits[i].toString());
});8. Withdraw
// Partial
const tx = await client.withdraw({
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amount: "5.0",
});
await tx.wait();
// Full (omit amount)
const tx = await client.withdraw({
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
});
await tx.wait();API Reference
Wallet
connectWallet(walletName)
Connects a wallet and switches it to the configured chain.
const state = await client.connectWallet("metamask");
// state: { address, chainId, connected }disconnectWallet()
await client.disconnectWallet();getWalletState()
Returns current wallet state or null if not connected.
const state = client.getWalletState(); // WalletState | nullswitchChain(chainId)
await client.switchChain(137); // switch to PolygonregisterWalletAdapter(name, adapter)
Register a custom wallet adapter at runtime.
client.registerWalletAdapter("my-wallet", new MyAdapter());Authentication
authenticate()
Signs a nonce with the connected wallet and retrieves a JWT. Flow: getNonce → signMessage → login.
const auth = await client.authenticate();
// auth: { token, address, authenticated, user }Requires wallet connected.
getAuthState()
const auth = client.getAuthState(); // AuthState (synchronous, no request)logout()
await client.logout();Vault
getVault(walletAddress?)
Fetches vault info. Falls back to connected wallet if no address passed. Public — no auth required.
const vault = await client.getVault();
// or: await client.getVault("0xAnyAddress");
vault.exists // boolean
vault.userVaultAddress // "0x..." | null
vault.homeChainId // 8453
vault.vaultVersion // "v4"
vault.isActive // boolean
vault.totalValueUSD // 13.03
vault.totalDepositedUSD // 13.03
vault.apyBreakdown // { currentAPY, nativeAPY, merklAPY, leagueAPY }
vault.earned // { totalEarningsUSD, nativeEarningsUSD, merklRewardsUSD, leagueEarnedUSD }
vault.league // { rank, totalXP, estimatedSURF, estimatedSURFUSD, joinDate } | null
vault.assets // VaultAsset[] — per-chain asset breakdowndeployVault()
Deploys a new vault. Flow: prepare → on-chain deployVault → confirm.
const result = await client.deployVault();
result.vaultAddress // "0x..."
result.transactionHash // "0x..."
result.salt // "0x..."Requires wallet + auth. Throws
VAULT_ALREADY_EXISTSif vault exists.
getSupportedAssets(chainId?)
Returns supported assets with live APY data. Public — no wallet or auth required.
const assets = await client.getSupportedAssets(); // all chains
const assets = await client.getSupportedAssets(8453); // Base only
const assets = await client.getSupportedAssets(137); // Polygon only
assets[0].assetSymbol // "USDC"
assets[0].assetAddress // "0x..."
assets[0].chainId // 8453
assets[0].chainStatus // "ACTIVE" | "STAKING"
assets[0].currentAPY // 15.99
assets[0].nativeAPY // 13.08
assets[0].merklAPY // 0.05
assets[0].leagueAPY // 2.86getAgentMessages(walletAddress?, page?, limit?)
Returns paginated activity feed — deposits, withdrawals, rebalances, bridges, Merkl claims. Public.
const result = await client.getAgentMessages(); // connected wallet, page 1, limit 20
const result = await client.getAgentMessages("0x...", 2, 10);
result.total // 42
result.pages // 3
result.messages // AgentMessage[]
result.messages[0].message // human-readable description
result.messages[0].transactionType // "USER_DEPOSIT" | "CROSS_CHAIN_REBALANCE" | ...
result.messages[0].executedBy // "USER" | "AGENT"
result.messages[0].chainId // 8453
result.messages[0].txHash // "0x..."
result.messages[0].timestamp // "2026-04-02T12:21:41.000Z"getOwnerVaults(owner?) / getOwnerVaultCount(owner?)
const vaults = await client.getOwnerVaults(); // string[]
const count = await client.getOwnerVaultCount(); // numberisVaultFromFactory(vaultAddress)
const valid = await client.isVaultFromFactory("0x..."); // booleangetAllowedAssets(vaultAddress?)
const assets = await client.getAllowedAssets(); // string[]getFeeInfo(vaultAddress?)
const fees = await client.getFeeInfo();
fees.feePercentage // bigint
fees.rebalanceFeePercentage // bigint
fees.merklClaimFeePercentage // bigintDeposits & Withdrawals
deposit(params)
const tx = await client.deposit({
asset: "0x...", // token address
amount: "10.0", // human-readable (not wei)
wrapEth: false, // wrap native ETH → WETH first
bestVault: "0x...", // optional: override Morpho vault
});
await tx.wait();If
autoApprove: true, the SDK automatically approves the token spend if needed.
withdraw(params)
const tx = await client.withdraw({
asset: "0x...",
amount: "5.0", // omit for full withdrawal
});
await tx.wait();getWithdrawableAmount(asset, vaultAddress?)
const wei = await client.getWithdrawableAmount("0x..."); // bigintgetBestVault(assetSymbol)
const options = await client.getBestVault("USDC");
// [{ chainId: 8453, vaultAddress: "0x..." }, ...]hasInitialDeposit(asset, vaultAddress?)
const done = await client.hasInitialDeposit("0x..."); // booleanPortfolio
getPortfolioSummary(vaultAddress?)
const p = await client.getPortfolioSummary();
p.activeCount // number
p.assets // string[] — asset addresses
p.deposited // bigint[] — deposited amounts in wei
p.currentValues // bigint[] — current values in wei
p.profits // bigint[] — profits in weigetAssetProfit(asset, vaultAddress?)
const profit = await client.getAssetProfit("0x..."); // bigint (wei)getAssetProfitPercentage(asset, vaultAddress?)
const pct = await client.getAssetProfitPercentage("0x..."); // bigintToken Utilities
getTokenBalance(token, owner?)
const balance = await client.getTokenBalance("0xusdcAddress"); // connected wallet
const balance = await client.getTokenBalance("0xusdcAddress", "0x..."); // any address
// returns bigint (wei)getSupportedTokens()
Returns tokens registered in the SDK for the active chain (synchronous).
const tokens = client.getSupportedTokens();
// [{ address, symbol, decimals, morphoVaults }, ...]getConfig()
const config = client.getConfig();
config.chainId // 8453
config.environment // "mainnet"
config.apiBaseUrl // "https://api.surfliquid.com"Events
client.on("event:name", handler);
client.off("event:name", handler);| Event | Payload | When |
|-------|---------|------|
| wallet:connected | WalletState | Wallet connects |
| wallet:disconnected | void | Wallet disconnects |
| wallet:accountChanged | { oldAddress, newAddress } | Account switch |
| wallet:chainChanged | { chainId } | Chain switch |
| auth:authenticated | AuthState | Login complete |
| auth:logout | void | Logout called |
| vault:deployed | DeployVaultResult | Vault deployed |
| deposit:started | { asset, amount } | Deposit begins |
| deposit:approved | { asset, txHash } | ERC20 approval sent |
| deposit:completed | { asset, amount, txHash } | Deposit confirmed |
| withdraw:started | { asset, amount } | Withdrawal begins |
| withdraw:completed | { asset, amount, txHash } | Withdrawal confirmed |
| error | { code, message } | Any SDK error |
Types
interface WalletState {
address: string;
chainId: number;
connected: boolean;
}
interface AuthState {
token: string | null;
address: string | null;
authenticated: boolean;
user: UserProfile | null;
}
interface VaultInfo {
userVaultAddress: string | null;
deploymentSalt: string | null;
exists: boolean;
homeChainId?: number | null;
vaultVersion?: string | null;
isActive?: boolean;
totalValueUSD?: number | null;
totalDepositedUSD?: number | null;
earned?: VaultEarned | null;
apyBreakdown?: VaultApyBreakdown | null;
league?: VaultLeague | null;
assets?: VaultAsset[];
}
interface SupportedAsset {
assetAddress: string;
assetSymbol: string;
assetDecimals: number;
chainId: number;
chainStatus: string;
currentAPY: number;
nativeAPY: number;
merklAPY: number;
leagueAPY: number;
}
interface AgentMessage {
message: string;
txHash: string;
timestamp: string;
transactionType: "INITIAL_DEPOSIT" | "USER_DEPOSIT" | "USER_WITHDRAWAL" | "REBALANCE" | "CROSS_CHAIN_REBALANCE" | "MIGRATE" | string;
executedBy: "USER" | "AGENT";
vaultVersion: string;
chainId: number;
}
interface DepositParams {
asset: string;
amount: string;
vaultAddress?: string;
bestVault?: string;
wrapEth?: boolean;
}
interface WithdrawParams {
asset: string;
amount?: string;
vaultAddress?: string;
}
interface TransactionResult {
hash: string;
wait: () => Promise<any>;
}Error Handling
All errors are instances of SurfError with a typed code and message.
import { SurfError, SurfErrorCode } from "surfliquid";
try {
await client.deposit({ asset: "0x...", amount: "10" });
} catch (err) {
if (err instanceof SurfError) {
switch (err.code) {
case SurfErrorCode.WALLET_NOT_CONNECTED: break;
case SurfErrorCode.INSUFFICIENT_BALANCE: break;
case SurfErrorCode.DEPOSIT_FAILED: break;
}
}
}| Code | Cause |
|------|-------|
| INVALID_CONFIG | Missing or invalid configuration |
| MISSING_PROJECT_ID | appId not provided |
| UNSUPPORTED_CHAIN | chainId not registered for environment |
| WALLET_NOT_INSTALLED | Wallet extension not found |
| WALLET_NOT_CONNECTED | Operation requires connected wallet |
| WALLET_REJECTED | User rejected the wallet request |
| WRONG_CHAIN | Wallet is on wrong chain |
| AUTH_FAILED | Login request failed |
| SIGNATURE_REJECTED | User rejected message signing |
| VAULT_NOT_FOUND | No vault exists for this address |
| VAULT_ALREADY_EXISTS | Vault already deployed |
| VAULT_DEPLOY_FAILED | On-chain deployment failed |
| INSUFFICIENT_BALANCE | Token balance too low |
| APPROVE_FAILED | ERC20 approval failed |
| DEPOSIT_FAILED | Deposit transaction failed |
| WITHDRAW_FAILED | Withdrawal transaction failed |
| NO_BEST_VAULT | No Morpho vault found for asset |
| API_ERROR | Backend API error |
| RPC_ERROR | RPC/blockchain call failed |
| TRANSACTION_FAILED | On-chain transaction failed |
Examples
React
import { useEffect, useState } from "react";
import { SurfClient } from "surfliquid";
const client = SurfClient.create({
projectName: "my-app",
appId: "your-app-id",
autoApprove: true,
});
export function App() {
const [address, setAddress] = useState<string | null>(null);
const [vault, setVault] = useState<string | null>(null);
useEffect(() => {
client.on("wallet:connected", (s) => setAddress(s.address));
client.on("wallet:disconnected", () => setAddress(null));
}, []);
async function setup() {
await client.connectWallet("metamask");
await client.authenticate();
const info = await client.getVault();
if (!info.exists) {
const r = await client.deployVault();
setVault(r.vaultAddress);
} else {
setVault(info.userVaultAddress);
}
}
async function deposit() {
const tx = await client.deposit({
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amount: "10.0",
});
await tx.wait();
}
return (
<div>
<button onClick={setup}>Connect & Setup</button>
{address && <p>Wallet: {address}</p>}
{vault && <button onClick={deposit}>Deposit 10 USDC</button>}
</div>
);
}Vanilla JS
<script type="module">
import { SurfClient } from "/node_modules/surfliquid/dist/index.js";
const client = SurfClient.create({
projectName: "my-app",
appId: "your-app-id",
autoApprove: true,
});
// No wallet needed — fetch supported assets
const assets = await client.getSupportedAssets(8453);
console.table(assets.map(a => ({
symbol: a.assetSymbol,
APY: a.currentAPY + "%",
address: a.assetAddress,
})));
// Full flow
await client.connectWallet("metamask");
await client.authenticate();
const vault = await client.getVault();
if (!vault.exists) await client.deployVault();
const tx = await client.deposit({
asset: assets[0].assetAddress,
amount: "10.0",
});
await tx.wait();
</script>Node.js (read-only)
import { SurfClient } from "surfliquid";
const client = SurfClient.create({
projectName: "dashboard",
appId: "your-app-id",
});
// No wallet needed for public endpoints
const vault = await client.getVault("0xUserWalletAddress");
console.log("Vault:", vault.userVaultAddress);
console.log("Value: $" + vault.totalValueUSD);
const assets = await client.getSupportedAssets();
console.log("Supported:", assets.map(a => a.assetSymbol));
const { messages } = await client.getAgentMessages("0xUserWalletAddress");
messages.forEach(m => console.log(`[${m.executedBy}] ${m.message}`));Custom wallet adapter
import { SurfClient, IWalletAdapter } from "surfliquid";
import type { ContractRunner } from "ethers";
class MyWalletAdapter implements IWalletAdapter {
async connect(chainId: number) { /* return WalletState */ }
async disconnect() {}
async switchChain(chainId: number) {}
async getSigner(): Promise<ContractRunner> { /* return ethers Signer */ }
async signMessage(message: string): Promise<string> { /* return sig */ }
onAccountsChanged(cb: (accounts: string[]) => void) {}
onChainChanged(cb: (chainId: number) => void) {}
onDisconnect(cb: () => void) {}
}
const client = SurfClient.builder()
.setProject("my-app", "your-app-id")
.registerWalletAdapter("my-wallet", new MyWalletAdapter())
.build();
await client.connectWallet("my-wallet");WalletConnect
import { SurfClient, WalletConnectAdapter } from "surfliquid";
import { EthereumProvider } from "@walletconnect/ethereum-provider";
const client = SurfClient.create({ projectName: "my-app", appId: "your-app-id" });
const wcProvider = await EthereumProvider.init({
projectId: "your-walletconnect-project-id", // from cloud.walletconnect.com
chains: [8453],
showQrModal: true,
});
client.registerWalletAdapter("walletconnect", new WalletConnectAdapter(() => wcProvider));
await client.connectWallet("walletconnect");Register custom chain / token
const client = SurfClient.builder()
.setProject("my-app", "your-app-id")
.registerChain("mainnet", {
chainId: 42161,
rpcUrl: "https://arb1.arbitrum.io/rpc",
factoryAddress: "0x...",
wethAddress: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
tokens: [],
})
.registerToken("mainnet", 42161, {
address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
symbol: "USDC",
decimals: 6,
morphoVaults: [],
})
.setChain(42161)
.build();Notes
projectNameandappIdare required.projectIdis accepted as a backwards-compatible alias forappId.- Default environment is
mainnet; default mainnet chain is Base (8453). - Amounts passed to
deposit()andwithdraw()are human-readable strings (e.g."10.5"), not wei. - Withdraw amounts are encoded using the token's on-chain
decimals()value. getVault(),getSupportedAssets(), andgetAgentMessages()are public — no wallet or auth required when passing an explicit wallet address.- Set
autoApprove: trueon the client to skip manual ERC20 approval before deposits.
