Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions benchmarks/bridge-execution-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/bridge-
# are documented in the methodology until they accrue enough samples to panel.
metric_panels:
- id: fee
label: Realized fee
metric: avg by (bridge) (ocb:bridge_fees_usd:avg_7d{region="eu-west"})
label: Realized fee (bps of ticket)
metric: avg by (bridge) (ocb:bridge_exec_realized_fee_bps:avg_7d{region="eu-west"})
label_key: bridge
unit: usd
unit: bps
higher_is_better: false
description: "Amount sent minus amount actually received on the destination chain, in USD, averaged over the last 7 days of executions. The fee a user really paid, read on-chain rather than from the quote."
description: "Ticket minus the value that actually landed on the destination chain, in basis points of the ticket so the $3 and $30 executions compare, averaged over the last 7 days of settled executions whose destination balance was read on-chain."
- id: slippage
label: Slippage vs quote
metric: avg by (bridge) (ocb:bridge_quote_slippage_usd:mean_7d{region="eu-west"})
Expand Down
33 changes: 14 additions & 19 deletions benchmarks/bridge-realized-cost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ slug: bridge-realized-cost
number: "263"
title: "Bridge slippage vs quote: what actually settles versus what was quoted"
seo_title: "Bridge execution slippage 2026: realized cost vs the quote"
seo_description: "Live gap between a bridge quote and the real on-chain settlement for Mobula, Relay and LI.FI, plus the gas the user actually pays, measured on real executed USDC transfers across Solana, Base and Arbitrum from EU-West."
seo_description: "Live gap between a bridge quote and the real on-chain settlement for Mobula, Relay, LI.FI and Near Intents, plus the gas the user actually pays, measured on real executed USDC transfers across Solana, Base and Arbitrum from EU-West."
subtitle: The gap between what a bridge quotes and what actually settles on-chain, in USD, on real executed USDC transfers, plus our own gas, at $3 and $30, measured from EU-West.
category: Bridges
status: live
Expand All @@ -24,7 +24,7 @@ dimensions:
seo_intro: |
Every bridge quotes a cost. This benchmark measures whether that quote
survives contact with the chain. We broadcast and settle real USDC
transfers through Mobula, Relay and LI.FI on a self-conserving triangle,
transfers through Mobula, Relay, LI.FI and Near Intents on a self-conserving triangle,
then record the realized fee against the fee the quote promised. The
headline is that gap, in dollars: a bridge whose settlement matches its
quote sits near zero, a bridge that fills below quote reads positive, and
Expand All @@ -45,23 +45,25 @@ abstract: |

methodology:
- "Route: a self-conserving all-USDC triangle (Solana to Base, Base to Arbitrum, Arbitrum to Solana). Each bridge completes the full triangle before the next starts, so inventory returns to origin and only fees burn."
- "Slippage vs quote: realized fee minus quote-projected fee, in USD (bridge_quote_slippage_usd). Realized fee is amount sent minus amount actually received on-chain. Near zero means the settlement matched the quote; positive means it settled worse; negative means it beat the quote."
- "Slippage vs quote: realized fee minus quoted fee, in USD (bridge_quote_slippage_usd). Realized fee is the ticket minus what actually landed on the destination, read from the balance delta; quoted fee is the ticket minus the output the quote promised, one definition for every bridge. Near zero means the settlement matched the quote; positive means it settled worse; negative means it beat the quote."
- "Why not the providers' own fee fields: some mix in gas paid in the native token, which the realized fee (ticket minus landed) can never contain, so one bridge read a constant negative offset at every ticket size. Both sides of the subtraction are now defined against the ticket."
- "All-in gas: our own on-chain gas to execute (approve plus deposit), measured as the source-chain native balance delta before and after (bridge_exec_gas_usd). This is the cost the user bears on top of the bridge fee."
- "Realized output: the USD value that actually landed on the destination chain (bridge_realized_output_usd), waited for on-chain rather than taken from the quote."
- "Ticket sizes: $3 and $30. Small on purpose, since this measures the quote-to-settlement gap, not large-ticket depth."
- "Realized fee: ticket minus the value that landed on the destination chain, in basis points of the ticket (bridge_exec_realized_fee_bps), so $3 and $30 executions compare. Published only for settled executions whose destination balance was read on-chain; a leg whose credit was not visible within 30 s publishes nothing and is counted in bridge_realized_fallback_total."
- "Ticket sizes: $3 daily and $30 on Monday and Thursday, pooled. Small on purpose, since this measures the quote-to-settlement gap, not large-ticket depth. Window: 7 days, about 7 to 9 executions per bridge and corridor."
- "Region: EU-West only. The execution node runs a single wallet, so a second origin would race the same inventory."
- "Cohort: Mobula, Relay and LI.FI, the bridges executed on all three legs. Quote-only providers are covered by the bridge-fee and bridge-quote-latency benchmarks."
- "Cohort: Mobula, Relay, LI.FI and Near Intents, the bridges executed on all three legs. Quote-only providers are covered by the bridge-fee and bridge-quote-latency benchmarks."

