Skip to main content

Every release, every change.

Pulled at build time from CHANGELOG.md on main. Format follows Keep a Changelog; SemVer for the public Go SDK, CalVer for binary releases.

Unreleased

unreleased

Added

  • Testnet / Futurenet support — a one-line `stellar.network` switch. The indexer now runs correctly (without corrupting data) against Stellar testnet and futurenet. Grounded in a cold adversarial hardcode audit + an independent fix-verifier pass (2026-08-26). Pubnet behaviour is byte-identical (every new default resolves to the old constant). - Config: stellar.soroban_genesis_ledger / stellar.movements_floor_ledger (pubnet values, or genesis=1 on test nets) so the SEP-41 supply and CAP-67 real-time movements feeds don't floor above the whole chain; timescale.MovementsFloor() + canonical.NetworkPassphrase() install seams resolve leaf-package reads to the configured network. - Corruption guards: SacContractID is network-aware (was serving the pubnet contract address on testnet /v1/assets); config validation rejects a pubnet (core-live) history_archive_url on a non-pubnet network; the cross-anchor archive filler refuses to write pubnet ledgers into a test-net archive; the SEP-41 supply genesis seed defaults its boundary from the config's network value. - Ansible: the archival-node role is network-aware (single-source stellar_passphrase — fixes the futurenet core.cfg bug — per-network history archive, boundary knobs, cap67 -floor-ledger); testnet + futurenet inventory templates. - CI/CD: deploy.yml gains testnet / futurenet targets; a fleet-model design proposal (docs/operations/cicd-fleet-model.md). - Docs: testnet/futurenet deployment guide + reset runbook.

v0.44.8

2026-08-26GitHub ↗

Changed

  • Real-time movement latency cadence tuning (~4s → ~2s). An adversarial audit of a proposed captive-core "fast lane" found the live-movement latency is a chain of hardcoded cadence constants, not a compute floor — so no new component/second core is needed. Tuned the safe ones: the indexer's caught-up MinIO re-check (liveTailRetryWait) 3s → 500ms (the single largest term — a caught-up indexer sat a flat ~3s behind the tip; MinIO is local, so a re-check is a cheap bucket LIST); the /v1/ledger/stream poll that drives the explorer's "watch it land" refetch 2s → 500ms; and the cap67 movements derive tick (FOLLOW_INTERVAL) 2s → 1s. The ClickHouse-part-sensitive LiveSink flush is deliberately left at 1s (sub-second flushing multiplies small parts on the capacity-bound store). An event-driven MinIO-bucket-notification ingest (still a single captive-core) is the documented next step toward ~100ms.

v0.44.7

2026-08-26GitHub ↗

