npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@surf_liquid/core-sdk

v0.4.1

Published

Framework-agnostic Surfliquid SDK for wallet auth, vault deployment, deposits, and withdrawals.

Readme

@surf_liquid/core-sdk

Framework-agnostic TypeScript SDK for Surfliquid — wallet authentication, vault deployment, deposits, withdrawals, portfolio queries, and activity feeds.

Integration examples & AI skill: runnable sample apps and an integration SKILL.md live in the surfliquid-sdk-core-integration repo.

Table of Contents


Installation

npm install @surf_liquid/core-sdk ethers

ethers v6 is a required peer dependency.


Quick Start

import { SurfClient } from "@surf_liquid/core-sdk";

const client = SurfClient.create({
  projectName: "my-app",
  appId: "your-app-id",
  autoApprove: true,
});

await client.verifyApp();          // validate your appId (throws on a bad/missing appId)
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 | | Ethereum | 1 | mainnet | | Polygon | 137 | mainnet | | Base Sepolia | 84532 | testnet |


Gas on Ethereum mainnet (low-fee defaults)

On Ethereum mainnet (chainId: 1) only, the SDK attaches explicit EIP-1559 fee overrides to every transaction it sends (vault deploy, deposit, withdraw, and the ERC-20 approval / WETH-wrap steps) so transactions cost as little as possible. On every other chain (Base, Polygon, Base Sepolia, and any custom chain) the SDK supplies no fee overrides and lets the wallet/provider pick fees as usual — this behavior is Ethereum-specific and there is no integration change required on any chain.

What the SDK sets on Ethereum:

  • type: 2 (EIP-1559) is set explicitly. This is deliberate: without an explicit type: 2, wallets such as MetaMask ignore a dapp's supplied 1559 fees and substitute their own "market" estimate, which is higher.
  • maxPriorityFeePerGas (the tip) is derived from eth_feeHistory over the last ~20 blocks: the SDK takes the per-block 10th-percentile priority fees, uses their median, and floors the result at 0.1 gwei (100_000_000 wei) so the tx stays includable.
  • maxFeePerGas is baseFee * 1.25 + tip — i.e. only 1.25x base-fee headroom, not the 2x "survive 6 blocks" headroom that ethers/EIP-1559 defaults to.
  • If eth_feeHistory is unavailable, it falls back to the latest block's baseFeePerGas plus the 0.1 gwei floor tip.

"Site suggested" vs the wallet's own "Low" preset. These values are tuned to line up with a wallet's "Low" fee tier. The reason the 2x headroom was dropped to 1.25x is that MetaMask was surfacing the 2x figure as a scarily high "max" on site-suggested (dapp-supplied) transactions, even though the effective cost is only ~baseFee + tip. With the 1.25x cap, the SDK's "Site suggested" fee no longer reads higher than the wallet's own lowest (Low) option. The trade-off is the same one the wallet's Low tier makes: a sharp base-fee spike between submission and inclusion can delay mining. Users who want faster inclusion can still override the fee in their wallet at confirmation time.

Effective cost is still approximately baseFee + tip; the headroom only caps the worst-case ceiling — you are not charged the max.

Robust write-gas handling (Ethereum mainnet)

To make first deposits and back-to-back deposit/withdraw flows reliable, the SDK pre-estimates the gas limit for write transactions itself (via the read RPC) and passes it to the wallet as an explicit gasLimit, instead of letting the wallet re-estimate. This is scoped to Ethereum mainnet (chainId: 1) — the chain where the failures below were observed and where the explicit low-fee overrides already apply — and covers deployVault, deposit (both the initialDeposit and userDeposit paths), and withdraw. A small 20% buffer is added to the estimate so a slight state shift between estimate and mining can't cause out-of-gas (you still only pay for gas actually used).

The estimate is done against the SDK's own read provider with the connected wallet as from. If that estimate hits a genuine contract revert, the error is re-thrown immediately (retrying wouldn't help); if it fails for a transient/non-revert reason, the SDK returns no gas limit and falls back to letting the wallet estimate — so robustness never makes a call strictly worse.

