Skip to main content
Glama

REALIZED

Start here

if you are…

read

why

judging, 2 minutes

FOR-JUDGES.md

problem, method, key numbers with ranges, 30-second reproduction

judging The Graph — AI track

SKILL.md + llms.txt

the agent surface: 7 MCP tools, reporting rules

judging The Graph — composability

COMPOSABILITY.md

one query shape across 3 protocols / 4 chains / 6 venue-chain pairs

judging Uniswap

FEEDBACK.md + Verify our integration

developer feedback citing Uniswap's own schema; every claim at its line

running it

Use it in 30 seconds

no API key needed, CORS open

checking our honesty

Honesty box · Canaries

our own retractions, and the failure we shipped instead of a signal

Repo layout

packages/core/src/   the library — all math and subgraph access (start at realized.js, v4.js)
bin/                 api.js (HTTP) · mcp-server.js (7 agent tools)
scripts/             build-*.js regenerate everything; spike-hunt*/probe-* are the
                     experiments we ran and published as failures, kept on purpose
data/                generated corpora + the evidence behind published findings.
                     Large and tracked deliberately — see REVIEW-GUARDRAILS.md before grepping
test/                canary.test.js — the negative control this project exists for

TL;DR — Every DEX shows liquidity providers an APR made only of fees, so it can never display a loss. Realized computes the number that can: fees + impermanent loss, from The Graph's historical poolDayData. Live site: realized.drainfun.xyz · library: packages/core · MCP server: bin/mcp-server.js (7 tools)

Judging this? FOR-JUDGES.md is the one-page version: problem, method, key numbers with their ranges, what refuses to produce a number, what we deliberately did not ship, and a 30-second reproduction.

Uniswap judges: FEEDBACK.md is our developer feedback on the v3 and v4 subgraphs — what worked, and the four things that cost us build time, each with the file and line it produced. Integration points to verify are listed at the bottom of that file.

The problem. You provide liquidity, the dashboard says +12% APR, and months later your position is worth less than if you had done nothing. The dashboard was not lying about fees — it simply has no term for price. Advertised APR is fee income annualized, so it is positive by construction.

The solution. Realized adds the missing term. Give it a pool and an entry date; it returns what an LP actually took home, next to what was advertised, with the gap named.

Why it's credible. Every number is recomputed live from The Graph and regenerable end-to-end by scripts/build-corpus.js. On live Uniswap v3 data, 34–62% of pools advertised a positive APR while LPs went backwards — and the share rises the tighter your range.

A real position

Snapshot as of 2026-09-13 from the live API (not a remembered card). The 30-day hero on the landing page is a different window; do not mix them.

USDC/WETH · entered 2026-07-28 · held 48 days · ±2x range

  fees earned         +0.51%
  impermanent loss    -3.20%
  ------------------------------
  realized            -2.69%   (-20.4% annualized)

In range the entire time. No liquidation, no exotic pair. This endpoint does not return advertised APR (that is the pool/window call). The loss is IL, not missing fees.

Those figures move as the window slides. Reproduce them yourself:

curl "https://realized.drainfun.xyz/api/position/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-28&range=2"

Who this is for

you are

you get

an LP

the real number for a position you already hold, at your entry date and range

a dashboard / protocol team

an API + library in this repo so your UI can stop showing a metric that cannot go negative

an AI agent

an MCP server whose tools answer "did this pool actually pay?" instead of reciting APR

Use it in 30 seconds

# 1. Ask the live API (no key, no install)
curl "https://realized.drainfun.xyz/api/position/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-28&range=2"

# 1b. Don't know your range? Read it off-chain from your address (read-only, no signing)
curl "https://realized.drainfun.xyz/api/wallet/0xYourAddress"

# 2. Or in code — the math is a standalone package in this repo
git clone https://github.com/jiggy-cadence/realized && cd realized && npm install
// packages/core is self-contained: no build step, no bundler, plain ESM.
import { gatewayUrl, fetchPoolFrom, positionRealized } from './packages/core/src/index.js';
const pool = await fetchPoolFrom(gatewayUrl(process.env.GRAPH_API_KEY), poolId, entryTs);
positionRealized(pool, 2).realizedReturnPct;   // the number your dashboard can't show

3. Or as an MCP server, for agents

