mevMay 7, 2026

How to track MEV block builder revenue from private mempool trades. A literate programming dashboard that monitors balance changes across private mempool swaps and aggregates per-builder PnL statistics.

MEV Boost Relay Trade Profit Monitor: Tracking Builder Revenue from Private Mempool Trades

A swap executes on-chain. The priority fee is zero. There’s no tip to the miner in the obvious sense. And yet, the block builder who included that transaction just made money.

The profit isn’t in the fee field. It’s scattered across token balance changes in the transaction. Tracking it requires a different lens. Not gas costs, but flows. Beaconcha.in tracks validator rewards but doesn’t categorize private mempool transactions specifically. That’s the gap this dashboard fills.

If there’s any scope for improvement or change in focus, please submit a pull request!

In the spirit of literate programming, this article will have the code files linked in raw format. We’ll explore how to track MEV builder profits, aggregate per-builder statistics, and analyze the token-level PnL from private mempool transactions.

Repository: mev-boost-relay-trade-profit-monitor

Overview

MEV-Boost is a protocol that allows Ethereum validators to outsource block building to specialized builders who can extract maximum value from transactions. These builders often receive transactions through private mempools (like Flashbots), where they can see and order transactions before they’re public. This private mempool activity is also examined through the lens of Tornado Cash transaction attribution, where the same balance-change analysis applies. This tool demonstrates how to:

Architecture

The system consists of five main components:

1. Data Fetching Layer

The dataservice.py module handles all interactions with the Bitquery MEV Balance Tracker API. It queries the streaming GraphQL endpoint to retrieve DEX trades where builders have zero priority fees (indicating private mempool execution).

def fetch_transaction_balances(limit: int = 20000) -> dict:
    """
    Fetch the latest DEXTrades from the Bitquery streaming API.
    Returns the decoded JSON payload as a Python dictionary.
    """
    query = _build_query(limit)
    payload = json.dumps({"query": query, "variables": "{}"})

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {config.TOKEN}",
    }

    response = requests.post(BITQUERY_URL, headers=headers, data=payload, timeout=60)
    response.raise_for_status()
    return response.json()

The GraphQL query filters for trades with PriorityFeePerGas: 0, which indicates transactions executed through private mempools where builders receive payment through balance changes rather than priority fees. The query retrieves:

2. Builder Filtering

The filter.py module maintains a list of known MEV builder addresses and filters trades to only include those where at least one builder address appears in the transaction balance changes.

DEFAULT_ADDRESSES = [
    "0xf2f5c73fa04406b1995e397b55c24ab1f3ea726c",
    "0x036C9c0aaE7a8268F332bA968dac5963c6aDAca5",
    "0xf573d99385c05c23b24ed33de616ad16a43a0919",
    # ... more builder addresses
]

def filter_trades_by_addresses(
    data: dict, addresses: Optional[Iterable[str]] = None
) -> dict:
    """
    Filter DEXTrades to only include those where TokenBalance.Address
    matches the provided addresses.
    """
    filter_addresses = {addr.lower() for addr in (addresses or DEFAULT_ADDRESSES)}

    filtered_trades = []
    for trade in trades:
        balance_joins = trade.get("joinTransactionBalances", [])
        # Check if any TokenBalance.Address matches our filter addresses
        for balance_join in balance_joins:
            token_address = balance_join["TokenBalance"]["Address"]
            if token_address.lower() in filter_addresses:
                filtered_trades.append(trade)
                break

    return filtered_trades

This filtering ensures the dashboard focuses on trades where MEV builders are actively participating, making it easier to track their profit patterns.

3. Data Processing and Aggregation

The processing.py module contains the core logic for aggregating trade data into builder-level statistics.

Builder Statistics Calculation

The calculate_stats() function aggregates data across multiple dimensions:

def calculate_stats(data):
    """Calculate statistics from the DEXTrades data, organized by block builder."""
    # Structure: builder_address -> block_number -> block summary
    builder_blocks = defaultdict(lambda: defaultdict(lambda: {
        "block_number": "",
        "block_time": "",
        "total_profit_usd": 0.0,
        "total_balance_change": 0.0,
        "transaction_count": 0,
        "tokens": defaultdict(lambda: {
            "balance_change": 0.0,
            "profit_usd": 0.0
        }),
    }))

Aggregation dimensions:

  1. Per-builder totals: Total profit in USD, total balance changes, transaction counts, blocks built
  2. Per-block aggregation: Profit and balance changes grouped by block number
  3. Token-level PnL: Balance changes and profits broken down by token/currency
  4. Protocol usage: Which DEX protocols builders are using most
  5. Balance change reasons: Categorization by reason codes (transfers, swaps, etc.)

Trade Processing

The process_builder_trades() function transforms raw trade data into a template-friendly structure for drill-down views:

def process_builder_trades(trades, builder_address):
    """
    Transform raw trades for a specific builder into a template-friendly structure.
    """
    processed_trades = []
    for trade in trades:
        # Extract buy/sell information
        buy_info = trade_info.get("Buy", {})
        sell_info = trade_info.get("Sell", {})

        # Extract builder-specific balance changes
        builder_balance_changes = []
        for balance_join in balance_joins:
            if token_address.lower() == builder_address_lower:
                builder_balance_changes.append({
                    "currency_name": currency.get("Name"),
                    "currency_symbol": currency.get("Symbol"),
                    "balance_change": post_balance - pre_balance,
                    "profit_usd": post_balance_usd - pre_balance_usd,
                    "reason_code": token_balance.get("BalanceChangeReasonCode"),
                })

        processed_trades.append({
            "tx_hash": transaction.get("Hash"),
            "block_number": block.get("Number"),
            "buy": {...},
            "sell": {...},
            "dex_protocol": trade_info.get("Dex", {}).get("ProtocolName"),
            "balance_changes": builder_balance_changes,
        })

    return processed_trades

