Skip to content

18 Pitfalls Guide

The LI.FI Earn API has 18 documented pitfalls that can cause failures, data corruption, or silent bugs. Pitfalls 1-14 originate from LI.FI’s official integration guide. Pitfalls 15-18 were discovered via live API probing against the full vault set (April 2026).

EarnForge eliminates every single one at the SDK layer. Downstream surfaces (CLI, React, MCP, bot) never need to think about them.


Root CauseEarn Data lives at earn.li.fi, Composer at li.quest. Mixing them returns 404.
SDK MitigationTwo typed clients: EarnDataClient (earn.li.fi) and ComposerClient (li.quest) with hard-coded defaults.
Testpitfall-01-wrong-base-url.test.ts
Root CauseSending an Authorization or x-lifi-api-key header to Earn Data returns 401 or is silently ignored.
SDK MitigationEarnDataClient.fetch() sends zero auth headers.
Testpitfall-02-auth-on-earn-data.test.ts
Root CauseComposer /v1/quote returns 401 without x-lifi-api-key.
SDK MitigationComposerClient constructor throws ComposerError if apiKey is falsy. createEarnForge() gates all Composer paths behind requireComposer().
Testpitfall-03-missing-composer-key.test.ts
Root CauseComposer /v1/quote is GET with query params. POST returns 405.
SDK MitigationComposerClient.getQuote() hard-codes method: 'GET' and serializes params to query string.
Testpitfall-04-post-instead-of-get.test.ts
Root CausePassing the underlying token address as toToken fails. The vault’s share token is the vault address.
SDK MitigationbuildDepositQuote() wires toToken = vault.address automatically.
Testpitfall-05-wrong-totoken.test.ts
Root Cause/v1/earn/vaults returns max 50 per page. Without nextCursor handling you see less than 8% of vaults.
SDK MitigationEarnDataClient.listAllVaults() is an async iterator that follows nextCursor until exhausted.
Testpitfall-06-ignoring-pagination.test.ts
Root Causeapy.total is always a number, but apy1d, apy7d, apy30d can be null on new or low-volume vaults.
SDK MitigationAnalyticsSchema types them as number | null. getBestApy() implements a fallback chain: apy.total -> apy30d -> apy7d -> apy1d -> 0.
Testpitfall-07-null-apy.test.ts
Root Causeanalytics.tvl.usd comes as "12345678.90" (string), not a number. Arithmetic on it silently produces NaN.
SDK MitigationparseTvl() returns { raw: string, parsed: number, bigint: bigint }. All SDK comparisons go through it.
Testpitfall-08-tvl-string.test.ts
Root CauseUSDC has 6 decimals, WETH has 18. Passing "1" raw sends 1 wei instead of 1 token.
SDK MitigationtoSmallestUnit(amount, decimals) and fromSmallestUnit() handle the conversion. buildDepositQuote() reads decimals from vault.underlyingTokens.
Testpitfall-09-decimal-mismatch.test.ts
Root CauseQuotes expire quickly. Using a cached quote for a transaction can revert.
SDK MitigationLRUCache with configurable TTL (default 60s). Quote results are not cached; only read-only data (vaults, chains, protocols) is cached.
Testpitfall-10-stale-quote.test.ts
Root CauseSubmitting a tx with 0 native balance reverts with an opaque EVM error.
SDK Mitigationpreflight() checks nativeBalance and returns a NO_GAS issue before any tx is built.
Testpitfall-11-no-gas-token.test.ts
Root CauseWallet connected to chain A, vault on chain B. Tx sent to wrong RPC.
SDK Mitigationpreflight() compares walletChainId vs vault.chainId and returns a CHAIN_MISMATCH issue.
Testpitfall-12-chain-mismatch.test.ts
Root CauseAbout 30% of vaults have isTransactional: false. Deposit calls fail with an obscure Composer error.
SDK MitigationbuildDepositQuote() and preflight() both check vault.isTransactional and throw/report before hitting the network.
Testpitfall-13-non-transactional.test.ts
Root CauseEarn Data API has undocumented rate limits. Hammering it returns 429.
SDK MitigationTokenBucketRateLimiter at 100 req/min. acquireAsync() waits instead of throwing when the bucket is empty.
Testpitfall-14-rate-limit.test.ts
Root CauseSome vaults return underlyingTokens: []. Accessing [0].address throws.
SDK MitigationVaultSchema allows empty arrays. buildDepositQuote() checks length and requires explicit fromToken when empty. preflight() warns.
Testpitfall-15-empty-underlying-tokens.test.ts
Root CauseAbout 14% of vaults omit the description field entirely. Accessing it without a guard crashes rendering.
SDK MitigationVaultSchema types description as z.string().optional(). All display code uses optional chaining.
Testpitfall-16-optional-description.test.ts
Root CauseMorpho vaults return reward: 0. Euler and Aave return reward: null. Inconsistent.
SDK MitigationApySchema applies .nullable().transform(v => v ?? 0) — every consumer sees a plain number.
Testpitfall-17-apy-reward-null-vs-zero.test.ts
Root CauseShort-lived or freshly deployed vaults return apy1d: null and sometimes apy7d: null. Code that divides by apy1d gets Infinity.
SDK MitigationAnalyticsSchema types all three as number | null. getBestApy() fallback chain skips nulls. riskScore() handles missing historical data with a moderate default.
Testpitfall-18-apy1d-null.test.ts

#PitfallSDK Guard
1Wrong base URLTwo typed clients with hard-coded URLs
2Auth on Earn DataZero auth headers on EarnDataClient
3Missing Composer keyConstructor throws; requireComposer() gate
4POST instead of GETHard-coded method: 'GET' on ComposerClient
5Wrong toTokenbuildDepositQuote() wires toToken = vault.address
6Ignoring paginationlistAllVaults() async iterator follows cursors
7Null APY valuesgetBestApy() fallback chain
8TVL is a stringparseTvl() returns typed { raw, parsed, bigint }
9Decimal mismatchtoSmallestUnit() / fromSmallestUnit()
10Stale quoteLRU cache excludes quotes; TTL on read data
11No gas tokenpreflight() checks native balance
12Chain mismatchpreflight() compares wallet chain vs vault chain
13Non-transactionalBoth buildDepositQuote() and preflight() check
14Rate limitTokenBucketRateLimiter at 100 req/min
15Empty underlyingTokensSchema allows []; explicit fromToken required
16Optional descriptionz.string().optional() in schema
17apy.reward null vs 0.nullable().transform(v => v ?? 0) normalization
18apy1d nullTyped as number | null; fallback chain; moderate default

All 18 tests are individually named and live under packages/sdk/test/pitfalls/:

Terminal window
# Run all SDK tests (including pitfalls)
pnpm turbo test --filter=@earnforge/sdk
# Run only pitfall tests
cd packages/sdk
pnpm vitest run test/pitfalls/

Each test constructs a minimal fixture that triggers the pitfall and asserts the SDK handles it without error.