Paste this into Claude Desktop (claude_desktop_config.json), Cursor (.cursor/mcp.json), or any MCP client, then restart it:

{
  "mcpServers": {
    "realized": {
      "command": "node",
      "args": ["/absolute/path/to/realized/bin/mcp-server.js"],
      "env": { "GRAPH_API_KEY": "your-subgraph-studio-key" }
    }
  }
}

7 tools: find_pool · realized_return · position_realized · simulate_exit · audit_pools · explain_gap · rank_pools

A free Graph key comes from Subgraph Studio. Or skip the key entirely — every tool above is also a plain HTTP GET against our server, which proxies its own key:

curl "https://realized.drainfun.xyz/api/find?q=WETH/USDC"

Ask your agent: "I entered the USDC/WETH 0.05% pool on 2026-07-28 at a ±2x range. What did I actually make, and what would it cost me to close?"

Machine-readable spec for every endpoint: /openapi.json (OpenAPI 3.1) — so an agent never has to guess a request shape or an error code.

Deciding whether to exit, not just checking the score

curl "https://realized.drainfun.xyz/api/simulate-exit/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-28&range=2&stake=25000"

Same math as position_realized, reframed around the moment of deciding: net dollars on a stated stake, today — now including what it actually costs to get out.

Gas is measured. Live gas price from 1inch × estimated units for decreaseLiquidity + collect (~270k), priced in the chain's native token. The price is live; the units are an estimate, so the figure ships with isEstimate: true rather than posing as exact.

Slippage on a plain close is zero — and that zero is measured, not missing. Closing a v3 position isn't a swap: you get both tokens back at the current tick, so there's no price impact. Slippage only appears if you then consolidate into one token:

curl "https://realized.drainfun.xyz/api/simulate-exit/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-28&stake=25000&consolidate=true"

That estimate is an explicit lower bound — modelled against total pool TVL, while a concentrated pool's depth at the active tick is thinner, so real impact is likely higher.

netAfterCostsUsd is null unless both costs are known. A partial subtraction dressed as a complete number is the exact defect this project exists to expose.

Your real range, not an assumed one

Every realized number needs a range width, and ±2× is a guess. Paste an address and we read the band you actually set:

curl "https://realized.drainfun.xyz/api/wallet/0xYourAddress"

Read-only — no signing, no wallet connection, no permissions. It is a subgraph lookup against a public address. You get each open position's real tickLower/tickUpper, a derived range.widthX to feed straight into /api/position/, and a plain-language label for humans.

It is position discovery, not per-position P&L — and that is a measured decision, not a missing feature. Two Position fields look like they'd hand you fees and entry value. Both are traps:

  • collectedFees* is a withdrawal record, not an earnings record — it populates only when the LP calls collect(). Measured live: of 150 positions with collectedFeesToken0 == 0, 71 had non-zero feeGrowthInside0LastX128. They earned fees and never collected. So uncollected fees report measurable: false with a reason and never $0 — a fabricated zero is a number whose label lies, which is the precise defect this project exists to expose, aimed at the user's own money.

  • deposited*/withdrawn* are lifetime cumulative, not entry state — 75% of live positions read as one-sided because of it. They ship namespaced under cumulative and must not be read as position value.

Every response carries a limits block stating both, and token decimals ship beside every raw amount so no consumer has to guess a divisor.

What we do NOT claim

The narrow claim is the strong one, and it is the only one we make:

  • We do not claim "LPs lose to HODL." Sometimes they do, often they don't. Not our finding.

  • We claim the advertised metric is systematically uninformative about realized outcomes for concentrated positions — and we measure it live rather than asserting it.

  • Fee uplift for concentrated ranges is not modelled. Tighter positions earn more fees than we credit them, so tight-range losses shown here are lower bounds. The direction is robust; treat tight-range magnitudes as bounded, not exact.

  • Fees are pool-level, not per-position. We cannot see your individual liquidity share, so this is what a representative LP at that range earned, not your exact wallet. /api/wallet/ gives us your real range, which is the input that actually moves the answer — it does not give us your fees, and we refuse to invent them (see below).

  • IL is computed closed-form from entry/exit price ratio, not by replaying every rebalance.

Full limitations, sensitivity across gates, and a retracted claim we killed ourselves are in Honesty below.


