Skip to content

SDK Reference

@earnforge/sdk is the foundation package. Every other EarnForge surface (CLI, React, MCP, skill, Studio) is built on top of it.

Terminal window
npm i @earnforge/sdk

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

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

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

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

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

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

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: '...' }, ...]

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

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); // 6
console.log(result.quote); // Full LI.FI quote with transactionRequest

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

Compute a composite 0-10 risk score for a vault. Higher score = safer.

const risk = forge.riskScore(vault);
console.log(risk.score); // 5.5
console.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.


Answers the question a headline APY cannot: is this real, or an incentive that ends?

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 stop
console.log(s.rewardShare) // 0.87
console.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.

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.


/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.

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.

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.


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.62
console.log(projection.maxSafeBorrowBase); // headroom, in oracle base units

projectBorrow() 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”.


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 breaks
Terminal window
pnpm --filter @earnforge/sdk drift

It checks vault fields and the response envelope of every endpoint. That second part was added after /v1/portfolio renamed its array positionsdata 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.


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 APY
for (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.


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: [...] }

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 first

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

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

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)

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

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"

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