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
60 changes: 60 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,60 @@
# How It Works

Where the funds are at each moment, and what happens when a step fails. Read this 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 takes your intent and does four things: verifies the target on-chain (`asset()` for an ERC-4626 vault, the reserve list for Aave), reads live vault state (`maxDeposit` headroom, deposit minimum, `previewDeposit` for expected shares), creates a [Smart Routing Address](/onramp/smart-routing-address) with the deposit actions stored at creation, and returns the quote. A wrong target fails here, typed, before any funds move. The stored actions cannot be changed afterwards.

**2. Fund.** The user signs one transfer of `amount` `token` to the SRA. That is the only signature in the flow.

**3. Bridge.** The relayer bridges the funds to the SRA on the destination chain. Same-chain deposits skip this.

**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, so no dust is stranded by a locked-in figure.

**5. Track.** Status comes from on-chain evidence at the SRA, never from an assertion the server cannot prove.

## 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 and never has custody. The SRA is a permissionless contract, and funds leave it in exactly two ways: the stored actions, or an owner withdrawal.

## When something fails

| Failure | What happens |
|---|---|
| Wrong target vault | Caught by the on-chain probe at quote time. `VAULT_TYPE_MISMATCH` or `ASSET_MISMATCH` in milliseconds, no funds moved. |
| Vault fills up after the quote | The deposit call reverts. Funds stay in the SRA, recoverable by the `owner`. |
| User sends the wrong token | Tokens outside the route rest in the SRA. Same recovery path. |
| User never sends funds | The recipe reports `ABANDONED` after one hour. Nothing is lost, and late funds still execute. |

