next · esc overview · p notes
Forwarding Address v1.0.0

Where the pulled
liquidity went.

Every “LP removed” alert stops at the row that fired it. This one follows the wallet.

Build with CMC · API Hackathon Built on the CoinMarketCap DEX API AI Agents & Automation keyless · 0 credits · MIT
Forwarding Address — the trail leaves the red pool and lands in the amber one
UNI on Ethereum · Ring Exchange (Ethereum) · UNI/WBTC · 2026-09-02 03:59:11 UTC
−$21,330,275
LP removed — the only line an alert bot prints

2 min 12 s later the same wallet put 99.8% of it — the same 1,962,475.54 UNI — into the pool next door.

The problem

True alert.
Wrong headline.

Marco · runs a DAO treasury's Telegram alert bot

Every “LP removed ≥ $100k” ping is a decision: unwind, hedge, or ignore. He gets several a week and cannot tell them apart from the alert — the data that would is in the same API, one query away, and nobody joins it.

67%
of 335 removals ≥ $100,000 were the same wallet putting it back within 6 h — 11 tokens, 3 chains, every wallet followed, 352 keyless calls
rebalance 58% (194) migration 6% (19) partial 3% (10) exit 33% (112)
The solution

Follow the wallet, not the row.

LP removed · severity red
−$21,330,275
Ring Exchange (Ethereum) · UNI/WBTC · 2026-09-02 03:59:11 UTC
maker 0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9
txn 0x5081d9…dcd495 · one row · where every alert stops
liquidity-change/list
?maker=
MIGRATION · severity amber
99.8%recovered
+$21,287,255 into Ring Exchange (Ethereum) · UNI/WETH · 2 min 12 s later
maker 0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9 — the same wallet
txn 0x9fafe3…71a0f3 · pool now holds $102.24M
the agent rewrites its own severity →REBALANCE · same poolMIGRATION · other poolPARTIAL · 10–70%EXIT · nothing, every follow 200refused · JIT pair
How it works · stdlib Python · no key anywhere

Three surfaces. One engine. Six endpoints.

SURFACES forwarding.pyCLI · investigate · follow · watch mcp_server.py3 tools over stdio · Claude Code api/lookup.pyVercel · same-chain · CORS · 60 s ENGINE · scripts/forwarding.py 1 trigger300 rows ≥ $100k · JIT refused 2 poolsidentity · depth · creation time 3 the join · liquidity-change/list?maker=<wallet>one call: this wallet's adds and removes in every pool of the token 4 cross-chainsearch → id · info · follow each 5 adjudicate5 classes · INCOMPLETE ≠ Exit 6 confirm · rewrite severityquotes/latest → destination depth now · red → amber → grey COINMARKETCAP · keyless /public-api · 0 credits GET /v1/dex/liquidity-change/listthe trigger, and with maker= the join GET /v1/dex/token/poolsevery pool of the token · depth · pubAt GET /v1/dex/searchaddress → id → the same asset on other chains GET /v2/cryptocurrency/infothe cross-chain contract registry GET /v4/dex/pairs/quotes/latestthe destination pool's depth now GET /v1/dex/token GET · no key verdict + receiptevery call · status · ms · every response under its sha256 · docs/proof/ render_site.py/ · /judge · /pitch · JUDGE.md verify.py · I1–I6every receipt replayed · evidence rows found byte-for-byte
Live · keyless · captured 2026-09-18 23:36:32 UTC · demo video on the BUIDL page
14 calls · 14 × 200 · 0 credits · 50.6 s · every CMC env var unset
$ python3 scripts/forwarding.py investigate --platform ethereum --address 0x1f9840…01f984
forwarding address — keyless · ethereum · 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984
  following the wallet
    GET /v1/dex/token                 address=0x1f98…f984 platform=ethereum               200   487 ms
    GET /v1/dex/liquidity-change/list address=0x1f98…f984 minVolume=100000 platform=eth…  200  1004 ms
    GET /v1/dex/liquidity-change/list address=0x1f98…f984 lastId=AVd6RT…PQ== minVolume=…  200   641 ms
    GET /v1/dex/liquidity-change/list address=0x1f98…f984 lastId=AVd6RT…PQ== minVolume=…  200   573 ms
    GET /v1/dex/token/pools           address=0x1f98…f984 platform=ethereum size=20       200   516 ms
    GET /v1/dex/liquidity-change/list address=0x1f98…f984 maker=0x4f0a…b9a9 platform=et…  200   806 ms
    GET /v1/dex/search                q=0x1f98…f984                                       200   466 ms
    GET /v1/dex/search                q=UNI                                               200  1180 ms
    GET /v2/cryptocurrency/info       id=7083                                             200   297 ms
    GET /v1/dex/liquidity-change/list address=0xbf51…e9b1 maker=0x4f0a…b9a9 platform=bsc  200   522 ms
    GET /v1/dex/liquidity-change/list address=0xfa7f…f7f0 maker=0x4f0a…b9a9 platform=ar…  200   501 ms
    GET /v1/dex/liquidity-change/list address=0x8f18…ea21 maker=0x4f0a…b9a9 platform=un…  200   541 ms
    GET /v1/dex/liquidity-change/list address=0xb33e…180f maker=0x4f0a…b9a9 platform=po…  200   955 ms
    GET /v4/dex/pairs/quotes/latest   contract_address=0x8626…7306 network_slug=ethereum  200   647 ms
  ● LP REMOVED  −$21,330,275   Ring Exchange (Ethereum) · UNI/WBTC
    2026-09-02 03:59:11 UTC
    maker 0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9
  ◆ MIGRATION · severity amber
    99.8% recovered — $21,287,255 of $21,330,275
    +$21,287,255 into Ring Exchange (Ethereum) · UNI/WETH
    2 min 12 s later · pool now holds $102,239,395
    21,287,254.93 ÷ 21,330,274.56 = 0.9980
  14 calls · 14 × 200 · 0 credits · 50.6 s · keyless
