Documentation

Vectorblock MEV Shield turns the live Ethereum mempool into a derived MEV-risk feed: per-transaction cliff_score, wallet-cluster attribution, and an MCP server for agents. All examples below were run against the live endpoints; secrets are redacted.

no key requiredapi.vectorblock.iomcp.vectorblock.io/sse→ /mempool API deep-link

Getting Started

Access model

The public API is an open research preview. The api-server sets permissive CORS (allow_origins=["*"]) and has no auth middleware, no API-key check, and no micropayment gating in code — so read endpoints require no key or signup. (A paid / metered access tier is not wired up yet; when it lands it will be documented here with the exact header.)

The only API key anywhere in the stack is a server-side ANTHROPIC_API_KEY that powers the /api/v1/chat RAG endpoint. It is never sent by clients and never required to read data.

Quickstart (3 steps)

1. Access. None needed — go straight to a call.

2. First REST call. Fetch the live mempool overview:

curl
curl -s https://api.vectorblock.io/api/v1/mempool/stats
response (live)
{
  "alerts_last_60s": 18,
  "alerts_last_5m": 18,
  "buffer_size": 18,
  "bot_breakdown": { "unknown": 18 },
  "timestamp": 1784177067.77
}

3. Connect the MCP server. Add the remote SSE endpoint to any MCP-capable client:

mcp client config (json)
{
  "mcpServers": {
    "vectorblock-mempool": {
      "url": "https://mcp.vectorblock.io/sse"
    }
  }
}

Then call a tool — e.g. get_mempool_stats. Live output from the tool:

get_mempool_stats → (live)
{
  "status": "LIVE",
  "last_event_sec_ago": 0,
  "buffer_size": 11,
  "alerts_last_60s": 11,
  "alerts_last_5m": 11,
  "avg_gas_gwei": 22.5,
  "high_risk_pool_count": 0,
  "high_risk_pools": [],
  "active_addresses": 4,
  "bot_breakdown": { "unknown": 11 },
  "wallets_tracked": 15769,
  "data_source": "Ethereum mempool — PublicNode + Alchemy (~75% coverage)"
}
Status (as of this writing): the public MCP endpoint https://mcp.vectorblock.io/sse currently returns 502 (gateway down), so the remote SSE connection is unverified live. The tool output above was captured by invoking the server code directly against the live Redis feed. As a fallback you can run the server locally over stdio via Docker:
fallback: local stdio
{
  "mcpServers": {
    "vectorblock-mempool": {
      "command": "docker",
      "args": ["exec", "-i", "mev-mcp", "python", "mempool_mcp_server.py"]
    }
  }
}

Base URLs & rate limits

paramtype / defaultdescription
REST APIhttps://api.vectorblock.ioFastAPI api-server (topology). CORS open.
MCP (SSE)https://mcp.vectorblock.io/sseFastMCP server — 8 tools. Currently 502 (see note).
Web apphttps://vectorblock.ioNext.js dashboard + same-origin /api/* proxy routes.
Rate limits: there is no rate-limiting code in the api-server or web routes today, so no documented limit applies. Please be considerate on the SSE stream and polling loops; formal limits will appear here if introduced.

Concepts

Source of truth: eth-mempool-capture/CLAUDE.md and README.md.

cliff_score

A 0–1 MEV-risk score per pending transaction. It is a fixed-weight heuristic (probabilistic calibration is deferred), combining a reduced, defensible core:

formula (CLAUDE.md)
cliff_score = 0.40 × lob_cliff        (liquidity cliff from LOB bands)
            + 0.30 × order_imbalance   (OFI — order-flow imbalance)
            + 0.20 × gas_z_norm        (EIP-1559 gas Z-score)
            + 0.10 × nonce_signal      (RBF / gap / spam)

Example: a routine V2 swap scores 0.30; the busiest pool in the last window peaked at cliff_peak 0.55 (from /mempool/pools/ranked). An alert is published when cliff_score ≥ CLIFF_THRESHOLD.

lob_cliff

The dominant term (0.40). A liquidity cliff is a tick range where Uniswap V3 concentrated liquidity drops off sharply — crossing it causes outsized slippage. It is read from materialized uniswap.lob_bands (per-pool, per-block band severities) via a Vertica join, keyed on the swap's resolved pool.

Order Imbalance (OFI)