The defect

Advertised APR is fee income, annualized. It has no price term. So it is positive by construction: a pool cannot advertise a loss, no matter what happened to the people in it.

Realized return = fee income + impermanent loss (IL is always ≤ 0).

This isn't academic. Our own trading stack scored an LP position +1.7% WIN when the real outcome was about −14%, because resolution was fee-only and arithmetically could not lose. That call is the negative control in test/canary.test.js — the test that failure never had.

Related MCP server: echoledger-mcp

Verify our integration — every claim, at its line

For Uniswap and The Graph judges: this table is the map. Each row is a claim we make somewhere else in this README, next to the exact code that implements it.

Uniswap stack integration

what we integrated

where, exactly

Uniswap v3 subgraph IDs, 4 chains (mainnet, Arbitrum, Polygon, Base)

venues.js:10-15

Uniswap v4 subgraph ID (mainnet)

venues.js:20-22 · v4.js:30

v3 position-state read — the range is on the NFT, so tickLower/tickUpper is a lookup

wallet.js:121-144 (POSITION_FIELDS) · fetch at wallet.js:146

v4 position reconstruction — the v4 Position entity has no tick range, so we replay events

v4.js:101-194 (reconstructPositions)

v4 paging by origin, not sender (sender is the position manager contract — returns zero rows)

v4.js:39-93, query at v4.js:58

v4 amount: 0 filter — ~48% of events are fee-collection no-ops; counting them invents positions

v4.js:107 and v4.js:216

v4 transferred-in positions — net-negative keys classified, size withheld

v4.js:140-153 (incompleteHistory)

v4 self-audit shipped in every response (audit.checks[] + allPass)

v4.js:196-251 (auditReconstruction)

Per-venue capability map — v4 is realizedReturn:false, exitSimulation:false on purpose

venues.js:38-74 (CAPABILITIES)

Concentrated-liquidity IL math (v3/v4 range geometry, not the v2 formula)

concentrated.js:17-54

Developer feedback for the Uniswap track

FEEDBACK.md

The Graph integration

what

where, exactly

Gateway URL built from a Subgraph Studio key (live data, no mocks)

realized.js:43-46 (gatewayUrl)

The single query function every venue and chain goes through

realized.js:48-69 (query)

poolDayData historical fetch — the join that makes realized return computable

realized.js:86-166

Realized return = fees + IL, one implementation for all venues/chains

realized.js:251-321 (positionRealized)

Exit simulation with live gas and measured-zero slippage

realized.js:168-249

Venue/chain resolution — one map, not one code path per chain

venues.js:81-89 (subgraphId, venueList)

MCP server, 7 tools

bin/mcp-server.js — tool defs at lines 24, 44, 73, 104, 136, 155, 167

HTTP API (/api/wallet, /api/position, /api/simulate-exit, /api/venues)

bin/api.js — venue dispatch + address validation at api.js:216-240

The refusals — where we decline to produce a number

These are the lines that make the honesty claims checkable rather than rhetorical.

refusal

where

Uncollected fees → measurable:false, never $0 (of 150 positions reading zero collected, 71 had real accrued fees)

wallet.js:174-200 (describePosition), rationale at wallet.js:241

Truncated v4 event history → openPositions:null, positions withheld entirely

v4.js:253-300 (walletV4)

Aerodrome wallet lookup → explicit error (no per-owner Position entity)

venues.js:64-72 · api.js:229

Malformed address → 400 on every venue path

api.js:216-232

Stable-pair canary fails → the build refuses to write output

test/canary.test.js


What this is

An MCP server + library that computes realized LP return from The Graph's historical poolDayData, and compares it to the advertised number.

  • realized_return(pool) — fees, IL, realized vs advertised, for one pool

  • audit_pools(n) — corpus-wide scan with sensitivity + canary

  • explain_gap(pool) — where the difference came from

Why The Graph specifically: per-day fee totals in USD are derived aggregates produced by the indexer.

Could you get this from an RPC node instead? Partly, and we checked rather than asserted. feeGrowthGlobal0X128 is public state on every v3 pool, readable at any historical block — verified live against an archive node on 2026-09-11. So fee accrual is reconstructible.

