diff --git a/benchmarks/bridge-execution-latency.yml b/benchmarks/bridge-execution-latency.yml index 283cd621..28521273 100644 --- a/benchmarks/bridge-execution-latency.yml +++ b/benchmarks/bridge-execution-latency.yml @@ -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"}) diff --git a/benchmarks/bridge-realized-cost.yml b/benchmarks/bridge-realized-cost.yml index fc5feae2..0dc0352d 100644 --- a/benchmarks/bridge-realized-cost.yml +++ b/benchmarks/bridge-realized-cost.yml @@ -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 @@ -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 @@ -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?" @@ -71,12 +73,12 @@ 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"}) @@ -84,13 +86,6 @@ metric_panels: 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 diff --git a/harnesses/bridge-monitor/cmd/monitor/executor.go b/harnesses/bridge-monitor/cmd/monitor/executor.go index 9f01c03d..b20932d0 100644 --- a/harnesses/bridge-monitor/cmd/monitor/executor.go +++ b/harnesses/bridge-monitor/cmd/monitor/executor.go @@ -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") @@ -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. @@ -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 { @@ -1015,12 +1029,23 @@ 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 @@ -1028,7 +1053,7 @@ func (e *Executor) recordExecutionMetrics(result *ExecutionResult) { // 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 { diff --git a/harnesses/bridge-monitor/cmd/monitor/metrics.go b/harnesses/bridge-monitor/cmd/monitor/metrics.go index 2beb6031..f1bec6c3 100644 --- a/harnesses/bridge-monitor/cmd/monitor/metrics.go +++ b/harnesses/bridge-monitor/cmd/monitor/metrics.go @@ -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{ diff --git a/infrastructure/monitoring/prometheus/ocb_bridge_execution.yml b/infrastructure/monitoring/prometheus/ocb_bridge_execution.yml index fc7092c8..51d71e6e 100644 --- a/infrastructure/monitoring/prometheus/ocb_bridge_execution.yml +++ b/infrastructure/monitoring/prometheus/ocb_bridge_execution.yml @@ -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]) diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index 4a67f201..1d1f956c 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -549,7 +549,11 @@ export default async function BenchmarkPage({ )} {!isDraft && !insufficient && ( - + )} diff --git a/src/components/bench-infobox.tsx b/src/components/bench-infobox.tsx index c5e63bdc..9f14ec21 100644 --- a/src/components/bench-infobox.tsx +++ b/src/components/bench-infobox.tsx @@ -122,7 +122,7 @@ export function BenchInfobox({ benchmark }: { benchmark: Benchmark }) { ) : null} {top && ( - + {top.name} {" "} diff --git a/src/components/live-indicator.tsx b/src/components/live-indicator.tsx index 873017de..f856f97d 100644 --- a/src/components/live-indicator.tsx +++ b/src/components/live-indicator.tsx @@ -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; }) { @@ -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 ( diff --git a/src/components/static-ledger.tsx b/src/components/static-ledger.tsx index d76bd745..f87ccb4d 100644 --- a/src/components/static-ledger.tsx +++ b/src/components/static-ledger.tsx @@ -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 (
@@ -33,7 +34,7 @@ export function StaticLedger({ benchmark }: { benchmark: Benchmark }) {
diff --git a/src/lib/citation.ts b/src/lib/citation.ts index 1d4961e8..2a0842f3 100644 --- a/src/lib/citation.ts +++ b/src/lib/citation.ts @@ -109,10 +109,10 @@ export function leader(b: Benchmark): { name: string; slug: string; value: numbe /** Honest window wording per unit. "(p50, 24h)" is only true for latency * style benches; USD revenue and count benches repurpose the p50 slot as * a plain rolling-window figure and percentile wording would mislead. */ -function windowSuffix(unit: string): string { - if (unit === "usd" || unit === "count") return "(24h)"; - if (unit === "pct" || unit === "bps") return "(24h avg)"; - return "(p50, 24h)"; +function windowSuffix(unit: string, window = "24h"): string { + if (unit === "usd" || unit === "count") return `(${window})`; + if (unit === "pct" || unit === "bps") return `(${window} avg)`; + return `(p50, ${window})`; } /** Short factual sentence ready to paste into an article. Templated, no LLM. */ @@ -177,7 +177,7 @@ export function headlineParts(b: Benchmark): { claim: string; rest: string } { const verb = b.higherIsBetter ? "leads" : "posts the lowest"; return { claim: `${top.name} ${verb} ${metricInSentence(b.metric)} at ${value}`, - rest: `${windowSuffix(b.unit)} on ${b.title}.`, + rest: `${windowSuffix(b.unit, b.window ?? "24h")} on ${b.title}.`, }; } diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index e84f52ec..65cd03b7 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -151,6 +151,8 @@ export function buildEditorial( disclaimer: spec.disclaimer, faq: spec.faq, excludedProviders: spec.excluded_providers, + window: spec.prometheus?.window, + expectedFreshnessSec: spec.prometheus?.expected_freshness_seconds, perChainExplainer: spec.per_chain_explainer, subtitle: spec.subtitle, category: spec.category, diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 93221595..3eb394d1 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -135,6 +135,8 @@ export function overlayEditorial(stored: Benchmark, spec: Spec): Benchmark { seoIntro: spec.seo_intro ?? stored.seoIntro, faq: spec.faq ?? stored.faq, excludedProviders: spec.excluded_providers ?? stored.excludedProviders, + window: spec.prometheus?.window ?? stored.window, + expectedFreshnessSec: spec.prometheus?.expected_freshness_seconds ?? stored.expectedFreshnessSec, perChainExplainer: spec.per_chain_explainer ?? stored.perChainExplainer, abstract: spec.abstract ?? stored.abstract, methodology: spec.methodology ?? stored.methodology, diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index b7ee5737..98ed02d7 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -238,6 +238,12 @@ export type Benchmark = { /** Providers audited and not listed, with the reason. Rendered under * the public endpoints table; never carries a URL (schema-refused). */ excludedProviders?: { name: string; reason: string; since?: string }[]; + /** Aggregation window of the headline queries ("24h" default, "7d" on the + * bridge execution benches). Drives the "(p50, 24h)" wording. */ + window?: string; + /** Seconds after which the data counts as stale for this bench (from + * prometheus.expected_freshness_seconds; 600 when unset). */ + expectedFreshnessSec?: number; /** Optional per-chain explainer blocks rendered as H2-anchored sections * below the main chart. Targets long-tail "X chain {metric}" queries * that benefit from a dedicated on-page anchor (#ethereum, #solana, ...).
- {benchmark.title}: {benchmark.metric} per provider, last 24 hours. + {benchmark.title}: {benchmark.metric} per provider, last {win === "24h" ? "24 hours" : win}.