Skip to content

Stellar Index v0.6.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 22:08

[v0.6.0] — 2026-07-01

Minor bump for the breaking LC-001 assets split (/v1/assets is now
Stellar-only; see Changed). This section also captures the changes accumulated
since v0.5.0-rc.128 — the CHANGELOG promotion had lagged behind the release
tags (which reached rc.151), so the entries below span rc.129rc.151 plus
the 2026-07-01 session.

Added

  • GET /v1/external/assets + GET /v1/external/assets/{slug}. The non-Stellar
    side of the assets split (LC-001) — fiat currencies + reference-only coins. See
    the BREAKING note under Changed.
  • flags.divergence_checked on the price envelope (CS-087). Signals whether
    the cross-reference divergence check actually ran (≥1 responding reference). When
    false, divergence_warning is blind and must not be read as "prices agree".
  • Regression guardrails. (1) scripts/ci/lint-i128.sh (ADR-0003) — rejects
    int64(x.Lo) i128 truncation + BIGINT/float monetary migration columns; (2) the
    exhaustive linter (ADR-0010), scoped to our domain enums, so a new
    AssetType/Class/etc. variant added to a switch without handling fails CI;
    (3) foundation-purity import rules (internal/canonical, nettools,
    sources/external/scale, version pinned to their dependency floor); (4) the
    testcontainers integration suite now RUNS in CI as a blocking gate (CS-070 —
    it was previously only compiled). All in make verify / CI.
  • Contributor docs for agents. /CAPABILITY-INVENTORY.md (intent→symbol index,
    to stop rebuilding existing helpers) + docs/contributing/ checklists (add a
    source / CEX / endpoint / metric / migration / observer).
  • Stablecoin self-peg pricing for the crypto-ticker form.
    /v1/price?asset=crypto:USDC&quote=fiat:USD (and the EUR/MXN pegs,
    /v1/price/tip, /v1/observations, /v1/oracle) now returns the ~$1 peg
    (price_type: "peg") instead of 404. The classic-issued form
    (USDC-GA5Z…) already resolved via the operator's
    usd_pegged_classic_assets list (F-1232), but the abstract global-ticker
    form the catalogue + explorer use (crypto:USDC, crypto:EURC, …) fell
    through — no on-chain trade quotes crypto:USDC in fiat:USD.
    tryStablecoinFiatProxy now consults aggregate.FiatProxy first: when the
    asset is a crypto:<STABLE> ticker whose peg fiat equals the requested
    quote, it synthesises 1.0 (consistent with the aggregator's
    stablecoins-as-fiat policy; a depeg still surfaces via the divergence
    subsystem). A cross-peg quote (crypto:USDC/fiat:EUR) deliberately does NOT
    fire — that's a real FX cross-rate. (launch-todo P2-4(b).)
  • SEP-41 token supply now served on /v1/assets/{id}. A Soroban (SEP-41)
    token has no LCM observer supply snapshot unless its contract is on an
    operator watch-list — impractical at 10k+ tokens, so Algorithm 3 produced
    nothing and total_supply was null for every SEP-41 token. /v1/assets/{id}
    now falls back to the lake-derived per-token supply (ch-supply's
    token_supply, Σmint−Σburn−Σclawback over the certified ClickHouse lake — the
    same source /v1/assets/{id}/supply already uses) for Soroban-contract assets
    with no observer snapshot, with a new sep41_lake_flows supply basis. Every
    SEP-41 token's total_supply / circulating_supply / market_cap_usd is now
    complete + served from the full archive. The CH read fires only for Soroban
    tokens (classic assets keep their Algorithm-2 snapshot).
  • Data-freshness watchdog — the "never get behind" alert. New
    data-freshness.sh (every 15 min via data-freshness.timer) emits per-domain
    ingest-freshness gauges (stellarindex_data_freshness_{age_seconds,stale}) AND
    the per-source ADR-0033 completeness verdict
    (stellarindex_completeness_incomplete) to the node_exporter textfile
    collector, with three alerts (stellarindex_data_source_stale,
    stellarindex_completeness_incomplete,
    stellarindex_data_freshness_watchdog_silent) + runbooks. Closes the gap the
    audit found: the ingest gap-detector covered on-chain source gaps, but
    reference oracles, FX, supply, the issuer-metadata cron, and the verdict itself
    had no freshness alert — so coingecko rotted 11 days and sep1 metadata never
    populated, unnoticed. Now any source past its cadence, or a real served≠lake
    gap, pages. (launch-todo steady-state / "never behind".)
  • massive FX feed registered as an external source. The active fiat-FX
    feed (internal/sources/forex worker, massive.com = Polygon's backend,
    fx_quotes path) was missing from external.Registry, so it never appeared
    in /v1/sources and Lookup("massive") fail-closed. Now registered as an
    external FX source (ClassExchange/SubclassFX, off-chain). Fixes a latent
    classification bug: IsOnChain("massive") previously fell through to true
    (which would have placed the off-chain FX feed on the explorer's Stellar
    /network surface); it is now correctly false. (launch-todo P0-7.)
  • Two missing cron timers — sep1-refresh + compute-completeness. Neither
    had ever existed as a systemd timer, so both data sets silently froze: issuer
    org_name/org_verified only updated on a manual sep1-refresh, and the
    ADR-0033 completeness verdict (completeness_snapshots) had drifted 17–21
    days
    stale (watermarks at ~63.0M while the network was at 63.27M). New
    Ansible templates under the archival-node role install both (daily, 05:12 /
    05:30 UTC). The completeness timer runs run-compute-completeness.sh — a
    self-chunking per-source driver: it walks each source's
    [watermark, tip] in 25k-ledger windows because (a) the watermark write
    overwrites rather than max()s, so a global run would regress sources already
    ahead, and (b) the high-volume SDEX projection reconcile blows ClickHouse's
    12 GiB per-query limit above ~30k ledgers. Self-healing: any backlog (initial
    catch-up or post-outage) is chunked automatically. Phase-0 of the launch
    to-do (docs/operations/launch-todo.md).
  • Bidirectional SEP-1 org verification (org_verified). /v1/issuers
    now carries org_verified — true only when the issuer's home_domain
    stellar.toml lists THIS issuer's account back in its [[CURRENCIES]]
    (i.e. the domain owner attests to the account). A one-directional
    home_domain → ORG_NAME match is spoofable: anyone can point their
    account's home_domain at circle.com and inherit "Circle". The
    explorer's issuer table renders a ✓ Verified badge only on the
    bidirectional match, so org grouping/merging is trustworthy. The
    sep1-refresh cron computes the flag (tomlListsIssuer) and persists it
    in the sep1_payload JSONB; /v1/issuers reads
    sep1_payload->>'OrgVerified'. Each OK line now prints verified=….
    New sep1-refresh -issuer <g_strkey> force-refreshes one specific account
    on demand (bypassing the staleness queue) — for onboarding a newly-verified
    org without waiting for it to surface through ~43k pubnet issuers.
  • stellarindex-ops state-snapshot — reads a history-archive checkpoint's full
    current ledger-entry state (the bucket list) via the SDK's
    CheckpointChangeReader and tallies it by entry type. The read-only
    foundation of the data-truth backfill (DATA-TRUTH-PLAN G1–G3): the served
    ledger_entries_current projection only holds entries changed since ledger
    ~62M, so dormant-pre-62M accounts / trustlines / contract code+instances are
    missing (the contract-WASM user-contract tail, incomplete account state +
    issuer flags, possible trustline-supply undercount). A checkpoint snapshot is
    the source of truth for that tail, read in one pass (no genesis replay). Its
    -write mode backfills the contract_code + contract_instance entries (the
    bounded G1 scope) into ledger_entry_changes via a direct insert that writes
    NO commit-marker ledgers row (so it never advances the completeness
    watermark) — closing the contract-WASM gap for user contracts whose code was
    deployed before the entry-capture window. Default mode is read-only tally.
  • Staff customer look-up (/account/admin, audit 2026-06-19 item 16):
    the cockpit's first tool is now live instead of a placeholder. New
    staff-gated GET /v1/account/admin/lookup?email=|slug= resolves an
    account (tier, status, overrides) plus the users on it; the explorer's
    admin page searches by email or account slug. Double-gated — RequireSession
    • an explicit is_staff check (a non-staff customer gets 403, never another
      customer's data) — and the access is audit-logged. Tier overrides + incident
      tooling remain honestly marked "Coming in Phase 1.5" (they need write/
      impersonation endpoints).
  • source_volume_1h continuous aggregate (migration 0068) — per-source
    hourly trade-count + pre-aggregated USD-volume inputs. The source
    page's activity chart now reads this CAGG instead of scanning raw
    trades, making the 7d window cheap (the live derivation was ~18s for
    the heaviest source, past the 8s API ceiling). The explorer's 24h/7d
    toggle on /dexes/{source} + /exchanges/{source} is now live, and
    the 24h sparkline is faster too. /v1/sources?include=sparkline7d is
    now surfaced by the frontend.
  • on_chain boolean on /v1/sources (and external.IsOnChain) — true
    for sources that observe the Stellar network directly (DEX, on-chain
    oracles, lending, routers, bridges), false for off-chain reference
    feeds (CEX / FX / aggregators / Chainlink).

Changed

  • BREAKING — assets split into Stellar (/v1/assets) and external
    (/v1/external/assets) (LC-001).
    /v1/assets now lists Stellar assets
    only
    (native XLM, classic credits, Soroban tokens, and verified-catalogue
    currencies with a Stellar issuance — USDC, EURC, AQUA). Fiat currencies and
    reference-only coins (BTC, ETH, …) — which have no Stellar issuance — moved to
    the new GET /v1/external/assets listing and GET /v1/external/assets/{slug}
    detail. A non-Stellar slug now returns 404 on /v1/assets/{slug} (with a
    cross-pointer; no redirect), and vice-versa — each asset resolves on exactly
    one path. asset_class=fiat returns an empty page on /v1/assets. The
    explorer gains an /external/assets directory + detail page and drops the
    fiat chip from /assets. Root cause of the old mixing: the browse listing fed
    off catalogue.Browseable(), which drops reference-only coins but still
    included fiat.
  • XLM circulating-supply basis is now honest when no SDF reserves are
    configured.
    Previously stamped xlm_sdf_reserve_exclusion even with an empty
    reserve set (circulating == total), silently overstating circulating supply +
    market cap; now emits xlm_total_only so the misconfiguration is self-evident.
    (The correct circulating still needs sdf_reserve_accounts set in inventory.)
  • Dependencies brought to latest. go-stellar-sdk v0.5→v0.6 (adapts the new
    datastore.GetFile size return; VERSIONS.md compat pass); the explorer + status
    apps to React 19.2 / Next 16 / TypeScript 6 / Tailwind CSS 4 / ESLint 10 (flat
    config; ESLint 10 via a one-line eslint-plugin-react pnpm patch), and the
    React Compiler (babel-plugin-react-compiler 1.0) is now enabled.
  • Re-enabled the min_usd_volume VWAP gate at $10k (r1 template). Pinned to 0
    during the on-chain-only bootstrap; the CEX connectors now flow live volume
    (binance/coinbase/kraken/bitstamp), so fiat:USD pairs clear the floor easily
    while thin/manipulable pairs are gated. The CS-040 fix (per-source Decimals in
    the USD-volume sum) makes the gate FX-safe.
  • Prometheus TSDB relocated off the 49G OS root onto a ZFS dataset. The
    ~13G TSDB kept the root chronically >90% full (stellarindex_node_root_disk_full
    alert). Moved to data/prometheus (zstd, ~12× → 1.31G on disk); root dropped
    94%→60%. Added the dataset to the archival-node ZFS-role defaults so a rebuild
    doesn't reland it on root. (launch-todo P0-5.)
  • The /network page is now Stellar-only: "Top markets" reads
    /v1/pools (on-chain DEX pools, not the CEX-dominated /v1/markets),
    "Most active sources" + the venue-composition donut + the hero
    Markets/Sources tiles all filter to on-chain sources. Off-chain
    reference feeds stay on /exchanges + /aggregators.
  • The /sources directory is now the Stellar on-chain source registry
    (DEX / oracle / lending / router / bridge) — previously it listed
    every venue and silently dropped the lending/router/bridge classes
    (blend, cctp, rozo, defindex, soroswap-router now appear).
  • Source activity chart defaults to the 7d window when available.
  • Market/asset OHLC chart now picks the finest granularity each window
    allows under the API's 1000-bar cap (24h→5m, 7d→15m, 30d→1h, 90d→4h)
    — far more detail per window.

Fixed

  • Security & correctness audit remediation (2026-07-01). Highlights:
    • SSE crash + DoS. streaming.Hub.Publish could send on a closed
      subscriber channel (process-crashing panic) when a client disconnected
      mid-publish — now guarded by a per-subscription mutex. The SSE handler
      cleared its write deadline entirely, so a non-reading client leaked its
      goroutine/conn/FD forever — now a rolling per-write deadline + a concurrent-
      stream cap (CS-012 / CS-013).
    • Dashboard CSRF. The session cookie was SameSite=None though the
      dashboard and API are same-site — now SameSite=Lax, blocking cross-site
      credentialed POSTs to the /v1/dashboard/* mutation handlers (CS-124).
    • SSRF. The OG-image edge function double-decoded + interpolated the URL
      path unescaped into satori markup (blind SSRF) — now escaped/single-decoded
      (CS-009). The three copies of the outbound-URL SSRF blocklist (SEP-1 +
      webhook registration/delivery) diverged — two missed Oracle Cloud's metadata
      IP 192.0.0.192; unified into one internal/nettools guard (CS-008).
    • Issuer impersonation. /v1/issuers/{id} dropped org_verified, so the
      explorer rendered an unverified self-declared org_name as authoritative —
      now surfaced + shown with a Verified/Unverified chip (CS-100).
    • Webhook replay. Delivery HMAC signed only the body — now timestamp-bound
      (X-StellarIndex-Timestamp) so a captured delivery can't be replayed (CS-055).
    • Data-truth signals. Completeness watermark could regress to a stale tip
      (now GREATEST-guarded, CS-083); a total divergence-reference outage counted
      as success (now a distinct no_reference outcome + alert, CS-088); the
      ingest cursor gauge advanced even on a failed persist (CS-029); dormant-pair
      VWAP served stale=false forever (CS-017); the USD-volume gate assumed 1e8
      for FX sources that stamp 1e6 (CS-040); negative circulating supply clamp
      (CS-038).
    • Accessibility. The API-request dialog + mobile nav drawer gained a real
      focus-trap/escape/restore; form errors/success now announce to screen
      readers (LC-050 / LC-051 / LC-052).
    • Ops config. Alertmanager rendered webhook secrets world-readable (now
      0640, CS-121); the sshd password-auth Ansible gate inverted on a string
      override (now | bool, CS-120); the User-Agent was injected unescaped into
      the plaintext magic-link email (CS-071).
  • Completeness verdict false-negative on factory-gated sources (blend).
    compute-completeness (the daily verdict, ADR-0033) never seeded the
    factory-child gate registry — only verify-reconciliation did. So its
    childgates were the static protocol_contracts seed and went stale as new
    pools deployed: blend reported complete=false (expected=0) on windows
    whose activity was on pools missing from the seed, while the live decoder
    (self-seeding from deploy events) captured them — i.e. a checker bug, not a
    served-data gap. Now compute-completeness preseeds factory children from the
    creation events [genesis, lo) before each re-derive (matching
    verify-reconciliation), making the watchdog self-maintaining as pools deploy.
  • CoinGecko Pro key would have 404'd — the poller now auto-switches to
    pro-api.coingecko.com.
    A Pro key (COINGECKO_API_KEY) only authenticates
    against the paid host; the poller hard-coded the public host
    (api.coingecko.com), so an operator upgrading to the paid tier (to fix the
    dead oracle feed — it had hit the 10k free-tier limit) would have silently
    kept failing. The poller now selects pro-api.coingecko.com whenever a Pro
    key is set and the endpoint wasn't explicitly overridden. (launch-todo P0-3.)
  • sep1-refresh could never reach good issuers — failed fetches now bump
    sep1_resolved_at.
    A resolve failure (dead home_domain, TLS error,
    SSRF-blocked) used to continue without writing anything, leaving the
    issuer's sep1_resolved_at NULL. Since IssuersNeedingSep1Refresh orders
    sep1_resolved_at ASC NULLS FIRST, the 43,156 pubnet issuers with dead
    domains permanently occupied the front of the queue — the refresh re-tried
    the same dead domains every run and never made forward progress to the
    ~100 good issuers behind them (Circle, Aquarius, …). org_name /
    org_verified could therefore never populate at scale. New
    MarkIssuerSep1Attempted bumps sep1_resolved_at on failure (without
    writing a payload), so a dead domain moves to the back of the queue and is
    retried only on the next -older-than cadence; a later success overwrites
    the payload as before.
  • /v1/price latency-burn incident (page severity) — root-caused + fixed.
    LatestClosedVWAP1mForPair's "latest closed bucket" predicate was
    bucket + INTERVAL '1 minute' <= now() — a function on the indexed bucket
    column, so it's not sargable: TimescaleDB couldn't do chunk exclusion or
    an ordered index scan, and max(bucket) ran a full per-chunk partial
    aggregate over the pair's ENTIRE prices_1m history (~13.7k rows/chunk × every
    chunk back to 2015). Harmless while a pair was sparse; once
    crypto:XLM/fiat:USD accrued dense history (CEX coinbase/kraken trades, from
    ~20:00 UTC 2026-06-19) it ballooned to ~446ms execution + 55k planner
    buffers
    , driving the price p95 from ~50ms to ~400ms and the SLO burn /
    sla-probe alerts. Two-part fix: (1) rewrote the non-sargable predicate to
    the arithmetically-identical bucket <= now() - INTERVAL '1 minute'
    (execution 446ms → 26ms); (2) that still left ~280ms of planning time —
    prices_1m has ~374 chunks and now() only enables runtime chunk exclusion,
    so the planner still enumerated every chunk. Added a LITERAL recent lower
    bound (bucket >= <cutoff>, computed in Go) so the planner prunes old chunks
    at PLAN time, collapsing planning to ~2ms. Net: ~390ms → ~8ms end-to-end.
    Idle pairs (no closed bucket in the 14-day fast window) fall back to the
    unbounded scan so the latest-closed-bucket contract is preserved. (The rc.133
    fix to this function only bounded the sparse case; the sibling
    ORDER BY bucket DESC LIMIT readers were verified unaffected.)
  • /v1/contracts/{id}/wasm now distinguishes a Stellar Asset Contract
    (the built-in SAC behind native, USDC, and every classic asset — among
    the busiest contracts on the network) from a genuinely-uncaptured WASM
    module (audit 2026-06-19 item 13). The reader found the SAC instance but,
    since its executable isn't a WASM module, returned the generic
    "unresolved" 404 — so the explorer wrongly said "resolves once a backfill
    lands" for contracts that will never have WASM. SACs now return a distinct
    contract-is-sac 404 and the explorer shows "this is a Stellar Asset
    Contract — no WASM." Because the busiest SACs (native XLM, USDC) were
    deployed long ago and their instance entries also predate capture, SACs are
    detected deterministically too — a contract id is matched against the
    operator sac_wrappers registry AND the computed SAC derivations of the
    native asset + every verified-catalogue classic asset — so
    native/USDC/AQUA/… report "SAC, no WASM" without needing a captured
    instance. (Real user contracts whose code was uploaded before the
    entry-capture window still show the honest "not captured yet" state pending
    the Phase-C backfill.) apiGet now also surfaces the RFC-9457 problem
    title/detail in thrown errors so clients can tell apart same-status
    failure modes.
  • Class-filtered + unified /v1/assets listings now carry price_usd.
    ?asset_class=crypto|stablecoin|fiat (and the explorer's
    ?asset_class=all first page) projected catalogue rows from the
    price-less catalogue projection, so every row — even XLM — listed
    price_usd: null (audit 2026-06-19 item 4). The sliced page now fills
    the headline price through the same three-tier chain as the single-asset
    /v1/assets/{slug} view, bounded to the page (not the whole catalogue)
    so the unified first page doesn't fan a price computation over every
    catalogue entry. Stellar-only tokens (AQUA, yXLM, SHX, …) that have no
    global CEX/aggregator price fall back to their Stellar trades-derived
    price (the same one the classic listing shows), so a class-filtered row
    matches the classic asset row instead of listing null.
  • /v1/assets market_cap_usd + circulating_supply now cover every
    classic asset
    , not just the ~9 with a precise supply-pipeline figure
    (audit 2026-06-19 item 4: market_cap was null for all 500). The precise
    three-domain supply_1d figure is still preferred where it exists; the
    long tail falls back to a broad circulating supply derived from the sum
    of all (non-removed, positive) trustline balances per asset — the exact
    definition of classic-asset circulating supply — read from the ClickHouse
    lake via one cached GROUP BY (~0.5s, 10-min TTL + single-flight, kept off
    the API hot path). market_cap = (circulating / 10^decimals) × price.
    Assets without a price stay honestly null (no fabrication).
  • /v1/protocols/{name} cold-path latency cut ~3× (audit 2026-06-19 item 8):
    the three independent lake reads (daily series, event breakdown,
    per-contract activity — ~5s each via the contract_id bloom index) ran
    serially (~15s total); they now run concurrently and write disjoint view
    fields, so the cold path is ~5s — comfortably under the 25s ceiling. The
    "untyped" reconciling bucket is appended after the barrier (it needs the
    series total). Repeat hits stay instant via the cache below.
  • /v1/protocols/{name} event breakdown now NAMES AMM swap/sync events
    instead of lumping them into "untyped" (data-truth G4). Soroswap's events
    are [String("SoroswapPair"), Symbol(name)], so the lake's topic_0_sym
    (which only captures a Symbol topic[0]) is empty and the real event name
    lives in topic[1]. The breakdown now recovers it: when topic[0] isn't a
    Symbol, it decodes topic[1]'s Symbol from the raw topics_xdr — so
    soroswap shows swap/sync/deposit/withdraw/skim (≈190k events that
    were "untyped") and the untyped remainder collapses to ~0.
  • /v1/protocols/{name} is now served from a 60s per-server single-flight
    cache, so concurrent requests no longer each re-run the ~15s lake scans
    and peg CPU (compounding the 25s ceiling below).
  • /v1/protocols/{name} can no longer peg CPU for minutes: the
    lake-analytics + bespoke scans (~15s warm) had no request ceiling and
    were observed running away to several minutes under concurrent load
    (2026-06-19 incident). Added a 25s timeout; the enrichment helpers
    degrade gracefully on cancellation. (The proper fix — a CAGG so these
    are fast — is tracked in docs/archive/page-audit-2026-06-19/.)
  • /v1/protocols/{name} event counts now reconcile (audit 2026-06-19
    item 8). events_total was the typed-breakdown sum, which counts only
    events whose topic[0] is a denormalized Symbol in the lake — so for
    Soroswap it read 236 while the activity chart summed to ~200k (the
    swap/sync events carry a non-Symbol topic[0]), and it could even fall
    below events_24h. events_total is now the unfiltered window total
    (= the activity-series sum), and event_breakdown carries a synthetic
    untyped bucket for the non-Symbol-topic'd remainder, so
    sum(event_breakdown) == events_total == sum(activity_series). This also
    fixes protocols (e.g. phoenix) showing an empty breakdown while the chart
    had data.
  • MEV feed notionals no longer read ~$0 on real cycles: the arb scanner
    read raw usd_volume (NULL for SDEX XLM/token + token/token legs), so
    cycle notionals summed to ~$0. It now estimates each leg's USD value
    from the XLM leg × current XLM/USD VWAP (the same fallback the markets
    queries use), matching how the rest of the API values on-chain volume.
    Applies to newly-detected cycles.
  • Chainlink divergence cross-check now actually runs — it had produced
    zero divergence_observations rows ever (audit 2026-06-19). The
    divergence Chainlink reference carried its own env-less rpc_url that
    fell back to a public RPC (eth.llamarpc.com) which now answers
    eth_call with a Cloudflare JS-challenge HTML page, so every
    LookupPrice failed its JSON decode silently. CHAINLINK_RPC_URL now
    also overrides the divergence reference's endpoint (it already drove the
    ingest poller), so one operator-provided RPC serves both. BTC/USD +
    ETH/USD now cross-check against Chainlink mainnet feeds (~0.1% delta,
    verified live on r1).
  • /v1/issuers/{g} now populates auth_required / auth_revocable /
    auth_immutable / auth_clawback from the on-chain AccountEntry flags
    bitmask we already index (via the explorer's AccountState), instead of
    leaving them null when the dedicated SEP-1 flag resolver hasn't run. The
    explorer's Auth-flags panel shows real values instead of "Not yet
    resolved" for any issuer whose account we've observed.
  • Explorer per-page audit fixes (2026-06-19, frontend):
    • /assets/XLM showed wrapped-XLM data (~330× wrong price) — a
      scam "XLM" classic asset shared the XLM listing slug. fetchCoin
      now resolves XLM/native directly to the native asset.
    • Case-sensitive asset/embed routes — lowercase slugs (/assets/btc,
      /embed/asset/xlm) 404'd or rendered a half-empty GlobalAssetView.
      fetchCoin now does a case-insensitive cache lookup and
      generateStaticParams emits both cases for every slug.
    • /convert/{from}/{to} inverted rates ("1 USD = 1.15 EUR" was the
      EUR→USD rate mislabeled) — now inverted correctly.
    • /sources "Last ingest" always "—" — the cursor venue lives in
      sub_source (the source field is the cursor type); both the index
      and per-source panels now key on the venue.
    • /oracles dropped the always-zero "24h updates" column (oracles
      don't trade).
    • /divergences now marks each reference Active / Configured / Planned
      — only CoinGecko + Chainlink-HTTP are actual cross-checks; Reflector/
      Redstone/Band are ingested as oracle feeds, not yet compared here, so
      the page no longer implies all five are live.
  • /v1/price latency regression (caused a latency-burn incident
    2026-06-19): the rc.131 cross-direction VWAP combine scanned a pair's
    ENTIRE prices_1m history (back to 2015) before LIMIT 1 — ~1s warm,
    ~9s under load. Now it finds the latest closed bucket via an index
    max() per direction (UNIONed), then point-reads + combines just that
    bucket — bounded, ~250ms, same result.
  • CEX dust no longer pollutes OHLC high/low on the API. Sub-$0.001
    streamed CEX fills — tiny integer amounts whose quote/base is a
    meaningless round fraction (1/8, 1/10, …) — are dropped at ingest
    (new stellarindex_external_dust_dropped_total). Ingested, a single
    such $0.00000001 fill set the unweighted /v1/ohlc high/low
    (e.g. an XLM/USD low wick of $0.125) while carrying ~zero real volume;
    the candle body (volume-weighted VWAP) was always correct. Existing
    dust was purged and the price CAGGs re-derived.
  • Flipped markets are no longer double-counted: XLM/USDC and USDC/XLM
    (the SDEX decoder records both on-chain trade directions) now collapse
    to a SINGLE market wherever pairs are read — /v1/markets, /v1/pools,
    and the per-pair /v1/price VWAP. Volume + trade count sum across both
    directions; the price uses one canonical orientation (quote-rank: fiat

    stablecoin > XLM > token, so XLM/USDC quotes in USDC), and the VWAP
    combines both directions over the latest closed bucket (so it uses full
    liquidity, and returns a price even when the latest minute traded only
    the flipped way). Query-time via canonical.Orient — no data migration.

  • Charts now label their time axis in the viewer's local timezone
    (intraday) instead of UTC, so the current bar lines up with the
    viewer's wall clock instead of reading an hour "behind". Date labels
    on daily/weekly views stay UTC (a daily bar is a UTC calendar bucket).
  • Source activity chart no longer shows a gap for the current hour:
    source_volume_1h now uses real-time aggregation (migration 0069),
    so the in-progress (not-yet-materialized) hour is computed live
    instead of reading as zero until the hour closed.

Changed

  • Alerting fanout migrated from Slack to Discord. Both the R1
    standalone Alertmanager config (configs/alertmanager/) and the
    multi-host Ansible template now use Alertmanager-native
    discord_configs. Discord incoming webhooks are locked to one
    channel each, so the page tier and ticket tier take separate webhook
    URLs (DISCORD_WEBHOOK_URL_PAGES / DISCORD_WEBHOOK_URL_ALERTS in
    /etc/default/alertmanager-secrets; alertmanager_discord_webhook_url_pages
    / _alerts vault vars for the Ansible role) — point both at the same
    webhook for a single channel. apply.sh now drops each receiver's
    config block independently when its URL is unset (marker-specific
    strip, so one empty URL never collateral-removes the other Discord
    receiver). Operator runbooks, the SEV playbook comms channels, and
    pre-launch-check.sh updated accordingly. The old SLACK_WEBHOOK_URL
    / alertmanager_slack_* knobs are removed.