What isn't cheap is everything after that. The accumulator is a Q128 value in token units, so per-day USD requires an archive node, a block lookup for each day boundary, a historical price for both tokens at each boundary, and the whole thing repeated per pool. For a 376-pool, 4-chain, daily-resolution corpus that is thousands of archive calls per rebuild.

So the honest claim is practicality, not impossibility: The Graph publishes that join already computed and consistent across every pool we measure. An earlier version of this README said "there is no RPC path," which was too strong — corrected here for the same reason the rest of this project exists.

Findings (live, reproducible)

Regenerate everything with GRAPH_API_KEY=... node scripts/build-corpus.jsdata/corpus.json. Diff it against the subgraph yourself.

The defect survives every knob we can turn

Uniswap v3 LPs concentrate their liquidity into a price band. Inside a band, impermanent loss is amplified; once price leaves the band you are fully converted into the losing asset and the loss is no longer "impermanent" at all. Full-range is therefore the most generous possible case for the pool — so we report every range, and the finding has to survive all of them.

range (±width)

median IL

median realized

knocked out of range

advertised + while real −

tight ±1.25x

−1.28%

−0.86%

25/102

62%

moderate ±2x

−0.46%

−0.04%

0/102

50%

wide ±4x

−0.27%

+0.02%

0/102

45%

full-range (v2-equivalent)

−0.13%

+0.06%

0/102

34%

Every row is the same 102 live volatile pools over the same 30-day window. Only the assumed LP range changes. The direction never flips: the tighter and more realistic your position, the more the advertised number lies to you.

The fee term is held constant across ranges, which is deliberately unfair to us — concentrating earns more fees too, so some of that IL is earned back. We did not model the fee uplift because we cannot measure per-position fees from pool-level data. The direction of the defect is what survives; treat the magnitudes as bounded by that caveat.

It is not one lucky month, and it is not one chain

Every number above came from a single 30-day window ending the day we ran it — one draw. "34–62% of pools mislead" could have been a fact about DeFi or a fact about August 2026, and nothing in the repo could tell the difference. So we re-ran the whole instrument twice more.

Stability check (scripts/walk-forward.jsdata/walkforward.json): 13 overlapping 30-day windows over 210 days of mainnet. 13 of 13 trusted, 0 untrusted, defect present in every one — tight-range min 44%, median 59%, max 69%.

Those 13 windows step every 15 days but span 30, so adjacent windows share half their data. They are not independent samples. It is a stability check, not a hypothesis test.

Independent months, four chains (scripts/history-run.jsdata/history.json): strictly non-overlapping 30-day tiles — no shared days, so the window count is a real sample count.

chain

trusted months

oldest

tight ±1.25x

moderate ±2x

full-range

mainnet

30

2024-04-20

26–69% (med 54%)

23–57% (med 39%)

11–43% (med 23%)

arbitrum

30

2024-04-20

31–87% (med 60%)

14–75% (med 46%)

3–58% (med 26%)

base

27

2024-04-20

55–95% (med 67%)

38–82% (med 54%)

18–57% (med 33%)

polygon

23

2024-04-20

17–92% (med 56%)

4–91% (med 43%)

0–68% (med 21%)

110 independent monthly windows across 4 chains. The defect is present on every chain and in nearly every month. Base is worst: its best month still had 55% of pools advertising a positive APR to a tight-range LP who lost money.

No Wayback Machine required. We were about to reconstruct history by scraping archived DeFi dashboards. Checked the primary source first: poolDayData reaches 2021-05-05 on mainnet (1,952 days for USDC/WETH). The subgraph carries full history natively — no archive gaps, no HTML parsing, no third-party copy. One query saved an entire subsystem.

Chains reported as unavailable, never as clean. Optimism's subgraph answers, but only 29 pools clear the TVL floor, leaving n=12 per window — below our n≥20 bar, so all 30 windows are marked trusted: false and no Optimism number is quoted anywhere. BNB, Celo and Avalanche return bad indexers / no allocations on the decentralized network. A chain we could not measure is never reported as a chain without the defect.

Not a Uniswap artifact

Everything above is Uniswap v3. If the defect lived only there, the honest headline would be "Uniswap's advertised APR is broken" — a much smaller claim. So we ran the same instrument, unchanged, on an independent DEX: scripts/cross-dex.jsdata/cross-dex.json.

