""" Auto-Labeling RAG System — Behavioral wallet labeling. ======================================================== Watches for on-chain patterns and automatically labels wallets over time. Uses FAISS similarity search against known labeled wallets. When a wallet matches known scam/deployer/actor patterns, it gets auto-labeled. Label categories: - repeat_deployer: Created 3+ tokens that rugged - funding_funnel: Received funds from known scam wallets - wash_trader: Circular transaction patterns - sniper_bot: Consistent sub-block-10 entries - sandwich_bot: MEV sandwich attack patterns - dust_attacker: Dust-level transfers to many addresses - honeypot_deployer: Deployed contracts with transfer restrictions - drainer_wallet: Receives from known phishing victims - cex_deposit_launderer: Moves through CEX to obfuscate - mixer_user: Interacts with sanctioned mixers - pig_butchering: Slow buildup then sudden drain pattern """ import logging import os import time from collections import Counter, defaultdict from datetime import datetime logger = logging.getLogger(__name__) # ── Label Definitions ───────────────────────────────────────── AUTO_LABELS = { "repeat_deployer_3": { "name": "Serial Deployer (3+)", "description": "Deployed 3+ tokens that later rugged or were abandoned", "entity_type": "scam_deployer", "risk_score": 85, "confidence_threshold": 0.7, "icon": "🏭", }, "repeat_deployer_5": { "name": "Rug Pull Factory (5+)", "description": "Deployed 5+ rug pull tokens — professional scam operation", "entity_type": "scam_operation", "risk_score": 95, "confidence_threshold": 0.8, "icon": "☠️", }, "funding_funnel": { "name": "Funding Funnel", "description": "Received funds from 3+ known scam wallets — likely launderer", "entity_type": "money_launderer", "risk_score": 80, "confidence_threshold": 0.6, "icon": "💰", }, "sniper_bot": { "name": "Sniper Bot", "description": "Consistently buys tokens in first 10 blocks of launch", "entity_type": "trading_bot", "risk_score": 30, "confidence_threshold": 0.8, "icon": "🎯", }, "sandwich_bot": { "name": "Sandwich Bot", "description": "Detected sandwich attack patterns — front-running trades", "entity_type": "mev_bot", "risk_score": 60, "confidence_threshold": 0.7, "icon": "🥪", }, "mixer_user": { "name": "Mixer User", "description": "Interacts with Tornado Cash or other sanctioned mixers", "entity_type": "mixer_user", "risk_score": 75, "confidence_threshold": 0.6, "icon": "🌪️", }, "drainer_wallet": { "name": "Wallet Drainer", "description": "Receives funds from known phishing/exploit victim wallets", "entity_type": "drainer", "risk_score": 90, "confidence_threshold": 0.7, "icon": "🪝", }, "honeypot_deployer": { "name": "Honeypot Deployer", "description": "Deployed contracts with sell restrictions or transfer blocks", "entity_type": "scam_deployer", "risk_score": 88, "confidence_threshold": 0.7, "icon": "🍯", }, "wash_trader": { "name": "Wash Trader", "description": "Circular transaction patterns — trading with self/controlled wallets", "entity_type": "wash_trader", "risk_score": 70, "confidence_threshold": 0.65, "icon": "🔄", }, "dust_attacker": { "name": "Dust Attacker", "description": "Sends dust amounts to 100+ addresses — phishing or tracking attempt", "entity_type": "dust_attacker", "risk_score": 45, "confidence_threshold": 0.8, "icon": "💨", }, "pig_butchering": { "name": "Pig Butchering Operator", "description": "Gradual fund accumulation then sudden drain to exchange — scam pattern", "entity_type": "scam_operation", "risk_score": 92, "confidence_threshold": 0.7, "icon": "🐷", }, "cex_launderer": { "name": "CEX Launderer", "description": "Routes through multiple CEX deposit addresses to break trace", "entity_type": "money_launderer", "risk_score": 78, "confidence_threshold": 0.65, "icon": "🏦", }, "sleeping_agent": { "name": "Sleeping Agent", "description": "Wallet dormant 90+ days then suddenly active — potential sleeper", "entity_type": "suspicious", "risk_score": 55, "confidence_threshold": 0.6, "icon": "😴", }, "flash_loan_attacker": { "name": "Flash Loan Attacker", "description": "Used flash loans for rapid price manipulation or exploits", "entity_type": "exploiter", "risk_score": 85, "confidence_threshold": 0.7, "icon": "⚡", }, } class AutoLabeler: """Auto-labeling engine that watches wallets and assigns labels based on behavior.""" def __init__(self): self.labels_applied = Counter() self.pending_observations = defaultdict(list) self.last_run = None self._known_scam_wallets = set() self._known_mixers = set() self._known_exchanges = set() self._initialize_known_sets() def _initialize_known_sets(self): """Load known scam wallets and mixers from our label database.""" clean_dir = os.path.join(os.environ.get("RMI_DATA_DIR", "/app/data"), "wallet-labels-clean") for chain in ["ethereum", "solana"]: path = os.path.join(clean_dir, f"wallet_labels_{chain}.csv") if not os.path.exists(path): continue import csv with open(path) as f: for row in csv.DictReader(f): addr = row["address"].lower() etype = row.get("entity_type", "") if etype in ( "malicious", "phishing_scam", "scam", "exploiter", "drainer", "nation_state_actor", "scam_operation", "scam_deployer", "money_launderer", ): self._known_scam_wallets.add(addr) if etype == "mixer" or "tornado" in row.get("name", "").lower(): self._known_mixers.add(addr) if etype == "exchange" or etype == "exchange_deposit": self._known_exchanges.add(addr) logger.info( f"AutoLabeler initialized: {len(self._known_scam_wallets):,} known scams, " f"{len(self._known_mixers):,} mixers, {len(self._known_exchanges):,} exchanges" ) async def observe_wallet(self, address: str, chain: str, observations: dict) -> list[dict]: """Record observations about a wallet and check if any labels should be applied.""" key = f"{chain}:{address.lower()}" self.pending_observations[key].append( { "timestamp": time.time(), **observations, } ) # Check label rules — return only NEW labels not already applied existing_label_keys = {line_list["label_key"] for line_list in self.pending_observations.get(f"_labels_{key}", [])} new_labels = await self._check_labels(address, chain, self.pending_observations[key]) unique_new = [line_list for line_list in new_labels if line_list["label_key"] not in existing_label_keys] # Track applied labels to prevent duplicates if f"_labels_{key}" not in self.pending_observations: self.pending_observations[f"_labels_{key}"] = [] self.pending_observations[f"_labels_{key}"].extend(unique_new) for label in unique_new: self.labels_applied[label["label_key"]] += 1 return unique_new async def _check_labels(self, address: str, chain: str, history: list[dict]) -> list[dict]: """Check all label rules against wallet history.""" applied = [] deploy_count = sum(1 for o in history if o.get("event") == "token_deployed") if deploy_count >= 5: applied.append(self._create_label("repeat_deployer_5", address, chain, {"deployments": deploy_count})) elif deploy_count >= 3: applied.append(self._create_label("repeat_deployer_3", address, chain, {"deployments": deploy_count})) # Check funding sources funders = set() for o in history: if o.get("event") == "received_funds" and o.get("from_address"): funders.add(o["from_address"].lower()) scam_funders = funders & self._known_scam_wallets if len(scam_funders) >= 3: applied.append( self._create_label("funding_funnel", address, chain, {"scam_funders": list(scam_funders)[:5]}) ) # Check mixer interaction mixer_interactions = sum( 1 for o in history if o.get("counterparty", "").lower() in self._known_mixers or "tornado" in o.get("protocol", "").lower() ) if mixer_interactions >= 1: applied.append(self._create_label("mixer_user", address, chain, {"mixer_interactions": mixer_interactions})) # Check CEX laundering pattern cex_deposits = set() for o in history: if o.get("event") == "deposited_to_exchange" and o.get("exchange"): cex_deposits.add(o["exchange"]) if len(cex_deposits) >= 3 and len(scam_funders) >= 1: applied.append(self._create_label("cex_launderer", address, chain, {"exchanges_used": list(cex_deposits)})) # Check draining pattern victim_funds = sum( 1 for o in history if o.get("counterparty", "").lower() in self._known_scam_wallets and o.get("event") == "received_funds" ) if victim_funds >= 5: applied.append(self._create_label("drainer_wallet", address, chain, {"victim_count": victim_funds})) # Check sleeping agent if len(history) >= 2: timestamps = sorted(o.get("timestamp", 0) for o in history) gap = timestamps[-1] - timestamps[0] if gap > 90 * 86400: # 90+ days dormancy applied.append(self._create_label("sleeping_agent", address, chain, {"dormant_days": int(gap / 86400)})) # Track applied labels for label in applied: self.labels_applied[label["label_key"]] += 1 return applied def _create_label(self, label_key: str, address: str, chain: str, evidence: dict) -> dict: """Create a label entry.""" config = AUTO_LABELS[label_key] return { "address": address, "chain": chain, "label_key": label_key, "name": config["name"], "description": config["description"], "entity_type": config["entity_type"], "risk_score": config["risk_score"], "icon": config["icon"], "source": "auto_labeler", "applied_at": datetime.now().isoformat(), "evidence": evidence, } async def batch_analyze(self, observations: list[dict]) -> dict: """Batch analyze multiple wallet observations and return all labels.""" results = {"labels_applied": [], "stats": {}} for obs in observations: addr = obs.get("address", "") chain = obs.get("chain", "ethereum") if addr: labels = await self.observe_wallet(addr, chain, obs.get("events", [])) results["labels_applied"].extend(labels) results["stats"] = { "total_wallets_analyzed": len(observations), "labels_applied": len(results["labels_applied"]), "label_counts": dict(self.labels_applied.most_common()), "known_scam_wallets": len(self._known_scam_wallets), "known_mixers": len(self._known_mixers), } return results def get_stats(self) -> dict: """Get auto-labeler statistics.""" return { "labels_applied_total": sum(self.labels_applied.values()), "label_counts": dict(self.labels_applied.most_common()), "known_scam_wallets": len(self._known_scam_wallets), "known_mixers": len(self._known_mixers), "known_exchanges": len(self._known_exchanges), "pending_observations": sum(len(v) for v in self.pending_observations.values()), "last_run": self.last_run, } # Singleton _auto_labeler: AutoLabeler | None = None def get_auto_labeler() -> AutoLabeler: global _auto_labeler if _auto_labeler is None: _auto_labeler = AutoLabeler() return _auto_labeler