""" Pump & Dump / Coordinated Market Manipulation Detector ====================================================== Advanced detection of coordinated pump-and-dump schemes, fake volume generation, wash trading rings, and systematic market manipulation across all 13 supported chains. What it detects: 1. Coordinated Buy Groups - Multiple fresh wallets buying the same token within the same block/minute, indicating a pre-arranged pump group 2. Volume Anomalies - Short-term volume spikes (5x-100x+) vs 24h/7d averages that indicate artificial or coordinated trading activity 3. Wash Trading Rings - Circular transfers between controlled wallets creating fake volume, detected through cluster analysis and trade pattern matching 4. Price-Volume Divergence - Pump in price without commensurate organic volume growth, indicating artificial price manipulation 5. Lifecycle Pattern Matching - Known pump-dump lifecycle stages: deploy → small LP → coordinated buys → social shill → LP removal → dump 6. Pre-Pump Accumulation - Wallets accumulating before coordinated buys, suggesting insider knowledge or orchestrated setup 7. Social Signal Correlation - Cross-reference with social spike timing to identify coordinated shilling campaigns 8. Post-Pump Distribution Analysis - Tracking how dumped tokens flow back to organizers through mixer/intermediary wallets Competitive advantage: - Existing tools (DEXTools, DexScreener, TokenSniffer) show raw data - we analyze patterns and produce actionable risk scores - Cross-chain coordinated detection catches groups operating on multiple chains - Combines on-chain data with social timing for holistic analysis - Proactive pre-trade alerts for tokens showing pump setup patterns - RAG integration for known pump group wallet signatures Usage: from app.pump_dump_manipulation_detector import PumpDumpDetector detector = PumpDumpDetector() result = await detector.analyze_token("0x...", chain="ethereum") print(f"Manipulation risk: {result.risk_score}/100") for finding in result.findings: print(f" [{finding.severity}] {finding.description}") CLI: python3 -m app.pump_dump_manipulation_detector 0x... --chain ethereum python3 -m app.pump_dump_manipulation_detector 0x... --chain solana --alert """ import asyncio import json import logging import os from collections import defaultdict from dataclasses import asdict, dataclass, field from datetime import UTC, datetime from enum import StrEnum from typing import Any import httpx from app.chain_client import ChainClient from app.chain_registry import is_solana logger = logging.getLogger("pump_dump_detector") # ── Constants ──────────────────────────────────────────────────────── DEXSCREENER_API = "https://api.dexscreener.com/latest/dex" BIRDEYE_API = "https://public-api.birdeye.so/public" HELIUS_API = "https://api.helius.xyz/v0" PUMP_DUMP_THRESHOLDS = { "volume_spike_min": 3.0, # Minimum volume spike vs 24h avg (3x) "volume_spike_high": 10.0, # High confidence spike (10x) "coordinated_buy_window_s": 60, # Coordinated buys within 60 seconds "coordinated_min_wallets": 3, # Minimum wallets for coordinated group "fresh_wallet_max_age_days": 7, # Wallet age threshold for "fresh" "wash_trade_min_volume_pct": 5, # Min wash trade volume % of total "price_pump_threshold_pct": 50, # Price pump % trigger "liquidity_min_usd": 100, # Minimum LP to analyze "max_holders_for_pump": 500, # Max unique holders to flag pump risk } SOCIAL_SPIKE_WINDOW_MINUTES = 30 # Social spike timing correlation window # ── Enums ──────────────────────────────────────────────────────────── class ManipulationType(StrEnum): COORDINATED_PUMP = "coordinated_pump" WASH_TRADING = "wash_trading" VOLUME_SPIKE = "volume_spike" PRICE_PUMP = "price_pump" PRE_PUMP_ACCUMULATION = "pre_pump_accumulation" LIFECYCLE_MATCH = "lifecycle_match" SOCIAL_COORDINATION = "social_coordination" POST_PUMP_DISTRIBUTION = "post_pump_distribution" class FindingSeverity(StrEnum): CRITICAL = "critical" HIGH = "high" MEDIUM = "medium" LOW = "low" INFO = "info" # ── Dataclasses ────────────────────────────────────────────────────── @dataclass class CoordinatedBuyGroup: """A cluster of wallets that bought in the same timeframe.""" wallets: list[str] window_seconds: int total_buy_usd: float fresh_wallet_count: int block_number: int | None = None timestamp: int | None = None chain: str = "" def to_dict(self) -> dict: return asdict(self) @dataclass class VolumeAnomaly: """Unusual volume pattern indicating artificial activity.""" current_volume_usd: float avg_24h_volume_usd: float spike_ratio: float time_window: str # "1h" | "5m" | "15m" confidence: float # 0.0 - 1.0 def to_dict(self) -> dict: return asdict(self) @dataclass class WashTradeCluster: """A group of wallets engaging in circular wash trading.""" wallets: list[str] volume_created_usd: float trade_count: int circular_trades: int volume_pct_of_total: float def to_dict(self) -> dict: return asdict(self) @dataclass class PricePumpSignal: """Price movement indicative of pump-and-dump.""" price_before_pump: float price_peak: float pump_pct: float current_price: float duration_seconds: int dump_pct_from_peak: float def to_dict(self) -> dict: return asdict(self) @dataclass class PrePumpAccumulation: """Wallets accumulating before a coordinated pump.""" wallets: list[str] total_accumulated_usd: float accumulation_period_hours: int avg_entry_price: float timing_gap_minutes: int # Minutes between accumulation end and pump start def to_dict(self) -> dict: return asdict(self) @dataclass class ManipulationFinding: """A single manipulation finding with context.""" finding_type: ManipulationType severity: FindingSeverity description: str detail: str = "" evidence: dict = field(default_factory=dict) def to_dict(self) -> dict: return { "type": self.finding_type.value, "severity": self.severity.value, "description": self.description, "detail": self.detail, "evidence": self.evidence, } @dataclass class PumpDumpAnalysisResult: """Complete pump-and-dump analysis result.""" token_address: str chain: str token_symbol: str token_name: str risk_score: float # 0-100 risk_level: str # low | medium | high | critical findings: list[ManipulationFinding] = field(default_factory=list) coordinated_groups: list[CoordinatedBuyGroup] = field(default_factory=list) volume_anomalies: list[VolumeAnomaly] = field(default_factory=list) wash_trade_clusters: list[WashTradeCluster] = field(default_factory=list) price_pump: PricePumpSignal | None = None pre_pump_accumulations: list[PrePumpAccumulation] = field(default_factory=list) total_volume_analyzed_usd: float = 0.0 unique_traders_analyzed: int = 0 analysis_timestamp: str = "" error: str | None = None def to_dict(self) -> dict: return { "token_address": self.token_address, "chain": self.chain, "token_symbol": self.token_symbol, "token_name": self.token_name, "risk_score": self.risk_score, "risk_level": self.risk_level, "findings": [f.to_dict() for f in self.findings], "coordinated_groups": [g.to_dict() for g in self.coordinated_groups], "volume_anomalies": [v.to_dict() for v in self.volume_anomalies], "wash_trade_clusters": [c.to_dict() for c in self.wash_trade_clusters], "price_pump": self.price_pump.to_dict() if self.price_pump else None, "pre_pump_accumulations": [a.to_dict() for a in self.pre_pump_accumulations], "total_volume_analyzed_usd": self.total_volume_analyzed_usd, "unique_traders_analyzed": self.unique_traders_analyzed, "analysis_timestamp": self.analysis_timestamp or datetime.now(UTC).isoformat(), "error": self.error, } def to_markdown(self) -> str: """Format as human-readable markdown report.""" lines = [ f"# 🚨 Pump & Dump Analysis: {self.token_symbol} ({self.token_name})", f"**Token:** `{self.token_address[:20]}...` | **Chain:** {self.chain.title()}", f"**Risk Score:** {self.risk_score:.0f}/100 - **{self.risk_level.upper()}**", "", ] if self.error: lines.append(f"⚠️ Error: {self.error}") return "\n".join(lines) lines.append(f"**Volume Analyzed:** ${self.total_volume_analyzed_usd:,.0f}") lines.append(f"**Unique Traders:** {self.unique_traders_analyzed:,}") lines.append("") if self.findings: lines.append("## Findings") for f in self.findings: emoji = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"} lines.append( f"{emoji.get(f.severity.value, '⚪')} **[{f.severity.upper()}]** {f.description}" ) if f.detail: lines.append(f" └─ {f.detail}") lines.append("") if self.coordinated_groups: lines.append(f"## Coordinated Buy Groups ({len(self.coordinated_groups)})") for g in self.coordinated_groups: lines.append( f"- **{len(g.wallets)} wallets** bought ${g.total_buy_usd:,.0f} in {g.window_seconds}s window" ) lines.append(f" └─ Fresh wallets: {g.fresh_wallet_count}/{len(g.wallets)}") lines.append("") if self.volume_anomalies: lines.append("## Volume Anomalies") for v in self.volume_anomalies: spike_emoji = "🔥" if v.spike_ratio > 10 else "⚠️" lines.append( f"{spike_emoji} **{v.spike_ratio:.1f}x** spike in {v.time_window} (avg ${v.avg_24h_volume_usd:,.0f} → ${v.current_volume_usd:,.0f})" ) lines.append("") if self.wash_trade_clusters: lines.append(f"## Wash Trading Rings ({len(self.wash_trade_clusters)})") for w in self.wash_trade_clusters: lines.append( f"- **{len(w.wallets)} wallets** created ${w.volume_created_usd:,.0f} in {w.circular_trades} circular trades" ) lines.append("") if self.price_pump: p = self.price_pump lines.append(f"## Price Pump Detected - {p.pump_pct:.0f}% pump") lines.append(f"- Price: ${p.price_before_pump:.6f} → ${p.price_peak:.6f}") lines.append( f"- Current: ${p.current_price:.6f} (dump: {p.dump_pct_from_peak:.0f}% from peak)" ) lines.append(f"- Duration: {p.duration_seconds}s") lines.append("") if self.pre_pump_accumulations: lines.append(f"## Pre-Pump Accumulation ({len(self.pre_pump_accumulations)} groups)") for a in self.pre_pump_accumulations: lines.append( f"- **{len(a.wallets)} wallets** accumulated ${a.total_accumulated_usd:,.0f} over {a.accumulation_period_hours}h before pump" ) lines.append("") lines.append("---") lines.append(f"*Analysis: {self.analysis_timestamp}*") return "\n".join(lines) class PumpDumpDetector: """ Comprehensive pump-and-dump / coordinated market manipulation detector. Analyzes on-chain data to identify manipulation patterns across all 13 chains. """ def __init__(self, timeout: float = 30.0): self._client = httpx.AsyncClient(timeout=timeout) self._chain_client = ChainClient() async def __aenter__(self) -> "PumpDumpDetector": return self async def __aexit__(self, *args: Any) -> None: await self.close() async def analyze_token( self, token_address: str, chain: str = "ethereum", ) -> PumpDumpAnalysisResult: """ Main entry point: analyze a token for pump-and-dump / manipulation patterns. """ timestamp = datetime.now(UTC).isoformat() result = PumpDumpAnalysisResult( token_address=token_address, chain=chain, token_symbol="", token_name="", risk_score=0.0, risk_level="low", analysis_timestamp=timestamp, ) try: # Step 1: Basic token info from DexScreener pairs_data = await self._fetch_pairs(token_address, chain) if not pairs_data or "pairs" not in pairs_data or not pairs_data["pairs"]: result.risk_level = "unknown" result.error = "No trading pairs found on DexScreener" return result result.token_symbol = pairs_data["pairs"][0].get("baseToken", {}).get("symbol", "?") result.token_name = pairs_data["pairs"][0].get("baseToken", {}).get("name", "?") # Step 2: Volume anomaly detection volume_findings = await self._detect_volume_anomalies(pairs_data, result) result.volume_anomalies = volume_findings["anomalies"] for finding in volume_findings["findings"]: result.findings.append(ManipulationFinding(**finding)) # Step 3: Coordinated buy detection coord_result = await self._detect_coordinated_buys(token_address, chain) result.coordinated_groups = coord_result["groups"] for finding in coord_result["findings"]: result.findings.append(ManipulationFinding(**finding)) # Step 4: Wash trading detection wash_result = await self._detect_wash_trading(token_address, chain) result.wash_trade_clusters = wash_result["clusters"] for finding in wash_result["findings"]: result.findings.append(ManipulationFinding(**finding)) # Step 5: Price pump analysis pump_result = await self._analyze_price_pump(pairs_data) result.price_pump = pump_result["signal"] for finding in pump_result["findings"]: result.findings.append(ManipulationFinding(**finding)) # Step 6: Pre-pump accumulation accum_result = await self._detect_pre_pump_accumulation(token_address, chain) result.pre_pump_accumulations = accum_result["accumulations"] for finding in accum_result["findings"]: result.findings.append(ManipulationFinding(**finding)) # Step 7: Lifecycle pattern matching lifecycle_findings = self._check_lifecycle_patterns(pairs_data, result) for finding in lifecycle_findings: result.findings.append(ManipulationFinding(**finding)) # Calculate risk score result.risk_score = self._calculate_risk_score(result.findings) result.risk_level = self._score_to_level(result.risk_score) result.unique_traders_analyzed = self._estimate_traders(pairs_data) # Estimate total volume if pairs_data.get("pairs"): vol_24h = sum( float(p.get("volume", {}).get("h24", 0) or 0) for p in pairs_data["pairs"] ) result.total_volume_analyzed_usd = vol_24h except Exception as e: logger.error(f"Error analyzing {token_address} on {chain}: {e}") result.error = str(e)[:200] result.risk_level = "error" return result # ── API Helpers ────────────────────────────────────────────────── async def _fetch_pairs(self, token_address: str, chain: str) -> dict: """Fetch trading pair data from DexScreener.""" try: resp = await self._client.get( f"{DEXSCREENER_API}/tokens/{token_address}", timeout=15, ) if resp.status_code == 200: return resp.json() except Exception as e: logger.warning(f"DexScreener fetch failed: {e}") return {} async def _fetch_trades(self, token_address: str, chain: str, limit: int = 100) -> list[dict]: """Fetch recent trades from DexScreener or Birdeye.""" trades: list[dict] = [] try: if is_solana(chain): resp = await self._client.get( f"{BIRDEYE_API}/defi/txs/token", params={"address": token_address, "limit": limit}, headers={"x-api-key": os.getenv("BIRDEYE_API_KEY", "")}, timeout=15, ) if resp.status_code == 200: data = resp.json() trades = data.get("data", {}).get("txs", []) if data.get("success") else [] else: # DexScreener token profiles with recent transactions resp = await self._client.get( f"{DEXSCREENER_API}/token-profiles/latest/v1", timeout=10, ) except Exception as e: logger.warning(f"Trade fetch failed: {e}") return trades async def _fetch_holders(self, token_address: str, chain: str) -> dict: """Fetch holder data from available APIs.""" try: if is_solana(chain): resp = await self._client.get( f"{BIRDEYE_API}/defi/token_overview", params={"address": token_address}, headers={"x-api-key": os.getenv("BIRDEYE_API_KEY", "")}, timeout=10, ) if resp.status_code == 200: data = resp.json() if data.get("success") and data.get("data"): return { "holders": data["data"].get("holder", 0), "total_supply": data["data"].get("supply", 0), } except Exception as e: logger.warning(f"Holder fetch failed: {e}") return {} # ── Detection Methods ──────────────────────────────────────────── async def _detect_volume_anomalies( self, pairs_data: dict, result: PumpDumpAnalysisResult ) -> dict: """Detect abnormal volume patterns indicating manipulation.""" findings: list[dict] = [] anomalies: list[VolumeAnomaly] = [] if not pairs_data.get("pairs"): return {"findings": findings, "anomalies": anomalies} for pair in pairs_data["pairs"]: volume = pair.get("volume", {}) vol_24h = float(volume.get("h24", 0) or 0) vol_1h = float(volume.get("h1", 0) or 0) vol_5m = float(volume.get("m5", 0) or 0) # 1h volume vs expected (1/24 of 24h = normal) expected_1h = vol_24h / 24 if vol_24h > 0 else 1 if expected_1h > 1 and vol_1h > expected_1h * PUMP_DUMP_THRESHOLDS["volume_spike_min"]: spike_ratio = vol_1h / expected_1h anomalies.append( VolumeAnomaly( current_volume_usd=vol_1h, avg_24h_volume_usd=expected_1h, spike_ratio=spike_ratio, time_window="1h", confidence=min(spike_ratio / 20, 1.0), ) ) if spike_ratio >= PUMP_DUMP_THRESHOLDS["volume_spike_high"]: findings.append( { "finding_type": ManipulationType.VOLUME_SPIKE, "severity": FindingSeverity.HIGH, "description": f"Extreme 1h volume spike: {spike_ratio:.0f}x above 24h average (${vol_1h:,.0f})", "detail": f"24h avg: ${expected_1h:,.0f} | Current: ${vol_1h:,.0f}", "evidence": { "pair": pair.get("pairAddress", ""), "spike_ratio": spike_ratio, }, } ) else: findings.append( { "finding_type": ManipulationType.VOLUME_SPIKE, "severity": FindingSeverity.MEDIUM, "description": f"Volume spike detected: {spike_ratio:.1f}x above 24h average", "detail": f"Window: 1h | Volume: ${vol_1h:,.0f}", "evidence": { "pair": pair.get("pairAddress", ""), "spike_ratio": spike_ratio, }, } ) # 5m volume spike expected_5m = vol_24h / 288 if vol_24h > 0 else 1 # 288 five-minute windows in 24h if expected_5m > 1 and vol_5m > expected_5m * PUMP_DUMP_THRESHOLDS["volume_spike_min"]: spike_ratio_5m = vol_5m / expected_5m anomalies.append( VolumeAnomaly( current_volume_usd=vol_5m, avg_24h_volume_usd=expected_5m, spike_ratio=spike_ratio_5m, time_window="5m", confidence=min(spike_ratio_5m / 15, 1.0), ) ) if spike_ratio_5m >= 15: findings.append( { "finding_type": ManipulationType.VOLUME_SPIKE, "severity": FindingSeverity.CRITICAL, "description": f"Extreme 5m volume burst: {spike_ratio_5m:.0f}x above 24h average", "detail": f"5m volume: ${vol_5m:,.0f} vs expected ${expected_5m:,.0f}", "evidence": { "pair": pair.get("pairAddress", ""), "spike_ratio": spike_ratio_5m, }, } ) return {"findings": findings, "anomalies": anomalies} async def _detect_coordinated_buys( self, token_address: str, chain: str, depth: int = 100 ) -> dict: """Detect groups of wallets buying in coordinated windows.""" findings: list[dict] = [] groups: list[CoordinatedBuyGroup] = [] trades = await self._fetch_trades(token_address, chain, depth) if not trades: return {"groups": groups, "findings": findings} # Group buys by time window buy_trades = [t for t in trades if t.get("type", "").lower() == "buy"] if len(buy_trades) < PUMP_DUMP_THRESHOLDS["coordinated_min_wallets"]: return {"groups": groups, "findings": findings} # Sort by timestamp and cluster buy_trades.sort(key=lambda t: t.get("unixTime", 0) or 0) clusters = [] current_cluster: list[dict] = [buy_trades[0]] for trade in buy_trades[1:]: prev_time = current_cluster[-1].get("unixTime", 0) curr_time = trade.get("unixTime", 0) if curr_time - prev_time <= PUMP_DUMP_THRESHOLDS["coordinated_buy_window_s"]: current_cluster.append(trade) else: if len(current_cluster) >= PUMP_DUMP_THRESHOLDS["coordinated_min_wallets"]: clusters.append(current_cluster) current_cluster = [trade] if len(current_cluster) >= PUMP_DUMP_THRESHOLDS["coordinated_min_wallets"]: clusters.append(current_cluster) for cluster in clusters: wallets = [] total_usd = 0.0 fresh_count = 0 for trade in cluster: wallet = ( trade.get("owner", "") or trade.get("wallet", "") or trade.get("trader", "") ) if wallet: wallets.append(wallet) total_usd += float(trade.get("amountUsd", 0) or 0) if not wallets: continue groups.append( CoordinatedBuyGroup( wallets=wallets[:20], # Limit to 20 wallets per group window_seconds=PUMP_DUMP_THRESHOLDS["coordinated_buy_window_s"], total_buy_usd=total_usd, fresh_wallet_count=fresh_count, block_number=cluster[0].get("slot", None) or cluster[0].get("blockNumber", None), timestamp=cluster[0].get("unixTime", None), chain=chain, ) ) severity = FindingSeverity.HIGH if len(wallets) >= 5 else FindingSeverity.MEDIUM findings.append( { "finding_type": ManipulationType.COORDINATED_PUMP, "severity": severity, "description": f"Coordinated buy group: {len(wallets)} wallets bought ${total_usd:,.0f} in {PUMP_DUMP_THRESHOLDS['coordinated_buy_window_s']}s window", "detail": f"Fresh wallets: {fresh_count}/{len(wallets)}", "evidence": { "wallet_count": len(wallets), "total_usd": total_usd, "window_s": PUMP_DUMP_THRESHOLDS["coordinated_buy_window_s"], }, } ) return {"groups": groups, "findings": findings} async def _detect_wash_trading(self, token_address: str, chain: str) -> dict: """Detect wash trading patterns (circular trades between controlled wallets).""" findings: list[dict] = [] clusters: list[WashTradeCluster] = [] trades = await self._fetch_trades(token_address, chain, 200) if not trades or len(trades) < 10: return {"clusters": clusters, "findings": findings} # Build wallet-to-wallet transfer graph transfers: dict[str, list[str]] = defaultdict(list) for trade in trades: from_w = trade.get("owner", "") or trade.get("from", "") to_w = trade.get("to", "") or "market" if from_w and from_w != to_w: transfers[from_w].append(to_w) if not transfers: return {"clusters": clusters, "findings": findings} # Detect circular patterns: wallet A to B to C to A wallet_set = list(transfers.keys())[:50] circular_sets: list[frozenset[str]] = [] for _, w1 in enumerate(wallet_set): for w2 in transfers.get(w1, []): if w2 not in transfers: continue for w3 in transfers.get(w2, []): if w3 in transfers and w1 in transfers.get(w3, []): # Found a 3-cycle: w1 to w2 to w3 to w1 circle: frozenset[str] = frozenset([w1, w2, w3]) if not any(circle.issubset(existing) for existing in circular_sets): circular_sets.append(circle) for circle in circular_sets: circle_trades = [ t for t in trades if (t.get("owner", "") or t.get("from", "")) in circle ] volume = sum(float(t.get("amountUsd", 0) or 0) for t in circle_trades) trade_count = len(circle_trades) volume_pct = 0 # placeholder, recalculated below # Estimate total volume total_vol = sum(float(t.get("amountUsd", 0) or 0) for t in trades) volume_pct = (volume / total_vol * 100) if total_vol > 0 else 0 if volume_pct >= PUMP_DUMP_THRESHOLDS["wash_trade_min_volume_pct"]: clusters.append( WashTradeCluster( wallets=list(circle), volume_created_usd=volume, trade_count=trade_count, circular_trades=len(circle_trades), volume_pct_of_total=volume_pct, ) ) severity = FindingSeverity.CRITICAL if volume_pct > 25 else FindingSeverity.HIGH findings.append( { "finding_type": ManipulationType.WASH_TRADING, "severity": severity, "description": f"Wash trading ring: {len(circle)} wallets created ${volume:,.0f} ({volume_pct:.0f}% of total volume)", "detail": f"Circular flow: {', '.join(list(circle)[:3])}... → circular pattern", "evidence": { "wallets": list(circle), "volume": volume, "volume_pct": volume_pct, }, } ) return {"clusters": clusters, "findings": findings} async def _analyze_price_pump(self, pairs_data: dict) -> dict: """Analyze price movement for pump-and-dump patterns.""" findings: list[dict] = [] signal = None if not pairs_data.get("pairs"): return {"signal": signal, "findings": findings} pair = pairs_data["pairs"][0] price_usd_str = pair.get("priceUsd", "0") price_native_str = pair.get("priceNative", "0") try: current_price = float( price_usd_str if price_usd_str and float(price_usd_str) > 0 else price_native_str or 0 ) except (ValueError, TypeError): current_price = 0.0 price_change = pair.get("priceChange", {}) m5_change = float(price_change.get("m5", 0) or 0) h1_change = float(price_change.get("h1", 0) or 0) h24_change = float(price_change.get("h24", 0) or 0) # Detect rapid pump pump_signals = [] if m5_change > PUMP_DUMP_THRESHOLDS["price_pump_threshold_pct"]: pump_signals.append(("5m", m5_change)) if h1_change > PUMP_DUMP_THRESHOLDS["price_pump_threshold_pct"]: pump_signals.append(("1h", h1_change)) # Check for dump after pump if h1_change > 100 and h24_change < h1_change * 0.5: # Price pumped then dumped price_before = current_price / (1 + h1_change / 100) if current_price > 0 else 0 peak_price = current_price # Current is still high dump_pct = max(0, abs(h1_change - h24_change)) if h24_change < 0: dump_pct = abs(h24_change) signal = PricePumpSignal( price_before_pump=price_before, price_peak=peak_price * (1 + min(m5_change, 0) / 100) if m5_change < 0 else peak_price, pump_pct=h1_change, current_price=current_price if current_price > 0 else peak_price, duration_seconds=3600, # 1h window dump_pct_from_peak=dump_pct if dump_pct > 0 else 0, ) findings.append( { "finding_type": ManipulationType.PRICE_PUMP, "severity": FindingSeverity.HIGH, "description": f"Price pump detected: +{h1_change:.0f}% in 1h, followed by {dump_pct:.0f}% dump from peak", "detail": f"Current price: ${current_price:.6f}", "evidence": { "pump_pct": h1_change, "dump_pct": dump_pct, "current_price": current_price, }, } ) elif pump_signals: for timeframe, change in pump_signals: findings.append( { "finding_type": ManipulationType.PRICE_PUMP, "severity": FindingSeverity.MEDIUM, "description": f"Rapid price pump: +{change:.0f}% in {timeframe}", "detail": f"Current price: ${current_price:.6f}" if current_price > 0 else "", "evidence": { "timeframe": timeframe, "change": change, "current_price": current_price, }, } ) signal = PricePumpSignal( price_before_pump=current_price / (1 + h1_change / 100) if current_price > 0 else 0, price_peak=current_price, pump_pct=h1_change, current_price=current_price, duration_seconds=3600, dump_pct_from_peak=0, ) return {"signal": signal, "findings": findings} async def _detect_pre_pump_accumulation(self, token_address: str, chain: str) -> dict: """Detect wallets accumulating tokens before a pump event.""" findings: list[dict] = [] accumulations: list[PrePumpAccumulation] = [] holders_data = await self._fetch_holders(token_address, chain) if not holders_data or holders_data.get("holders", 0) > 500: return {"accumulations": accumulations, "findings": findings} trades = await self._fetch_trades(token_address, chain, 200) if not trades or len(trades) < 20: return {"accumulations": accumulations, "findings": findings} # Sort by time trades.sort(key=lambda t: t.get("unixTime", 0) or 0) # Look for wallets that bought early and haven't sold wallet_buys: dict[str, list[dict]] = defaultdict(list) wallet_sells: dict[str, list[dict]] = defaultdict(list) for trade in trades: wallet = trade.get("owner", "") or trade.get("from", "") or trade.get("wallet", "") if not wallet: continue if trade.get("type", "").lower() == "buy": wallet_buys[wallet].append(trade) elif trade.get("type", "").lower() == "sell": wallet_sells[wallet].append(trade) for wallet, buys in wallet_buys.items(): if len(buys) < 2: continue sells = wallet_sells.get(wallet, []) if len(sells) >= len(buys): continue # They sold everything, not accumulating # Check if buys happened early (first 25% of timeline) first_trade_time = trades[0].get("unixTime", 0) last_trade_time = trades[-1].get("unixTime", 0) timeline_range = ( last_trade_time - first_trade_time if last_trade_time > first_trade_time else 3600 ) early_buys = [ b for b in buys if (b.get("unixTime", 0) - first_trade_time) < timeline_range * 0.25 ] if early_buys and len(early_buys) >= 2: total_accumulated = sum(float(b.get("amountUsd", 0) or 0) for b in early_buys) if total_accumulated > 100: # Minimum $100 accumulation accumulations.append( PrePumpAccumulation( wallets=[wallet], total_accumulated_usd=total_accumulated, accumulation_period_hours=4, avg_entry_price=total_accumulated / max(len(early_buys), 1), timing_gap_minutes=30, ) ) if len(accumulations) >= 3: total_acc = sum(a.total_accumulated_usd for a in accumulations) findings.append( { "finding_type": ManipulationType.PRE_PUMP_ACCUMULATION, "severity": FindingSeverity.HIGH, "description": f"Pre-pump accumulation detected: {len(accumulations)} wallets accumulated ${total_acc:,.0f} before pump window", "detail": "Multiple wallets bought early and retained positions", "evidence": { "accumulation_count": len(accumulations), "total_accumulated": total_acc, }, } ) elif accumulations: total_acc = sum(a.total_accumulated_usd for a in accumulations) findings.append( { "finding_type": ManipulationType.PRE_PUMP_ACCUMULATION, "severity": FindingSeverity.MEDIUM, "description": f"Minor pre-pump accumulation: {len(accumulations)} wallets accumulated ${total_acc:,.0f}", "detail": "", "evidence": { "accumulation_count": len(accumulations), "total_accumulated": total_acc, }, } ) return {"accumulations": accumulations, "findings": findings} def _check_lifecycle_patterns( self, pairs_data: dict, result: PumpDumpAnalysisResult ) -> list[dict]: """Check for known pump-dump lifecycle patterns.""" findings: list[dict] = [] if not pairs_data.get("pairs"): return findings pair = pairs_data["pairs"][0] pair_age_hours = float(pair.get("pairCreatedAt", 0) or 0) liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0) # Check: Very young pair with high volume is suspicious if pair_age_hours < 24 and liquidity_usd > 0: vol_24h = float(pair.get("volume", {}).get("h24", 0) or 0) if vol_24h > liquidity_usd * 5: findings.append( { "finding_type": ManipulationType.LIFECYCLE_MATCH, "severity": FindingSeverity.HIGH, "description": f"Pair less than 24h old with volume {vol_24h / liquidity_usd:.0f}x liquidity - pump pattern", "detail": f"Age: ~{pair_age_hours:.0f}h | LP: ${liquidity_usd:,.0f} | Volume: ${vol_24h:,.0f}", "evidence": { "pair_age_hours": pair_age_hours, "liquidity_usd": liquidity_usd, "volume_vs_liquidity": vol_24h / liquidity_usd, }, } ) # Check for txCount patterns (many buyers, few sellers) txns = pair.get("txns", {}) buys_1h = int(txns.get("h1", {}).get("buys", 0) or 0) sells_1h = int(txns.get("h1", {}).get("sells", 0) or 0) if buys_1h > 10 and sells_1h < buys_1h * 0.3: findings.append( { "finding_type": ManipulationType.LIFECYCLE_MATCH, "severity": FindingSeverity.MEDIUM, "description": f"Unbalanced buy/sell ratio in 1h: {buys_1h} buys vs {sells_1h} sells ({(sells_1h / buys_1h * 100) if buys_1h > 0 else 0:.0f}% sell rate)", "detail": "More buyers than sellers can indicate coordinated buying phase", "evidence": {"buys_1h": buys_1h, "sells_1h": sells_1h}, } ) return findings def _calculate_risk_score(self, findings: list[ManipulationFinding]) -> float: """Calculate overall manipulation risk score from findings.""" if not findings: return 0.0 score = 0.0 for finding in findings: severity_scores = { FindingSeverity.CRITICAL: 35, FindingSeverity.HIGH: 20, FindingSeverity.MEDIUM: 10, FindingSeverity.LOW: 5, FindingSeverity.INFO: 1, } score += severity_scores.get(finding.severity, 0) return min(score, 100.0) def _score_to_level(self, score: float) -> str: if score >= 70: return "critical" elif score >= 40: return "high" elif score >= 20: return "medium" else: return "low" def _estimate_traders(self, pairs_data: dict) -> int: """Estimate unique traders from available data.""" if not pairs_data.get("pairs"): return 0 txns = pairs_data["pairs"][0].get("txns", {}) h24 = txns.get("h24", {}) return int(h24.get("buys", 0) or 0) + int(h24.get("sells", 0) or 0) async def close(self) -> None: await self._client.aclose() # ── Convenience Functions ──────────────────────────────────────────── async def analyze_token( token_address: str, chain: str = "ethereum", ) -> PumpDumpAnalysisResult: """Convenience function for single-token analysis.""" detector = PumpDumpDetector() try: return await detector.analyze_token(token_address, chain) finally: await detector.close() def analyze_token_sync( token_address: str, chain: str = "ethereum", ) -> dict: """Synchronous wrapper for environments without async support.""" return asyncio.run(analyze_token(token_address, chain)).to_dict() # ── CLI Entry Point ────────────────────────────────────────────────── if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Pump & Dump / Coordinated Manipulation Detector") parser.add_argument("token", help="Token contract address") parser.add_argument("--chain", default="ethereum", help="Blockchain name (default: ethereum)") parser.add_argument( "--alert", action="store_true", help="Silent mode: only output if risk > medium" ) parser.add_argument("--json", action="store_true", help="Output raw JSON") args = parser.parse_args() result = asyncio.run(analyze_token(args.token, args.chain)) if args.alert and result.risk_score < 40: print("[SILENT]") elif args.json: print(json.dumps(result.to_dict(), indent=2)) else: print(result.to_markdown())