# Reference (/onramp/earn/reference)

> For the complete documentation index, see [llms.txt](/llms.txt)



<Callout type="warn" title="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.
</Callout>

## Client [#client]

```ts
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 [#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                        |

```ts
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](/onramp/earn/supported-vaults) for adapter behavior and custom-vault constraints.

## Discovery methods [#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  |

```ts
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 [#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                       |

```ts
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 [#withdrawal-method]

```ts
earn.withdrawFromVault(params: VaultWithdrawParams): Promise<VaultWithdrawResult>;

type VaultWithdrawParams = {
  owner: Address;
  vaultId: string;
  chainId: number;
  amount?: number | string;
  max?: boolean;
};
```

Pass:

* `amount` for a partial withdrawal.
* `max: true` for 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.

```ts
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 [#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 [#quote]

All deposit methods return the same `Quote` shape.

```ts
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 [#errors]

Rejected SDK requests use `EarnError` or a code-specific subclass.

```ts
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 [#exports]

```ts
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.