4. Web Application Layer

The app.py module provides a Flask-based web interface with intelligent caching.

Caching Strategy

The application implements a 5-minute cache to avoid excessive API calls:

_data_cache = None
_cache_timestamp = None
CACHE_TTL = 300  # Cache for 5 minutes (300 seconds)

def load_data(force_refresh=False, use_cache_only=False):
    """
    Fetch data directly from the API and filter by addresses.
    Uses caching to avoid repeated API calls.
    """
    # Check if we have valid cached data
    if not force_refresh and _data_cache is not None:
        age = time.time() - _cache_timestamp
        if age < CACHE_TTL:
            return _data_cache

    # Fetch fresh data
    data = fetch_transaction_balances()
    data = filter_trades_by_addresses(data)
    _data_cache = data
    _cache_timestamp = time.time()
    return data

Routes

The application exposes three main routes:

  1. / - Main dashboard showing aggregated builder statistics
  2. /refresh - Manually refresh the cache to get latest data
  3. /builder/<address> - Drill-down view showing all trades for a specific builder

The builder drill-down route uses use_cache_only=True to avoid making additional API calls, instead filtering the cached data:

@app.route("/builder/<address>")
def builder_trades(address):
    """Show individual trades for a specific builder. Only filters cached data."""
    data = load_data(use_cache_only=True)
    trades = get_builder_trades(data, address)
    processed_trades = process_builder_trades(trades, address)
    return render_template("builder_trades.html",
                         builder_address=address,
                         trades=processed_trades)

5. User Interface

The templates directory contains three HTML templates:

The UI is built with Bootstrap for responsive design and displays:

How Profit Tracking Works

The profit tracking mechanism relies on analyzing token balance changes in transactions:

1. Identifying Private Mempool Trades

Trades executed through private mempools (like Flashbots) have PriorityFeePerGas: 0 because builders receive payment through balance changes in the transaction itself, rather than through priority fees. The GraphQL query filters for these zero-priority-fee trades.

2. Balance Change Analysis

For each trade, the system examines joinTransactionBalances to find token balance changes for builder addresses:

# Calculate balance change (post-pre)
pre_balance = float(token_balance.get("PreBalance", 0) or 0)
post_balance = float(token_balance.get("PostBalance", 0) or 0)
balance_change = post_balance - pre_balance

# Calculate USD profit
pre_balance_usd = float(token_balance.get("PreBalanceInUSD", 0) or 0)
post_balance_usd = float(token_balance.get("PostBalanceInUSD", 0) or 0)
profit_usd = post_balance_usd - pre_balance_usd

3. Aggregation by Builder

The system groups trades by builder address and block number:

4. Reason Code Analysis

Balance changes are categorized by BalanceChangeReasonCode, which indicates the type of operation:

Usage Example

from dataservice import fetch_transaction_balances
from filter import filter_trades_by_addresses, DEFAULT_ADDRESSES
from processing import calculate_stats, process_builder_trades

# Fetch data from Bitquery API
data = fetch_transaction_balances(limit=20000)

# Filter to only include trades with known builder addresses
filtered_data = filter_trades_by_addresses(data, DEFAULT_ADDRESSES)

# Calculate aggregated statistics
stats = calculate_stats(filtered_data)

# Access builder summaries
for builder in stats["builder_summary"]:
    print(f"Builder: {builder['address']}")
    print(f"Total Profit: ${builder['total_profit_usd']:.2f}")
    print(f"Blocks Built: {builder['total_blocks']}")
    print(f"Transactions: {builder['total_transactions']}")

    # Token-level breakdown
    for token, token_data in builder['tokens'].items():
        print(f"  {token}: ${token_data['profit_usd']:.2f}")

# Get trades for a specific builder
from app import get_builder_trades
builder_trades = get_builder_trades(filtered_data, "0xf2f5c73fa04406b1995e397b55c24ab1f3ea726c")
processed = process_builder_trades(builder_trades, "0xf2f5c73fa04406b1995e397b55c24ab1f3ea726c")

Running the Dashboard

  1. Install dependencies:

    pip install -r requirements.txt
  2. Configure Bitquery token: Create config.py with your Bitquery OAuth token:

    TOKEN = "ey...your_bitquery_token..."
  3. Run the Flask application:

    python app.py
  4. Access the dashboard: Visit http://localhost:5000 to see the main dashboard with builder statistics.

  5. Refresh data: Visit /refresh to force a cache refresh and get the latest data.

  6. View builder details: Click on a builder address or visit /builder/<address> to see individual trades.

Key Insights

MEV Builder Profit Patterns

  1. Token Diversity: Builders profit across multiple tokens, not just ETH
  2. Block Concentration: Some builders build many blocks, others focus on high-value blocks
  3. Protocol Preferences: Different builders may prefer different DEX protocols
  4. Timing Patterns: Profit patterns may correlate with market conditions and gas prices

Private Mempool Economics

  1. Zero Priority Fees: Private mempool trades use balance changes instead of priority fees
  2. Builder Selection: Users choose builders based on execution quality and profit sharing
  3. Competitive Landscape: Multiple builders compete for block-building opportunities

Limitations

Conclusion

This tool demonstrates how to track MEV builder profits by analyzing token balance changes in DEX trades executed through private mempools. By aggregating data at multiple levels (builder, block, token), it provides insights into the economic incentives driving Ethereum’s block production landscape.

The codebase follows literate programming principles, with each module clearly documented and linked. The implementation is modular, allowing researchers and developers to:

Explore the code:

Repository: mev-boost-relay-trade-profit-monitor


Read more from Cryptogrammar