Skip to main content

Methodology

How every price Stellar Index serves is computed, from raw on-chain event to the final aggregate. Each section links to the underlying ADR for the full rationale, alternatives considered, and consequences.

Source classes

What gets included in the VWAP — and what doesn't

Every venue we ingest from is tagged with one of four source classes. The class determines whether a venue contributes price observations to the aggregate or is reported alongside as context.

exchange
Real trading venues — DEXes (Soroswap, Phoenix, Aquarius, Comet, sdex), CEXes (Coinbase, Binance, Kraken). These are the only sources that contribute to the VWAP. Subdivided into dex / cex / fx for grouping.
aggregator
Third parties (CoinGecko, CoinMarketCap) that already aggregate the same upstream venues. Including them in our VWAP would double-count. We surface their numbers separately for divergence checks.
oracle
Reflector, Band, Redstone, Chainlink. Each runs its own methodology — adding their output to our VWAP would impose theirs on top of ours. We surface them as parallel readings + use them for cross-checks.
authority_sanity
A small set of Stellar-blessed reference points (anchor home-domains, canonical fiat rates) used as sanity bounds, not price input. Catches catastrophic drift.

The full per-venue registry — including include_in_vwap, paid, backfill_safe, and 24h trade counts — is at /sources.

VWAP weighting

Volume-weighted average across all eligible exchange-class trades

For an asset pair (BASE/QUOTE) over a window, the VWAP is:

VWAP = Σ(pricei × volumei) / Σ(volumei)

where each trade i is from a source with class = exchange. No per-venue weighting tier or boost — the weight is the trade's quote-side volume, period. A million dollars of XLM/USD trading at $0.12 on Coinbase counts the same as a million dollars of XLM/USD trading at $0.12 on Soroswap.

Outliers are filtered before the average using a per-asset statistical baseline (ADR-0019). A trade that prints more than N MAD-deviations from the rolling median is dropped from that bucket; multiple consecutive outliers from the same source flag the source as “misbehaving” and mute its contribution.

Stablecoin → fiat proxy

Why XLM/USDC contributes to the XLM/USD rate

On-chain trade venues quote against stablecoins (USDC, USDT, EURC) far more often than against raw fiat. To surface a useful XLM/USD rate, we proxy stablecoins to their pegged fiat at the aggregator layer, not at ingest. Specifically:

  • Ingest stores the real pair as observed (XLM/USDC, XLM/USDT, XLM/PYUSD, etc.).
  • The aggregator maps the pegged stablecoins to their fiat at VWAP compute time: USDT, USDC, DAI, PYUSD, USDP → USD; EURC, EUROC, EUROB → EUR; MXNe → MXN.
  • Eager normalisation at ingest would hide a depeg event. Late binding keeps the data honest — when a stablecoin loses its peg, the divergence from real fiat shows up in the historical record.

Freeze policy

When the API stops serving a price

Some failures shouldn't be smoothed over with a fresh number the aggregate can no longer stand behind. For those, the API keeps serving the last known-good value but stamps it with flags.frozen=true so consumers know not to act on it — rather than silently returning a misleading live rate. Freeze triggers (ADR-0019):

Outlier storm
More than 50% of trades in the window were filtered as statistical outliers. Indicates upstream-data noise that the aggregate cannot trust.
Source-class collapse
All exchange-class sources for a pair drop out simultaneously. Common cause: vendor outage taking out CEX feeds, leaving only DEX trades whose volume is too thin for a confident VWAP.
Cross-oracle divergence
Our VWAP and ≥2 independent oracles disagree by more than the configured tolerance for the asset class. Catches cases where our ingest has gone wrong without catching the failure ourselves.
Operator-triggered
On-call can freeze a pair manually during incident response — surfaced on the status page.

Active freezes are reported in real time on status.stellarindex.io, and historically as Atom syndication via /v1/incidents.atom.

Closed-bucket-only contract

Why every region serves the same number at the same wall-clock time

The aggregator computes prices in fixed time buckets (1m, 5m, 15m, 1h, 1d). The API only ever serves closed buckets — the in-progress bucket is invisible until it rolls over.

This is the load-bearing invariant behind cross-region consistency. Three regions ingest independently with slightly different latency profiles, but because they all only serve closed buckets, the value they return for a given timestamp is identical to the byte. No eventually-consistent reconciliation, no last-writer-wins, no stale-cache footgun. The one cost is a bucket-width of latency at the very tip — the 1-minute bucket isn't visible until ~5–10 seconds after the minute ends.

Tip-price callers who can't tolerate that latency use the separate/v1/price/tip endpoint, which serves the rolling-window in-progress aggregate explicitly flagged as such (ADR-0018).

Latency targets

What we measure ourselves against

The serving SLOs (ADR-0009):

PercentileTargetMeasured live
p50< 50 msstatus.stellarindex.io
p95< 200 mslive
p99< 500 mslive

End-to-end freshness target: a trade landing on Stellar mainnet at ledger close T is queryable via the API by T+30 seconds in the typical case (longer at bucket-roll boundaries). Each component's slice of the budget — Galexie → indexer → aggregator → API → CDN — is enumerated in the ADR.

Numerical precision

Why every amount is a string

Soroban stores token quantities as i128 / u128 — two 64-bit words. At the standard 7-decimal precision, any amount above ~922 billion tokens overflows int64. So we never truncate (ADR-0003):

  • *big.Int in Go.
  • NUMERIC column in TimescaleDB.
  • Strings on the wire. JSON numbers are IEEE-754 doubles; precision loss kicks in above 253, well below the i128 range. Treating amounts as numbers silently corrupts every value above ~9 quadrillion tokens.

Why every decision is documented

Stellar already has Horizon. The reason a second pricing stack adds value is methodology — what gets included, how we handle edge cases, what triggers a freeze. None of that is useful behind a closed door.

Every load-bearing decision has an Architecture Decision Record at /research. Every alert has a runbook. Every Soroban contract is audited per WASM-version before backfill is permitted. Every incident gets a public postmortem on status.stellarindex.io.