venue

chain

trusted months

tight ±1.25x

moderate ±2x

full-range

Aerodrome Slipstream

base

24

23–69% (med 51%)

12–63% (med 30%)

0–43% (med 16%)

Different team, different codebase, different incentive model (veAERO emissions rather than pure fee capture) — same defect, same shape, same range-ordering. It is a property of how concentrated liquidity advertises itself, not a quirk of one DEX.

What each venue can actually answer

Not every venue supports every question, and the differences are not cosmetic — they come from what the subgraph exposes. /api/venues publishes this as booleans so an agent can check capability instead of assuming it.

venue

chains

source

pool analytics

wallet lookup

real range

realized return

exit sim

Uniswap v3

mainnet, arbitrum, polygon, base

position state

yes

yes

yes

yes

yes

Uniswap v4

mainnet

event reconstruction

yes

yes

yes

no

no

Aerodrome

base

pool-only

yes

no

Why v4 is a different kind of answer. The v4 Position entity carries only id/tokenId/owner/origin/createdAtTimestampno tick range at all. In v3 the range sits on the position NFT, so reading "the band you actually set" is a lookup: the chain already did the bookkeeping. In v4 the range exists only in the event log, so we reconstruct it by summing signed ModifyLiquidity.amount per (pool, tickLower, tickUpper).

That is bookkeeping, not new mathematics — and it is worth being precise about what it buys and what it costs. It supports the range and in/out-of-range. It does not support realized return or exit pricing, and we refuse to compute them rather than publish a confident wrong number: event-derived state is a strictly weaker evidence class than a state read, and it inherits gaps a state read never has. Three of those gaps are handled explicitly, each found by measurement:

  • ~48% of v4 events are amount: 0 fee-collection no-ops. Counting them invents positions at real-looking tick ranges, so they are discarded.

  • Removals with no matching add mean the position was transferred in — its adds happened under a different origin. Reported as incompleteHistory[] with the reason, size deliberately withheld. Neither dropped silently nor counted as liquidity.

  • Over 60000 events, we report nothing. openPositions: null plus an explicit error. Found by running the reconstruction across five unrelated wallets: two hit the fetch cap and reported 1471 and 501 "open positions" while every consistency check passed. A truncated sum is self-consistent and wrong — consistency is not completeness. The audit now checks both, and ships audit.checks[] + audit.allPass in every v4 response.

Lookups key on origin (the EOA), never sendersender is the position manager contract and matches nothing.

Uniswap v2 is excluded on purpose. v2 LP shares are fungible and always full-range, so "read the range you actually set" has no meaning there. Adding it would dilute the claim, not extend it.

Aerodrome is pool-level only. Its subgraph exposes no per-owner Position entity, so /api/wallet?dex=aerodrome returns an explicit error. Our own capability map claimed otherwise until we tested it — the map was the thing that was wrong, not the endpoint.

SushiSwap v3 is reported as unmeasurable, not as clean. Its subgraph answers, but only 8 pools clear the $250k TVL floor and none is a stable/stable pair — so the canary cannot prove the instrument works there. All 30 windows are trusted: false and no SushiSwap number is quoted, including the 12.5% sitting in the raw JSON. PancakeSwap v3 (BSC + Ethereum), QuickSwap v3 and Camelot v3 return bad indexers / subgraph not found. Six venues probed, two answered, one could be validated.

The volatility check is a falsification test, not a finding

Impermanent loss is mathematically a function of price divergence, so the defect must get worse in volatile months. That is a prediction our own theory makes — if the data did not show it, our instrument would be broken. Across 110 windows, misleading% vs realized volatility:

range

Pearson

Spearman

tight

0.430

0.572

moderate

0.341

0.447

wide

0.303

0.384

full

0.235

0.285

Positive, and monotonically stronger as the range tightens — exactly what the math requires. We ran it to try to break ourselves and failed to.

This is NOT evidence about macro, equities, or crypto beta. We did not test those series and will not imply we did. With enough candidate predictors you can find a "relationship" to anything. A correlation predicted in advance from an identity is a self-check; a correlation found by fishing would be a story.

Per-pool trust: which pools' advertised APR has historically been honest