The 0.30 term. This is order-flow imbalance |buy_frac − 0.5| × 2 over a rolling window (0 = balanced, 1 = extreme one-sided). It is NOT VPIN: the earlier "VPIN" label was misleading and has been removed everywhere. In alerts the field is ofiImbalance (0 = all sell, 1 = all buy).

gas z-score

The 0.20 term. A rolling Z-score of effective_gas_price_gwei (EIP-1559), normalized 0–1 and capped at 3σ — a spike indicates a searcher bidding aggressively for inclusion.

nonce / RBF / spam signals

The 0.10 term, from a per-sender nonce tracker: rbf (same sender+nonce re-sent with higher gas), nonceGap (missing pending nonce — arb staging), and nonceSpam (many pending nonces — eviction flooding). Ethereum has no native replace-by-fee flag like Bitcoin; its RBF-equivalent is same-nonce replacement, which is exactly what these signals detect.

MEV categories & feed hygiene

Each tx is classified by its 4-byte selector into a mevCategory. The MEV feed carries only real MEV categories:

paramtype / defaultdescription
V3_SWAP / V2_SWAP / V3_MULTIHOPin feed ✅Uniswap V2/V3 swaps
UNIV_ROUTER / V3_ROUTER2 / 1INCH / 1INCH_V5in feed ✅Router / aggregator swaps
V3_MINT / V3_BURN / V3_COLLECTin feed ✅LP add / remove / collect (JIT)
ERC20filtered ❌token transfer / approve — no swap
neutralfiltered ❌selector not in the DEX map — often a plain ETH transfer
unknownfiltered ❌unrecognized call

neutral/ERC20/unknown are non-MEV noise and are dropped before the feed and the wallet graph (one filter cleans both). Extreme-spam floods go to a separate mev-spam channel, never the MEV feed.

Bot families / wallet clusters

Wallets that co-appear on the same pools within a short window are linked in a Neo4j graph; Louvain community detection groups them into operator clusters. Each cluster gets a heuristic bot_family: arb_searcher, jit_liquidity, sandwich, spam_bot, or unknown. Live example: 20 active clusters, families {arb_searcher, jit_liquidity, unknown}.

Coverage caveat

Capture currently runs on public nodes only (PublicNode + Alchemy), which sees roughly 70–85% of the public mempool. Premium sources (bloXroute, Infura) are not configured, so private order flow (Flashbots & other private relays) is invisible — those bundles never hit the public mempool. Treat scores as a lower bound on activity.

Shelved signals

Two signals ship in the code but are experimental — currently disabled behind flags and hidden from the UI, because they are non-functional without data they don't yet have:

paramtype / defaultdescription
Kyle's λENABLE_KYLE_LAMBDA=falseNeeds signed ERC20 trade sizes we don't decode yet → reads ~0 everywhere. Re-enable when ERC20 amount decoding lands.
Hawkes intensityENABLE_HAWKES=falseNeeds 48h of per-pool history it only thinly has. Re-enable after real history + a passing backtest.

When off, the worker emits kyleLambda, estimatedImpact, hawkesIntensity, hawkesPrediction as null (see the live alert in the API reference). The classes are retained, not deleted.

Contrast with mempool.space

mempool.space is descriptive telemetry read straight from a Bitcoin node (getmempoolinfo / getrawmempool + light tx-type/RBF heuristics) — it reports what the node already knows. Vectorblock is derived Ethereum MEV-risk scoring (cliff_score = OFI × LOB-depth × gas-z + nonce) computed from observations the node does not hand you. That is why feed hygiene and coverage matter here in a way they don't for a fee explorer.

MCP Tools

Source: eth-mempool-capture/app/mempool_mcp_server.py (FastMCP). Endpoint: https://mcp.vectorblock.io/sse. Connect with the config in Quickstart. Eight tools read the live Redis alert buffer + Neo4j wallet graph. All responses below are live captures.

The public SSE endpoint is currently 502; examples were captured by running the server code against the live feed. get_hawkes_intensity is experimental — currently disabled in spirit (Hawkes is shelved); it still computes a crude self-contained intensity from buffer timestamps and should not be relied on.

get_wallet_reputation

Is this address a known MEV bot? Queries Neo4j + the live buffer.