findings:
- "{{name:mobula}} settles within {{p50:mobula}} of its quote (median, 24 h) on executed USDC transfers."
- "{{name:mobula}} settles within {{p50:mobula}} of its quote (median, 7 d) on executed USDC transfers."
- "{{name:relay}} settles within {{p50:relay}} of its quote."
- "{{name:lifi}} settles within {{p50:lifi}} of its quote."
- "{{name:near-intents}} settles within {{p50:near-intents}} of its quote."

faq:
- q: "How is this different from the bridge fee benchmark?"
a: "The fee benchmark measures the cost a quote advertises. This one measures the cost that actually settled on-chain and subtracts what the quote promised, so it captures the gap a user only discovers after they commit. A bridge can quote cheap and settle worse, and only an execution benchmark surfaces that."
- q: "Why can the number be negative?"
a: "Because a bridge can settle better than it quoted. Slippage vs quote is realized fee minus quoted fee, so a solver that fills above the quoted output makes the figure negative. Near zero means the quote was honest; positive means the execution cost more than promised."
a: "Because a bridge can settle better than it quoted. Slippage vs quote is realized fee minus quoted fee, both defined against the ticket (ticket minus landed, ticket minus quoted output), so a solver that fills above the quoted output makes the figure negative. Near zero means the quote was honest; positive means the execution cost more than promised."
- q: "Does this include the gas I pay?"
a: "The headline is the bridge's own quote-to-settlement gap. Our own execution gas (approve plus deposit) is reported separately in the all-in gas panel, measured as the real native-token balance delta, because it is a cost the user bears on top of the bridge fee."
- q: "Where does this run from?"
Expand All @@ -71,26 +73,19 @@ source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/bridge-

metric_panels:
- id: fee
label: Realized fee
metric: avg by (bridge) (ocb:bridge_fees_usd:avg_7d{region="eu-west"})
label: Realized fee (bps of ticket)
metric: avg by (bridge) (ocb:bridge_exec_realized_fee_bps:avg_7d{region="eu-west"})
label_key: bridge
unit: usd
unit: bps
higher_is_better: false
description: "Amount sent minus amount actually received on the destination chain, in USD, averaged over the last 7 days of executions. The fee a user really paid, read on-chain rather than from the quote."
description: "Ticket minus the value that actually landed on the destination chain, in basis points of the ticket so the $3 and $30 executions compare, averaged over the last 7 days of settled executions whose destination balance was read on-chain."
- id: gas
label: All-in gas we paid
metric: avg by (bridge) (ocb:bridge_exec_gas_usd:avg_7d{region="eu-west"})
label_key: bridge
unit: usd
higher_is_better: false
description: "Our own on-chain gas (approve plus deposit), measured as the source-chain native balance delta. The cost the user bears on top of the bridge fee."
- id: realized
label: Realized output on-chain
metric: avg by (bridge) (ocb:bridge_realized_output_usd:avg_7d{region="eu-west"})
label_key: bridge
unit: usd
higher_is_better: true
description: "The USD value that actually landed on the destination chain, waited for on-chain rather than read from the quote."

