Skip to content

Rates Engine v0.5.0-rc.40

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 May 14:04
· 1029 commits to main since this release

[v0.5.0-rc.40] — 2026-05-11

Added

  • Ansible: monitoring playbook + log-discipline task codify the
    post-incident operator lore from 2026-05-10. The
    configs/ansible/roles/prometheus/templates/alertmanager.yml.j2
    template now uses the page / ticket / informational
    severity vocabulary matching every rule in deploy/monitoring/
    and configs/prometheus/rules.r1/, plus a deadmansswitch
    receiver wired to a Healthchecks.io URL. New
    configs/ansible/playbooks/monitoring.yml invokes the role on
    archival_nodes. New configs/ansible/roles/archival-node/tasks/ 15-log-discipline.yml installs an /etc/logrotate.d/rsyslog
    override (100 MB cap, 7-rotation history, gzip-compressed) and
    /etc/systemd/journald.conf.d/00-cap.conf with
    SystemMaxUse=500M + SystemKeepFree=300M — both directly
    addressing the operational follow-ups in
    internal/incidents/data/2026-05-10-redis-writes-blocked-disk-full.md.
    A from-scratch r1 (or future R2/R3) rebuild now picks up the
    fix automatically. Operator action: provision
    /etc/default/alertmanager-secrets (Slack + Healthchecks URLs)
    and run ansible-playbook -i inventory/r1.yml playbooks/monitoring.yml
    on r1 to actually start delivering alerts.

  • /v1/assets/{slug} global view + /v1/coins deprecation
    (R-018 Phase 1.4a). The /v1/assets/{asset_id} route now
    dispatches on the path parameter: a verified-currency slug
    (usdc, eurc, aqua, …) returns the new GlobalAssetView
    wire shape — cross-chain identity + price block from the Phase
    1.3a three-tier fallback chain + a networks[] list with
    Stellar deep_link entries pointing at the per-Stellar-asset
    view. Canonical asset_ids (USDC-GA5Z…, native, C…,
    fiat:USD) still route to the existing per-Stellar-asset
    surface unchanged. Production wiring binds
    aggregate.GlobalPriceReader to *timescale.Store + the
    existing Redis triangulated looker, with
    external.AggregatorSources() as the tier-2 source list.
    /v1/coins and /v1/coins/{slug} now emit Deprecation: true

    • Link: </v1/assets/{slug}>; rel="successor-version" headers
      per RFC 9745 / 8288 — runtime behaviour unchanged so the
      explorer (Phase 1.5) keeps working. Actual /v1/coins
      deletion (1.4b) lands after the explorer migrates.
  • Three-tier global-price fallback chain
    (R-018 Phase 1.3a). New internal/aggregate/ComputeGlobalPrice
    walks vwap_nativeaggregator_avgtriangulated in
    order, returning the first tier whose data satisfies its
    threshold (trade-count floor for tier 1, freshness window for
    tier 2). Result carries Price, Authority (one of the three
    tier labels), Sources, and AsOf so Phase 1.4's /v1/assets/ {slug} global view can surface provenance per response. New
    external.AggregatorSources() helper returns the aggregator-
    class source names in deterministic order — matches the
    pre-existing FXSources() pattern. The cross-chain
    ticker-bucketed VWAP CAGG (1.3b) is explicitly deferred — it's
    algorithmically distinct from the per-pair VWAP and only
    meaningful once we ingest non-Stellar-chain trades.

  • Catalogue-driven CoinGecko coverage + aggregator-price reader
    (R-018 Phase 1.2). The CoinGecko poller's ticker map and the
    indexer's aggregator pair set now derive from the verified-
    currency catalogue: adding a verified currency with a
    coingecko_id in internal/currency/data/seed.yaml
    automatically extends polling coverage. CG's hardcoded
    ticker-to-slug map (13 entries) remains a fallback for tests
    and pre-Phase-1.2 callers. New storage method
    Store.LatestAggregatorPricesForPair(ctx, base, quote, sources)
    returns the most-recent observation per aggregator-class source
    — the seam Phase 1.3's aggregator_avg price-authority tier
    consumes. Reuses the existing oracle_updates hypertable (no
    new migration). CG catalogue-augmentation worker (top-N
    market-cap refresh) deferred — separate trust surface; the
    hand-curated seed suffices for v1.

  • Unverified-ticker-collision warning on /v1/assets/{id}
    (R-018 Phase 1.1). Requests for an asset whose code matches a
    verified currency's Stellar ticker (USDC, EURC, AQUA, …) but
    whose issuer doesn't match the verified entry now carry an
    unverified_warning body pointing at the verified canonical
    asset, plus flags.unverified_ticker_collision = true on the
    envelope. Warning body fields: verified_slug,
    verified_asset_id, verified_name, verified_issuer, note
    (a one-sentence message rendered verbatim by clients). Powered
    by a new internal/currency package + a 26-currency seed
    YAML embedded in the binary
    (internal/currency/data/seed.yaml) covering Stellar native +
    major USD stablecoins (USDC, USDT, PYUSD), EUR stablecoins
    (EURC), Stellar-native tokens (AQUA, yXLM, SHX, VELO, BLND,
    PHO, yUSDC) and globals without verified Stellar issuers (BTC,
    ETH, SOL, BNB, XRP, ADA, DOGE, AVAX, MATIC, DOT, LINK, UNI,
    AAVE, WBTC). Foundation for the multi-network assets migration
    (docs/architecture/multi-network-assets-migration.md);
    Phases 1.2 (CG/CMC connectors) → 1.5 (explorer migration) build
    on this catalogue. OpenAPI + pkg/client.UnverifiedWarning +
    Flags.UnverifiedTickerCollision in lockstep.

  • /v1/methodology — machine-readable summary of the active
    aggregation policy (R-023). Returns the VWAP method,
    per-endpoint outlier filters, the operator's stablecoin →
    fiat-USD proxy allow-list, the four source classes
    (exchange / aggregator / oracle / authority_sanity) and which
    contributes to the served price, the flat list of registered
    venues with class / weight / VWAP-inclusion flags, and pointers
    to the long-form ADRs that govern each section. Designed for
    transparency consumers (compliance, auditors, integrators) who
    want to verify the policy without parsing the explorer's HTML
    /methodology page or chasing ADR cross-refs. Sub-millisecond —
    derived from compile-time constants + the in-memory source
    registry + operator config; no DB call. OpenAPI spec, pkg/client
    Methodology shape, and explorer types kept in lock-step.
    Three regression tests pin baseline shape, peg-config
    round-trip, and empty-pegs deployment.

