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 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-06-13 13:05:57 -04:00
parent e1ee7d13e7
commit 93b0c8c94e
2 changed files with 124 additions and 8 deletions
+111 -5
View File
@@ -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 hundredsthousands; 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__":