For the complete documentation index, see llms.txt. This page is also available as markdown.

Usage

Configure deposits, submit quotes, track status, withdraw positions, and recover SRA funds.

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.

Choose a deposit target

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

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

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

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

Deposit amounts use display units. Prefer strings to avoid floating-point precision loss. Slippage uses basis points and defaults to 100 (1%). See Reference for all parameters.

Protocol facade

Use a facade when the protocol is known.

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

Aave

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

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

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

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.

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

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

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

Using a smart account

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

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

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

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:

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

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

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

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

console.log(preview.available);

Build a full exit:

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

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.

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 provides a user interface for the same owner-authorized recovery flow.

On this page