Changed

  • /v1/markets default sort changed from pair (alphabetical)
    to volume_24h_usd_desc
    (R-014). The alphabetical default
    surfaced spam tokens (0-…, 0TAX-…, 0x1F3D4-…) at the top
    of every cold listing — useless for the "what's interesting on
    Stellar" query and the explorer always passed
    ?order_by=volume_24h_usd_desc explicitly to work around it.
    Now the implicit default matches what every consumer wants.
    Callers paginating the entire universe of pairs in lex order can
    still pass ?order_by=pair explicitly. Cursor-format
    compatibility note:
    because the cursor is sort-key-tagged
    (validated via ValidateMarketsCursor), cursors generated under
    the old alphabetical default will return 400 against the new
    default — pass ?order_by=pair alongside the cursor to resume
    the alphabetical pagination, or drop the cursor and start fresh
    on the new default.

  • /v1/observations now sets flags.triangulated=true on an
    empty result when /v1/price would have served a value via the
    Redis VWAP cache or stablecoin-fiat proxy
    . The endpoint is
    raw-per-source by ADR-0018, so a triangulated pair has no rows
    to return — but the empty data: [] was indistinguishable from
    "this pair is unpriced" and sent integrators chasing nonexistent
    data. The hint never fires when the caller passed ?source=
    (source-filtered queries are asking about a specific venue, not
    the aggregate). R-011 in docs/review-2026-05-10.md.

