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 } diff --git a/src/app/alternatives/[slug]/page.tsx b/src/app/alternatives/[slug]/page.tsx index 3809608d..73df1a31 100644 --- a/src/app/alternatives/[slug]/page.tsx +++ b/src/app/alternatives/[slug]/page.tsx @@ -151,6 +151,10 @@ export default async function AlternativePage({ url, mainEntityOfPage: url, articleBody: alt.intro, + // Google Article rich results require image (1200px on the longer + // side). Reuse the referenced bench's OG card so the schema image + // matches what X + LinkedIn + Search Console already render. + image: `${SITE.url}/api/og/${bench.slug}`, datePublished: getBenchCreatedAt(bench.slug).toISOString(), ...(citableAsOf(bench) ? { dateModified: bench.lastRunAt } : {}), author: { "@id": `${SITE.url}/#org` }, diff --git a/src/lib/hub-jsonld.ts b/src/lib/hub-jsonld.ts index 30fc2fac..1ab36d7a 100644 --- a/src/lib/hub-jsonld.ts +++ b/src/lib/hub-jsonld.ts @@ -79,6 +79,12 @@ export function methodologyPageLd() { headline: "OpenChainBench methodology", description: "How OpenChainBench measures: design principles, statistical conventions, and reproduction guidelines.", + // Article rich results require image + datePublished. Reuse the + // page's dedicated OG image so schema.org matches the social card. + // datePublished pinned to the site launch (matches Org.foundingDate) + // since the methodology has been stable since day one. + image: `${ORIGIN}/methodology/opengraph-image`, + datePublished: "2026-04-28", author: { "@id": `${ORIGIN}/#org` }, publisher: { "@id": `${ORIGIN}/#org` }, inLanguage: "en",