""" Real-CATS and MBAL Dataset Providers ===================================== Two massive free datasets for AML detection and address labeling. 1. Real-CATS - 153,121 addresses (50,943 criminal + 102,178 benign) with full transaction profiles. Ideal for risk scoring and AML. Source: https://github.com/sjdseu/Real-CATS 2. MBAL - 10 million annotated crypto addresses across 5 chains with 62 categories. The largest free label dataset available. Source: https://www.kaggle.com/datasets/yidongchaintoolai/mbal-10m-crypto-address-label-dataset NOTE: Requires manual Kaggle download. Place files in ~/rmi/mbal/ """ import csv import logging import os logger = logging.getLogger("databus.dataset_providers") # ═══════════════════════════════════════════════════════════════ # 1. REAL-CATS - Criminal + Benign Address Dataset # ═══════════════════════════════════════════════════════════════ REAL_CATS_PATHS = [ "/tmp/Real-CATS", "/app/Real-CATS", os.path.expanduser("~/rmi/Real-CATS"), os.path.expanduser("~/rmi/datasets/Real-CATS"), ] # File → label mapping for Real-CATS naming convention REAL_CATS_FILE_LABELS = { "CB.tsv": "criminal", # Criminal Bitcoin "CE.tsv": "criminal", # Criminal Ethereum "BB.tsv": "benign", # Benign Bitcoin "BE.tsv": "benign", # Benign Ethereum "Sup-CATS.tsv": "criminal", # Supplementary criminal "TI_B.tsv": "benign", # Transaction Info Benign "TI_M.tsv": "criminal", # Transaction Info Malicious "Identifier.tsv": "mixed", # Identifier mappings } def _find_real_cats_dir() -> str | None: for p in REAL_CATS_PATHS: if os.path.isdir(p) and any(f.endswith(".tsv") for f in os.listdir(p)): return p return None def _load_real_cats() -> dict: """Load Real-CATS dataset into memory.""" base = _find_real_cats_dir() if not base: return { "error": "Real-CATS dataset not found. Clone from https://github.com/sjdseu/Real-CATS" } result = {"criminal": [], "benign": [], "stats": {}} for filename, label in REAL_CATS_FILE_LABELS.items(): filepath = os.path.join(base, filename) if not os.path.exists(filepath): continue is_criminal = label == "criminal" is_benign = label == "benign" try: with open(filepath, encoding="utf-8") as f: reader = csv.DictReader(f, delimiter="\t") # TSV = tab-separated for row in reader: addr = row.get("address", "").strip() if not addr: continue entry = { "address": addr, "label": row.get("label", "criminal" if is_criminal else "benign"), "chain": "bitcoin" if filename.startswith("B") else "ethereum", "source_file": filename, } # Include key features for risk scoring for k in ( "balance", "total_received_BTC", "total_sent_BTC", "total_received_USD", "total_sent_USD", "transaction_number", "first_time", "last_time", "lifetime", ): if k in row: entry[k] = row[k] if is_criminal: result["criminal"].append(entry) elif is_benign: result["benign"].append(entry) else: # Mixed file - use the actual label field if ( "scam" in (row.get("label", "") or "").lower() or "criminal" in (row.get("label", "") or "").lower() ): result["criminal"].append(entry) else: result["benign"].append(entry) except Exception as e: logger.warning(f"Real-CATS: failed to parse {filepath}: {e}") result["stats"] = { "criminal_count": len(result["criminal"]), "benign_count": len(result["benign"]), "total": len(result["criminal"]) + len(result["benign"]), "source": "Real-CATS (GitHub)", "url": "https://github.com/sjdseu/Real-CATS", "paper": "https://arxiv.org/html/2501.15553v1", "files_loaded": sum( 1 for f in REAL_CATS_FILE_LABELS if os.path.exists(os.path.join(base, f)) ), } return result async def fetch_real_cats( address: str | None = None, category: str = "all", limit: int = 50 ) -> dict: """Query Real-CATS - check if address is criminal, or list criminal/benign addresses.""" data = _load_real_cats() if "error" in data: return data if address: addr = address.lower() # Search both categories for entry in data["criminal"] + data["benign"]: if entry["address"].lower() == addr: return { "address": address, "match": entry, "is_criminal": entry["label"] == "criminal", "source": "Real-CATS", } return {"address": address, "match": None, "found": False, "source": "Real-CATS"} if category == "criminal": results = data["criminal"][:limit] elif category == "benign": results = data["benign"][:limit] else: results = data["criminal"][: limit // 2] + data["benign"][: limit // 2] return { "category": category, "results": results, "stats": data["stats"], "source": "Real-CATS", } # ═══════════════════════════════════════════════════════════════ # 2. MBAL - 10 Million Annotated Crypto Addresses # ═══════════════════════════════════════════════════════════════ MBAL_PATHS = [ os.path.expanduser("~/rmi/mbal"), "/app/mbal", os.path.expanduser("~/rmi/datasets/mbal"), "/tmp/mbal", ] MBAL_README = """ MBAL: 10 Million Crypto Address Label Dataset ============================================== Source: https://www.kaggle.com/datasets/yidongchaintoolai/mbal-10m-crypto-address-label-dataset To use: 1. Download from Kaggle (requires free account) 2. Place CSV/parquet files in ~/rmi/mbal/ 3. The provider auto-loads them Chains: Bitcoin, Ethereum, BNB Chain, Polygon, Avalanche Categories: 62 distinct classifications """ def _find_mbal_dir() -> str | None: for p in MBAL_PATHS: if os.path.isdir(p) and os.listdir(p): return p return None def _get_mbal_install_instructions() -> str: return MBAL_README async def fetch_mbal( address: str | None = None, chain: str | None = None, category: str | None = None, limit: int = 20, ) -> dict: """Query MBAL - 10M labeled addresses. Schema: chain,address,categories,entity,source""" base = _find_mbal_dir() if not base: return { "error": "MBAL dataset not installed", "instructions": _get_mbal_install_instructions(), "download_url": "https://www.kaggle.com/datasets/yidongchaintoolai/mbal-10m-crypto-address-label-dataset", "source": "MBAL (Kaggle)", } # Find the main dataset file main_file = None for f in sorted(os.listdir(base)): if f.startswith("dataset_10m") and f.endswith(".csv"): main_file = os.path.join(base, f) break if not main_file: # Fallback: any CSV csv_files = [f for f in os.listdir(base) if f.endswith(".csv")] if csv_files: main_file = os.path.join(base, csv_files[0]) if not main_file: return {"error": "No CSV files found", "path": base, "source": "MBAL"} try: results = [] with open(main_file, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: match = True # Filter by address if address and address.lower() not in row.get("address", "").lower(): match = False # Filter by chain if chain and chain.lower() not in row.get("chain", "").lower(): match = False # Filter by category if category and category.lower() not in row.get("categories", "").lower(): match = False if match: results.append( { "chain": row.get("chain", ""), "address": row.get("address", ""), "categories": row.get("categories", ""), "entity": row.get("entity", ""), "source": row.get("source", ""), } ) if len(results) >= limit: break # Stats (quick estimate from filename) total_estimate = "10,000,023 rows" return { "results": results, "match_count": len(results), "filters": {"address": address, "chain": chain, "category": category}, "source": "MBAL - 10M annotated addresses (Kaggle)", "total_estimate": total_estimate, "categories": "62 distinct: cex, dex, l2, bridge, mixer, scam, gambling, nft, defi, ...", "chains_covered": [ "bitcoin_mainnet", "ethereum_mainnet", "bsc_mainnet", "polygon_mainnet", "avalanche", ], } except Exception as e: logger.warning(f"MBAL query failed: {e}") return {"error": str(e), "source": "MBAL"}