prometheus:
window: 7d
Expand Down
45 changes: 35 additions & 10 deletions harnesses/bridge-monitor/cmd/monitor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ type ExecutionResult struct {
ExecutionLatencyMs int64 // Settlement latency: destination block timestamp minus source block timestamp (on-chain); wall clock when a hash is missing
ObservedLatencyMs int64 // Wall clock from broadcast to the poll that saw the terminal status (the pre-2026-09-19 figure), kept for the audit trail
DestTxHash string
LatencyMethod string // "watch" (credit observed minus source inclusion observed, ms) | "watch-broadcast" (credit minus broadcast) | "blocks" (block timestamp delta) | "poll"
OnchainBlockDeltaMs int64 // destination block timestamp minus source block timestamp, -1 when unknown
E2ELatencyMs int64 // Time from quote start to funds received
LatencyMethod string // "watch" (credit observed minus source inclusion observed, ms) | "watch-broadcast" (credit minus broadcast) | "blocks" (block timestamp delta) | "poll"
OnchainBlockDeltaMs int64 // destination block timestamp minus source block timestamp, -1 when unknown
QuotedOutputUSD float64 // what the quote said would land (USD), before any on-chain read
RealizedOnChain bool // OutputUSD and ActualFeeUSD come from the destination balance delta, not from the quote
E2ELatencyMs int64 // Time from quote start to funds received
Success bool
Reverted bool
Refunded bool // subset of Reverted: provider returned capital (status "refunded")
Expand Down Expand Up @@ -438,6 +440,17 @@ func (e *Executor) executeOnBridge(bridge string, route TestRoute, amount, amoun
// Prometheus correctly classify it (Reverted takes precedence over Success).
result.Success = !result.Reverted

// One definition of the quoted fee for every bridge: ticket minus the
// output the quote promised. Provider fee sums mixed in gas paid in the
// native token (Mobula: TotalFeeUsd + GasFeeUsd), which the realized fee
// (ticket minus landed) can never contain, so their slippage read a
// constant negative offset regardless of ticket (-$0.0015 at $3 and at
// $30 on Base). Gas we pay ourselves is its own panel.
result.QuotedOutputUSD = result.OutputUSD
if amountUSD > 0 && result.QuotedOutputUSD > 0 && result.QuotedOutputUSD <= amountUSD {
result.QuoteFeeUSD = amountUSD - result.QuotedOutputUSD
}

// Read the destination balance again to compute the REALIZED fill on-chain.
// Bridge status "filled" sometimes precedes the destination credit by 1-3
// blocks; pollRealizedFill waits up to 30s for the delta to materialise.
Expand All @@ -451,6 +464,7 @@ func (e *Executor) executeOnBridge(bridge string, route TestRoute, amount, amoun
realizedUSD := realizedToken * destinationUSDPerToken(route)
log.Printf(" 💰 Realized fill on-chain: %.6f tokens = $%.4f (quote projected $%.4f)", realizedToken, realizedUSD, result.OutputUSD)
result.OutputUSD = realizedUSD
result.RealizedOnChain = true
// Recompute fees from realized: amount sent - amount received
realFees := amountUSD - realizedUSD
if realFees < 0 {
Expand Down Expand Up @@ -1015,20 +1029,31 @@ func (e *Executor) recordExecutionMetrics(result *ExecutionResult) {
}

// Record fees + the new execution-cost metrics
pulse(bridgeFeesUSD, labels, result.ActualFeeUSD)
if result.AmountUSD > 0 {
pulse(bridgeFeesPercent, labels, (result.ActualFeeUSD/result.AmountUSD)*100)
}
if result.OutputUSD > 0 {
pulse(bridgeRealizedOutputUSD, labels, result.OutputUSD)
// Realized cost metrics come from the destination balance delta only.
// A failed leg, a 30 s destination poll miss or a pre-balance read
// failure leaves OutputUSD at the quote's projection: published as
// "realized", that was a quote number wearing an on-chain label (Relay
// $30 Base: one errored leg published $29.97 of "landed" value).
if result.Success && result.RealizedOnChain {
pulse(bridgeFeesUSD, labels, result.ActualFeeUSD)
pulse(bridgeExecRealizedFeeUSD, labels, result.ActualFeeUSD)
if result.AmountUSD > 0 {
pulse(bridgeFeesPercent, labels, (result.ActualFeeUSD/result.AmountUSD)*100)
pulse(bridgeExecRealizedFeeBps, labels, (result.ActualFeeUSD/result.AmountUSD)*10000)
}
if result.OutputUSD > 0 {
pulse(bridgeRealizedOutputUSD, labels, result.OutputUSD)
}
} else if result.Success {
bridgeRealizedFallback.WithLabelValues(result.Bridge, result.FromChain, result.ToChain, e.region).Inc()
}
// Execution slippage vs quote = realized fee - quote-projected fee. Only on
// a real fill: on a revert / refund / pre-broadcast failure there is no
// realized fee (ActualFeeUSD stays 0 while QuoteFeeUSD was set), so recording
// it would inject spurious 0 / negative samples into the realized-cost bench.
// Slippage is only meaningful against a quoted fee; Near Intents used to
// publish its whole realized fee here (QuoteFeeUSD was never set).
if result.Success && result.QuoteFeeUSD > 0 {
if result.Success && result.RealizedOnChain && result.QuoteFeeUSD > 0 {
pulse(bridgeQuoteSlippageUSD, labels, result.ActualFeeUSD-result.QuoteFeeUSD)
}
if result.ExecGasUSD > 0 {
Expand Down
16 changes: 16 additions & 0 deletions harnesses/bridge-monitor/cmd/monitor/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ var (
Help: "Settled executions whose published latency did not come from the two-ended watch, by method (watch-broadcast: source inclusion missed; blocks: block-timestamp delta; poll: wall clock to the status poll)",
}, []string{"bridge", "from_chain", "to_chain", "region", "method"})

// Execution-only realized fee (bridge_fees_usd is also written by the
// quote loop at $5/$50/$300 and off-triangle routes, so a panel reading
// it averaged quotes into "realized"). Pulses.
bridgeExecRealizedFeeUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "bridge_exec_realized_fee_usd",
Help: "Realized fee of one settled execution in USD: ticket minus the value that landed on the destination, read from the balance delta (pulse)",
}, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"})
bridgeExecRealizedFeeBps = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "bridge_exec_realized_fee_bps",
Help: "Realized fee of one settled execution in basis points of the ticket (comparable across $3 and $30) (pulse)",
}, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"})
bridgeRealizedFallback = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "bridge_realized_fallback_total",
Help: "Settled executions whose destination balance could not be read (pre-balance read failed or the credit was not visible within 30 s): no realized fee, output or slippage published for them",
}, []string{"bridge", "from_chain", "to_chain", "region"})

// Persistent: unix time of the last completed execution per bridge and
// region, the site's freshness source (prometheus.freshness_timestamp_metric).
bridgeLastExecutionTs = promauto.NewGaugeVec(prometheus.GaugeOpts{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ groups:
expr: avg_over_time(bridge_exec_gas_usd[7d])
- record: ocb:bridge_realized_output_usd:avg_7d
expr: avg_over_time(bridge_realized_output_usd[7d])
- record: ocb:bridge_fees_usd:avg_7d
expr: avg_over_time(bridge_fees_usd[7d])
- record: ocb:bridge_exec_realized_fee_usd:avg_7d
expr: avg_over_time(bridge_exec_realized_fee_usd[7d])
- record: ocb:bridge_exec_realized_fee_bps:avg_7d
expr: avg_over_time(bridge_exec_realized_fee_bps[7d])
- record: ocb:bridge_exec_onchain_ms:mean_7d
expr: avg_over_time(bridge_exec_onchain_ms[7d])
6 changes: 5 additions & 1 deletion src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,11 @@ export default async function BenchmarkPage({
)}
{!isDraft && !insufficient && (
<span className="ml-auto">
<LiveIndicator lastRunAt={benchmark.lastRunAt} slug={benchmark.slug} />
<LiveIndicator
lastRunAt={benchmark.lastRunAt}
slug={benchmark.slug}
staleAfterSec={Math.max(300, benchmark.expectedFreshnessSec ?? 300)}
/>
</span>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/bench-infobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function BenchInfobox({ benchmark }: { benchmark: Benchmark }) {
) : null}
</InfoPair>
{top && (
<InfoPair label="Leader (24h)">
<InfoPair label={`Leader (${benchmark.window ?? "24h"})`}>
<span className="font-medium text-ink">{top.name}</span>
<span className="text-ink-faint">
{" "}
Expand Down
7 changes: 6 additions & 1 deletion src/components/live-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import { useEffect, useState } from "react";
export function LiveIndicator({
lastRunAt,
slug,
staleAfterSec = 300,
}: {
/** Seconds after which the dot turns "Stale". Benches whose data moves
* once a day (bridge executions) pass their expected freshness; the
* default is the 5-minute ISR rhythm. */
staleAfterSec?: number;
lastRunAt: string;
slug?: string;
}) {
Expand Down Expand Up @@ -72,7 +77,7 @@ export function LiveIndicator({
}, [slug]);

const ageSec = Math.max(0, Math.floor((now - new Date(canonical).getTime()) / 1000));
const stale = ageSec > 300;
const stale = ageSec > staleAfterSec;

return (
<span className="inline-flex items-center gap-2 normal-case tracking-normal text-xs tabular text-ink-muted">
Expand Down
5 changes: 3 additions & 2 deletions src/components/static-ledger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ export function StaticLedger({ benchmark }: { benchmark: Benchmark }) {
if (rows.length === 0) return null;
const chain = rpcChainLabel(benchmark);
const pausedOn = isStaleBench(benchmark) && benchmark.lastRunAt ? benchmark.lastRunAt.slice(0, 10) : null;
const win = benchmark.window ?? "24h";
const heading = chain && pausedOn
? `Results: measurement paused since ${pausedOn}, last ranking of ${rows.length} free public ${chain} RPC endpoint${rows.length === 1 ? "" : "s"}`
: chain
? `Results: ${rows.length} free public ${chain} RPC endpoint${rows.length === 1 ? "" : "s"} ranked by p50 latency (24h, 3 regions)`
: `Results: ${rows.length} providers ranked by ${benchmark.metric} (p50, 24h)`;
: `Results: ${rows.length} providers ranked by ${benchmark.metric} (p50, ${win})`;
const showTail = benchmark.unit === "ms" || benchmark.unit === "s";
return (
<section className="mt-8" aria-labelledby="results">
Expand All @@ -33,7 +34,7 @@ export function StaticLedger({ benchmark }: { benchmark: Benchmark }) {
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<caption className="sr-only">
{benchmark.title}: {benchmark.metric} per provider, last 24 hours.
{benchmark.title}: {benchmark.metric} per provider, last {win === "24h" ? "24 hours" : win}.
</caption>
<thead>
<tr className="border-y-2 border-ink text-left">
Expand Down
Loading
Loading