Skip to content
Open
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
42 changes: 42 additions & 0 deletions benchmarks/bridge-fee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,45 @@ providers:
success: avg(avg_over_time(bridge_quote_success{bridge="near-intents", amount_usd="300", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h]))
sample_size: sum(count_over_time(bridge_cost_percent{bridge="near-intents", amount_usd="300", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h]))
series: avg_over_time(bridge_cost_percent{bridge="near-intents", amount_usd="300", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h])

- slug: squid
name: Squid Router
tag: Aggregator (Axelar)
formula: "Per-corridor 24h median of total cost percent on a $300 USDC quote from Squid Router's v2 API, averaged across supported EVM corridors (Base to Arb). Cost = fromAmountUSD minus toAmountUSD including all relayer and gas fees."
type: aggregator
queries:
p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h]))
p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h]))
p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h]))
mean: avg(avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="squid", amount_usd="300"}[24h]))
sample_size: sum(count_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[24h]))
series: avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[1h])

- slug: socket
name: Socket Protocol
tag: Aggregator
formula: "Per-corridor 24h median of total cost percent on a $300 USDC best-output route from Socket Protocol's v2 API, averaged across supported EVM corridors (Base to Arb). Routes through underlying bridges (Across, Stargate, CCTP); cost = inputValueInUsd minus outputValueInUsd."
type: aggregator
queries:
p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h]))
p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h]))
p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h]))
mean: avg(avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="socket", amount_usd="300"}[24h]))
sample_size: sum(count_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[24h]))
series: avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[1h])

- slug: mayan
name: Mayan Finance
tag: Intent layer (Wormhole)
formula: "Per-corridor 24h median of total cost percent on a $300 USDC quote from Mayan Finance's v3 API (Swift fast path), averaged across supported Solana corridors (Sol to Base, Arb to Sol). Cost = input minus expectedAmountOut including relayer and swap fees."
type: intent
queries:
p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mayan", amount_usd="300"}[24h]))
p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="mayan", amount_usd="300"}[24h]))
p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="mayan", amount_usd="300"}[24h]))
mean: avg(avg_over_time(bridge_cost_percent{bridge="mayan", amount_usd="300"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="mayan", amount_usd="300"}[24h]))
sample_size: sum(count_over_time(bridge_cost_percent{bridge="mayan", amount_usd="300"}[24h]))
series: avg_over_time(bridge_cost_percent{bridge="mayan", amount_usd="300"}[1h])
39 changes: 39 additions & 0 deletions benchmarks/bridge-quote-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,42 @@ providers:
success: avg(avg_over_time(bridge_quote_success{bridge="across"}[24h]))
sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="across"}[24h]))
series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="across"}[1h])))

- slug: squid
name: Squid Router
tag: Aggregator (Axelar)
formula: "Median wall-clock ms over 24h for Squid Router's v2 API to return a best-output USDC route, sampled every 5 minutes across supported EVM corridors (Base to Arb) and 3 notionals from 3 regions. Solana corridors excluded: Squid v2 Solana source returns a server-side error."
queries:
p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h])))
p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h])))
p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h])))
mean: sum(rate(bridge_quote_latency_ms_sum{bridge="squid"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="squid"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="squid"}[24h]))
sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="squid"}[24h]))
series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[1h])))

- slug: socket
name: Socket Protocol
tag: Aggregator
formula: "Median wall-clock ms over 24h for Socket Protocol's v2 API to return the best-output USDC route across underlying bridges (Across, Stargate, CCTP), sampled every 5 minutes across supported EVM corridors (Base to Arb) and 3 notionals from 3 regions. Solana is receive-only in Socket's chain list."
queries:
p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h])))
p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h])))
p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h])))
mean: sum(rate(bridge_quote_latency_ms_sum{bridge="socket"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="socket"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="socket"}[24h]))
sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="socket"}[24h]))
series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[1h])))

