Architecturederived from the code, not from a design documentevery name on this page is greppable

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.

stdlib Python, zero runtime deps 6 keyless endpoints 0 credits billed 3 surfaces, one engine every response receipted under its sha256
01 · the shape of the thing

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.

Forwarding Address: callers, the engine's eight stages, the CoinMarketCap API, and the receipt path to the site The judge reaches the engine through the CLI in scripts/forwarding.py, the MCP server in scripts/mcp_server.py, or the Vercel proxy in api/lookup.py. Inside scripts/forwarding.py the stages run in order: largest_removals, pools_of, follow_maker (the join), resolve_asset, follow_maker on every sibling chain, adjudicate (no network), pool_liquidity, and the Verdict. All stages but adjudicate issue keyless GETs to pro-api.coinmarketcap.com/public-api. The Verdict is written by receipt() into docs/proof/*.json, which scripts/verify.py replays and scripts/render_site.py turns into the site, the judge page and JUDGE.md. CALLERS the judge a terminal · their own Claude · a browser scripts/forwarding.py CLI · investigate · removals · follow · watch scripts/mcp_server.py stdio JSON-RPC 2.0 · 3 tools api/lookup.py Vercel · same chain · CORS · 60 s cache PROOF PRODUCERS scripts/seed.py base_rate.py · bench.py capture · base rate · p50/p95 scripts/forwarding.py the engine · stdlib Python · keyless · investigate(platform, address) largest_removals() trigger · 300 rows ≥ $100k · JIT and implausible rows out 1 pools_of() identity (f, {t0a, t1a}) · liqUsd · pubAt · share of pool 2 follow_maker() THE JOIN · liquidity-change/list?maker=<m> · to window start 3 resolve_asset() search by address → cid · by symbol → siblings · info → registry 4 follow_maker() × each sibling chain the join again, on every EVM chain with ≥ $10k of liquidity 5 adjudicate() pure arithmetic, no network · REBALANCE · MIGRATION · EXIT … 6 pool_liquidity() pairs/quotes/latest → the destination pool's depth now 7 Verdict kind · severity · recovered_share · evidence[] · follows[] · calls[] 8 GET · no key 6 · no network UPSTREAM pro-api.coinmarketcap.com /public-api six endpoints · no key credit_count reported, 0 billed 429 / 5xx → 15 s, 30 s, 60 s backoff a throttled follow is never an Exit receipt() docs/proof/*.json verbatim responses under their sha256 · hero · exit · rebalance · jit · base_rate · bench · spike_maker · mcp_session · tests scripts/verify.py replay · I1–I6 · chain of custody · page drift scripts/render_site.py slot tokens filled only from receipts · --check is the drift gate site/ · JUDGE.md index.html · judge.html · pitch/ what the judge reads first the judge lands here; the live box calls api/lookup.py
solid · a call into the engine, or a stage handing rows to the next dashed blue · a keyless GET; stages 1–5 and 7 issue one, stage 6 never does amber · the join — the one call that turns an alert into a verdict green · receipt() — every verdict lands on disk with its responses

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.

02 · one investigation, call by call

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.

CallEndpointWhat it is for
0GET /v1/dex/tokencard header: name, symbol, token liquidity — context, never an input to the verdict
1–3GET /v1/dex/liquidity-change/list?minVolume=100000 ×3the 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
4GET /v1/dex/token/pools?size=20pool identity → address, depth now, creation time; the removal's share of its pool
5GET /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
6GET /v1/dex/search?q=<address>the asset's CoinMarketCap id
7GET /v1/dex/search?q=<symbol>the same id on every other chain
8GET /v2/cryptocurrency/info?id=the canonical contract registry the sibling rows are checked against
9–12GET /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 networkadjudicate()no network. Sums and ratios over the rows already fetched; the Verdict dataclass, printed by print_verdict() or serialised by receipt()
13GET /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.

03 · the arithmetic, in full

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)
m
/v1/dex/liquidity-change/list

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.

maker=
same endpoint, query parameter

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.

f · t0a · t1a
same endpoint, on every row

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.

04 · failure handling

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.

ConditionBehaviour
retryHTTP 429 (error 1022 or 1011) · any 5xx · a dropped connectiontransient: 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 triggerThrottled — what happened and the two ways through: wait, or the optional key
incompletethrottled on a followthat 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 hostreturned immediately, never retried, never reported as a rate limit
exit 3no removal ≥ $100k, or every one below 10% of its poolNoCandidate — a finding, with the refused rows and their reasons
exit 3a JIT transaction named by hashNoCandidate — “not an event”
refuseda row with |tu| above $10Ba 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.

05 · repository layout

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)
06 · deliberate non-architecture

What is not here, and why that is the design.

not present

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.

not present

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.

not present

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.

not present

Runtime dependencies

scripts/forwarding.py and scripts/mcp_server.py import only the standard library. pytest, hypothesis, ruff and mypy are dev-only.

not present

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.