Forwarding Address isone join, three ways to run it.
The join no endpoint performs — the same maker m on a remove row and an add row in different pools — wrapped in an investigation a judge can run as a CLI, as an MCP server inside their own Claude, and as an autonomous watch loop. No database, no cache beyond 60 seconds inside the proxy, no model of our own, no key anywhere.
One engine, three callers, one upstream.
Every caller hands the same scripts/forwarding.py a platform and an address. The engine walks eight stages, six of which hit the keyless CoinMarketCap surface; one is pure arithmetic. Every run ends in a Verdict and a receipt, and the receipts are what the site, the judge guide and the verifier are rendered from.
Boxes are files and functions, not concepts: largest_removals, pools_of, follow_maker, resolve_asset, adjudicate, pool_liquidity, investigate and receipt are module-level functions in scripts/forwarding.py; Verdict is its dataclass.
Every call printed as it happens, every one receipted.
investigate(platform, address) runs the stages below in order. The numbers are the call slots of a full cross-chain run; a token with no sibling chains stops sooner. Each response is stored verbatim under its sha256, so a judge replays the verdict without the network.
| Call | Endpoint | What it is for |
|---|---|---|
| 0 | GET /v1/dex/token | card header: name, symbol, token liquidity — context, never an input to the verdict |
| 1–3 | GET /v1/dex/liquidity-change/list?minVolume=100000 ×3 | the trigger: 300 rows keyed (txn, lgid), cursor from the envelope's data.lastId; a transaction that both adds and removes is JIT and is discarded; a row above SANE_USD is a price artefact and is refused |
| 4 | GET /v1/dex/token/pools?size=20 | pool identity → address, depth now, creation time; the removal's share of its pool |
| 5 | GET /v1/dex/liquidity-change/list?maker=<m> | the join — the same wallet's events across every pool of the token, walked back to the window start, server-side, in one call |
| 6 | GET /v1/dex/search?q=<address> | the asset's CoinMarketCap id |
| 7 | GET /v1/dex/search?q=<symbol> | the same id on every other chain |
| 8 | GET /v2/cryptocurrency/info?id= | the canonical contract registry the sibling rows are checked against |
| 9–12 | GET /v1/dex/liquidity-change/list?platform=<sibling>&maker=<m> | the join again, on each EVM chain where the asset has ≥ $10k of DEX liquidity — for UNI: bsc, arbitrum, unichain, polygon |
| no network | adjudicate() | no network. Sums and ratios over the rows already fetched; the Verdict dataclass, printed by print_verdict() or serialised by receipt() |
| 13 | GET /v4/dex/pairs/quotes/latest?contract_address=<dest> | the destination pool's depth now (and the source pool's, when it is listed) |
Constants are named, not buried: MIN_USD 100,000 · MIN_SHARE 0.10 · W_BACK_H/W_FWD_H 6 h · FULL 0.70 · PARTIAL_MIN 0.10 · TRIGGER_PAGES 3 · SIBLING_MIN_LIQ 10,000 · SANE_USD 1e10 — all at the top of scripts/forwarding.py.
A verdict is a ratio of tu fields. Nothing else.
No model produces a number. Every float in a Verdict is a sum or a ratio of the signed USD values on API rows (invariant I1), and two runs on the same rows are byte-identical (I4). The full statement, thresholds and six invariants are one page: docs/SPEC.md (opens in a new tab).
identity(row) = (row.f, {row.t0a, row.t1a}) # factory + unordered pair
adds = same-maker rows in [ts−6h, ts+6h], tp == "add"
# JIT and implausible rows excluded
same = Σ |tu| of adds, identity == identity(removal), same chain
other = Σ |tu| of every other add # other pool or chain
REBALANCE same/|removed| ≥ 0.70 and same ≥ other
MIGRATION other/|removed| ≥ 0.70
(CONSOLIDATION if destination.pubAt > removal.ts)
PARTIAL 0.10 ≤ (same+other)/|removed|
EXIT otherwise, and every planned follow returned 200
INCOMPLETE otherwise # a throttle is never an Exit (I6)
recovered_share = same/|removed| (REBALANCE)
· other/|removed| (MIGRATION, CONSOLIDATION)
· total (PARTIAL)
elapsed_s = ts(first add into destination) − ts(removal)
The wallet. Verified to be the transaction sender's own address, never a position manager — 32 distinct makers on 71 Uniswap v3 rows (docs/proof/spike_maker.json). Without it there is nothing to join.
The join in one call. The same wallet's adds and removes across every pool of the token, filtered server-side. This is the call no other endpoint replaces.
Pool identity. Rows carry no pool address; the factory plus the unordered pair is the closest stable key, and f is present even when the venue name en is not.
The anonymous tier can never manufacture an Exit.
Transient errors are retried and recorded; everything else is returned at once and named for what it is. A finding with nothing to follow is an exit code, not a stack trace.
| Condition | Behaviour |
|---|---|
| retryHTTP 429 (error 1022 or 1011) · any 5xx · a dropped connection | transient: retried with 15 s, 30 s, 60 s backoff (BACKOFF_S, RETRIES); a call that backed off and then answered records attempts in its receipt |
| exit 75throttled on every retry, on the trigger | Throttled — what happened and the two ways through: wait, or the optional key |
| incompletethrottled on a follow | that follow is complete: false; the verdict is INCOMPLETE if nothing was found, or a MIGRATION / REBALANCE marked incomplete if something was — never an EXIT (invariant I6) |
| at onceany other 4xx · malformed JSON · unreachable host | returned immediately, never retried, never reported as a rate limit |
| exit 3no removal ≥ $100k, or every one below 10% of its pool | NoCandidate — a finding, with the refused rows and their reasons |
| exit 3a JIT transaction named by hash | NoCandidate — “not an event” |
| refuseda row with |tu| above $10B | a price-feed artefact, listed under refused with its reason; the next candidate is taken |
Exit codes are the contract the tests pin: 0 a verdict · 3 a finding with no candidate · 75 throttled (EX_TEMPFAIL) · 2 bad arguments. See tests/test_cli.py.
Two files are the product. The rest proves it.
scripts/forwarding.py and scripts/mcp_server.py import only the standard library, so a judge runs them on a clean machine. Everything else captures, verifies or renders what those two files did.
scripts/
forwarding.py the product: Client, walk, largest_removals, follow_maker, resolve_asset,
pools_of, pool_liquidity, adjudicate, investigate, watch, and the CLI
mcp_server.py stdio JSON-RPC 2.0: where_did_liquidity_go · largest_removals · follow_maker
seed.py the published selection rule → docs/proof/{hero,runner_up,exit,rebalance,jit}.json
base_rate.py every ≥ $100k removal on the watchlist, each wallet followed → base_rate.json
bench.py p50/p95: investigate end to end, follow, adjudicate — live and replay
verify.py replay every receipt, assert I1–I6 and chain of custody, check page drift
render_site.py docs/proof/*.json → site/index.html + site/judge.html + site/pitch/ + JUDGE.md (--check = drift gate)
check_submission_readiness.py placeholders, stale counts, stale numbers → exit 1
spike_maker.py the day-1 spike: is `m` the wallet? (answered: yes)
site_templates/ landing.html, judge.html, pitch.html, JUDGE.md — slot tokens filled only from receipts
api/
lookup.py GET /api/lookup?platform=&address= — same-chain investigate, CORS, 60 s cache
health.py GET /api/health — engine, rule, receipt ages, python version
tests/
conftest.py live-shaped rows and a routed FakeClient whose receipts come from the real _record()
test_adjudicate.py the rule, threshold by threshold; regressions named for the live defect they pin
test_fetch.py keyless default, backoff, the cursor, dedupe, stall, attempts
test_investigate.py every branch: JIT, share, throttle, cross-chain, Solana, registry, watch()
test_property.py hypothesis cases over adjudicate(): I1–I6 never violated
test_cli.py exit codes 0/3/75/2, the receipt file, what is printed
test_mcp.py the protocol over a real pipe; the three tools; the refusal
test_boundary.py least privilege, proven: the agent surface cannot send or be handed a key
test_published_counts.py the counts the documents state are the counts pytest collects
test_live.py against the real API and the deployment (pytest -m live)
docs/
SPEC.md the rule, formally
proof/ hero · runner_up · exit · rebalance · uni_v3_v4 · jit · base_rate · live_run ·
bench_live · bench_replay · spike_maker · seed_sweep · mcp_session.md + .jsonl · tests.json
site/ index.html, judge.html, pitch/, architecture/ (this page), assets/ (icon, fonts under OFL)
What is not here, and why that is the design.
A model of our own
The judge's Claude is the model in the loop; it chooses the token and the removal, and deterministic code decides what is true. An LLM producing the recovered share would fail the track's own gate.
A database
Every verdict is recomputed from live rows; receipts are committed JSON under docs/proof/. The only cache is 60 seconds inside api/lookup.py.
A key
Every endpoint used is on the keyless surface, so the proxy holds no secret and judge traffic bills nothing. CMC_API_KEY is honoured only as an escape hatch for a throttled IP, and a keyed run announces itself on its first line, its last line and in its receipt.
Runtime dependencies
scripts/forwarding.py and scripts/mcp_server.py import only the standard library. pytest, hypothesis, ruff and mypy are dev-only.
A pool address on the row
The API does not carry one; identity is venue + pair, and the limit is stated rather than guessed around.