Methodology
Source classes
What gets included in the VWAP — and what doesn't
Every venue we ingest from is tagged with one of seven source classes. The class determines whether a venue contributes price observations to the aggregate or is reported alongside as context. The first four carry a price; the last three are indexed for flow and state, and never reach the aggregate.
- exchange
- Real trading venues — DEXes (Soroswap, Phoenix, Aquarius, Comet, sdex), CEXes (Coinbase, Binance, Kraken, Bitstamp), FX vendors. 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.
- lending
- On-chain lending protocols (Blend, SoroCredit). Their events are directional state changes — supply, borrow, liquidation auction, bad debt — taken on top of some other oracle’s price rather than new price observations. Surfaced as a secondary validation and protocol-health surface.
- router
- Soroban DEX routers and aggregator vaults (soroswap-router, defindex, upshift). They emit no independent trades — they invoke the contracts that do — so counting them would double-count the underlying pool’s swap. Indexed for per-transaction attribution (which router drove this swap) and requested-vs-realised path.
- bridge
- Cross-chain transfer protocols (Circle CCTP, Rozo). These move tokens between chains rather than exchanging them at a price: a deposit_for_burn on Stellar plus a mint_and_withdraw elsewhere is one logical transfer, not a two-leg trade. Surfaced for cross-chain flow attribution and USDC supply accounting.
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 base-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.
The VWAP is not outlier-filtered by default. /v1/vwap defaults outlier_sigma to 0. /v1/ohlc and /v1/twap filter by default: OHLC because its High and Low have no statistical robustness at all — one dust trade pins them — and TWAP because a time weight is unrelated to trade size. Pass outlier_sigma explicitly on /v1/vwap if you need filtering, or 0 on the other two to disable it.
Do not read volume-weighting as an outlier defence. On a sparse window a single print carries the whole result, and because VWAP is Σquote / Σbase an actor supplying the quote asset dominates the numerator for free. Time-weighting is weaker still — TWAP has no volume term.
Separately from filtering, anomaly detection can refuse to publish a bucket outright (ADR-0019); see freeze policy below.
The authoritative per-endpoint defaults are served, not written here: see aggregation.outlier_filter on /v1/methodology.
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 operator-declared pegged stablecoins to their fiat at VWAP compute time (ADR-0026). Which stablecoins those are is deployment configuration, not a fixed list — this deployment's live mapping is served as
aggregation.stablecoin_fiat_proxyon/v1/methodology. A peg is declared by its fullCODE-ISSUERidentity, never by code alone. - 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):
- Single-source deviation
- A closed-bucket VWAP move at or above the class freeze threshold, contributed by only one source — the manipulation signature the threshold table is calibrated against. The same move with more than one corroborating source is a warning, not a freeze.
- Statistical divergence
- The published price scores below the confidence floor AND its deviation from the rolling per-asset baseline exceeds 5 standard deviations AND it is still single-source, all three at once. Confidence itself blends the baseline z-score, source count and diversity, liquidity, and cross-oracle agreement — divergence alone never freezes a pair on its own.
Active freezes are reported in real time on the status page, and historically as Atom syndication via /v1/incidents.atom. Freezes are automatic only — on-call cannot start one by hand. Once a freeze has climbed the full extension ladder without clearing on its own, an operator can end it early with stellarindex-ops freeze-unfreeze, which is logged and reflected on the status page immediately.
Closed-bucket-only contract
Why any 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. One region serves the API today; the three-region active/active topology is ratified in ADR-0050 and scheduled after v1.0. Each region will ingest independently, with slightly different latency profiles, so the guarantee has to hold without any coordination between them: because a region only ever serves closed buckets, the value any of them returns 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 | the status page |
| 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 the status page.