mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
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:
+111
-5
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user