Added

  • Real-time account-movements follow daemon (5.3). The CAP-67 movements derive (stellar.account_movements — the money trail served on /v1/accounts/{g}/movements) now runs as a continuous follow daemon (ch-cap67-movements -follow) instead of a ~30s timer + oneshot: it catches up to the CONTIGUOUS lake tip, sleeps a short interval (2s), and repeats — cutting movement latency from ~30s to ~2s behind the chain tip so a user watches their transactions land in near real time. Builds on the contiguity gate (#174, Cap67Range) so it never derives past a near-tip lake hole; the timer is retired (single writer, no watermark race); Restart=always + StartLimitBurst make a crash-loop trip to failed (visible to node-healthcheck.sh, which now covers the daemon); a transient ClickHouse error holds the watermark and retries (no ledger skipped). The initial P23→tip backfill runs as the daemon's first catch-up. Operator cutover: apply the ansible (or manually stop+disable cap67-movements.timer and start the -follow daemon) AFTER the ops binary is deployed.

v0.44.6

2026-08-26GitHub ↗

Added

  • Soroban resource metering on `stellar.transactions`. Nine additive DEFAULT 0 columns capture, per Soroban transaction, the DECLARED resource bid (instruction count, disk-read / write bytes, read/write footprint entry counts, total resource-fee bid) decoded from the tx envelope's SorobanTransactionData, plus the ACTUAL charged fees (non-refundable, refundable, rent) from the tx meta's SorobanTransactionMetaExtV1. Both are decoded at ingest from the LedgerCloseMeta the indexer already holds; the decoder is envelope-type-aware (unwraps a fee-bump to its inner tx — a naive access nil-panics) and meta-version-aware (V3 + p27 V4). No actual-instructions value is stored — pubnet ledger meta carries none (it lives only in diagnostic-event core_metrics the lake does not store). Populated go-forward; the sparse Soroban-only columns compress to near-nothing. Requires the additive transactions_soroban_metering.sql migration applied BEFORE the indexer binary (else the tx INSERT halts ingest). Follow-up exposes the columns on GET /v1/tx.

Fixed

  • `stellarindex_aggregator_outlier_storm` alert rescoped from a self-poisoning relative-spike comparator (>5× a [1h] offset 1h baseline — a sustained storm's own drops entered that baseline window and flipped the ratio false at ~72m, so the alert could never fire on the very storm it exists to catch, and it ticketed on every benign single-pair robust-VWAP trimming burst) to an absolute per-pair sustained gate (sum by (pair) rate[10m] > 10 for 2h). Silent on transient dispersion, fires on a persistent dispersion / broken-connector storm.
  • Explorer static export now rides out a transient 502/503/504 from the API (typically the API mid-deploy) with the same patient, Retry-After-aware, bounded wait buildFetch already used for 429, instead of failing the whole next build on one asset's momentary unavailability.

v0.44.5

2026-08-26GitHub ↗

Fixed

  • The trades.signer sweeper (v0.44.4) failed every tick with tuple decompression limit exceeded (SQLSTATE 53400) and tagged nothing: its TagTradesSigner UPDATE joined on (ledger, tx_hash) with no ts predicate, so on the ts-partitioned trades hypertable it scanned every chunk — including compressed ones — and tripped the per-DML decompression limit. Added the ts bound (the close-time span of the tagged txs, threaded from the lake read) so TimescaleDB prunes to the window's chunks, mirroring TagTradesRoutedVia. Same fix applies to the tag-signer backfill. Tested against Stellar protocol 22.

Added

  • AMM/Soroban swap actor attribution (`trades.signer`). The AMM decoders (comet/soroswap/aquarius/phoenix) set taker to the on-chain caller and leave maker empty, so a router- or contract-driven swap had no human/EOA attribution — the taker is the router contract. The tx source account is that missing initiator, but it is NOT re-derivable from the lake events the projector replays (they carry no source account), so it cannot be set on the decode path. New nullable trades.signer column (migration 0150, mirroring routed_via: O(1) ADD COLUMN, deliberately kept out of the trades UPSERT so a re-derive cannot clobber it) is back-tagged first-wins by a trailing-window sweeper (pipeline.RunSignerTagger) that reads the signer from the lake's stellar.transactions. The lake read is scoped to the small ledger span of AMM trades still needing a signer (not every recent tx), so it stays cheap at pubnet volume, and a per-tick ledger cap bounds a cold-start / catch-up sweep. For gaps longer than the sweeper's 30-min lookback (an indexer/ClickHouse outage or a projector lag), the stellarindex-ops tag-signer -from N -to N command back-fills the range through the same first-wins primitive. Exposed as signer on GET /v1/accounts/{id}/trades.

v0.44.3

2026-08-26GitHub ↗

Added

  • /tx now shows the Soroban authorization-invocation tree. An InvokeHostFunction operation's decoded fields gain an authorizations tree — the nested SorobanAuthorizedInvocation structure from the op's auth entries (contract + function + args, recursively), rendered on the explorer /tx view as "Authorized invocations." This surfaces the nested contract-call structure the view previously omitted (a step toward the richer /tx detail stellar.expert shows). Decoded from the already-stored operation BodyXDR, so no schema change or backfill. It is the AUTHORIZATION subtree, not the full execution trace (that lives in the tx meta the lake does not store) — labeled as such. The full execution tree + Soroban resource metering remain a separate follow-up (they need tx-meta/resources the lake does not persist).
  • Exploit-shaped detector for AMM self-pair swaps (post-2026-08-25 Blend/Comet). A self-pair swap (token_in == token_out) on a curated AMM pool moves no value between distinct assets and has no honest purpose — it is the primitive the exploit ran ~390 times to walk a pool's spot price, and the freeze + divergence guards were blind to it because the self-pair rows decode to zero rows and never reach the served trades table. New counter stellarindex_amm_self_pair_swap_total{source} is incremented at the comet decoder's drop point, and a stellarindex_amm_self_pair_swap_burst alert (ticket) fires on increase[15m] > 10 — far above the historical-zero baseline. Detection only: it changes no serving or freeze decision, so it cannot create a false freeze. The counter increments only for LIVE (recent ledger close time) events, so a backfill or completeness re-derive of the historical exploit window does not re-fire the alert. Zero-seeded (F-0033) so operators can tell "armed" from "dead metric," and ships with false-positive + replay-suppression guard tests plus a runbook.

Fixed

  • stellarindex_priceless_coverage_check_stale paged a perma-stale FALSE positive from the indexer and api instances. The ..._last_success_unix gauge is registered in the shared obs registry, so every binary exports it, but ONLY the aggregator runs the coverage sweep that sets it — on the other two it sits at unix 0 forever, so time() - 0 crossed the 1800s staleness threshold on every evaluation. Scoped the alert expr to job="stellarindex-aggregator"; a genuinely-wedged aggregator (its own gauge stuck at 0) still fires, and a promtool case guards the non-aggregator-instance suppression.

v0.44.2

2026-08-25GitHub ↗

Added

  • Scam-pricing gate. An asset whose issuer is flagged scam-class (malicious/unsafe/fraud/scam/hack/phishing) in the curated account directory now has its aggregated price AND market cap/FDV withheld — a scam token no longer publishes a value that lends it legitimacy, even when its market clears the thin-market substance floor (RIO-GBNLJIYH… did: it showed a $0.0072 price + a $540k market cap on a deprecated-scam issuer). Wired at the price-reader seam so one gate covers /v1/price, /v1/price/batch, /v1/twap, /v1/vwap, the SEP-40 oracle price paths, the asset headline and the live tip (keyed on the base, so it holds across quotes incl. XLM triangulation), plus a payload suppression on the /v1/assets listing + detail (market_cap / fdv / price / change). Raw trade surfaces (/v1/ohlc, /v1/observations, /v1/history) and circulating_supply stay visible; the gate fails open on a directory outage. Deliberately overturns the directory's historical "display-only, tags never gate pricing" invariant. (#182)

Fixed

  • completeness_incomplete{source=comet} fired persistently after the 2026-08-25 Blend/Comet exploit — NOT a data gap (the lake is 100% complete, watermark at tip) but a verdict artifact: the exploit's 36 self-pair swaps (token_in == token_out) fail canonical.NewPair, so the comet decoder returned an error and the completeness re-derive counted each as an undecodable blind spot, holding the source complete=false forever (the INV-3 do-nothing re-derive trap). The decoder now returns "zero rows, no error" for determinate business-rule rejections (self-pair swap, non-positive amounts) so the re-derive counts them as expected=0; the error path stays reserved for indeterminate parse failures. (#185)
  • external_fx_rate_rejections{reason=history_deviation} paged indefinitely on a correctly-refused broken ETB history bar (2026-08-19 = 44, the pre-float peg vs the correct ~160). The band was right to refuse it; the _stuck reclassification that de-noises the alert never engaged because the in-band branch cleared the stuck streak that a good sibling bar in the same trailing-7d sweep had just incremented. Removed the reset so a persistently-broken bar reaches the _stuck threshold (and drops out of the alert), while a genuinely new bad feed still pages. (#185)
  • Explorer asset-page price chart was blank for USDC and every fiat currency: those chart against fiat:USD, which the /v1/ohlc candle path has no rows for (a fiat pair has no on-chain constituent; only /v1/chart carries the fx-cross series). Fiat currencies now render a USD line from /v1/chart, and USDC — the dollar reference, which has no USDC/USD series of its own — shows a "≈ $1.00 reference" panel (linking the divergence board for depeg watching) instead of an empty grid. (#184)
  • Explorer "Top assets by activity" ranked by all-time observation_count (a cumulative counter that floats long-lived stablecoins to the top) and omitted native XLM entirely (it has no classic_assets row, so it never appears in /v1/assets) — so USDC ranked #1 and XLM, the most-traded asset on Stellar, was absent. Now ranks by trailing-24h volume and injects native XLM (useNativeCoin over /v1/assets/native); XLM takes the #1 spot it earns on volume (~$43M vs USDC's ~$36M). (#181)
  • Explorer nav: restored a top-level Ledgers entry in the Stellar section (it had been folded into the Network hub, making it undiscoverable from the rail). (#181)
  • /v1/protocols/{name} per-contract activity ran the raw contract_events FINAL scan (merge-on-read of the 12.8B-row ReplacingMergeTree), which blew ClickHouse's 2 GiB per-query memory limit (Code 241) — that memory kill *was* the "certified-lake reader unavailable" verdict on the protocol page, and the 57s / 3.2B-row scans were a primary CH-load source behind the API p95/p99 latency alerts and the tx-outcome read timeouts. Now routed through the existing contract_events_daily pre-aggregation (like the daily-activity and event-breakdown views already are) — measured 0.5s vs 57s, no memory kill. Last-seen is day-grain (sufficient for the roster column). (#180)
  • /v1/anomalies returned 500 on every request — FreezeReasonCounts and FreezeDailyReasonCounts used the same fragile ($1 || ' days') interval concat that took down /v1/divergence: it types $1 as text, but the handler passes an int, and pgx v5 has no int→text encode plan. Also fixed the latent same bug in ListDivergenceSeries (/v1/divergence/series, $4/$5). All now use make_interval, and a package-wide test forbids the concat form so it can't return a third time. (#179)

v0.44.1

2026-08-25GitHub ↗

Added

  • Live account-movements feed: the /accounts/{id} movements view now auto-follows the ledger tip — an SSE-nudged refetch (~4s coalesced, shared tab-wide) gated to the first page so a keyset walk into history is never yanked back, with a 20s fallback poll if the stream drops. Classic-asset movement lag is cut from ~5min to ~30s by tightening the incremental cap67 derive timer (the derive runs in ~0s per fire; true up-to-the-second via a continuous follow-worker is a follow-on). (#172)

Fixed

  • cap67 account-movements derive could permanently lose movements under lake pressure. The LiveSink drops whole ledgers under buffer pressure, leaving holes near the tip, but the derive resolved its upper bound with raw MaxLedger and advanced its watermark past any hole with no trailing re-derive — so a dropped ledger's classic/native account movements were never revisited (the raw lake self-heals via ch-live-catchup; this derive did not → a permanent gap in account history). Now clamped to ContiguousWatermark (mirroring the real-time projector), so the derive stalls at a hole until catch-up heals it — delayed, never lost. Found by an adversarial audit of the real-time-movements plan. (#174)
  • /v1/divergence (the per-reference divergence board) returned 500 on every request — ListDivergenceLatest wrote its trailing-window filter as now() - ($1 || ' days')::interval, which makes Postgres infer $1 as text, but the handler passes sinceDays as an int. pgx v5 has no int→text encode plan, so the query failed before executing (unable to encode 7 into text format for text (OID 25)). The window is now make_interval(days => $1), which types $1 as an integer. Adds a regression test that forbids the text-concat form. (#176)
  • The volume_character rollup worker refreshed the all-asset 14-day account-structure roll every 15 minutes; each pass is a multi-minute full scan of the trades hypertable (72M rows/7d), so it ran effectively continuously and starved the customer API (the p99 2259ms regression introduced in v0.44.0). The cadence is now 6h and the roll caps its own max_parallel_workers_per_gather + statement_timeout so a single refresh can't monopolize the primary. (#175)
  • Explorer pill contrast: category / venue / type chips now route through the adaptive design tokens instead of hard-coded colors, fixing low-contrast pills in the dark theme. (#173)

v0.44.0

2026-08-25GitHub ↗

Added

  • Materialized volume_character rollup (design §2): a worker-maintained per-asset table (migration 0149) computes the wash-vs-market account-structure signals in one all-asset pass on the aggregate cadence. The /v1/assets/{id} detail now reads it as a keyed lookup instead of a per-request 14-day roll (which timed out at 4s on high-volume assets like USDC), and volume_character is now carried on the /v1/assets listing. (#35)
  • §4-B "annotate + demote": the default volume_24h_usd_desc sort ranks by concentration-adjusted volume (raw × (1 − top_account_pair_vol_share) for concentrated/operational assets), so wash/operational volume no longer tops the directory. The raw volume_24h_usd chain fact stays visible and every asset stays present — a sort-key overlay only, never a value change or a hidden row. (#35)

Security

  • Bumped golang.org/x/mod v0.39→v0.40 (CVE-2026-56864, CVE-2026-56865 — malicious GOPROXY/GOSUMDB) and github.com/moby/go-archive v0.2→v0.3 (CVE-2026-17106 — tar path traversal). govulncheck clean. (#169)

Fixed

  • TestExternalFleet_EndToEnd integration flake: the consumer goroutine inserted drained events with the fleet context that shutdown cancels mid-drain; inserts now use a decoupled context. (#169)

CI / tooling

  • Dependabot ignores TypeScript major bumps in the explorer (openapi-typescript is not yet TS7-compatible), stopping a recurring red PR. (#169)
  • Corrected the stale ansible-drift comment: the vault secrets are restored and the check works; a failure now signals genuine r1 drift. (#169)

Operator notes

  • Migration 0149 (asset_volume_character rollup table) applies via the standard deploy migration step. No new Prometheus rules or systemd units.

v0.43.0

2026-08-25GitHub ↗

Added

  • Alias-aware asset directory: SAC/alias twins fold onto their canonical classic row (summing 24h volume + trades with exact big.Rat); a configured SAC asset_id resolves to its classic on the detail path. (#28)
  • Priceless-popular pricing-coverage tripwire: an aggregator sweep pages when a genuinely popular asset (market-character volume, wash excluded) has no served price. New stellarindex_assets_popular_priceless gauge + sweep-health metrics, bounded by a 5m per-sweep timeout. (#28)
  • stellar-expert scam-label + volume_character signals surfaced on the asset directory + detail (issuer_directory_{tags,domain,name}, volume_character, volume_character_signals). (#30)
  • Per-account activity watermark bounds the ops-by-account ClickHouse scan (fail-safe: a missing watermark falls back to the pre-existing unbounded scan). (#31)
  • W8 ops observability: projector-wedge gauge, notify-send metric, and a verify-archive Tier-B nightly timer. (#33)

Fixed

  • /v1/network/stats stamps honest flags.stale + as_of on the stale-while-revalidate serve path instead of silently asserting fresh (REC-05, same class as /v1/markets).
  • /v1/markets stamps honest stale + as_of on SWR stale-serve. (#160)
  • Convert page hydrates header/inverse/ladder live off the shared query instead of serving build-frozen residue. (#32.10a)

Security

  • Removed permissionless DeFindex strategy self-registration: the curated MainnetStrategies set is the sole trust root; a factory create body can no longer seed a poisoned strategy into the gated registry. (W8 6c)

Operator notes

  • New Prometheus alert rules (notify, pricing-coverage, projector, verify-archive), the verify-archive-tier-b systemd timer, and the account_activity ClickHouse table + MVs are config/schema that a binary-only deploy does not apply — apply them alongside the binaries.

v0.42.0

2026-08-25GitHub ↗

Added

  • Declared fiat-peg pricing (AUDD/AUDR → AUD × served fx), price_basis=declared_peg. (#154)
  • USDC + SAC quote bridges for directory pricing. (#152)

Fixed

  • FX confirm-veto: an agreeing 7d history majority refuses a pending confirm, stopping the persistent-broken-upstream (UZS) re-poison; genuine devaluations still confirm. Outlier-drop counter gains a pair label. (#157)
  • /v1/assets/{id} detail overlay is substance-gated; dust prices no longer leak onto detail. (#154)
  • Account ops pages: detached budget for the tx-outcome stitch. (#155)

Fixed

  • FX guard: history-majority confirm veto (the Massive UZS second act). A persistently-broken current feed can no longer re-poison a healed baseline through the two-fetch confirmation: when a ticker's trailing-7d majority (≥4 bars mutually agreeing within 10%) REFUTES a pending candidate, the confirm is refused (deviation_history_conflict; repeats reclassify to …_conflict_stuck, excluded from the rejection alert). History still never SETS a baseline — genuine devaluations confirm as soon as the majority stops refuting (follows the move, or the split window yields no majority). Red-proven tests. (task #29)

Added

  • stellarindex_aggregator_dropped_trades_total now carries a pair label (the configured target pair, bounded ~12), so an outlier_storm is attributable with topk by pair instead of ad-hoc SQL — the 2026-08-14 single-issuer SDEX token-farm wave took the latter. Storm/spike alert exprs sum() across labels and are unchanged. (task #29)

v0.41.1

2026-08-24GitHub ↗

Fixed

  • FX guard: history-majority heal for poisoned bootstrap baselines + jitter-tolerant stuck streak (the Massive UZS incident). A broken current-feed bar bootstrap-accepted at restart no longer poisons the baseline against the ticker's own correct 7-day history: ≥4 mutually-agreeing rejected bars refute an unconfirmed single-sample baseline (median wins, the poisoned current-day row is scrubbed before write). Confirmed baselines are never healed; split series never heal. Stuck-streak reclassification now tolerance-matches (exact float equality never matched a live jittering upstream). Two-lens verified; red-proven tests. (#146)

Added

  • Synthetic USD-cross divergence reference — non-USD-fiat pairs (XLM/EUR, XLM/GBP, …) get a second reference (on-chain oracle base/USD ÷ reflector-fx or chainlink fiat/USD), so SuccessCount reaches the divergence trust floor and ADR-0019's corroborated release can auto-release genuine repricings unattended — four operator freeze-releases on 2026-08-24 alone were this class. Migration 0148 admits the source to divergence_observations (pure-widening CHECK; decompress dance). Two-lens verified incl. live migration exercise against compressed chunks. (#149)

v0.41.0

2026-08-24GitHub ↗

Fixed

  • Freeze lifecycle: escalated-freeze ratchet + restart stall + corroborated release (ADR-0019 amendment 2026-08-24). Mid-freeze buckets now score per-tick returns against a shadow comparator (kills the drift-since-freeze ratchet that kept XLM/GBP-style freezes from ever releasing at a new stable level, and the restart→unscored stall). Because any HELD level reads calm under per-tick scoring, auto-unfreeze now additionally requires a corroborating lens reading that agrees within 5% with the bucket's own fresh candidate price (Signal.ReleaseCorroborated): a genuine repricing whose references follow releases; a parked manipulation walks the ladder to the operator. Pairs with no usable reference never auto-release (fail-closed; they escalate and page). Verified by a 3-lens adversarial panel; red-proven regression tests at both the policy and orchestrator layers. (#142)

Changed

  • Explorer: /sdex is the one canonical SDEX surface (protocol analytics view; /protocols/sdex 301s server-side). Accounts page frame + logo polish. (#141, #143, #144)

v0.40.1

2026-08-24GitHub ↗

Fixed

  • /v1/price p95 tail eliminated: the serving pool now forces custom plans (plan_cache_mode=force_custom_plan post-connect). Root cause: Postgres flipped the request path's raw-trades fallback to a generic plan whose build costs ~206 ms across the ~870-chunk trades hypertable and is rebuilt on every plancache invalidation (~1/min) — a steady ~5 % of serving binds paid 250–330 ms. Custom plans bind in 0.2–3 ms. Background/ops pools keep the default plan mode.
  • Stuck-upstream FX rejections no longer hold the alert red: after 12 consecutive refusals of the SAME broken history bar (the Massive ETB=44 case) repeats reclassify to history_deviation_stuck, excluded from the alert; fresh disagreement still alerts immediately. The guard refuses the bar either way.
  • Explorer: navigation revised into Stellar / External / Developers sections, Stellar-mark + Inter wordmark logo with the live ledger number beside it, new /insights hub, /network sub-surface links. No migrations.

v0.40.0

2026-08-22GitHub ↗

Fixed

  • OHLC bars are now bit-for-bit reproducible (migration 0147): the price CAGGs' open/close resolve same-instant ties by a total key (epoch-µs ‖ ledger ‖ tx_hash ‖ op_index ‖ source) mirroring the raw-trades serve order, instead of physical scan order. VWAP switches to the exact single-division form (≤1e-16 relative, below wire truncation — the 0115-invited free rider). ⚠ The migration recreates the seven price CAGGs + twap_1h/1d WITH NO DATA; re-materialization is the deploy follow-up (recent-first plan in the migration header).
  • Freeze markers now write for Phase-2 freezes on Phase-1-off deployments: the freeze writer was gated on the Phase 1 anomaly checker while the Phase 2 confidence lifecycle runs unconditionally — engaged freezes (r1 XLM/GBP) refused publication with no Redis marker, serving the last value with flags.frozen absent. Writer is now built unconditionally; AST tripwire added.
  • The daily supply-snapshot writer can now actually run: the auto snapshot-ledger resolver clamps to the lake's landed tip (bounded, 512 ledgers) instead of demanding the realtime cursor's not-yet-landed stellar.ledgers row — the structural race that failed every timed run. Operator -ledger stays exact fail-closed; wall-clock stamping remains impossible.
  • Integration tests quiesce CAGG refresh policies in the shared bootstrap (the 55P03 concurrent-refresh flake).

Changed

  • Monitoring: the system recognition census is a drift gauge (stellarindex_recognition_unattributed_shapes) with a step-change alert, no longer a permanently-red completeness_incomplete row; new galexie-archive partition-contiguity guard (hourly scan + page alert on any gap/overlap outside the declared capacity trim).
  • Explorer: shared LastPriceCell (restores the tick flash DexesView's fork had lost), /dexes pools board follows ledger closes, home Recent Trades ticks on ledger closes instead of a blind 30s poll.

v0.39.1

2026-08-21GitHub ↗

Added

  • `GET /v1/livez/lake` — the lake-critical LB probe (ADR-0050 §7.3): 200 iff ClickHouse pings; 503 on failure or when no lake is wired (fail-closed). Complements /v1/readyz's deliberate CH-non-criticality so a lake-dead instance can be pulled for lake routes without touching pricing.
  • SLO lake-guard test: CI now fails if any SLO'd handler (/v1/price*, /v1/oracle/*) reads a ClickHouse-backed field — the "no SLO'd route touches the lake" invariant, enforced.

Changed

  • Phoenix completeness reconcile is now STRICT per-ledger — the aggregate netting opt-out is retired (own-ledger attribution removed the sweep-shift it absorbed; proven with 0 mismatched ledgers before removal). A real drop can no longer net against a phantom.

Fixed

  • Served-reader determinism: TradesInRange gains the full ORDER BY tiebreak (raw OHLC bars no longer depend on arbitrary same-timestamp ordering); account_movements gains the LIMIT 1 BY read-time dedup its sibling readers already had; NetworkThroughput derives its day window and Partial flag from the data's tip close time instead of the wall clock.

v0.39.0

2026-08-21GitHub ↗

Added

  • DeFindex `dfees` fee distributions are now modelled (W5.2, the last open launch item). Body shape proven from captured on-chain blobs (Map{"distributed_fees" → Vec[(token, i128)]}, per-asset, empty vec valid): one row per distributed-fee token into the new defindex_fees table, with full sink/projector/reconcile registry parity. The ~12.8K historical events backfill via projected-rebuild -source defindex after this release deploys.
  • Explorer feels alive: live data across the whole site. Pool reserves, pair tables, and lending reserves refresh on every ledger close (shared useLedgerFollow); charts advance their forming candle; the home "live USD price" actually streams and flashes; venue/DEX last-price cells flash again; the asset History tab is a true live trade tape over the previously-unused /v1/observations/stream; rollup panels and activity feeds auto-poll.
  • Failed transactions are first-class on the explorer and API with explicit failed status and failure reason (D-PART-FAILEDTX decision), plus the 2026-08-14 audit's decisions batch.
  • OpenAPI spec overhaul: exact route parity (129/129 with unique operationIds), valid OpenAPI 3.1 null unions, 0 Spectral errors, regenerated Postman + types.

Fixed

  • Completeness re-derive counts sweep-rescued outputs at their own ledger (eventLedgerCarrier): a correlation-buffer rescue (phoenix 7-field era) is now attributed where its served row lives instead of at the sweep-trigger ledger, removing the CS-084 ± shift noise from strict per-ledger reconciles.
  • `projected-rebuild` clamps `-workers` to 1 for correlation-buffer decoders — concurrent out-of-order windows starve sweep triggers and silently drop groups (measured: 4 workers lost ~650 of 5,154 phoenix era trades in a dry-run; 1 worker lost none).
  • SQLSTATE class extraction guards malformed codes (sqlStateClass) instead of slicing blind.
  • The pgBackRest restore drill had never once run on its schedule (BDR-04). CS-110's whole point is evidence that the backups restore, and the scheduled path produced none — for three stacked reasons, each hidden behind the one before it: 1. PrivateTmp=true gives the unit its own empty /tmp and /var/tmp, so DRILL_ROOT=/var/tmp/restore-drill — a provisioned 5.2 TB ZFS dataset, plainly present on the host — did not exist inside the service's mount namespace. ReadWritePaths on that path failed namespace setup and systemd aborted the unit with 226/NAMESPACE BEFORE ExecStart. Every passing drill on record was run by hand, which has no namespace. 2. With that cleared, NoNewPrivileges=true blocked sudo's setuid transition ("unable to open /etc/sudoers: Operation not permitted"). The unit runs as root by design and DROPS privilege to postgres; no-new-privs protects nothing on an already-root unit while disabling the one mechanism it uses to run with LESS privilege. 3. Then pgbackrest, running as postgres, could not traverse /var/lib/stellarindex (drwxr-x---). The dataset now lives at /srv/restore-drill, postgres-owned — /srv is world-traversable, is not shadowed by PrivateTmp, stays writable under ProtectSystem=full, and already hosts history-archive. The ZFS role gained optional per-dataset dir_owner/dir_group (default(omit), so every other dataset is untouched).
  • `tip_lag` was measuring the backup's AGE, not recoverability (BDR-05). The scratch instance runs hot_standby = on and is started with pg_ctl -w, which returns the moment CONSISTENCY is reached — while replay of the remaining archived WAL continues in the background. The drill then measured the restored tip immediately, so the number it reported was "how old was the backup we restored from". Measured 2026-08-19: lag 13,392 ledgers (~18.6h) against a diff taken 21h earlier, while archive-get was demonstrably still streaming segments in ~10ms each minutes later. On a daily-diff schedule that made the < 5000 threshold unpassable except by drilling shortly after a diff — the 2026-07-03 pass (240 ledgers) was exactly that accident, and a threshold met only by luck is not evidence. The drill now drains the archive stream to an LSN captured from the live primary before measuring, treating BOTH terminal states as drained (replay passed the target, or recovery ended and promoted — the latter returns NULL from pg_last_wal_replay_lsn() and would otherwise spin to the timeout on the very run that succeeded). The drain is a reported check of its own, so a timeout can never masquerade as a clean measurement.
  • The ReadWritePaths directive is gone entirely rather than repointed: ProtectSystem=full already leaves /var and /srv writable, and the directive's only effect here was to make a missing path a hard start failure.

v0.38.2

2026-08-19GitHub ↗

Fixed

  • phoenix stake-init events no longer trip `recognition_ok=FALSE` (#108). 20 real LP-share staking init events matched no decoder shape, so the ADR-0033 recognition census counted them as unhandled topics and downgraded the whole source — even though they carry no financial row to project. The decoder now *recognises* the init topic (Matches() returns true) and emits nothing by design, so recognition is honest and projection is unchanged. No served-data change.
  • sorocredit `TreasuryUpdated` config event is now recognised and captured (#108). The main contract's TreasuryUpdated topic (a treasury-pointer rotation, body Vec[Address old, Address new]) matched no decoder shape — one real lake event at ledger 63,847,367 was dropped end-to-end, tripping recognition_ok=FALSE. It is now captured verbatim into credit_events.attributes["body"] (exactly like BeaconUpdated / CollateralHashUpdated), with migration 0145 admitting treasury_updated into the credit_events_event_type_check CHECK. No promoted column, no invented semantics.
  • blend_emitter reconcile fan-out false-red (#107). The projection reconcile compared served rows against a lake re-derive that counted the drop event_kind — a fan-out kind the emitter carves out of the served projection — inflating the expected count and reporting a phantom Σ|Δ|=14 mismatch on a source whose data was always correct. The reconcile now excludes drop (event_kind <> 'drop'), so blend_emitter_events reconciles exactly. No served-data change.

v0.38.1

2026-08-18GitHub ↗

Fixed

  • Completeness reconcile no longer times out on factory-gated sources (#104). The -pass per-source projection re-derive streamed the entire ~6B-event CH lake for identity-gated sources with empty catalogue contractIDs (aquarius, phoenix), blowing the 120-min pass deadline (aquarius: projection: context deadline exceeded failed the whole pass on r1). For opted-in gated sources it now scopes the -ch re-derive to a guaranteed superset of the gated contract set (factory ∪ curated seed ∪ protocol_contracts children ∪ lake-announced children) via the contract-indexed contractIDs prefilter — counts-identical to the full stream (Matches() rejects non-gated contracts regardless), just orders of magnitude faster. Opt-in is pinned to {aquarius, phoenix}; defindex is excluded (its decode correlates events across contracts in a tx, which a contract prefilter would break). Fail-closed: a missing contract would under-count → a visible red, never a false green.

Security

  • govulncheck gated behind a documented lib/pq accepted-risk allowlist (#105). The 2026-08 CVE-2026-56868..56874 batch surfaced 7 unpatched *called* vulnerabilities in github.com/lib/pq@v1.12.3 (the latest release of the now- unmaintained driver), failing CI on every PR. All require a malicious/compromised Postgres server or a pre-auth MITM; stellarindex connects only to its own Postgres over 127.0.0.1 (sslmode=disable, no GSS/.pgpass) → not exploitable in this deployment. A reviewed allowlist (scripts/ci/govulncheck-allow.txt + a JSON-mode wrapper that still reds CI on any *other* called vuln) documents the accepted risk; the durable fix (migrate to jackc/pgx) is tracked as a post-launch follow-up.

v0.38.0

2026-08-18GitHub ↗

Fixed

  • soroswap recognition false-red (#100). The ADR-0033 recognition census built its dispatcher without the soroswap pair registry, so its soroswap decoder's pairTokens map was empty and Matches() rejected every real SoroswapPair protocol event — each became a false "unhandled topic" gap attributed to soroswap (and the watermark clamp cascaded into spurious projection floor-loss alarms), even though the indexer decodes + serves those trades correctly. Both recognition-census paths now seed the soroswap decoder from the same LoadSoroswapPairRegistry set attribution already uses.
  • aquarius `set_protocol_fee` Vec-body decode (#101). set_protocol_fee events on registered Aquarius pools carry a Vec body (SCV_VEC[SCV_U32] = the new pool-wide protocol-fee fraction, per the pool WASM's singular set_protocol_fee_fraction) that the Map-only decoder dropped, blocking aquarius projection with "undecodable-but-matched" blind spots. decodeFee now branches on the SCVal kind; the absent prior fraction lands NULL (not invented).
  • phoenix incomplete gating seed (#102). The curated MainnetGatedSet was missing 14 verified-genuine phoenix contracts (1 pool + 13 per-pool stake contracts), so the reconcile under-counted them AND the live gated pipeline was silently dropping some still-active contracts' events. All 14 were verified on-chain (factory pool-create co-occurrence / shared reward keeper / stake-v1.1 migration events) and added to the seed; the pre-upgrade 7-field sweep-emit ledger shift is absorbed via aggregateReconcile.
  • defindex projection dirty window re-verified clean and cleared (the #91 harvest-count fix, live since v0.36.0).

v0.37.0

2026-08-17GitHub ↗

Added

  • Comprehensive per-source projection reconciliation + a static catalogue-completeness invariant (#96). The projection axis previously reconciled only a subset of protocol tables; it now carries reconTargets for the 1:1 tables it was missing (aquarius admin / protocol-fee / kill-switches / liquidity / reserves-sync / rewards, soroswap_liquidity, phoenix initialize/admin), with the genuine per-token fan-out tables (aquarius reserves/liquidity, sdex_offer_events) explicitly noReconcile-waived rather than left silently unvalidated. A new AST-walking invariant test asserts every decoder EventKind that routes to a persisted table is either reconciled-by-kind/census or explicitly waived — so a future decoder kind can no longer silently fall out of the reconcile's EXPECTED sum (the exact class of the defindex strategy.harvest undercount that produced a phantom 976-mismatch false-red).

Fixed

  • The nightly completeness-verdict driver no longer times out and freezes the alphabetical tail. run-compute-completeness.sh re-invoked compute-completeness -ch per source AND per 25k chunk, and every invocation re-ran the load-heaviest step — the global DistinctTopicShapes recognition scan (~60s over full history, identical regardless of -source/-from). A source pinned far below tip (aquarius, recognition-capped near its genesis) walked hundreds of chunks, so that one scan ran hundreds of times per night — the 3h52m timeout (Result=timeout) that left the alphabetical tail's verdicts days stale. A new compute-completeness -ch -pass mode proves recognition + substrate ONCE at full range for the whole catalogue and reconciles each source's projection incrementally from its own watermark; the wrapper now makes one such call. This also (a) clears the low-tip substrate flap — a full-tip substrate proof advances the tip and is never blocked by the CS-083 write guard — and (b) finally gives every catalogue source a verdict, including the never-seeded blend_emitter/blend_backstop/sorocredit (they reconcile from genesis on the first pass). INV-5, the projection dirty-window mechanism, the substrate/projection fail-closed claims and CS-083 are all preserved unchanged.

v0.36.0

2026-08-17GitHub ↗

Fixed

  • Aquarius pool governance events are no longer silently dropped. The decoder gated 7 governance topic symbols (apply_upgrade, commit_upgrade, set_privileged_addrs, apply_/commit_transfer_ownership, enable_/disable_emergency_mode) on the canonical router only — but the 337 registered Aquarius pools emit them too (a protocol-wide staged WASM upgrade of 320/337 pools). Pool-emitted governance events returned Matches()=false, becoming an ADR-0033 recognition gap (holding aquarius completeness red) AND never reaching Decode → ~1,679 real events lost since ledger 55,363,632. The gate now accepts registered pools (reg.Has || reg.IsFactory; unidentified emitters still fail closed), and the upgrade decoder handles the pool body arities (router = 1 wasm hash, pool apply = 2, pool commit = 3 → staged hashes in attributes.wasm_hash_N). The events now land in the already-served aquarius_admin table. (A backfill re-processes the historical drop.)
  • Defindex `strategy.harvest` flows are counted in the completeness verdict. The reconciliation catalogue omitted defindex.strategy.harvest from the defindex_flows expected-count kinds, so the ADR-0033 verdict under-counted every genuine harvest by exactly 974 (served=1, expected=0), false-flagging defindex complete=false. The served data was correct; adding the kind fixes the count. Count-only — no data mutation.

v0.35.0

2026-08-16GitHub ↗

Fixed

  • XLM circulating-supply refresh no longer falsely freezes during quiet periods. The supply freshness gate anchored on MAX(ledger) over account_observations, which only rows on a watched SDF-reserve-account *balance change* — so any market-quiet stretch beyond the ~1-day dormancy horizon made the anchor go stale and the gate fail closed, freezing XLM supply and firing a continuous supply_refresh_error_dominant ticket (which in turn masked a genuine future observer death). The served value was always correct — only its freshness signal was wrong. The gate now anchors on a true per-tick observer watermark (new account_observer_watermark table, migration 0144), advanced every ledger by the indexer: a healthy-but-quiet observer stays fresh, a genuinely dead observer still trips the gate. Found only by a live audit of r1 — the code looked correct; the live quiet-reserve state triggered the latent flaw.

v0.34.0

2026-08-16GitHub ↗

Security

  • Account-history participant injection closed. A Soroban InvokeContract op's call arguments and SorobanAuthorizationEntry entries are attacker- controllable at the XDR-decode layer, so they are no longer indexed as account participants. Previously an attacker could inject an arbitrary victim's address into that victim's permanent, public /accounts/{g}/operations history under the attacker's own signature.
  • audit-2026-08-14 remediation — 79 verified fixes across money-correctness (SDEX single-leg plausibility ceiling, oracle-execution corroboration for the Band adapter, MEV-detector evidence-gating + mev_events retention), auth/data-integrity (self-service key-mint scope hardening, session token hashing at rest via migration 0143), and projector durability. Each landed with a proven-red regression test.
  • Go toolchain 1.25.12 → 1.25.13. govulncheck reported 7 standard-library vulnerabilities reachable from live call paths — net/http (GO-2026-5026, Punycode label handling) via the ClickHouse reader, the CoinGecko supply client, the history-archive checkpoint resolver and the galexie trim's S3 calls, and encoding/asn1 via the WebAuthn passkey registration path. All are fixed in go1.25.13. Every workflow reads go-version-file: go.mod, so the toolchain directive is the only pin to move. Verified clean locally: "0 vulnerabilities".

Changed

  • Asset identity: one alias registry. A binary-startup AliasRegistry built from [supply].sac_wrappers folds an asset's SAC-wrapped form into a single identity (SAC form ordered last), threaded through the price/volume read paths. Fixes alias-blind volume/price reads across ~11 money endpoints (asset detail, VWAP/TWAP/OHLC, pairs, markets, aggregate global tiers) that previously split an asset's SAC and classic forms into two un-aliased identities. Non-XLM folding activates per [supply].sac_wrappers config.

Fixed

  • Absent-vs-zero honesty across the read surface. /v1/status incidents now carry an explicit ok|degraded|unknown tri-state (a failed alert query no longer serialises as a false all-clear); /v1/tx distinguishes partial event / op-result coverage; /v1/protocols serves from an SWR cache instead of a per-request unauthenticated scan; the explorer degraded-banner and network-insight no longer read a failed query's zero as real data.
  • Incidents Atom feed `<updated>` now reflects the most-recent entry's timestamp (empty feed → a stable sentinel) instead of wall-clock now(), so a stale or empty feed is no longer syndicated as freshly updated every crawl.
  • `TestMigrationsRoundTrip` could deadlock against TimescaleDB's own job scheduler, turning `main` red for 30 hours and firing the ci-health tripwire every two hours. The test asserts compression and CAGG-refresh policies are attached, then rolls every migration back — so migrate down's DROP ... AccessExclusiveLock raced the 16 background workers running those very policies, and the two could form a lock cycle ("deadlock detected, Process 94 waits for AccessExclusiveLock on relation 21724; blocked by process 161"). It only reproduces under load, which is why it passes locally in 5s. Retrying is not available as a fix: a failed migration leaves golang-migrate's version DIRTY. The container now runs with timescaledb.max_background_workers=0, removing the concurrent actor entirely, and the test asserts the setting actually applied — a Cmd override that silently failed to take would otherwise look exactly like a fix. The assertions are unchanged in strength: they check policies are ATTACHED (a metadata row), not that they run.

v0.33.2

2026-08-13GitHub ↗

Changed

  • `GET /v1/contracts/{id}/interactions` now anchors its window to the contract's own recent activity, so `?days=` is an UPPER bound rather than the window served. Both halves of the read scale with the ledger span they cover, and over the default 90 days a busy contract cost 3–6s — the slowest panel left on the contract page once the /wasm scan was bounded. Narrowing to the contract's 500 most recent active ledgers brings that to 0.705s. This is a deliberate trade, not a free win: shared_txs counts drop for busy contracts. The ranking — which is what the panel is for — was unchanged in the same order on the measured sample, and the endpoint has always reported a bounded recent sample (subjectTxCap truncates at 50,000 transactions). Quiet contracts, which are most of them, have fewer active ledgers than the cap and keep the full window. since_ledger reports the floor actually served, and the OpenAPI description now says so.

v0.33.1

2026-08-13GitHub ↗

Fixed

  • Every cold contract page served at least one failed panel, because the page starved itself at the refresh gate. The contract view fans out to five concurrent reads, but four of them (detail events, interactions, code-history, account activity) all acquired the single refresh-gate class contract_detail, capped at half the global limit — two slots. So on a cold contract two of the four refreshes were refused, and a refusal with nothing cached is a 503, not a stale serve. Measured on 20 of 20 cold random contract pages, and it was not crawl pressure: the same rate held with seconds of think time between pages. The per-class cap exists to stop one class starving the OTHERS, so the classes are now keyed per panel, which restores that intent without letting a page compete with itself. The global bound was also below one page's width (4 for a 5-read page) — raised to 8, with the explorer ClickHouse pool 8 → 16 so "detached refreshes can never consume the whole pool" still holds. r1 has 20 cores and idles at ~2 concurrent queries, and every explorer scan is pinned to max_threads = 4.
  • scripts/ops/contract-page-audit.py now scores a non-2xx/404 panel as UNLOADED instead of as a fast response, and reports it separately from latency. The first version counted a 503 as a loaded panel, so it rated pages "ok" at 0.10s while three of five panels were failing — a broken page scored better than a slow one. It also takes PACE, because "is one cold page fast" and "does the site hold up under a sustained crawl" are different questions and were being answered by one number.

v0.33.0

2026-08-13GitHub ↗

Security

  • Registered API keys were completely unmetered in production (audit 2026-08-13 F1): MirroredKey carried no monthly quota, so the record the deployed Redis validator reads had none, and the quota middleware short-circuits at <= 0 — every key /v1/register handed out advertised a 1,000,000/month cap (in its own response body and in the public agent docs) and was enforced nowhere. The rate limiter was the only live bound. Quota now flows through the mirror, with a round-trip test (real store → real validator) asserting LITERAL expected values: the prior tests compared a component against its own input, which is why a dropped field read as correct on both sides.
  • `POST /v1/register` was cross-site invocable (F4): the Content-Type gate only validated the header when present, so a header-less POST — a CORS *simple* request, never preflighted — let any page create an account plus a permanent credential per visitor via fetch(…, {mode:'no-cors'}), while burning tokens from the per-IP throttle this endpoint shares with /v1/signup (with the source addresses distributed across victims). The header is now required; docs and examples send it.

Fixed

  • A contract page took ~8s to finish loading because the WASM panel paid an unbounded lake scan to produce a nicer 404. When a contract has no captured instance — the common case — /wasm asked "is this a SAC?" via contract_id = ? ORDER BY ledger_seq DESC LIMIT 1 over contract_events, the quiet-contract reverse-scan trap that contract_active_ledgers exists to prevent. That cost ~0.34s idle, but the contract page fires five reads at once and the other four return via stale-while-revalidate while spawning background refreshes, so the inline WASM read was starved to its full 8s request deadline. 23 of 25 cold random contract pages breached the 1s budget on this single call, and it also starved sibling panels into intermittent 503s. The probe is now bounded to the contract's own recent active ledgers (0.008s measured on r1, ~40x), and an empty active-ledger walk answers authoritatively without touching contract_events at all. New scripts/ops/contract-page-audit.py measures the whole page the way a browser loads it — concurrently, scored on the SLOWEST panel — because the per-endpoint harness reported every one of these reads as passing.
  • SECURITY (live surface): a captured passkey sign-in was an unlimited, never-expiring session mint. POST /v1/auth/passkey/finish-login accepted a replay of the same ceremony cookie + assertion body indefinitely: the ceremony carried no server-side expiry, and nothing marked a challenge used. The expiry was believed to be covered — the guard was written — but go-webauthn only stamps SessionData.Expires when Config.Timeouts.<ceremony>.Enforce is true and that field defaults FALSE, so Expires was always the zero time and the check was dead code. The only bound was the cookie's Max-Age, which is a browser hint an attacker's HTTP client ignores. Two fixes: the timeouts are now configured (5 minutes, enforced) and an unstamped ceremony is refused rather than treated as eternal; and each challenge is now SINGLE-USE, spent through a Redis-SETNX guard (passkey:ceremony:*, the same mechanism the SEP-10 replay guard uses — F-1224) after the assertion verifies and before any session is minted. The guard fails CLOSED: if the store is unreachable the sign-in is refused (500) rather than granted on trust, and email-code sign-in is unaffected. Redis-less deployments fall back to an in-process spent-set (single-instance accounting, warned at boot). Note for reviewers of the old behaviour: the sign-counter clone check was NOT a backstop here — go-webauthn deliberately exempts counter 0, which is what Apple/iCloud passkeys report forever. Regression tests drive the real ceremony end-to-end against a software authenticator, including a mint-then-replay.
  • SECURITY (live surface): passkey sign-in never asked for or required user verification, making passwordless sign-in possession-only — whoever held the authenticator was the account, no biometric or PIN involved. AuthenticatorSelection was unset and neither begin call passed a user-verification requirement, so the library's shouldVerifyUser was false, the UV bit was never checked, and the options JSON omitted the field entirely (browsers then applied their own default). Both ceremonies now require user verification. Trade-off, taken deliberately: a security key with no PIN configured can no longer be enrolled or used as a first factor.
  • A passkey label with 34+ multi-byte characters 500'd instead of saving. The name was truncated by BYTES while the storage CHECK counts CHARACTERS, so a CJK label was cut mid-rune, and Postgres rejects invalid UTF-8 — after the authenticator had already burned a resident-credential slot for a credential the server then never stored. Truncation is now by runes.
  • "Body too large" was unreachable on four auth endpoints (/v1/auth/login, /v1/auth/verify-code, both passkey finish routes): io.ReadAll(io.LimitReader(…)) returns a nil error at its cap, so an oversize body was silently TRUNCATED and then surfaced as a confusing parse error. All four now use http.MaxBytesReader, the pattern the rest of the repo already follows.
  • `/v1/accounts/{g}/positions` runs its six protocol folds in parallel (sub-second audit's last warm breach, 1.99s): the folds are independent Postgres reads and were executed serially, so the endpoint's latency was their sum rather than their max. Output is byte-identical — each fold writes its own slot and the results plus coverage notes merge in the original fixed order. Fixing this also required making the shared per-request asset resolver concurrency-safe: it memoises into a plain map, and concurrent map writes are a FATAL runtime throw no recover() catches, so the parallel folds would have crashed the process under load.

Fixed

  • Protocol pages keep their bespoke visual suite when the battery misses its budget (§2.6b grounding incident): the detail VIEW has been prewarmed + stale-served since 2026-07-31, but the bespoke block INSIDE it had no cache of its own — it is built last, so it inherited whatever was left of the rebuild's 90s budget, and when that ran out (protocol bespoke build failed … context deadline exceeded) the block was dropped and, on a key with no healthy entry yet, the suite-less view was cached and stamped fresh. The block now has a last-good cache with a detached, single-flighted, gate-classed (protocol_bespoke, its own served-tier gate — these are Postgres queries, not lake scans) refresh: a build serves the previous block instantly and never blocks, only a true first-ever miss computes inline (bounded by its caller's context, with the compute surviving it so the next build lands warm), and a failed or starved refresh keeps the last good block. A block older than 45 minutes (≈3 prewarm sweeps) is still served but reported: analytics.status gains a stale value, distinct from unavailable, and such a build now counts as COMPLETE for cache displacement instead of being pinned out as degraded.
  • `/v1/network/throughput` is prewarmed and snapshot-served: the /network page's daily series is a FINAL scan over up to a year of stellar.ledgers with three argMax columns, and it ran inline on the 8s request budget — so a cold or loaded first load lost the panel (the "no operations in 24h" half of the same incident) and, because the scan died with the request, no retry could land warm. It now rides the established SWR shape (5-minute TTL matching the API's 5-minute prewarm loop, detached single-flight refresh under the network_throughput gate class, stale entries served with flags.stale + their real as_of). ONE entry holds the maximum 365-day window and every request slices its tail, which also collapses the key space: an unauthenticated caller walking ?window_days=1..365 previously bought 365 distinct year-class scans. partial is now decided at serve time, so a cached series that crosses UTC midnight no longer advertises a complete day as still accumulating.

Fixed

  • Explorer: absent data no longer renders as a factual zero (frontend-honesty sweep, follow-on to the CCTP / roster / /network incident in docs/operations/v1-launch-plan.md §2.6b). A whole class of surfaces coalesced a MISSING value — an expensive aggregate the API honestly omitted on a budget miss, a 503 from an 8s query ceiling, a build-time transport blip — into ?? 0 / ?? [], then published the result as an empirical claim about the chain. Absent now renders or an explicit "unavailable" affordance; a served zero is still rendered as `0` / "no X", which is the entire point of the distinction. Fixed: - /dexes/{source} + /exchanges/{name}: a /v1/markets 503 claimed "No pools/pairs found in the last 14 days" (and "0 on this page"). - /exchanges: the CEX pair board is a Promise.all over four venue fetches — one 503 headlined "0 CEX pairs · No CEX pairs reporting". - /dexes, /oracles, /aggregators: a failed /v1/sources read claimed Stellar has no DEXes / no oracles / no aggregators. - /issuers/{g}, /issuers long-tail shell, and the issuer panel on every asset page: /v1/issuers/{g} SOFT-FAILS its per-asset fan-out (error *or* deadline), so absent assets was baking "Assets 0", "Total observations 0", "Issued assets (0)" and "No issued assets observed" for issuers with live assets. Unknown first/last-seen ledgers also rendered as #0, a ledger that cannot exist. - /assets/{slug} liquidity tab: a bespoke fetcher swallowed 5xx, 429 and its own timeout into [], baking "No DEX pools observed touching {code}" into the static export. - /assets/{slug} supply tab: a failed /v1/chart asserted "No market-cap history for this asset". - /external/assets/{slug}: any transport failure baked the flat denial "We don't track an external asset with the slug X"; only an authoritative 4xx may say that now. - /lending/{pool}: an empty listing (what the API serves when no lending reader is wired) baked "Auctions (total): 0". - /sources/{name}: a null market read baked "0 pairs · No markets observed for this source". - /status: an unreachable latency backend rendered "0.0 ms" in green (a perfect-SLO claim from a missing measurement) and a failed freshness probe rendered "0 / 0" active sources. Each fix ships a render test asserting BOTH directions — absent → /unavailable, served zero → 0/"no X".

v0.32.1

2026-08-13GitHub ↗

Fixed

  • `/v1/accounts/{g}/transactions` 6.7× faster (sub-second audit 2026-08-13, r1-measured): both union arms carried the WIDE tx column set (memo, result_code, source_account, …) through their own scan and sort of stellar.transactions, and the outer DISTINCT then materialised both. The query now resolves the KEYSET in the union and hydrates the wide columns once over the surviving ≤limit keys — 1.479s → 0.219s for the same 50 rows, with the cross-arm dedupe now provided by the hydration pass's LIMIT 1 BY.
  • `/v1/accounts/{g}/operations` 2.7× faster — same two-phase shape, and it matters more here: opCols carries body_xdr, the column the code itself measures at ~600ms over the 24B-row table, and both arms were paying it. 0.407s → 0.153s (r1, 50 rows).
  • CI integration gate stopped failing on the clock: the suite's go-test deadline is raised 20m→35m. It hit the ceiling on three consecutive pushes with the running test 1s in, while the same suite completes in ~13m locally (CI runners are ~1.5× slower) — a gate that reports "the clock ran out" as a failure stops being a signal. The next raise should split the suite by package instead.
  • `POST /v1/register` returned keys that could not authenticate (found in the v0.32.0 post-deploy battery): the mint wrote only the Postgres MANAGEMENT row, but r1's auth middleware validates against the REDIS store (backend=redis), so a freshly registered key 401'd on first use — a 200 response carrying a dead credential, worse than an honest failure. The mint now mirrors the same plaintext into the validator's own store (RedisAPIKeyStore.CreateWithSecret) whenever that store is wired, and a mirror failure fails the request instead of handing back a key that cannot work. The agent-onboarding flow is functional again.

v0.32.0

2026-08-11GitHub ↗

Added

  • Passkey (WebAuthn) sign-in for the dashboard: six new endpoints under /v1/auth/passkey/begin-login / finish-login (anonymous, usernameless discoverable-credential flow; finish mints the SAME session cookie the email-code flow does, via the shared session-mint path), begin-register / finish-register (session-gated; resident key required so the credential can sign in usernameless), and credentials (GET list + DELETE {id}, session-gated, owner-scoped). Server is github.com/go-webauthn/webauthn v0.17.4; RP ID/origin derive from the existing api.dashboard.base_url. Ceremony state rides a 5-minute HMAC-signed HttpOnly cookie (purpose-bound so a registration challenge can't finish a login); a sign-count regression (possible cloned authenticator) refuses the login and logs. Storage is the new webauthn_credentials table (migration 0140, additive). Explorer: "Sign in with a passkey" on /signin (feature-detected) + a Passkeys list/add/remove card on /dashboard/settings. OpenAPI paths + all three generated artifacts refreshed; SDK triage recorded in uncoveredOperations.

Security

  • Dashboard 6-digit sign-in codes are no longer derivable from the database (parked audit finding, aggregate+dashboardauth cold audit 2026-08-03): the code was an unkeyed public function of magic_link_tokens.token_hash (base32 of its first 4 bytes), so any Postgres read — SQL injection elsewhere, a stolen backup — yielded every in-flight sign-in code directly, no brute force needed, and with it a session for any address the reader could trigger a login for. The code is now HMAC-SHA256(server_secret, token_hash) reduced to 6 digits — same UX, same storage, one derivation swapped; the secret lives in config/env (api.dashboard.code_secret_env, default STELLARINDEX_DASHBOARD_CODE_SECRET), never in Postgres. With the env unset the API falls back to a random per-process secret (still keyed; in-flight codes just don't survive a restart — they live 15 minutes and the magic link is unaffected). Deploy note: codes emailed before the deploy stop verifying for their remaining TTL; links keep working. onboarding path**: one unauthenticated POST (empty body fine; optional name + contact-only email, never verified) creates a free-tier platform account and mints its first Postgres-backed API key, returning {account_id, api_key, key_id, key_prefix, tier, limits} with the plaintext shown once. Rides the same per-IP signup throttle as /v1/signup (shared budget → 429) and the signup Content-Type CSRF gate. OpenAPI path + all three generated artifacts refreshed (docs-api, docs-postman, web-generate-api); SDK triage recorded in uncoveredOperations; agent-facing walkthrough at docs/agent-onboarding.md.

Changed

  • Tier model collapsed to `anon` / `free` / `partner` (follow-up to the Stripe removal — the platform is free). free is every registered account's default, anchored to the old Starter numbers (1000 req/min, 1M req/month, 25 keys, 10 webhooks, 25 price alerts); partner is staff-set per-account limits via the existing PATCH /v1/admin/accounts/{id} override + key-clamp path, with the old Enterprise numbers as ceilings when no override is set (100k req/min, 1B req/month, 250 keys, 100 webhooks, 1000 alerts); anon documents the unauthenticated 60/min per-IP baseline. Legacy stored tier strings map in code (platform.Tier.Canonical: starter→free, pro/business/enterprise→partner; unknown fails closed to free) and writes fold back to CHECK-legal strings (platform.Tier.StorageValue) — migrations untouched. The admin PATCH accepts both vocabularies and canonicalises.

Removed

  • Stripe/billing integration removed — the platform is free (operator decision 2026-08-10: anonymous access, free accounts, staff-set partner limits; no payments). Deleted the POST /v1/webhooks/stripe endpoint (handler + route + OpenAPI path + generated artifacts), [api.stripe] config (STELLARINDEX_STRIPE_WEBHOOK_SECRET), platform.BillingStore / Subscription / StripeEvent and their Postgres store, Account.StripeCustomerID + GetByStripeCustomerID, the stellarindex_stripe_platform_sync_errors_total + stellarindex_stripe_dead_letters_open metrics with both alert-rule trees and their runbooks, and paid-plan copy in the explorer (pricing/signup/company/dashboard now describe free access). The shared tier-clamp machinery the admin PATCH /v1/admin/accounts/{id} path uses survives in internal/api/v1/keybudgets.go (StripeKeyManagerSelfServiceKeyManager). Migrations are untouched — historical stripe_* columns/tables stay in place, unused.

Fixed

  • Contract WASM view resolves pre-capture contracts ("this contract's on-chain WASM isn't in the captured ledger window yet", operator report 2026-08-11): the instance→hash hop now reads the genesis-complete contract_instance_changes index first, so any contract whose instance was ever written resolves its current executable (or SAC verdict) regardless of the live-capture window; the code-bytes hop was already lake-complete (r1-measured: all 4,534 contract_code keys present). Legacy read remains the fallback.
  • Trade USD valuation: divergent-leg cross-check (fake-XMR incident 2026-08-11): an attacker planted an INDUSX/XLM bridge rate for the cost of the $0.01 dust floor and two no-XLM-leg trades were stamped ~$91M each off the poisoned quote-side rate (real value <$0.01 — a $182M fake spike in the SDEX volume series). The FX tier now values BOTH legs through the resolver when possible and stores the SMALLER when they disagree beyond 10× — inflating a print now requires pumping both legs' markets with real value. The two poisoned rows re-derive to honest values via the generation-guarded corrective path.

v0.31.0

2026-08-10GitHub ↗

Fixed

  • Detached-refresh gate is now class-fair (inventory #26 item 5, second half): the single global bound stopped the unauthenticated scan-amplification but let one key class starve the rest — a crawler churning fabricated contract ids could hold every slot while cold account/holders/directory pages fast-503d behind it. Each refresh class (account state, contract detail, asset holders, contracts directory) is now additionally capped at half the global limit; the global pool-safety bound is unchanged.
  • /v1/contracts directory census: 40s scans replaced by a day-keyed rollup (inventory #26 item 2 — the single heaviest explorer read, ~160 runs per 3h across the prewarm rungs). New stellar.contracts_census_daily (plain per-day per-contract counts; whole days recomputed and swapped via REPLACE PARTITION — no MV, so the Summing double-count class cannot arise) maintained by the new stellarindex-ops ch-census-rollup on a 30-min timer; the reader sums day rows (sub-second) with a coverage check that falls back to the exact scan while a backfill is incomplete. Window floors round to UTC-day resolution.
  • Aquarius `claim_protocol_fee` now records WHICH token was claimed (sources-decode audit 2026-08-04, finding 5): the token address lives in topic[1] — not the body — and migration 0129 shipped no token column on the documented premise that a recent trade could resolve it; the lake refutes that (one tx claims two different tokens with near-identical amounts), and per-pool SUM(amount) without the token adds integers of different token scales. FeeEvent gains Token (decode refuses a claim without it), migration 0139 adds the nullable column, and the 163 token-less rows already on r1 re-derive via projector-replay -source aquarius (queued).
  • DeFindex `harvest` events are now decoded (sources-decode audit 2026-08-04, finding 4): the recognise-and-drop premise ("body never observed on-chain") was disproved by the lake — 1,018 harvests with body {amount, from, price_per_share}, the exact shape decodeFlow reads by name. Harvests now emit direction='harvest' strategy-flow rows (migration 0138 widens the CHECK; user-position sums exclude them by construction — harvest is strategy yield, not a user flow). Historical recovery via projector-replay -source defindex (queued).
  • Phoenix pre-upgrade swaps no longer dropped (sources-decode audit 2026-08-04, finding 1 — HIGH): the pre-upgrade pool WASM (ledgers 51,019,036 → 53,134,167) emitted 7 field-events per swap — no "actual received amount" — but RawSwap.Complete() required that slot even though decodeSwap deliberately never reads it, so all 5,161 pre-upgrade swaps aged out as orphans (r1-confirmed: zero phoenix trades before ledger 53,134,242). Aged-out groups whose decode-consumed slots are present are now decoded at sweep time instead of orphaned; the current era's eager 8-field emit and orphan accounting are unchanged. Recovery of the historical rows needs projector-replay -source phoenix -from 51019036 (queued).
  • `/v1/contracts/{id}/code-history` cold reads (the last persistent 503 class in the route sweep): new keyed stellar.contract_instance_changes index — an MV-fed ReplacingMergeTree holding one narrow row per captured instance-entry write with the executable verdict pre-extracted via fixed-offset XDR substrings (byte-verified against go-stellar-sdk marshalling and against live r1 data). The reader walks the contract's primary key instead of a scan-shaped key_xdr predicate over the whole changes log; legacy scan remains the fallback where the index is absent. Historical fill via the new stellarindex-ops ch-instance-backfill (windowed, resumable, under run-heavy-job.sh).

Fixed

  • /ledgers table was permanently stuck on "Loading…" — the page wrapped LedgersTable (which takes no useSearchParams) in a vestigial <Suspense fallback={null}>, and the static exporter emitted that boundary as a never-completing pending template, so browsers never hydrated or client-rendered the subtree (zero network activity; the only such boundary on the site — audited all pages). Wrapper removed. The live-follow refetch on /ledgers + /operations is also throttled to one per 10s (operations' newest row structurally trails the ingest-tip stream, so unthrottled it refetched every close, ~12 req/min per viewer) and no longer fires while the initial page fetch is in flight.

v0.30.0

2026-08-08GitHub ↗

Fixed

  • `/v1/price/stream` no longer interleaves aggregation windows on one topic (cold audit 2026-08-03, r1-confirmed: three consecutive price_update events carried window_seconds 300/3600/86400 with materially different prices, so a client reading value_decimal saw the price flap three times per tick). The Hub topic key now includes the window (closed:<asset>/<quote>/<window_seconds>) and the stream accepts ?window_seconds= (default 300) to pick one series. Also fixed: subscribing with an alias spelling (?asset=native vs the aggregator's crypto:XLM) silently matched nothing forever — the handler now subscribes to every alias spelling of the pair.
  • `/v1/observations` (+ its stream) now scans every alias spelling of the pair — CEX observations live under crypto:XLM while SDEX legs live under native, so a ?asset=native query was silently blind to the CEX rows (and vice versa). Alias results merge keeping the newest trade per source; single-spelling pairs still do one scan.
  • `/v1/price/stream` events now carry the documented `/v1/price` envelope shape from BOTH producers (data + as_of; flags / sources only when evaluated). Previously the aggregator bridge emitted {asset, quote, window_seconds, value_decimal, observed_at} and streampublish emitted {snapshot, sources, stale} — two bespoke shapes on one endpoint, neither matching the OpenAPI example. No fabricated flags: an absent flags object means "not evaluated", never "fresh". as_of is the bucket end, keeping cross-region payloads byte-identical (ADR-0015).
  • `/v1/price/tip/stream` now shares one tip-compute loop per distinct (asset, quote, window) across all connections via the streaming Hub, instead of running a private 5-second query loop per connection (cold audit 2026-08-04: "tip stream = 6 DB queries/s PER CONNECTION — pool saturates at ~2300 streams"). Steady-state DB cost now scales with distinct pairs being watched, not with viewer count; producers linger 30 s after their last subscriber leaves to absorb reconnects, and Hub resume (Last-Event-ID) now works on the tip stream. Per-connection pre-flight verdicts (404 / withheld / 400) and the instant first frame are unchanged. Hub-less deployments keep the legacy per-connection producer. This is the scaling precondition for the explorer's live-ticking pages (RT-1).
  • The accounts hub's most-held chart no longer includes native XLM — every funded account holds XLM by definition, so charting it collapsed the issued-asset bars to slivers.
  • CI's web advisory gate: pnpm override floors raised for four fresh high GHSAs (nanoid, undici, brace-expansion, js-yaml — the prior js-yaml range >=4.2.0 <4.3.0 excluded the patched 4.3.1).

Added

  • Explorer live ticks (RT-2): a shared SSE multiplexer (web/explorer/src/lib/live/) — one refcounted EventSource per endpoint per tab, slow reopen on hard failure (WB-04) — now powers a live network heartbeat in the sidebar (latest closed ledger, pulsing, linking to its /ledgers page) and live tip-price streaming on asset pages: the headline price ticks in real time with an up/down flash and a "live tip price · streaming" caption, degrading to the existing 60s poll + baked value when the stream is unavailable. The home strip's XLM cell and the market-pair headline (previously a BUILD-frozen price captioned "as of <build time>") stream the same way, and /ledgers + /operations follow the network live: new rows animate in on every ledger close while page 1 is on screen (paging into history pauses following). The markets board follows ledger closes with a throttled refresh and per-cell price flashes — one SSE connection for the whole table. Animations respect prefers-reduced-motion.
  • Contract activity card (insight program unit 1): every contract page shows first-seen / last-seen ledgers, lifetime active-ledger count, and a 30-day activity sparkline — µs reads off the contract-keyed contract_active_ledgers index.

v0.29.0

2026-08-08GitHub ↗

Added

  • Accounts hub analytics (operator request 2026-08-08; API 1.19.0+ /v1/accounts/stats): network totals (funded accounts, trustlines, XLM held), balance statistics (avg/median/p90/p99 — stroops as strings, ADR-0003), top-100 concentration, a log10 wealth- distribution histogram, trustlines-per-account bands, and the most-held assets board — all computed by the same 30-minute ch-holders-rollup cycle (the analytics ride the scans the holders boards already pay for) and served from keyed tables, sub-second by construction. The /accounts page grows a stat strip, two distribution charts, and the most-held board above the wealth directory.

v0.28.1

2026-08-08GitHub ↗

Added

  • Sub-second holders boards (inventory #4): ch-holders-rollup + 30-min timer precompute every asset's top-500 holders + count into exchange-swapped rollup tables; /v1/assets/{id}/holders serves keyed reads with a probed fallback to the legacy scans. Part of the sub-second page goal (2026-08-08); scripts/ops/subsecond-audit.py is the goal's acceptance harness.

Fixed

  • Saturation rejects no longer claim to be timeouts (inventory #5).
  • /v1/readyz single-flight + 1s cache — unauthenticated spam can no longer exhaust the DB pool through the checkers (inventory #26).
  • Migration 0137 disarms the comet replay double-count (inventory #19); REQUIRES projector-replay -source comet -from 51499000 after deploy.

v0.28.0

2026-08-08GitHub ↗

Added

  • The movements feed covers every asset again — inventory #1. stellarindex-ops ch-cap67-movements derives post-P23 account movements for ALL assets (native XLM included — its SAC is deliberately excluded from the Postgres watched-token projection) straight from the lake's CAP-67 transfer events into the ClickHouse movement archive (provenance cap67_derived), windowed + resumable via a watermark, kept following the tip by a 5-minute timer. The /v1/accounts/{g}/movements merge boundary is now DYNAMIC: the ClickHouse arm serves everything at/below the watermark, the Postgres tail only above it — gap-free and double-count-free at any derive progress — and the coverage note states the live scope ("complete for all assets through ledger N") instead of the static P23 disclosure. Closes the "history stops 340 days ago" class for classic-payment accounts.

Fixed

  • main CI is green again (the ci-health email flood): explorer CI Node 20→22 (vitest/undici), web/status audit advisories fixed via overrides, a mid-test miniredis close race made deterministic, the cross-check convergence test reworked to the demoted-leg contract, two fixture bugs (63-char tx hashes; a hardcoded day that aged out of a now-relative read window), and a fake-timer test that native AbortSignal.timeout ignores on Node 22 switched to short real timers.

v0.27.1

2026-08-08GitHub ↗

Fixed

  • Account pages stop dying on compressed history (the GATL report). Three per-account Postgres reads walked chunks that have no per-account index once compressed: /accounts/{g}/trades burned 8s proving even a zero-trade account empty (16.4M buffers), the movements post-P23 tail used the from OR to shape and statement-timed-out for busy accounts (so /movements silently showed only the pre-P23 archive — "history stops 340d ago"), and the activity trades count decompress-scanned all 248 compressed chunks. The movements tail is now the two-arm UNION shape; trades list + count are bounded to the compression horizon and SAY SO (note/trades_total_since) instead of serving a silently-partial "all time" answer.
  • The movements feed discloses its post-P23 scope. After the P23 boundary the feed carries watched Soroban/SAC tokens only — classic XLM payment history after 2025-09-03 is not served there yet (the lake captures it; the all-asset archive is inventory item #1). The coverage note now states this on every response rather than letting a busy XLM account's feed masquerade as complete.

Added

  • docs/operations/open-fixes-inventory-2026-08-08.md — the complete deduplicated register of sidelined findings (35 items, three tiers), compiled on operator request; items leave it only when deployed + verified.

v0.27.0

2026-08-08GitHub ↗

Added

  • `stellar.contract_active_ledgers` — the quiet-contract index. Cold /v1/contracts/{id} for a low-activity contract scanned the whole events key range backwards (avg 9.3s / 114M rows — the inverse of the v0.26.1 busy-contract pathology; three user-reported 503s). New per-(contract, ledger) activity index (deploy/clickhouse/contract_active_ledgers.sql, ~20-40 GiB vs ~800 GiB for a per-event index): the reader walks the contract's recent active ledgers by primary key (µs) and bounds the events read to them — lossless for the page and its cursor, and an empty walk is an authoritative "no events". Deliberately count-free so re-derives/replays can't double-count (the migration-0059 class); kept current by a materialized view, with stellarindex-ops ch-contract-ledgers-backfill (windowed, resumable) covering history. Reader falls back to the unbounded walk when the index is absent or empty (requireRows probe — an MV-drop must not read as "no contract has events").

Fixed

  • Capacity errors never render as 500. /accounts/{g}/activity served "detached refresh capacity saturated" as a 500 Internal error, and /operations did the same with driver-level ClickHouse i/o timeouts. All 14 explorer handler sites now classify through one retryableColdMiss (extended to net.Error timeouts) → the truthful retryable 503.

v0.26.1

2026-08-06GitHub ↗

Fixed

  • Busy-contract pages no longer 503. /v1/contracts/{id} for a high-activity contract (the Soroswap router has 17.9M events) blew the 8s explorer read budget on every cold read: the query's LIMIT 1 BY dedup clause disabled ClickHouse's reverse read-in-order early exit, turning "most recent 100 events" into a sort over every event the contract ever emitted — measured 16.3s vs 0.16s (100×) with the clause removed. The W4-storage-1 duplicate-part dedup is preserved: duplicates are adjacent under the full ORDER BY tuple, so the reader collapses them in Go over a 100-row over-fetch, falling back to the in-ClickHouse dedup shape in the (never-observed) case a duplicate storm exhausts the headroom.

v0.26.0

2026-08-06GitHub ↗

Added

  • Curated address labels from the StellarExpert public directory (API 1.19.0). The MIT-licensed stellar-expert/public-directory set (~18.5k well-known G-account and C-contract addresses: names, domains, tags like exchange/sdf/custodian/malicious) is mirrored into a new account_directory table (migration 0136) by stellarindex-ops directory-sync — one tarball fetch, transactional upsert + prune, empty-parse refusal so a broken fetch can never wipe the set. /v1/accounts/{g} and /v1/contracts/{id} gain a directory object, and GET /v1/directory?addresses=… batch-resolves up to 100 addresses for list decoration. Display attribution only: listing is not endorsement, nothing feeds verification or the in-repo scam list, and malicious/unsafe render as warnings.

Fixed

  • Issuer home-domain precedence: the account's own on-chain `home_domain` field now outranks the hand-curated known-issuers map on /v1/assets/{id}, /v1/assets/{id}/metadata, and /v1/issuers/{g}. Founding case: the ex-apay ETH issuer (GBFXOHVAS…SOCC) rendered home_domain=apay.io with sep1_status=verified — a dead anchor's identity, verified against the wrong domain — while the issuer's live on-chain field has said ultracapital.xyz since Ultra Stellar acquired apay.io's wrapped assets. A 2026-08-06 sweep found 8 of 27 curated entries diverged from chain; the flagrantly wrong ones (both apay.io accounts → ultracapital.xyz, Circle's centre.iocircle.com for USDC and EURC, a USDx issuer mis-attributed to Mykobo → FxDAO) are also corrected in the map itself, which remains only a last-resort fallback for issuers with no on-chain domain.
  • S-010 identity suppression can no longer be undone by the account-state enrich on /v1/issuers/{g}: suppression for a flagged, unverified issuer now runs after all enrichment, so the scammer's self-declared on-chain home_domain (the impersonation itself, e.g. lobstr.co) is never refilled into the cleared field. Auth flags still populate — they are objective account state, not identity claims.
  • The explorer no longer renders scientific notation for prices$3.353e-4 now renders $0.0003353 (plain decimal, significant-digits tail, trailing zeros trimmed) via one shared formatSubunitPrice, replacing the two shared formatters' exponent branches and five inline copies.
  • `anomaly_freeze_sustained` pages only on ESCALATION (both rule trees): the old shape paged whenever any pair freeze-cycled for an hour, including fire→extend→auto-release cycles resolving exactly as ADR-0019 designs. Engaged/active stay ticket/informational.
  • `metrics_registry_absent` excludes `component="ledgerstream"` — the documented exception (archive-only deployments) that kept the alert permanently red on r1.

Changed

  • The partial-wrap supply cross-check's over-mint leg is diagnostic-only. Its premise — cumulative SAC net mint ≤ current classic outstanding — only holds for one-way wraps, and two live assets falsified it: BLND retires supply classically after SAC minting (no SAC burn fires on a classic payment to the issuer), and PHO minted its entire 200M supply through the SAC once while classic outstanding is issuer-excluded. Both paged for a week with every unit accounted for. The over-mint excess stays computed and reported (OverMintStroops); the paging divergence is now leg 2 alone — escrow ≤ net mint, the bound that is impossible to breach under correct accounting for a partial wrap.

v0.25.2

2026-08-05GitHub ↗

Fixed

  • A freeze can no longer escalate to operator-only without a single scored evaluation. During the ~30-minute post-restart confidence bootstrap every bucket is unscored, so a freeze rehydrated across a deploy could never accumulate an unfreeze streak while its hold expiries still burned extensions — it marched deterministically to ESCALATED (live occurrence: crypto:XLM/fiat:GBP 24h escalated entirely inside the v0.25.0 restart window). An unscored expiry now slides the hold without consuming an extension (TransitionHeldUnscored); ADR-0019's 2-hour escalation budget thereby counts two hours of SCORED asking. Escalated freezes still never auto-unfreeze.
  • The permanently-firing `dex_nonstandard_decimals_detected` alert was removed — it compared an all-time counter to zero, so once any nonstandard token traded it fired forever (three already-confirmed, auto-normalized aquarius tokens held it active). The awareness signal lives on the dashboard metric; the actionable failure mode keeps its own correction_failing ticket alert.

Changed

  • Classic-asset slugs are now the fully-qualified asset_id (migration 0135, superseding 0134's abbreviation one day later). An 8-character issuer prefix is a ~2^32 vanity-grind — minutes on consumer GPUs, the same economics dust-attack address mimicry already exploits — and the abbreviation bought nothing but URL length. The full form is self-certifying (the URL IS the identity, nothing to grind or front-run) and deletes the writer's whole collision apparatus, since the slug equals the primary key's value. Slug resolution on /v1/assets/{id} is now effectively a case-insensitive canonical-id match. Verified-currency slugs (usdc, aqua) are unaffected — that's the catalogue namespace.
  • Explorer HTML now ships `Cache-Control: public, max-age=0, must-revalidate` — a zone-level cache rule was edge-caching page HTML for 7 days, so users saw day-old baked prices and pre-redesign chart code under three successive deploys. Fingerprinted assets keep their immutable year. (The zone rule itself is dashboard-managed and needs the operator: respect-origin + one purge.)
  • **The /assets/* runtime shell carries generic metadata** ("Asset · Stellar Index") instead of baking the literal "shell" title, and the client view restamps document.title once the real asset loads.

v0.25.1

2026-08-05GitHub ↗

Added

  • `/v1/assets/{id}` resolves classic-asset slugsusdt-gcqtgzqq now serves the same detail as USDT-G…. Completes migration 0134: the explorer links every classic asset by its public slug, and before this those URLs were only resolvable through the explorer's own build-time cache, so any page not baked at build time 404'd (live report: /assets/usdt-gasu4kif). Optional reader capability, forwarded through the cache wrapper explicitly.
  • Asset-page headline prices are now LIVE (LiveAssetPrice): the static build still bakes an initial value, but the browser re-fetches /v1/price on mount and every 60s — a 12-day-old deploy was showing XLM at $0.186 against a live $0.17 chart. Un-refreshed values are captioned "as baked at deploy"; a price-withheld verdict replaces any baked number rather than leaving a lower-trust snapshot on screen.

Changed

  • Every asset price chart is anchored to USDC — never XLM (operator decision). The XLM quote toggle is gone: an XLM-quoted chart re-denominates the asset in a floating unit, and XLM-quoted thin markets were the manipulated visuals of the 2026-08-04 incident. USDC itself charts against fiat:USD so a depeg is visible; native XLM and fiat assets chart against the CEX-fed USD series.
  • Chart price precision is now adaptive — the axis scales its decimals to the series magnitude (XLM renders ~6 decimals instead of a flat "$0.17"; sub-cent assets no longer flatline at "$0.00").

Showing the 40 most recent of 209 releases. Full changelog →