Skip to content
Merged
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
138 changes: 138 additions & 0 deletions benchmarks/union-rpc.yml
Original file line number Diff line number Diff line change
@@ -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, 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).
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])
21 changes: 21 additions & 0 deletions harnesses/axelar-gmp-latency/cmd/script/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<lowered>` so we never drop data.
var axelarChainSlug = map[string]string{
// EVM L1s
Expand Down
6 changes: 4 additions & 2 deletions harnesses/axelar-gmp-latency/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions harnesses/chainlink-ccip-latency/cmd/script/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions harnesses/chainlink-ccip-latency/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions harnesses/layerzero-message-latency/cmd/script/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<lowered>` slug in
// main.go so we never drop data silently.
var lzChainSlug = map[string]string{
Expand Down
4 changes: 4 additions & 0 deletions harnesses/layerzero-message-latency/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions harnesses/rpc-capabilities/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions public/logos/highstakes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/logos/nodes-guru.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/logos/stakeandrelax.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/logos/union.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 12 additions & 3 deletions src/app/api/fee-compare/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,27 +1110,36 @@ 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<number, { open?: GainsApiTrade; close?: GainsApiTrade }>();
for (const t of trades) {
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,
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/fee-compare-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<span className="font-mono text-sm font-bold text-ink w-12 shrink-0">
<span className="font-mono text-sm font-bold text-ink w-20 shrink-0 truncate">
{c.coin}
</span>
<span className="text-xs text-ink-faint w-14 shrink-0">{c.fills} fills</span>
Expand Down
Loading