Skip to content

Repository files navigation

Yield Router

A web app that finds the best stablecoin yield across chains and protocols, decides whether moving capital is worth the switching cost, and lets the user execute the deposit with their own wallet via the LI.FI Widget. The app never holds keys.

The full design brief is in CLAUDE_CODE_HANDOVER.md.

What's inside

  • Pure decision engine (src/engine/*) — decide() is a pure async function that returns ranked, qualifying Moves. The switching-cost lookup is injected so the engine is fully unit-testable with no network. Tests live in test/.
  • Server-side discovery + cost (src/lib/earnClient.ts, src/engine/cost.ts) — wrappers over the LI.FI Earn Data API and the Core API's /v1/quote. They run only inside /api/* route handlers so LIFI_API_KEY never reaches the client.
  • Dashboard (src/app/page.tsx) — connects a wallet, reads the user's USDC balance on Base, calls /api/plan, and renders the ranked vault list and the recommendation card.
  • Deposit via Widget (src/components/DepositWidget.tsx) — mounts @lifi/widget with toToken set to the recommended vault so Composer auto-activates. Shares the app's wagmi config via useSyncWagmiConfig, so the same connected account drives both the dashboard and the deposit flow.
  • Fixtures (test/fixtures/) — real responses from earn.li.fi/v1/vaults, /portfolio/{addr}/positions, and a Composer li.quest/v1/quote. Captured against the live API; used as the source of truth for engine tests.

Setup

Node 22 LTS (Node 20 also works with a warning). Copy and fill in the env file:

cp .env.local.example .env.local
# fill in: LIFI_API_KEY, NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, RPC URLs
  • LIFI_API_KEY — get one at https://portal.li.fi. Required: the public earn.li.fi endpoints now return 401 without a key.
  • NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID — from https://cloud.reown.com. Required to drive the wallet-connect modal.
  • RPC_URL_* — server-only. Used for the optional dev-only Hardhat fork harness (Section 18 of the handover). Not required for the web app.

Then:

npm install --legacy-peer-deps
npm run test:unit   # engine tests, no network
npm run dev         # http://localhost:3000

Note: --legacy-peer-deps is required because @lifi/widget lists peer deps for non-EVM chain SDKs (Solana, Sui, Bitcoin) that bring their own incompatible peer ranges. We install the minimum needed to satisfy the imports.

End-to-end test (M6 acceptance)

  1. Open http://localhost:3000 in a browser with a wallet extension.
  2. Connect a wallet that holds at least a few dollars of USDC on Base.
  3. Wait for /api/plan to return — you should see the Recommended move card and the Base USDC vaults (ranked) table.
  4. Click Execute via LI.FI Widget → on the recommendation (or Deposit → on any eligible vault). The Widget mounts inline, prefilled with the vault as the destination token. Composer activates automatically because toToken is a vault.
  5. Sign the route in your wallet to complete the deposit.

Project layout

yield-router/
  CLAUDE_CODE_HANDOVER.md            # the brief; canonical reference
  README.md
  package.json
  next.config.mjs
  postcss.config.mjs                 # empty; overrides any parent config
  tsconfig.json
  .env.local.example
  src/
    app/
      layout.tsx
      page.tsx                       # dashboard
      providers.tsx                  # wagmi + RainbowKit + react-query
      globals.css
      api/
        vaults/route.ts              # GET → proxied Earn discovery
        plan/route.ts                # GET → engine recommendation
    components/
      ConnectBar.tsx
      ClientOnly.tsx
      DepositWidget.tsx
      RecommendationCard.tsx
      VaultList.tsx
    engine/
      score.ts                       # effective APY + eligibility
      cost.ts                        # cost extraction + fetcher
      decide.ts                      # PURE decision function
    hooks/
      useHydrated.ts
    lib/
      chains.ts
      earnClient.ts                  # server-only Earn fetcher
      planTypes.ts
      policy.ts                      # default Policy + presets
      types.ts                       # NormalizedVault, Position, ...
      wagmi.ts                       # app-wide wagmi config
  test/
    fixtures/                        # captured Earn + quote responses
    score.test.ts
    cost.test.ts
    decide.test.ts

Notes carried over from implementation

  • isComposerSupported is a query filter on /vaults, not a vault field. Engine eligibility uses isTransactional alone, which the OpenAPI spec defines as "Supports Composer API".
  • tvl.usd is a string in the API response; parsed to a number in the engine.
  • apy.base and apy.reward can be null. Coerced to 0 with explicit guards (src/engine/score.ts).
  • Position objects carry no APY. currentNetApy is resolved via a vault lookup in /api/plan before decide() is called; idle balances enter the engine with currentNetApy = 0.
  • Effective cost uses fromAmountUSD − toAmountUSD with a fallback to sum(gasCosts + feeCosts) if the USD amounts aren't present. The side-effect: vaults whose routes go via a DEX (rather than a native deposit step) get penalised correctly for the price impact.
  • Wagmi sharing uses useSyncWagmiConfig from @lifi/wallet-management, which is the supported v3-widget pattern.

Out of scope (per the brief)

  • No CLI. No server-side signing. No private keys anywhere.
  • No vault-to-vault rebalancing yet — MVP only deploys idle stablecoin.
  • No non-EVM chains.
  • The Hardhat fork harness in Section 18 of the brief is dev-only and not wired in.

Useful commands

npm run test:unit       # engine + cost unit tests (no network)
npm run typecheck       # tsc --noEmit
npm run dev             # local development server
npm run build           # production build

About

Experimental yield optimisation engine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages