Skip to content

sync: dev → main - #1240

Closed
Flotapponnier wants to merge 96 commits into
mainfrom
dev
Closed

sync: dev → main#1240
Flotapponnier wants to merge 96 commits into
mainfrom
dev

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Full sync of `dev` into `main`. Includes this session's fixes plus other in-flight work already on dev.

This session's fixes (already cherry-picked to main separately)

Other dev commits pending on main

After merge

Run `vercel --prod` from a fresh checkout of `main` to deploy the site changes to production.

Florent Tapponnier and others added 30 commits July 12, 2026 21:39
…#1136)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
Robinhood Chain: hub entry + 3 bench integrations
…cution mutex primitive

DailySpentUSD only ever incremented and was zeroed by every restart, so a
crash-loop minted a fresh budget each time. The tracker resets exactly at
UTC midnight, persists (date, spent) to SPEND_STATE_PATH (default
./spend-state.json, not /tmp) and is mutex-guarded. Also declares the
package-level single-flight lock and the FAILED_TX_FEE_ESTIMATE_USD
(default 0.50) helper for gas bled by failed broadcasts.
…get, single-flight lock

Fund-loss fixes from hostile review:
- a corrective transfer whose deposit broadcast but whose bridge status
  never resolved is now TERMINAL for the scheduler slot (funds are likely
  still in flight, a retry double-sends); only pre-broadcast failures may
  consume another attempt. executeOnBridge keeps TxHash on error paths so
  callers can tell the two apart.
- ONE corrective-transfer budget (max 2) per scheduler slot, shared across
  all downgrade-ladder rungs (was per rung: up to 6 transfers per slot).
- package-level execution mutex: scheduler slots own the wallets end to
  end, the reaper TryLocks and skips its tick instead of queueing.
- broadcasts that failed or timed out after getting a TxHash book a flat
  FAILED_TX_FEE_ESTIMATE_USD against the daily cap instead of bleeding
  unaccounted gas; all spend goes through the persisted UTC-day tracker.