1 4
forwarding-cmc.vercel.app
The deployed landing page: the red LP REMOVED card for −$21,330,275 with the maker address and the agent's trace of every call beneath it
2 3
forwarding-cmc.vercel.app · the two rows, verbatim
The two liquidity-change rows side by side, the remove and the add, the same maker in both, with the division written underneath
1the alert as fired 2the remove row 3the add row, same maker 4the trace: sha256 per call
Check it by hand · both rows from one call

Two fields. One division.

remove · Ring Exchange (Ethereum) · txn 0x5081d9…dcd495
{
  "ts":  "1788321551000",
  "tp":  "remove",
  "tu":  -21330274.564875204,
  "m":   "0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9",
  "t0s": "UNI", "t1s": "WBTC",
  "a0":  -1962475.5391248302
}
add · Ring Exchange (Ethereum) · txn 0x9fafe3…71a0f3
{
  "ts":  "1788321683000",
  "tp":  "add",
  "tu":  21287254.934237212,
  "m":   "0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9",
  "t0s": "UNI", "t1s": "WETH",
  "a0":  1962475.5391248302
}
recovered share = 21,287,254.93 ÷ 21,330,274.56 = 0.9980
elapsed = 1788321683000 − 1788321551000 = 132,000 ms = 2 min 12 s GET /v1/dex/liquidity-change/list?platform=ethereum&address=0x1f98…f984&maker=0x4f0a…b9a9 — the full response is in live_run.json under the sha256 the trace cites; verify.py checks both rows appear in it byte-for-byte.
The API · 6 endpoints · keyless · 0 credits

One parameter is the product.

endpointwhat the agent takes from it
/v1/dex/liquidity-change/listthe trigger, and with maker= the join: one wallet's adds and removes across every pool of the token, in one call
/v1/dex/token/poolsevery pool the wallet could have moved to — address, depth now, creation time
/v1/dex/searchaddress → CoinMarketCap id, then the same id on every other chain
/v2/cryptocurrency/infothe canonical cross-chain contract registry the sibling rows are checked against
/v4/dex/pairs/quotes/latestthe destination pool's depth now — the money actually arrived
/v1/dex/tokenthe card header: name, symbol, token liquidity
the 25-point criterion: “does the agent do something a plain API call could not”

A plain call stops at the removal.

A plain call shows the removal. The maker join, the five-way adjudication and the rewritten severity are what no endpoint returns — and no model of ours produces a number.

Remove CoinMarketCap

and the join needs four systems — a multi-chain liquidity-event indexer, a per-DEX pool registry, a cross-chain contract registry, a pool-depth oracle — to replace six free endpoints.

It runs · the agent surface · a real Claude Code session, committed
docs/proof/mcp_session.md · 5 turns · 72 s · every figure from a tool result
$ claude mcp add forwarding -- python3 $PWD/scripts/mcp_server.py

tools exposed: largest_removals · where_did_liquidity_go · follow_maker

 where did the UNI liquidity go?
→ where_did_liquidity_go {platform: ethereum, address: 0x1f9840…}
MIGRATION · 99.8% recovered — $21,287,255 of $21,330,275
into Ring Exchange (Ethereum) · UNI/WETH 2 min 12 s later

 and DAI, txn 0x3f8c69…f2fd84?
→ where_did_liquidity_go {platform: ethereum, address: 0x6b1754…,
                          txn: 0x3f8c69…f2fd84}
REFUSED — txn 0x3f8c69…f2fd84 is not an event — it adds and removes
in the same transaction (JIT)

the model never produced a number of its own
27.0 s
live investigation p50 · n=5 · p95 28.0 s
0
credits · no API key · every CMC env var unset
126 + 10
offline tests · live against the real contract
2,000
property cases over adjudicate() · 0 failing
0.004 ms
adjudicate replay p50 · byte-identical 1000/1000
CI follows
the wallet
every push · live-api job · no secret configured
Honest limits · eight in the README, four here

What this is not.

Pool identity is venue + pair

Rows carry no pool contract, so two fee tiers of one pair collapse into one identity — and the hero's share of its pool is unknown, printed as unknown, never as a number.

A window, not a day

±6 h, walked with the cursor because startTime is plan-gated keyless. A wallet that comes back tomorrow reads as an Exit for that window, and the card says which window it searched.

A wallet is not an entity

Liquidity moved through a second wallet is not followed. Guessing one would put a number on the card that no row supports — deliberately not built.

One IP, one anonymous quota

The tier throttles per IP; the tool backs off 15 / 30 / 60 s and records every retry. A throttled follow is INCOMPLETE, never an Exit. The base rate follows same-chain only, so its Exit count is an upper bound.

The ask

Run it before
you score it.

thirty seconds from a fresh clone · no key · no install
$ git clone https://github.com/edycutjong/forwarding.git
$ cd forwarding
$ python3 scripts/forwarding.py investigate \
    --platform ethereum \
    --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984

The alert was true.
The panic was not.

Forwarding Addressv1.0.0captured 2026-09-18 23:36:32 UTC
01 / 11
speaker notes