Reference
ZeroDev Earn client configuration, methods, types, defaults, exports, and errors.
Beta
ZeroDev Earn is an experimental product offered in beta. It supports your integration with third-party protocols. ZeroDev does not provide these protocols or the services they offer, does not custody assets, and does not execute or control transactions; any transactions are solely between you (or your end users) and the applicable third-party protocol. Any APY, rewards, or other amounts are provided by such protocols, not ZeroDev, and all data is supplied by third parties for informational purposes only. Use involves significant risk, including smart contract failures, market volatility, illiquidity, lockup, and loss of principal. ZeroDev is not liable for any losses arising from use of the product or the third-party protocols it surfaces. You are responsible for ensuring your end users understand these risks and for any disclosures required by applicable law. Do your own independent research and proceed at your own risk.
Client
import { createEarnClient } from "@zerodev/earn";
const earn = createEarnClient({
projectId: "<ZERODEV_PROJECT_ID>",
});createEarnClient is synchronous.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
projectId | string | Yes | — | Sent as x-project-id on each request |
serverUrl | string | No | DEFAULT_SERVER_URL | Override for staging or self-hosting |
fetch | typeof fetch | No | Global fetch | Custom fetch implementation |
timeoutMs | number | No | 30000 | Per-request timeout |
maxRetries | number | No | 2 | Retry limit for idempotent GET requests |
Quote-building POST requests are not retried automatically because each request can create an SRA. GET requests retry after 429, 502, 503, network failures, and timeouts.
Deposit methods
All deposit methods return Promise<Quote>.
| Method | Params | Constraints |
|---|---|---|
earn.aave.deposit(params) | AaveDepositParams | destChainId required; into optional |
earn.morpho.deposit(params) | VaultDepositParams | into required |
earn.fluid.deposit(params) | VaultDepositParams | into required |
earn.yearn.deposit(params) | VaultDepositParams | into required |
earn.erc4626.deposit(params) | VaultDepositParams | into required; target must pass ERC-4626 validation |
earn.depositIntoVault(params) | VaultDepositParams & { protocol: string } | Registered protocol required |
type DepositParams = {
owner: Address;
amount: number | string; // display units
token: TokenSymbol | Address;
srcChainId: number;
destChainId?: number;
into?: string | Vault;
slippage?: number; // 1–5000 bps; default 100
};
type AaveDepositParams = DepositParams & {
destChainId: number;
};
type VaultDepositParams = DepositParams & {
into: string | Vault;
};When into is a Vault, destChainId is derived from vault.chainId. Providing a different explicit destChainId returns INVALID_REQUEST.
See Supported protocols and vaults for adapter behavior and custom-vault constraints.
Discovery methods
| Method | Returns | Notes |
|---|---|---|
earn.listVaults(params?) | Promise<VaultPage> | Lists supported Aave, Morpho, Fluid, and Yearn vaults |
earn.getVault(vaultId, chainId?) | Promise<VaultDetails> | Pass chainId for direct lookup |
earn.getChains() | Promise<ChainInfo[]> | Reads the SRA chain registry |
earn.getTokens({ chainId? }) | Promise<TokenInfo[]> | Reads supported SRA token addresses |
earn.preflight(params) | Promise<PreflightResult> | Resolves a vault by address and reads current capacity |
earn.listOpportunities(params?) | Promise<VaultPage> | Legacy discovery method with a different filter shape |
type ListVaultsParams = {
asset?: TokenSymbol | Address;
chains?: number[];
protocol?: string;
minTvl?: number; // USD; omitted means $100,000
minApy?: number; // percent
page?: number; // zero-based
};
type VaultPage = {
vaults: Vault[];
nextPage: number | null;
};
type ListOpportunitiesParams = {
chainId?: number;
category?: "lend" | "liquid-staking" | "fixed-yield";
protocol?: string;
minTvl?: number;
minApy?: number;
page?: number;
};listOpportunities accepts chainId, category, protocol, minTvl, minApy, and page. It does not accept the asset or chains filters used by listVaults.
Listed vaults have both an id and a contract address. getVault and withdrawFromVault accept either value as vaultId; pass chainId to identify the network. preflight.vaultId accepts only the contract address.
Status and SRA methods
| Method | Returns | Notes |
|---|---|---|
earn.getStatus(sra) | Promise<EarnStatus> | Single status read |
earn.watchStatus(sra, options) | Watcher | Polls until terminal state, manual stop, or error |
earn.getSraInfo({ sra }) | Promise<SraInfoResult> | Owner, chain, source tokens, slippage, and stored actions |
earn.getSraFeeEstimates({ sra }) | Promise<SraFeeEstimatesResult> | Per-chain fee data and sponsorship flags |
earn.getWithdrawCalls({ sra, tokens }) | Promise<WithdrawCallsResult> | Owner-authorized SRA recovery calls |
type WatchStatusOptions = {
interval?: number; // default 4000 ms
timeout?: number; // default 600000 ms; 0 disables timeout
maxRetries?: number; // default 5 consecutive failures
onStatusChange: (status: EarnStatus) => void;
onError?: (error: unknown) => void;
};
type Watcher = {
(): void;
stop(): void;
done: Promise<void>;
};
type EarnState =
| "PENDING"
| "BRIDGING"
| "EXECUTING"
| "COMPLETED"
| "FAILED"
| "ABANDONED";
type WithdrawCallsResult = {
data: {
chainId: number;
calls: {
to: Address;
data?: Hex;
value: string;
}[];
}[];
receiver: Address;
};watcher.done resolves after a terminal state or manual stop. It rejects after persistent polling failure or WATCH_TIMEOUT.
Withdrawal method
earn.withdrawFromVault(params: VaultWithdrawParams): Promise<VaultWithdrawResult>;
type VaultWithdrawParams = {
owner: Address;
vaultId: string;
chainId: number;
amount?: number | string;
max?: boolean;
};Pass:
amountfor a partial withdrawal.max: truefor a full exit.- Neither for a preview with no calls.
amount and max cannot be used together. Withdrawal is same-chain. The vault must be indexed by vaults.fyi and use an allowlisted protocol, but it does not need to match the listVaults discovery filters.
type VaultWithdrawResult = {
chainId: number;
protocol: string;
target: Address;
asset: Address;
assetSymbol: string;
decimals: number;
amount: string;
exitAll: boolean;
available: string;
receiver: Address;
calls: OnChainCall[];
};Disabled method
earn.bridgeAndSwap(params) rejects locally with FEATURE_DISABLED. Standalone bridge-and-swap is not available. Token conversion required by a deposit can still be handled by an SRA-supported route.
Quote
All deposit methods return the same Quote shape.
type Quote = {
quoteId: string;
expiresAt: string;
sra: Address | null;
transaction: {
chainId: number;
calls: OnChainCall[];
};
userOp: {
callData: Hex;
calls: OnChainCall[];
chainId: number;
};
estimatedFees: {
totalFeeAmount: string | null;
totalFeeToken: Address | null;
perChain: {
chainId: number;
feeAmount: string | null;
feeToken: Address | null;
}[];
};
estimatedReceiveAmount: string;
estimatedShares?: string;
vaultApy?: number;
route?: {
bridgeTokenType: string | null;
bridgeTokenSrc?: Address;
bridgeTokenDest?: Address;
sameChain: boolean;
};
};
type OnChainCall = {
to: Address;
data: Hex;
value: string;
};Amounts in API responses are base-unit decimal strings. Convert them with BigInt before passing them to on-chain clients.
totalFeeAmount and totalFeeToken are both null when fees use more than one denomination. Render perChain in that case. Sponsored fee entries are excluded from charged totals.
Errors
Rejected SDK requests use EarnError or a code-specific subclass.
import {
EarnError,
VaultCapExceededError,
} from "@zerodev/earn";
try {
await earn.morpho.deposit(params);
} catch (error) {
if (error instanceof VaultCapExceededError) {
suggestSmallerAmount();
} else if (error instanceof EarnError) {
reportError(error.code, error.message, error.requestId);
}
}Include requestId in bug reports. Some codes, including SLIPPAGE_TOO_LOW, use the base EarnError without a dedicated subclass.
| Code | Meaning | Suggested action |
|---|---|---|
INVALID_REQUEST | Missing, malformed, or conflicting parameter | Fix request parameters |
UNSUPPORTED_TOKEN | Funding token is not supported on the source chain | Select a supported token or route |
UNKNOWN_PROTOCOL | No adapter is registered for protocol | Use a supported facade or erc4626 |
VAULT_TYPE_MISMATCH | Target does not match the selected adapter | Correct the protocol or target |
ASSET_MISMATCH | Target asset differs from the resolved deposit asset | Correct token or target |
VAULT_NOT_ALLOWLISTED | Vault lookup failed or its protocol is not allowlisted | Verify the address, chain, and protocol |
CHAIN_NOT_SUPPORTED | Source or destination chain is unavailable | Select a supported chain |
VAULT_CAP_EXCEEDED | Amount exceeds current deposit capacity | Reduce amount or select another vault |
VAULT_DEPOSITS_DISABLED | On-chain maxDeposit is zero | Select another vault |
INSUFFICIENT_AMOUNT | Amount does not cover route minimum or fees | Increase amount or slippage as described in details |
SLIPPAGE_TOO_LOW | Slippage does not cover route fees | Retry using details.minSlippageBps |
SWAP_ROUTE_NOT_FOUND | Required cross-token route is unavailable | Use another token, vault, or chain |
QUOTE_EXPIRED | Quote is no longer accepted by the endpoint | Request another quote |
SANCTIONED_ADDRESS | Owner failed sanctions screening | Do not retry with the same owner |
VAULT_BLOCKED | Target is blocked by service policy | Select another vault |
ACCESS_DENIED | Origin or IP is not allowlisted for the project | Update project allowlist |
RATE_LIMITED | Request rate exceeded | Retry after backoff |
PAYLOAD_TOO_LARGE | Request body exceeds the server limit | Reduce request size |
IDEMPOTENCY_KEY_CONFLICT | Idempotency key was reused with another body | Use a new key |
FEATURE_DISABLED | Requested flow is disabled | Use an enabled deposit flow |
SRA_NOT_FOUND | SRA address is unknown | Verify the address and network |
SRA_UNAVAILABLE | SRA service failed | Retry by requesting a new quote |
QUOTER_UNAVAILABLE | Route quote provider failed | Retry later |
RPC_UNAVAILABLE | Destination-chain RPC failed | Retry later |
SERVICE_UNAVAILABLE | Required service is unavailable | Retry later |
INTERNAL_ERROR | Unexpected server failure | Report requestId |
WATCH_TIMEOUT | Client-only watcher timeout | Read status again or increase timeout |
Exports
import {
createEarnClient,
DEFAULT_SERVER_URL,
TOKENS,
EarnError,
VaultCapExceededError,
VaultDepositsDisabledError,
WatchTimeoutError,
} from "@zerodev/earn";Public types include Quote, OnChainCall, Vault, VaultDetails, DepositParams, VaultDepositParams, VaultWithdrawParams, VaultWithdrawResult, EarnStatus, EarnState, Watcher, and method-specific parameter and result types.