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:
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):
| Percentile | Target | Measured live |
|---|---|---|
| p50 | < 50 ms | status.stellarindex.io |
| p95 | < 200 ms | live |
| p99 | < 500 ms | live |
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.Intin Go.NUMERICcolumn 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.