npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zerodev/smart-recipes

v0.8.1

Published

Cross-chain DeFi deposits in one signature. Quote a vault deposit, have the user fund a single address, and a relayer bridges and deposits on the destination chain.

Downloads

572

Readme

@zerodev/smart-recipes

Thin HTTP client for the Smart Recipes server. Express a high-level intent — "deposit into Aave / a Morpho vault / any ERC-4626" or "bridge-and-swap to this token" — as one namespaced call. The call returns a Quote: a Smart Routing Address (SRA) plus a ready-to-sign source-chain transaction (and an equivalent ERC-4337 user op). The user funds the SRA; ZeroDev's relayer bridges to the destination chain (if cross-chain) and runs the pre-baked calls there.

Zero on-chain deps — no viem at runtime, no signer, no bundler. All dynamic logic (SRA creation, calldata, quoter, RPC, vault discovery, fees) lives server-side. Friendly → wire resolution (token symbols, decimal scaling) happens on the server; the SDK returns wire strings verbatim.

Install

pnpm add @zerodev/smart-recipes

ESM + CJS dual build. Node ≥ 18 (uses global fetch; inject your own otherwise).

Quick start

import { createSmartRecipes, TOKENS } from '@zerodev/smart-recipes'

// projectId is the only required option; serverUrl defaults to ZeroDev's hosted server.
const sr = createSmartRecipes({ projectId: 'abc' })

// Cross-chain deposit into a Morpho vault.
const quote = await sr.morpho.deposit({
  owner: '0xUser…',
  amount: '100',          // display units — server scales by token decimals
  token: TOKENS.USDC,     // symbol → canonical per-chain address (or pass a raw address)
  srcChainId: 8453,       // Base
  destChainId: 42161,     // Arbitrum
  into: vault,            // a Vault from listVaults(), or a vault id / address
})

// The call PREPARES, it does not broadcast. Send it yourself:
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 })

createSmartRecipes is synchronous — all async work happens at call time.

Init config