Deposit allowance-settle wait. When autoApprove sends an approval before a deposit, the SDK then polls the allowance (up to 8 times, ~1s apart) via the read provider until the new allowance is visible before submitting the deposit. If the budget elapses it proceeds anyway (a genuinely-insufficient allowance surfaces on the deposit itself).

Which estimateGas failures this fixes:

  • Post-approval first-deposit revert. The deposit's gas estimation can race the approval: if the estimating node hasn't yet observed the freshly-set allowance, the deposit's transferFrom reverts during estimateGas with require(false) / no revert data. The allowance-settle wait plus SDK-side pre-estimation removes this race.
  • Stale-state re-estimation on deposit/withdraw. Submitting a withdraw (or a second deposit) right after a deposit could make the wallet re-estimate against stale state and fail with missing revert data / could not decode result data. Pre-estimating against the read provider and passing an explicit gasLimit avoids the wallet's faulty re-estimate.

All of this is internal — no integration change is required; deposits/withdrawals/deploys are simply more reliable on Ethereum.


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", ({ address, authenticated }) => {
  console.log("Authenticated:", authenticated, "as", address);
});
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);  // 8453

4. Authenticate

const auth = await client.authenticate();
console.log(auth.authenticated); // true
// Auth is cookie-based: the session token lives in an httpOnly cookie set by the
// backend, so `auth.token` is always null. Gate on `auth.authenticated` instead.
console.log(auth.token);         // null

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

Setup

verifyApp()

Validates your appId against the backend (GET /api/sdk/public/overview with the x-app-id header). Call it right after create() to fail fast on a bad app ID. Throws MISSING_PROJECT_ID if no appId was provided, or INVALID_APP_ID if the backend rejects it.

await client.verifyApp(); // resolves if the appId is valid; throws otherwise

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 | null

switchChain(chainId)

Re-points the whole SDK at chainId: it updates config.chainId, rpcUrl, factoryAddress, and the read provider from the chain registry, and switches the connected wallet too (works read-only without a wallet). Throws UNSUPPORTED_CHAIN for a chain not registered for the active environment. Your auth (the cookie session) is preserved — it's per-wallet, not per-chain. Any custom rpcUrl / factoryAddress you set at creation are replaced by the target chain's registry values.

await client.switchChain(137); // SDK now operates on Polygon

registerWalletAdapter(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 logs in. Auth is cookie-based: on login the backend sets an httpOnly session cookie (SameSite=None; Secure) — the token is never returned in the response body, so auth.token is always null. Gate on auth.authenticated / auth.user, never on auth.token. Flow: getNoncesignMessagelogin.

const auth = await client.authenticate();
// auth: { token, address, authenticated, user } — token is always null (lives in the cookie)

Requires wallet connected.

Cross-origin: the SDK sends every request with credentials: "include" (no Authorization: Bearer header). For the cookie to work cross-origin the API must return a specific Access-Control-Allow-Origin (never *) plus Access-Control-Allow-Credentials: true, and your web origin must be allowlisted.

refreshSession()

Extends the session using the existing httpOnly cookie — no wallet signature and no body. Calls POST /api/auth/refresh; the backend rotates the cookie (new 7-day token) and invalidates the old one immediately. Returns { expiresAt } (ISO 8601). Call it proactively before expiry (e.g. when less than 24h remain) to keep the session alive without re-signing.

const { expiresAt } = await client.refreshSession();
console.log("Session valid until", expiresAt);

Throws SurfError (API_ERROR, status 401) if the cookie is missing, expired, or revoked.

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, totalAPY?, apy7d?, apy14d?, apy30d? } — windowed APYs are portfolio-weighted, null until enough history
vault.earned             // { totalEarningsUSD, nativeEarningsUSD, merklRewardsUSD, leagueEarnedUSD }
vault.league             // { rank, totalXP, estimatedSURF, estimatedSURFUSD, joinDate } | null
vault.assets             // VaultAsset[] — per-chain asset breakdown

deployVault()

Deploys a new vault. Flow: prepare → on-chain deployVaultconfirm.

const result = await client.deployVault();
result.vaultAddress      // "0x..."
result.transactionHash   // "0x..."
result.salt              // "0x..."

Requires a connected wallet (the SDK enforces a wallet, not auth — the backend still expects an authenticated session). Throws VAULT_ALREADY_EXISTS if a vault already 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.86
assets[0].apy7d         // 14.80 — trailing 7-day APY (null until 7 days of history)
assets[0].apy14d        // 15.10 — trailing 14-day APY (null until 14 days)
assets[0].apy30d        // 15.40 — trailing 30-day APY (null until 30 days)

getAgentMessages(walletAddress?, page?, limit?, from?, to?)

Returns paginated activity feed — deposits, withdrawals, rebalances, bridges, Merkl claims. Public. from/to are optional ISO-8601 strings; when provided, results are filtered by timestamp inclusive on both ends.

const result = await client.getAgentMessages();           // connected wallet, page 1, limit 20
const result = await client.getAgentMessages("0x...", 2, 10);

// Filter by a date range (ISO 8601, inclusive on both ends)
const result = await client.getAgentMessages(
  "0x...", 1, 20,
  "2026-01-01T00:00:00.000Z", // from
  "2026-03-31T23:59:59.999Z", // to
);

result.total    // 42
result.pages    // 3
result.messages // AgentMessage[]

result.messages[0].message          // human-readable description
result.messages[0].transactionType  // "USER_DEPOSIT" | "REBALANCE_COMPLETED" | "MERKL_CLAIM" | ...
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"

// Structured fields — present only when applicable to the event type:
result.messages[0].amount     // 10.0 | null
result.messages[0].token      // "USDC" | null
result.messages[0].fromVault  // { name, address, apy } | null
result.messages[0].toVault    // { name, address, apy } | null
result.messages[0].apyBefore  // 13.2 | null
result.messages[0].apyAfter   // 15.4 | null
result.messages[0].signal     // string | null

getOwnerVaults(owner?) / getOwnerVaultCount(owner?)

const vaults = await client.getOwnerVaults();   // string[]
const count  = await client.getOwnerVaultCount(); // number

isVaultFromFactory(vaultAddress)

const valid = await client.isVaultFromFactory("0x..."); // boolean

getAllowedAssets(vaultAddress?)

const assets = await client.getAllowedAssets(); // string[]

getFeeInfo(vaultAddress?)

const fees = await client.getFeeInfo();
fees.feePercentage            // bigint
fees.rebalanceFeePercentage   // bigint
fees.merklClaimFeePercentage  // bigint

Deposits & 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..."); // bigint

getBestVault(assetSymbol)

const options = await client.getBestVault("USDC");
// [{ chainId: 8453, vaultAddress: "0x..." }, ...]

hasInitialDeposit(asset, vaultAddress?)

const done = await client.hasInitialDeposit("0x..."); // boolean

Portfolio

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 wei

getAssetProfit(asset, vaultAddress?)

const profit = await client.getAssetProfit("0x..."); // bigint (wei)

getAssetProfitPercentage(asset, vaultAddress?)

const pct = await client.getAssetProfitPercentage("0x..."); // bigint

Token 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, vaultAddresses }, ...]

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; // Always null under cookie auth — the token lives in an httpOnly cookie, not here
  address: string | null;
  authenticated: boolean;
  user: UserProfile | null;
}

interface RefreshResult {
  expiresAt: string; // ISO 8601 expiry of the freshly issued session token
}

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[];
  // Per-chain vault addresses. A vault can be deployed at a different address per
  // chain (e.g. Ethereum), so this can differ from userVaultAddress.
  chainAddresses?: VaultChainAddress[];
}

interface VaultChainAddress {
  chainId: number;
  vaultAddress: string;
}

interface VaultApyBreakdown {
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  totalAPY?: number;
  // Trailing-window APYs (portfolio-weighted). Null until enough history exists.
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
}

interface VaultAsset {
  assetAddress: string;
  assetSymbol: string;
  assetDecimals: number;
  chainId: number;
  chainStatus: string;
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  // Trailing-window APYs for this asset. Null until enough history exists.
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
  // ...balances, earnings, and vault metadata
}

