SDK Reference
@earnforge/sdk is the foundation package. Every other EarnForge surface (CLI, React, MCP, skill, Studio) is built on top of it.
Installation
Section titled “Installation”npm i @earnforge/sdkpnpm add @earnforge/sdkyarn add @earnforge/sdkcreateEarnForge()
Section titled “createEarnForge()”The main entry point. Returns an EarnForge instance with namespaced methods.
import { createEarnForge } from '@earnforge/sdk';
const forge = createEarnForge({ apiKey: process.env.LIFI_API_KEY, // Required -- earn.li.fi 401s without it cache: { ttl: 60_000, maxSize: 200 }, // Optional -- LRU cache config});Options
Section titled “Options”| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
process.env.LIFI_API_KEY |
Required. One key authenticates both the Earn Data API and Composer. |
composerApiKey |
string |
falls back to apiKey |
Only needed if you hold separate keys for the two services. |
earnData |
EarnDataClientOptions |
{} |
Override the Earn Data API client options (base URL, headers). |
composerBaseUrl |
string |
"https://li.quest" |
Override the Composer API base URL. |
cache.ttl |
number |
60000 |
Cache time-to-live in milliseconds. |
cache.maxSize |
number |
200 |
Maximum number of cached entries. |
Vault Methods
Section titled “Vault Methods”vaults.list()
Section titled “vaults.list()”Fetch a single page of vaults (max 100 per page, cursor-based pagination).
const page = await forge.vaults.list({ chainId: 8453, // Filter by chain (number, NOT chain name) asset: 'USDC', // Filter by underlying token symbol minTvl: 1_000_000, // Minimum TVL in USD sortBy: 'apy', // Sort field strategy: 'conservative', // Apply strategy preset filters cursor: undefined, // Pagination cursor from previous response});
console.log(page.data); // Vault[]console.log(page.nextCursor); // string | nullvaults.listAll()
Section titled “vaults.listAll()”Async iterator that auto-paginates through all vaults. Follows nextCursor until exhausted.
const vaults: Vault[] = [];for await (const vault of forge.vaults.listAll({ chainId: 8453 })) { vaults.push(vault);}console.log(`Found ${vaults.length} vaults on Base`);vaults.get()
Section titled “vaults.get()”Fetch a single vault by its slug. Live slugs look like
protocol:chainId:_:address; the legacy chainId-address form is still
accepted so stored references keep resolving.
const vault = await forge.vaults.get('morpho:8453:_:0xee8f4ec5672f09119b96ab6fb59c27e1b7e44b61');console.log(vault.name, vault.analytics.apy.total);vaults.top()
Section titled “vaults.top()”Fetch the highest-APY vaults with optional filters and strategy presets.
const top = await forge.vaults.top({ asset: 'USDC', chainId: 8453, limit: 10, strategy: 'risk-adjusted', minTvl: 5_000_000,});| Parameter | Type | Default | Description |
|---|---|---|---|
asset |
string |
all | Underlying token symbol filter |
chainId |
number |
all | EVM chain ID filter |
limit |
number |
10 |
Maximum vaults to return |
strategy |
StrategyPreset |
none | Apply strategy preset filters |
minTvl |
number |
none | Minimum TVL in USD |
Reference Data
Section titled “Reference Data”chains.list()
Section titled “chains.list()”Returns every chain with at least one indexed vault: 19 as of Aug 2026, and rising. Read it rather than hardcoding a list.
const chains = await forge.chains.list();// [{ id: 1, name: 'Ethereum', ... }, { id: 8453, name: 'Base', ... }, ...]protocols.list()
Section titled “protocols.list()”Returns every indexed protocol: 27 as of Aug 2026. Ids are read from here and
never hardcoded: a stale id returns 200 with zero results, not an error.
const protocols = await forge.protocols.list();// [{ name: 'aave', url: '...' }, { name: 'morpho', url: '...' }, ...]portfolio.get()
Section titled “portfolio.get()”Fetch all Earn positions for a wallet address.
const portfolio = await forge.portfolio.get('0xYourWalletAddress');for (const position of portfolio.positions) { console.log(position.protocolName, position.asset.symbol, position.balanceUsd);}Deposit Flow
Section titled “Deposit Flow”buildDepositQuote()
Section titled “buildDepositQuote()”Build an unsigned deposit transaction via the LI.FI Composer API. Requires a composerApiKey.
const result = await forge.buildDepositQuote(vault, { fromAmount: '100', // Human-readable amount (e.g. "100" for 100 USDC) wallet: '0xYourWallet', fromToken: undefined, // Optional: override source token address fromChain: undefined, // Optional: override source chain ID for cross-chain slippage: 0.03, // Optional: slippage tolerance (3%)});
console.log(result.humanAmount); // "100"console.log(result.rawAmount); // "100000000" (for 6-decimal USDC)console.log(result.decimals); // 6console.log(result.quote); // Full LI.FI quote with transactionRequestpreflight()
Section titled “preflight()”Run pre-deposit checks against a vault. Returns a PreflightReport with pass/fail status
and any issues found.
const report = forge.preflight(vault, '0xYourWallet', { walletChainId: 1, // Optional: detect chain mismatch depositAmount: '100', // Optional: check balance sufficiency});
if (!report.ok) { for (const issue of report.issues) { console.warn(`[${issue.severity}] ${issue.code}: ${issue.message}`); }}Checks performed:
| Code | Description |
|---|---|
NOT_TRANSACTIONAL |
Vault cannot accept deposits |
CHAIN_MISMATCH |
Wallet on wrong chain |
NO_GAS |
Insufficient native token for gas |
EMPTY_UNDERLYING_TOKENS |
Vault has no underlying tokens listed |
NOT_REDEEMABLE |
Vault may not support withdrawals |
Risk Scoring
Section titled “Risk Scoring”riskScore()
Section titled “riskScore()”Compute a composite 0-10 risk score for a vault. Higher score = safer.
const risk = forge.riskScore(vault);console.log(risk.score); // 5.5console.log(risk.label); // "high"console.log(risk.breakdown);// { tvl: 3, apyStability: 10, protocol: 7, redeemability: 10,// assetType: 9, verification: 1, rewardDependency: 2 }console.log(risk.flags);// [ 'flagged by LI.FI verification: apy_outlier',// '87% of APY comes from token incentives',// 'analytics 2514 minutes stale' ]Seven dimensions, not five: verification carries LI.FI’s own undocumented
quality signal, and rewardDependency how much of the APY is emissions. The
flags array is the actionable part: the score says something is wrong, the
flags say what.
The 8 threshold is deliberate. Verification carries 0.22 of the weight, so a
flagged vault caps at 7.96: no flagged vault can ever be labelled low risk.
See the full Risk Scoring Guide for dimension details, weights, and thresholds.
Reward Sustainability
Section titled “Reward Sustainability”Answers the question a headline APY cannot: is this real, or an incentive that ends?
rewardSustainability()
Section titled “rewardSustainability()”import { rewardSustainability } from '@earnforge/sdk';
const s = rewardSustainability(vault);console.log(s.label); // "incentive-dependent"console.log(s.organicApy); // 13.76, what remains if emissions stopconsole.log(s.rewardShare) // 0.87console.log(s.reasons);// [ 'Incentives are 87% of total APY. Organic yield is only 13.76%.' ]Labels are organic, mostly-organic, incentive-heavy,
incentive-dependent, or unknown.
analyzeRewardSustainability()
Section titled “analyzeRewardSustainability()”The async form additionally reads the DeFiLlama APY curve, because a decaying curve separates this emission exists from this emission is ending.
const s = await analyzeRewardSustainability(vault);console.log(s.trend); // { direction: 'decaying', change: -0.41, samples: 30 }History is advisory: if DeFiLlama has no match, the base score stands rather than the call failing.
Composer Flows
Section titled “Composer Flows”/v1/quote builds one step at a time, so swap-then-deposit is two transactions
and the second has to guess what the first returned. A Flow binds the swap’s
amountOut handle straight into the deposit’s amountIn. The exact received
amount, one transaction, atomic.
createComposerFlows()
Section titled “createComposerFlows()”import { createComposerFlows } from '@earnforge/sdk';
const flows = createComposerFlows({ apiKey: process.env.LIFI_API_KEY });
const sim = await flows.buildDepositFlow({ vault, wallet: '0xYourWallet', fromToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC amount: '1000000', // base units: 1 USDC, never a decimal slippageBps: 100,});
if (sim.ok) { console.log(sim.transaction); // unsigned, ready to sign console.log(sim.producedResources); // what you would actually receive} else { console.log(sim.revert); // structured revert diagnostics}The slippage guard attaches to the zap, not the swap: lifi.swap’s amountOut
declares providesMinimum because the aggregator already bakes minOut in, and
a second guard there is a compile-time guard_error rather than extra safety.
vaultRoutability()
Section titled “vaultRoutability()”Answers what no Earn field does. The vault list is a superset of what Composer can execute.
const r = await flows.vaultRoutability(vault);// { canEnter: true, canExit: false, edgeCount: 1 }Protocols ingested from DeFiLlama appear in the vault list with no routing edges
at all, and several protocols are deposit-only. So a vault can be listed, carry
isTransactional: true, and still be impossible to enter: or possible to enter
and impossible to exit.
Aave v3 Lifecycle
Section titled “Aave v3 Lifecycle”Everything else here deals in deposits and withdrawals, where the worst outcome is a failed transaction. Borrowing is different: a position can be liquidated while you do nothing, so the borrow path is paired with a health-factor projection rather than offered bare.
import { getAaveAccountData, assertBorrowSafe } from '@earnforge/sdk';
const account = await getAaveAccountData(rpcUrl, AAVE_POOL, wallet);console.log(account.healthFactor); // 1.65, or null when there is no debt
// Throws if the borrow would breach the floor, naming the largest that would not.const projection = assertBorrowSafe(account, 1_000_00000000n, 1.5);console.log(projection.after); // 1.62console.log(projection.maxSafeBorrowBase); // headroom, in oracle base unitsprojectBorrow() is the non-throwing form. A floor at or below 1.0 is
rejected outright: a position that reaches Aave’s liquidation threshold is
already liquidatable, so a floor there means “warn me once it is too late”.
Drift Detection
Section titled “Drift Detection”Born from this project shipping broken for three months. Compares three sources (the live API, LI.FI’s OpenAPI spec, and our schemas) and reports which pair disagrees, so a vendor documentation bug is distinguishable from ours.
import { detectDrift, formatDriftReport } from '@earnforge/sdk';
const report = await detectDrift({ apiKey: process.env.LIFI_API_KEY });console.log(formatDriftReport(report));console.log(report.ok); // false only when OUR schema breakspnpm --filter @earnforge/sdk driftIt checks vault fields and the response envelope of every endpoint. That
second part was added after /v1/portfolio renamed its array positions →
data and a vault-only check reported “no breaking drift” straight through it.
CI runs it daily, so an API change surfaces within a day rather than whenever someone next opens a pull request.
Portfolio Suggestions
Section titled “Portfolio Suggestions”suggest()
Section titled “suggest()”Get a risk-adjusted portfolio allocation across multiple vaults.
const result = await forge.suggest({ amount: 10_000, // Total USD to allocate asset: 'USDC', // Filter by asset maxChains: 3, // Max chains to spread across maxVaults: 5, // Max vaults in the allocation strategy: 'diversified', // Optional strategy preset});
console.log(result.expectedApy); // Weighted-average APYfor (const alloc of result.allocations) { console.log(`${alloc.vault.name}: $${alloc.amount} (${alloc.percentage}%) APY ${alloc.apy}%`);}| Parameter | Type | Default | Description |
|---|---|---|---|
amount |
number |
required | Total USD amount to allocate |
asset |
string |
all | Filter by underlying token symbol |
maxChains |
number |
5 |
Maximum chains in the allocation |
maxVaults |
number |
5 |
Maximum vaults in the allocation |
strategy |
StrategyPreset |
none | Apply strategy preset |
The allocation engine scores each vault using apy * (riskScore / 10) and distributes
funds proportionally by that score, enforcing the maxChains diversification constraint.
Strategy Presets
Section titled “Strategy Presets”Four built-in presets for common yield strategies:
| Preset | Description | TVL Floor | Protocols | Tags |
|---|---|---|---|---|
conservative |
Stablecoin, blue-chip, high TVL | $50M | aave, morpho, euler, pendle, yearn | stablecoin |
max-apy |
Highest APY, no restrictions | none | all | all |
diversified |
Multi-chain, multi-protocol spread | $1M | all | all |
risk-adjusted |
Risk score >= 7, then sort by APY | none | all | all |
import { STRATEGIES, getStrategy } from '@earnforge/sdk';
const config = getStrategy('conservative');console.log(config.description); // "Stablecoin-tagged, TVL > $50M, APY 3-7%, blue-chip protocols only"console.log(config.filters); // { tags: ['stablecoin'], minTvlUsd: 50_000_000, protocols: [...] }Gas Route Optimization
Section titled “Gas Route Optimization”optimizeGasRoutes()
Section titled “optimizeGasRoutes()”Compare deposit costs from multiple source chains to find the cheapest route.
const routes = await forge.optimizeGasRoutes(vault, { fromAmount: '100', wallet: '0xYourWallet', fromChains: [1, 10, 8453], // Compare Ethereum, Optimism, Base});
for (const route of routes) { console.log(`${route.fromChainName}: gas=$${route.gasCostUsd} fee=$${route.feeCostUsd} total=$${route.totalCostUsd}`);}// Routes are sorted by totalCostUsd ascending -- cheapest firstWatch / Monitoring
Section titled “Watch / Monitoring”watch()
Section titled “watch()”Watch a vault for APY and TVL changes. Returns an async generator of events.
const watcher = forge.watch('morpho:8453:_:0xbeef...', { apyDropPercent: 20, // Alert when APY drops 20%+ tvlDropPercent: 30, // Alert when TVL drops 30%+});
for await (const event of watcher) { console.log(`[${event.type}] APY: ${event.current.apy}% (was ${event.previous.apy}%)`);}APY History
Section titled “APY History”getApyHistory()
Section titled “getApyHistory()”Fetch 30-day APY history from DeFiLlama yields API.
const history = await forge.getApyHistory('0xVaultAddress', 8453);for (const point of history) { console.log(`${point.date}: ${point.apy}%`);}Utility Functions
Section titled “Utility Functions”parseTvl()
Section titled “parseTvl()”Parse the string-typed TVL value from the API into usable formats.
import { parseTvl } from '@earnforge/sdk';
const tvl = parseTvl(vault.analytics.tvl);console.log(tvl.raw); // "12345678.90" (original string)console.log(tvl.parsed); // 12345678.9 (number)console.log(tvl.bigint); // 12345678n (bigint, truncated)getBestApy()
Section titled “getBestApy()”Get the best available APY using the fallback chain: apy.total -> apy30d -> apy7d -> apy1d -> 0.
import { getBestApy } from '@earnforge/sdk';
const apy = getBestApy(vault.analytics);toSmallestUnit() / fromSmallestUnit()
Section titled “toSmallestUnit() / fromSmallestUnit()”Convert between human-readable amounts and on-chain smallest-unit amounts.
import { toSmallestUnit, fromSmallestUnit } from '@earnforge/sdk';
toSmallestUnit('100', 6); // "100000000" (100 USDC)fromSmallestUnit('100000000', 6); // "100"Error Types
Section titled “Error Types”The SDK exports typed error classes for precise error handling:
| Error Class | Code | When |
|---|---|---|
EarnForgeError |
varies | Base class for all SDK errors |
EarnApiError |
EARN_API_ERROR |
HTTP error from earn.li.fi |
ComposerError |
COMPOSER_ERROR |
HTTP error from li.quest (Composer) |
PreflightError |
PREFLIGHT_ERROR |
Preflight checks found blocking issues |
RateLimitError |
RATE_LIMIT |
Rate limit exceeded (429) |
import { EarnApiError, ComposerError, RateLimitError } from '@earnforge/sdk';
try { await forge.vaults.get('invalid-slug');} catch (err) { if (err instanceof EarnApiError) { console.error(`API error ${err.status}: ${err.message} (${err.url})`); } else if (err instanceof RateLimitError) { console.error(`Rate limited. Retry after ${err.retryAfter}ms`); }}