| Param | Type | Required | Notes | |--------------|----------------|----------|-----------------------------------------------------| | projectId | string | yes | ZeroDev project ID (x-project-id header) | | serverUrl | string | no | Defaults to DEFAULT_SERVER_URL (ZeroDev's hosted server). Set only for a self-hosted or staging server; a blank string counts as absent | | fetch | typeof fetch | no | Injectable fetch (tests / non-browser) | | timeoutMs | number | no | Per-request abort timeout, default 30000 | | maxRetries | number | no | Retries on transient failures (idempotent GETs only), default 2 |

Quote-building POSTs are never retried automatically (each call can create a new SRA server-side); GETs retry 429/502/503 and network failures with backoff.

Surface

Facades (protocol namespaces)

Each binds a protocol — the server resolves it to a per-protocol adapter that builds the deposit calls and verifies the target on-chain before quoting (a Morpho-Blue market passed where a 4626 vault is expected fails fast, not after a refund). Same-chain vs cross-chain is picked by srcChainId === destChainId.

sr.aave.deposit(params): Promise<Quote>     // no `into` — pool implied by token + destChainId
sr.morpho.deposit(params): Promise<Quote>   // `into` required (many vaults)
sr.fluid.deposit(params): Promise<Quote>
sr.yearn.deposit(params): Promise<Quote>
sr.erc4626.deposit(params): Promise<Quote>  // unbranded ERC-4626 vault by id/address

Engine methods (generic, top-level)

sr.depositIntoVault(params): Promise<Quote>  // generic deposit — protocol REQUIRED in params
sr.bridgeAndSwap(params): Promise<Quote>     // swap-only; funds returned to owner

depositIntoVault is the unbound path: pass the vault's protocol explicitly ('fluid', 'morpho', …) or 'erc4626' for an unbranded 4626 vault. An unregistered protocol → UNKNOWN_PROTOCOL.

await sr.depositIntoVault({
  owner, amount: '100', token: TOKENS.USDC,
  srcChainId: 42161, destChainId: 42161,
  into: vault, protocol: 'fluid',          // required
})

Discovery + status

sr.listVaults({ asset?, chains?, protocol?, minTvl?, minApy?, page? }): Promise<VaultPage>
sr.getVault(vaultId, chainId?): Promise<VaultDetails>
sr.getChains(): Promise<ChainInfo[]>
sr.getTokens({ chainId? }): Promise<TokenInfo[]>
sr.getStatus(sra): Promise<RecipeStatus>
sr.watchStatus(sra, { interval?, maxRetries?, timeout?, onStatusChange, onError? }): Watcher

listVaults is paged: it returns { vaults: Vault[]; nextPage: number | null }. Pass a zero-based page and walk until nextPage is null. minTvl filters by USD TVL — omit it and vaults.fyi applies its own $100,000 minimum, so pass minTvl: 0 to see lower-TVL vaults. Pass chainId to getVault when known — it hits the direct single-vault endpoint instead of a list scan.

SRA lifecycle + preflight

sr.getSraInfo(params)          sr.getWithdrawCalls(params)
sr.getSraFeeEstimates(params)  sr.getDepositStatus(params)
sr.preflight(params)           sr.listOpportunities(params)   // back-compat alias of listVaults

getWithdrawCalls is the recovery path: a failed or partially-executed recipe leaves funds resting in the SRA, and this returns the owner-signed calls that drain them. preflight reports vault state without quoting — including depositsDisabled (on-chain maxDeposit = 0), which vault listings miss.

Shared param vocabulary

Every deposit facade/engine takes this core shape. Facades narrow it — Aave drops into; Morpho/Fluid/Yearn require it.

type DepositParams = {
  owner: Address               // funds + signs + receives shares + gets refunds (one role)
  amount: number | string      // display units; server scales by decimals (string math, no float)
  token: TokenSymbol | Address // symbol → canonical per-chain address; address to disambiguate (USDC.e)
  srcChainId: number           // chain the user funds from
  destChainId?: number         // 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%)
}

Use a string amount for very large / >15-sig-fig values. owner is a single role: funder = signer = shares receiver = refund recipient (no beneficiary).

When into is a Vault object from listVaults(), omit destChainId — the vault carries its own chainId, the single source of truth:

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

TOKENS: USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE. Pass a symbol for the canonical per-chain address, or a raw address for a variant / arbitrary token.

Quote

Unified return for every recipe. Every deposit routes through an SRA — the transaction is a funding transfer to it (same-chain included), so sra is always set on deposit quotes.

type Quote = {
  quoteId: string
  expiresAt: string                            // ISO-8601, typically now + 60s
  sra: Address | null                          // the address to fund; always set for deposits
  transaction: { chainId: number; calls: OnChainCall[] }       // the whole src batch; EOA sends in order
  userOp: { callData: Hex; calls: OnChainCall[]; chainId: number }  // kernel client fills the rest
  estimatedFees: {                             // amounts in route-token base units, NOT USD
    totalFeeAmount: string | null              // null when chains mix denominations — render perChain
    totalFeeToken: Address | null              // null whenever totalFeeAmount is
    perChain: { chainId: number; feeAmount: string | null; feeToken: Address | null }[]
  }
  estimatedReceiveAmount: string               // base units on destChain
  estimatedShares?: string                     // vault recipes only
  vaultApy?: number                            // percent (2.57 = 2.57%), from vaults.fyi
  route?: {                                    // resolved flow, for UI display
    bridgeTokenType: string | null             // token the SRA is funded with
    bridgeTokenSrc?: Address
    bridgeTokenDest?: Address                  // route token, or the vault asset when SRA converts
    sameChain: boolean                         // no bridge leg (still an SRA deposit)
  }
}

type OnChainCall = { to: Address; data: Hex; value: string }   // value is a decimal string

transaction and userOp carry the same intent: one ERC-20 transfer funding the SRA, preceded by owner-signed approve + swap calls only when the funding token differs from the route token. An EOA sends transaction.calls sequentially; a kernel client sends userOp.callData as one ERC-4337 user op (the server encodes a Kernel v3 / ERC-7579 executeBatch(calls); your client fills sender / nonce / gas / signature). The vault-side approve + deposit runs on the destination chain by the SRA relayer, not by the user.

Vault

Returned by listVaults, accepted directly as into. Discriminated union on category — a slim projection of the vaults.fyi listing, carrying both the fields you choose a vault by and the fields a deposit routes with (no re-fetch).

type VaultCommon = {
  id: string                   // server vault id — what `into` keys on
  address: Address             // the vault contract (deposit target)
  chainId: number
  protocol: string             // 'aave' | 'morpho' | 'fluid' | 'yearn' | ...
  asset: { symbol: string; address: Address; decimals: number }
  apy: number | null           // percent (2.57 = 2.57%)
  tvlUsd: number | null
  name?: string
}

type Vault =
  | (VaultCommon & { category: 'lend' })
  | (VaultCommon & { category: 'liquid-staking' })
  | (VaultCommon & { category: 'fixed-yield'; maturity: string })
//                                            ^ reachable only after narrowing on category

maturity is meaningful only for fixed-yield vaults, so reading it off a lend vault is a compile error until you narrow on category.

Status

const watcher = sr.watchStatus(quote.sra, {
  interval: 4000,                              // ms; default 4000
  timeout: 600_000,                            // total watch bound; 0 = poll indefinitely
  onStatusChange: (s) => console.log(s.state), // PENDING → BRIDGING → EXECUTING → COMPLETED
  onError: (e) => console.error(e),            // persistent poll failure or timeout
})
await watcher.done   // resolves on terminal state / unsubscribe, rejects on failure
watcher.stop()       // or call watcher() — unsubscribe

type RecipeState =
  | 'PENDING' | 'BRIDGING' | 'EXECUTING' | 'COMPLETED' | 'FAILED'
  | 'ABANDONED' // SRA never received funds within the abandonment window (1h after quote)

Polling stops automatically on COMPLETED / FAILED / ABANDONED. FAILED statuses carry a failureReason; recover resting funds via getWithdrawCalls. An ABANDONED recipe is not locked out: funds arriving late still execute, and a fresh watchStatus picks the recipe back up from live evidence.

Errors

Every rejection is a SmartRecipeError with a code, message, and requestId (from the server's x-request-id — quote it when reporting a bug). Code-bound subclasses let you branch with instanceof.

import { SmartRecipeError, QuoteExpiredError } from '@zerodev/smart-recipes'

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

Codes: INVALID_REQUEST, UNSUPPORTED_TOKEN, INSUFFICIENT_AMOUNT, SLIPPAGE_TOO_LOW, SWAP_ROUTE_NOT_FOUND, QUOTE_EXPIRED, SANCTIONED_ADDRESS, VAULT_BLOCKED, VAULT_NOT_ALLOWLISTED, CHAIN_NOT_SUPPORTED, VAULT_CAP_EXCEEDED, VAULT_DEPOSITS_DISABLED, UNKNOWN_PROTOCOL, VAULT_TYPE_MISMATCH, ASSET_MISMATCH, FEATURE_DISABLED, IDEMPOTENCY_KEY_CONFLICT, SRA_NOT_FOUND, SRA_UNAVAILABLE, QUOTER_UNAVAILABLE, RPC_UNAVAILABLE, SERVICE_UNAVAILABLE, INTERNAL_ERROR.

Security-gate codes (raised by middleware before the route runs): ACCESS_DENIED (403 — origin/IP not allowlisted), RATE_LIMITED (429 — too many requests; idempotent GETs are retried with backoff), PAYLOAD_TOO_LARGE (413 — request body over the cap).

Worth special handling:

  • FEATURE_DISABLED (403) — the route needs a swap leg and swaps are soft-blocked server-side. Same-token routes (same-chain deposits and plain cross-chain bridges) are unaffected. Retrying with different params won't help until the server flag flips.
  • VAULT_DEPOSITS_DISABLED (400) — the vault's on-chain maxDeposit is 0 (deposits switched off); pick another vault rather than lowering the amount (VAULT_CAP_EXCEEDED is the try-a-smaller-amount case).

Notes

  • All wire amounts are base-10 strings (JSON has no bigint). Parse to bigint yourself: BigInt(quote.estimatedReceiveAmount).
  • A quote prepares, it never broadcasts — you send transaction or userOp.
  • There is no on-chain refund fallback: a reverted destination action leaves funds resting in the SRA, recoverable with getWithdrawCalls.

See SMART_RECIPES_SDK.md (repo root) for the full design.