- gas top-up now honors MaxDailySpendUSD before broadcasting.
The reaper (and any other consumer) used to accept simulated balances even
in production with broadcast-capable keys, so phantom numbers could
green-light real transfers. SimulateBalances now takes the execution mode
and returns nil outside dry-run; startup logs a warning when the flag is
set in a broadcast-capable mode.
bridge-monitor: self-healing execution loop (dormant)
…st-fill (#1141)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
tokenized-stock-peg: bench 076, first onchain equity pricing bench
…he miss (bnb-rpc awaiting incident) (#1144)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
tokenized-stock-peg: fix spark parser for the flat response shape
tokenized-stock-peg: spec validation fix + methodology hardening
Flotapponnier and others added 28 commits July 16, 2026 12:50
…ges==0) (#1195)

Companion to feed-down banner: makes the 40min Codex blackout from
this morning (08:23-09:04 UTC) visible on the 24h chart instead of a
flat line covering the outage. Per-series match preserves the
aggregate line as long as one chain still receives events.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Per-chain siblings of bench 081 for the WebSocket head-lag cluster.
Same harness (harnesses/ws-head-latency), same race scoring, one
spec per chain to avoid mixing block cadences (Ethereum 12s vs Base
2s vs Solana 400ms) in a single leaderboard.

Base cohort: PublicNode + dRPC keyless endpoints. Tenderly does
not expose a keyless Base WebSocket path.

Solana cohort: PublicNode + Solana Foundation. slotSubscribe
replaces newHeads on the harness side; metric names are shared so
the queries are structurally identical to the EVM specs.

Data already flows via the Paris VPS deployment (chain label carries
base and solana). No harness change needed for this ship. Multi-region
is a follow-up on the parent bench.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…s) (#1198)

* ws-head-latency: region label via ConstLabels + Railway config

Adds a static region ConstLabel to every ws-head-latency metric,
bound at process startup from OCB_REGION (preferred) or
RAILWAY_REPLICA_REGION (Railway auto-set), with eu-west fallback
matching the historical single-vantage Paris VPS deployment.

No observation site needs to change: the label is baked into every
counter, gauge and histogram at registration, so all existing
WithLabelValues(provider, chain) calls stay identical.

Adds railway.toml so Railway can build the harness from Dockerfile
with a /health check, matching the rpc-capabilities service layout.

Follow-up: create the Railway ws-head-latency service with 3
replicas (us-east, eu-west, sgp) and OCB_REGION set on each, then
decommission or leave the Paris VPS container as a 4th vantage.

* ws-head-latency specs: add region dimension tabs (081 + 087 + 088)

Adds `dimensions.region` block + `rank_matrix_query` on provider,
region + per-provider `regions:` sub-arrays with region-scoped
queries on the 3 ws-head-latency specs, following the polkadot-rpc
pattern.

Depends on the harness region label change (previous commit): every
metric now carries a region ConstLabel so the region-scoped
histogram queries return values as soon as multi-region replicas
publish. Until Railway 3-replica deploy lands, only region=eu-west
has data and the other tabs render empty; the All regions tab keeps
working because the aggregate query is unscoped by region.

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ot OCB_REGION) (#1200) (#1201)

Every other multi-region harness on Railway (rpc-capabilities, aggregator-head-lag, evm-swap-quoting, pm-rate-limits, etc.) reads REGION from env. Making ws-head-latency read REGION too keeps the Railway service config uniform across the whole harness fleet.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Companion to #1195 (series null gaps). The gaps themselves render as
line breaks but a break can read as 'the chart missed a sample'. A
soft red column behind the break spans the exact outage window and
makes the reader understand it was a real feed-side silence. Per
line + per contiguous gap range, extended by one step so 1-bucket
outages stay visible instead of collapsing to a zero-width sliver.
Hidden for excluded (legend-toggled) providers.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…arity (#1204)

081 becomes ws-head-latency-ethereum so the 3 per-chain benches in the head-lag cluster share the same shape: ws-head-latency-ethereum / -base / -solana. The old ws-head-latency slug is dropped (was staging-gated via REMOVED_BENCH_SLUGS, never live on prod, no external SEO to preserve).

Ungated on prod at the same time so the new slug ships alongside the base + solana siblings that are already live. Sibling text references updated to the new URL. Cache keys bench-unfiltered and all-benchmarks bumped so cached v29/v32 entries do not keep the old slug alive.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
CoinStats /wallet/blockchains returns HTTP 406 "Credits limit reached" on every fetch: the free tier quota is spent below our 6h cadence and the provider never publishes a network count. Dropping until a paid key is provisioned.

Removes the coinstats.go adapter, the CoinStatsAPIKey config field, the coinstats entry in main.go, and the coinstats provider row + prose mentions in the spec. Cohort drops 6 -> 5 providers (GeckoTerminal, Codex, Mobula, CoinPaprika, Dune via Sim API).

Co-authored-by: Florent Tapponnier <contact@mobula.io>
#1206)" (#1208)

This reverts commit d295f71.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ers (#1210)

Feedback on #1203: the neutral red band read as 'something happened
here' without telling the reader WHO. Tint each band in the
provider's own color, add dashed edges at the outage boundaries, and
label 'PROVIDER · SILENT' above the band when it fits (with a caret
that survives on narrow single-bucket bands). Works uniformly for
Mobula/Codex/Gecko or any future provider with a live_activity
query.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Screenshot review: the small text-only label above the band was still
too subtle to read as 'this provider is down', and 3 simultaneous
provider outages produced overlapping unreadable labels stacked on
the same y. Replace with high-contrast rounded pills (white text on
provider color) that stack vertically when their X-ranges overlap.
Renders as '● NAME DOWN' in bold small caps so a reader gets the
event and the provider identity at a glance.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
#1212)

Bench 080 published only 7 samples across 3 of 11 assets in 24h at
50 bps trigger. Two contributing causes fixed together:

1) Trigger from 50 -> 25 bps. 50 bps in 90s is a headline move on
   Nasdaq (earnings, macro prints), not a steady-state event. 25 bps
   is above normal intra-minute noise on liquid stocks and captures
   real arb-worthy moves without collapsing the semantic.

2) Sub-poll convergence credited. When the reference moves past the
   trigger AND the pool is already within band on the same tick, the
   arb closed inside one poll interval. Previously dropped silently;
   now observed at pollInterval (60s) so the fast-arb tail on liquid
   pools stops disappearing from the histogram.

Convergence threshold stays at 20 bps: the "how close is the pool to
fair" question is unchanged, only trigger sensitivity moves. Spec
text (subtitle, methodology, FAQ) updated to 25 bps throughout.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
Two fixes based on 30D view review:

- Leading gaps (Prom retention shorter than the visible window,
  harness started mid-range, provider added recently) rendered as
  a chart-wide band claiming every provider was down for the
  missing period. Skip runs that started before the first observed
  sample.

- Rename 'DOWN' to 'DATA MISSING'. A silent series can be the
  provider going down, our harness losing its WebSocket, or a Prom
  scrape failure — the pill stays neutral instead of blaming the
  provider.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
HyperSync and The Graph need paid keys for the sustained cadence the
bench expects; Mobula alone left the leaderboard single-provider and
the spec never made it out of REMOVED_BENCH_SLUGS gating. Simpler to
drop the whole cluster than to buy 2 SaaS subscriptions to prop up
a bench nobody has cited yet.

Removed:
  * benchmarks/indexer-latency.yml
  * harnesses/indexer-latency/ (whole directory)

Kept in REMOVED_BENCH_SLUGS as 410-gone so any indexed URL returns
Gone rather than 404. Cache keys bumped so cached v30/v33 entries
that still enumerate the slug age out on next revalidate.

VPS follow-up (separate ops): stop ocb-indexer-latency container,
remove from docker-compose.yml + prometheus.yml scrape config.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
#1220)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
)

Bug: the guard was designed for modern 15s-cadence data, where
changes([15m]) works cleanly. On backfilled historical data compacted
to ~1 sample/hour, changes([15m]) returns 0 even though the gauge is
healthy — the 30D view lost the first 8 days of data to false-drops
(all three providers marked DATA MISSING for a whole week).

Real-time silence is still surfaced by the live_activity scalar
(pill + banner + downtime band), which runs against fresh scrape
cadence and stays accurate. The chart series now shows every point
Prom has, flat during old outages instead of gapped.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
- badge/[slug]/[provider]: serve pending SVG (200) instead of 404 when a provider is missing ranking data
- hyperliquid/[slug]: render lightweight placeholder page (200) instead of 307 to /hyperliquid when history blob is missing the builder
- compare adHocPairs: bump inclusion threshold to 3 (brand) / 4 (other) so a single bench flap can't cross both the sitemap gate and the page noindex gate

Fixes 3 recurring Ahrefs issues (broken badge images, noindex-in-sitemap on compare, 3xx-in-sitemap on hyperliquid) all rooted in the sitemap/page ISR race.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
#1224)

Follow-up to #1221. Dropping the guard entirely restored the 30D
range but silently hid today's Codex morning blackout on the 24H
view (frozen gauge value = flat line, indistinguishable from a
healthy quiet period).

Bring the guard back, gated on count_over_time > 5 samples in the
window. Modern data at 15s cadence carries ~60 samples per 15 min
so the gate fires the moment a WS goes silent. Backfilled data
compacted to ~1 sample/hour has count <= 2 in a 15m window so the
gate stays open and every point Prom kept is served — no more fake
wall-to-wall gap at the start of the 30D range.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…#1225)

Fix the leader-collapses-to-2.5ms artifact: histogram_quantile on a
[0,5ms] first bucket interpolates every winner's p50 to ~2.5ms
(chain-independent). Replace with a 0.5ms floor climbing to 10s so
sub-ms wins actually resolve on Solana (~400ms slots) and Base (2s).

Add keyed provider slots for Base (Alchemy/Infura/Chainstack/QuickNode)
and Solana (Alchemy/Chainstack/Helius/QuickNode) so the cohort can
extend beyond keyless once contributor keys are wired. Keyless tier
is saturated on all three chains (audit failed: Ankr, Blast, LlamaRPC,
BlockPI, 1RPC, NodeReal, Omniatech, base.org, extrnode, chainstack-
public — none accepted a keyless WSS handshake as of 2026-07).

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Titan leads with sample=6973 in 24h, data healthy since the harness
ship. Removing from REMOVED_BENCH_SLUGS so the slug renders on prod
next revalidate. Cache keys bumped to bench-unfiltered-v32 +
all-benchmarks-v35 so cached entries do not keep dropping the slug
via the old prod gate.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Fine-grained outages (24h step = 20 min buckets) rarely align with
coarser evaluation timestamps (7d step = 2h, 30d step = 12h) so the
Prom query returns a healthy average and the reader sees the pill on
24H but nothing on 7D/30D. Post-process: for every contiguous null
run detected on s24, translate the time-ago range to bucket indices
on s7 and s30 and null every coarse bucket the run straddles.

Contiguity preserved so a single short outage doesn't split into two
adjacent pills after propagation. Same treatment applied to per-region
series.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…tion (#1230)

Bug: the previous math scaled by the fine window fraction, which
projected a 7h-ago outage down to 40h-ago on the 7d grid (the pill
landed at -1.7d instead of -6h). Both grids share 'now' as their
right anchor and only the step sizes differ — take the fine idx's
absolute time-ago and divide by the coarse step to find the
covering bucket.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…1231)

Their Uniswap v4 pools on Robinhood Chain have no arb activity: pool spot stays frozen at the last swap value tick after tick while the Yahoo reference moves normally. Events open on ref triggers but roll over as still_open with zero latency samples. Cohort restricted to the 6 tickers with observable arb activity: NVDA AAPL PLTR META AMD MU.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…order, GMX Subsquid error surfacing (#1233)

- Paradex: cap fill ratio at 90% of visible book (depth=100). Tiers that eat through the tail now return book_too_thin and skip publishing instead of inflated 150-500 bps ghost values.
- gains: only cache USD-quoted pairs during pair discovery, widen scan 60->200. Root cause was a SOL/BTC pair overwriting the SOL/USD cache entry, publishing the wrong feeIndex and uniform 4.333 bps across ETH/BTC/SOL.
- dydx: sort v4-indexer level responses (asks ascending, bids descending) before walking the book. Insertion-order walk produced 1.7-2x inflated headline at 100k/1M tiers.
- gmx: prefix Subsquid errors, surface GraphQL errors[], fall back to unsuffixed positionFeeFactor if the impact-branch field is absent, and error out on zero factor. Prevents the silent-6bps-constant fallback path.

Verified: go build + go vet clean.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Screenshot review: rpc-reliability rendered a 'LAVANODIES DATA MISSING'
pill even though its spec never declared live_activity — natural
nulls in its series24h (sparse scrapes, backfill edges) triggered
the DowntimeBands render. Gate the render on
benchmark.results.some(r => r.liveStatus != null), which is only set
when the spec declared live_activity. Same gate on
propagateNullsToCoarser so other benches' 7d/30d series stay
untouched.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…#1236)

* site: provider_notes chip on ranking rows + rpc-capabilities copy fixes

- validator-yield now surfaces a per-provider chip (MEV inc./MEV excl./no MEV) beside each row so scan-readers can't accidentally compare Solana MEV-inclusive APR against Ethereum consensus-only APR without seeing the caveat
- New optional spec field provider_notes: {slug: string} plumbed end-to-end (Zod schema, TS type, materialize loader, spec overlay, ledger-table render)
- Chip renders only when the note is set, no visual change to any other bench
- rpc-capabilities: '10 EVM chains' copy corrected to 26 chains (24 EVM + Solana + Polkadot) across title/subtitle/seo_intro/abstract/methodology + FAQ
- rpc-capabilities disclaimer expanded to call out that the 'All chains' aggregate rewards single-chain foundation endpoints and to document Cloudflare-eth exclusion (returns -32603 on POST)

* perp-fees harness: add Aster + edgeX venues

- Aster (BSC): Binance-style depth walk against fapi.asterdex.com, hardcoded 5 bps taker (no public fee schedule API)
- edgeX (offchain CLOB): getDepth REST + hardcoded 3.8 bps taker per gitbook docs. Asset→contractId map declared inline (verify on first prod run; wrong ID surfaces as an orderbook fetch error, not a silent 0)
- Both venues probed for ETH/BTC/SOL at all 3 notional tiers ($1k/$100k/$1M) via applyBookTiers
- Closes the biggest coverage gap flagged by the perp-fees GEO audit (Aster is #2 perp DEX by 30d volume)

* validator-yield harness: add Cardano, Sui, Cosmos Hub, Avalanche

Closes the 'Highest staking yield' coverage gap: the bench previously ranked only Solana, Ethereum and Hyperliquid but AI answers to that question almost always include Cardano ($14B staked) and other top chains.

- Cardano (Koios API): top-50 pools by stake, net = ρ×(1-τ)×(1-margin). Simplified vs full a0/k/saturation formula but lands within ~50-100 bps of pooltool.io.
- Sui (public RPC suix_getValidatorsApy + suix_getLatestSuiSystemState): all active validators, apy is already net of commission per Sui docs.
- Cosmos Hub (LCD REST): top-200 bonded validators, net = inflation×(1-communityTax)/bondedRatio×(1-commission).
- Avalanche (P-Chain platform.getCurrentValidators): top-100 by stake, base 8.5% APR × uptime × (1 - delegationFee/100).

Reuses existing ocb_validator_* gauges from metrics.go (no new metrics, no go.mod changes). Env-overridable endpoints declared inline in each fetcher matching solana.go style. Shortcuts (uptime=100% for Cardano/Sui/Cosmos, hardcoded community_tax=2%, base 8.5% for Avalanche) documented in code.

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…bloXroute) (#1238)

Broadens the keyless cohort for benches 010 (rpc-capabilities), 083
(rpc-reliability) and the per-chain RPC benches. All 4 providers
verified live returning valid eth_getBlockByNumber on their
respective chains before inclusion.

New (provider, chain) cells:
  ThirdWeb    8 chains (ETH POL ARB OP BASE BSC AVAX LINEA)
  Blast API   4 chains (ETH ARB BASE BSC) — POL OP AVAX LINEA are 403 paid tier
  bloXroute   3 chains (ETH BASE BSC)
  Gateway.fm  1 chain  (ETH)

Total = 16 new cells. Metrics auto-flow into bench 083 (rank_matrix
groups by provider+chain) and the per-chain leaderboards after the
Railway rebuild picks up main.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
@Flotapponnier

Copy link
Copy Markdown
Collaborator Author

Closing: dev and main have diverged via cherry-pick pattern. Every fix from this session is already on main via #1217, #1223, #1234, #1237. Other pending dev commits (from other work streams) should be synced by their respective authors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant