Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/pages/onramp/smart-recipes/api-reference.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# API Reference

This page lists every method on the client from `createSmartRecipes(config)`. All methods are async. All rejections are typed [`SmartRecipeError`](/onramp/smart-recipes/errors)s.

## Deposits

All deposit methods return a [`Quote`](/onramp/smart-recipes/quotes). They share the core parameters: `owner`, `amount`, `token`, `srcChainId`, `destChainId?`, `slippage?`. See [Deposits](/onramp/smart-recipes/deposits) for the shared vocabulary.

| Method | Extra params | Notes |
|---|---|---|
| `sr.aave.deposit(p)` | `destChainId` required, `into` optional | Omit `into` to supply the funding token's reserve. Pass an Aave listing to select a different reserve. |
| `sr.morpho.deposit(p)` | `into` required | Vault id, address, or a `Vault` object. |
| `sr.fluid.deposit(p)` | `into` required | |
| `sr.yearn.deposit(p)` | `into` required | |
| `sr.erc4626.deposit(p)` | `into` required | Any ERC-4626 vault, listed or not. |
| `sr.depositIntoVault(p)` | `into` + `protocol` required | The generic engine. `protocol`: `'aave' \| 'morpho' \| 'fluid' \| 'yearn' \| 'erc4626'`. |

## Swaps

| Method | Params | Returns |
|---|---|---|
| `sr.bridgeAndSwap(p)` | `owner`, `amount`, `token`, `toToken`, `srcChainId`, `destChainId`, `slippage?` | `Quote` (no vault fields) |

## Discovery

| Method | Params | Returns |
|---|---|---|
| `sr.listVaults(p?)` | `asset?`, `chains?`, `protocol?`, `minTvl?`, `minApy?`, `page?` | `{ vaults: Vault[]; nextPage: number \| null }` |
| `sr.getVault(vaultId, chainId?)` | Pass `chainId` when known, for a direct lookup | `VaultDetails` |
| `sr.getChains()` | (none) | `ChainInfo[]` |
| `sr.getTokens(p?)` | `chainId?` | `TokenInfo[]` |
| `sr.listOpportunities(p?)` | Alias of `listVaults` (back-compat) | `VaultPage` |

If you omit `minTvl` on `listVaults`, a $100,000 default minimum applies. Pass `minTvl: 0` to include smaller vaults.

## Status

| Method | Params | Returns |
|---|---|---|
| `sr.getStatus(sra)` | The SRA address | `RecipeStatus`: `state`, `deposits`, `failureReason?` |
| `sr.watchStatus(sra, opts)` | `interval?` (default 4000 ms), `timeout?` (default 10 min, `0` = forever), `maxRetries?` (default 5), `onStatusChange`, `onError?` | `Watcher`: callable unsubscribe, with `.stop()` and `.done` |

`watchStatus` polls with backoff. It stops on `COMPLETED`, `FAILED`, or `ABANDONED`. A persistent poll failure rejects `watcher.done` and calls `onError`.

## SRA lifecycle

| Method | Params | Returns |
|---|---|---|
| `sr.preflight(p)` | `owner`, `vaultId`, `destChainId`, `amount`, `srcChainId?`, `srcToken?` | Vault entry, `maxDeposit`, `depositsDisabled`, route |
| `sr.getSraInfo(p)` | `sra` | The stored routing config: owner, actions, src tokens, slippage |
| `sr.getSraFeeEstimates(p)` | `sra` | Per-chain fee estimates, with `isSponsored` flags |
| `sr.getDepositStatus(p)` | `sra`, `owner`, `destChainId`, `vaultId` (the vault address) | `phase`, `deposits`, `vaultBalance` |
| `sr.getWithdrawCalls(p)` | `sra`, `tokens: [{ chainId, token }]` | Owner-signed recovery calls, per chain |

## Client config

```ts
createSmartRecipes({
serverUrl: string, // required
projectId: string, // required - sent on every request
fetch?: typeof fetch, // injectable, for tests / non-browser runtimes
timeoutMs?: number, // per-request abort timeout, default 30000
maxRetries?: number, // GET retries on 429/502/503/network, default 2
})
```

The factory is synchronous. POST requests (quotes) are never retried. Each attempt can create a new SRA.

## Exports

```ts
import {
createSmartRecipes,
TOKENS, // USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE
SmartRecipeError, // base error, plus one subclass per code
QuoteExpiredError,
VaultCapExceededError,
// ... see Errors for the full list
} from "@zerodev/smart-recipes";
```

Types: `Quote`, `Vault`, `VaultDetails`, `DepositParams`, `BridgeAndSwapParams`, `RecipeStatus`, `RecipeState`, `Watcher`, and the params and result types of every method above.
31 changes: 31 additions & 0 deletions docs/pages/onramp/smart-recipes/bridge-and-swap.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Bridge & Swap

Bridge funds to a different chain and swap them into a target token, with one signature. The flow uses the same SRA as a deposit. The destination action is a swap. The `owner` receives the output.

```ts
const quote = await sr.bridgeAndSwap({
owner: "0xUSER",
amount: "100",
token: TOKENS.USDC, // source funding token
srcChainId: 8453, // Base
destChainId: 42161, // Arbitrum
toToken: "0xTARGET", // the token to receive on the destination chain
slippage: 100, // bps, default 100 (= 1%)
});
```

Execute and track the quote in the same way as a deposit:

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

await sr.watchStatus(quote.sra, { onStatusChange: (s) => console.log(s.state) }).done;
```

The quote does not include the vault fields (`vaultApy`, `estimatedShares`). It includes `swapMinOutput`. This value is an estimate. The server derives the enforced minimum output from your `slippage` value.

:::info
The server can disable swap routes. A disabled route rejects with `FEATURE_DISABLED` (403). Same-token routes are not affected. This includes same-chain deposits and plain cross-chain bridges. See [Errors](/onramp/smart-recipes/errors).
:::
95 changes: 95 additions & 0 deletions docs/pages/onramp/smart-recipes/deposits.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Deposits

Each deposit routes through a Smart Routing Address and returns a [Quote](/onramp/smart-recipes/quotes). This applies to same-chain and cross-chain deposits. The route is same-chain when `srcChainId === destChainId`. You do not select the route yourself.

## Protocol facades

Each facade binds its protocol. The server maps the protocol to an adapter. The adapter builds the deposit calls and verifies the target on-chain before it quotes. Example: you pass a Morpho-Blue market where a 4626 vault is expected. The call fails immediately with a typed error. No funds move.

```ts
sr.aave.deposit(params) // no `into` - the pool follows from token + destChainId
sr.morpho.deposit(params) // `into` required
sr.fluid.deposit(params) // `into` required
sr.yearn.deposit(params) // `into` required
sr.erc4626.deposit(params) // unbranded ERC-4626 vault by id/address
```

## Parameters

Each deposit takes this core shape. Facades narrow it. Aave drops `into`. Morpho, Fluid, and Yearn require it.

```ts
type DepositParams = {
owner: Address // funds + signs + receives shares + gets refunds (one role)
amount: number | string // display units; the server scales by decimals (string math, no float)
token: TokenSymbol | Address // symbol → canonical per-chain address; address to disambiguate (USDC.e)
srcChainId: number // the chain the user funds from
destChainId?: number // the execution chain; equal to srcChainId ⇒ same-chain (no bridge).
// optional when `into` is a Vault (derived from vault.chainId);
// required for Aave or a string id/address `into`
into?: string | Vault // multi-vault only: vault id/address, or a Vault from listVaults()
slippage?: number // bps, default 100 (= 1%)
}
```

- `owner` is one role: funder, signer, shares receiver, and refund recipient. There is no separate `beneficiary`.
- `amount` is in display units. `"100"` means 100 USDC. Use a `string` for very large values or values with more than 15 significant figures.
- `token` accepts a symbol or an address. `TOKENS` contains `USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE`. A symbol resolves to the canonical per-chain address. Pass a raw address for a variant (USDC.e) or an arbitrary token.
- `slippage` is an integer in bps. `50` means 0.5%. The server enforces the real floor. The quote's `swapMinOutput` is an estimate.

### Chain discovery into deposits with `into`

When `into` is a `Vault` object from [`listVaults()`](/onramp/smart-recipes/vault-discovery), you can omit `destChainId`. The vault contains its own `chainId`:

```ts
const { vaults } = await sr.listVaults({ asset: TOKENS.USDC, chains: [42161] });

await sr.morpho.deposit({
owner,
amount: "100",
token: TOKENS.USDC,
srcChainId: 8453,
into: vaults[0], // destChainId derived = vault.chainId
});
```

## Aave

Aave has one pool for each chain. The `token` and `destChainId` identify the target:

```ts
await sr.aave.deposit({
owner,
amount: "100",
token: TOKENS.USDC,
srcChainId: 8453, // Base
destChainId: 42161, // Arbitrum
slippage: 50, // 0.5%
});
```

The `owner` receives the canonical aToken position.

`into` is optional for Aave. Omit it to supply the reserve of the funding token. Pass an Aave listing from `listVaults({ protocol: 'aave' })` to select a different reserve. The funding token then routes into that reserve.

## Generic deposits with `depositIntoVault`

Use `depositIntoVault` for a vault that has no facade. This includes ERC-4626 vaults that ZeroDev does not list. No facade binds the protocol here, so you must pass it:

```ts
await sr.depositIntoVault({
owner,
amount: "100",
token: TOKENS.USDC,
srcChainId: 42161,
destChainId: 42161,
into: "0xVAULT",
protocol: "erc4626", // required - 'aave' | 'morpho' | 'fluid' | 'yearn' | 'erc4626'
});
```

The server rejects an unknown protocol with `UNKNOWN_PROTOCOL`. Use a facade when one exists.

## Failure behavior

A destination action can revert after the bridge. In that case the funds stay in the SRA. ZeroDev does not hold the funds. Only the `owner` can recover them, with `getWithdrawCalls`. See [Tracking Status](/onramp/smart-recipes/tracking-status#recovering-funds).
53 changes: 53 additions & 0 deletions docs/pages/onramp/smart-recipes/errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Errors

Each rejection is a `SmartRecipeError`. It contains a `code`, a `message`, and a `requestId` (from the server's `x-request-id` header). Include the `requestId` in a bug report. Each code has a bound subclass, so you can branch with `instanceof`:

```ts
import { SmartRecipeError, QuoteExpiredError } from "@zerodev/smart-recipes";