paramtype / defaultdescription
addressstringEthereum wallet address (0x…, 42 chars)
response (live)
{
  "address": "0xbcde93bf363ad3f650289862539fcd325878a949",
  "bot_type": "unknown",
  "confidence": 0.0,
  "risk_level": "LOW",
  "cluster_id": null,
  "cluster_size": 74,
  "tx_count": 56,
  "last_seen_sec": null,
  "summary": "No bot signals for 0xbcde93bf…"
}

get_pool_risk

Aggregate MEV risk for a pool: cliff, OFI, active bots, recommended route.

paramtype / defaultdescription
pool_addressstringUniswap pool/router address (0x…)
response shape (from source)
{
  "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
  "alert_count": 12,
  "cliff_peak": 0.55,
  "cliff_avg": 0.34,
  "risk_level": "LOW",
  "ofi_imbalance": 1.0,
  "order_flow": "BUY",
  "hawkes_intensity": 0.0,      // shelved → 0
  "next_attack_sec": null,      // shelved
  "active_bots": 0,
  "recommended_route": "public",
  "reason": "public submission safe"
}

get_active_bot_clusters

Operator clusters from the Neo4j graph (Louvain over CO_APPEARED edges).

paramtype / defaultdescription
minutesint = 60Look-back window
response (live, trimmed)
{
  "window_minutes": 60,
  "total_clusters": 20,
  "total_wallets": 3021,
  "by_family": { "arb_searcher": 860, "unknown": 1954, "jit_liquidity": 207 },
  "clusters": [
    { "cluster_id": "3939", "bot_family": "arb_searcher", "size": 650, "active_members": 171 },
    { "cluster_id": "1830", "bot_family": "unknown",      "size": 1130, "active_members": 184 }
  ],
  "summary": "20 clusters, 3021 wallets — 860 arb searcher wallets, 207 jit liquidity wallets"
}

get_recent_alerts

Last N scored alerts from the live buffer, with filters.

paramtype / defaultdescription
limitint = 10Max alerts (capped at 50)
min_clifffloat = 0.3Minimum cliff score
pool_addressstring? = nullFilter to a pool
bot_typestring? = nullFilter by bot type
get_recent_alerts(limit=1) → (live)
{
  "count": 1,
  "buffer": 11,
  "alerts": [{
    "tx": "0x1a1c713d053a2e4ccf…",
    "pool": "0x7a250d56…",
    "category": "V2_SWAP",
    "cliff": 0.3,
    "risk": "low",
    "bot_type": "unknown",
    "confidence": 0.0,
    "cluster": null,
    "hawkes": null,          // shelved
    "impact_pct": null,      // shelved (Kyle)
    "gas_gwei": 0.001,
    "value_usd": 10.5,
    "age_sec": 0
  }],
  "stats": { "avg_cliff": 0.3, "high_risk": 0, "bots_detected": 0 }
}

get_pool_ofi

Current Order-Flow Imbalance for a pool (renamed from the old get_pool_vpin).

paramtype / defaultdescription
pool_addressstringPool address (0x…)
window_secondsint = 30Rolling window
get_pool_ofi → (live)
{
  "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
  "window_sec": 30,
  "ofi_imbalance": 1.0,
  "buy_pct": "100.0%",
  "sell_pct": "0.0%",
  "trade_count": 11,
  "volume_eth": 0.4068,
  "signal": "STRONG BUY — sandwich bots likely"
}
The deployed container still exposes the legacy name get_pool_vpin (returning vpin_imbalance) until the OFI rename ships. Source and this doc use get_pool_ofi / ofi_imbalance.

get_hawkes_intensity experimental — disabled

Attack-cluster intensity + time-to-next estimate. Hawkes is shelved; do not rely on this.

paramtype / defaultdescription
pool_addressstringPool address (0x…)
get_hawkes_intensity → (live, crude heuristic)
{
  "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
  "intensity": 1.0,
  "level": "CRITICAL",
  "prediction_sec": 0.2,
  "obs_count": 11,
  "narrative": "CRITICAL — cluster active, next attack ~0s"
}

check_transaction_safety

Private-vs-public relay recommendation for a swap. Use before swaps > $1,000.

