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.
- Pure decision engine (
src/engine/*) —decide()is a pure async function that returns ranked, qualifyingMoves. The switching-cost lookup is injected so the engine is fully unit-testable with no network. Tests live intest/. - 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 soLIFI_API_KEYnever 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/widgetwithtoTokenset to the recommended vault so Composer auto-activates. Shares the app's wagmi config viauseSyncWagmiConfig, so the same connected account drives both the dashboard and the deposit flow. - Fixtures (
test/fixtures/) — real responses fromearn.li.fi/v1/vaults,/portfolio/{addr}/positions, and a Composerli.quest/v1/quote. Captured against the live API; used as the source of truth for engine tests.
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 URLsLIFI_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:3000Note:
--legacy-peer-depsis required because@lifi/widgetlists 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.
- Open http://localhost:3000 in a browser with a wallet extension.
- Connect a wallet that holds at least a few dollars of USDC on Base.
- Wait for
/api/planto return — you should see the Recommended move card and the Base USDC vaults (ranked) table. - 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
toTokenis a vault. - Sign the route in your wallet to complete the deposit.
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
isComposerSupportedis a query filter on/vaults, not a vault field. Engine eligibility usesisTransactionalalone, which the OpenAPI spec defines as "Supports Composer API".tvl.usdis a string in the API response; parsed to a number in the engine.apy.baseandapy.rewardcan be null. Coerced to 0 with explicit guards (src/engine/score.ts).- Position objects carry no APY.
currentNetApyis resolved via a vault lookup in/api/planbeforedecide()is called; idle balances enter the engine withcurrentNetApy = 0. - Effective cost uses
fromAmountUSD − toAmountUSDwith a fallback tosum(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
useSyncWagmiConfigfrom@lifi/wallet-management, which is the supported v3-widget pattern.
- 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.
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