Mempool visibility - 2026-02-02
Analysis of transaction visibility in the public mempool before block inclusion on Ethereum mainnet.
Methodology: A transaction is counted as "seen in mempool" only if it was observed by our sentries before the slot start time of the block that included it. This corrects for transactions that appear in the mempool after block propagation.
Show code
display_sql("mempool_availability", target_date)
View query
Show code
df = load_parquet("mempool_availability", target_date)
df["tx_type_label"] = df["tx_type"].map(TX_TYPE_LABELS)
df["coverage_pct"] = df["seen_before_slot"] / df["total_txs"] * 100
# Calculate never seen (truly private)
df["never_seen"] = df["total_txs"] - df["seen_before_slot"] - df["seen_after_slot"]
# Extract p50 age from percentiles array (index 0)
df["p50_age_ms"] = df["age_percentiles_ms"].apply(lambda x: x[0] if x is not None and len(x) > 0 else np.nan)
df["p50_age_s"] = df["p50_age_ms"] / 1000
# Add hour column for time-series aggregation
df["hour"] = df["slot_start_date_time"].dt.floor("h")
total = df["total_txs"].sum()
before = df["seen_before_slot"].sum()
after = df["seen_after_slot"].sum()
never = total - before - after
print(f"Loaded {len(df):,} slot/type rows")
print(f"Slots: {df['slot'].nunique():,}")
print(f"Total transactions: {total:,}")
print(f" Seen before slot: {before:,} ({100*before/total:.1f}%)")
print(f" Seen after slot: {after:,} ({100*after/total:.1f}%)")
print(f" Never seen: {never:,} ({100*never/total:.1f}%)")
Coverage by transaction type¶
Percentage of transactions seen in the public mempool before the slot they were included in. Low coverage indicates private or MEV transactions that bypass the public mempool or are submitted just-in-time.
Show code
# Aggregate by type
df_summary = df.groupby(["tx_type", "tx_type_label"]).agg({
"total_txs": "sum",
"seen_before_slot": "sum",
"seen_after_slot": "sum",
}).reset_index()
df_summary["never_seen"] = df_summary["total_txs"] - df_summary["seen_before_slot"] - df_summary["seen_after_slot"]
df_summary["before_pct"] = df_summary["seen_before_slot"] / df_summary["total_txs"] * 100
df_summary["after_pct"] = df_summary["seen_after_slot"] / df_summary["total_txs"] * 100
df_summary["never_pct"] = df_summary["never_seen"] / df_summary["total_txs"] * 100
# Display summary table
summary_display = df_summary[["tx_type_label", "total_txs", "before_pct", "after_pct", "never_pct"]].copy()
summary_display.columns = ["Type", "Total", "Before slot %", "After slot %", "Never seen %"]
for col in summary_display.columns[2:]:
summary_display[col] = summary_display[col].round(1)
summary_display
Show code
# Coverage stacked bar chart showing before/after/never breakdown
fig = go.Figure()
fig.add_trace(go.Bar(
x=df_summary["tx_type_label"],
y=df_summary["before_pct"],
name="Before slot (public)",
marker_color="#27ae60",
text=df_summary["before_pct"].round(1),
textposition="inside",
))
fig.add_trace(go.Bar(
x=df_summary["tx_type_label"],
y=df_summary["after_pct"],
name="After slot (propagated)",
marker_color="#3498db",
text=df_summary["after_pct"].round(1),
textposition="inside",
))
fig.add_trace(go.Bar(
x=df_summary["tx_type_label"],
y=df_summary["never_pct"],
name="Never seen (private)",
marker_color="#95a5a6",
text=df_summary["never_pct"].round(1),
textposition="inside",
))
fig.update_traces(texttemplate="%{text:.1f}%")
fig.update_layout(
barmode="stack",
margin=dict(l=60, r=30, t=30, b=60),
xaxis=dict(title="Transaction type"),
yaxis=dict(title="Percentage", range=[0, 105]),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
height=400,
)
fig.show(config={"responsive": True})
Hourly coverage trends¶
Mempool visibility percentage over time for each transaction type.
Show code
# Aggregate to hourly for time-series
df_hourly = df.groupby(["hour", "tx_type", "tx_type_label"]).agg({
"total_txs": "sum",
"seen_before_slot": "sum",
"seen_after_slot": "sum",
}).reset_index()
df_hourly["coverage_pct"] = df_hourly["seen_before_slot"] / df_hourly["total_txs"] * 100
fig = px.line(
df_hourly,
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": "Seen before slot (%)", "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 - 3-way breakdown
df_volume = df.groupby("hour").agg({
"total_txs": "sum",
"seen_before_slot": "sum",
"seen_after_slot": "sum",
}).reset_index()
df_volume["never_seen"] = df_volume["total_txs"] - df_volume["seen_before_slot"] - df_volume["seen_after_slot"]
fig = go.Figure()
fig.add_trace(go.Bar(
x=df_volume["hour"],
y=df_volume["seen_before_slot"],
name="Before slot (public)",
marker_color="#27ae60",
))
fig.add_trace(go.Bar(
x=df_volume["hour"],
y=df_volume["seen_after_slot"],
name="After slot (propagated)",
marker_color="#3498db",
))
fig.add_trace(go.Bar(
x=df_volume["hour"],
y=df_volume["never_seen"],
name="Never seen (private)",
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 aggregated data
df_pivot = df_hourly.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