paramtype / defaultdescription
pool_addressstringTarget pool (0x…)
value_ethfloatSwap size in ETH
from_addressstring? = nullSender — checks if known bot
check_transaction_safety(pool, 5.0) → (live)
{
  "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
  "value_eth": 5.0,
  "route": "public",
  "confidence": 0.07,
  "recommendation": "✅  PUBLIC SAFE (7% conf) — risk=0.42 — public submission safe",
  "cliff_score": 0.55,
  "hawkes_intensity": 0,
  "ofi_imbalance": 1.0,
  "active_bots": 0,
  "from_is_known_bot": false,
  "estimated_impact_pct": 0.89,
  "estimated_mev_loss_usd": 110.62,
  "private_relay": "https://protect.flashbots.net"
}

get_mempool_stats

Overall mempool activity — alert rates, gas, active bots, coverage. No params.

get_mempool_stats → (live)
{
  "status": "LIVE",
  "last_event_sec_ago": 0,
  "buffer_size": 11,
  "alerts_last_60s": 11,
  "alerts_last_5m": 11,
  "avg_gas_gwei": 22.5,
  "high_risk_pool_count": 0,
  "high_risk_pools": [],
  "active_addresses": 4,
  "bot_breakdown": { "unknown": 11 },
  "wallets_tracked": 15769,
  "data_source": "Ethereum mempool — PublicNode + Alchemy (~75% coverage)"
}

API Reference

REST + SSE from the api-server (api.vectorblock.io) and the web app's own same-origin routes. Every example below was run live; secrets redacted (there are none in these payloads).

Mempool endpoints

GET/api/v1/mempool/recent
paramtype / defaultdescription
limitint = 50 (1–200)Max alerts returned
min_clifffloat = 0.3Minimum cliff score
poolstring? = nullFilter to a pool address
curl
curl -s "https://api.vectorblock.io/api/v1/mempool/recent?limit=1&min_cliff=0.3"
response (live — note ofiImbalance; hawkes/kyle null)
{
  "count": 1,
  "buffer_size": 33,
  "alerts": [{
    "id": "0x518397835e1c048603b1ecc7710ce62306f9013badf969615b5bebaf0fd5d0c9",
    "type": "swap",
    "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
    "value": 12.5,
    "gasPrice": 0.01,
    "size": 0.005,
    "mevRisk": "low",
    "cliffScore": 0.3,
    "mevCategory": "V2_SWAP",
    "timestamp": 1784177127876,
    "from": "0xbcde93bf363ad3f650289862539fcd325878a949",
    "source": "txpool_poll",
    "nonceSignal": 0.0, "rbf": false, "nonceGap": false, "nonceSpam": false,
    "is_buy": true, "decode_method": "selector_name",
    "ofiImbalance": 1.0, "ofi_imbalance": 1.0,
    "hawkesIntensity": null, "hawkesPrediction": null,
    "kyleLambda": null, "estimatedImpact": null,
    "fpBotType": "unknown", "fpConfidence": 0.0, "fpClusterId": null
  }]
}
GET/api/v1/mempool/alerts (SSE)

Server-Sent Events stream of scored alerts. Empty data: {} lines are keep-alive heartbeats (ignored by EventSource); real events carry the same object shape as /recent.

paramtype / defaultdescription
min_clifffloat = 0.3Minimum cliff score to stream
curl (stream)
curl -sN "https://api.vectorblock.io/api/v1/mempool/alerts?min_cliff=0.3"
stream (live)
data: {}

data: {"id": "0x63d5c7…", "type": "swap", "pool": "0x7a250d56…",
       "cliffScore": 0.3, "mevCategory": "V2_SWAP",
       "ofiImbalance": 1.0, "hawkesIntensity": null, "kyleLambda": null,
       "fpBotType": "unknown", "timestamp": 1784177138493}
