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
4 changes: 2 additions & 2 deletions benchmarks/bridge-quote-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ methodology:
- "Notional sizes: $5, $50, $300 per quote."
- "Cadence: full sweep (4 routes × 3 amounts × N bridges) every 5 minutes for 24 hours."
- "Region: eu-west. Single point of measurement; multi-region requires running additional monitor instances."
- "Histogram buckets: 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms."
- "Histogram buckets: 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms. Sub-50ms buckets added 2026-07-13 so solver-cached fast paths (Near Intents on HyperCore in particular) surface their true p50 instead of being floored at the interpolation of the (0, 50] bucket."
- "Failures (quote_failed, execution_failed, unsupported route) excluded from latency aggregates and counted toward success rate."

# Per-corridor leader rewrite deferred: this YAML does not yet declare
Expand Down Expand Up @@ -89,7 +89,7 @@ faq:
- q: "Are intent and relay bridges always faster than aggregators?"
a: "On a single-route query, usually. Intent layers like Relay quote against a pool of pre-positioned solvers, so the API is a thin price-discovery call. Aggregators like LI.FI run a route-search graph over N underlying bridges per request, which adds 100 to 400 ms of irreducible work. On multi-hop or rare-corridor queries the aggregator advantage in coverage matters more than the latency gap, and the comparison flips."
- q: "How does OpenChainBench measure bridge quote latency?"
a: "The harness issues identical quote requests against every bridge for the same route and notional, every five minutes, from a single eu-west origin. Each request times wall-clock duration from send to last byte received, then publishes a Prometheus histogram (50, 100, 200, 500, 1000, 2000, 5000, 10000 ms buckets). p50, p90 and p99 are derived via `histogram_quantile` over a rolling 24-hour window. Errored quotes (quote_failed, unsupported route, timeout) are excluded from the latency aggregate and counted toward success rate so a fast-but-broken bridge cannot game the headline."
a: "The harness issues identical quote requests against every bridge for the same route and notional, every five minutes, from a single eu-west origin. Each request times wall-clock duration from send to last byte received, then publishes a Prometheus histogram (10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms buckets). p50, p90 and p99 are derived via `histogram_quantile` over a rolling 24-hour window. Errored quotes (quote_failed, unsupported route, timeout) are excluded from the latency aggregate and counted toward success rate so a fast-but-broken bridge cannot game the headline."
- q: "Does quote latency affect the price the user gets?"
a: "Indirectly, yes. A slower quote API leaves a larger window between the moment the price is quoted and the moment the user signs. On volatile corridors the underlying spot can drift in that window, which surfaces as a quoted-vs-realized gap. Intent layers compensate by re-quoting at signing time; aggregators usually pass through a slippage parameter. The fee benchmark on this site (`/benchmarks/bridge-fee`) measures the realized cost; this page measures the latency half of the same flow."

Expand Down
2 changes: 1 addition & 1 deletion harnesses/bridge-monitor/cmd/monitor/across_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (a *AcrossBridge) TestRoute(route TestRoute, amount, amountUsd float64, raw
// Latency is only meaningful for quotes that returned a usable route
// (same rule as the other bridges). Fast 4xx rejections must not enter
// the histogram or they skew the leaderboard.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

// fees.total.amountUsd is Across's own USD valuation of input value minus
Expand Down
2 changes: 1 addition & 1 deletion harnesses/bridge-monitor/cmd/monitor/debridge_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (d *DebridgeBridge) TestRoute(route TestRoute, amount, amountUsd float64, r
// (the published methodology measures exactly that). Fast failures, e.g.
// Cloudflare 403s answered in 30ms, must not enter the histogram: they
// made deBridge look 15x faster the moment its API started rejecting us.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

// Debridge returns input (with opEx ajusté) and output
Expand Down
2 changes: 1 addition & 1 deletion harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (l *LiFiBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawUn
// (the published methodology measures exactly that). Fast failures, e.g.
// Cloudflare 403s answered in 30ms, must not enter the histogram: they
// made deBridge look 15x faster the moment its API started rejecting us.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

inUsd, _ := strconv.ParseFloat(quote.Estimate.FromAmountUSD, 64)
Expand Down
10 changes: 8 additions & 2 deletions harnesses/bridge-monitor/cmd/monitor/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import (
)

var (
// Quote latency (time to get quote response)
// Quote latency (time to get bridge quote in ms). Sub-50ms buckets
// (10, 25) added 2026-07-13 because Near Intents on solver-cached
// corridors (HyperCore in particular) returns in <50ms via the 1Click
// coordinator's cached-price fast path, and the previous [50, ...]
// scheme lumped every one of those observations into the (0, 50]
// bucket. histogram_quantile then linearly interpolated to ~25ms
// regardless of the true value, hiding the real bimodality.
bridgeQuoteLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "bridge_quote_latency_ms",
Help: "Latency to get bridge quote in milliseconds",
Buckets: []float64{50, 100, 200, 500, 1000, 2000, 5000, 10000},
Buckets: []float64{10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000},
}, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"})

// Execution latency (broadcast to funds received)
Expand Down
2 changes: 1 addition & 1 deletion harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func (m *MobulaBridge) TestRoute(route TestRoute, amount, amountUsd float64, reg
}

// Success-only: see debridge_bridge.go, failures must not enter the histogram.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)

bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func (n *NearIntentsBridge) TestRoute(route TestRoute, amount, amountUsd float64
// Latency is only meaningful for quotes that returned a usable response
// (same rule as the other bridges). Fast 4xx rejections must not enter
// the histogram or they skew the leader board.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

inUsd, _ := strconv.ParseFloat(quote.Quote.AmountInUsd, 64)
Expand Down
2 changes: 1 addition & 1 deletion harnesses/bridge-monitor/cmd/monitor/relay_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (r *RelayBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawU
// (the published methodology measures exactly that). Fast failures, e.g.
// Cloudflare 403s answered in 30ms, must not enter the histogram: they
// made deBridge look 15x faster the moment its API started rejecting us.
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds()))
bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

inUsd, _ := strconv.ParseFloat(quote.Details.CurrencyIn.AmountUsd, 64)
Expand Down
10 changes: 10 additions & 0 deletions src/lib/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ const loadBenchmarkUnfilteredCached = unstable_cache(
// ISR re-render bursts.
const stored = await benchFromStore(slug, "");
if (stored) return slimBenchmarkForCache(overlayEditorial(stored, spec));
if (spec.status === "live") {
// A live spec with no readable blob is a transient store failure
// (proxy timeout, worker mid-write), not a real state. Returning
// undefined would cache the miss for the whole revalidate window:
// home row + /api/stat then serve "Awaiting samples" while the
// bench page reads fine (bnb-rpc incident, 2026-07-13). Throwing
// makes unstable_cache keep the last good entry; cold-cache
// callers already catch and fall back to the draft placeholder.
throw new Error(`store blob missing for live bench ${slug}`);
}
return undefined;
},
// Version key bumped when Benchmark shape changes so stale cache entries
Expand Down
Loading