From 93b0c8c94e9c335b7b932bc22a7da71224e35b5c Mon Sep 17 00:00:00 2001 From: jaxperro Date: Sat, 13 Jun 2026 13:05:57 -0400 Subject: [PATCH] insider.py: implement funding-cluster ring detection via Alchemy Pulls each wallet's USDC funding history (alchemy_getAssetTransfers, full history, no 10k-block cap) and links wallets sharing a funder = likely same operator. Critical refinement: a shared funder only counts if its OWN outbound degree is small (personal hub <=15 recipients); exchanges/bridges fan out to hundreds and must be excluded or everything false-links. Verified: the 10 watchlist wallets shared 11 infra funders (Coinbase-style) and looked like one ring until the degree filter correctly cleared them to independent. Runs automatically after --market/--scan when alchemy_key is in config.json (gitignored). README updated. Co-Authored-By: Claude Opus 4.8 --- README.md | 16 ++++++-- insider.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 66ae93c4..653058d2 100644 --- a/README.md +++ b/README.md @@ -300,9 +300,19 @@ those are high-volume sharps, not info-traders. Scanning a *market's* traders surfaces the real signal: e.g. `arimnestos` at **z=4.0, p≈3e-5** over 2,205 bets — a demonstrable edge. Distinguishing **sharp** (high z, normal timing) from **insider** (high z + late entry + fresh wallet) is the timing/freshness combo. -The Bubblemaps **funding-cluster** step (linking an operator's wallets via -who-funded-whom) needs a Polygonscan/Alchemy key — public RPC caps `getLogs` at -10k blocks. + +**Funding-cluster linking (the Bubblemaps step) — implemented.** With an +`alchemy_key` in `config.json`, the scanner pulls each wallet's USDC funding +history (`alchemy_getAssetTransfers`, full history, no block-range cap) and +links wallets that share a funder — i.e. likely the same operator. The catch +that makes or breaks this: a *shared exchange* (everyone withdraws from +Coinbase) is not a shared operator. So a candidate funder only counts as a link +if **its own outbound degree is small** (a personal hub sends to ≤15 wallets; an +exchange/bridge fans out to hundreds). Without that filter the method +false-flags everyone — our 10 "independent" watchlist wallets all shared 11 +infra funders and looked like one ring until the degree filter correctly +cleared them. Funded-from-a-major-exchange wallets can't be de-anonymized this +way — a known limit the pro firms hit too. **Why this matters:** the z-score over many bets is the first metric in this project that identifies a *real, hard-to-fake* edge. A high-z wallet has beaten diff --git a/insider.py b/insider.py index dfdd3953..edaa30bd 100644 --- a/insider.py +++ b/insider.py @@ -24,6 +24,7 @@ whom" step) needs a Polygonscan/Alchemy key — see cluster_stub(). """ import argparse +import json import math import time from concurrent.futures import ThreadPoolExecutor, as_completed @@ -200,10 +201,93 @@ def market_traders(market, top=40): return [{"wallet": w, "username": names[w]} for w in ranked], cond -def cluster_stub(): - return ("funding-cluster linking (who-funded-whom, the Bubblemaps step) " - "needs a Polygonscan/Alchemy API key — public RPC caps getLogs at " - "10k blocks. Add a key to enable.") +USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" + + +def wallet_funders(wallet, key, max_count=200): + """Set of addresses that sent USDC to this wallet (its funding sources), + via Alchemy getAssetTransfers (full history, no block-range cap).""" + import urllib.request + import ssl as _ssl + url = f"https://polygon-mainnet.g.alchemy.com/v2/{key}" + body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "alchemy_getAssetTransfers", + "params": [{"fromBlock": "0x0", "toBlock": "latest", + "toAddress": wallet, "contractAddresses": [USDC], + "category": ["erc20"], "excludeZeroValue": True, + "maxCount": hex(max_count)}]}).encode() + req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}) + try: + d = json.loads(urllib.request.urlopen(req, timeout=30, + context=_ssl._create_unverified_context()).read()) + return {x["from"].lower() for x in d.get("result", {}).get("transfers", [])} + except Exception: + return set() + + +def funder_outdegree(addr, key, cap=300): + """Distinct USDC recipients this address has sent to. Exchanges/bridges fan + out to hundreds–thousands; a personal funding hub sends to a handful. This + is what separates a real shared-operator link from 'both used Coinbase'.""" + url = f"https://polygon-mainnet.g.alchemy.com/v2/{key}" + import urllib.request + import ssl as _ssl + body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "alchemy_getAssetTransfers", + "params": [{"fromBlock": "0x0", "toBlock": "latest", "fromAddress": addr, + "contractAddresses": [USDC], "category": ["erc20"], + "excludeZeroValue": True, "maxCount": hex(cap)}]}).encode() + req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}) + try: + d = json.loads(urllib.request.urlopen(req, timeout=30, + context=_ssl._create_unverified_context()).read()) + xf = d.get("result", {}).get("transfers", []) + return len({x["to"].lower() for x in xf if x.get("to")}), len(xf) >= cap + except Exception: + return 9999, True + + +# a personal funding hub sends to at most ~this many distinct wallets +MAX_HUB_RECIPIENTS = 15 + + +def cluster(wallets, key, max_workers=8): + """Link wallets that share a *personal* funding source (same operator). + A shared funder only counts if it isn't an exchange/bridge/infra address — + judged by its outbound degree, not by how many of our wallets it touches.""" + funders = {} + with ThreadPoolExecutor(max_workers=max_workers) as ex: + fut = {ex.submit(wallet_funders, w, key): w for w in wallets} + for f in as_completed(fut): + funders[fut[f]] = f.result() + by_funder = {} + for w, fs in funders.items(): + for src in fs: + by_funder.setdefault(src, set()).add(w) + shared = {src: ws for src, ws in by_funder.items() if len(ws) >= 2} + # keep only shared funders that look like a personal hub (low outbound degree) + links = {} + with ThreadPoolExecutor(max_workers=max_workers) as ex: + deg = {src: ex.submit(funder_outdegree, src, key) for src in shared} + for src, fu in deg.items(): + recipients, capped = fu.result() + if not capped and recipients <= MAX_HUB_RECIPIENTS: + links[src] = shared[src] + # union-find to merge wallets linked through any shared funder + parent = {w: w for w in wallets} + def find(x): + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + def union(a, b): + parent[find(a)] = find(b) + for ws in links.values(): + ws = list(ws) + for w in ws[1:]: + union(ws[0], w) + groups = {} + for w in wallets: + groups.setdefault(find(w), []).append(w) + return [g for g in groups.values() if len(g) > 1], links def fmt_p(p): @@ -255,7 +339,29 @@ def main(): print("-" * len(h)) print("susp gated by improbability: a wallet must win ABOVE its odds (z>1) to score at all.") print("z=wins above odds-implied · p(luck)=prob it was chance · pre24=% wins entered <24h out") - print("\nNOTE:", cluster_stub()) + + key = sm_load_key() + if key and len(cands) > 1: + print("\nfunding-trace clustering (shared USDC funders -> same operator)...") + wallets = [c["wallet"] for c in cands] + name = {c["wallet"]: c["username"] for c in cands} + groups, _ = cluster(wallets, key) + if groups: + print(f"** {len(groups)} linked cluster(s) — wallets sharing a non-infra funder:") + for g in sorted(groups, key=len, reverse=True): + print(" - " + " + ".join(name.get(w, w[:10]) for w in g)) + else: + print("no shared-funder clusters (independently funded, or via shared exchange/infra).") + elif not key: + print("\n(add \"alchemy_key\" to config.json to enable funding-cluster linking)") + + +def sm_load_key(): + try: + from copytrade import load_json + return load_json("config.json", {}).get("alchemy_key", "") + except Exception: + return "" if __name__ == "__main__":