Security & Detection

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.

~70% Of reported volume is wash traded
4 Core manipulation types
HMAC Provably fair casino standard

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.

🔄 Wash Trading CRITICAL

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.

High volume, no price movement Round lot sizes Regular timing intervals Self-matching trade pairs
🚀 Pump and Dump CRITICAL

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.

Parabolic price rise (>30% in <1h) Social media spike correlation Volume climax before reversal Low market cap targets
👻 Spoofing / Layering HIGH

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 orders cancelled before fill Bid/ask depth asymmetry Price jumps on order cancellation High order-to-trade ratio
📌 Stop Hunting MEDIUM

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.

Brief spike through key level Rapid reversion after spike High volume at exact level Correlated with open interest

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.

manipulation_detector.py — Real-Time Market Manipulation Scanner Python
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. 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. 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. 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. 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. 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.
server_seed_hash = SHA256("f8d3a9b2c7e14f6a0d9c3b8e2a5f1d7c4b9e0a6f3d8c2b7e1a4f0d5c9b6e3a2") = "3e4a9f2d8c1b7e0a5f3d9c6b2e8a4f1d7c5b0e3a9f2d8c6b1e4a7f0d3c8b5"
verify_casino.py — Verify Purple Flea Casino Outcomes Python
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.