GET/api/v1/mempool/stats
curl + response (live)
$ curl -s https://api.vectorblock.io/api/v1/mempool/stats
{
  "alerts_last_60s": 18,
  "alerts_last_5m": 18,
  "buffer_size": 18,
  "bot_breakdown": { "unknown": 18 },
  "timestamp": 1784177067.77
}
GET/api/v1/mempool/pools/ranked
paramtype / defaultdescription
limitint = 10Number of pools by cliff peak
response (live)
{
  "pools": [
    { "pool": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", "pair": "", "alert_count": 26, "cliff_peak": 0.55 },
    { "pool": "0xe592427a0aece92de3edee1f18e0157c05861564", "pair": "", "alert_count": 4,  "cliff_peak": 0.3 }
  ]
}

Community / reputation

GET/api/v1/community/active
paramtype / defaultdescription
minutesint = 60Look-back window for active wallets
response (live, trimmed)
{
  "window_minutes": 60,
  "active_communities": 20,
  "communities": [
    { "cluster_id": "5443", "bot_family": "arb_searcher", "size": 352,
      "active_members": 158, "last_activity": 1784176246.554 }
  ]
}
GET/api/v1/community/{cluster_id}
curl + response (live, trimmed)
$ curl -s https://api.vectorblock.io/api/v1/community/5443
{
  "cluster_id": "5443",
  "bot_family": "arb_searcher",
  "size": 352,
  "members": [
    { "address": "0xe7708e0562622f168ecb8b9c652018e276893236",
      "bot_type": "unknown", "confidence": 0.0, "tx_count": 54 }
  ],
  "active_pools": [],
  "avg_confidence": 0.0
}
404 with {"detail":"Community <id> not found"} if the cluster has no members in the graph.
GET/api/v1/wallet/{address}/reputation
response (live)
{
  "address": "0xe5a665b3c6ca4a3ae020792fd9631dccfb51ba6b",
  "bot_type": "unknown", "confidence": 0.0,
  "basis": "mempool_signals", "confirmed_onchain": false,
  "signals": [], "tx_count": 0, "unique_pools": 0,
  "active_pools": [], "cluster_id": null, "cluster_size": null,
  "data_window_hours": 168
}
This REST route is a stub — it always returns unknown. For real reputation use the MCP get_wallet_reputation tool (queries Neo4j + buffer).

Pools & LOB

GET/api/v1/pools
response (live, trimmed)
{
  "pools": [
    { "pool_address": "0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa",
      "token0": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",
      "token1": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
      "fee": 100, "tick_spacing": 1 }
  ]
}
GET/api/v1/lob/{pool_address}
response (live, trimmed)
{
  "pool_address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
  "block_number": 25542926,
  "current_tick": 200731,
  "active_liquidity": "14138374530141853205",
  "sqrt_price_x96": "1809240231296221848372330688219320",
  "ask_bands": [ { "band_no": 1, "band_start_tick": 200... } ],
  "bid_bands": [ ... ]
}

Web app routes (same-origin proxy)

The Next.js app exposes convenience routes under https://vectorblock.io/api/* that wrap the backend and add a { success, data, timestamp } envelope.

GET/api/pools
response (live, trimmed)
{
  "success": true,
  "data": [
    { "pool_address": "0x11b815efb8f581194ae79006d24e0d814b7697f6",
      "token0_symbol": "WETH", "token1_symbol": "USDT", "fee_tier": 500,
      "current_tick": -195840, "current_price": 2512.45,
      "total_value_locked_usd": 125000000 }
  ],
  "timestamp": 1784177187000
}
GET/api/lob?pool=WETH_USDT_005
response (live, trimmed)
{
  "success": true,
  "data": { "pool": { "pool_address": "0x11b815efb8f581194ae79006d24e0d814b7697f6",
                       "token0_symbol": "WETH", "token1_symbol": "USDT",
                       "fee_tier": 500, "current_tick": -195840,
                       "current_price": 2513.80, "total_value_locked_usd": 129856750 } },
  "timestamp": 1784177187000
}
POST/api/impact
response (live — intentionally unavailable, HTTP 503)
{
  "success": false,
  "error": "Trade impact calculation is unavailable (no verified slippage engine wired up).",
  "timestamp": 1784177187429
}
/api/impact returns 503 by design: there is no verified slippage engine wired up, so the route refuses rather than surface wrong numbers.
POST/api/chat

Proxies POST /api/v1/chat (RAG over the datasets). Body: { "query": "..." }. Availability depends on a server-side ANTHROPIC_API_KEY; check GET /api/v1/chat/status.

curl
curl -s -X POST https://vectorblock.io/api/chat \
  -H 'content-type: application/json' \
  -d '{"query":"which pools have the steepest liquidity cliffs?"}'
Unverified: the chat response body was not captured live (depends on server-side key + model availability). Shape: { answer, sources?, ... } — labeled unverified rather than fabricated.
GET/api/mempool/alerts (SSE proxy)

Same-origin SSE proxy the dashboard uses; forwards /api/v1/mempool/alerts from the backend. Same event shape as the backend stream above.