No failure mode hands the funds to ZeroDev or to a third party. See [recovering a failed deposit](/onramp/smart-recipes/recipes#recovering-a-failed-deposit).

## Quote expiry

`expiresAt` is stamped about 60 seconds out, and it is **advisory**. The server does not reject a late funding transaction, and the SRA stays valid: funds sent after `expiresAt` still execute.

What goes stale is the numbers. `estimatedFees`, `estimatedShares`, and `vaultApy` drift, and vault headroom can disappear, which turns into a revert at execution rather than an error at quote time. So re-quote to keep a UI honest, and re-quote whenever the user edits the form. A quote is free and read-only.

## Slippage

You set `slippage` in bps at quote time, and SRA enforces the minimum-output floor on-chain at execution. A quoted estimate cannot be front-run below your floor. If the slippage you ask for cannot cover the route's own fees, the quote is rejected up front with `SLIPPAGE_TOO_LOW` rather than stranding the deposit at fill time.

## Why the server builds the route

Routes, vault lists, calldata, and compliance screening change weekly, so 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, and every quote screens the owner against the OFAC SDN list. The SDK stays a thin, dependency-free HTTP client that you rarely need to upgrade.
43 changes: 43 additions & 0 deletions docs/pages/onramp/smart-recipes/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Smart Recipes

**Cross-chain DeFi deposits in one call.** Your user holds USDC on Base. Your app deposits it into a vault on Arbitrum. One quote, one signature. You write no bridge, vault, or relayer code.

```ts
const quote = await sr.morpho.deposit({
owner: user,
amount: "100",
token: TOKENS.USDC,
srcChainId: 8453, // funds on Base
into: vault, // vault on Arbitrum
});
// user signs one transfer, funds arrive in the vault
```

Without it you would build a bridge integration, per-protocol deposit calldata, a relayer to execute on the destination chain, cross-chain status tracking, and a recovery path for each failure. Smart Recipes replaces all of it: the server quotes the route, a [Smart Routing Address](/onramp/smart-routing-address) (SRA) executes it, your user signs a single transfer.

Three things worth knowing up front:

- **Non-custodial.** Funds move through permissionless contracts. ZeroDev never holds them, and only the `owner` can recover a failed deposit.
- **No protocol maintenance.** Vault lists, routes, and calldata live server-side. A new vault needs no SDK update.
- **Any wallet stack.** Each quote carries both a plain transaction batch and an ERC-4337 user operation. The SDK has no signer and no runtime dependencies.

## Recipes

There are two, and one of them is currently off:

| Recipe | Call it with | Status |
|---|---|---|
| Deposit into a vault | `sr.aave.deposit`, `sr.morpho.deposit`, `sr.fluid.deposit`, `sr.yearn.deposit`, `sr.erc4626.deposit`, or `sr.depositIntoVault` | Available |
| Withdraw from a vault | `sr.withdrawFromVault` | Available, same-chain only |
| Bridge and swap | `sr.bridgeAndSwap` | Disabled, always rejects |

The five deposit facades are one recipe with the protocol pre-bound. `sr.depositIntoVault` is the same engine with `protocol` passed explicitly. Everything else in the SDK is discovery, status, and recovery around those calls.

Smart Recipes performs no swaps of its own. A deposit whose funding token differs from the vault asset still works: SRA converts on delivery as part of its own route. See [Recipes](/onramp/smart-recipes/recipes).

## Next

- [Quickstart](/onramp/smart-recipes/quickstart): a first deposit in about 20 lines
- [Recipes](/onramp/smart-recipes/recipes): every parameter, deposits through recovery
- [How it works](/onramp/smart-recipes/how-it-works): the flow, the custody model, failure behavior
- [Reference](/onramp/smart-recipes/reference): the `Quote` type, every method, every error code
100 changes: 100 additions & 0 deletions docs/pages/onramp/smart-recipes/quickstart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Quickstart

## Install

:::code-group

```bash [npm]
npm i @zerodev/smart-recipes
```

```bash [pnpm]
pnpm i @zerodev/smart-recipes
```

```bash [yarn]
yarn add @zerodev/smart-recipes
```

```bash [bun]
bun add @zerodev/smart-recipes
```

:::

Node 18 or later. Ships ESM and CJS, uses the global `fetch`, and has no runtime dependencies.

## Create a client

```ts
import { createSmartRecipes, TOKENS } from "@zerodev/smart-recipes";

const sr = createSmartRecipes({
projectId: "<your ZeroDev project ID>",
});
```

`projectId` is the only required option. There is no API key: the server authorizes requests by `projectId` plus a per-project allowlist of origins and IP addresses.

| Option | Type | Default | Notes |
|---|---|---|---|
| `projectId` | `string` | required | Sent as `x-project-id` on every request |
| `serverUrl` | `string` | ZeroDev's hosted server | Set only for a self-hosted or staging server |
| `fetch` | `typeof fetch` | global `fetch` | Injectable, for tests or non-browser runtimes |
| `timeoutMs` | `number` | `30000` | Per-request abort timeout |
| `maxRetries` | `number` | `2` | Retries on transient failures, idempotent GETs only |

`createSmartRecipes` is synchronous. All async work happens when you call a method.

## Quote a deposit

Find a vault, then deposit into it:

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

// The user's funds are on Base; the vault is on Arbitrum
const quote = await sr.morpho.deposit({
owner: "0xUSER", // funds, signs, and receives the shares
amount: "100", // display units; the server scales by token decimals
token: TOKENS.USDC, // symbol resolves to the canonical per-chain address
srcChainId: 8453, // Base
into: vaults[0], // a Vault object, so destChainId comes from it
});

console.log(quote.sra); // the address the user funds
console.log(quote.estimatedShares); // expected vault shares
console.log(quote.vaultApy); // APY snapshot
```

A quote prepares the transaction and does not broadcast. The SDK holds no signer.

## Execute

Send the funding transaction with your own wallet. The two forms carry the same intent:

```ts
// EOA: send the calls in order
for (const call of quote.transaction.calls) {
await wallet.sendTransaction({ ...call, value: BigInt(call.value) });
}

// ...or as one ERC-4337 user op
await kernelClient.sendUserOp({ callData: quote.userOp.callData });
```

The relayer then runs the vault-side approve and deposit on the destination chain. Your user does not sign those.

Persist `quote.sra` before you execute. It is the handle for tracking and for recovery.

## Track

```ts
const watcher = sr.watchStatus(quote.sra, {
onStatusChange: (s) => console.log(s.state), // PENDING, BRIDGING, EXECUTING, COMPLETED
});
await watcher.done;
```

Next: [Recipes](/onramp/smart-recipes/recipes) for every parameter, or [Reference](/onramp/smart-recipes/reference) for the `Quote` type and error codes.
Loading