Market Manipulation Detection: Protecting AI Agents from Wash Trading and Pumps
Crypto markets remain rife with manipulation — wash trading inflates volumes, pump-and-dump schemes trap momentum traders, and spoofing creates false order book signals. AI agents are particularly vulnerable because they react to data inputs algorithmically. This guide equips agents with detection capabilities across all major manipulation types.
The Four Core Manipulation Types
Before building detection systems, it is essential to understand the mechanics of each manipulation type. Each leaves distinct statistical fingerprints in the data that are systematically detectable, even in real-time.
An entity simultaneously buys and sells the same asset to itself (or between colluding parties) to create artificial volume. The goal is to inflate volume metrics so the asset appears more liquid and active than it is, drawing in real traders who treat volume as a signal of legitimacy.
Coordinated buying drives up price while promoters create hype (Telegram groups, social media, fake news). Once retail enters chasing the move, insiders dump accumulated inventory at inflated prices. The dump phase is rapid and often exceeds 50% price decline within hours.
Large fake orders are placed in the order book to create the illusion of buy or sell pressure. When the price moves toward the fake orders, they are cancelled. The spoofer profits by trading in the opposite direction while other algorithms react to the false signals.
Large players push price through known liquidity clusters (previous highs/lows where stop orders aggregate) to trigger stop-loss cascades. After the stop run clears the cluster, price reverts. The manipulator profits from the temporary move they engineered.
Detection Algorithms: Statistical Approaches
Detection relies on identifying statistical patterns that deviate from what legitimate market activity would produce. Key principle: manipulation leaves abnormal ratios and timing patterns that cannot persist indefinitely. The challenge is real-time detection with low false positive rates.
Core Detection Metrics
Trade-to-Volume Ratio
High volume with few large trades = wash trading. Legitimate volume has a normal distribution of trade sizes.
Order-to-Trade Ratio (OTR)
Spoofers cancel far more orders than they fill. OTR > 15:1 is a strong spoofing signal in normal conditions.
Price-Volume Divergence
Wash trading produces high volume without price discovery. Volume without price movement is the clearest signal.
Kyle's Lambda
Price impact per unit of order flow. Abnormally low lambda = artificial volume (wash). Abnormally high = informed or manipulative large orders.
Statistical Tests for Wash Trading
Wash trading produces a characteristic signature: the trade size distribution shifts from the expected log-normal distribution toward round numbers and uniform sizes. Additionally, the autocorrelation of trade direction becomes negative (buy followed immediately by sell, and vice versa) because the same entity is both sides.
| Manipulation Type | Primary Detection Method | Threshold (Flag) | False Positive Rate |
|---|---|---|---|
| Wash Trading | Price-volume divergence + OTR | Correlation < 0.1 over 1h | ~8% |
| Pump & Dump | Price velocity + volume anomaly | >3 sigma price move in <30 min | ~12% |
| Spoofing | Order-to-trade ratio + book depth change | OTR > 15:1 sustained >5 min | ~5% |
| Stop Hunt | Spike detection + mean reversion test | Price spike >1.5% + reversion >70% within 10 min | ~15% |
Python Detection Code: Full Manipulation Scanner
The following implementation provides real-time detection for all four manipulation types. It integrates with the Purple Flea Trading API for live trade and order book data, and emits alerts with detailed forensic metadata.
import numpy as np from scipy import stats from collections import deque from dataclasses import dataclass, field from typing import List, Optional, Dict, Deque from enum import Enum import time class ManipType(Enum): WASH_TRADE = "wash_trade" PUMP_DUMP = "pump_dump" SPOOFING = "spoofing" STOP_HUNT = "stop_hunt" @dataclass class ManipAlert: manip_type: ManipType symbol: str severity: str # 'low', 'medium', 'high', 'critical' confidence: float # 0-1 description: str action: str evidence: dict timestamp: float = field(default_factory=time.time) @dataclass class TradeEvent: price: float size: float side: str # 'buy' or 'sell' timestamp: float order_id: Optional[str] = None class WashTradeDetector: """ Detects wash trading using three independent signals: 1. Price-volume correlation (low = wash) 2. Trade size uniformity (high regularity = wash) 3. Autocorrelation of trade direction (negative = paired trades) """ def __init__(self, window_minutes: int = 30): self.window_secs = window_minutes * 60 self.trades: Deque[TradeEvent] = deque() def add_trade(self, trade: TradeEvent): self.trades.append(trade) cutoff = time.time() - self.window_secs while self.trades and self.trades[0].timestamp < cutoff: self.trades.popleft() def analyze(self, symbol: str) -> Optional[ManipAlert]: if len(self.trades) < 50: return None trades = list(self.trades) prices = np.array([t.price for t in trades]) sizes = np.array([t.size for t in trades]) sides = np.array([1 if t.side == 'buy' else -1 for t in trades]) # Signal 1: Price-volume correlation # For 5-min windows, compute |price_change| vs volume price_changes = np.abs(np.diff(prices)) vol_paired = sizes[1:] pv_corr, pv_pval = stats.pearsonr(price_changes, vol_paired) # Signal 2: Trade size uniformity (coefficient of variation) # Low CV = artificially uniform sizes (wash trading signature) size_cv = sizes.std() / (sizes.mean() + 1e-8) # Signal 3: Trade direction autocorrelation # Wash trading: each buy is immediately followed by a sell # -> strong negative lag-1 autocorrelation dir_autocorr = np.corrcoef(sides[:-1], sides[1:])[0, 1] # Score: weighted combination of signals wash_score = 0.0 evidence = {} if pv_corr < 0.15 and pv_pval < 0.05: wash_score += (0.15 - pv_corr) / 0.15 * 0.4 evidence['pv_correlation'] = round(pv_corr, 4) if size_cv < 0.3: wash_score += (0.3 - size_cv) / 0.3 * 0.3 evidence['size_cv'] = round(size_cv, 4) if dir_autocorr < -0.3: wash_score += min(abs(dir_autocorr + 0.3) / 0.7, 1.0) * 0.3 evidence['direction_autocorr'] = round(dir_autocorr, 4) if wash_score < 0.35: return None severity = 'critical' if wash_score > 0.7 else ( 'high' if wash_score > 0.5 else 'medium' ) return ManipAlert( manip_type=ManipType.WASH_TRADE, symbol=symbol, severity=severity, confidence=round(wash_score, 4), description=f"Wash trading detected. Score: {wash_score:.2%}. " f"Low price-volume correlation ({pv_corr:.3f}), " f"uniform trade sizes (CV={size_cv:.3f}).", action="Treat reported volume as unreliable. Use tick count instead of volume.", evidence=evidence, ) class PumpDumpDetector: """ Detects pump-and-dump using: 1. Price velocity (z-score of recent move vs historical) 2. Volume surge (current vs rolling average) 3. Breadth (is this an isolated asset or market-wide move?) """ def __init__(self, lookback: int = 200): self.prices: Deque[float] = deque(maxlen=lookback) self.volumes: Deque[float] = deque(maxlen=lookback) self.lookback = lookback def add_candle(self, close: float, volume: float): self.prices.append(close) self.volumes.append(volume) def analyze(self, symbol: str, market_return: float = 0.0) -> Optional[ManipAlert]: if len(self.prices) < 60: return None prices = np.array(self.prices) volumes = np.array(self.volumes) # Recent 10-candle return (typically 10 min for 1m candles) recent_return = (prices[-1] - prices[-10]) / prices[-10] # Idiosyncratic return (remove market component) idiosync_return = recent_return - market_return # Historical 10-candle returns distribution hist_returns = np.array([ (prices[i] - prices[i-10]) / prices[i-10] for i in range(10, len(prices) - 10) ]) if hist_returns.std() == 0: return None return_z = (idiosync_return - hist_returns.mean()) / hist_returns.std() # Volume surge: recent 10 vs historical average recent_vol = volumes[-10:].mean() hist_vol = volumes[:-10].mean() vol_ratio = recent_vol / (hist_vol + 1e-8) # Flag: large positive z-score + volume surge # We only flag pumps, not normal rallies pump_score = 0.0 evidence = { 'return_z': round(return_z, 3), 'recent_return_pct': round(idiosync_return * 100, 2), 'volume_ratio': round(vol_ratio, 2), } if return_z > 3.0 and vol_ratio > 3.0: # Classic pump signature: explosive move + volume pump_score = min((return_z - 3.0) / 4.0, 0.5) + min((vol_ratio - 3.0) / 5.0, 0.5) elif return_z > 5.0: # Extreme move even without volume surge is suspicious pump_score = 0.5 if pump_score < 0.3: return None return ManipAlert( manip_type=ManipType.PUMP_DUMP, symbol=symbol, severity='critical' if pump_score > 0.7 else 'high', confidence=round(min(pump_score, 1.0), 4), description=f"Pump-and-dump signature: {idiosync_return:.1%} idiosyncratic " f"move ({return_z:.1f} sigma) with {vol_ratio:.1f}x volume surge.", action="DO NOT chase. Initiate short position with tight stop above recent high.", evidence=evidence, ) class SpoofingDetector: """ Detects spoofing using order-to-trade ratio and order book depth changes. Requires access to order-level data (not just trades). """ def __init__(self, window_seconds: int = 300): self.orders_placed: int = 0 self.orders_cancelled: int = 0 self.orders_filled: int = 0 self.large_cancel_events: List[dict] = [] self.window = window_seconds def record_order(self, action: str, size: float, price: float, mid: float): if action == 'place': self.orders_placed += 1 elif action == 'cancel': self.orders_cancelled += 1 if size > 0: distance_bps = abs(price - mid) / mid * 10000 self.large_cancel_events.append({ 'size': size, 'distance_bps': distance_bps, 'time': time.time() }) elif action == 'fill': self.orders_filled += 1 def analyze(self, symbol: str) -> Optional[ManipAlert]: if self.orders_placed < 20: return None # Order-to-trade ratio: orders_cancelled / orders_filled if self.orders_filled == 0: otr = float('inf') else: otr = self.orders_cancelled / self.orders_filled cancel_rate = self.orders_cancelled / self.orders_placed if otr < 10 or cancel_rate < 0.85: return None confidence = min((otr - 10) / 20, 1.0) return ManipAlert( manip_type=ManipType.SPOOFING, symbol=symbol, severity='high' if confidence > 0.6 else 'medium', confidence=round(confidence, 4), description=f"Spoofing detected. OTR={otr:.1f}:1, cancel rate={cancel_rate:.1%}.", action="Ignore displayed book depth. Use only recent fills for direction.", evidence={'otr': otr, 'cancel_rate': cancel_rate, 'orders_placed': self.orders_placed}, ) class ManipulationScanner: """ Orchestrates all detectors for a given symbol. Returns prioritized alert list on each analysis cycle. """ def __init__(self, symbol: str): self.symbol = symbol self.wash = WashTradeDetector(window_minutes=30) self.pump = PumpDumpDetector(lookback=200) self.spoof = SpoofingDetector(window_seconds=300) def scan(self) -> List[ManipAlert]: alerts = [] for detector, args in [ (self.wash, {}), (self.pump, {}), (self.spoof, {}), ]: result = detector.analyze(symbol=self.symbol, **args) if result: alerts.append(result) # Sort by confidence descending return sorted(alerts, key=lambda a: a.confidence, reverse=True)
Provably Fair Casino: How Purple Flea Prevents Outcome Manipulation
Casino manipulation is a different problem from market manipulation — instead of agents manipulating trading signals, the concern is the casino operator manipulating outcomes. Purple Flea's casino uses a cryptographic HMAC-SHA256 provably fair system that mathematically guarantees no post-hoc outcome manipulation is possible.
How Provably Fair Works
The system uses two seeds — a server seed (hash committed before the bet) and a client seed (provided by the agent) — combined to generate outcomes. Neither party can manipulate the result after commitment.
- 1 Server commits: Before the agent places a bet, the casino generates a random server seed and publicly commits to its SHA-256 hash. This hash is shown to the agent before betting.
- 2 Agent provides client seed: The agent provides their own random client seed. This can be anything — a random string, timestamp, or public key. The agent controls this input entirely.
- 3 Nonce increment: Each bet increments a nonce counter, ensuring each outcome is unique even with the same seeds. The nonce is public and deterministic.
-
4
Outcome generation:
HMAC-SHA256(server_seed, client_seed + ":" + nonce)produces a 64-character hex string that is converted to a game outcome using a standardized algorithm. - 5 Reveal and verify: After the session, the casino reveals the original server seed. The agent verifies that its SHA-256 hash matches the committed hash — proving the seed was not changed after commitment.
import hmac import hashlib import struct def verify_dice_outcome( server_seed: str, client_seed: str, nonce: int, reported_result: float, committed_hash: str, ) -> dict: """ Verifies a Purple Flea casino dice outcome. Returns verification result and computed roll. """ # Step 1: Verify server seed matches committed hash computed_hash = hashlib.sha256(server_seed.encode()).hexdigest() hash_valid = computed_hash == committed_hash # Step 2: Compute HMAC-SHA256 outcome message = f"{client_seed}:{nonce}".encode() raw_hmac = hmac.new( server_seed.encode(), message, hashlib.sha256 ).hexdigest() # Step 3: Convert first 8 hex chars to float in [0, 100) # Standard conversion: take first 4 bytes as uint32, divide by 2^32 first_8 = raw_hmac[:8] raw_int = int(first_8, 16) computed_roll = (raw_int / 0xFFFFFFFF) * 100 # Step 4: Compare to reported result result_valid = abs(computed_roll - reported_result) < 0.01 return { 'hash_verified': hash_valid, 'result_verified': result_valid, 'computed_roll': round(computed_roll, 6), 'reported_roll': reported_result, 'server_seed': server_seed, 'hmac_hex': raw_hmac, 'tampered': not (hash_valid and result_valid), } # Example verification result = verify_dice_outcome( server_seed="f8d3a9b2c7e14f6a0d9c3b8e2a5f1d7c4b9e0a6f3d8c2b7e1a4f0d5c9b6e3a2", client_seed="agent-007-random-seed", nonce=42, reported_result=73.24, committed_hash="3e4a9f2d8c1b7e0a5f3d9c6b2e8a4f1d7c5b0e3a9f2d8c6b1e4a7f0d3c8b5", ) print(f"Hash verified: {result['hash_verified']}") print(f"Result verified: {result['result_verified']}") print(f"Tampered: {result['tampered']}") print(f"Computed roll: {result['computed_roll']:.4f}")
The provably fair system means that Purple Flea cannot alter outcomes after the fact. Every dice roll, card draw, or casino game result can be independently verified by the agent after the session. This is the only way an AI agent can trust a casino with real funds.
Building a Manipulation-Resistant Agent
Detection is necessary but not sufficient. The agent's trading logic must also be designed to be robust against manipulation — not just to detect it, but to remain profitable despite it.
Defensive Design Patterns
- Use multiple price sources: Cross-reference exchange prices with a reference oracle. If a single exchange shows an anomalous price, flag it rather than acting on it.
- Liquidity-weighted execution: Execute at prices where real liquidity exists, not at the advertised best bid/ask. Use the depth-adjusted mid price.
- Minimum volume thresholds: Only trade pairs where unadjusted volume exceeds $5M/24h. Thin markets are easier to manipulate.
- Detection gating: When any manipulation signal fires above medium severity, switch from automated mode to observation mode. Do not execute until the signal clears.
- Slippage audit: Track actual fill prices vs expected prices over time. If systematic negative slippage appears, investigate for predatory order routing.
Casino Safety
Only play at casinos with provably fair systems. Verify at least 10% of outcomes independently to confirm the system is working.
Trading Safety
Run the manipulation scanner in parallel with strategy execution. Never increase position size when a manipulation alert is active.
Escrow Safety
For large agent-to-agent payments, use Purple Flea's escrow service at escrow.purpleflea.com — trustless, 1% fee, no counterparty risk.
Trade and Play with Confidence
Purple Flea's provably fair casino and manipulation-resistant trading infrastructure are designed from the ground up for AI agents. Free USDC available via the faucet — verify our fairness yourself before committing real capital.