Carry

Carry shows your true LP return on Uniswap - net of LVR, not headline APR.

Project Description

Carry is a concentrated-liquidity strategist for Uniswap on Base that shows your real return — net of LVR.

Every LP dashboard quotes a headline fee APR: fees ÷ capital. It looks amazing (40%, 80%, 200%) and it's misleading, because it ignores the single biggest cost of providing liquidity — LVR (loss-versus-rebalancing). An LP is structurally short gamma: on every price move, arbitrageurs rebalance your position against you at stale prices, so you're always selling the winner and buying the loser. That's not hand-wavy "impermanent loss" — it's a closed-form cost that scales with volatility (≈ σ²/8 per year for a full-range pool; Milionis–Moallemi–Roughgarden, 2022). Most LPs never see it, so they chase fat APRs into pools that are quietly bleeding.

Carry collapses this into one honest number: Net APR = Gross fee carry − LVR drag. Green means the fees pay you to take the volatility — LP it. Red means you're paying the pool to hold your bags — don't.

How it works. For any pool, Carry pulls realized volatility and volume from daily on-chain price history (GeckoTerminal) and reads live pool state — tick, liquidity — directly from Uniswap on Base via viem. It then models four observable quantities: σ (realized volatility), time-in-range (a lognormal price model for how long your band stays active), the v3 concentration factor (how much a tight range deepens your liquidity, 6–200×), and your fee share. The key insight it surfaces: concentration is a double-edged sword — tightening your range multiplies your fees and your LVR by the exact same factor, so there's a real optimum to find.

What you can do. Drag a price range in the live explorer and watch net-of-LVR APR update instantly; take a risk-profiled recommendation (ranges sized in volatility sigmas — conservative 2.5σ, balanced 1.5σ, aggressive 0.75σ); compare pools on a leaderboard ranked by net carry rather than headline yield; and review and manage live positions with a connected wallet.

Stack: Next.js 16, React 19, TanStack Query, shadcn/ui, viem/wagmi, Dynamic (wallet), GeckoTerminal + on-chain Uniswap data, all on Base.

How it's Made

The core is a pure, tested TypeScript model — everything else feeds it.

The LVR engine (src/lib/lvr) is dependency-free and unit-tested: realized volatility from log-returns, the v3 concentration factor 1/(1−⁴√(p_lo/p_hi)), a driftless-lognormal time-in-range, gross fee APR, and the σ²/8 · C · P LVR term — netting to a single Net APR. Because it's pure math with zero I/O, the range explorer recomputes net-of-LVR live on every drag of the slider with no network round-trip — the snappy "aha" of the demo is just a function call.

Two-tier data layer. Pool economics (price, realized σ, annualized volume) come from GeckoTerminal's daily OHLCV — cached server-side with Next's revalidate: 60 to stay under the free-tier rate limit. Live pool microstructure (tick, liquidity, token decimals) is read straight from Uniswap v3 on Base via viem multicall, batching slot0/liquidity/token0/token1 into one RPC call. Open positions are discovered by reading the NonfungiblePositionManager (balanceOf → tokenOfOwnerByIndex → positions), again batched through multicall and filtered to live liquidity.

Uniswap partner APIs do the heavy lifting on transactions: the Trade API (/quote) gives an orientation-agnostic spot price, and the Liquidity API (/lp/create) returns unsigned mint transactions for a chosen tick range — so we never hand-roll calldata. That call is wrapped in a retry that backs off on 5xx (Uniswap's gateway throws transient 504s) but fails fast on 4xx.

Frontend: Next.js 16 / React 19, TanStack Query for fetching/caching, shadcn/ui + Recharts for the price-history range chart, Dynamic for wallet connection — all on Base.

Hacky bits worth calling out:

  • Orientation-robust pricing. GeckoTerminal arbitrarily labels either token as "base," so we always price the non-USDC (volatile) side — otherwise half the pools quote upside-down. Same care flows through which token is token0/token1 on-chain.
  • Bypassing Dynamic's proxy for sends. Dynamic's proxied wallet client throws 4100 for external wallets on Base, so for broadcasting we reach past it to MetaMask's raw injected provider (de-duping via window.ethereum.providers.find(isMetaMask)), then switchChain/addChain to Base and waitForTransactionReceipt on each tx in sequence.
  • The whole model runs identically on server and client — same file powers the API recommendations and the live in-browser explorer, so there's a single source of truth for the math.

Stack: Next.js 16, React 19, TypeScript, TanStack Query, shadcn/ui, Recharts, viem/wagmi, Dynamic, Uniswap Trade + Liquidity APIs, GeckoTerminal, Base.

background image mobile

Join the mailing list

Get the latest news and updates

Carry | ETHGlobal