# Usage (/onramp/earn/usage)

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

## Choose a deposit target [#choose-a-deposit-target]

Use `listVaults` for listed Aave, Morpho, Fluid, and Yearn positions.

```ts
const { vaults, nextPage } = await earn.listVaults({
  asset: TOKENS.USDC,
  chains: [42161],
  protocol: "morpho",
  minTvl: 1_000_000,
  minApy: 2,
  page: 0,
});
```

`listVaults` returns up to 50 results per page. Continue with `nextPage` until it is `null`. Omitting `minTvl` applies a $100,000 floor; pass `minTvl: 0` to include smaller vaults. Use `getVault(vaultId, chainId)` for one vault's details. TVL, APY, and other vault metadata can be up to 12 hours old.

## Check current capacity [#check-current-capacity]

Vault listings can be stale relative to on-chain deposit limits. Use `preflight` before quoting when the UI must show current capacity.

```ts
const check = await earn.preflight({
  owner,
  vaultId: vault.address,
  destChainId: vault.chainId,
  amount: parseUnits("100", vault.asset.decimals).toString(),
});

if (check.depositsDisabled) {
  // Select another vault.
}

console.log(check.maxDeposit); // base units, or null when uncapped
```

`preflight.amount` uses base units. Deposit methods use display units.

`preflight` queries the address directly through vaults.fyi, then validates it on-chain. The vault does not need to appear in `listVaults`, but vaults.fyi must know it and its protocol must be allowlisted.

## Request a deposit quote [#request-a-deposit-quote]

Deposit amounts use display units. Prefer strings to avoid floating-point precision loss. Slippage uses basis points and defaults to 100 (1%). See [Reference](/onramp/earn/reference#deposit-methods) for all parameters.

### Protocol facade [#protocol-facade]

Use a facade when the protocol is known.

```ts
const quote = await earn.morpho.deposit({
  owner,
  amount: "100",
  token: TOKENS.USDC,
  srcChainId: 8453,
  into: vault,
});
```

### Aave [#aave]

Omit `into` to supply the reserve corresponding to the funding token on `destChainId`.

```ts
const quote = await earn.aave.deposit({
  owner,
  amount: "100",
  token: TOKENS.USDC,
  srcChainId: 8453,
  destChainId: 42161,
});
```

Pass an Aave `Vault` as `into` to select another listed reserve.

### Generic engine [#generic-engine]

Use `depositIntoVault` when the protocol comes from discovery or is selected dynamically.

```ts
const quote = await earn.depositIntoVault({
  owner,
  amount: "100",
  token: TOKENS.USDC,
  srcChainId: 8453,
  into: vault,
  protocol: vault.protocol,
});
```

For an ERC-4626 contract not returned by discovery, pass its address, `destChainId`, and one of the registered ERC-4626 protocol values: `morpho`, `fluid`, `yearn`, or `erc4626`. See [Supported protocols and vaults](/onramp/earn/supported-vaults#custom-vaults).

## Submit a quote [#submit-a-quote]

A quote prepares source-chain calls. It does not sign or broadcast them.

Save `quote.sra` before submitting any call. Deposit status and recovery are keyed by this address.

### Using an EOA [#using-an-eoa]

Execute every call in `quote.transaction.calls` in order on `quote.transaction.chainId`.

```ts
for (const call of quote.transaction.calls) {
  await walletClient.sendTransaction({
    to: call.to,
    data: call.data,
    value: BigInt(call.value),
  });
}
```

### Using a smart account [#using-a-smart-account]

`quote.userOp.callData` contains the same call batch encoded as Kernel v3 / ERC-7579 `executeBatch` calldata.

```ts
const userOpHash = await kernelClient.sendUserOperation({
  callData: quote.userOp.callData,
});
```

For another account implementation, encode `quote.userOp.calls` using that account's batch format.

## Track a deposit [#track-a-deposit]

`watchStatus` polls the SRA and calls `onStatusChange` when the state changes.

```ts
const watcher = earn.watchStatus(quote.sra, {
  interval: 4_000,
  timeout: 600_000,
  maxRetries: 5,
  onStatusChange: (status) => setPhase(status.state),
  onError: (error) => reportError(error),
});

await watcher.done;
```

Cross-chain deposits normally progress through:

```text
PENDING -> BRIDGING -> EXECUTING -> COMPLETED
                                 -> FAILED
PENDING (no funds within 1 hour) -> ABANDONED
```

`ABANDONED` means no funding transaction was observed during the one-hour window. It does not invalidate the SRA; funds received later can still execute. Start a new watcher to resume tracking.

Use `earn.getStatus(sra)` for a single status read.

## Withdraw a vault position [#withdraw-a-vault-position]

`withdrawFromVault` builds same-chain calls. It does not use an SRA.

Pass neither `amount` nor `max` to preview the currently withdrawable amount:

```ts
const preview = await earn.withdrawFromVault({
  owner,
  vaultId: vault.id,
  chainId: vault.chainId,
});

console.log(preview.available);
```

Build a full exit:

```ts
const exit = await earn.withdrawFromVault({
  owner,
  vaultId: vault.id,
  chainId: vault.chainId,
  max: true,
});

for (const call of exit.calls) {
  await walletClient.sendTransaction({
    to: call.to,
    data: call.data,
    value: BigInt(call.value),
  });
}
```

Pass `amount` or `max`, not both. `available` is the current withdrawal ceiling after protocol constraints. For Aave, this can be below the supplied position when debt health or reserve liquidity limits the exit.

`withdrawFromVault` queries vaults.fyi directly by address or vault ID and chain. Discovery filters such as the default TVL floor do not apply, but vaults.fyi must know the vault and its protocol must be allowlisted.

## Recover funds from an SRA [#recover-funds-from-an-sra]

If a destination action fails or the SRA receives a token outside its route, funds can remain in the SRA. Only the `owner` can withdraw them.

```ts
const { data } = await earn.getWithdrawCalls({
  sra,
  tokens: [{ chainId: 42161, token: usdcAddress }],
});

for (const group of data) {
  // Switch the wallet to group.chainId first.
  for (const call of group.calls) {
    await walletClient.sendTransaction({
      to: call.to,
      data: call.data ?? "0x",
      value: BigInt(call.value),
    });
  }
}
```

The [SRA portal](https://smart-routing-address.zerodev.app/) provides a user interface for the same owner-authorized recovery flow.
