Releases: Stellar-Index/StellarIndex
Releases · Stellar-Index/StellarIndex
Release list
Stellar Index v0.24.0
[v0.24.0] — 2026-08-02
Fixed
- Router serving is anchored to the highest-confidence route (audit H1) —
a confidence-blind outlier omission ran before confidence-weighted serving,
so once a target resolved to ≥3 routes a divergent low-confidence majority
could evict the trusted route as a price-outlier and set the served composite.
CombineRoutesnow serves the highest-confidence route(s) among all gated
routes, using a member-median (never an averaged midpoint), so a lower-
confidence route corroborates and can flag divergence but can never move the
served price. Outlier omission still computes thediverged/corroboration
signals. Single-route serving (the whole shipped config) is byte-identical. - Corroboration requires a per-route confidence floor (audit M2) — a route
counts toward the anomaly-freeze corroboration only if its weakest-link
confidence clears 0.5, so a thin route can't fake independence when
min_route_confidenceships at 0. - Router staleness is immune to wall-clock steps (audit M1) — the composite
corroboration timestamp retains its monotonic reading (time.Now(), not
.UTC()), so a backward NTP/VM clock step can't latch a stale freeze-
suppressing corroboration count. - Undirected market dedup (audit M3) — a market quoted in both orientations
(XLM/USD+USD/XLM) no longer produces duplicate edges / double-counted
routes. - Frozen-leg reroute is gated + flagged like a dry-leg reroute (audit H2) —
a reroute around a frozen chain leg is now subject torerouteMinConfidence
and markedRerouted; a target frozen on its own direct market withholds
(frozen_leg) rather than overwriting its frozen last-known-good. - Dust guard no longer suppresses on a placeholder
"0"volume (audit M5) —
a Soroban SEP-41 volume-reader artifact ("0") is treated as unmeasured
(likenil); only a positive sub-floor volume suppresses a market cap. - Catalogue rows surface
market_cap_low_liquidity(audit M4) — the flag is
now propagated throughmergeTwinStats, so a dust-suppressed verified-currency
twin no longer shows a null cap without the explaining flag. - Native XLM is never dust-suppressed on the detail path (audit L2) —
mirrors the listing path's existing carve-out. - Negative market cap guard —
usdMarketValueomits a negative market cap
(a negative-supply data error) rather than serving it, matching the listing
path'scomputeMarketCapUSD. - Hardening: non-finite route confidence is sanitised and
medianRatguards
empty input (audit L5); cached non-FX chain legs no longer enter the graph at
max confidence (audit L1); the single-Tick invariant on the orchestrator's
per-tick maps is documented (audit L4).
Added
/v1/pricerouter quality flags (audit L3) —flags.diverged(the
composite came from disagreeing routes) andflags.rerouted(priced via a
substitute path around a dry configured leg) now surface the router quality
signal the aggregator already computed and persisted but that had no reader.
Stellar Index v0.23.0
[v0.23.0] — 2026-08-02
Added
- Multi-path rate router with anti-manipulation corroboration — thin FX
crosses (e.g. XLM/GBP served 100% from a single venue) can now be priced
and cross-checked through multiple independent routes over the XLM/BTC/USD
hub topology (internal/aggregate/router.go), modelled on the predecessor
Rates Engine. Shortest routes preferred; each route's price is an exact
big.Ratcomposite and its confidence the weakest link. The freeze's
source_countis widened only by routes that are INDEPENDENT
(edge-disjoint), TIGHTLY agreeing (within 3%, far tighter than the 40%
outlier band) and non-diverged — a manipulator cannot manufacture false
corroboration by wash-trading one shared leg (fail-closed: a divergent or
shared-bottleneck set drops below the single-path baseline). The SERVED
price is the median of only the highest-confidence surviving route(s), so a
lower-confidence route (e.g. through a thin, USD-volume-floor-exempt bridge)
corroborates and can flag divergence but can never move the served value.
New configaggregate.max_hops(default 3) +aggregate.min_route_confidence
(default 0.0). Single-route targets — the entire shipped config today — are
byte-identical to the pre-router behaviour; the machinery activates only when
a target has a second independent route configured. - Valuation-integrity guard on market cap / FDV — a market cap is no
longer presented as authoritative when its backing price came from
negligible liquidity (an obscure asset with one $10 SDEX trade must not
read as worth billions).market_cap_usdandfdv_usdare now SUPPRESSED
(served null) with a newmarket_cap_low_liquidity: trueflag on
AssetandGlobalAssetViewwhen the backing price came from a single
venue AND the asset's trailing-24h USD volume is below the new
aggregate.min_market_cap_volume_usdfloor (default 1000 USD). The AND is
load-bearing: a single-venue asset with real volume, or any multi-source
asset, keeps its cap; theprice_usditself is untouched — the guard is on
the valuation, not the price. Applies to both the/v1/assets/{id}detail
path (populateMarketCap) and the/v1/assetslisting path
(computeMarketCapUSD); the fiat market cap (M2 × deep FX rate) is
deliberately not gated. New config knobaggregate.min_market_cap_volume_usd
(0 disables).
Stellar Index v0.22.0
[v0.22.0] — 2026-08-01
Added
- Cycle-level regression test for the v0.21.12 sink-side adaptive
shrink (internal/projector/cycle_wedge_test.go): drives
cycleOneSourceunder an already-expired cycle budget with a sink
fast-failing oncontext.DeadlineExceededand pins that the window
pointer halves per cycle, floors atMinBatchLimit, the cursor holds
(no advance-past-loss, no quarantine), and a healthy cycle then
commits + doubles the window back. The fix previously had only the
shrinkWindowunit test. tx_hash_index_parityconfig assertion
(scripts/ops/config-assertions.sh): hourly sampled parity probe —
500 random tx hashes from the trailing 10k ledgers must all resolve
instellar.tx_hash_indexvia primary-key point lookups. The
explorer treats an index miss as an authoritative 404 and the index
was parity-verified genesis→tip only once (2026-07-30); this catches
future divergence (MV drop/recreate windows, ATTACH-style loads that
bypass the MV). Runbook row added to
docs/operations/runbooks/config-assertion-failed.md.
Fixed
- Served TWAP now folds the two stored market directions by minute
COVERAGE, not trade count (audit finding M-B;
internal/storage/timescale/aggregates.go, migration
0126_twap_sample_count). SDEX stores each market in both
orientations, andTWAPPointsInRangemerged them with a
trade-count-weighted mean of{twap, 1.0/twap_flipped}. But the
twap_1h/twap_1dCAGGs (migration 0081) define twap as
avg(prices_1m.twap)— equal per elapsed MINUTE, deliberately NOT per
trade — so the only correct merge weight is each direction's minute
coverage (how manyprices_1mminute-buckets it contributed), which
the CAGGs did not store; count-weighting was exact only when trade
count happened to track coverage and wrong by an unbounded factor
otherwise. Migration 0126 addssample_count(count(*)) to both
hierarchical TWAP CAGGs, and the read now folds in Go with the new
combineDirTWAP:Σ(oriented_twap·sample_count)/Σ(sample_count)in
exactbig.Rat, flipped rows inverted as an exact1/twap(no
1.0/twapSQL rounding — ADR-0003). Pinned by
internal/storage/timescale/twap_direction_combine_test.go
(cannedConn) andTestTWAPSampleCount_CoverageWeighted(real
TimescaleDB). ⚠ DEPLOY: migration isWITH NO DATA— after applying
it the operator must re-materialize the two views:
CALL refresh_continuous_aggregate('twap_1h', NULL, now());then
('twap_1d', NULL, now());(run afterprices_1mis whole). - Empty
tx_hash_indexno longer grants authoritative 404s
(internal/storage/clickhouse/explorer_reader.go): the tx-hash
availability probe now requires the index to be NON-EMPTY, not merely
to exist. An existing-but-empty index (the MV-drop / TRUNCATE
pathology: transactions keep flowing, the index silently stops) is
treated as index-unavailable — hash lookups take the bloom-scan path
and the verdict is not cached, so a repopulated index is picked back
up on re-probe ("empty table is not a definitive answer", the
DailyActivityAvailableconvention). A per-hash miss against a
non-empty index remains an authoritative not-found (the DoS
protection), pinned by new unit tests
(TestTransactionByHashEmptyIndexFallsBackToScan,
TestTransactionByHashRepopulatedIndexRegainsAuthority). Complements
the hourlytx_hash_index_parityassertion, which catches PARTIAL
divergence. Un-redsTestClickHouseTxHashIndexProbeFallbackin CI. - Explorer scan-settings integration test seeds genuine LedgerKey
XDR (test/integration/explorer_scan_settings_test.go): the
trustline seed carried a synthetic"e5-trustline-key"placeholder
that could never match the trustline/offer readers' real-XDR
PK-prefixLIKEranges introduced in 85f706e
(accountEntryKeyPrefix), so the test failed while production (real
keys) worked. The seed now marshals real trustline AND offer
xdr.LedgerKeys + entries for the seeded account, exercising both
prefix-range paths end-to-end (balance, limit, offer round-trip
asserted).
now mirrors the registry/catalogue — the stale 62,403,000 floor left
each source's true head range invisible to the supporting gap signal
(the authoritative ADR-0033 verdict was already corrected on 07-31).
Security
- Unauthenticated metric-cardinality DoS closed (audit W4-obs-1;
internal/obs/http_middleware.go): unrecognized HTTP methods passed
through to the Prometheusmethodlabel verbatim;net/httpaccepts any
token, socurl -X <random>in a loop minted unbounded label children.
Unknown verbs now collapse to a bounded"other". - SSE price-bridge payloads validated + re-marshalled (audit F2;
internal/api/streaming/redispub): the subscriber fanned the raw Redis
payload to all stream clients with only an asset/quote-non-empty check;
with no-AUTH Redis a host-adjacentPUBLISHcould inject a forged price.
Now the decoded event is bounds-validated (exactbig.RatVWAP, sane
window, fresh timestamp) and the validated struct re-marshalled, dropping
injected fields. - SAC/asset label cross-checked on
/movements(audit W2-explorer-1):
a hostile token could set asep0011label impersonating a trusted asset
(e.g. Circle USDC) on the public feed. The fallback now derives the SAC
address and trusts the label only on a contract-id match (the check its
siblingwasm_viewalready had); also fixes/v1/accounts/{g}/positions
and the?asset=filter. - WASM export-parser prealloc capped (audit W6-go-1): a section's
attacker-influenced LEB128 count sizedmake()before any read, risking a
multi-GB allocation on adversarial WASM; the prealloc hint is now bounded
by the reader's remaining bytes (defensive — reachable only if an
un-validated WASM path is ever added).
Changed
- Cache-gate saturation now returns HTTP 503, not 500 (audit recon R3):
a saturated detached-refresh gate on a cold/v1/accounts/{g}read was
indistinguishable from a real error; a distinctErrRefreshSaturated
sentinel now maps to the retryable 503 (already declared on the route),
while genuine scan failures stay 500. - Anomaly-freeze now releases reliably (audit W3-freeze-1 + W3-freeze-3):
(1) the shared freeze marker'sClearis deferred until the last frozen
window for a pair releases, so a short window auto-releasing no longer
unfreezes still-frozen sibling windows; (2) once a freeze is active Phase 1
stands down so the Phase 2 lifecycle (the sole auto-unfreeze authority) can
release it — a freeze whose price settled at a residual level past
FreezePctbut statistically healthy could previously stay frozen forever. - Divergence warning debounced (audit W3-guards-2): comparing our
shortest-window VWAP against an instantaneous reference spot produced false
warnings on fast moves; the warning now fires only after the over-threshold
condition persists a full VWAP-window horizon (default 5m), so a transient
VWAP-vs-spot gap self-clears while a sustained genuine divergence still
fires.
Fixed
- decimalsguard retries the durable write (audit W3-guards-1): it latched
firedbefore the DB upsert and never retried, so a transient write failure
left a non-7-decimal token serving a 10^(7-decimals)-skewed price until
restart. Split into an in-memory alarm latch (fire-once) and apersisted
latch set only on a successful upsert (retries otherwise). /v1/pricefirst-ever bucket served as low-confidence (audit W6-fresh-1):
the served-VWAP guard failed open (unbounded) on an empty baseline, so a
thin pair's first minute — a lone manipulated print — was served
stale=false. It is now flagged low-confidence/stale (value still served,
never a blackout).- SAC-balance watched-asset keys canonicalized (audit W1-supply-1): the
SAC-balance observer copied the asset_key verbatim, skipping the
CanonicalizeWatchedClassicits sibling observers apply, so a dash-form SAC
wrapper never matched the colon-form supply and its balances were
under-reported (same class as the 2026-07-02 trustline miss). (Historical
dash-form rows need a re-derive.) - One-side-zero SDEX fills no longer sink the trades batch (audit W1-defi-1):
a fill with one leg rounded to 0 (decoder-kept, census-counted) violated the
base>0 AND quote>0CHECK, rolling back the whole all-or-nothing batch and
firing a spuriousinsert_errorsalert. They are now filtered before the
batch (consistent with the completeness reconcile, which already excludes
them); genuine Validate failures stay loud. - Duplicate contract events no longer served during the RMT merge window
(audit W4-storage-1):ContractEventsRecent(the/v1/contracts/{id}feed)
andEventsByTxreadcontract_events(a ReplacingMergeTree) without
dedup, returning re-ingested events twice until a background merge. Added
the package's per-shape idiom:LIMIT 1 BY <sort-tuple>andFINALon the
bounded read respectively. - Aggregator background workers panic-isolated (audit W4-cmd-1): the
aggregator had zero panic recovery while the API isolates every worker, so
one worker panic crash-looped the price pipeline. Added a shared
internal/worker.Recoverand wrapped all 14 workers + 3 inner fan-out
goroutines. - Three dead alerts made fireable (audit W5-mon, W5-mon-3):
supply_refresh_stalled+config_assertions_staleusedtimestamp()
(scrape freshness, always ~0) and never fired — rewritten to real age /
changes(); the ledgerstreamboth_missingpage's counter was
nil-in-prod (registry-gated) and is now registered unconditionally in
obs. Each gained a promtool positive control. - Unbounded asset-detail response cache bounded (audit W6-perf-1): capped
at 4096 with opportunistic expired-purge / oldest-eviction (a crawler...
Stellar Index v0.21.12
[v0.21.12] — 2026-08-01
Fixed
- Projector: the adaptive window now also shrinks when the cycle budget
dies in SINK writes, not only in the CH scan. The 2026-07-10 shrink
fired only on stream-level timeouts; a window dense enough that the scan
finishes but the per-event writes exhaustPerSourceTimeoutretried the
identical range forever — the aquarius reserves replay wedged 3.5 h at
ledger 63,488,687 this way (every remaining write fast-failing on the
dead cycle context, cursor held, no shrink). Budget-exhausted cycles
with held transient rows now halve the window to the same
MinBatchLimitfloor so the retry converges.
Stellar Index v0.21.11
[v0.21.11] — 2026-08-01
Fixed
- Aquarius zero-amount trades are recognized no-ops, not decode errors
(internal/sources/aquarius/decode.go+dispatcher_adapter.go). The
lake disproves the "every trade event has positive amounts" assumption:
genuine dust swaps whose output rounds to zero — first exemplar ledger
53,626,410, body(sold=2, bought=0, fee=0)from a registered pool —
still emit thetradeevent, and refusing them as malformed made the
ADR-0033 projection re-derive blind (40 of the 41
undecodable-but-matched events on aquarius's first full-range
completeness reconcile, 2026-08-01; all 40 arebought=0dust swaps,
ledgers 53.63M → 57.32M).canonical.Trade.Validateforbids
non-positive amounts, so these can never be served trade rows: decode
now succeeds with zero rows (newErrZeroAmountTradesentinel consumed
by the adapter), the reconcile sees expected == served == 0, and
negative amounts remainErrMalformedPayload. Real-lake-bytes golden
tests pin both the no-op and the negative refusal. Same classification
pattern as redstone's emptywrite_pricesbatch. - Aquarius
set_privileged_addrsdecodes BOTH wire generations
(internal/sources/aquarius/decode_admin.go). Contract-schema
evolution: every lake event ≤ ledger 57,604,772 carries the 4-element
bodyVec[Address×3, Vec[Address]]; every event from 57,697,794
(2025-06-25 WASM) onward carries 5 elements — the same four plus ONE
trailing role Address. The decoder pinned arity==4 (the 2026-07-10
audit sampled a v1 exemplar), leaving the canonical router's single
v2 event (ledger 57,711,797) as the 41st blind event. The v2 arm
decodes the trailing address intoAttributes["addr_3"]; any other
arity still fails closed. Real-lake-bytes golden test pins the v2
shape. Post-deploy for both fixes: re-run
compute-completeness -source aquarius(no replay needed — neither
class changes served rows; expect blind 41 → 0).
Stellar Index v0.21.10
[v0.21.10] — 2026-08-01
Fixed
- v0.21.9's deploy was blocked by the migration rollback-safety gate
on 0124's CHECK rebuild (rule 9). The change is a pure enum widening
— every value an old binary writes remains valid — now annotated
migration-compat:okinline. No code changes vs v0.21.9.
Stellar Index v0.21.9
[v0.21.9] — 2026-08-01
Fixed
- Carried-claim invalidation gap: a projector-replay below the watermark
now forces re-reconciliation (migration 0125 +
internal/ops/ingest/projector.go+internal/ops/chops/compute_completeness.go).
The daily completeness driver reconciles each source only from its prior
watermark to tip and CARRIES the prior clean projection claim for the older
prefix (projectionClaim rule 3 / INV-5) — sound only while the served tier
below the watermark is immutable. Aprojector-replayrewind rewrites
exactly that region, and nothing forced a re-check: the carried claim kept
certifying rows the replay had changed, which is how the 07-30 cctp
replay's 19,366 over-projected event_index-0 twin rows (62.27M–63.55M)
escaped the verifier.projector-replaynow records a per-source dirty
window (projection_dirty_windows, widened LEAST/GREATEST on overlap)
BEFORE rewinding the cursor — refusing to rewind if the record fails —
projected-rebuild -writerecords the same window before its first write
(same invalidation, bulk path), and
compute-completenessfails closed on reading the table, extends its
projection reconcile floor over any pending window regardless of-from,
and clears the window only after a CLEAN projection verdict whose scope
covered it (bounded delete, race-safe against a concurrent widening replay). - cctp + rozo reconciliation-catalogue genesis floors corrected to the
lake-derived exact first events (internal/ops/chops/reconciliation_catalogue.go),
mirroring the 07-30protocols_registry.gofix: cctp 62_403_000 →
62_146_641 (410 real served rows sat permanently BELOW the verify floor,
structurally out of every verdict), rozo 62_403_000 → 60_829_397. All other
catalogue entries audited against the registry — no further drift.
(DefaultGapDetectorTargetsinper_source_gaps.gostill carries the old
floors — supporting signal only, owned by another change.) - cctp bespoke reader post-twin-deletion cleanup
(internal/storage/timescale/protocol_bespoke_cctp.go). The flow CTEs
collapsed per (tx_hash, op_index) withmax(amount)— a workaround for the
legacy event_index-0 twin rows deleted from r1 on 2026-07-31 — which would
silently HALVE a future genuine batched double-transfer in one op (the 0112
class: same-op same-type groups are proven on the wire by admin events).
Value-carrying CTEs (deposit_for_burn / mint_and_withdraw) now read raw
per-event rows; the message_received CTE keeps its one-body-per-op group
(join semantics — a second body row would fan out every joined mint row),
and the mint_and_forward restatement exclusion (a real semantic rule, not
twin dedup) is untouched. Also: the all-time "Unique recipients" KPI now
countsDISTINCTrecipient addresses as labelled — it previously counted
recipient-bearing ops. - Explorer honesty wave — cold-audit findings across
web/explorer/src.
Eight fabrication/robustness classes fixed in the explorer only (no
API/server changes): (1)/lendingpools table: an API failure now
renders an explicit unavailable state instead of the fabricated
"No Blend pools have emitted auction events yet" empty-state, and
PoolRealStats(hoisted to module scope — it was re-created every
parent render) computes TVL/utilization/weighted APRs over PRICED
reserves only with the basis labelled, instead of coercing absent
supplied_usd/borrowed_usd/APRs to 0 inside sums and weighted
averages; (2) partial trailing daily bucket: a new shared
@/lib/serieshelper (dropPartialTrailingDay) drops today's
accumulating UTC bucket from every daily-grain chart mapper
(BespokeSection standalone + grouped panels, BridgeShowcase lines,
ProtocolView activity, SourceVolumeHistory, SdexVolumeSection;
hourly 24h series keep the live day) andTimeSeriesChartheadlines
"Latest" from the last COMPLETE point; (3)/network
protocol-upgrade markers now require BOTH sides of a step to be a
real (>0) version — lake sentinel rows minted phantom "protocol v0/vN"
markers (the field is a uint32 without omitempty, so null-guards never
fired); (4)/anomaliesfreeze durations render "—" instead of
"NaNd" for missing/garbage timestamps; (5) holder balances render via
a BigInt-divide-firstformatBaseUnits/scaleBaseUnits
(@/lib/format, ADR-0003) instead ofNumber()on >2^53
smallest-unit strings (absent → "—", not NaN); SupplyFlowsBar shares
the same scaler; (6)toChartNumberreturns null — the point is
dropped — for non-numeric series values (previously plotted as
fabricated 0s) and for compact-suffixed figures ("1.2M" previously
mis-scaled 10^6× to 1.2); (7) non-parsable point dates are dropped
beforesetDatainstead of the epoch-0 fallback that plotted 1970
points and could crash lightweight-charts on duplicate time:0;
(8) small honesty items: SdexVolumeSection renders a served 1-point
series as the point ("insufficient history"), not a false
"unavailable"; the anomalies ReasonHeatmap derives its day columns
from the served data's date range, not the client clock; a bespoke
block's notes no longer caption other windows' data after a failed
window refetch. - Daily-grain protocol/bespoke series exclude the current (partial)
UTC day server-side. Every bespoke daily series (DEX activity/
traders/top-pairs, lending sides/backstop/auctions/per-pool, credit,
oracle, defindex, CCTP flows + cumulative net inflow, rozo) and the
protocol activity series/breakdown included today's still-accumulating
bucket with no marker — the UXP-16 "phantom cliff" class, previously
fixed only on /network. Daily-grain SQL now bounds at the day start
(sharedcompleteDaysOnlyfragment; ClickHouse readers bound
close_time/daylikewise — the event breakdown shares the bound so
sum(event_breakdown) == events_totalstill reconciles). The 24h
window's hourly grain keeps its live edge. - Three small honesty guards (audit 2026-07-31). (a) The SDEX
order-book reader no longer silently skips a non-removed offer change
whose XDR fails to decode — each skip freezes that key's prior served
state, so it now logs a warn line and increments the new
stellarindex_sdex_orderbook_undecodable_offers_totalcounter.
(b)mapFreezeReasonno longer defaults unrecognized automated
freeze decisions tomanual(the operator-initiated reason) — they
record as the newotherreason (migration 0124 extends the CHECK;
spec 1.18.0 adds the enum value).
(c) A soroswap swap with all FOUR amounts non-zero satisfied both
direction arms and silently decoded as 0→1, dropping the 1→0 leg —
now refused whole asErrAmbiguousSwapDirection(wraps
ErrNonDirectionalSwap, so it stays a recognized no-op for ADR-0033
completeness). - Blend bespoke block no longer sums token amounts across assets.
The headline "Net supplied/borrowed" KPIs and the per-pool table's
cross-asset sums + "Util %" ratio addedtoken_amountacross tokens
with different decimals — a meaningless number under an authoritative
label (the sibling count-first suite inbespoke_lending.godocuments
exactly why). KPIs are now count-first (active users, flash loans,
alongside the existing side-event/pool counts), the per-pool table
serves supply/borrow-side event + user counts, and amount sums survive
only where scoped to a single asset (the per-asset table). - Stale "soroswap has no taker" claims removed from the wire. The
soroswap decoder captures the SwapEventtorecipient since
2026-07-30 (100% taker coverage on new rows), but the
/v1/accounts/{g}/tradesscope note still excluded soroswap and the
DEX bespoke block carried a hard-coded "0% taker coverage" note. The
scope note now includes soroswap (with the pre-2026-07-30 caveat) and
the bespoke omission note is data-driven — emitted only when the
served window observably has zero taker-stamped trades. usd_volumeno longer hard-codes the off-chain 1e8 amount scale.
The FX pollers stamp trades at 1e6 (registryAmountDecimals: 6),
but both the insert-path stamper (tradeUSDVolume/tradeUSDVolumeViaFX)
andClassifyUSDVolumeTier(theverify-usd-volumechecker, which
would have certified the error) divided by a hard-coded 1e8 — a latent
100× under-valuation the moment FX trades re-enable. Both now consult
the source's registeredAmountScaleDecimals(CS-040).- Explorer detached SWR refreshes are now globally bounded. The
stale-while-revalidate caches (account state, asset holders, contracts
directory, contract detail) single-flighted per key but were unbounded
ACROSS keys — and the key space is attacker-chosen on unauthenticated
routes, so churning fabricated G-/C-addresses queued one detached
multi-minute lake scan per key onto the 8-connection serving pool. A
shared semaphore (clickhouse.RefreshGate, limit 4 — half the pool)
now bounds all detached refreshes; on saturation the refresh is
skipped, never queued: stale entries keep serving, cold keys 503 with
the retryable timeout contract. /v1/assets/{asset_id}/holdersnormalizes the asset spelling before
the cache and the lake query. ParseAsset admits the Horizon
CODE:ISSUERspelling, but the raw request string was used as both
the SWR cache key and the query asset — the colon spelling scanned
for a value the lake never stores and cached an authoritative-looking
empty board under a duplicate key. Both spellings now share one
canonical (CODE-ISSUER) cache entry and query.- Protocol-detail cache: a degraded fast-failing rebuild can no longer
displace a good cached entry. The keep-old-entry guard only checked
the rebuild context's timeout, so a build that failed FAST (ClickHouse
down → every enrich errors in ms) replaced the previously-good view
with an analytics-empty one stamped fresh — one ...
Stellar Index v0.21.8
[v0.21.8] — 2026-07-31
Added
- Visuals wave 3 — /network chain economics, /divergences Δ% history,
/anomalies visual pack (spec 1.17.0). Three M-effort items off the
visuals survey, each a small backend addition + charts over it:
(1)/v1/network/throughputbuckets now carry end-of-day chain state
off each day's last ledger —fee_pool+total_coins(stroop
strings, ADR-0003) andprotocol_version— added asargMax
aggregates inside the SAME bounded partition-pruned
stellar.ledgersscan;/networkcharts daily fee burn (delta of
consecutive complete days), a total-XLM line, and protocol-upgrade
step markers on the throughput chart (via a new optional
markers/priceLinesextension to the sharedLineChart).
(2) NewGET /v1/divergence/series?pair=&reference=&days=— the Δ%
history for one (pair, reference) fromdivergence_observations,
the same(asset, quote, reference, observed_at)index scan as the
board minus theDISTINCT ON, last-observation-per-bucket
downsampled (5m/30m/2h for the whitelisted 1/7/30-day windows, ≤
~360 points;firingis bucket-wide any-breach) with the
operator's realdivergence.threshold_pcton the wire;
/divergencesplots the selected board row's history with dashed
±threshold lines plus a signed |Δ%| bar list of the current board.
(3)/v1/anomalies?include=dailyadds a per-(UTC day, reason)
freeze tally (daily:null= not requested,[]= zero freezes
— degraded ≠ zero) via a new chunk-prunedFreezeDailyReasonCounts
reader;/anomaliesrenders a reason-composition donut, a
freeze-duration histogram over the recovered events served, the
previously-fetched-never-rendereddetail.deviation_pctas a
table column, and a day×reason calendar heatmap (new token-driven
ReasonHeatmap, sequential single-hue). API minor → 1.17.0; all
three spec artifacts regenerated. - Explorer visuals — quick-win wave over data already on the wire.
Five under-visualized surfaces now chart what their pages already
fetch, reusing the shared chart system plus three new bar
primitives (web/explorer/src/components/charts/Bars.tsx:
HBarList,PairedBars,DivergingColumns): (1)/dexes/[source]
renders the full/v1/protocols/{name}bespoke DEX suite (KPI
cards, trades + unique-traders series, top-pairs multi-line,
volume-by-pair donut, window pills) via a sharedBespokeSection
with the standalone USD-volume series de-duplicated against the
page's own 90d volume panel; (2)/protocols(and every locked
category page overProtocolsIndex) charts per-protocol TVL as a
bar list with the priced/unpriced honesty split — "≥" prefix +
hatched tail +pools_priced/pools_totalon lower-bound rows, the
server'sbasisas the tooltip, and NO bar for protocols without a
TVL derivation; (3)/liquidity-poolsexpanded rows replace the
nested depth tables with per-direction slippage/depth bars (one
chart per direction — different units never share an axis) and a
reserve-composition donut valued at the pool's own mid price;
(4)/accounts/[g]gains an account-character pack: ops-by-type
bars, cumulative-USD trade area + volume-by-venue donut over the
loaded priced trades, daily in/out diverging movement counts
(counts, not cross-asset amount sums), and inbound-vs-outbound
bridge bars; (5)/lending/[pool]reserves get real
supplied-vs-borrowed, fixed-scale utilization (near-cap flagged),
and supply-vs-borrow APR bars replacing the 16px CSS strips, the
/lendingindex gets an all-time liquidation-auctions-by-pool bar,
and/yieldswaps its staticCategoryHubfor the live
ProtocolsIndex lockedCategory="yield"directory (the/bridges
pattern). - Explorer flagship visuals: SDEX cumulative depth chart + asset
visual pack./markets/[pair]and/sdexgain a mirrored
step-area cumulative depth chart with a best-bid/ask + mid/spread
stat strip over the served/v1/sdex/orderbooklevels (new
DepthChartcomponent; a crossed snapshot renders an explicit
warning instead of a negative spread, and/sdex— previously a
static brochure — now shows the live book for four headline classic
pairs plus the daily USD-volume series + KPIs from
/v1/protocols/sdex)./assets/[slug]gains the asset visual
pack: the volume-by-source donut (/v1/markets/sources?asset=,
handler + component both existed unwired), per-market 24h volume
sparklines in the Markets tab (?include=sparkline), a live
1h/24h/7d/30d change strip + streak + honestly-labelled
"low since indexed" chip from the previously consumer-less
/v1/changes/coin/{id}endpoint (absent windows render "—"), a
holders concentration donut explicitly scoped to the served top-100
rows, and mint/burn/clawback comparison bars in the Supply tab.
Also fixes the/markets/[pair]build-time/v1/chartfetch
discarding ~95% of its series (now rendered as a 24h VWAP sparkline- summed 24h volume stat) and the OrderBookPanel test's cross-test
mock leak.
- summed 24h volume stat) and the OrderBookPanel test's cross-test
- Protocol analytics are warm by construction, and degradation is
now explicit on the wire (spec 1.16.0). Under replay load every
on-demand/v1/protocols/{name}bespoke build died at the request
deadline ("protocol bespoke build failed", 2026-07-31), pages lost
their visual suites, and the omitted block was indistinguishable
from a protocol with no data. Three changes: (1) a dedicated API
prewarm worker sweeps ALL registry protocols ×?days=windows
(one build at a time, 2 s pause, re-swept 10 min after each sweep
ends — sized from r1 timings under replay: soroswap 90d bespoke
~1.9 s, cctp ~0.4 s) so every protocol page + window is built
before anyone asks, with the detail cache now stale-serving past
its (raised, 20 min) TTL — a previously-built view is served with
flags.stalewhile ONE detached rebuild (own 90 s budget, never a
request deadline) refreshes it, and a failed/timed-out rebuild
keeps the old entry instead of blanking the block; (2) a new
analytics: {status: ok|stale|unavailable, as_of}object on the
detail view so clients can tell fresh / stale-but-served / degraded
apart from real zeros — the explorer renders an explicit
"temporarily unavailable" hint from the status instead of inferring
from field absence; (3) the/v1/operationsop-type panel joins
the API's 5-minute prewarm loop (it had SWR + detached refresh but
nothing warmed it at boot, so the first directory hit after every
deploy rendered without it; the aggregate measured ~70 ms warm on
r1). New paired metrics
stellarindex_protocol_detail_refresh_{total,duration_seconds}
(ok|degraded|timeout) cover the prewarm sweep and request-kicked
revalidations;/v1/assets/{id}/holderswas audited as already
stale-serving (never hard-misses a previously-seen asset).
Fixed
stellarindex_dex_trade_unit_ratio_detectedalerts on the FRACTION
of a source's flow (>50% AND >25 count), not absolute count — a busy
wrapper-redemption morning (yUSDC/USDC, yETH/ETH parity pairs) tripped
the old threshold at ~2% of sdex flow; the decode-bug fingerprint the
alert exists for makes EVERY trade 1:1. Both rule trees + runbook.- soroswap: non-directional swaps are recognized no-ops, not decode
errors — closes the last soroswap undecodable-but-matched blind
ledger (57,403,300). The lake disproves the "a well-formed swap
has exactly one in/out pair non-zero" assumption:pair.swap()is
directly invokable (Uniswap-v2-style) and accepts any argument
combination keeping K non-decreasing, so a swap can settle with all
value movement confined to ONE token side (the real case: pair
CAM7DY…, tx be7028b9…,amount_1_in=265, amount_1_out=70, both
token0 amounts zero). That is a real, recognized on-chain event but
NOT a trade — no (base, quote, price) exists — sodecodeSwapnow
surfacesErrNonDirectionalSwapand the Decoder projects zero rows
with nil error (same recognized-no-op contract as redstone's empty
write_pricesbatches), flipping the blind ledger to verifiable
expected-zero under the ADR-0033 honest-blind accounting. Golden
test pins the real lake bytes. Alongside,classify()now
enumerates the pair WASM's LP-share SEP-41 token events
(transfer/mint/burn/approve, lake-verified: 1,622/907/333/0
across all 230 registered pairs) asEventPairToken— classified
per the EVERY-event principle but deliberately NOT claimed by
Matches(): they are the sep41 domain, and the dispatcher is
first-match-wins, so claiming them would silently swallow any
LP-share token later added towatched_sep41_contracts. /v1/sdex/orderbookserved CROSSED books — zombie offers dead
since 2021 quarantined out and lake-verified. The live XLM/USDC
book quoted best bid 0.4327 vs best ask 0.1722 and carried ~46k
bids / ~40k asks where the real pair book is a few hundred offers.
Root cause: historical entry-change backfill wrote
intra_ledger_seq = 0on every row, so all same-ledger changes to
one offer key TIE onledger_entries_current's ReplacingMergeTree
version and an arbitrary row survives the merge — an offer updated
then fully consumed within one ledger can survive asupdated
forever (founding zombies: offers 845025288 / 845025425 / 845025699
/ 845028065, consumed at ledger 38,224,736± on 2021-11-10, still
"live" on 2026-07-31; their losingremovedrows are physically
gone fromledger_entries_currentbut intact in
ledger_entry_changes). Class fix in the book maintainer: loaded
offers whose winning version carries intra 0 (the tie-ambiguous
class) are quarantined out of the served book and drained at
2,500/tick by a new batched, partition-pruned
OfferRemovedAt(ledger, key)probe against the change stream —
proven-dead offers are dropped for good (offer LedgerKeys are never
reused, so a same...
Stellar Index v0.21.7
[v0.21.7] — 2026-07-30
Fixed
- Soroswap trades now carry their trader (
taker) — and a replay
backfills all history.SwapEvent.to(the swap recipient) is
on-chain in every soroswap swap since genesis, but the decoder
dropped it — soroswap was the one venue with 0%trades.taker
coverage (every sibling: 100%), which excluded soroswap from the
per-address trades view and its page's trader metrics. Captured
best-effort (an unreadable recipient never drops the trade); the
trades upsert already carriestaker = EXCLUDED.taker, so a
projector-replay -source soroswapretro-fills every historical
trade.
Added
- Every protocol category now ships a full visual suite (the
"primary point for protocols to show their stats" directive) —
window-reactive (24h/7d/30d/90d pills now on EVERY protocol page,
hourly buckets at 24h): Lending/Blend — activity-by-pool +
event-kind donuts, supply-vs-borrow / backstop / auction series,
per-pool top-5 lines, flash-loan table (counts over mixed-asset
sums by policy); SoroCredit — positions + settlement series;
Yield/DeFindex — vault deposits-vs-withdrawals lines, flows-by-
vault donut, per-vault table (single-asset amounts only — the 2
multi-asset vaults honestly render "—"); Oracles (reflector ×3,
redstone, band) — update-cadence series, per-feed top-5 lines,
updates-by-feed donut, feeds table, freshness + median-interval
KPIs (counts and timestamps only — no price restatement). All new
SQL validated read-only on r1 with timings pinned in comments.
Fixed
- CCTP flow volumes no longer double-count: transfers are deduplicated
andmint_and_forwardis excluded from sums. Ground-truthed on r1
(2026-07-30, all 52,205 cctp_events rows): (1) rows decoded before
event_index landed (migration 0112) coexist with their re-derived
copies — 12,139 (tx, op, type) groups hold a legacy event_index-0 row
plus the true-index row with identical amounts — so every flow query
now collapses to one row per (tx_hash, op_index) transfer; (2) every
mint_and_forwardop also emitsmint_and_withdrawfor the SAME
funds (0 forward-only ops), with the forward amount exactly 10× on
all 13,651 pairs — it restates the canonical 6-decimal value at the
local 7-decimal scale, so summing both inflated a forwarded transfer
11×. Inbound sums now use deduplicatedmint_and_withdrawonly.
Shape tests pin both rules. (The stale legacy rows themselves remain
in the table; queries are dedupe-safe regardless — an operator
cleanup can drop them independently.) - Redstone subset attribution is order-preserving — the residual 170
blind ledgers resolve. The v0.21.5 full verify cut the blind class
1,626 → 170; the residue was prices matching TWO candidates' medians
simultaneously, which the unordered rule refused. The adapter builds
updated_feedsin a single pass overfeed_ids(the same fact that
makes the equal-arity case a positional zip), so the surviving
entries are an ordered SUBSEQUENCE — attribution is now a
DP-counted subsequence alignment that attributes only a UNIQUE
complete alignment. Order can only disambiguate, never misattribute
(any true assignment is order-preserving by construction); genuinely
ambiguous events still refuse. Golden-pinned on the real
ledger-60104689 event (7 feeds → 5 survivors with a shared-median
collision). Completeness flips at the post-deploy replay + verify.
Added
- Explorer: the 24h/7d/30d/90d analytics window pills now live at
BespokeSection level, so EVERY protocol category's bespoke block —
KPIs included — is window-reactive. Previously only the bridge
showcase owned pills (and its KPIs stayed pinned to 90d while its
charts switched). Switching a pill refetches
/v1/protocols/{name}?days=Nand swaps the whole block;
BridgeShowcase now consumes the section-level window as props (its
all-time cumulative headline stays pinned to the initial fetch), and
non-bridge categories gain generic rendering for " · "
series — folded into one palette-colored multi-line panel per group
(the DEX "Top pairs · XLM/USDC" lines) instead of one panel per
series — plus window-labelled donuts/tables and honest
loading/empty/error states. TimeSeriesChart accepts the hourly
"YYYY-MM-DDTHH:MM" 24h-grain point dates with a time-of-day axis.
Vitest covers the lift for both paths: all categories refetch, the
bridge suite is unaffected. - DEX/AMM protocol pages get the full visual analytics suite
(soroswap / aquarius / phoenix / comet / sdex; no spec change — every
shape exists in 1.15.0). The bespoke block now serves: a "Volume by
pair" composition (top 8 pairs by USD volume + a SQL-side "Others"
fold, so SDEX's ~99k window pairs never cross the wire); USD-volume,
trade-count and unique-trader time-series plus top-5-pairs-by-volume
multi-series ("Top pairs · XLM/USDC", …), hourly-grained at the 24h
window via the real-timesource_volume_1hrollup; avg-trade-size
(exact NUMERIC division by the PRICED trade count) and unique-trader
KPIs; honest "since 2026-03-18" lifetime totals labelled by the daily
rollup's materialization floor (never "all-time" — raw SDEX history
reaches 2018); and a window-scoped "Largest trades" top-10 table with
tx links. Pair labels resolve token contracts through the verified-
currency catalogue's derived SAC addresses (XLM/USDC, …); unverified
tokens show truncated contract ids, never guessed symbols. Every
query was validated read-only on r1 (2026-07-30): two deliberate
SQL shapes keep SDEX's 90d window sub-second (hash-agg pair count
0.67s vs 6.0s naive; direct CAGG join for top-5 series 0.87s vs
19.8s), and raw-trades-derived surfaces (traders / avg size / largest
trades) are gated to ≤7d windows on SDEX with the omission Noted on
the block — soroswap's trader metrics are omitted honestly too (its
decoder captures no taker). Shape tests pin the grain, the window
bounds, the ADR-0003 NUMERIC rules and both perf-critical shapes. - CCTP SDF-showcase analytics suite: where USDC comes from and where
it goes (spec 1.15.0, additive)./v1/protocols/cctp's bespoke
block gains a genericbreakdownsfield (named composition datasets
for donut rendering) carrying "Inflows by source chain" / "Outflows by
destination chain"; per-chain top-5 inbound + outbound time-series
("Inbound · Base", …); an all-time daily "Cumulative net inflow"
series; all-time inbound/outbound/net KPIs plus unique depositor and
recipient counts; and a window-scoped "Largest transfers" table.
Source chains are attributed from the burn-side USDC token embedded in
each transfer's CCTP BurnMessage; destinations via Circle's public
domain registry — both maps verified against Circle's primary docs
(USDC contract addresses + domain list, 2026-07-30), including the
32-byte-address tails for Solana / Aptos / Starknet. Unrecognised
tokens/domains are labelled "Unverified (0x…)" / "Domain N" — never
guessed. The explorer's CCTP page renders the suite: side-by-side
source/destination donuts, per-chain multi-line charts, the headline
cumulative net-inflow chart, and largest transfers linking to
/transactions/{hash}, all reactive to the existing window pills. /v1/protocols/{name}accepts?days=(1/7/30/90, default 90) to
window the bespoke analytics block (spec 1.14.0, additive). Anything
outside the whitelist is a 400 problem+json (never a silent clamp);
the detail TTL cache now keys on (protocol, window) so window switches
can't serve another window's cached numbers. Atdays=1the bridge
flow series bucket HOURLY (a daily bucket collapses 24h to one point);
series names dropped their grain prefix and are now window-stable —
cctp "Inbound (USDC)" / "Outbound (USDC)", rozo "Settled volume
(USDC)". Lake-analytics fields keep their fixed 90-day lookback. The
explorer's bridge pages (cctp/rozo) render the flows as one combined
inbound/outbound line chart with 24h/7d/30d/90d window pills.- Bridge pages show USDC-denominated in/out FLOWS, not just counts.
/v1/protocols/cctpgains directional volumes (inbound = mints from
remote burns, outbound = deposit_for_burn), daily in/out series, and
per-domain volumes;/v1/protocols/rozogains its first bespoke
block (settled payment volume + daily series; admin flush sweeps
excluded to avoid double-counting). Amount scales ground-truthed
on-chain against the USDC SAC leg of real transactions — and they
DIFFER: CCTP events are canonical 6-decimal, Rozo events are local
7-decimal SAC stroops (the documented external-scaling trap class).
Exact NUMERIC division end-to-end. Registry genesis labels corrected
to lake-derived exact first events (cctp 62,403,000→62,146,641 —
the re-projected head; rozo 62,403,000→60,829,397).
Fixed
- Unknown tx hashes no longer trigger a 10.5B-row scan.
TransactionByHashfell through to the full-table bloom probe on
every index miss — written whentx_hash_indexwas partially
backfilled. The index now covers genesis→tip (20.78B rows from
ledger 3, verified on r1), so an index miss is an authoritative
not-found; the scan remains only for index-path errors and
index/base inconsistencies (both pinned by tests). Closes the last
serving-path instance of the non-sort-key filter class — and an
unauthenticated multi-second-per-request lever.
Fixed
- Account history (tx + ops) sourced arms are primary-index reads —
the 6-second bloom probe is gone./v1/accounts/{g}/transactions
and/operationsresolved "transactions this account SOURCED" via
thesource_accountbloom skip-index over the full 23B/34B-row
tables — granule-pruned but scan-shaped (measured on r1: 6.17 s for
the sourced arm vs 0.056 s for the participant arm on the SAME
328-op account), which is what pushed heavy...
Stellar Index v0.21.6
[v0.21.6] — 2026-07-30
Fixed
- Account trustline + offer reads are primary-index range reads — 75×
faster./v1/accounts/{g}'s trustline and offer queries rode the
account_idbloom skip-index (scan-shaped over the 43.6M-row
current-state table; the whale account's trustline read alone measured
5.18 s). An account's trustline/offer LedgerKeys share a fixed
key_xdrbyte prefix and sit contiguous in the table's
(entry_type, key_xdr) sort order, so both reads now prune by a
52-char base64 PK prefix (accountEntryKeyPrefix; exact
account_idequality closes the prefix's one-byte residual) —
0.069 s measured on the same whale. Account detail is now
interactive for every account, including the top wealth-ranked
ones; the stale-serving layer below remains as the belt-and-braces
for pathological cases. GET /v1/accounts/{g}whale accounts no longer 503 outside the
30-second post-fill window. The v0.21.5 detached account-state fill
fixed the cache-never-fills problem, but the 30sAccountStateCacheTTL
still treated an expired entry as a hard miss — so an account whose
scan outruns the request budget was warm for only ~30s after each
fill and cold the rest of the time (the final route-sweep holdout).
An expired entry is now SERVED while one detached refresh runs —
the same stale-serving contract the wealth cache adopted 2026-07-29 —
and the stale serve is surfaced honestly on the envelope's
flags.stale(AccountStateCached now returns the staleness so the
handler can pair the serve with the flag).