Forwarding AddressWhere the pulled liquidity went.

For judges

Everything you need in one page. No setup, no key, no account. Built on the CoinMarketCap DEX API for the Build with CMC: API Hackathon — AI Agents & Automation track.

The claim

Every “LP removed” alert stops at the row that fired it. Forwarding Address follows the wallet across every pool of the token — on this chain and on the asset’s other EVM chains — and rewrites the alert: Rebalance, Migration, Partial, or a real Exit. A plain API call shows the removal; only the maker-keyed cross-pool join and the adjudication turn “liquidity gone” into “liquidity moved to Ring Exchange (Ethereum), 99.8% recovered”.

The 30-second path

The judged capability is one stdlib Python file and the endpoint it calls is keyless:

git clone https://github.com/edycutjong/forwarding.git && cd forwarding
python3 scripts/forwarding.py investigate --platform ethereum --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984

Expected: the trace of every call as it happens (endpoint, status, ms), then a red ● LP REMOVED line, then the verdict — for UNI, as of 2026-09-18 23:36:32 UTC (live_run.json):

◆ MIGRATION · severity amber
    99.8% recovered — $21,287,255 of $21,330,275
    +$21,287,255 into Ring Exchange (Ethereum) · UNI/WETH (ethereum)   2 min 12 s later · pool now holds $102,239,395

Then the agent surface, in one line:

claude mcp add forwarding -- python3 $PWD/scripts/mcp_server.py

and ask Claude Code “where did the UNI liquidity go?” — it calls where_did_liquidity_go and the tool answers with the verdict and the raw rows. Hand it a just-in-time transaction and it refuses. Real session transcript: docs/proof/mcp_session.md.

  1. Nothing else is required. No pip install, no .env, no API key, no signup. One thing that can look like a bug is not one: the endpoint is CoinMarketCap’s shared anonymous tier, rate-limited per IP. A clean run of the pinned path is 27.0 s (p50 of 5 live runs, 11 calls at 2 s spacing; the bare command adds the sweep pages — 14 calls in the run above). If the tier is throttling, the script backs off (15 s, 30 s, 60 s) and says so; if your IP’s quota is exhausted it exits 75 with the way through — wait a minute, or export a free key as CMC_API_KEY (an escape hatch, never a requirement; every receipt here was taken with no key set).
  2. Watch the trace. Every call is printed as it happens — that is the event’s “visible evidence of a real API call” on the judged surface, and the same calls are committed with their verbatim responses in docs/proof/.
  3. Read the verdict. The two rows it was computed from are printed underneath, with the division written out. Change the token, or hand it --txn and --maker, and it follows that wallet instead.

Full annotated transcript with the receipt: DEMO.md.

Receipt — two live runs of the same removal

The bare command picks the largest qualifying removal in the token’s newest 300 rows, so weeks from now it may pick a newer one; --txn 0x5081d9…dcd495 --maker 0x4f0a…b9a9 pins this one (the two flags together skip the sweep and walk the wallet; --maker alone still sweeps). Both runs are committed: live_run.json is the bare command (2026-09-18T23:36:32Z); hero.json is the pinned invocation seed.py made after its watchlist sweep chose this removal (2026-09-18T23:16:31Z) — its selection_rule field records that choosing, its calls are the pinned path (no sweep pages). Same rows, same verdict.

The removal−$21,330,275 out of Ring Exchange (Ethereum) · UNI/WBTC, 2026-09-02 03:59:11 UTC, an unknown share of the pool, maker 0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9
The verdictMIGRATION · 99.8% recovered — +$21,287,255 into Ring Exchange (Ethereum) · UNI/WETH, 2 min 12 s later, the same 1,962,475.5391248302 UNI to ten decimals
Re-derive it21,287,254.93 ÷ 21,330,274.56 = 0.9980 — two tu fields from /v1/dex/liquidity-change/list?maker=
Same wallet, same amounta0 = -1962475.5391248302 on the remove, a0 = 1962475.5391248302 on the add — identical to ten decimals; 1788321683000 − 1788321551000 = 132,000 ms = 2 min 12 s
API callsbare command: 14, all HTTP 200, 50.6 s wall clock (1 backoff) · pinned: 11, all HTTP 200, 73.0 s (2 backoffs)
Credits used0 — every endpoint is on the keyless /public-api surface
Credentialsnone; run with every CMC env var unset
Base rate335 removals ≥ $100,000 followed one wallet at a time: rebalance 194 · migration 19 · partial 10 · exit 112 — 67% were the same wallet putting liquidity back within 6 h
Tests324 offline tests + 10 live · 2,000 property cases, 0 failing
Latencyadjudicate replay p50 0.004 ms (n=1000) · live investigation p50 27.0 s, p95 28.0 s (n=5)
Raw receiptshero.json · exit.json · rebalance.json · jit.json · base_rate.json · live_run.json · bench_live.json · bench_replay.json · spike_maker.json

The other three outcomes, captured by the same published rule: EXIT (LINK, 0% recovered), REBALANCE (LINK, 105% recovered), refused (DAI JIT pair, $229,834).

Reproduce

python3 scripts/forwarding.py investigate --platform ethereum --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984   # the hero, live — the bare command
python3 scripts/forwarding.py investigate --platform ethereum --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --txn 0x5081d91ab6eae50971e0dec5b5f2b03561893a638768ec4773197622dddcd495 --maker 0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9   # the same removal, pinned by txn + wallet: reproduces this card after the sweep has moved on
python3 scripts/forwarding.py investigate --platform ethereum --address 0x514910771af9ca656af840dff83e8264ecf986ca   # LINK
python3 scripts/forwarding.py watch --cycles 1                    # the autonomous loop, one pass over 11 tokens
make test                                                         # 324 offline tests
make test-live                                                    # 10 tests against the real CMC contract
make verify                                                       # replay every receipt, assert I1–I6, check the page
pytest tests/test_property.py --hypothesis-show-statistics        # the 2,000

CI / deterministic replay — this is not the product: make bench replays adjudicate() over the committed receipts. Nothing on the judged path reads them; investigate always runs live.

Why only CoinMarketCap

/v1/dex/liquidity-change/list returns, per liquidity event and with no key, the maker address (m), the side (tp), the USD value (tu), the venue (en/f) and the pair — and it accepts maker= as a server-side filter. That one parameter is the product: the same wallet’s adds in every other pool of the token come back in a single call. Remove CoinMarketCap and reproducing the join needs a multi-chain liquidity-event indexer, a per-DEX pool registry, a cross-chain contract registry and a pool-depth oracle — four systems to replace 6 keyless endpoints.

Honest limitations

Links

Demo videoyoutu.be/ED5h27Xi2V0 — 2 min 54 s, a real keyless run at real speed
DoraHacks BUIDLdorahacks.io/buidl/49020 — the submission page
Run itDEMO.md — annotated transcript + receipt
How it worksARCHITECTURE.md — derived from the code
The ruledocs/SPEC.md — state machine, thresholds, invariants I1–I6
API feedback for CMCFEEDBACK.md — where the API got in the way
The productscripts/forwarding.py · scripts/mcp_server.py
Landing pageforwarding.edycu.dev — the receipt, the flip, and a live box