- slug: mayan
name: Mayan Finance
tag: Intent layer (Wormhole)
formula: "Median wall-clock ms over 24h for Mayan Finance's v3 quote API to return a Swift or MCTP USDC route, sampled every 5 minutes across supported Solana corridors (Sol to Base, Arb to Sol) and 3 notionals from 3 regions. Pure EVM corridors are outside Mayan's scope."
queries:
p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mayan"}[24h])))
p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mayan"}[24h])))
p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mayan"}[24h])))
mean: sum(rate(bridge_quote_latency_ms_sum{bridge="mayan"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="mayan"}[24h]))
success: avg(avg_over_time(bridge_quote_success{bridge="mayan"}[24h]))
sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="mayan"}[24h]))
series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mayan"}[1h])))
4 changes: 4 additions & 0 deletions harnesses/bridge-monitor/cmd/monitor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Config struct {
DebridgeAPIKey string
LiFiAPIKey string
NearIntentsAPIKey string
SquidIntegratorID string
SocketAPIKey string

// Wallet Configuration
WalletEVMPrivateKey string
Expand Down Expand Up @@ -74,6 +76,8 @@ func loadEnv() (*Config, error) {
config.DebridgeAPIKey = strings.TrimSpace(os.Getenv("DEBRIDGE_API_KEY"))
config.LiFiAPIKey = strings.TrimSpace(os.Getenv("LIFI_API_KEY"))
config.NearIntentsAPIKey = strings.TrimSpace(os.Getenv("NEARINTENTS_API_KEY"))
config.SquidIntegratorID = strings.TrimSpace(os.Getenv("SQUID_INTEGRATOR_ID"))
config.SocketAPIKey = strings.TrimSpace(os.Getenv("SOCKET_API_KEY"))

// Wallet configuration
config.WalletEVMPrivateKey = strings.TrimSpace(os.Getenv("WALLET_EVM_PRIVATE_KEY"))
Expand Down
33 changes: 31 additions & 2 deletions harnesses/bridge-monitor/cmd/monitor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ func main() {
log.Println("⚠️ Near Intents bridge initialized (anonymous mode - adds ~10 bps appFee and lower solver priority)")
}

squidBridge := NewSquidBridge(config.SquidIntegratorID)
log.Println("✅ Squid bridge initialized (public integrator ID)")

socketBridge := NewSocketBridge(config.SocketAPIKey)
log.Println("✅ Socket bridge initialized (public demo key)")

mayanBridge := NewMayanBridge()
log.Println("✅ Mayan bridge initialized (no key needed)")

// Initialize wallet manager
walletManager, err := NewWalletManager(config)
if err != nil {
Expand Down Expand Up @@ -233,7 +242,7 @@ func main() {
}

// Run quote tests immediately on startup (all routes)
runQuoteTests(mobulaBridge, relayBridge, debridgeBridge, lifiBridge, acrossBridge, nearIntentsBridge, allRoutes, config.MonitorRegion, solAddress, evmAddress)
runQuoteTests(mobulaBridge, relayBridge, debridgeBridge, lifiBridge, acrossBridge, nearIntentsBridge, squidBridge, socketBridge, mayanBridge, allRoutes, config.MonitorRegion, solAddress, evmAddress)

// If in dry-run mode, run a single dry-run test
if config.ExecutionMode == "dry-run" && executor != nil {
Expand Down Expand Up @@ -281,7 +290,7 @@ func main() {
done := make(chan struct{})
go func() {
defer close(done)
runQuoteTests(mobulaBridge, relayBridge, debridgeBridge, lifiBridge, acrossBridge, nearIntentsBridge, GetTestRoutes(), config.MonitorRegion, solAddress, evmAddress)
runQuoteTests(mobulaBridge, relayBridge, debridgeBridge, lifiBridge, acrossBridge, nearIntentsBridge, squidBridge, socketBridge, mayanBridge, GetTestRoutes(), config.MonitorRegion, solAddress, evmAddress)
}()
select {
case <-done:
Expand Down Expand Up @@ -568,6 +577,9 @@ func runQuoteTests(
lifiBridge *LiFiBridge,
acrossBridge *AcrossBridge,
nearIntentsBridge *NearIntentsBridge,
squidBridge *SquidBridge,
socketBridge *SocketBridge,
mayanBridge *MayanBridge,
routes []TestRoute, region, solAddress, evmAddress string,
) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
Expand Down Expand Up @@ -613,6 +625,23 @@ func runQuoteTests(
// honest (no fake unsupported-route failures).
nearIntentsBridge.TestRoute(route, amount, amountUsd, rawUnits, region, solAddress, evmAddress)
time.Sleep(500 * time.Millisecond)

// Squid Router (Axelar): EVM-only (Base/Arb). Solana source returns
// server-side 500 — TestRoute returns early for unsupported corridors.
squidBridge.TestRoute(route, amount, amountUsd, rawUnits, region, solAddress, evmAddress)
time.Sleep(500 * time.Millisecond)

// Socket Protocol (Bungee): EVM-only aggregator (Base/Arb).
// Returns best-output route across Across, Stargate, CCTP, etc.
socketBridge.TestRoute(route, amount, amountUsd, rawUnits, region, solAddress, evmAddress)
time.Sleep(500 * time.Millisecond)

// Mayan Finance: Solana-EVM only. TestRoute returns early for pure
// EVM corridors. May emit ROUTE_NOT_FOUND on some corridors during
// API outages; harness records those as quote_failed so success rate
// reflects availability rather than latency.
mayanBridge.TestRoute(route, amount, amountUsd, rawUnits, region, solAddress, evmAddress)
time.Sleep(500 * time.Millisecond)
}
}

Expand Down
180 changes: 180 additions & 0 deletions harnesses/bridge-monitor/cmd/monitor/mayan_bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package main

import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)

// MayanBridge integrates the Mayan Finance v3 quote API. Mayan is a Solana-
// native intent layer that routes via Wormhole (Swift fast path ~2-5s,
// MCTP Circle-based ~20s). Public REST, keyless (verified live 2026-07-27,
// though ROUTE_NOT_FOUND may be transient on some corridors — the harness
// records failures as quote_failed and excludes them from latency aggregates
// so any outage window is surfaced as a drop in success rate, not a latency
// floor).
//
// Coverage: any corridor involving Solana. EVM↔EVM routes are outside Mayan's
// product scope and return early without emitting metrics (honesty convention
// shared with Across / Near Intents).
//
// Chain name mapping follows Mayan's API conventions (lowercase, no numbers).
type MayanBridge struct {
client *http.Client
}

func NewMayanBridge() *MayanBridge {
return &MayanBridge{client: &http.Client{Timeout: 45 * time.Second}}
}

func mayanChainName(chain string) string {
switch strings.ToLower(chain) {
case "solana":
return "solana"
case "base":
return "base"
case "arbitrum":
return "arbitrum"
}
return ""
}

// mayanHasSolana returns true when at least one end of the route is Solana.
// Mayan's value prop is Solana-EVM bridging; pure EVM pairs are out of scope.
func mayanHasSolana(route TestRoute) bool {
return strings.EqualFold(route.FromChain, "Solana") || strings.EqualFold(route.ToChain, "Solana")
}

type mayanQuoteResponse struct {
// Happy path: array of route options
Routes []struct {
ExpectedAmountOut float64 `json:"expectedAmountOut"`
MiminumAmountOut float64 `json:"minimumAmountOut"`
Price float64 `json:"price"`
PriceImpact float64 `json:"priceImpact"`
SwapFee float64 `json:"swapFee"`
RedeemRelayerFee float64 `json:"redeemRelayerFee"`
SendRelayerFee float64 `json:"sendRelayerFee"`
ClientRelayerFee float64 `json:"clientRelayerFee"`
Eta float64 `json:"eta"`
Type string `json:"type"` // "SWIFT" | "MCTP" | "WH"
} `json:"routes"`
// Error shape
Code string `json:"code,omitempty"`
Message string `json:"msg,omitempty"`
}

func (m *MayanBridge) getQuote(route TestRoute, amountUsd float64) (*mayanQuoteResponse, time.Duration, error) {
start := time.Now()

params := url.Values{}
// Mayan takes the human-readable amount in USD equivalent, not raw token units.
params.Set("amount", strconv.FormatFloat(amountUsd, 'f', 2, 64))
params.Set("fromToken", route.FromToken)
params.Set("toToken", route.ToToken)
params.Set("fromChain", mayanChainName(route.FromChain))
params.Set("toChain", mayanChainName(route.ToChain))
params.Set("slippage", "0.5")
params.Set("sdkVersion", "15") // latest as of 2026-07-27

req, err := http.NewRequest(http.MethodGet, "https://price-api.mayan.finance/v3/quote?"+params.Encode(), nil)
if err != nil {
return nil, time.Since(start), fmt.Errorf("mayan request create: %w", err)
}
req.Header.Set("User-Agent", "OpenChainBench-harness/1.0")
req.Header.Set("Accept", "application/json")

resp, err := m.client.Do(req)
if err != nil {
return nil, time.Since(start), fmt.Errorf("mayan request: %w", err)
}
defer resp.Body.Close()

raw, _ := io.ReadAll(resp.Body)
latency := time.Since(start)
if resp.StatusCode != http.StatusOK {
return nil, latency, fmt.Errorf("mayan %d: %s", resp.StatusCode, truncate(string(raw), 300))
}

var out mayanQuoteResponse
if err := json.Unmarshal(raw, &out); err != nil {
return nil, latency, fmt.Errorf("mayan decode: %w", err)
}
if out.Code != "" {
return nil, latency, fmt.Errorf("mayan %s: %s", out.Code, out.Message)
}
if len(out.Routes) == 0 {
return nil, latency, fmt.Errorf("mayan: no routes returned")
}
return &out, latency, nil
}

func (m *MayanBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawUnits string, region, solAddress, evmAddress string) {
amountStr := strconv.FormatFloat(amountUsd, 'f', 0, 64)
labels := []string{"mayan", route.FromChain, route.ToChain, route.FromToken, route.ToToken, amountStr, region, route.ToChain}

// Mayan is Solana-EVM only. Skip pure-EVM corridors and HyperCore.
if !mayanHasSolana(route) {
return
}
if mayanChainName(route.FromChain) == "" || mayanChainName(route.ToChain) == "" {
return
}

result, quoteLatency, err := m.getQuote(route, amountUsd)
if err != nil {
log.Printf("[MAYAN][%s][%.0f USD] ❌ %v", route.Name, amountUsd, err)
bridgeErrors.WithLabelValues(append(labels, "quote_failed")...).Inc()
bridgeQuoteSuccess.WithLabelValues(labels...).Set(0)
return
}

bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6)
bridgeQuoteSuccess.WithLabelValues(labels...).Set(1)

// Use the best (first) route. Mayan sorts by output amount descending.
best := result.Routes[0]

// Mayan quotes output in destination token units (USDC = 6 decimals).
// expectedAmountOut is in token base units; normalise to USD (1 USDC = $1).
outUsd := best.ExpectedAmountOut / 1e6

// All-in cost: relayer fees + swap fee. Mayan does not expose a single
// USD total; we reconstruct from the fee fields (all in source token units,
// 6-decimal USDC normalised to USD here).
bridgeFeeUsd := (best.RedeemRelayerFee + best.SendRelayerFee + best.ClientRelayerFee) / 1e6
swapFeeUsd := best.SwapFee / 1e6
gasUsd := 0.0 // gas is rolled into relayer fees for Mayan
costUsd := amountUsd - outUsd
if costUsd < 0 {
costUsd = 0
}
costPct := 0.0
if amountUsd > 0 {
costPct = (costUsd / amountUsd) * 100
}
slippage := swapFeeUsd
if slippage < 0 {
slippage = 0
}

bridgeFeesUSD.WithLabelValues(labels...).Set(bridgeFeeUsd)
bridgeFeesPercent.WithLabelValues(labels...).Set((bridgeFeeUsd / amountUsd) * 100)
bridgeCostUSD.WithLabelValues(labels...).Set(costUsd)
bridgeCostPercent.WithLabelValues(labels...).Set(costPct)
bridgeSlippageUSD.WithLabelValues(labels...).Set(slippage)
bridgeGasUSD.WithLabelValues(labels...).Set(gasUsd)
bridgeFixFeeUSD.WithLabelValues(labels...).Set(0)
bridgeOutputUSD.WithLabelValues(labels...).Set(outUsd)
bridgeEstimatedTimeMs.WithLabelValues(labels...).Set(best.Eta * 1000)

log.Printf("[MAYAN][%s][%.0f USD] ✅ Quote: %dms | Cost: $%.4f (%.3f%%) | Type: %s | Est: %.0fs",
route.Name, amountUsd, quoteLatency.Milliseconds(),
costUsd, costPct, best.Type, best.Eta)
}
Loading
Loading