Walk-forward: does advertised APR at day T predict realized return over T..T+30?

  • 0% sign-flip rate (advertised APR trustworthy): USDT/USDf, AUSD/USDC, cbETH/WETH, WBTC/cbBTC, tBTC/WBTC, LINK/WETH

  • 60–71% sign-flip rate (advertised APR routinely lied): FET/WETH, DAI/WETH, QNT/WETH, PEPE/WETH

The trustworthy set is entirely correlated pairs and the untrustworthy set is entirely divergent ones — the instrument is sorting by the mechanism that actually causes the error, which is evidence it measures something real rather than noise.

Honesty box

Things that are true and inconvenient, kept here on purpose:

  • RETRACTED: corr(advertised, realized) = 0.06 was this README's headline and it does not reproduce. It existed in no committed code. Recomputed honestly (lib/realized.js pearson/spearman, run by build-corpus.js) it lands at Pearson 0.08–0.16, Spearman 0.30–0.33 raw; 0.33–0.38 trimmed across three liveness gates. That is a real spread driven by outlier choice and rank-vs-linear, so quoting any single number as the correlation was the mistake. The correlation is reported as a range with its estimator named, and the headline is now the pool-count finding, which is measured directly rather than estimated. Found 2026-09-07 by grepping our own repo for the number and not finding it.

  • An earlier correlation of +0.42 was also wrong (lookahead in the liveness gate, dead pools included). Two bad correlations in a row is why this metric no longer leads the README.

  • The v2 formula was being applied to a v3 venue. Until 2026-09-07 every realized return here used constant-product IL, which is correct only for full-range positions. That silently reported the best case for every pool. Fixed in lib/concentrated.js; the old number is kept as the full row above so the change is auditable rather than overwritten.

  • Our convergence canary failed us first, and it was right to. The new v3 math was checked by asserting it converges to the v2 formula as range → ∞. Against a hand-picked epsilon it "failed" and nearly got correct code deleted. The error actually falls at the analytic rate — 11.0x, 10.1x, 10.0x per 100x range — so the test now asserts the rate, not a threshold. A tolerance you tune until it passes is a parameter; a convergence rate is not.

  • "1 in 3 pools mislead you" was retracted as a headline because it moved 41%→34% with the liveness cutoff. It appears above only as a full grid across ranges and gates, labeled as what it is.

  • "LPs lose to HODL" is not our discovery. Topaze Blue / Bancor established that in 2021. Our contribution is not the loss — it is the calibration of the advertised metric.

  • DefiLlama already ships yield predictions (11,489 pools carry predictedClass / predictedProbability). What nobody publishes is whether those predictions were rightso we graded them, twice, because the first grade was wrong in two different ways. Attempt 1: wrongly concluded no retrospective archive exists (it does — Wayback Machine, 517 snapshots of /pools back to Oct 2022, full predictions payload intact). Attempt 2 (n=69 pools, capped by rate-limiting): reported a clean +3.2pp edge over baseline. Attempt 3, same method with backoff and no fetch failures (n=459 pools, 8,092 graded instances): the edge flips sign across bands (strict 0%: −1.4pp: WORSE than guessing; mid 5%: +4.5pp; loose 10%: +1.5pp). Overall accuracy is not a finding — it moves with the band, and the small sample's clean number was a fluke of which pools survived rate-limiting. What DID survive the 6x larger sample: their stated confidence is genuinely calibrated. bin1 n=1987 edge −0.3pp, bin2 n=2639 edge +3.1pp, bin3 n=3466 edge +8.3pp — monotonic, wide spread, holds up. The model's raw accuracy is a mirage; its self-reported confidence is an honest, usable signal. See data/llama-grade-backdated.json and scripts/grade-llama-backdated.py. Either way: the target variable (fee-only apy) was already shown near-orthogonal to actual LP outcome (ceiling test above, corr=−0.008).

  • The liveness gate is imperfect. Activity/volume/TVL alone let dead tokens through; priceCollapsed() closes most of that, with the threshold set from observed depeg data (real depegs bottomed at 2–4% of peak, so a <1% cutoff would have caught none of them).

Canaries

