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
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():,}")
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
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})
Hourly coverage trends¶
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
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})