From 880c4af084a4baf82c39771e8f03ffbf7fa5f368 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:27:17 +0200 Subject: [PATCH 1/3] feat(proxy): selective per-host proxy for Railway IP-blocked providers - rpc-capabilities: patch http.DefaultTransport at startup with a host-keyed proxy function; PROBE_PROXY_URL + PROBE_PROXY_HOSTS env vars control which providers route through the rotating proxy vs direct connection - pm-rate-limits: add proxy.go with makeTransport() helper; UseProxy field on Venue routes blocked venues (predictit, smarkets) through PROBE_PROXY_URL; cold and ramp clients use makeTransport() so proxy applies consistently - venues.go: mark predictit + smarkets UseProxy=true; fix smarkets list URL (sort=popular was rejected by API, replaced with sort=start_datetime,id) - cronos-zkevm-rpc.yml: remove blastapi provider (no public endpoint exists), update counts from 3 to 2 providers throughout --- benchmarks/cronos-zkevm-rpc.yml | 35 +++------------ harnesses/pm-rate-limits/cmd/script/main.go | 3 +- harnesses/pm-rate-limits/cmd/script/probe.go | 2 +- harnesses/pm-rate-limits/cmd/script/proxy.go | 43 +++++++++++++++++++ harnesses/pm-rate-limits/cmd/script/ramp.go | 2 +- harnesses/pm-rate-limits/cmd/script/venues.go | 6 ++- harnesses/rpc-capabilities/cmd/script/main.go | 39 +++++++++++++++++ 7 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 harnesses/pm-rate-limits/cmd/script/proxy.go diff --git a/benchmarks/cronos-zkevm-rpc.yml b/benchmarks/cronos-zkevm-rpc.yml index 819a5429..517c293c 100644 --- a/benchmarks/cronos-zkevm-rpc.yml +++ b/benchmarks/cronos-zkevm-rpc.yml @@ -4,7 +4,7 @@ slug: cronos-zkevm-rpc number: "193" title: Fastest free Cronos zkEVM RPC, live no-key endpoint latency seo_title: "Fastest free Cronos zkEVM RPC 2026" -seo_description: "{{best_name}} leads free Cronos zkEVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Cronos zkEVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Cronos zkEVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Cronos zkEVM (chain 388) is Crypto.com's zkEVM Layer 2 built on the zkSync Era stack, distinct from Cronos EVM. CRO is the bridged gas token. Three keyless endpoints are probed continuously: the Cronos official mainnet gateway (mainnet.zkevm.cronos.org), dRPC (cronos-zkevm.drpc.org), and Blast API. + Cronos zkEVM (chain 388) is Crypto.com's zkEVM Layer 2 built on the zkSync Era stack, distinct from Cronos EVM. CRO is the bridged gas token. Two keyless endpoints are probed continuously: the Cronos official mainnet gateway (mainnet.zkevm.cronos.org) and dRPC (cronos-zkevm.drpc.org). abstract: | Per-chain member of the RPC latency cluster, extended to Cronos zkEVM. @@ -31,16 +31,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"cronos-zkevm\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"cronos-zkevm\"`. Provider coverage at launch: 2 no-key endpoints." findings: - - "{{best_name}} currently leads free Cronos zkEVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Cronos zkEVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free Cronos zkEVM RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Cronos zkEVM RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: Cronos official, dRPC, and Blast API. Every endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: Cronos official and dRPC. Every endpoint was live-verified before inclusion." - q: "Does the fastest Cronos zkEVM RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Cronos zkEVM RPC latency measured here?" @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="cronos-zkevm", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="cronos-zkevm", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Cronos zkEVM - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Cronos zkEVM endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="cronos-zkevm"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="cronos-zkevm"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="cronos-zkevm"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="cronos-zkevm"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="cronos-zkevm"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="cronos-zkevm"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="sgp"}[1h]) diff --git a/harnesses/pm-rate-limits/cmd/script/main.go b/harnesses/pm-rate-limits/cmd/script/main.go index 9a540c9a..934a5b50 100644 --- a/harnesses/pm-rate-limits/cmd/script/main.go +++ b/harnesses/pm-rate-limits/cmd/script/main.go @@ -20,6 +20,7 @@ func main() { log.SetFlags(0) log.Printf("[pm-rate-limits] starting, region=%s ramp_hour=%02d:00 UTC build=%s", currentRegion, rampHour(), buildMarker) + initProxy() cfg := loadConfig() go func() { @@ -40,7 +41,7 @@ func main() { stateByVenue[v.Slug] = v.state go initialPin(ctx, v, pinClient) go repinLoop(ctx, v, v.state, pinClient) - warmClient := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 4, IdleConnTimeout: 90 * time.Second}} + warmClient := &http.Client{Transport: makeTransport(v.UseProxy)} for _, cl := range v.Classes { go warmLoop(ctx, v, cl, v.state, warmClient) } diff --git a/harnesses/pm-rate-limits/cmd/script/probe.go b/harnesses/pm-rate-limits/cmd/script/probe.go index 03d4ca68..af8466ff 100644 --- a/harnesses/pm-rate-limits/cmd/script/probe.go +++ b/harnesses/pm-rate-limits/cmd/script/probe.go @@ -283,7 +283,7 @@ func coldLoop(ctx context.Context, v *Venue, st *venueState) { if st.getPin().Market == "" { continue } - tr := &http.Transport{DisableKeepAlives: true} + tr := makeTransport(v.UseProxy, func(t *http.Transport) { t.DisableKeepAlives = true }) client := &http.Client{Transport: tr} res := probeOnce(ctx, client, v, cl, "cold") tr.CloseIdleConnections() diff --git a/harnesses/pm-rate-limits/cmd/script/proxy.go b/harnesses/pm-rate-limits/cmd/script/proxy.go new file mode 100644 index 00000000..7fd1bfbf --- /dev/null +++ b/harnesses/pm-rate-limits/cmd/script/proxy.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "net/http" + "net/url" + "os" + "time" +) + +// proxyURL is parsed once at startup from PROBE_PROXY_URL. Nil means no proxy. +var proxyURL *url.URL + +func initProxy() { + raw := os.Getenv("PROBE_PROXY_URL") + if raw == "" { + return + } + u, err := url.Parse(raw) + if err != nil { + log.Printf("[proxy] bad PROBE_PROXY_URL %q: %v", raw, err) + return + } + proxyURL = u + log.Printf("[proxy] rotating proxy configured: %s://%s@%s", u.Scheme, u.User.Username(), u.Host) +} + +// makeTransport returns a transport with optional proxy and any extra tweaks +// applied via functional options. Call with useProxy=true for venues blocked +// from Railway IPs. +func makeTransport(useProxy bool, opts ...func(*http.Transport)) *http.Transport { + tr := &http.Transport{ + MaxIdleConnsPerHost: 4, + IdleConnTimeout: 90 * time.Second, + } + if useProxy && proxyURL != nil { + tr.Proxy = http.ProxyURL(proxyURL) + } + for _, opt := range opts { + opt(tr) + } + return tr +} diff --git a/harnesses/pm-rate-limits/cmd/script/ramp.go b/harnesses/pm-rate-limits/cmd/script/ramp.go index edb13982..e324385c 100644 --- a/harnesses/pm-rate-limits/cmd/script/ramp.go +++ b/harnesses/pm-rate-limits/cmd/script/ramp.go @@ -96,7 +96,7 @@ func runRamp(ctx context.Context, v *Venue) { } cl := v.Classes[0] url := cl.URL(pin) - client := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 32}} + client := &http.Client{Transport: makeTransport(v.UseProxy, func(t *http.Transport) { t.MaxIdleConnsPerHost = 32 })} log.Printf("[ramp][%s] start, baseline p50=%.0fms (n=%d), target %s", v.Slug, baseP50*1000, n, url) for _, rate := range v.RampRates { diff --git a/harnesses/pm-rate-limits/cmd/script/venues.go b/harnesses/pm-rate-limits/cmd/script/venues.go index a75cf8e9..4b8861e4 100644 --- a/harnesses/pm-rate-limits/cmd/script/venues.go +++ b/harnesses/pm-rate-limits/cmd/script/venues.go @@ -39,6 +39,7 @@ type Venue struct { StopOn429 bool // Kalshi: documented token bucket, stop at first 429 InvalidBody []string // body substrings that mark a probe_invalid (stale pin) RequestMutator func(*http.Request) // optional: stamps auth/custom headers before send + UseProxy bool // route all requests for this venue through PROBE_PROXY_URL state *venueState // wired at startup } @@ -155,6 +156,7 @@ func venues(cfg Config) []*Venue { PinFunc: pinPredictIt, RampRates: nil, // rate-limited: no ramp; probing at their published cap already StopOn429: true, + UseProxy: true, // Railway IPs blocked by PredictIt }, { Slug: "smarkets", @@ -169,12 +171,12 @@ func venues(cfg Config) []*Venue { }}, {Name: "list", Interval: 30 * time.Second, Timeout: 15 * time.Second, URL: func(Pin) string { - // Popular events listing, no event-ID dependency for the list probe - return "https://api.smarkets.com/v3/events/?type_domain=politics&sort=popular&per_page=20&state=upcoming" + return "https://api.smarkets.com/v3/events/?type_domain=politics&sort=start_datetime%2Cid&per_page=20&state=upcoming" }}, }, PinFunc: pinSmarkets, RampRates: []int{10, 25, 50}, + UseProxy: true, // Railway IPs blocked by Smarkets }, // Metaculus: public forecasting platform, no auth, no order book. // Primary class is price (single question endpoint). Conservative ramp diff --git a/harnesses/rpc-capabilities/cmd/script/main.go b/harnesses/rpc-capabilities/cmd/script/main.go index 9b18b764..9fed87f1 100644 --- a/harnesses/rpc-capabilities/cmd/script/main.go +++ b/harnesses/rpc-capabilities/cmd/script/main.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "net/http" + "net/url" "os" "os/signal" "strings" @@ -59,8 +61,45 @@ func normalizeRailwayRegion(raw string) string { } } +// initSelectiveProxy patches http.DefaultTransport so requests to hosts listed +// in PROBE_PROXY_HOSTS are routed through PROBE_PROXY_URL. All other requests +// go direct. This lets us unblock providers that ban Railway datacenter IPs +// without corrupting latency measurements for providers that don't. +func initSelectiveProxy() { + raw := strings.TrimSpace(os.Getenv("PROBE_PROXY_URL")) + if raw == "" { + return + } + proxyURL, err := url.Parse(raw) + if err != nil { + fmt.Printf("[proxy] bad PROBE_PROXY_URL: %v\n", err) + return + } + hostsRaw := strings.Split(os.Getenv("PROBE_PROXY_HOSTS"), ",") + hostSet := make(map[string]bool, len(hostsRaw)) + for _, h := range hostsRaw { + if h = strings.TrimSpace(h); h != "" { + hostSet[h] = true + } + } + if len(hostSet) == 0 { + fmt.Println("[proxy] PROBE_PROXY_URL set but PROBE_PROXY_HOSTS is empty — skipping") + return + } + dt := http.DefaultTransport.(*http.Transport).Clone() + dt.Proxy = func(r *http.Request) (*url.URL, error) { + if hostSet[r.URL.Hostname()] { + return proxyURL, nil + } + return nil, nil + } + http.DefaultTransport = dt + fmt.Printf("[proxy] selective proxy active for %d host(s): %v\n", len(hostSet), hostsRaw) +} + func main() { installLogCapture() // capture stdout into /logs ring buffer + initSelectiveProxy() fmt.Println("=== RPC Capabilities Harness ===") fmt.Println("OpenChainBench - public RPC latency, reliability, and archive depth.") fmt.Printf("Region: %s (set via $REGION env)\n", currentRegion) From f1710172ea8d37e686785c7f7c6e1705683a8fe3 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:27:45 +0200 Subject: [PATCH 2/3] feat(proxy): selective proxy for Railway IP-blocked providers (#2020) * feat(chains): add 10 new chain entries + logos for benches 222-231 * feat(proxy): selective per-host proxy for Railway IP-blocked providers - rpc-capabilities: patch http.DefaultTransport at startup with a host-keyed proxy function; PROBE_PROXY_URL + PROBE_PROXY_HOSTS env vars control which providers route through the rotating proxy vs direct connection - pm-rate-limits: add proxy.go with makeTransport() helper; UseProxy field on Venue routes blocked venues (predictit, smarkets) through PROBE_PROXY_URL; cold and ramp clients use makeTransport() so proxy applies consistently - venues.go: mark predictit + smarkets UseProxy=true; fix smarkets list URL (sort=popular was rejected by API, replaced with sort=start_datetime,id) - cronos-zkevm-rpc.yml: remove blastapi provider (no public endpoint exists), update counts from 3 to 2 providers throughout --- benchmarks/cronos-zkevm-rpc.yml | 35 +++------------ harnesses/pm-rate-limits/cmd/script/main.go | 3 +- harnesses/pm-rate-limits/cmd/script/probe.go | 2 +- harnesses/pm-rate-limits/cmd/script/proxy.go | 43 +++++++++++++++++++ harnesses/pm-rate-limits/cmd/script/ramp.go | 2 +- harnesses/pm-rate-limits/cmd/script/venues.go | 6 ++- harnesses/rpc-capabilities/cmd/script/main.go | 39 +++++++++++++++++ 7 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 harnesses/pm-rate-limits/cmd/script/proxy.go diff --git a/benchmarks/cronos-zkevm-rpc.yml b/benchmarks/cronos-zkevm-rpc.yml index 819a5429..517c293c 100644 --- a/benchmarks/cronos-zkevm-rpc.yml +++ b/benchmarks/cronos-zkevm-rpc.yml @@ -4,7 +4,7 @@ slug: cronos-zkevm-rpc number: "193" title: Fastest free Cronos zkEVM RPC, live no-key endpoint latency seo_title: "Fastest free Cronos zkEVM RPC 2026" -seo_description: "{{best_name}} leads free Cronos zkEVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Cronos zkEVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Cronos zkEVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Cronos zkEVM (chain 388) is Crypto.com's zkEVM Layer 2 built on the zkSync Era stack, distinct from Cronos EVM. CRO is the bridged gas token. Three keyless endpoints are probed continuously: the Cronos official mainnet gateway (mainnet.zkevm.cronos.org), dRPC (cronos-zkevm.drpc.org), and Blast API. + Cronos zkEVM (chain 388) is Crypto.com's zkEVM Layer 2 built on the zkSync Era stack, distinct from Cronos EVM. CRO is the bridged gas token. Two keyless endpoints are probed continuously: the Cronos official mainnet gateway (mainnet.zkevm.cronos.org) and dRPC (cronos-zkevm.drpc.org). abstract: | Per-chain member of the RPC latency cluster, extended to Cronos zkEVM. @@ -31,16 +31,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"cronos-zkevm\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"cronos-zkevm\"`. Provider coverage at launch: 2 no-key endpoints." findings: - - "{{best_name}} currently leads free Cronos zkEVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Cronos zkEVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free Cronos zkEVM RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Cronos zkEVM RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: Cronos official, dRPC, and Blast API. Every endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: Cronos official and dRPC. Every endpoint was live-verified before inclusion." - q: "Does the fastest Cronos zkEVM RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Cronos zkEVM RPC latency measured here?" @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="cronos-zkevm", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="cronos-zkevm", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Cronos zkEVM - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Cronos zkEVM endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="cronos-zkevm"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="cronos-zkevm"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="cronos-zkevm"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="cronos-zkevm"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="cronos-zkevm"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="cronos-zkevm"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="cronos-zkevm", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="cronos-zkevm", region="sgp"}[1h]) diff --git a/harnesses/pm-rate-limits/cmd/script/main.go b/harnesses/pm-rate-limits/cmd/script/main.go index 9a540c9a..934a5b50 100644 --- a/harnesses/pm-rate-limits/cmd/script/main.go +++ b/harnesses/pm-rate-limits/cmd/script/main.go @@ -20,6 +20,7 @@ func main() { log.SetFlags(0) log.Printf("[pm-rate-limits] starting, region=%s ramp_hour=%02d:00 UTC build=%s", currentRegion, rampHour(), buildMarker) + initProxy() cfg := loadConfig() go func() { @@ -40,7 +41,7 @@ func main() { stateByVenue[v.Slug] = v.state go initialPin(ctx, v, pinClient) go repinLoop(ctx, v, v.state, pinClient) - warmClient := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 4, IdleConnTimeout: 90 * time.Second}} + warmClient := &http.Client{Transport: makeTransport(v.UseProxy)} for _, cl := range v.Classes { go warmLoop(ctx, v, cl, v.state, warmClient) } diff --git a/harnesses/pm-rate-limits/cmd/script/probe.go b/harnesses/pm-rate-limits/cmd/script/probe.go index 03d4ca68..af8466ff 100644 --- a/harnesses/pm-rate-limits/cmd/script/probe.go +++ b/harnesses/pm-rate-limits/cmd/script/probe.go @@ -283,7 +283,7 @@ func coldLoop(ctx context.Context, v *Venue, st *venueState) { if st.getPin().Market == "" { continue } - tr := &http.Transport{DisableKeepAlives: true} + tr := makeTransport(v.UseProxy, func(t *http.Transport) { t.DisableKeepAlives = true }) client := &http.Client{Transport: tr} res := probeOnce(ctx, client, v, cl, "cold") tr.CloseIdleConnections() diff --git a/harnesses/pm-rate-limits/cmd/script/proxy.go b/harnesses/pm-rate-limits/cmd/script/proxy.go new file mode 100644 index 00000000..7fd1bfbf --- /dev/null +++ b/harnesses/pm-rate-limits/cmd/script/proxy.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "net/http" + "net/url" + "os" + "time" +) + +// proxyURL is parsed once at startup from PROBE_PROXY_URL. Nil means no proxy. +var proxyURL *url.URL + +func initProxy() { + raw := os.Getenv("PROBE_PROXY_URL") + if raw == "" { + return + } + u, err := url.Parse(raw) + if err != nil { + log.Printf("[proxy] bad PROBE_PROXY_URL %q: %v", raw, err) + return + } + proxyURL = u + log.Printf("[proxy] rotating proxy configured: %s://%s@%s", u.Scheme, u.User.Username(), u.Host) +} + +// makeTransport returns a transport with optional proxy and any extra tweaks +// applied via functional options. Call with useProxy=true for venues blocked +// from Railway IPs. +func makeTransport(useProxy bool, opts ...func(*http.Transport)) *http.Transport { + tr := &http.Transport{ + MaxIdleConnsPerHost: 4, + IdleConnTimeout: 90 * time.Second, + } + if useProxy && proxyURL != nil { + tr.Proxy = http.ProxyURL(proxyURL) + } + for _, opt := range opts { + opt(tr) + } + return tr +} diff --git a/harnesses/pm-rate-limits/cmd/script/ramp.go b/harnesses/pm-rate-limits/cmd/script/ramp.go index edb13982..e324385c 100644 --- a/harnesses/pm-rate-limits/cmd/script/ramp.go +++ b/harnesses/pm-rate-limits/cmd/script/ramp.go @@ -96,7 +96,7 @@ func runRamp(ctx context.Context, v *Venue) { } cl := v.Classes[0] url := cl.URL(pin) - client := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 32}} + client := &http.Client{Transport: makeTransport(v.UseProxy, func(t *http.Transport) { t.MaxIdleConnsPerHost = 32 })} log.Printf("[ramp][%s] start, baseline p50=%.0fms (n=%d), target %s", v.Slug, baseP50*1000, n, url) for _, rate := range v.RampRates { diff --git a/harnesses/pm-rate-limits/cmd/script/venues.go b/harnesses/pm-rate-limits/cmd/script/venues.go index a75cf8e9..4b8861e4 100644 --- a/harnesses/pm-rate-limits/cmd/script/venues.go +++ b/harnesses/pm-rate-limits/cmd/script/venues.go @@ -39,6 +39,7 @@ type Venue struct { StopOn429 bool // Kalshi: documented token bucket, stop at first 429 InvalidBody []string // body substrings that mark a probe_invalid (stale pin) RequestMutator func(*http.Request) // optional: stamps auth/custom headers before send + UseProxy bool // route all requests for this venue through PROBE_PROXY_URL state *venueState // wired at startup } @@ -155,6 +156,7 @@ func venues(cfg Config) []*Venue { PinFunc: pinPredictIt, RampRates: nil, // rate-limited: no ramp; probing at their published cap already StopOn429: true, + UseProxy: true, // Railway IPs blocked by PredictIt }, { Slug: "smarkets", @@ -169,12 +171,12 @@ func venues(cfg Config) []*Venue { }}, {Name: "list", Interval: 30 * time.Second, Timeout: 15 * time.Second, URL: func(Pin) string { - // Popular events listing, no event-ID dependency for the list probe - return "https://api.smarkets.com/v3/events/?type_domain=politics&sort=popular&per_page=20&state=upcoming" + return "https://api.smarkets.com/v3/events/?type_domain=politics&sort=start_datetime%2Cid&per_page=20&state=upcoming" }}, }, PinFunc: pinSmarkets, RampRates: []int{10, 25, 50}, + UseProxy: true, // Railway IPs blocked by Smarkets }, // Metaculus: public forecasting platform, no auth, no order book. // Primary class is price (single question endpoint). Conservative ramp diff --git a/harnesses/rpc-capabilities/cmd/script/main.go b/harnesses/rpc-capabilities/cmd/script/main.go index 9b18b764..9fed87f1 100644 --- a/harnesses/rpc-capabilities/cmd/script/main.go +++ b/harnesses/rpc-capabilities/cmd/script/main.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "net/http" + "net/url" "os" "os/signal" "strings" @@ -59,8 +61,45 @@ func normalizeRailwayRegion(raw string) string { } } +// initSelectiveProxy patches http.DefaultTransport so requests to hosts listed +// in PROBE_PROXY_HOSTS are routed through PROBE_PROXY_URL. All other requests +// go direct. This lets us unblock providers that ban Railway datacenter IPs +// without corrupting latency measurements for providers that don't. +func initSelectiveProxy() { + raw := strings.TrimSpace(os.Getenv("PROBE_PROXY_URL")) + if raw == "" { + return + } + proxyURL, err := url.Parse(raw) + if err != nil { + fmt.Printf("[proxy] bad PROBE_PROXY_URL: %v\n", err) + return + } + hostsRaw := strings.Split(os.Getenv("PROBE_PROXY_HOSTS"), ",") + hostSet := make(map[string]bool, len(hostsRaw)) + for _, h := range hostsRaw { + if h = strings.TrimSpace(h); h != "" { + hostSet[h] = true + } + } + if len(hostSet) == 0 { + fmt.Println("[proxy] PROBE_PROXY_URL set but PROBE_PROXY_HOSTS is empty — skipping") + return + } + dt := http.DefaultTransport.(*http.Transport).Clone() + dt.Proxy = func(r *http.Request) (*url.URL, error) { + if hostSet[r.URL.Hostname()] { + return proxyURL, nil + } + return nil, nil + } + http.DefaultTransport = dt + fmt.Printf("[proxy] selective proxy active for %d host(s): %v\n", len(hostSet), hostsRaw) +} + func main() { installLogCapture() // capture stdout into /logs ring buffer + initSelectiveProxy() fmt.Println("=== RPC Capabilities Harness ===") fmt.Println("OpenChainBench - public RPC latency, reliability, and archive depth.") fmt.Printf("Region: %s (set via $REGION env)\n", currentRegion) From c5165a3c21a7e06a2d0b6ae8a8be91ed8f81785a Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:53:47 +0200 Subject: [PATCH 3/3] fix(sitemap): use getBenchmarksSafe to avoid 25s timeout fallback --- src/lib/sitemap-builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index 8f83305f..ff9a3b28 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -1,7 +1,7 @@ import { readFileSync, statSync } from "node:fs"; import path from "node:path"; import type { MetadataRoute } from "next"; -import { getBenchmarks } from "@/data/benchmarks"; +import { getBenchmarksSafe } from "@/data/benchmarks"; import { getAllReports, getAllReportCategories } from "@/lib/reports/loader"; import { COMPARE_PAIRS } from "@/data/compare-pairs"; import { adHocPairs } from "@/lib/compare/adhoc-pairs"; @@ -209,7 +209,7 @@ async function buildStaticFallback(): Promise { async function buildFullSitemap(): Promise { const [benchmarks, alternatives, answers, providerSlugs] = await Promise.all([ - safeLoad("benchmarks", () => getBenchmarks(), []), + safeLoad("benchmarks", () => getBenchmarksSafe(), []), safeLoad>>( "alternatives", () => loadAllAlternatives(),