try {
await sr.aave.deposit({ /* … */ });
} catch (e) {
if (e instanceof QuoteExpiredError) {
// request a new quote
} else if (e instanceof SmartRecipeError) {
console.error(`[${e.code}] ${e.message} (request ${e.requestId})`);
}
}
```

## Error codes

| HTTP | Code | When |
|---|---|---|
| 400 | `INVALID_REQUEST` | A parameter is missing or malformed |
| 400 | `UNSUPPORTED_TOKEN` | The token does not resolve |
| 400 | `UNKNOWN_PROTOCOL` | The `protocol` has no registered adapter |
| 400 | `VAULT_TYPE_MISMATCH` | The target is not the expected vault kind (on-chain probe) |
| 400 | `ASSET_MISMATCH` | The vault asset is not the expected token |
| 400 | `CHAIN_NOT_SUPPORTED` | The chain is not configured on the server |
| 400 | `VAULT_DEPOSITS_DISABLED` | The vault's on-chain `maxDeposit` is 0. It accepts no deposits. |
| 403 | `SANCTIONED_ADDRESS` | The owner is on the OFAC SDN list |
| 403 | `VAULT_BLOCKED` | The vault is on the server blocklist |
| 403 | `FEATURE_DISABLED` | Swap-leg routes are disabled on the server |
| 403 | `ACCESS_DENIED` | The origin or IP is not on the project's allowlist |
| 409 | `VAULT_CAP_EXCEEDED` | The amount is over the vault's remaining capacity |
| 410 | `QUOTE_EXPIRED` | The quote is past its TTL. Request a new quote. |
| 413 | `PAYLOAD_TOO_LARGE` | The request body is over the size cap |
| 422 | `INSUFFICIENT_AMOUNT` | The amount is below the vault minimum after fees |
| 422 | `SWAP_ROUTE_NOT_FOUND` | No swap route was found |
| 429 | `RATE_LIMITED` | Too many requests. Idempotent GETs retry with backoff. |
| 500 | `INTERNAL_ERROR` | An unexpected server error |
| 502 | `SRA_UNAVAILABLE` / `QUOTER_UNAVAILABLE` / `RPC_UNAVAILABLE` | An upstream service failed |

## Codes that need special handling

`VAULT_CAP_EXCEEDED` and `VAULT_DEPOSITS_DISABLED` are different conditions. For `VAULT_CAP_EXCEEDED`, try a smaller amount. For `VAULT_DEPOSITS_DISABLED`, the vault accepts no deposits at all. Select a different vault. [`preflight`](/onramp/smart-recipes/vault-discovery#preflight) shows both conditions before you quote.

`FEATURE_DISABLED` means the route needs a swap leg, and the server has swaps disabled. Same-token routes are not affected. This includes same-chain deposits and plain cross-chain bridges. A retry with different parameters does not help.

`QUOTE_EXPIRED` occurs because a quote lives for about 60 seconds. Request a new quote.

## Retry behavior

The SDK does not retry quote-building POST requests. Each POST can create a new SRA on the server. The SDK retries idempotent GET requests on 429, 502, 503, and network failures, with backoff, up to the client's `maxRetries`.
76 changes: 76 additions & 0 deletions docs/pages/onramp/smart-recipes/how-it-works.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# How It Works

This page explains the flow from signature to vault shares. It shows where the funds are at each moment. It also shows what happens on failure. Read it to evaluate the trust model.

## The flow

```
your app sr.morpho.deposit(...) ── 1. quote
user signs one transfer to the SRA ── 2. fund
ZeroDev relayer bridges to the destination ── 3. bridge (cross-chain only)
ZeroDev relayer runs the stored deposit calls ── 4. execute
your app sr.watchStatus(sra) ── 5. track
```

### 1. Quote

The server receives your intent: owner, amount, token, chains, target vault. It then does four things.

- It verifies the target on-chain. For an ERC-4626 vault it reads `asset()`. For Aave it checks the reserve list. A wrong target fails here, with a typed error. No funds have moved.
- It reads the vault's live state: `maxDeposit` headroom, the deposit minimum, and `previewDeposit` for expected shares.
- It creates a [Smart Routing Address](/onramp/smart-routing-address) (SRA v1). The deposit actions are stored at creation. Nobody can change them afterwards.
- It returns the Quote: the SRA, a ready-to-sign transaction, a user operation, fees, and expected output.

### 2. Fund

The user signs one transfer of `amount` `token` to the SRA. That is the only signature in the flow. A source swap adds approve and swap calls to the same batch.

### 3. Bridge

The relayer bridges the funds to the SRA on the destination chain. A same-chain deposit skips this step.

### 4. Execute

The relayer runs the actions stored in the SRA. Approve the vault. Deposit. Credit the shares to the `owner`. The calls take the full arrived amount. No dust is stranded by a locked-in amount.

### 5. Track

The status comes from on-chain evidence at the SRA: deposits seen, bridges sent, executions settled. The server never asserts a state it cannot prove. `watchStatus` polls for you and stops on a terminal state.

## Where the funds are

| Moment | Funds are |
|---|---|
| Before funding | In the user's wallet |
| After funding, before bridge | In the user's SRA |
| In the bridge | In the bridge protocol, addressed to the SRA |
| After bridge, before execution | In the user's SRA on the destination chain |
| After execution | Vault shares, credited to the `owner` |

ZeroDev operates the relayer. ZeroDev never has custody. The SRA is a permissionless contract. Funds leave it in two ways only: the stored actions, or an owner withdrawal.

## When something fails

**The target vault is wrong.** The on-chain probe catches it at quote time. You get `VAULT_TYPE_MISMATCH` or `ASSET_MISMATCH` in milliseconds. No funds moved.

**The vault fills up after the quote.** The deposit call reverts. The funds stay in the SRA. The `owner` recovers them with [`getWithdrawCalls`](/onramp/smart-recipes/tracking-status#recovering-funds), or through the [SRA portal](https://smart-routing-address.zerodev.app/).

**The user sends the wrong token.** Tokens outside the route rest in the SRA. Same recovery path.

**The user never sends funds.** The recipe becomes `ABANDONED` after one hour. Nothing is lost. Late funds still execute.

No failure mode gives the funds to ZeroDev or to a third party.

## Slippage

You set `slippage` in bps at quote time. The SRA enforces the minimum-output floor on-chain, at execution. A quoted estimate cannot be front-run below your floor.

## Why the server builds the route

Routes, vault lists, calldata, and compliance screening change weekly. They live server-side. The result:

- A new vault or protocol reaches your users with no SDK update.
- A bad vault can be blocklisted globally, at once.
- Each quote screens the owner against the OFAC SDN list, refreshed daily.

The SDK stays a thin, dependency-free HTTP client. You almost never need to upgrade it.
Loading