MEV Boost Relay Trade Profit Monitor: Tracking Builder Revenue from Private Mempool Trades
How do MEV block builders profit from private mempool trades? This dashboard monitors live MEV-Boost builder income by tracking balance changes across swaps executed through private mempools. I believe beaconcha.in tracks validator rewards but does not categorize or focus on private mempool transactions in specific.
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 tool demonstrates how to:
- Track builder profits by monitoring balance changes across DEX trades
- Aggregate per-builder statistics including total profit, blocks built, and token-level PnL
- Filter trades by builder addresses to focus on specific MEV actors
- Analyze protocol usage and balance-change reason codes
- Provide drill-down views of individual trades for detailed analysis
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:
- Transaction metadata: Hash, block number, timestamp, gas costs
- Trade details: Buy/sell amounts, currencies, DEX protocol
- Token balance changes: Pre/post balances, balance change reason codes, USD values
- Fee information: Burnt fees, miner rewards, gas prices
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:
- Per-builder totals: Total profit in USD, total balance changes, transaction counts, blocks built
- Per-block aggregation: Profit and balance changes grouped by block number
- Token-level PnL: Balance changes and profits broken down by token/currency
- Protocol usage: Which DEX protocols builders are using most
- 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:
/- Main dashboard showing aggregated builder statistics/refresh- Manually refresh the cache to get latest data/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:
dashboard.html- Main dashboard displaying builder summary statisticsbuilder_trades.html- Detailed view of individual trades for a specific buildererror.html- Error handling template
The UI is built with Bootstrap for responsive design and displays:
- Builder summary table: Total profit, balance changes, transaction counts, blocks built
- Token-level breakdown: PnL by token/currency for each builder
- Protocol usage statistics: Which DEX protocols are most used
- Trade-level details: Individual transaction cards with buy/sell information and balance changes

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:
- Per-block aggregation: Tracks profit and balance changes for each block a builder participates in
- Cross-block totals: Sums up all profits across all blocks for each builder
- Token-level tracking: Maintains separate PnL calculations for each token/currency
4. Reason Code Analysis
Balance changes are categorized by BalanceChangeReasonCode, which indicates the type of operation:
- Transfers
- Swaps
- Protocol-specific operations
- Other balance-affecting operations
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
-
Install dependencies:
pip install -r requirements.txt -
Configure Bitquery token: Create
config.pywith your Bitquery OAuth token:TOKEN = "ey...your_bitquery_token..." -
Run the Flask application:
python app.py -
Access the dashboard: Visit
http://localhost:5000to see the main dashboard with builder statistics. -
Refresh data: Visit
/refreshto force a cache refresh and get the latest data. -
View builder details: Click on a builder address or visit
/builder/<address>to see individual trades.
Key Insights
MEV Builder Profit Patterns
- Token Diversity: Builders profit across multiple tokens, not just ETH
- Block Concentration: Some builders build many blocks, others focus on high-value blocks
- Protocol Preferences: Different builders may prefer different DEX protocols
- Timing Patterns: Profit patterns may correlate with market conditions and gas prices
Private Mempool Economics
- Zero Priority Fees: Private mempool trades use balance changes instead of priority fees
- Builder Selection: Users choose builders based on execution quality and profit sharing
- Competitive Landscape: Multiple builders compete for block-building opportunities
Limitations
- Address Coverage: Only tracks known builder addresses in
DEFAULT_ADDRESSES - Balance Change Accuracy: USD values depend on Bitquery's price feeds
- Private Mempool Detection: Relies on
PriorityFeePerGas: 0heuristic, which may not catch all private mempool trades
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:
- Extend builder address lists to track additional MEV actors
- Add new aggregation dimensions for different analysis perspectives
- Integrate with other data sources for comprehensive MEV analysis
- Build custom visualizations using the processed data structures
Explore the code:
app.py- Flask web application with cachingprocessing.py- Data aggregation and statisticsdataservice.py- Bitquery API clientfilter.py- Builder address filtering
Repository: mev-boost-relay-trade-profit-monitor