From 1978cfc4cfc461837d61cfc071880a7c6f959ede Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:40:29 +0200 Subject: [PATCH 1/5] =?UTF-8?q?merge:=20dev=20=E2=86=92=20main=20(Gains=20?= =?UTF-8?q?carry=20fix=20+=20vault=20fee=20split)=20(#2191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease --- .../axelar-gmp-latency/cmd/script/chains.go | 21 +++++++++++ .../axelar-gmp-latency/cmd/script/main.go | 6 ++-- .../cmd/script/chains.go | 26 ++++++++++++++ .../chainlink-ccip-latency/cmd/script/main.go | 4 +++ .../cmd/script/chains.go | 29 +++++++++++++++ .../cmd/script/main.go | 4 +++ src/app/api/fee-compare/route.ts | 15 ++++++-- src/lib/aggregate-blob.ts | 35 +++++++++++++++++-- 8 files changed, 133 insertions(+), 7 deletions(-) diff --git a/harnesses/axelar-gmp-latency/cmd/script/chains.go b/harnesses/axelar-gmp-latency/cmd/script/chains.go index 2c83c54f4..2b62f1769 100644 --- a/harnesses/axelar-gmp-latency/cmd/script/chains.go +++ b/harnesses/axelar-gmp-latency/cmd/script/chains.go @@ -14,6 +14,27 @@ import "strings" // - Cosmos chains (osmosis, injective, sei, celestia, kava) are // Axelar-exclusive coverage vs Wormhole/LayerZero/CCIP/Hyperlane // +// axelarTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed, capping cardinality at len²×buckets. +// Derived from the bench YAML provider slugs. +var axelarTrackedChains = map[string]bool{ + "ethereum": true, + "polygon": true, + "base": true, + "moonbeam": true, + "osmosis": true, + "arbitrum": true, + "avalanche": true, + "bnb": true, + "celo": true, + "injective": true, + "linea": true, + "mantle": true, + "optimism": true, + "scroll": true, +} + // Unknown names fall through to `chain-` so we never drop data. var axelarChainSlug = map[string]string{ // EVM L1s diff --git a/harnesses/axelar-gmp-latency/cmd/script/main.go b/harnesses/axelar-gmp-latency/cmd/script/main.go index a3652e996..55ff6b41a 100644 --- a/harnesses/axelar-gmp-latency/cmd/script/main.go +++ b/harnesses/axelar-gmp-latency/cmd/script/main.go @@ -173,8 +173,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { totalMs := float64(m.TimeSpent.Total) * 1000 if totalMs > 0 && totalMs <= maxLatencyMs { dst := canonicalizeAxelarChain(m.Call.ReturnValues.DestinationChain) - axelarE2ELatencyMs.WithLabelValues(src, dst).Observe(totalMs) - axelarSeenTotal.WithLabelValues(src, dst).Inc() + if axelarTrackedChains[src] && axelarTrackedChains[dst] { + axelarE2ELatencyMs.WithLabelValues(src, dst).Observe(totalMs) + axelarSeenTotal.WithLabelValues(src, dst).Inc() + } } seen.add(m.ID) diff --git a/harnesses/chainlink-ccip-latency/cmd/script/chains.go b/harnesses/chainlink-ccip-latency/cmd/script/chains.go index e551d3c46..bb3a17fed 100644 --- a/harnesses/chainlink-ccip-latency/cmd/script/chains.go +++ b/harnesses/chainlink-ccip-latency/cmd/script/chains.go @@ -12,6 +12,32 @@ package main // makes the mapping legible and the audit trail obvious when CCIP // adds a new chain we didn't anticipate. // +// ccipTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed, capping cardinality at len²×buckets. +// Derived from the bench YAML provider slugs. +var ccipTrackedChains = map[string]bool{ + "ethereum": true, + "bnb": true, + "polygon": true, + "avalanche": true, + "arbitrum": true, + "base": true, + "robinhood": true, + "berachain": true, + "celo": true, + "ink": true, + "linea": true, + "mantle": true, + "monad": true, + "moonbeam": true, + "optimism": true, + "scroll": true, + "solana": true, + "unichain": true, + "world-chain": true, +} + // Only mainnet entries are mapped; testnet rows are dropped in main.go // via the `environment != "mainnet"` guard so we never emit test-chain // latency. diff --git a/harnesses/chainlink-ccip-latency/cmd/script/main.go b/harnesses/chainlink-ccip-latency/cmd/script/main.go index 19b6e3c8d..c4a0cece9 100644 --- a/harnesses/chainlink-ccip-latency/cmd/script/main.go +++ b/harnesses/chainlink-ccip-latency/cmd/script/main.go @@ -189,6 +189,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { seen.add(m.MessageID) continue } + if !ccipTrackedChains[srcSlug] || !ccipTrackedChains[dstSlug] { + seen.add(m.MessageID) + continue + } ccipLatencyMs.WithLabelValues(srcSlug, dstSlug).Observe(deltaMs) ccipSeenTotal.WithLabelValues(srcSlug, dstSlug).Inc() seen.add(m.MessageID) diff --git a/harnesses/layerzero-message-latency/cmd/script/chains.go b/harnesses/layerzero-message-latency/cmd/script/chains.go index 2c8887410..ded442922 100644 --- a/harnesses/layerzero-message-latency/cmd/script/chains.go +++ b/harnesses/layerzero-message-latency/cmd/script/chains.go @@ -10,6 +10,35 @@ package main // - LayerZero exposes a bunch of exotic chains (orderly, flare, ape, // robinhood, hyperliquid) that map to our slugs where they exist. // +// lzTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed. This caps cardinality at len²×buckets instead of the full +// N×N cross-product of all chains LayerZero supports. +// Derived from the bench YAML provider slugs — add here when adding a +// new chain to the bench. +var lzTrackedChains = map[string]bool{ + "ethereum": true, + "solana": true, + "bnb": true, + "arbitrum": true, + "base": true, + "optimism": true, + "polygon": true, + "avalanche": true, + "robinhood": true, + "monad": true, + "berachain": true, + "celo": true, + "injective": true, + "ink": true, + "linea": true, + "mantle": true, + "moonbeam": true, + "scroll": true, + "sui": true, + "unichain": true, +} + // Unknown names fall through to a synthetic `chain-` slug in // main.go so we never drop data silently. var lzChainSlug = map[string]string{ diff --git a/harnesses/layerzero-message-latency/cmd/script/main.go b/harnesses/layerzero-message-latency/cmd/script/main.go index 5f7a4f6d9..3ccfc0d07 100644 --- a/harnesses/layerzero-message-latency/cmd/script/main.go +++ b/harnesses/layerzero-message-latency/cmd/script/main.go @@ -206,6 +206,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { continue } dstSlug := chainSlug(m.Pathway.Receiver.Chain) + if !lzTrackedChains[srcSlug] || !lzTrackedChains[dstSlug] { + seen.add(m.GUID) + continue + } lzLatencyMs.WithLabelValues(srcSlug, dstSlug).Observe(deltaMs) lzSeenTotal.WithLabelValues(srcSlug, dstSlug).Inc() seen.add(m.GUID) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index cd724592e..b4f9e2e12 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1110,7 +1110,10 @@ function augmentWithHlOpenPositions( } function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): PositionSlice[] { - const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted"]); + // v5 names: MarketOpened, LimitOrderExecuted — v6 names: TradeOpenedMarket, TradeOpenedLimit + const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted", "TradeOpenedMarket", "TradeOpenedLimit"]); + // TradePosSizeIncrease updates the position size; use latest size as notional + const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); const byId = new Map(); @@ -1118,19 +1121,25 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P if (!byId.has(t.id)) byId.set(t.id, {}); const e = byId.get(t.id)!; if (OPEN_ACTIONS.has(t.action)) e.open = t; + else if (INCREASE_ACTIONS.has(t.action) && e.open) { + e.open = { ...e.open, size: t.size, leverage: t.leverage }; + } else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } + const now = Date.now(); const slices: PositionSlice[] = []; for (const { open, close } of byId.values()) { - if (!open || !close) continue; + if (!open) continue; const openMs = new Date(open.date).getTime(); if (openMs < cutoffMs) continue; + // Still-open positions use now as close time (same as reconstructHlPositions) + const closeMs = close ? new Date(close.date).getTime() : now; slices.push({ coin: open.pair.split("/")[0], notionalUsd: open.size * open.leverage, openMs, - closeMs: new Date(close.date).getTime(), + closeMs, isLong: open.buy !== false, }); } diff --git a/src/lib/aggregate-blob.ts b/src/lib/aggregate-blob.ts index 0c2ff32e5..f920fb1fd 100644 --- a/src/lib/aggregate-blob.ts +++ b/src/lib/aggregate-blob.ts @@ -26,6 +26,37 @@ import type { Benchmark } from "@/types/benchmark"; import { loadSpecsUncached } from "@/lib/materialize/load"; import { overlayEditorial, slimBenchmarkForCache } from "@/lib/spec"; +// Aggressive slim for the aggregate blob. Hub pages (homepage, categories, +// chains, products) only need card data — they never render editorial text +// or metric panels. Stripping these fields drops the serialized aggregate +// from ~4.3 MB to well under the 2 MB unstable_cache ceiling. +// +// Fields stripped beyond slimBenchmarkForCache (which already removes +// 7d/30d series): +// - extras.seriesByRegion24h (only used on bench detail pages) +// - metricPanels (only used on bench detail pages) +// - seoIntro, faq, disclaimer (editorial, bench detail only) +// - perChainExplainer (bench detail + worker's sitemap.json handles sitemap) +// - findings, methodology (bench detail only; required fields → []) +// - abstract (bench detail only; required field → "") +function slimForBlobAggregate(b: Benchmark): Benchmark { + const base = slimBenchmarkForCache(b); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { seriesByRegion24h: _sbr, ...slimExtras } = base.extras; + return { + ...base, + extras: slimExtras, + metricPanels: undefined, + seoIntro: undefined, + faq: undefined, + perChainExplainer: undefined, + disclaimer: undefined, + findings: [], + methodology: [], + abstract: "", + }; +} + // On any Vercel deployment (production or preview), use the self-hosted // CDN proxy (/api/aggregate on openchainbench.com) so Vercel functions // pay ~1 ms (edge cache hit) instead of ~12 s fetching the 7.5 MB blob @@ -113,7 +144,7 @@ async function fetchAndProject(): Promise { for (const bench of raw.benches) { const spec = specBySlug.get(bench.slug); if (!spec) continue; // Bench in blob no longer has a spec — skip. - projected.push(slimBenchmarkForCache(overlayEditorial(bench, spec))); + projected.push(slimForBlobAggregate(overlayEditorial(bench, spec))); } return projected.sort((a, b) => (a.number ?? "").localeCompare(b.number ?? ""), @@ -130,6 +161,6 @@ async function fetchAndProject(): Promise { */ export const loadAggregateFromBlob = unstable_cache( fetchAndProject, - ["aggregate-blob-v2"], + ["aggregate-blob-v3"], { revalidate: 60, tags: ["bench-aggregate", "benchmarks"] }, ); From cf6906b552c7cefe76d577ced80039d109886bbb Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:02:33 +0200 Subject: [PATCH 2/5] feat(rpc): Union bench #253 (#2195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) --- benchmarks/union-rpc.yml | 138 ++++++++++++++++++ .../rpc-capabilities/cmd/script/config.go | 11 ++ public/logos/union.svg | 4 + src/components/fee-compare-client.tsx | 2 +- src/data/provider-registry.ts | 20 +++ src/lib/brand.ts | 3 +- src/lib/logo-manifest.ts | 1 + 7 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 benchmarks/union-rpc.yml create mode 100644 public/logos/union.svg diff --git a/benchmarks/union-rpc.yml b/benchmarks/union-rpc.yml new file mode 100644 index 000000000..0373e4819 --- /dev/null +++ b/benchmarks/union-rpc.yml @@ -0,0 +1,138 @@ +# OpenChainBench. Bench No 253 + +slug: union-rpc +number: "253" +title: Fastest free Union RPC, live no-key endpoint latency +seo_title: "Fastest free Union RPC 2026" +seo_description: "{{best_name}} leads free Union RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public Union (union-1) endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification — no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Union (union-1). + We measure the round-trip latency of a Tendermint /status query against + every available public Union endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (20 blocks, around 80 s at 4 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Union-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field (string-encoded integer) is extracted as the current block height." + - "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 (parsable block height returned), http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain 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=union. Provider coverage at launch: 3 endpoints (Nodes.Guru, Stake And Relax, High Stakes)." + +findings: + - "{{best_name}} leads free Union RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Union RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Union block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Union RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Nodes.Guru (rpc-1.union.nodes.guru), Stake And Relax (union-rpc.stakeandrelax.net), and High Stakes (union-rpc.highstakes.ch). Every listed endpoint was live-verified before inclusion." + - q: "What is Union and why does its RPC latency matter?" + a: "Union is a ZK-based cross-chain interoperability protocol with a native Cosmos SDK chain (union-1). It connects blockchains without trusted intermediaries by verifying consensus proofs on-chain. Developers building cross-chain applications, bridges, or omnichain protocols on Union need reliable low-latency RPC access to query transactions, proofs, and chain state." + - q: "Does the fastest Union RPC change by region?" + a: "Often. Community validators like Nodes.Guru and High Stakes host in different datacentres. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." + - q: "What makes Union different from other cross-chain protocols?" + a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions — the security of the bridge reduces to the security of the underlying chains and ZK proof system." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="union"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: nodes-guru + name: Nodes.Guru + tag: Nodes.Guru public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc-1.union.nodes.guru." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="nodes-guru", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="nodes-guru", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="nodes-guru", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="nodes-guru", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="nodes-guru", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="nodes-guru", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="sgp"}[1h]) + + - slug: stakeandrelax + name: Stake And Relax + tag: Stake And Relax public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to union-rpc.stakeandrelax.net." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="stakeandrelax", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="stakeandrelax", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="stakeandrelax", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="stakeandrelax", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="stakeandrelax", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="stakeandrelax", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="sgp"}[1h]) + + - slug: highstakes + name: High Stakes + tag: High Stakes public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to union-rpc.highstakes.ch." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="highstakes", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="highstakes", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="highstakes", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="highstakes", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="highstakes", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="highstakes", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index c985e4479..186460674 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1693,6 +1693,17 @@ func chains() []Chain { {Slug: "cosmos-directory", Name: "Cosmos Directory", URL: envDefault("RPC_URL_SENTINEL_COSMOSDIRECTORY", "https://rpc.cosmos.directory/sentinel")}, }, }, + // 2026-08-29 wave-12. Union — Cosmos SDK (union-1), Tendermint /status. Nodes.Guru + Stake And Relax + High Stakes. + { + Slug: "union", + Name: "Union", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "nodes-guru", Name: "Nodes.Guru", URL: envDefault("RPC_URL_UNION_NODESGURU", "https://rpc-1.union.nodes.guru")}, + {Slug: "stakeandrelax", Name: "Stake And Relax", URL: envDefault("RPC_URL_UNION_STAKEANDRELAX", "https://union-rpc.stakeandrelax.net")}, + {Slug: "highstakes", Name: "High Stakes", URL: envDefault("RPC_URL_UNION_HIGHSTAKES", "https://union-rpc.highstakes.ch")}, + }, + }, // 2026-08-28 wave-11. Fetch.ai (FetchHub-4) — Cosmos SDK, Tendermint /status. Official + PublicNode + Cosmos Directory. { Slug: "fetchhub", diff --git a/public/logos/union.svg b/public/logos/union.svg new file mode 100644 index 000000000..a1957ae25 --- /dev/null +++ b/public/logos/union.svg @@ -0,0 +1,4 @@ + + + UNO + diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index 6e931a1c0..a7ada7a71 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -956,7 +956,7 @@ function HlTopCoinsCard({ key={c.coin} className="flex items-center gap-3 px-5 py-3 hover:bg-ink/2 transition-colors" > - + {c.coin} {c.fills} fills diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index a3e40769c..9046f4ba6 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2619,6 +2619,26 @@ export const PROVIDER_REGISTRY: Record = { "Fetch.ai official public Tendermint RPC node for the FetchHub-4 mainnet. Keyless endpoint maintained by the Fetch.ai / ASI Alliance team.", twitter: "@Fetch_ai", }, + + // ─── Union providers (bench 253) ───────────────────────────────────── + "nodes-guru": { + url: "https://nodes.guru", + description: + "Nodes.Guru community validator and public RPC operator. Runs keyless Tendermint RPC endpoints for multiple Cosmos SDK chains including Union.", + twitter: "@nodes_guru", + }, + stakeandrelax: { + url: "https://stakeandrelax.net", + description: + "Stake And Relax community validator providing public keyless Tendermint RPC for Cosmos SDK chains including Union.", + twitter: "@StakeAndRelax", + }, + highstakes: { + url: "https://highstakes.ch", + description: + "High Stakes Swiss validator and public RPC operator. Provides keyless Tendermint RPC endpoints for multiple Cosmos SDK chains including Union.", + twitter: "@HighStakesCH", + }, }; /** diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 8fb6c311a..453dc405d 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -173,11 +173,12 @@ const BRANDS: Record = { acala: { color: "#E40C5B" }, // acala red/pink (official brand) interlay: { color: "#1A3BDB" }, // interlay blue (official brand) - // ─── Cosmos SDK chains (benches 247, 250-252) ─── + // ─── Cosmos SDK chains (benches 247, 250-253) ─── babylon: { color: "#F8811A" }, // babylon orange (official brand) chihuahua: { color: "#E05F2A" }, // chihuahua orange-red (official brand) sentinel: { color: "#00C2FF" }, // sentinel cyan (official brand) fetchhub: { color: "#3B2D8E" }, // fetch.ai deep purple (official brand) + union: { color: "#6366F1" }, // union indigo (brand kit) "cosmos-directory": { color: "#1B1B2F", dark: true }, // cosmos directory dark navy // ─── Bitcoin Cash chain + providers (bench 244) ─── diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 3fca81c47..d221da4df 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -331,6 +331,7 @@ const RAW: Record = { chihuahua: "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/logos/chihuahua.svg", sentinel: "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/logos/sentinel.svg", fetchhub: "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/logos/fetchai.svg", + union: "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/logos/union.svg", "cosmos-directory": "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/logos/cosmos-directory.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── From 4a47227a2f0f2c3c6c696a8aea2b8649ae7ad034 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:13:26 +0200 Subject: [PATCH 3/5] fix(union-rpc): remove em dashes from YAML --- benchmarks/union-rpc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/union-rpc.yml b/benchmarks/union-rpc.yml index 0373e4819..c075123a4 100644 --- a/benchmarks/union-rpc.yml +++ b/benchmarks/union-rpc.yml @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification — no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. + Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification, with no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. abstract: | Per-chain member of the RPC latency cluster, extended to Union (union-1). @@ -50,7 +50,7 @@ faq: - q: "Does the fastest Union RPC change by region?" a: "Often. Community validators like Nodes.Guru and High Stakes host in different datacentres. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." - q: "What makes Union different from other cross-chain protocols?" - a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions — the security of the bridge reduces to the security of the underlying chains and ZK proof system." + a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions: the security of the bridge reduces to the security of the underlying chains and ZK proof system." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities From 407e002c2526afe7f5cb5b1ebbfa6069b83fc144 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:36:33 +0200 Subject: [PATCH 4/5] feat(union): add provider SVG logos and brand colors --- public/logos/highstakes.svg | 5 +++++ public/logos/nodes-guru.svg | 5 +++++ public/logos/stakeandrelax.svg | 5 +++++ src/lib/brand.ts | 5 ++++- src/lib/logo-manifest.ts | 3 +++ 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 public/logos/highstakes.svg create mode 100644 public/logos/nodes-guru.svg create mode 100644 public/logos/stakeandrelax.svg diff --git a/public/logos/highstakes.svg b/public/logos/highstakes.svg new file mode 100644 index 000000000..220f0fad4 --- /dev/null +++ b/public/logos/highstakes.svg @@ -0,0 +1,5 @@ + + + HIGH + STAKES + diff --git a/public/logos/nodes-guru.svg b/public/logos/nodes-guru.svg new file mode 100644 index 000000000..c55d6fd0c --- /dev/null +++ b/public/logos/nodes-guru.svg @@ -0,0 +1,5 @@ + + + NODES + GURU + diff --git a/public/logos/stakeandrelax.svg b/public/logos/stakeandrelax.svg new file mode 100644 index 000000000..3664a52dd --- /dev/null +++ b/public/logos/stakeandrelax.svg @@ -0,0 +1,5 @@ + + + STAKE + RELAX + diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 453dc405d..bb62f3166 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -178,7 +178,10 @@ const BRANDS: Record = { chihuahua: { color: "#E05F2A" }, // chihuahua orange-red (official brand) sentinel: { color: "#00C2FF" }, // sentinel cyan (official brand) fetchhub: { color: "#3B2D8E" }, // fetch.ai deep purple (official brand) - union: { color: "#6366F1" }, // union indigo (brand kit) + union: { color: "#6366F1" }, // union indigo (brand kit) + "nodes-guru": { color: "#F59E0B" }, // nodes.guru amber + stakeandrelax: { color: "#10B981" }, // stake and relax emerald + highstakes: { color: "#3B82F6" }, // high stakes blue "cosmos-directory": { color: "#1B1B2F", dark: true }, // cosmos directory dark navy // ─── Bitcoin Cash chain + providers (bench 244) ─── diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index d221da4df..6aa6d2b7a 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -332,6 +332,9 @@ const RAW: Record = { sentinel: "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/logos/sentinel.svg", fetchhub: "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/logos/fetchai.svg", union: "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/logos/union.svg", + "nodes-guru": "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/logos/nodes-guru.svg", + stakeandrelax: "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/logos/stakeandrelax.svg", + highstakes: "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/logos/highstakes.svg", "cosmos-directory": "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/logos/cosmos-directory.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── From 3822048b08f5fa6b6cc6da639d574fe521e57c95 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:41:13 +0200 Subject: [PATCH 5/5] fix(citation): allow negative p50 for deviation benches --- src/lib/citation.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/citation.ts b/src/lib/citation.ts index 672811f94..f52e76857 100644 --- a/src/lib/citation.ts +++ b/src/lib/citation.ts @@ -302,9 +302,12 @@ export function isInsufficient(b: InsufficientCheckInput): boolean { // benches as insufficient on /api/citable while /api/stat returned // live values for the same slug. The liveResults length and p50 // finiteness checks below already catch the genuine empty case. + // Use isFinite rather than > 0 so deviation benches (where a negative + // p50 is valid data, e.g. rwa-yield-accuracy reporting -6 bps) are not + // mis-classified as insufficient. const live = b.results.filter( - (r) => r.availability !== "unavailable" && r.ms.p50 > 0, + (r) => r.availability !== "unavailable" && Number.isFinite(r.ms.p50) && r.ms.p50 !== 0, ); if (live.length === 0) return true; - return live.every((r) => !Number.isFinite(r.ms.p50) || r.ms.p50 <= 0); + return live.every((r) => !Number.isFinite(r.ms.p50)); }