Fri, Jan 9, 2026 Latest

Mempool visibility

Analysis of transaction visibility in the public mempool before block inclusion on Ethereum mainnet.

Show code
display_sql("mempool_coverage", target_date)
View query
SELECT
    toStartOfHour(slot_start_date_time) AS hour,
    type AS tx_type,
    count() AS total_txs,
    countIf(hash GLOBAL IN (
        SELECT DISTINCT hash
        FROM mempool_transaction
        WHERE meta_network_name = 'mainnet'
          AND event_date_time >= '2026-01-09'::date - INTERVAL 1 HOUR
          AND event_date_time < '2026-01-09'::date + INTERVAL 1 DAY
    )) AS seen_in_mempool
FROM canonical_beacon_block_execution_transaction
WHERE meta_network_name = 'mainnet'
  AND slot_start_date_time >= '2026-01-09' AND slot_start_date_time < '2026-01-09'::date + INTERVAL 1 DAY
GROUP BY hour, tx_type
ORDER BY hour, tx_type
Show code
df = load_parquet("mempool_coverage", target_date)
df["tx_type_label"] = df["tx_type"].map(TX_TYPE_LABELS)
df["coverage_pct"] = df["seen_in_mempool"] / df["total_txs"] * 100

print(f"Loaded {len(df):,} hour/type rows")
print(f"Hours: {df['hour'].nunique():,}")
print(f"Total transactions: {df['total_txs'].sum():,}")
Loaded 120 hour/type rows
Hours: 24
Total transactions: 2,086,825

Coverage by transaction type

Summary of how many transactions were seen in the public mempool before block inclusion. Low coverage indicates private or MEV transactions that bypass the public mempool.

Show code
# Aggregate by type
df_summary = df.groupby(["tx_type", "tx_type_label"]).agg({
    "total_txs": "sum",
    "seen_in_mempool": "sum",
}).reset_index()
df_summary["coverage_pct"] = df_summary["seen_in_mempool"] / df_summary["total_txs"] * 100

# Display summary table
summary_display = df_summary[["tx_type_label", "total_txs", "seen_in_mempool", "coverage_pct"]].copy()
summary_display.columns = ["Type", "Total", "Seen", "Coverage %"]
summary_display["Coverage %"] = summary_display["Coverage %"].round(1)
summary_display
Type Total Seen Coverage %
0 Legacy 412217 328456 79.7
1 Access list 2013 597 29.7
2 EIP-1559 1646206 850201 51.6
3 Blob 15341 15335 100.0
4 EIP-7702 11048 4839 43.8
Show code
# Coverage bar chart
fig = px.bar(
    df_summary,
    x="tx_type_label",
    y="coverage_pct",
    color="tx_type",
    color_discrete_map=TX_TYPE_COLORS,
    labels={"tx_type_label": "Transaction type", "coverage_pct": "Mempool visibility (%)"},
    text="coverage_pct",
)
fig.update_traces(texttemplate="%{text:.1f}%", textposition="outside", showlegend=False)
fig.update_layout(
    margin=dict(l=60, r=30, t=30, b=60),
    yaxis=dict(range=[0, 105]),
    height=400,
)
fig.show(config={"responsive": True})

Mempool visibility percentage over time for each transaction type. Blob transactions (type 3) typically have the highest visibility since they propagate through the public network.

Show code
# Data is already hourly from the query
fig = px.line(
    df,
    x="hour",
    y="coverage_pct",
    color="tx_type_label",
    color_discrete_map={v: TX_TYPE_COLORS[k] for k, v in TX_TYPE_LABELS.items()},
    labels={"hour": "Time", "coverage_pct": "Mempool visibility (%)", "tx_type_label": "Type"},
    markers=True,
)
fig.update_layout(
    margin=dict(l=60, r=30, t=30, b=60),
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
    height=400,
)
fig.show(config={"responsive": True})

Transaction volume over time

Hourly transaction counts split by public (seen in mempool) vs private (not seen). The private portion represents MEV bundles and other transactions submitted directly to builders.

Show code
# Aggregate across types by hour (already hourly data)
df_volume = df.groupby("hour").agg({
    "total_txs": "sum",
    "seen_in_mempool": "sum",
}).reset_index()
df_volume["private_txs"] = df_volume["total_txs"] - df_volume["seen_in_mempool"]

fig = go.Figure()
fig.add_trace(go.Bar(
    x=df_volume["hour"],
    y=df_volume["seen_in_mempool"],
    name="Public (seen in mempool)",
    marker_color="#3498db",
))
fig.add_trace(go.Bar(
    x=df_volume["hour"],
    y=df_volume["private_txs"],
    name="Private (not seen)",
    marker_color="#95a5a6",
))
fig.update_layout(
    barmode="stack",
    margin=dict(l=60, r=30, t=30, b=60),
    xaxis=dict(title="Time"),
    yaxis=dict(title="Transaction count"),
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
    height=400,
)
fig.show(config={"responsive": True})

Coverage heatmap

Heatmap showing mempool visibility over time for each transaction type. Darker colors indicate higher coverage (more transactions seen in the public mempool).

Show code
# Pivot for heatmap using hourly data directly
df_pivot = df.pivot(index="tx_type_label", columns="hour", values="coverage_pct").fillna(0)

fig = go.Figure(
    data=go.Heatmap(
        z=df_pivot.values,
        x=df_pivot.columns,
        y=df_pivot.index,
        colorscale="Greens",
        colorbar=dict(title=dict(text="Coverage %", side="right")),
    )
)
fig.update_layout(
    margin=dict(l=100, r=30, t=30, b=60),
    xaxis=dict(title="Time"),
    yaxis=dict(title="Transaction type"),
    height=300,
)
fig.show(config={"responsive": True})

Sentry coverage

How much of the canonical transaction set each mempool observer (sentry) captured. Higher coverage indicates better mempool visibility from that observation point.

Show code
display_sql("sentry_coverage", target_date)
View query
WITH canonical_hashes AS (
    SELECT DISTINCT hash
    FROM canonical_beacon_block_execution_transaction
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-09' AND slot_start_date_time < '2026-01-09'::date + INTERVAL 1 DAY
),
total_canonical AS (
    SELECT count() AS total FROM canonical_hashes
)
SELECT
    meta_client_name AS sentry,
    count(DISTINCT hash) AS txs_seen,
    round(count(DISTINCT hash) * 100.0 / (SELECT total FROM total_canonical), 2) AS coverage_pct
FROM mempool_transaction
WHERE meta_network_name = 'mainnet'
  AND event_date_time >= '2026-01-09'::date - INTERVAL 1 HOUR
  AND event_date_time < '2026-01-09'::date + INTERVAL 1 DAY
  AND hash GLOBAL IN (SELECT hash FROM canonical_hashes)
GROUP BY meta_client_name
ORDER BY txs_seen DESC
Show code
df_sentry = load_parquet("sentry_coverage", target_date)

# Shorten sentry names for display
df_sentry["sentry_short"] = df_sentry["sentry"].str.replace("ethpandaops/mainnet/", "")

fig = px.bar(
    df_sentry.head(15),
    x="coverage_pct",
    y="sentry_short",
    orientation="h",
    labels={"coverage_pct": "Coverage (%)", "sentry_short": "Sentry"},
    text="coverage_pct",
)
fig.update_traces(texttemplate="%{text:.1f}%", textposition="outside")
fig.update_layout(
    margin=dict(l=250, r=60, t=30, b=60),
    xaxis=dict(range=[0, 105]),
    yaxis=dict(autorange="reversed"),
    height=500,
)
fig.show(config={"responsive": True})