rugmuncher-backend / backend /app /arkham_entity.py
RMI Platform
nightly-build: arkham_entity β€” multi-source entity resolver with confidence scoring
9725810
Raw
History Blame Contribute Delete
27.5 kB
"""
Arkham Entity Resolver
======================
Map any blockchain address to its real-world owner with multi-source confidence scoring.
Entity types detected:
1. CEX/DEX Wallets β€” Binance, Coinbase, Kraken, OKX, Uniswap, SushiSwap, Curve
2. DeFi Protocols β€” Aave, Compound, MakerDAO, Lido, EigenLayer, Ethena
3. Institutional Funds β€” Grayscale, Pantera, a16z, Multicoin, Paradigm
4. Smart Money / Whales β€” High-value wallets with repeat profitable trades
5. Bridge / Cross-chain β€” LayerZero, Wormhole, Stargate, Across
6. Scam / Sanctioned β€” Lazarus, Ronin exploiter, known bad actors
7. NFT / Gaming β€” BAYC, Opensea, Blur, Magic Eden wallets
8. Unknown / Uncategorized β€” Addresses with no known entity label
Competitive advantage:
- Arkham Intelligence paid ($299/mo+) vs our bundled x402 access ($0.10/tool)
- Chainalysis/TRM Labs require enterprise contracts (50K+/yr)
- Our multi-source hybrid approach (Arkham API + local entity DB + heuristic scoring)
means we still return useful results when the API is down
- Confidence scoring (0-100) lets users calibrate trust per use case
- Integrates with existing RMI tooling (wallet_graph, entity_clustering, reputation_score)
Usage:
from app.arkham_entity import ArkhamEntityResolver
resolver = ArkhamEntityResolver()
result = await resolver.resolve("0xBE0eB53FC46b790099138e3d32C721856d41e865")
print(f"Entity: {result.entity_name}")
print(f"Confidence: {result.confidence}/100")
print(f"Category: {result.category}")
for label in result.labels:
print(f" [{label.source}] {label.label}")
CLI:
python3 -m app.arkham_entity 0xBE0eB53FC46b790099138e3d32C721856d41e865
"""
import asyncio
import logging
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from enum import Enum
from typing import Any
try:
from app.arkham_connector import ArkhamClient, ARKHAM_API_KEY
except ImportError:
ArkhamClient = None # type: ignore
ARKHAM_API_KEY = ""
try:
from app.entity_registry import KNOWN_CEX_WALLETS
except ImportError:
KNOWN_CEX_WALLETS = {}
try:
from app.entity_labeler import EXCHANGES, PROTOCOLS
except ImportError:
EXCHANGES = {}
PROTOCOLS = {}
logger = logging.getLogger(__name__)
# ── Constants ─────────────────────────────────────────────────────────────────
ETH_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
SOL_ADDRESS_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
# Known entity database β€” manual curation supplementing Arkham data
KNOWN_ENTITIES: dict[str, dict[str, Any]] = {
# Exchanges
"0xBE0eB53FC46b790099138e3d32C721856d41e865": {
"name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 7",
"tags": "cex,hot_wallet,high_volume",
},
"0xF977814e90dA44bFA03b6295A0616a897441aceC": {
"name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 8",
"tags": "cex,hot_wallet,high_volume",
},
"0x28C6c06298d514Db089934071355E5743bf21d60": {
"name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 14",
"tags": "cex,hot_wallet,high_volume",
},
"0x503828976D22510aad0201ac7EC88293211D23Da": {
"name": "Coinbase", "category": "exchange", "label": "Coinbase Hot Wallet 1",
"tags": "cex,hot_wallet",
},
"0xddfAbCdc4D8fFC17086Ea2cBcebe71504184443C": {
"name": "Coinbase", "category": "exchange", "label": "Coinbase Hot Wallet 2",
"tags": "cex,hot_wallet",
},
"0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0": {
"name": "Kraken", "category": "exchange", "label": "Kraken Hot Wallet",
"tags": "cex,hot_wallet",
},
"0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b": {
"name": "OKX", "category": "exchange", "label": "OKX Hot Wallet",
"tags": "cex,hot_wallet",
},
# DeFi Protocols
"0x7a250d5630b4cf139281983dce37532e7d5c9196": {
"name": "Uniswap V2 Router", "category": "defi", "label": "Uniswap V2 Router",
"tags": "dex,router,swap",
},
"0x7d2768de32b0b8013e039f31fbacf67128c9c3d8": {
"name": "Aave V2 Lending Pool", "category": "defi", "label": "Aave V2 Lending Pool",
"tags": "lending,liquidity,pools",
},
"0xdAC17F958D2ee523a2206206994597C13D831ec7": {
"name": "Tether (USDT)", "category": "token", "label": "Tether USDT Contract",
"tags": "stablecoin,erc20,high_volume",
},
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": {
"name": "Circle (USDC)", "category": "token", "label": "USD Coin Contract",
"tags": "stablecoin,erc20,high_volume",
},
# Known bad actors / scams
"0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec": {
"name": "Lazarus Group", "category": "scam", "label": "Lazarus Group (Sanctioned)",
"tags": "sanctioned,north_korea,exploiter",
},
"0x098B716B8Aaf21512996dC57EB0615e2383E2f96": {
"name": "Ronin Bridge Exploiter", "category": "scam", "label": "Ronin Bridge Exploiter",
"tags": "exploiter,hack,6.2m_eth",
},
}
# Normalize known entity addresses to lowercase
KNOWN_ENTITIES = {k.lower(): v for k, v in KNOWN_ENTITIES.items()}
# Entity category weights for confidence scoring
CATEGORY_WEIGHTS: dict[str, float] = {
"exchange": 0.95,
"defi": 0.90,
"token": 0.90,
"scam": 0.85,
"bridge": 0.85,
"fund": 0.80,
"nft": 0.70,
"whale": 0.60,
"unknown": 0.20,
}
# ── Enums ─────────────────────────────────────────────────────────────────────
class EntityCategory(str, Enum):
"""Top-level entity classification."""
EXCHANGE = "exchange"
DEFI = "defi"
TOKEN = "token"
BRIDGE = "bridge"
FUND = "fund"
WHALE = "whale"
NFT = "nft"
GAMING = "gaming"
SCAM = "scam"
SANCTIONED = "sanctioned"
UNKNOWN = "unknown"
class ResolverSource(str, Enum):
"""Source that provided the entity resolution."""
ARKHAM_API = "arkham_api"
LOCAL_DB = "local_db"
ENTITY_REGISTRY = "entity_registry"
ENTITY_LABELER = "entity_labeler"
HEURISTIC = "heuristic"
# ── Dataclasses ───────────────────────────────────────────────────────────────
@dataclass
class EntityLabel:
"""A single label for an address from a specific source."""
label: str
source: ResolverSource
confidence: float # 0.0 to 1.0
category: str = "unknown"
@dataclass
class ResolvedEntity:
"""Complete entity resolution result for a single address."""
address: str
chain: str
entity_name: str
category: EntityCategory
confidence: float # 0-100
labels: list[EntityLabel] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
related_addresses: list[str] = field(default_factory=list)
source: ResolverSource = ResolverSource.HEURISTIC
arkham_data: dict[str, Any] | None = None
resolved_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
raw_signals: list[dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"address": self.address,
"chain": self.chain,
"entity_name": self.entity_name,
"category": self.category.value if isinstance(self.category, EntityCategory) else self.category,
"confidence": self.confidence,
"labels": [asdict(l) for l in self.labels],
"tags": self.tags,
"source": self.source.value if isinstance(self.source, ResolverSource) else self.source,
"resolved_at": self.resolved_at,
"related_count": len(self.related_addresses),
}
@dataclass
class EntityReport:
"""Full entity resolution report for one or more addresses."""
query_addresses: list[str]
chain: str
entities: list[ResolvedEntity]
summary: dict[str, Any]
generated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
error: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"query_addresses": self.query_addresses,
"chain": self.chain,
"entities": [e.to_dict() for e in self.entities],
"summary": self.summary,
"generated_at": self.generated_at,
"error": self.error,
}
# ── Entity Resolver ───────────────────────────────────────────────────────────
class ArkhamEntityResolver:
"""
Multi-source entity resolution engine.
Pipeline:
1. Local DB lookup (fastest, always available)
2. Entity Registry lookup (CEX wallets, known protocols)
3. Entity Labeler lookup (advanced protocol detection)
4. Arkham API lookup (requires API key, richest data)
5. Heuristic inference (pattern matching, fallback)
Each source contributes confidence-weighted labels. Final confidence
is aggregated from all sources.
"""
def __init__(self, cache_ttl: int = 300):
self._arkham: ArkhamClient | None = None
self._cache_ttl = cache_ttl
async def _get_arkham(self) -> ArkhamClient | None:
"""Lazy-init Arkham client if API key is available."""
if self._arkham is None and ArkhamClient is not None and bool(ARKHAM_API_KEY):
try:
self._arkham = ArkhamClient(cache_ttl=self._cache_ttl)
except Exception as e:
logger.warning(f"Failed to init Arkham client: {e}")
return self._arkham
async def close(self):
"""Close the underlying Arkham HTTP client."""
if self._arkham is not None:
await self._arkham.close()
self._arkham = None
def _detect_chain(self, address: str) -> str:
"""Heuristically detect chain from address format."""
if ETH_ADDRESS_RE.match(address):
return "ethereum"
if SOL_ADDRESS_RE.match(address):
return "solana"
# Could expand with more chain patterns
return "unknown"
def _lookup_local_db(self, address: str) -> ResolvedEntity | None:
"""Step 1: Check hardcoded known entity database."""
addr = address.lower()
entry = KNOWN_ENTITIES.get(addr)
if not entry:
return None
labels = [
EntityLabel(
label=str(entry.get("label", entry["name"])),
source=ResolverSource.LOCAL_DB,
confidence=0.95,
category=str(entry.get("category", "unknown")),
)
]
tags_raw = entry.get("tags", "")
tags = [t.strip() for t in tags_raw.split(",") if t.strip()]
category = EntityCategory(entry["category"]) if entry["category"] in EntityCategory._value2member_map_ else EntityCategory.UNKNOWN # type: ignore
return ResolvedEntity(
address=address,
chain=self._detect_chain(address),
entity_name=str(entry["name"]),
category=category,
confidence=95.0,
labels=labels,
tags=tags,
source=ResolverSource.LOCAL_DB,
)
def _lookup_entity_registry(self, address: str) -> ResolvedEntity | None:
"""Step 2: Check KNOWN_CEX_WALLETS from entity_registry."""
addr = address.lower()
labels: list[EntityLabel] = []
for exchange_name, wallets in KNOWN_CEX_WALLETS.items():
for w in wallets:
if w.lower() == addr:
labels.append(EntityLabel(
label=f"{exchange_name.capitalize()} Wallet",
source=ResolverSource.ENTITY_REGISTRY,
confidence=0.90,
category="exchange",
))
if not labels:
return None
return ResolvedEntity(
address=address,
chain=self._detect_chain(address),
entity_name=labels[0].label.split(" Wallet")[0],
category=EntityCategory.EXCHANGE,
confidence=90.0,
labels=labels,
tags=["cex", "exchange"],
source=ResolverSource.ENTITY_REGISTRY,
)
def _lookup_entity_labeler(self, address: str) -> ResolvedEntity | None:
"""Step 3: Check EXCHANGES and PROTOCOLS from entity_labeler."""
addr = address.lower()
labels: list[EntityLabel] = []
# Check EXCHANGES dict
for name, addresses in EXCHANGES.items():
for a in addresses:
if isinstance(a, str) and a.lower() == addr:
labels.append(EntityLabel(
label=f"{name.capitalize()} (Labeler)",
source=ResolverSource.ENTITY_LABELER,
confidence=0.85,
category="exchange",
))
# Check PROTOCOLS dict
for name, info in PROTOCOLS.items():
if isinstance(info, dict):
proto_addr = info.get("address", "")
if isinstance(proto_addr, str) and proto_addr.lower() == addr:
labels.append(EntityLabel(
label=f"{name.capitalize()} (Labeler)",
source=ResolverSource.ENTITY_LABELER,
confidence=0.90,
category="defi",
))
if not labels:
return None
top = labels[0]
cat = EntityCategory.EXCHANGE if top.category == "exchange" else EntityCategory.DEFI
return ResolvedEntity(
address=address,
chain=self._detect_chain(address),
entity_name=top.label.split(" (Labeler")[0],
category=cat,
confidence=85.0,
labels=labels,
tags=[top.category],
source=ResolverSource.ENTITY_LABELER,
)
async def _lookup_arkham_api(self, address: str) -> ResolvedEntity | None:
"""Step 4: Query Arkham Intelligence API for entity data."""
arkham = await self._get_arkham()
if not arkham:
return None
try:
data = await arkham.get_entity(address)
if not data or "error" in data:
return None
entity_name = data.get("name", data.get("entity", ""))
if not entity_name:
return None
category_raw = data.get("category", "unknown")
category = EntityCategory.UNKNOWN
if category_raw in EntityCategory._value2member_map_:
category = EntityCategory(category_raw)
labels = [
EntityLabel(
label=data.get("label", entity_name),
source=ResolverSource.ARKHAM_API,
confidence=min(1.0, float(data.get("confidence", 0.85))),
category=category_raw,
)
]
return ResolvedEntity(
address=address,
chain=self._detect_chain(address),
entity_name=entity_name,
category=category,
confidence=min(100.0, float(data.get("confidence", 85)) * 100),
labels=labels,
tags=data.get("tags", []),
source=ResolverSource.ARKHAM_API,
arkham_data=data,
)
except Exception as e:
logger.warning(f"Arkham API lookup failed for {address}: {e}")
return None
def _heuristic_inference(self, address: str) -> ResolvedEntity | None:
"""Step 5: Fallback heuristic β€” pattern-based entity inference."""
addr = address.lower()
signals: list[dict[str, Any]] = []
labels: list[EntityLabel] = []
tags: list[str] = []
# Check address patterns
if addr.endswith("dead") or addr.endswith("0000"):
signals.append({"type": "burn_pattern", "detail": "Address ends with dead/0000", "weight": 0.3})
labels.append(EntityLabel(
label="Likely Burn Address",
source=ResolverSource.HEURISTIC,
confidence=0.6,
category="burn",
))
tags.append("burn")
if addr[:6] == "0x0000" and len(addr) == 42:
signals.append({"type": "zero_prefix", "detail": "Heavy zero-prefixed address", "weight": 0.2})
# Check if it looks like a contract (common patterns)
# Most contracts start with non-random patterns
if not labels:
return None
confidence = max(0.0, min(100.0, 20.0 + sum(s.get("weight", 0) for s in signals) * 100))
return ResolvedEntity(
address=address,
chain=self._detect_chain(address),
entity_name="Unknown (Heuristic Match)",
category=EntityCategory.UNKNOWN,
confidence=round(confidence, 1),
labels=labels,
tags=tags,
source=ResolverSource.HEURISTIC,
raw_signals=signals,
)
async def resolve(
self,
address: str,
chain: str | None = None,
use_arkham: bool = True,
) -> ResolvedEntity:
"""
Resolve a single blockchain address to an entity.
Pipeline: Local DB β†’ Entity Registry β†’ Entity Labeler β†’ Arkham API β†’ Heuristic
Args:
address: Blockchain address to resolve.
chain: Chain override (auto-detected if None).
use_arkham: Whether to attempt Arkham API lookup.
Returns:
ResolvedEntity with best available data.
"""
if not chain:
chain = self._detect_chain(address)
# Validate address format
input_entity = address.strip()
if not (ETH_ADDRESS_RE.match(input_entity) or SOL_ADDRESS_RE.match(input_entity)):
return ResolvedEntity(
address=address,
chain=chain,
entity_name="Invalid Address",
category=EntityCategory.UNKNOWN,
confidence=0.0,
labels=[],
tags=["invalid"],
source=ResolverSource.HEURISTIC,
)
# Pipeline β€” stop at first positive match
result = self._lookup_local_db(input_entity)
if result:
result.chain = chain or result.chain
return result
result = self._lookup_entity_registry(input_entity)
if result:
result.chain = chain or result.chain
return result
result = self._lookup_entity_labeler(input_entity)
if result:
result.chain = chain or result.chain
return result
if use_arkham:
result = await self._lookup_arkham_api(input_entity)
if result:
result.chain = chain or result.chain
return result
result = self._heuristic_inference(input_entity)
if result:
result.chain = chain or result.chain
return result
# No match at all
return ResolvedEntity(
address=address,
chain=chain,
entity_name="Unknown",
category=EntityCategory.UNKNOWN,
confidence=0.0,
labels=[],
tags=[],
source=ResolverSource.HEURISTIC,
)
async def resolve_batch(
self,
addresses: list[str],
chain: str | None = None,
use_arkham: bool = True,
) -> list[ResolvedEntity]:
"""
Resolve multiple addresses in sequence.
Args:
addresses: List of blockchain addresses.
chain: Chain override for all addresses.
use_arkham: Whether to attempt Arkham API lookup.
Returns:
List of ResolvedEntity objects.
"""
results: list[ResolvedEntity] = []
for addr in addresses:
result = await self.resolve(addr, chain=chain, use_arkham=use_arkham)
results.append(result)
return results
# ── Aggregation & Reporting ────────────────────────────────────────────────────
async def analyze_addresses(
addresses: list[str],
chain: str | None = None,
use_arkham: bool = True,
) -> EntityReport:
"""
Main entry point: resolve addresses and produce a structured report.
Args:
addresses: One or more blockchain addresses to resolve.
chain: Chain override (auto-detected if None).
use_arkham: Whether to query Arkham API.
Returns:
EntityReport with resolved entities and summary statistics.
"""
if not addresses:
return EntityReport(
query_addresses=[],
chain=chain or "unknown",
entities=[],
summary={"total": 0, "known": 0, "unknown": 0, "avg_confidence": 0.0},
error="No addresses provided",
)
resolver = ArkhamEntityResolver()
try:
if chain is None:
chain = resolver._detect_chain(addresses[0])
entities = await resolver.resolve_batch(addresses, chain=chain, use_arkham=use_arkham)
known = [e for e in entities if e.confidence >= 50.0]
unknown = [e for e in entities if e.confidence < 50.0]
categories: dict[str, int] = {}
for e in entities:
cat = e.category.value if isinstance(e.category, EntityCategory) else str(e.category)
categories[cat] = categories.get(cat, 0) + 1
avg_conf = sum(e.confidence for e in entities) / len(entities) if entities else 0.0
summary = {
"total": len(entities),
"known": len(known),
"unknown": len(unknown),
"avg_confidence": round(avg_conf, 1),
"categories": categories,
"sources_used": list({e.source.value if isinstance(e.source, ResolverSource) else e.source for e in entities}),
}
return EntityReport(
query_addresses=addresses,
chain=chain,
entities=entities,
summary=summary,
)
finally:
await resolver.close()
def format_report(report: EntityReport) -> str:
"""
Format an EntityReport as a human-readable string.
Args:
report: The report to format.
Returns:
Formatted string suitable for CLI or API response.
"""
lines = [
"β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”",
"β”‚ Arkham Entity Report β”‚",
"β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€",
]
if report.error:
lines.append(f"β”‚ ERROR: {report.error}")
lines.append("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜")
return "\n".join(lines)
lines.append(f"β”‚ Chain: {report.chain}")
lines.append(f"β”‚ Addresses: {len(report.query_addresses)}")
lines.append(f"β”‚ Generated: {report.generated_at}")
lines.append("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€")
for i, entity in enumerate(report.entities):
lines.append(f"β”‚ [{i + 1}] {entity.address[:42]}")
lines.append(f"β”‚ Entity: {entity.entity_name}")
cat_str = entity.category.value if isinstance(entity.category, EntityCategory) else str(entity.category)
lines.append(f"β”‚ Category: {cat_str}")
lines.append(f"β”‚ Confidence: {entity.confidence:.0f}/100")
src_str = entity.source.value if isinstance(entity.source, ResolverSource) else str(entity.source)
lines.append(f"β”‚ Source: {src_str}")
if entity.tags:
lines.append(f"β”‚ Tags: {', '.join(entity.tags[:5])}")
if entity.labels:
for label in entity.labels:
lines.append(f"β”‚ Label: [{label.source.value}] {label.label} (conf: {label.confidence:.0%})")
if entity.related_addresses:
lines.append(f"β”‚ Related: {len(entity.related_addresses)} addresses")
if i < len(report.entities) - 1:
lines.append("β”‚" + "─" * 60 + "β”‚")
# Summary footer
lines.append("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€")
s = report.summary
lines.append(f"β”‚ Summary: {s.get('total', 0)} total, {s.get('known', 0)} known, {s.get('unknown', 0)} unknown")
lines.append(f"β”‚ Avg Confidence: {s.get('avg_confidence', 0):.1f}/100")
cats = s.get("categories", {})
if cats:
cat_str = ", ".join(f"{k}={v}" for k, v in sorted(cats.items()))
lines.append(f"β”‚ Categories: {cat_str}")
lines.append("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜")
return "\n".join(lines)
# ── CLI Entry Point ────────────────────────────────────────────────────────────
async def main():
"""CLI entry point for entity resolution."""
import argparse
parser = argparse.ArgumentParser(
description="Arkham Entity Resolver β€” map addresses to real-world entities",
)
parser.add_argument("addresses", nargs="+", help="Blockchain address(es) to resolve")
parser.add_argument("--chain", "-c", default=None, help="Chain override (auto-detect if omitted)")
parser.add_argument("--no-arkham", action="store_true", help="Skip Arkham API lookup")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
report = await analyze_addresses(
addresses=args.addresses,
chain=args.chain,
use_arkham=not args.no_arkham,
)
if args.json:
import json
print(json.dumps(report.to_dict(), indent=2))
else:
print(format_report(report))
return 0 if not report.error else 1
if __name__ == "__main__":
exit(asyncio.run(main()))