interface SupportedAsset {
  assetAddress: string;
  assetSymbol: string;
  assetDecimals: number;
  chainId: number;
  chainStatus: string;
  currentAPY: number;
  nativeAPY: number;
  merklAPY: number;
  leagueAPY: number;
  // Trailing-window APYs. Null until enough history exists (e.g. apy7d is null with < 7 days).
  apy7d?: number | null;
  apy14d?: number | null;
  apy30d?: number | null;
}

interface VaultRef {
  name: string;
  address: string;
  apy: number;
}

interface AgentMessage {
  message: string;
  txHash: string;
  timestamp: string;
  transactionType:
    | "INITIAL_DEPOSIT" | "USER_DEPOSIT" | "USER_WITHDRAWAL" | "REBALANCE" | "CROSS_CHAIN_REBALANCE" | "MIGRATE"
    | "DEPOSIT" | "WITHDRAWAL" | "REBALANCE_COMPLETED" | "REBALANCE_CANCELLED" | "MERKL_CLAIM"
    | "ASSET_ADDED" | "ASSET_REMOVED" | "ASSET_SWAPPED"
    | string;
  executedBy: "USER" | "AGENT";
  vaultVersion: string;
  chainId: number;
  // Optional structured fields — omitted when not applicable to the event type:
  amount?: number | null;
  token?: string | null;
  fromVault?: VaultRef | null;
  toVault?: VaultRef | null;
  apyBefore?: number | null;
  apyAfter?: number | null;
  signal?: string | null;
}

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 "@surf_liquid/core-sdk";

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 | | INVALID_APP_ID | appId rejected by the backend (verifyApp()) | | UNSUPPORTED_CHAIN | chainId not registered for environment | | ENVIRONMENT_CHAIN_MISMATCH | chainId doesn't belong to the configured 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

Full runnable examples + AI integration guide: surfliquid-sdk-core-integration — a React + Vite sample app and a SKILL.md an AI agent can use to integrate this SDK. The snippets below are quick references.

React

import { useEffect, useState } from "react";
import { SurfClient } from "@surf_liquid/core-sdk";

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/@surf_liquid/core-sdk/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 "@surf_liquid/core-sdk";

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 "@surf_liquid/core-sdk";
import type { Signer } from "ethers";

class MyWalletAdapter implements IWalletAdapter {
  async connect(chainId: number) { /* return WalletState */ }
  async disconnect() {}
  async switchChain(chainId: number) {}
  async getSigner(): Promise<Signer> { /* 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 "@surf_liquid/core-sdk";
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,
    vaultAddresses: [],
  })
  .setChain(42161)
  .build();

Notes

  • projectName and appId are required. projectId is accepted as a backwards-compatible alias for appId.
  • Default environment is mainnet; default mainnet chain is Base (8453).
  • Amounts passed to deposit() and withdraw() are human-readable strings (e.g. "10.5"), not wei.
  • Withdraw amounts are encoded using the token's on-chain decimals() value.
  • getVault(), getSupportedAssets(), and getAgentMessages() are public — no wallet or auth required when passing an explicit wallet address.
  • Set autoApprove: true on the client to skip manual ERC20 approval before deposits.
  • Auth is cookie-based. Login sets an httpOnly session cookie (SameSite=None; Secure); the token is never in the response body, so authState.token is always null. Gate on authState.authenticated / authState.user. The SDK sends every request with credentials: "include" (no Authorization: Bearer header), so the API must return a specific Access-Control-Allow-Origin (never *) plus Access-Control-Allow-Credentials: true, and your web origin must be allowlisted. Use refreshSession() to rotate the cookie before it expires without re-signing.
  • A vault can have a different address per chain. VaultInfo.chainAddresses lists per-chain vault addresses; on some chains (e.g. Ethereum) the address differs from userVaultAddress (the home-chain address). The SDK resolves the correct per-chain address automatically when you omit the vaultAddress argument on deposit/withdraw/getPortfolioSummary/getWithdrawableAmount/etc. Don't pass userVaultAddress explicitly on a non-home chain — it points at an address with no vault contract there.