#!/usr/bin/env python3 """ Seed the kol_wallets table with publicly attributed on-chain addresses. Why this exists: The talks-vs-trades divergence module can only fire on KOLs whose wallets we know. As of 2026-05-24 only 3 KOLs had wallets configured, which is why we had ~3 divergence detections across the whole dataset. Adding a new wallet here REQUIRES: 1. Public attribution — Arkham label, the KOL's own X bio, a public investigation by ZachXBT / Inspex / similar, or the KOL's ENS clearly visible in transactions. 2. The `handle` field MUST exactly match a `handle` value in app/services/kol_substack.py KOL_FEEDS — otherwise the divergence scanner cannot match "post by handle X" against "wallet activity by handle X". 3. The `source_url` must link to that public attestation. Don't add speculative addresses, even if "everyone knows" — misattribution damages both the KOL and our credibility. Idempotent: runs UPSERT-style. Existing rows for (handle, address) are preserved; only new ones are inserted. Usage: DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\ venv/bin/python scripts/seed_kol_wallets.py """ from __future__ import annotations import asyncio import sys from datetime import datetime, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from sqlalchemy import select from app.database import AsyncSessionLocal from app.models import KolWallet # ──────────────────────────────────────────────────────────────────────────── # Verified seed list. # # Each entry MUST have a working source_url. If you can't link to a public # attestation, DON'T add it. # # To find more candidates yourself: # • https://platform.arkhamintelligence.com/ — search by handle, click # "labels" tab. Labels prefixed with "Entity:" are Arkham-verified. # • https://etherscan.io/labelcloud — Etherscan public label registry. # • ZachXBT investigations on X — he posts the underlying tx evidence. # • Many KOLs put their address in their X bio or pinned tweet. # # When the KOL's handle in KOL_FEEDS doesn't match the on-chain handle here, # add it under the handle that's IN KOL_FEEDS — divergence join is by handle. # ──────────────────────────────────────────────────────────────────────────── SEED_WALLETS: list[dict] = [ # ── Already in DB (kept here so the script is self-documenting) ────── { "handle": "cryptohayes", "chain": "ethereum", "address": "0xa86e3d1c80a750a310b484fb9bdc470753a7506f", "label": "Arthur Hayes (main)", "source_url": "https://etherscan.io/address/0xa86e3d1c80a750a310b484fb9bdc470753a7506f", }, { "handle": "cryptohayes", "chain": "ethereum", "address": "0x534a0076fb7c2b1f83fa21497429ad7ad3bd7587", "label": "Arthur Hayes (secondary)", "source_url": "https://etherscan.io/address/0x534a0076fb7c2b1f83fa21497429ad7ad3bd7587", }, # ⚠️ ORPHANED: the two entries below have VERIFIED on-chain attribution # but their `handle` does NOT match any handle in KOL_FEEDS, so the # divergence scanner has no post-side data to join against — they produce # ZERO divergence detections today. They are kept (a) because the wallet # attribution is sound and (b) so that if/when X/Twitter ingestion or a # matching RSS feed is added under these handles, the wallets light up # automatically. The cross-check in main() prints a warning for these. # • andrewkang — publishes on X/@Rewkang only (no Substack) → needs # X ingestion before post-side data exists. # • murad — not currently in KOL_FEEDS at all. { "handle": "andrewkang", "chain": "ethereum", "address": "0xff3879b8a363aed92a6eaba8f61f1a96a9ec3c1e", "label": "Andrew Kang (beanwhale.eth)", "source_url": "https://etherscan.io/address/0xff3879b8a363aed92a6eaba8f61f1a96a9ec3c1e", }, { "handle": "murad", "chain": "ethereum", "address": "0x93f019699ef400df7dc3477dbb6400ed9445a657", "label": "Murad Mahmudov (via ZachXBT investigation)", "source_url": "https://www.blocmates.com/news-posts/24million-in-memecoins-zachxbt-unveils-murad-mahmudov-s-alleged-wallets", }, # ── Add new VERIFIED entries below this line ───────────────────────── # # Template — copy, replace fields, push only after verifying source_url: # # { # "handle": "", # "chain": "ethereum", # or "solana", "base", "arbitrum" # "address": "0x...", # "label": "", # "source_url": "", # }, # # Candidates to research (NOT seeded — verify before adding): # # • niccarter — Nic Carter has spoken openly about his wallet # history on podcasts; check Coin Center filings. # • pomp — Pompliano has a public BTC-only treasury; less # useful for ETH-side divergence detection. # • dragonfly — Dragonfly Capital portfolio wallets often # labeled on Arkham as "Dragonfly Fund". # • placeholder — Placeholder VC fund wallets per their public # investment disclosures. # • eugene — Eugene Ng Ah Sio — verify before adding. # # Also worth tracking even if not in KOL_FEEDS (would need to add the # corresponding feed entry first or they'll never have post-side data): # # • justinsuntron — Tron founder, very active ETH trader, well-labeled. # • cobie — Jordan Fish, address known via Arkham labels. # • gcr / sam — anon traders, no reliably verifiable address. ] async def main() -> int: inserted = 0 skipped = 0 async with AsyncSessionLocal() as session: for entry in SEED_WALLETS: # Idempotency: skip if (handle, address) already present. existing = await session.execute( select(KolWallet).where( KolWallet.handle == entry["handle"], KolWallet.address == entry["address"].lower(), ) ) if existing.scalar_one_or_none(): skipped += 1 continue row = KolWallet( handle=entry["handle"], chain=entry["chain"], address=entry["address"].lower(), label=entry["label"], source_url=entry["source_url"], active=True, added_at=datetime.now(timezone.utc).replace(tzinfo=None), ) session.add(row) inserted += 1 print(f" + {entry['handle']:18s} {entry['address']} ({entry['label']})") await session.commit() print() print(f"Inserted {inserted} wallets, skipped {skipped} existing.") # ── Coverage cross-check ───────────────────────────────────────────── # A wallet is only useful for divergence detection if its handle has a # matching post-side feed in KOL_FEEDS. Flag any orphans loudly, and # report which feeds still have no wallet at all (the real coverage gap). try: from app.services.kol_substack import KOL_FEEDS feed_handles = {f["handle"] for f in KOL_FEEDS} except Exception as exc: # pragma: no cover - diagnostic only print(f"\n(could not import KOL_FEEDS for cross-check: {exc})") return 0 wallet_handles = {e["handle"] for e in SEED_WALLETS} orphaned = sorted(wallet_handles - feed_handles) feeds_without_wallet = sorted(feed_handles - wallet_handles) print() print(f"Coverage: {len(wallet_handles - set(orphaned))}/{len(feed_handles)} " f"KOL_FEEDS handles have ≥1 wallet.") # ── Orphan reconciliation ──────────────────────────────────────────── # An orphaned wallet (handle ∉ KOL_FEEDS) can NEVER produce a divergence # because there's no post-side data to join against — but kol_onchain # still burns a scan cycle (HL clearinghouseState + Etherscan) on it every # run. Park such rows as active=False so the scanner skips them. This is # REVERSIBLE: the row + its verified attribution stay in the DB, and the # moment a matching feed (e.g. X/Twitter ingestion) is added, flip it back. if orphaned: async with AsyncSessionLocal() as session: res = await session.execute( select(KolWallet).where( KolWallet.handle.in_(orphaned), KolWallet.active.is_(True), ) ) rows = res.scalars().all() for row in rows: row.active = False await session.commit() deactivated = len(rows) print() print(f"⚠️ ORPHANED wallets (handle not in KOL_FEEDS → ZERO divergence) " f"— deactivated {deactivated} so the scanner skips them:") for h in orphaned: print(f" {h} — re-activate once a feed under this handle exists.") if feeds_without_wallet: print() print(f"📭 {len(feeds_without_wallet)} feeds have NO wallet (no on-chain " f"divergence possible):") print(" " + ", ".join(feeds_without_wallet)) print() print("Next step: edit SEED_WALLETS above and re-run. Each new wallet") print("MUST cite a public attestation in source_url — see the docstring.") return 0 if __name__ == "__main__": raise SystemExit(asyncio.run(main()))