9.9 KiB
ADR-0040 — Operator-configurable price denomination fiat + denomination-agnostic API field rename (cp128)
Status: Accepted (shipped 2026-05; pre-launch hardening campaign)
Date: 2026-05-23
Deciders: project maintainer (Ken)
Related: ADR-0039 (self-sovereign pricing — established the denominationFiat parameter at the factory level; this ADR exposes it as operator config).
Context
ADR-0039 designed the morphit_native price fetcher as a generic
factory parameterized on (asset, denominationFiat, ...). The
factory call site in apps/indexer/src/indexer/price/factory.ts
hardcoded denominationFiat: 'USD' and the listing-fee API
returned field names base_fee_usd and blurt_price_usd — names
that locked in USD as the indexer's display unit.
Two related limitations:
-
Operators serving non-USD markets today. A Brazilian operator wants the listing-fee echo in BRL; a Eurozone operator wants EUR; an Iranian operator may want IRR or XAU. All of these are realistic, immediate use cases — not hypothetical future scenarios.
-
USD-collapse / petrodollar-erosion hedge. Ken's framing was forward-looking: if USD eventually loses its role as the global unit of account (replaced by a BRICS arrangement, an XDR-like basket, gold-anchored pricing, or any other emerging unit), the indexer should be able to switch denomination without a code change.
Both concerns map to the same fix: expose denominationFiat as
operator config and rename the API fields to be denomination-
agnostic.
Pre-launch leverage (no instances live anywhere) means the API field rename costs nothing — there are no consumers to migrate beyond the in-repo frontend and tests.
Decision
Backend
-
New env var
MORPHIT_INDEXER_PRICE_FEED_DENOMINATION_FIAT. Default'USD'(preserves cp127 behavior). Validated against/^[A-Z]{3,8}$/— 3-8 uppercase letters, broad enough to accommodate ISO 4217 codes (USD, EUR, JPY, ...), IMF Special Drawing Rights (XDR), precious metals (XAU, XAG), and any future ticker. -
Config wiring: new field
priceFeedDenominationFiat: stringon theConfiginterface. Passed to the morphit_native fetcher's factory infactory.ts(replacing the hardcoded'USD'), and read by the receipt endpoint as its default-when-query-param-omitted denomination. -
Listing-fee API field rename:
Pre-cp128 Post-cp128 base_fee_usdbase_fee_fiatblurt_price_usdblurt_price_fiat(n/a) denomination_fiat: 'USD' | 'EUR' | ...The
denomination_fiatcompanion field tells the consumer which fiat the numeric values are in. Frontends read this to render with the appropriate symbol / locale rules.
Frontend
-
New formatter
formatFiat(amount, ticker)inapps/web/src/lib/i18n/formatters.ts. Knows about ISO 4217 codes (usesIntl.NumberFormat's currency style for those) and has a fallback"{number} {TICKER}"format for non-ISO tickers. Picks per-ticker decimal precision (JPY → 0 digits, XAU/XAG → 8 digits, XDR → 4 digits, default → 2 digits). -
formatUsdremoved — pre-launch leverage; all call sites migrated toformatFiat(amount, ticker)with ticker coming from the listing-fee response'sdenomination_fiatfield. -
Two consumer call sites updated:
apps/web/src/lib/components/StrangerFeeModal.svelteapps/web/src/routes/[lang]/post/+page.svelte
Both renamed local
usdPerBlurt→fiatPerBlurt, addeddenominationFiatstate, switched toformatFiat.
Setup wizard
-
New picker step at the end of the listing-fee wizard (
apps/ops-cli/src/init/steps.ts:stepListingFee). Curated list of common fiats (USD/EUR/GBP/JPY/BRL/CNY/INR/RUB/AED/XDR/XAU) plus an "Other (enter ticker)" free-text option validated against/^[A-Z]{3,8}$/. Default USD. -
Generated env file (
render.ts) includes the newMORPHIT_INDEXER_PRICE_FEED_DENOMINATION_FIAT=...line with an explanatory comment block.
Documentation
- This ADR.
OPERATIONS.md+RUN-A-MORPHIT-NODE.mdupdates.- Canonical env example file (
ops/env/indexer.env.example) documents the new env var with operator-facing notes about realistic use cases. - FAQ entry
where_does_blurt_price_come_fromgains one paragraph mentioning denomination configurability × 10 locales.
Resilience scenarios — what changes, what doesn't
Operator in Brazil sets denomination=BRL. Listing-fee
response shows denomination_fiat: "BRL", blurt_price_fiat: 0.012 (or whatever the BRL value is). Frontend renders
"60 BLURT (~R$0.72)" — the Real symbol and Brazilian formatting.
Operator in Iran sets denomination=XAU. Listing-fee response
shows denomination_fiat: "XAU", blurt_price_fiat: 0.0000023
(BLURT/oz). Frontend renders "60 BLURT (~XAU 0.00013800)" — hard-
currency hedge against fiat instability.
Soft USD-erosion scenario. USD still dominant but operators in jurisdictions with regulatory pressure on USD pricing switch to local fiat or XDR. No code change required; flip the env var, restart the indexer.
Hard USD-collapse scenario. All operators globally agree to move to a new common unit. Federation-wide coordination via operator messaging (not in Morphit's scope); each operator flips their env var to the new ticker. Architecturally Morphit survives without modification.
Tier 2 (stablecoin) caveat under non-USD denomination
The morphit_native fetcher's Tier 2 (stablecoin-anchored) anchor assumes the configured stablecoins are pegged to the denomination fiat, not to USD. Today the registry only has USD-pegged stablecoins (USDT, USDC, DAI), which means:
- Operators with denomination=USD: Tier 2 works as designed.
- Operators with denomination=EUR: Tier 2 with USD-pegged
stablecoins would give a price ~0% off from the EUR
equivalent at current USD/EUR rates — operator should either
set
MORPHIT_INDEXER_PRICE_FEED_STABLECOIN_KEYS=''(disable Tier 2 stablecoin anchoring, fall back to Tier 1 USD-direct orders priced in EUR) or wait for EUR-pegged stablecoins to be added to the asset registry (EURT, EURC, EURS — none currently wired).
This is documented honestly in the env example and ADR. Future work (cp129+) can add EUR-pegged stablecoins to the registry.
What this design DOESN'T solve (honest limitations)
-
Cross-instance denomination coordination. Different operators can pick different denominations. A user on a EUR-denominated instance sees EUR prices; the same orderbook viewed via a USD-denominated instance shows USD prices. This is by design (federation = per-operator sovereignty) but means there's no single "the BLURT price" displayed everywhere. Users should treat the displayed price as a courtesy, not a cross-instance authoritative quote.
-
Fiat-to-fiat conversion. Morphit doesn't convert between fiats. An order posted with
fiat_currency='USD'shown on a EUR-denominated instance still displays the USD price stated by the trader. Only the listing-fee echo and morphit_native- derived price use the denomination_fiat config. -
Operator picks the "wrong" denomination. If an operator in the US sets
denomination_fiat='XAU', their users see gold-priced fees. That's the operator's choice; users who disagree can switch to a different instance.
Operator action required
-
None mandatory. Default remains
USD; pre-launch instances inherit this automatically. -
Optional for non-USD operators: set the env var or use the wizard.
Privacy + decentralization posture
Per priorities #1 and #2:
- No new on-chain data. Denomination is a per-instance display preference; nothing crosses the federation.
- No new federation-wide constants. Each operator decides their own.
- No new operator-mandatory config. Default works.
Future work
-
EUR-pegged / non-USD-pegged stablecoins in the asset registry (cp129+). Enables Tier 2 anchoring for non-USD denominations.
-
Per-asset denomination configurability (cp130+). Today the env var applies to BLURT pricing only. When morphit_native gets wired for BTC/USD, XMR/USD, etc. (future ADR), each asset's denomination might want independent config — e.g. an operator could denominate BLURT pricing in EUR but BTC pricing in USD if that makes sense for their market. Cp128 doesn't block this; the factory already accepts per-instance denominationFiat.
-
Symbol-table extension in
formatFiatfor region-specific formatting niceties (e.g. Indian numbering system for INR uses lakhs/crores instead of millions/billions). TodayIntl.NumberFormathandles the basics correctly; locale- specific polish can come later.
Related
apps/indexer/src/config/index.ts— env var + Config fieldapps/indexer/src/indexer/price/factory.ts— reads configapps/indexer/src/indexer/price/morphitNativeFetcher.ts— generic factory was already parameterized (cp127)apps/indexer/src/api/priceReceipt.ts— reads configapps/indexer/src/api/listingFeeBody.ts— field renameapps/indexer/src/api/listingFee.ts— doc-comment updateapps/indexer/scripts/api-response-shape-smoke.ts— rename + new EUR + XAU scenariosapps/indexer/test/testutils/context.ts— fakeConfig defaultapps/web/src/lib/i18n/formatters.ts—formatFiatadded,formatUsdremovedapps/web/src/lib/components/StrangerFeeModal.svelte— consumerapps/web/src/routes/[lang]/post/+page.svelte— consumerapps/web/scripts/i18n-formatters-smoke.ts— updated forformatFiatapps/ops-cli/src/init/render.ts— ListingFeeResult interface extended; env file generator emits new lineapps/ops-cli/src/init/steps.ts— wizard step pickerops/env/indexer.env.example— documents the new env var- FAQ entry
where_does_blurt_price_come_from× 10 locales — new paragraph about configurability