realized-lp
Check what liquidity providers ACTUALLY earned in a Uniswap v3 or Aerodrome pool — fee income minus impermanent loss — against the fees-only APR that DEX UIs advertise. Use when asked whether an LP position, pool, or yield number is actually profitable, whether an advertised APR is trustworthy, or to audit many pools at once for misleading yield. Backed by live Graph subgraph data.
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 81
- Forks
- 17
01 前言 Front Matter
- name
- realized-lp
- description
- Check what liquidity providers ACTUALLY earned in a Uniswap v3 or Aerodrome pool — fee income minus impermanent loss — against the fees-only APR that DEX UIs advertise. Use when asked whether an LP position, pool, or yield number is actually profitable, whether an advertised APR is trustworthy, or to audit many pools at once for misleading yield. Backed by live Graph subgraph data.
02 资源文件 98
- 其他.gitignore32 B
- 其他.grepignore59 B
- 参考文档api/pools.json70.1 KB
- 资产文件assets/logo-256.png43.5 KB
- 资产文件assets/logo-64.png4.7 KB
- 资产文件assets/logo.jpg33.0 KB
- 脚本bin/api.js29.1 KB
- 脚本bin/mcp-server.js24.4 KB
- 参考文档COMPOSABILITY.md6.1 KB
- 参考文档data/corpus.json1.1 MB
- 参考文档data/cross-dex.json128.2 KB
- 参考文档data/fee-uplift-bound.json3.2 KB
- 参考文档data/history.json357.1 KB
- 参考文档data/leaderboard.json14.4 KB
- 参考文档data/llama-grade-backdated.json2.2 MB
- 参考文档data/llama-layer.json2.5 KB
- 参考文档data/llama-predictions-t0.json3.5 MB
- 参考文档data/pools.json68.3 KB
- 参考文档data/pricecheck.json1.9 KB
- 参考文档data/select-v2.json2.8 KB
- 参考文档data/spike-hunt.json2.4 KB
- 参考文档data/spike-hunt2.json5.0 KB
- 参考文档data/spike-hunt3.json3.5 KB
- 参考文档data/spike-hunt4.json2.3 KB
- 参考文档data/spike-hunt5.json2.9 KB
- 参考文档data/spike-hunt6.json32.1 KB
- 参考文档data/spike-hunt7.json419.9 KB
- 参考文档data/spike-hunt8.json749.2 KB
- 参考文档data/spreads.json25.9 KB
- 参考文档data/walkforward.json29.6 KB
- 参考文档deck.html18.9 KB
- 参考文档DEMO-SCRIPT.md9.9 KB
- 脚本demo.js5.9 KB
- 脚本ecosystem.realized-api.config.cjs307 B
- 参考文档FEEDBACK.md10.4 KB
- 参考文档FOR-JUDGES.md7.7 KB
- 参考文档index.html132.0 KB
- 脚本lib/concentrated.js150 B
- 脚本lib/realized.js358 B
- 脚本lib/venues.js138 B
- 脚本lib/walkforward.js148 B
- 许可证LICENSE1.0 KB
- 参考文档llms.txt7.5 KB
- 参考文档openapi.json42.7 KB
- 参考文档package-lock.json41.4 KB
- 参考文档package.json651 B
- 参考文档packages/core/package.json880 B
- 参考文档packages/core/README.md3.8 KB
- 脚本packages/core/src/concentrated.js2.6 KB
- 脚本packages/core/src/index.js2.9 KB
- 脚本packages/core/src/oneinch.js9.1 KB
- 脚本packages/core/src/realized.js25.6 KB
- 脚本packages/core/src/v4.js13.0 KB
- 脚本packages/core/src/venues.js3.8 KB
- 脚本packages/core/src/walkforward.js5.7 KB
- 脚本packages/core/src/wallet.js11.7 KB
- 参考文档PREREG-selection-v2.md2.5 KB
- 参考文档README.md35.0 KB
- 参考文档report.html19.2 KB
- 参考文档REVIEW-GUARDRAILS.md950 B
- 脚本scripts/build-app.js72.0 KB
- 脚本scripts/build-corpus.js7.6 KB
- 脚本scripts/build-deck.js26.3 KB
- 脚本scripts/build-leaderboard.js6.7 KB
- 脚本scripts/build-llama-layer.js5.7 KB
- 脚本scripts/build-pools.js5.6 KB
- 脚本scripts/build-pricecheck.js8.6 KB
- 脚本scripts/build-report.js15.4 KB
- 脚本scripts/build-spreads.js5.9 KB
- 脚本scripts/cross-dex.js6.8 KB
- 脚本scripts/demo-refresh.js6.1 KB
- 脚本scripts/dex-probe.mjs2.2 KB
- 脚本scripts/fee-uplift-bound.js5.7 KB
- 脚本scripts/grade-llama-backdated.py8.5 KB
- 脚本scripts/grade-llama.py6.7 KB
- 脚本scripts/history-run.js10.5 KB
- 脚本scripts/persistence-test.js3.6 KB
- 脚本scripts/probe-tick-widths.mjs3.1 KB
- 脚本scripts/probe-tick-widths2.mjs3.5 KB
- 脚本scripts/probe-tick-widths3.mjs4.2 KB
- 脚本scripts/select-v2.js10.2 KB
- 脚本scripts/signal-probe.js5.6 KB
- 脚本scripts/spike-hunt.js9.3 KB
- 脚本scripts/spike-hunt2.js14.2 KB
- 脚本scripts/spike-hunt3.js8.5 KB
- 脚本scripts/spike-hunt4.js8.1 KB
- 脚本scripts/spike-hunt5.js10.1 KB
- 脚本scripts/spike-hunt6.js12.1 KB
- 脚本scripts/spike-hunt7.js12.9 KB
- 脚本scripts/spike-hunt8.js13.5 KB
- 脚本scripts/walk-forward.js5.5 KB
- 参考文档SHOT-LIST.md8.9 KB
- 参考文档SOURCE.md329 B
- 参考文档TALKING-POINTS.md12.1 KB
- 脚本test/canary.test.js16.6 KB
- 脚本test/il-tick-crossing-probe.mjs3.2 KB
- 参考文档TRUST.auto.yaml2.2 KB
- 参考文档WALLET-CONNECT-NOTES.md3.6 KB
03 SKILL.md 正文
Realized LP
Advertised LP APR is fee income, annualized. It has no price term, so it cannot go negative.
A pool cannot advertise a loss no matter what happened to the people in it. This skill computes
the number that can: realized = fees + impermanent loss.
When to use this
- "Is this pool actually profitable?" / "Is this APR real?"
- "I'm LPing WETH/USDC, am I making money?"
- "Find me pools where the advertised yield is lying"
- Any yield-farming or LP decision where the advertised number is the only evidence on offer
Quick start (no install, no API key)
curl -s https://realized.drainfun.xyz/api/pools
202 live pools across 4 validated venues. The response embeds its own schema and the exact
impermanent-loss formula in howToComputeRealizedReturn, so you can compute any range yourself
with no further docs. Fields per pool:
| field | meaning |
|---|---|
pair |
token0/token1 symbols |
dex / chain |
uniswap-v3 | aerodrome / mainnet | arbitrum | polygon | base |
r |
price ratio over the window (exit ÷ entry) |
fees |
fee income over window, % of entry TVL |
adv |
advertised APR % — what the DEX UI shows |
days |
window length |
Compute realized return for any range
function il(r, w) { // w = range half-width: 1.25 tight, 2 typical, 1e8 full
const sa = Math.sqrt(1 / w), sb = Math.sqrt(w);
let pos;
if (r <= 1 / w) pos = (1 / sa - 1 / sb) * r;
else if (r >= w) pos = sb - sa;
else pos = 2 * Math.sqrt(r) - sa - r / sb;
return ((pos / ((1 - sa) + (1 - 1 / sb) * r)) - 1) * 100;
}
const realizedPct = pool.fees + il(pool.r, 2);
const realizedApr = (realizedPct / pool.days) * 365;
const misleading = pool.adv > 0 && realizedApr < 0; // advertised a profit you didn't get
r <= 1/w or r >= w means price left the band — the LP is fully converted into the losing
asset and the loss is realized, not impermanent. Always say so; it changes the advice.
Live queries (MCP server)
For pools not in the cached set, or a custom window, run the MCP server. Needs a free Subgraph Studio API key.
git clone https://github.com/jiggy-cadence/realized && cd realized && npm install
Paste 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" }
}
}
}
No key? Skip the MCP server entirely. Every tool is also a plain HTTP GET against our server, which proxies its own Graph key — no auth, CORS open:
curl "https://realized.drainfun.xyz/api/find?q=WETH/USDC"
| tool | use |
|---|---|
find_pool(query) |
start here — resolve "WETH/USDC" or "PEPE" to ranked poolIds. You never need to know an address. |
realized_return(poolId, days?, rangeWidthX?) |
fees vs IL vs advertised for one pool over a recent window |
position_realized(poolId, entryDate, rangeWidthX?) |
what YOU actually made on a position entered on a specific date, however long ago — not "the last 30 days," but "since I actually put money in." See below. |
explain_gap(poolId) |
plain-language verdict + the evidence chain that produced it |
audit_pools(limit?) |
corpus-wide sweep: how many pools advertise a profit LPs didn't get |
Stop assuming the range — read the real one
Every number above needs a range width, and ±2× is a guess. /api/wallet/{address} reads what
the LP actually set:
curl -s "https://realized.drainfun.xyz/api/wallet/0xYourAddress"
Read-only. No signing, no wallet connection, no permissions — it is a subgraph lookup on a public
address, nothing more. Returns each open position with its real tickLower/tickUpper, the
derived range.widthX, and a plain-language range.label. Feed range.widthX into
position_realized (or /api/position/{poolId}?range=) to get realized return at the LP's actual
band instead of an assumed one. That is the whole point of the endpoint.
It searches Uniswap v3 AND v4 — and they are not the same kind of answer
With no ?dex=, the endpoint queries both v3 and v4 on mainnet in parallel and returns
{ searched, foundIn, openPositions, venues: { "uniswap-v3": {…}, "uniswap-v4": {…} } }.
The venues stay in separate blocks deliberately: merging them would produce one position list
where some rows silently cannot support a realized-return number.
| v3 (+ arbitrum/polygon/base), Aerodrome | v4 (mainnet) | |
|---|---|---|
| source | position state read | event reconstruction |
| real range | yes | yes |
| realized return | yes | no |
| exit simulation | yes | no |
Why the asymmetry: the v4 Position entity carries only id/tokenId/owner/origin/timestamps —
no tick range at all. Ranges are reconstructed by summing signed ModifyLiquidity.amount per
(pool, tickLower, tickUpper). That is a strictly weaker evidence class than reading a struct the
chain already maintains, so we report the range and inRange and refuse to price a v4 exit.
/api/venues exposes this per venue as realizedReturn / exitSimulation booleans, so an agent
can check capability instead of assuming it.
If you are an agent consuming v4 results, respect these four things:
- Check
audit.allPassbefore using any number. Every v4 response shipsaudit.checks[]with 5 invariants (completeness, negatives classified, positions strictly positive, ticks ordered, all keys accounted). openPositions: null+errormeans the history was truncated (>5000 ModifyLiquidity events). A truncated sum is self-consistent and wrong, so no positions are reported at all rather than a partial total that looks authoritative.incompleteHistory[]lists ranges where liquidity was removed with no matching add in this wallet's events — almost always a position transferred in. Their size is deliberately not reported; do not infer one.- Lookups key on
origin(the EOA).senderis the position manager contract and matches nothing — if you query v4 events yourself, do not usesender.
Uniswap v2 is not supported, on purpose. v2 LP shares are fungible and always full-range, so "read the range you actually set" has no meaning there — including it would dilute the claim rather than extend it.
It is position discovery, not per-position P&L — deliberately. Two fields look like they'd give you fees and entry value. Both lie:
collectedFees*is a withdrawal record, not an earnings record — it only populates when the LP callscollect(). Measured live: of 150 positions withcollectedFeesToken0 == 0, 71 had non-zerofeeGrowthInside0LastX128— they earned fees and never collected. So uncollected fees returnmeasurable: falsewith a reason, never$0. Reporting zero there would reproduce the exact defect this project exists to expose, pointed 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 are namespaced undercumulativeand must not be read as position value.
Every response carries a limits block stating both, and token decimals ship beside every raw
amount so you never guess the divisor. Never render a position's fees as a number unless
fees.measurable is true.
position_realized — the one question every real LP actually has
realized_return answers "what does the average LP get in this pool right now."
position_realized answers "I put money into this exact pool on this exact date at this exact
range — what have I actually made since." Anchors to a real calendar date, not a fixed lookback:
{ "poolId": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "entryDate": "2026-07-28", "rangeWidthX": 2 }
Returns fees earned, impermanent loss, realized return, whether price ever left your band (if
so the loss is locked in, not impermanent), and a day-by-day series so you can see exactly
when the position turned. Refuses to answer (measurable: false) rather than guess if fewer
than 2 days have passed since entry — there is nothing to measure yet, and 0% is not the honest
answer to "not enough data."
Same math as everywhere else in this project (lib/concentrated.js's own IL formula, not a
separate implementation) — hand-verified in test/canary.test.js against an independent,
from-scratch calculation, plus a live check against a real 45-day-old position.
Also available over plain HTTP (no MCP client, no API key — we proxy our own Graph key):
curl -s "https://realized.drainfun.xyz/api/position/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-01&range=2"
entry accepts an ISO date or a unix timestamp; range is the band width (omit for ±2×, use a
huge value for full range). Errors are explicit and never fabricated: a future entry, an
unparseable date, an unknown pool, and a window too short to measure each return a stated reason
rather than a number. The same call backs the date picker on the web UI — one implementation of
the math, three transports (MCP, HTTP, browser).
simulate_exit — decision support for closing a position TODAY
Not a new claim: calls the identical position_realized math and reframes it around the
question a holder actually has at the moment of deciding — dollars on a stated stake, not just
percent since entry.
curl -s "https://realized.drainfun.xyz/api/simulate-exit/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640?entry=2026-07-28&range=2&stake=25000"
Gas is real. Live gas price from 1inch × estimated gas units for decreaseLiquidity +
collect (~270k), priced in the chain's native token. The price is measured; the units are
an estimate, so the response carries isEstimate: true and you should report the dollar figure
as approximate. It degrades to { available: false, reason } rather than guessing.
Slippage is zero for a plain close, and that zero is measured, not missing. Closing a v3
position is not a swap — decreaseLiquidity + collect returns both tokens at the current
tick, so there is no price impact. Slippage only exists if the holder then chooses to swap one
side into the other:
curl -s "https://realized.drainfun.xyz/api/simulate-exit/0x88e6...5640?entry=2026-07-28&stake=25000&consolidate=true"
With consolidate=true the estimate is an explicit lower bound: impact is modelled against
total pool TVL, but a concentrated pool's depth at the active tick is thinner than its TVL, so
real impact is likely higher. Say "at least," never "exactly."
netAfterCostsUsd is populated only when both gas and slippage are known, and is null
otherwise — a partial subtraction presented as a complete figure is the same defect this project
exists to expose. If outOfRange is true, say plainly that the loss is locked in regardless of
timing — waiting does not un-realize it.
Reporting rules (these matter more than the numbers)
- Always state the range. The same pool can be honest full-range and misleading at ±1.25×. Full-range is the most generous possible case for the pool — never quote it alone.
measurable: falseis not zero. It means the window couldn't be priced. Say "couldn't measure," never "no loss."- Check the canary before quoting an aggregate.
audit_poolsreturnscanary.passed— stable/stable pairs must show ~0 IL. If it's false the instrument is unproven and every number in that response is untrusted. - Fees are held constant across ranges, which is unfair to this tool: concentrating earns more fees too, so some IL is earned back. The direction is reliable; treat magnitudes as bounded by that caveat.
- This is backward-looking measurement, not a forecast. It says what happened over the window, not what will happen.
What holds this up
110 independent (non-overlapping) 30-day windows across 4 chains, plus the same instrument run unchanged on Aerodrome Slipstream — different team, codebase, incentive model — showing the same defect with the same shape. Venues whose stable pairs can't prove ~0 IL are excluded, not reported as clean. Full tables: https://realized.drainfun.xyz/report.html
Data: The Graph. Per-day feesUSD are indexer-derived aggregates. Not unreachable by RPC —
feeGrowthGlobal0X128 is public pool state readable at any historical block, verified live
2026-09-11 — but reconstructing per-day USD from it needs an archive node, a block lookup per
day boundary, and a historical price for both tokens at each one, repeated per pool. The Graph
publishes that join already computed across 256 pools and 4 chains. The claim is practicality,
not impossibility.
Machine-readable API spec
Full OpenAPI 3.1 description of every endpoint, parameter, response shape and error: https://realized.drainfun.xyz/openapi.json — so an agent never has to guess a request shape.
04 同仓库更多 Skills
recipe-performance
Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold/warm/background-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout/paint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.
mexc-spot-websocket
MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.
mexc-spot-rest
MEXC Spot REST workflows for public data, signed account requests, orders, wallet/admin actions, and secure signing without exposing API keys.
mexc-futures-websocket
MEXC Futures WebSocket workflows for public streams, private account/order/position streams, ping/pong, subscriptions, and depth maintenance.
0xklen-version-and-refresh-sanctions-lists
暂无简介
verify-tests-cover-changed-lines
Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.