You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ADR-0044 (Proposed): explorer rendering moves to edge SSR — the
2026-07-03 site audit traced three production failure classes (the
silent 20k-file deploy freeze, bake-time poisoned pages, between-deploy
staleness) to rendering at build time; OpenNext on Cloudflare Workers
with per-route-family edge caching replaces static export, with a
staged migration and the pruned static path supported until cutover.
Fixed
Three site-audit P0/P1 fixes in one pass (S-006/S-009/crawl):
(a) contract pages now identify uncaptured-instance SACs from their
CAP-67 event topics with a spoof-proof derivation cross-check — ~55k
contracts (e.g. the upvoteICE SAC) stop rendering an unexplained void
and instead say which asset's SAC they are; (b) the API's
trailing-slash 308 now runs INSIDE the CORS middleware — it previously
carried no Access-Control-Allow-Origin, so browsers killed the
redirect and every trailing-slash API URL was as dead as the 404 the
redirect exists to prevent; (c) market-pair pages no longer canonical
to double-encoded 404 URLs (the route param arrives pre-encoded;
~500 pages were telling crawlers their real URL was a dead page).
The /assets listing serves the real universe (site audit
S-002/S-011): page 1 of the unified listing now fills from the ~191K
classic long tail when the curated catalogue is shorter than the limit
(the pager's own doc always promised this; in practice page 1 was 11
rows presented as the entire asset universe), and the search box's q=
now actually filters server-side across both phases (the storage layer
supported it all along — the handler never passed it).
Navigation tells the truth (site audit S-001/S-017/S-019): "DEX /
AMM" now lands on /dexes (the per-protocol venue + pools view that
already existed) instead of the verification index; the rail's
single-protocol "Soroswap Router" entry becomes "Verification" (the whole
15-protocol index); the search palette's "Account" entry points at
/dashboard instead of a route that never existed.
Impersonated identities no longer render on flagged issuers (site
audit S-010): the "LOBSTR — SCAM" row was actually a stellar.expert-listed
counterfeiter whose on-chain home_domain impersonates lobstr.co — our
pipeline was serving the stolen identity as the row's name, indicting the
victim brand. Flagged, UNVERIFIED issuers now serve no self-declared
org_name/home_domain (the G-key + reason are the honest identity); the
explorer badge is category-aware (SCAM / DEPRECATED / UNSAFE), so a real
org's deprecated legacy issuer no longer wears a red SCAM tag.
ClickHouse snapshot rows were being merge-destroyed (site audit,
P0-data): checkpoint state-snapshot rows all carried the same
(tx_hash="", op_index=-1, change_index=0) tail of the ReplacingMergeTree
sort key, so every snapshot entry modified in the same ledger collapsed
to ONE arbitrary survivor at merge time — measured >55% of the 48M-entry
Phase-C set already gone (blast radius: account-state, trustline, supply,
wasm readers). ChangeIndex is now crc32(key_xdr) — per-key unique,
re-run idempotent; the wasm/SAC instance reads repoint at
ledger_entries_current (current-state MV, merge-immune, PK-prefix
lookup). Operator: re-run state-snapshot -write -scope all to re-land
the destroyed entries.
Added
Asset logos for wallets (board #47): /v1/assets/verified rows now
carry image (the issuer's SEP-1 logo URL, https-only + sanitized — the
bulk surface for wallet icon loading; per-asset detail already served it).
Root-caused the biggest asset's missing metadata en route: Circle's
on-chain home_domain (circle.com) 404s its stellar.toml — a curated
domain-override map (same hand-vetted pattern as knownIssuers) redirects
the FETCH to the still-serving centre.io while the on-chain value stays
authoritative for identity; the bidirectional org-verification still
holds because centre.io's TOML lists the issuer back.
Point-in-time price: GET /v1/price/at?asset=&ts= (board #46, the
wallet-builder accommodation the RFP audit recommended): the closed
1-minute VWAP bucket at-or-before a historical instant — the
cost-basis/PnL/tax lookup every portfolio tool needs. observed_at is
the bucket's own close time (never the requested ts), and a nearest
bucket more than 24h before ts is an honest 404 instead of fabricated
continuity across dead markets. SDK gains Client.PriceAt.
Deep CEX history + queryable per-market inception (board #44):
kraken's raw-fills endpoint (/Trades, full history, nanosecond-cursor
pagination) is now a backfill path — backfill-external -raw-trades
reaches 2018-era XLM/USD where the OHLC endpoint's 720-candle horizon
returned nothing (golden-tested on a real captured 2018 frame; venue
rate-limit paced). /v1/markets?include=inception serves first_trade_at per market — the RFP's "since inception = first
recorded trade" as a queryable fact rather than a footnote.
1-month OHLC granularity + query-selectable price window (board #43,
the last RFP-text gaps): /v1/ohlc?interval=1mo serves true
calendar-month bars from the prices_1mo CAGG (which existed since
migration 0002 — only the endpoint's validator rejected it), completing
the RFP's suggested-granularity ladder; /v1/price?window=300|3600|86400
serves the aggregator's continuously-published rolling VWAP for that
window (default 60 = the closed-1m-bucket behavior, unchanged) — an
unpublished window is an honest 404, never a silent substitution.
Tip freshness hardening (board #42): an empty rolling window on
/v1/price/tip now escalates once to the 30s SLA bound before ANY
closed-bucket fallback. Live samples had shown ~90s staleness on quiet
seconds — the 5s default window missed and fell straight through to the
closed-minute store price; staleness now exceeds 30s only when the pair
genuinely had no trade in the last 30s (window_seconds reports the window
actually used; test pins the quiet-pair path). The /price-vs-/price/tip
freshness contract is now spelled out in the spec.
Batch price rows carry change_24h_pct (board #41): /v1/price/batch
rows with a fiat:USD quote pair current price with the signed trailing-24h
change — the exact bulk shape the Freighter RFP names for portfolio
screens. Silent-nil when no comparison bucket exists (a missing change
never costs the price itself). Audit corrected en route: fdv_usd, max_supply, and detail change_24h_pct already existed with correct
null-omission semantics — the first pass mistook null-omission for
absence.
SAC contract-address lookups now return the wrapped asset's full detail
and price (board #40, the RFP audit's biggest wallet-facing gap): a /v1/assets/{C...} lookup whose contract is a Stellar Asset Contract
resolves to the classic (or native) identity it wraps — trust-anchored on
the lake instance's core-minted StellarAsset executable AND a
derivation cross-check (the resolved asset must re-derive to the queried
address, so a spoofed metadata name can never redirect pricing;
adversarial test pins it). Classic + native asset detail now carries contract_id (the deterministically-derived SAC address, valid even
pre-deployment), golden-tested against the on-chain USDC + XLM SACs.
Changed
Legacy-brand purge (pre-rebrand naming), everywhere it was still live.
The API's diagnostic cache header is now X-Stellarindex-Cache; region.home_domain serves the current domain; the legacy-domain Caddy
alias is REMOVED (hard cutover — the old domain no longer serves); the
spec + docs drop the legacy key-prefix examples and the last legacy-prefix
API key was deleted from the store; MinIO's root identity and the
duplicate legacy reader user/policy are renamed/removed on r1 along with
the old hostname, orphaned /etc/default/* env files, and /opt remnants;
the prod-target guard and load-test allowlists drop the old hosts.
Deliberately kept: immutable history (changelog entries, frozen audit
records) and operator instructions that name legacy external artifacts
slated for deletion.
Added
Ansible is now r1's config deployment path, with drift guardrails.
After the two-way audit: 18 --check --diff rounds reconciled the
archival-node role against live r1 (the dry runs caught an inventory
pointing the partition-carver at a live pool disk, a placeholder
authorized_keys that would have locked out operator + deploy, a
pre-ADR-0034 toml template that would have dropped the ClickHouse config,
and stale/broken vault secrets), then staged application converged the
host: services de-privileged to the stellarindex user (CS-118/119),
galexie moved off MinIO root creds onto the dedicated writer user,
postgres config single-sourced (the hand-tuned 8GB max_wal_size had been
inert behind postgresql.auto.conf all along). One real incident during
apply — the role downgrade-broke the upstream OpenZFS userspace and
deleted the dkms module (recovered in minutes from the migration debs;
packages now apt-mark held, install gated, three new assertions).
Guardrails: weekly ansible-drift.yml (fails on divergence), CI ansible
syntax+lint job, hourly config-assertions (now 12 checks), and the
CLAUDE.md rule: every r1 host change lands in configs/ansible in the
same PR.
r1 ↔ ansible drift audit + config-assertion watchdog. Follow-up to the
rsyslog apply-gap finding: audited BOTH directions between r1's live state
and the ansible roles. Live-only fixes that a playbook render would have
erased are now codified (CS-010 supply reserves → template + r1 inventory
vars; redis maxmemory → archival-node task; ssh root-access var pinned);
repo-ahead apply-gaps closed (three alert groups + rebrand wording synced
to r1's rules). New hourly config-assertions.sh timer on r1 asserts the
load-bearing guard configs' CONTENT (9 assertions, all green) with stellarindex_config_assertion_failed/_stale alerts in both trees;
full findings table + standing "check --diff first" rule in
docs/operations/r1-ansible-drift-2026-07-03.md.
Root-disk fast-fill early warning (stellarindex_node_root_disk_filling_fast,
both rule trees + runbook + catalog): pages when root trends to full within
30 minutes — the 2026-06-11 ClickHouse log-wedge loop filled root at
~3.8 GB/min, going healthy→full in ~5 minutes, faster than the static <10%
page can be acted on. Deployed live to r1.
Fixed
The 2026-06-11 log-discipline rsyslog rules were never live on r1 —
the loki + clickhouse-server stop rules (the belt-and-braces that keeps a
journald flood out of /var/log/syslog) existed only in ansible role
15-log-discipline.yml, which does not auto-run against r1; the postmortem
recorded codified-as-applied, leaving the syslog half of the root-fill loop
open. Applied and probe-verified (tagged test line reaches journald, not
syslog); the file names the ansible role as source of truth.