From 9fa021e5f9a8757b64df7638bbb38404cc469ade Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sat, 25 Jul 2026 20:33:19 +0200 Subject: [PATCH] add Relay provider (Solana+Base+BSC+Robinhood, no API key) --- benchmarks/token-quote-coverage.yml | 15 ++- .../token-quote-coverage/cmd/monitor/relay.go | 102 ++++++++++++++++++ .../cmd/monitor/scheduler.go | 1 + 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 harnesses/token-quote-coverage/cmd/monitor/relay.go diff --git a/benchmarks/token-quote-coverage.yml b/benchmarks/token-quote-coverage.yml index dc6066a1..d42e6925 100644 --- a/benchmarks/token-quote-coverage.yml +++ b/benchmarks/token-quote-coverage.yml @@ -141,7 +141,7 @@ rank_matrix_query: > providers: - slug: mobula name: Mobula - tag: EVM only, Base + BNB + Robinhood, API key required + tag: Multi-chain, Solana + Base + BNB + Robinhood, API key required formula: "Rolling 24h ratio of token_quote_coverage_success_total to token_quote_coverage_attempts_total for provider=mobula, across all launchpads and chains Mobula supports." queries: p50: sum(increase(token_quote_coverage_success_total{provider="mobula"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="mobula"}[24h])) * 100 @@ -191,4 +191,17 @@ providers: sample_size: sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])) series: sum(rate(token_quote_coverage_success_total{provider="paraswap"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="paraswap"}[1h])) * 100 + - slug: relay + name: Relay + tag: Multi-chain, Solana + Base + BNB + Robinhood, no key + formula: "Rolling 24h ratio for provider=relay. Covers all 4 chains including Solana (via dFlow router). No API key required." + queries: + p50: sum(increase(token_quote_coverage_success_total{provider="relay"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])) * 100 + p90: sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])) + p99: sum(increase(token_quote_coverage_success_total{provider="relay"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])) * 100 + mean: sum(increase(token_quote_coverage_success_total{provider="relay"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])) * 100 + success: clamp_max(sum(increase(token_quote_coverage_success_total{provider="relay"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])), 1) + sample_size: sum(increase(token_quote_coverage_attempts_total{provider="relay"}[24h])) + series: sum(rate(token_quote_coverage_success_total{provider="relay"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="relay"}[1h])) * 100 + diff --git a/harnesses/token-quote-coverage/cmd/monitor/relay.go b/harnesses/token-quote-coverage/cmd/monitor/relay.go new file mode 100644 index 00000000..62dae5a0 --- /dev/null +++ b/harnesses/token-quote-coverage/cmd/monitor/relay.go @@ -0,0 +1,102 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const ( + relayUSDCSolana = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + relayUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + relayUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + relayUSDGRobinhood = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168" + relaySolanaUser = "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ" + relayEVMUser = "0x0000000000000000000000000000000000000001" +) + +type RelayProvider struct { + client *http.Client +} + +func NewRelayProvider() *RelayProvider { + return &RelayProvider{client: newWarmHTTPClient()} +} + +func (p *RelayProvider) Slug() string { return "relay" } + +func (p *RelayProvider) SupportsChain(chain string) bool { + switch chain { + case "solana", "base", "bsc", "robinhood": + return true + } + return false +} + +func (p *RelayProvider) Quote(ctx context.Context, token Token) (ok bool) { + var chainId int + var tokenIn, user string + switch token.Chain { + case "solana": + chainId, tokenIn, user = 792703809, relayUSDCSolana, relaySolanaUser + case "base": + chainId, tokenIn, user = 8453, relayUSDCBase, relayEVMUser + case "bsc": + chainId, tokenIn, user = 56, relayUSDCBSC, relayEVMUser + case "robinhood": + chainId, tokenIn, user = 4663, relayUSDGRobinhood, relayEVMUser + default: + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + payload, _ := json.Marshal(map[string]interface{}{ + "user": user, + "originChainId": chainId, + "destinationChainId": chainId, + "originCurrency": tokenIn, + "destinationCurrency": token.Address, + "amount": "1000000", + "tradeType": "EXACT_INPUT", + }) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://api.relay.link/quote", bytes.NewReader(payload)) + if err != nil { + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + req.Header.Set("Content-Type", "application/json") + + resp, err := p.client.Do(req) + if err != nil { + fmt.Printf("[relay] %s/%s net error: %s\n", token.Chain, token.Address, classifyNetErr(err)) + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + var r struct { + Details struct { + CurrencyOut struct { + Amount string `json:"amount"` + } `json:"currencyOut"` + } `json:"details"` + Message string `json:"message"` + } + if err := json.Unmarshal(body, &r); err != nil { + fmt.Printf("[relay] %s/%s parse error: %v\n", token.Chain, token.Address, err) + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + ok = r.Details.CurrencyOut.Amount != "" && r.Details.CurrencyOut.Amount != "0" + if !ok && r.Message != "" { + fmt.Printf("[relay] %s/%s: %s\n", token.Chain, token.Address, r.Message) + } + RecordProbe(p.Slug(), token.Venue, token.Chain, ok) + return ok +} diff --git a/harnesses/token-quote-coverage/cmd/monitor/scheduler.go b/harnesses/token-quote-coverage/cmd/monitor/scheduler.go index b74ce06b..c94eec12 100644 --- a/harnesses/token-quote-coverage/cmd/monitor/scheduler.go +++ b/harnesses/token-quote-coverage/cmd/monitor/scheduler.go @@ -20,6 +20,7 @@ func buildProviders(cfg *Config) []Provider { NewMobulaProvider(cfg.MobulaAPIKey), NewKyberSwapProvider(), NewParaSwapProvider(), + NewRelayProvider(), } return providers }