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-fee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ dimensions:
- { value: Arbitrum, label: Base to Arb }
- { value: Solana, label: Arb to Sol }
- { value: HyperCore, label: Arb to HyperCore }
# us-east and sgp probes are temporarily paused (2026-09-10): only the
# EU-West (Paris) origin is live, so the bench is scoped to a single region.
# Restore the all/us-east/sgp values when the Railway us/sgp probes resume.
region:
- { value: all, label: All regions }
- { value: eu-west, label: EU-West }
- { value: us-east, label: US-East }
- { value: sgp, label: Singapore }
amount_usd:
- { value: "300", label: "$300" }
- { value: "50", label: "$50" }
Expand Down Expand Up @@ -65,7 +65,7 @@ methodology:
- "Cadence: full sweep every 5 minutes for 24 hours."
- "Aggregation: the headline number is the average of per-corridor 24h medians over the corridors a provider actually quotes. The TRUMP to BRETT calibration route is excluded. Near Intents has no USDT legs, so its aggregate covers the Solana to Base and Arbitrum to HyperCore corridors only. deBridge does not quote the Arbitrum to HyperCore corridor, so its aggregate covers the remaining three. Across also skips Arbitrum to HyperCore, so its aggregate covers the same three corridors as deBridge."
- "Normalization: cost is quoted USD in minus quoted USD delivered, the same all-in definition for every provider. Near Intents bundles the destination chain withdrawal fee into its quoted amountOut (the 1Click API documents withdrawFee as already deducted), so no extra gas term is added on top."
- "Regions: eu-west, us-east, sgp. Three independent instances probe simultaneously; the All-regions aggregate uses the full union; per-region tabs isolate the view to one origin."
- "Regions: eu-west only at present. The us-east and sgp probes are temporarily paused, so the live figures reflect the EU-West (Paris) origin; the other two tabs return when those probes resume."
- "Failures (quote_failed, unsupported route) excluded from cost aggregates and counted toward success rate."
- "Provider types are surfaced as badges: direct protocols (deBridge) charge their own bridging fees; aggregators (LI.FI) route through whichever underlying bridge is cheapest, so their effective fee includes that bridge's cost plus any aggregator markup; intent / relay layers (Mobula, Relay, Across, Near Intents) bake cost into the spread of a settlement intent or optimistic fill rather than charging an explicit fee."
- "Excluded by design: Stargate, because its keyless quote endpoint was deprecated in favor of a key-gated LayerZero API, and its on-chain quoteOFT path is same-asset EVM-only, which matches none of this bench's corridors. CCTP, because Circle's burn/mint has no quote that returns a delivered amount; standard transfers mint 1:1 and the whole cost is user-paid gas we would have to estimate ourselves, which breaks the quoted USD in minus quoted USD delivered definition used for every other row."
Expand Down
32 changes: 17 additions & 15 deletions benchmarks/bridge-quote-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ dimensions:
- { value: Arbitrum, label: Base to Arb }
- { value: Solana, label: Arb to Sol }
- { value: HyperCore, label: Arb to HyperCore }
# us-east and sgp probes are temporarily paused (2026-09-10): only the
# EU-West (Paris) origin is live, so the bench is scoped to a single region.
# Restore the all/us-east/sgp values to bring the multi-region tabs back once
# the Railway us/sgp probes resume.
region:
- { value: all, label: All regions }
- { value: eu-west, label: EU-West }
- { value: us-east, label: US-East }
- { value: sgp, label: Singapore }
amount_usd:
- { value: "300", label: "$300" }
- { value: "50", label: "$50" }
Expand All @@ -41,9 +42,10 @@ seo_intro: |
flow into a wallet, an aggregator or a chat-app care about the same
number every public roundup ignores. how many milliseconds the API
burns before the user sees a price. We hammer the same USDC routes
(Solana, Base, Arbitrum) against every bridge from three geographic
origins (EU-West, US-East, Singapore), every five minutes, on three
notional sizes. p50, p90 and p99 are computed via Prometheus
(Solana, Base, Arbitrum) against every bridge from the EU-West (Paris)
origin, every five minutes, on three notional sizes. The US-East and
Singapore origins are temporarily paused, so the live numbers reflect
EU-West only. p50, p90 and p99 are computed via Prometheus
`histogram_quantile` over the 24-hour window, so the leaderboard
reflects sustained API performance, not a lucky cold-start. The same
harness records failure mode for every request (quote_failed,
Expand All @@ -63,7 +65,7 @@ methodology:
- "Routes: USDC pairs spanning Solana, Base and Arbitrum."
- "Notional sizes: $5, $50, $300 per quote."
- "Cadence: full sweep (4 routes × 3 amounts × N bridges) every 5 minutes for 24 hours."
- "Regions: eu-west, us-east, sgp. Three independent instances probe simultaneously; the All-regions aggregate uses the full union; per-region tabs isolate the view to one origin."
- "Regions: eu-west only at present. The us-east and sgp probes are temporarily paused, so the live figures reflect the EU-West (Paris) origin; the harness is built to probe three origins simultaneously and the other two tabs return when those probes resume."
- "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."

Expand Down Expand Up @@ -91,15 +93,15 @@ prometheus:

faq:
- q: "Which cross-chain bridge has the lowest API latency?"
a: "{{best_name}} currently leads the cross-corridor aggregate at {{best_p50}} (p50, 24 h) across {{count}} measured bridges, averaged over the 4 USDC routes the harness sweeps. The leaderboard re-sorts every five minutes against fresh Prometheus samples, so the ranking reflects the last 24 hours of actual API performance from 3 regions (eu-west, us-east, sgp) under the same routes. Per-corridor latency leaders can differ from this aggregate where one provider has a solver pool closer to a specific destination chain, that breakdown is on the roadmap once corridor dimensions are surfaced in the page UI."
a: "{{best_name}} currently leads the cross-corridor aggregate at {{best_p50}} (p50, 24 h) across {{count}} measured bridges, averaged over the 4 USDC routes the harness sweeps. The leaderboard re-sorts every five minutes against fresh Prometheus samples, so the ranking reflects the last 24 hours of actual API performance from the EU-West (Paris) origin under the same routes. Per-corridor latency leaders can differ from this aggregate where one provider has a solver pool closer to a specific destination chain, that breakdown is on the roadmap once corridor dimensions are surfaced in the page UI."
- q: "What is the difference between quote latency and bridge fill time?"
a: "Quote latency is the wall-clock time the bridge API takes to return a price. Fill time is the wall-clock time the funds take to land on the destination chain after the user signs. They are decoupled: a bridge can quote in 100 ms and take 30 seconds to settle, or quote in 2 seconds and settle in 5 seconds. This benchmark measures the quote half because that is the part a wallet, an aggregator or an embedded swap UI feels first and tunes its UX around."
- q: "Why does bridge quote latency matter for builders?"
a: "Every product that integrates a bridge sits behind that API. Wallets, aggregators, intent layers, chat-app swap modals. A 1500 ms p99 forces a long loading spinner; a 5000 ms p99 forces a timeout fallback. The number controls perceived UX, time-to-first-quote in side-by-side comparators, and how aggressively the integrator has to parallelise calls across bridges to mask the slowest one."
- 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 three origins (eu-west, us-east, sgp) simultaneously. 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."
a: "The harness issues identical quote requests against every bridge for the same route and notional, every five minutes, from the EU-West (Paris) 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 All @@ -118,7 +120,7 @@ providers:
- slug: mobula
name: Mobula
tag: Aggregator + intent layer
formula: "Median wall-clock ms over 24h for Mobula's quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)."
formula: "Median wall-clock ms over 24h for Mobula's quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from the EU-West (Paris) origin."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula"})
Expand All @@ -141,7 +143,7 @@ providers:
- slug: relay
name: Relay
tag: Cross-chain relay
formula: "Median wall-clock ms over 24h for Relay's solver-quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)."
formula: "Median wall-clock ms over 24h for Relay's solver-quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from the EU-West (Paris) origin."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay"})
Expand All @@ -164,7 +166,7 @@ providers:
- slug: lifi
name: LI.FI
tag: Aggregator
formula: "Median wall-clock ms over 24h for LI.FI's route-search API to return a usable USDC route across underlying bridges, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)."
formula: "Median wall-clock ms over 24h for LI.FI's route-search API to return a usable USDC route across underlying bridges, sampled every 5 minutes across 4 routes and 3 notionals from the EU-West (Paris) origin."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi"})
Expand All @@ -187,7 +189,7 @@ providers:
- slug: debridge
name: deBridge
tag: Native-token fees
formula: "Median wall-clock ms over 24h for deBridge's direct-protocol quote API to return a single-route USDC quote, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)."
formula: "Median wall-clock ms over 24h for deBridge's direct-protocol quote API to return a single-route USDC quote, sampled every 5 minutes across 4 routes and 3 notionals from the EU-West (Paris) origin."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge"})
Expand All @@ -210,7 +212,7 @@ providers:
- slug: near-intents
name: Near Intents
tag: Intent layer (NEAR)
formula: "Median wall-clock ms over 24h for Near Intents 1Click API to return a usable USDC quote via the solver auction bus, sampled every 5 minutes across the supported routes and 3 notionals from 3 regions (eu-west, us-east, sgp)."
formula: "Median wall-clock ms over 24h for Near Intents 1Click API to return a usable USDC quote via the solver auction bus, sampled every 5 minutes across the supported routes and 3 notionals from the EU-West (Paris) origin."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents"})
Expand All @@ -233,7 +235,7 @@ providers:
- slug: across
name: Across
tag: Intent layer
formula: "Median wall-clock ms over 24h for Across's Swap API to return a usable USDC quote across supported corridors (Sol to Base, Base to Arb) from 3 regions. Arb to Sol excluded: no destination swaps to Solana."
formula: "Median wall-clock ms over 24h for Across's Swap API to return a usable USDC quote across supported corridors (Sol to Base, Base to Arb) from the EU-West origin. Arb to Sol excluded: no destination swaps to Solana."
queries:
p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="across"})
p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="across"})
Expand Down
4 changes: 2 additions & 2 deletions harnesses/bridge-monitor/cmd/monitor/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func indexAssets(result map[string]map[string]float64, resp *MobulaWalletRespons

// fetchWalletBalance fetches balance for a single wallet (all chains)
func (bc *BalanceChecker) fetchWalletBalance(address string) (*MobulaWalletResponse, error) {
url := fmt.Sprintf("https://demo-api.mobula.io/api/1/wallet/portfolio?wallet=%s", address)
url := fmt.Sprintf(mobulaAPIBase()+"/api/1/wallet/portfolio?wallet=%s", address)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
Expand Down Expand Up @@ -298,7 +298,7 @@ func (bc *BalanceChecker) fetchWalletBalance(address string) (*MobulaWalletRespo

// fetchWalletBalanceByChain fetches balance for a specific chain
func (bc *BalanceChecker) fetchWalletBalanceByChain(address, chain string) (*MobulaWalletResponse, error) {
url := fmt.Sprintf("https://demo-api.mobula.io/api/1/wallet/portfolio?wallet=%s&blockchains=%s", address, chain)
url := fmt.Sprintf(mobulaAPIBase()+"/api/1/wallet/portfolio?wallet=%s&blockchains=%s", address, chain)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
Expand Down
59 changes: 38 additions & 21 deletions harnesses/bridge-monitor/cmd/monitor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (

type Config struct {
// API Keys
RelayAPIKey string
MobulaAPIKey string
DebridgeAPIKey string
LiFiAPIKey string
NearIntentsAPIKey string
SquidIntegratorID string
SocketAPIKey string
RelayAPIKey string
MobulaAPIKey string
DebridgeAPIKey string
LiFiAPIKey string
NearIntentsAPIKey string
SquidIntegratorID string
SocketAPIKey string

// Wallet Configuration
WalletEVMPrivateKey string
Expand All @@ -27,13 +27,14 @@ type Config struct {
WalletSOLAddress string

// Execution Configuration
ExecutionMode string // "dry-run", "single-test", "production"
Freq5USD time.Duration // Frequency for $5 tests
Freq50USD time.Duration // Frequency for $50 tests
Freq300USD time.Duration // Frequency for $300 tests
EnableDebridge bool // Execute Debridge (expensive)
MaxDailySpendUSD float64 // Safety cap
TestAmountUSD float64 // Override test amount (for testing with small amounts)
ExecutionMode string // "dry-run", "single-test", "production"
Freq3USD time.Duration // Frequency for $3 tests
Freq30USD time.Duration // Frequency for $30 tests
EnableDebridge bool // Execute Debridge (expensive)
EnableR4RoundTrip bool // Execute R4 TRUMP<->BRETT round-trip (memes, not fee-neutral)
EnableR5Hypercore bool // Execute R5 Arb<->HyperCore round-trip (needs HL withdraw path)
MaxDailySpendUSD float64 // Safety cap
TestAmountUSD float64 // Override test amount (for testing with small amounts)

// General
MonitorRegion string
Expand All @@ -55,6 +56,16 @@ func parseDuration(s string, defaultVal time.Duration) time.Duration {
return d
}

// mobulaAPIBase returns the Mobula REST base URL (no trailing slash). Defaults
// to the production host; override with MOBULA_API_BASE. We moved off
// demo-api.mobula.io to the production api.mobula.io endpoint.
func mobulaAPIBase() string {
if v := strings.TrimSpace(os.Getenv("MOBULA_API_BASE")); v != "" {
return strings.TrimRight(v, "/")
}
return "https://api.mobula.io"
}

// parseFloat parses a float string, returns default if invalid
func parseFloat(s string, defaultVal float64) float64 {
if s == "" {
Expand Down Expand Up @@ -91,14 +102,19 @@ func loadEnv() (*Config, error) {
config.ExecutionMode = "dry-run" // Safe default
}

// Frequencies (default: $5 daily, $50 2x/week, $300 2x/month)
config.Freq5USD = parseDuration(os.Getenv("FREQ_5_USD"), 24*time.Hour)
config.Freq50USD = parseDuration(os.Getenv("FREQ_50_USD"), 84*time.Hour) // ~3.5 days
config.Freq300USD = parseDuration(os.Getenv("FREQ_300_USD"), 168*time.Hour) // 7 days (weekly)
// Frequencies (default: $3 daily, $30 2x/week)
config.Freq3USD = parseDuration(os.Getenv("FREQ_3_USD"), 24*time.Hour)
config.Freq30USD = parseDuration(os.Getenv("FREQ_30_USD"), 84*time.Hour) // ~3.5 days

// Debridge execution (default: disabled, too expensive)
config.EnableDebridge = os.Getenv("ENABLE_DEBRIDGE_EXEC") == "true"

// R4 (TRUMP<->BRETT) and R5 (Arb<->HyperCore) round-trip execution are
// opt-in: both move value across non-stable or asymmetric legs, so they
// stay quote-only until deliberately enabled and validated in single-test.
config.EnableR4RoundTrip = os.Getenv("ENABLE_R4_ROUNDTRIP_EXEC") == "true"
config.EnableR5Hypercore = os.Getenv("ENABLE_R5_HYPERCORE_EXEC") == "true"

// Max daily spend (default: $10/day for safety)
config.MaxDailySpendUSD = parseFloat(os.Getenv("MAX_DAILY_SPEND_USD"), 10.0)

Expand Down Expand Up @@ -216,10 +232,11 @@ func (c *Config) LogConfig() {
log.Printf(" EVM Private Key: %s", maskKey(c.WalletEVMPrivateKey))
log.Printf(" Solana Private Key: %s", maskKey(c.WalletSOLPrivateKey))
log.Printf(" Execution Mode: %s", c.ExecutionMode)
log.Printf(" $5 Frequency: %v", c.Freq5USD)
log.Printf(" $50 Frequency: %v", c.Freq50USD)
log.Printf(" $300 Frequency: %v", c.Freq300USD)
log.Printf(" $3 Frequency: %v", c.Freq3USD)
log.Printf(" $30 Frequency: %v", c.Freq30USD)
log.Printf(" Debridge Execution: %v", c.EnableDebridge)
log.Printf(" R4 Round-trip Execution: %v", c.EnableR4RoundTrip)
log.Printf(" R5 HyperCore Execution: %v", c.EnableR5Hypercore)
log.Printf(" Max Daily Spend: $%.2f", c.MaxDailySpendUSD)
if c.TestAmountUSD > 0 {
log.Printf(" Test Amount Override: $%.2f", c.TestAmountUSD)
Expand Down
Loading
Loading