Context
The API must publish total_supply, circulating_supply and max_supply for every indexed asset (requirement F2.4, docs/architecture/coverage-matrix.md). Native XLM, classic credit assets and SEP-41 tokens have three structurally different sources of truth, so no single pipeline serves all three.
Decision
Three domain-specific algorithms share one schema, plus an operator-configurable locked-set policy for circulating supply.
Algorithm 1, native XLM.
total_supplyis the constant50_001_806_812 * 10^7stroops (50 B genesis plus inflation pool, frozen October 2019), andmax_supplyequals it.circulating_supplyistotal_supplyminus the SDF reserve account balances; the reserve list is version-controlled config, not derivable on chain.
Algorithm 2, classic credit assets.
total_supplyis the sum of trustline, claimable-balance, LP-reserve pro-rata and SAC-wrapped contract balances, reconstructed from Galexie ledger meta and kept as a running total per (asset, ledger).max_supplyisnullby default. An operator override wins; otherwise a SEP-1[[CURRENCIES]]declaration applies (max_number, elsefixed_number; blocked byis_unlimited = true), scaled from display units to raw units by the asset's decimals.circulating_supplyistotal_supplyminus the locked set: by default the issuer's own balance, extendable per asset in the supply policy YAML. LP-reserve balances are not excluded.
Algorithm 3, SEP-41 tokens.
total_supplyis the lifetime sum of mint minus burn minus clawback events.max_supplyfollows the same order: operator override, then SEP-1, thennull.circulating_supplyistotal_supplyminus the locked set: by default the token's admin balance, extendable per token.
SEP-1 overlay. The overlay applies at serving time only (internal/api/v1/assets_f2.go) and never rewrites asset_supply_history. The declaration is passed verbatim as max_number / fixed_number / is_unlimited, and an applied overlay sets max_supply_basis: "sep1_declared_max". supply_basis keeps naming the policy behind total and circulating (xlm_sdf_reserve_exclusion, issuer_exclusion, admin_exclusion, override, no_metadata).
SAC-wrapped classics are computed by both Algorithm 2 and Algorithm 3 and cross-checked by internal/supply.WrapClass.
WrapClassFull, selected only for an operator-attested pair in[supply].fully_wrapped_sacs, requires equality within 1 stroop.- Every other pair is
WrapClassPartial, where classic total legitimately exceeds the SAC total, and it checks one bound:classic.SACWrappedStroops <= sac.TotalSupply. - That bound alone drives
DivergenceStroopsand thestellarindex_supply_cross_check_divergence_stroopsalert. - The over-mint leg,
sac.TotalSupply <= classic.TotalSupply, is computed asOverMintStroopsand is diagnostic only, because classic-side burns and one-way distributions (BLND, PHO) falsify it. - When
SACWrappedStroopsis nil the bound is not evaluated andSubsetBoundCheckedis false; a reader ofWithinTolerancemust readSubsetBoundCheckedwith it. - Known blind spots: an under-counted
SACWrappedStroopspasses silently (cured bysupply seed-sac-balances -full-history), and the non-SAC half of Algorithm 2 has no second observation.
Schema. All supply fields are decimal strings on the wire (ADR-0003); any field without a defensible value is null, never fabricated. asset_supply_history is a hypertable keyed by (asset_key, ledger_sequence) carrying total, circulating, nullable max, basis and ledger. It is idempotent-corrective, not append-only: the writer (internal/storage/timescale/supply.go) upserts a re-derive in place when derive_generation is at least the stored value (migration 0109).
Rejected: one unified algorithm, importing third-party aggregator supply, per-asset hard-coded locked sets, omitting max_supply, and deriving max from auth flags.
Invariant
- Supply amounts are
*big.Int/NUMERICand strings on the wire; ADR-0003 and its guard tests enforce it. - An unknown supply field is
null, never zero or a guess. - The SEP-1 overlay labels
max_supply_basisand leavessupply_basisuntouched;internal/api/v1tests enforce it. - The cross-check pages only on the
WrapClassPartialescrow bound, or equality forWrapClassFull;internal/supply/crosscheck_test.goenforces it. - The application layer is the only enforcement of single-writer on
asset_supply_history; no Postgres role split exists.
Consequences
Three algorithms mean three test surfaces, and the locked-set YAML needs curation per asset; uncurated assets fall back to issuer-only exclusion and say so in supply_basis. Market-cap, FDV and supply-percentage fields depend on this table.
Evidence
internal/supply/ (policy, overlay, crosscheck), internal/storage/timescale/supply.go, docs/architecture/supply-pipeline.md, and docs/operations/runbooks/supply.md#stellarindex_supply_cross_check_divergence.