Fixed

  • /v1/assets/{id} and /v1/assets/{id}/metadata now run their
    responses through the same enrichIssuer known-issuers backfill
    that /v1/issuers already used
    . Pre-fix, the two surfaces
    disagreed on whether SEP-1 metadata existed for the same issuer:
    /v1/assets/USDC-G… reported home_domain: null, sep1_status: "not_applicable" while /v1/issuers/G… reported
    home_domain: "centre.io". The asset surface relied on the
    watched-set sep1-refresh worker having populated the storage row,
    which doesn't run on a fresh deployment. Now both surfaces fall
    back to the curated internal/api/v1/known_issuers.go map when
    the storage row is empty. R-016 in docs/review-2026-05-10.md.

Added

  • /v1/chart envelope carries a truncated flag plus
    data_starts_at and requested_from timestamps when the
    requested timeframe extends before the deployment's earliest
    available data
    . r1 today only retains ~7 days of high-resolution
    history but still accepts ?timeframe=1y — pre-fix, consumers
    couldn't tell whether the returned 7 daily points were the last 7
    days of a long history or all the history this deployment has.
    timeframe=all always reports truncated=false (that timeframe
    means "everything you have" by definition). OpenAPI + pkg/client
    ChartSeries updated. R-013 in docs/review-2026-05-10.md.

Fixed

  • Handler-timeout paths return 503, not 500, when the per-call
    context deadline fires and Postgres returns its own
    canceling statement error
    . errors.Is(err, context.DeadlineExceeded) doesn't match the pq.Error
    SQLSTATE 57014 that lib/pq surfaces after the v3 cancel-request
    flow — so /v1/markets, /v1/pools, and /v1/coins were
    500ing on the cold-cache path that the 8s ceiling was
    specifically meant to convert into a retryable 503. New
    handlerTimedOut(callCtx, err) helper consults the per-call
    context's Err() as the authoritative signal. R-021 in
    docs/review-2026-05-10.md.

  • All-time-high (/v1/coins/{slug}.ath, ?include=ath on
    /v1/coins) is now derived from prices_1d.vwap instead of
    prices_1d.high_price
    . The single-tick max was being polluted
    by sub-stroop dust trades — XLM was reporting an ATH of $1.03
    on r1 because a single 1-stroop ↔ 1-stroop SDEX dust ManageOffer
    cross set max(quote/base) = 1.0 for the day. Day-VWAP is
    volume-weighted and naturally rejects dust. Same family of fix
    as /v1/ohlc outlier filtering. R-008 in
    docs/review-2026-05-10.md.

Changed

  • /v1/ohlc applies a 4σ outlier filter by default. OHLC's
    High/Low have no statistical robustness — a single 1-stroop ↔
    1-stroop SDEX dust ManageOffer cross at the offer-book boundary
    was pinning XLM/USD high=$1.0000000000 on the live r1 surface
    even though the real cluster was at ~$0.168. The handler now
    routes trades through aggregate.FilterOutliers (already used by
    the aggregator orchestrator and /v1/vwap) before
    aggregate.ComputeOHLC. New ?outlier_sigma=N query param lets
    callers tune the threshold; pass outlier_sigma=0 to opt out for
    raw extremes (the explorer's "show every print" view). The
    per-bar volume + trade_count fields reflect the post-filter set,
    matching the High/Low semantics. R-007 in
    docs/review-2026-05-10.md.

Fixed

  • /v1/price/batch no longer silently drops asset_ids whose price
    comes via the stablecoin → fiat:USD proxy chain
    . The batch path
    inlined only the Redis-VWAP and fiat-cross-rate fallbacks, so
    asset_ids that returned 200 from the single-asset /v1/price
    (because they hit tryStablecoinFiatProxy) were missing from the
    batch envelope without warning. fetchBatchRow now shares the
    full three-layer priceFallback chain with handlePrice.
    Regression test added in price_batch_test.go.
    R-005 in docs/review-2026-05-10.md.