Every number here is gated on instruments proving they can find a known-present signal first:

  • Stable/stable pairs must show ~0 impermanent loss, or the whole run is marked untrusted and build-corpus.js refuses to write output (exit 1). Live: worst |IL| = 0.0007%.

  • IL math is checked against closed-form constant-product values (r=2 → −5.72%, r=4 → −20%).

  • "Unmeasurable" stays distinguishable from "zero" — a missing price returns measurable: false, never a fabricated 0.

  • Every headline is reported at three liveness cutoffs. If a finding flips across them, it is labeled a parameter, not a finding.

  • The correlation estimator has its own canary. corr(advertised APR, fee return) must come back clearly positive — both are fee-derived, so a weak value means the estimator is broken, not that the market is strange. Live: 0.726. build-corpus.js exits non-zero and refuses to write if it drops below 0.3.

  • Concentrated IL is checked against the v2 closed form at four range widths, asserting the convergence rate rather than a tuned tolerance.

  • Every historical window runs its own canary. A window that cannot prove its instrument works is emitted as trusted: false, never silently dropped — "we could not measure this month" and "this month was fine" must not look the same in the output.

  • Survivorship bias is disclosed and points against us. Pools are ranked by current volume, so pools that died are absent from historical windows. That makes the past look better than it was, weakening our thesis rather than inflating it. The subgraph exposes no point-in-time ranking, so we state the bias instead of pretending to correct it.

  • Base first returned 0 trusted windows out of 30, and that was the canary working. The top 80 Base pools by volume contained no stable/stable pair, so the instrument could not prove it measures IL correctly and refused to certify. The fix was to fetch the canary its reference pools — never to weaken the canary. Those pools are validation-only and excluded from every headline. An instrument that confidently reports a number it cannot validate is the exact failure this repo exists to prevent.

Run it

One command, the whole pitch, live data, no setup beyond a free API key:

npm install
GRAPH_API_KEY=your-key node demo.js

Get a free key at thegraph.com/studio (Subgraph Studio -> create API key). demo.js fetches a live pool sample from The Graph right now, proves its own canary passes before printing anything, shows one concrete pool's advertised-vs-realized gap across all four concentration ranges, and reports the misleading-pool rate on that sample. No pre-built corpus, no server process — everything above is a live number, fetched during that one command.

For the deeper evidence (110 independent monthly windows, 4 chains, cross-DEX, the DefiLlama grading) or to run the full corpus/MCP server yourself:

node test/canary.test.js                  # offline math + negative control
GRAPH_API_KEY=... node test/canary.test.js --live
GRAPH_API_KEY=... node scripts/build-corpus.js   # single-window corpus
GRAPH_API_KEY=... node scripts/walk-forward.js   # 13 overlapping windows, mainnet
GRAPH_API_KEY=... node scripts/history-run.js    # 110 independent months, 4 chains
GRAPH_API_KEY=... node scripts/cross-dex.js      # Aerodrome + SushiSwap, same instrument
node scripts/build-report.js                     # render report.html from the JSON

The realized_return tool takes an optional rangeWidthX — your actual concentrated position (1.25 = a tight ±25% band, 2 = typical, 4 = wide, omit for full-range). It answers the question no yield dashboard answers: given the range I was actually in, what did I actually make? — including whether price left your band, in which case the loss is realized, not impermanent.

Live example (USDC/WETH, 30d, price ratio 1.3091):

full-range realized:  -0.58%
  tight  ±1.25x       -7.95%   OUT OF RANGE
  moderate ±2x        -2.75%
  wide   ±4x          -1.48%

MCP client config:

{ "mcpServers": { "realized": {
    "command": "node",
    "args": ["bin/mcp-server.js"],
    "env": { "GRAPH_API_KEY": "your-subgraph-studio-key" } } } }

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables querying DeFi yield pool data and risk signals from DefiLlama, including APY, TVL, exploit history, and a gate-then-score risk assessment for curated protocols.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Analyzes live Uniswap V2/V3, Balancer, and Curve stableswap pools for positions, price moves, pool health, rug signals, slippage, and depeg risk, and builds portable State Twins for offline analysis.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to analyze Uniswap V3 pool liquidity depth and estimate price impact at 1%, 2%, 5%, and 10% levels before large trades, with pay-per-call micropayments via x402.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables read-only research on live Hyperliquid vaults through search, ranking, comparison, risk explanation, HLP metrics, and alert tools with timestamped evidence and caveats.
    -