Skip to main content
Pharos
PHAROSlive stablecoin signals

Mint Burn Flows

Mint/burn ingestion, scoring, and admin backfills.

On-chain mint and burn event tracker for stablecoins on their configured issuance chains via Alchemy JSON-RPC. Detects Transfer events (and USDT-specific Issue/Redeem events), aggregates them into hourly flow buckets, exposes per-coin raw Net Flow plus baseline-relative Pressure Shift vs 30D, computes a market-cap-weighted Bank Run Gauge, and flags flight-to-quality signals. Live ingestion runs in two lanes: a critical 30-minute lane for major coverage and an offset extended 30-minute lane for long-tail backlog drain.

Product scope note: the public /flows page now surfaces the configured issuance scope plus per-coin coverage metadata so partial history or lagging sync states are visible to users instead of implied as complete market-wide coverage. Current production scope is Ethereum for most tracked assets, with USDai tracked on native Arbitrum as its canonical issuance/redemption chain.

Operational freshness configuration is shared via worker/src/lib/mint-burn-health-config.ts:

  • major-symbol baseline (USDT, USDC, DAI, USDS, GHO, FRXUSD, BOLD, reUSD)
  • warning threshold (6h)
  • critical threshold (24h)

Scheduled/http handlers apply env overrides on top of these defaults (worker/src/handlers/scheduled.ts, worker/src/handlers/http.ts). Public /api/health now keys mint/burn freshness to the critical-lane sync timestamp / run status (the same semantics exposed by /api/mint-burn-flows) so quiet majors do not falsely mark the health surface stale just because no new events occurred.

Public /api/mint-burn-flows freshness metadata and the /flows page intentionally allow one missed 30-minute critical-lane slot before warning. User-facing freshness is fresh <= 60m, degraded <= 90m, stale > 90m, which keeps the public warning surface aligned with /status cron-health grace windows instead of flagging a single late slot as an incident.


Methodology Versioning

  • Current methodology version: v6.0
  • Public changelog page: /methodology/mint-burn-flow-changelog/
  • Internal reconstructed timeline: Mint/Burn Flow Methodology Timeline

Note: v6.0 went live on 2026-04-17 with bridge-mint tagging, LayerZero endpoint-only signal, canonical-chain gauge weighting, 0.5% roundtrip tolerance, config deferral, concurrent tx-context fetch, extended cron metadata, and migrations 0096/0097. Historical rows are reclassified progressively via the operator playbook (/api/reclassify-atomic-roundtrips?stablecoinId=<id> for partition-scoped reverse flips; /api/backfill-mint-burn for chunked bridge-mint replay).


Cron Schedule

  • Critical lane pattern: 4,34 * * * * (every 30 minutes, offset at :04/:34)
  • Extended lane pattern: 13,43 * * * * (every 30 minutes, offset at :13/:43 — 9 minutes after critical lane starts)
  • Trigger mode: isolated. sync-blacklist runs on its own dedicated 6-hourly trigger (3 */6 * * *); sync-dex-discovery runs on a dedicated 2-hourly trigger (6 */2 * * *).
  • Function: syncMintBurn(db, alchemyApiKey, { lane, jobName, ... })
  • Provider: Alchemy JSON-RPC
  • File: worker/src/cron/sync-mint-burn.ts
  • Registration: cron declared in worker/wrangler.toml, executed via worker/src/handlers/scheduled.ts
  • Returns: { itemCount, status, metadata } where itemCount = rowsInserted (not parsed rows). Metadata includes lane, jobName, nullPricesHealed, and per-config coverage-frontier diagnostics when scans are partial.
  • Operator notes: internal ingestion process notes are kept outside the public documentation archive.

Lane policy:

  • sync-mint-burn = critical lane. Uses the existing job id so freshness alerts and API freshness remain keyed to the major-symbol path.
  • sync-mint-burn-extended = extended lane. Uses its own mint_burn_run_state.job key and warning-only coverage semantics so long-tail backlog churn does not escalate the critical lane to error.

UI note: when /flows receives a mint/burn-specific sync.warning, it renders that targeted banner and suppresses the generic stale-data banner for the same query so users do not see duplicate amber warnings describing the same freshness condition. Cached fallback API responses now preserve only freshness-derived headers; a transient live-query failure no longer emits an extra generic Warning while the cached dataset is still inside the public 60-minute freshness window.


Constants & Thresholds

ConstantValuePurpose
dustThreshold10,000 default (token-native); 10 for gold tokensEvents below this amount are discarded
INDEXING_SAFETY_SEC900 (15 min)Safety margin when advancing sync state to chain head
ETHEREUM_BLOCK_TIME_SEC12 secApproximate Ethereum block time (yields ~75-block safety margin)
DENOM_SCALE0.3Pressure-shift denominator = 30% of baseline daily absolute flow
DENOM_FLOOR$1,000,000Minimum pressure-shift denominator
Z_MULTIPLIER50Z-score amplification in the pressure-shift formula
Pressure-shift clamp range-100 to +100Signed baseline-relative score output range
MIN_DATA_DAYS7Days of history required before pressure shift returns a value
MIN_ACTIVITY_USD50,00024h absolute flow below this returns NR pressure shift
FTQ_THRESHOLD$100,000,000Minimum net flow (both sides) to trigger flight-to-quality
MAX_SCAN_RANGE50KMax block range per contract per cycle
startBlockper-config (non-uniform)Each contract config has its own start block
Subrequest budget200 per cron runGlobal Alchemy API call budget
Per-config request cap60 critical / 25 extendedPrevents one hot config from consuming the full lane budget
Config tier policycritical / extendedCritical and extended lanes run on separate cron schedules; each config also has a per-config request cap

Contract Configurations

File: worker/src/lib/mint-burn-contracts.ts

Token identity now resolves from the shared stablecoin loader in shared/lib/stablecoins/index.ts, which validates the checked-in metadata assets under shared/data/stablecoins/*.json at module load. The mint/burn config file only keeps tracker-specific fields such as event signatures, startBlock, dustThreshold, tiering, and bridge-detection hints. The only explicit address overrides are the two reUSD vault-event configs, which intentionally track non-token contracts.

Tracked Stablecoins

Current scope: 132 contract configs across 131 stablecoin IDs (7 critical + 125 extended).

March 24, 2026 expansion: an additional 40 transfer-only configs were added for tracked assets that already had shared contract metadata but were not yet wired into the mint/burn registry. That wave initially included USDai on Ethereum, but canonical USDai issuance tracking now runs on native Arbitrum after LayerZero bridge-transfer filtering work. GYD was later removed when the asset moved to the cemetery. The broader active wave includes U, A7A5, USDA (Avalon), BRZ, KAG, satUSD, rwaUSDi, FPI, AEUR, USDQ, USDX, MIM, USA₮, ZeUSD, GGBR, XSGD, IDRT, TRYB, EURS, pUSD (Plume), USBD, DGLD, AxCNH, EURQ, GYEN, USDU Finance, ZARP, USDp, PHT, VCHF, USSD, CADC, VEUR, dUSD (dTRINITY), USDaf, EURAU, DUSD (Alto), and ebUSD.

SymbolIDDecimalsCategoryEvents
USDTusdt-tether6Safe havenTransfer + Issue/Redeem
USDCusdc-circle6Safe havenTransfer
FDUSDfdusd-first-digital18Safe havenTransfer
PYUSDpyusd-paypal6Safe havenTransfer
DAIdai-makerdao18RiskyTransfer
GHOgho-aave18RiskyTransfer
USDeusde-ethena18RiskyTransfer
USDSusds-sky18RiskyTransfer
FRXUSDfrxusd-frax18RiskyTransfer
BOLDbold-liquity18RiskyTransfer
fxUSDfxusd-f-x-protocol18ExtendedTransfer
crvUSDcrvusd-curve18ExtendedTransfer
AUSDausd-agora6ExtendedTransfer
ZCHFzchf-frankencoin18ExtendedTransfer
EURCeurc-circle6ExtendedTransfer
PAXGpaxg-paxos18ExtendedTransfer
XAUTxaut-tether6ExtendedTransfer
USDGusdg-paxos6ExtendedTransfer
USD1usd1-world-liberty-financial18ExtendedTransfer
USDfusdf-falcon18ExtendedTransfer
USYCusyc-hashnote6ExtendedTransfer
RLUSDrlusd-ripple18ExtendedTransfer
USDYusdy-ondo-finance18ExtendedTransfer
BUIDLbuidl-blackrock6ExtendedTransfer
USDDusdd-tron-dao-reserve18ExtendedTransfer
USDTBusdtb-ethena18ExtendedTransfer
Mm-m06ExtendedTransfer
USD0usd0-usual18ExtendedTransfer
TUSDtusd-trueusd18ExtendedTransfer
CUSDcusd-cap18ExtendedTransfer
USRusr-resolv18ExtendedTransfer
FRAXfrax-frax18ExtendedTransfer
DOLAdola-inverse-finance18ExtendedTransfer
IUSDiusd-infinifi18ExtendedTransfer
GUSDgusd-gate6ExtendedTransfer
avUSDavusd-avant18ExtendedTransfer
pmUSDpmusd-precious-metals18ExtendedTransfer
USDzusdz-anzen18ExtendedTransfer
MNEEmnee-mnee18ExtendedTransfer
TBILLtbill-openeden6ExtendedTransfer
USDOusdo-openeden18ExtendedTransfer
EURCVeurcv-societe-generale-forge18ExtendedTransfer
REUSDreusd-resupply18ExtendedTransfer
EURIeuri-banking-circle18ExtendedTransfer
GUSDgusd-gemini2ExtendedTransfer
USDPusdp-paxos18ExtendedTransfer
XUSDxusd-straitsx6ExtendedTransfer
MUSDmusd-metamask6ExtendedTransfer
YUSDyusd-aegis18ExtendedTransfer
SUSDsusd-synthetix18ExtendedTransfer
LUSDlusd-liquity18ExtendedTransfer
USDCVusdcv-societe-generale-forge18ExtendedTransfer
EUREeure-monerium18ExtendedTransfer
USNusn-noon18ExtendedTransfer
EUSDeusd-electronic-usd18ExtendedTransfer
meUSDmeusd-mezo18ExtendedTransfer
MSUSDmsusd-metronome18ExtendedTransfer
NUSDnusd-neutrl18ExtendedTransfer
ALUSDalusd-alchemix18ExtendedTransfer
FIDDfidd-fidelity18ExtendedTransfer
MSUSDmsusd-main-street18ExtendedTransfer
WUSDwusd-worldwide18ExtendedTransfer
SBCsbc-brale18ExtendedTransfer
OUSDousd-origin-protocol18ExtendedTransfer
USPusp-pikudao18ExtendedTransfer
USDRusdr-stablr6ExtendedTransfer
USTBustb-superstate6ExtendedTransfer
OUSGousg-ondo-finance18ExtendedTransfer
mTBILLmtbill-midas18ExtendedTransfer
wsrUSDwsrusd-reservoir18ExtendedTransfer
AUDDaudd-novatti6ExtendedTransfer
JPYCjpyc-jpyc18ExtendedTransfer
XAUmxaum-matrixdock18ExtendedTransfer
EURReurr-stablr6ExtendedTransfer
EUROPeurop-schuman6ExtendedTransfer
DEUROdeuro-deuro18ExtendedTransfer
tGBPtgbp-tokenised18ExtendedTransfer
syrupUSDCsyrupusdc-maple6ExtendedTransfer
syrupUSDTsyrupusdt-maple6ExtendedTransfer
AIDaid-gaib18ExtendedTransfer
apxUSDapxusd-apyx18ExtendedTransfer
reUSDreusd-re-protocol18RiskyDeposited + InstantRedemptionProcessed (2 configs, Ethereum)

Public /api/mint-burn-flows and the daily digest collector now use the same report-card-cache driven FTQ classification. Coins with report-card score >= 65 are treated as safe, scores < 50 are treated as risky, and the middle band is ignored for FTQ. When report_card_cache is missing, stale, or malformed, FTQ classification is treated as unavailable instead of silently falling back to a hardcoded safe-haven list.

Per-config adapter provenance is now surfaced through coin coverage metadata:

  • adapterKinds — active decoding families for the coin (transfer-zero-address, custom-events, mixed)
  • startBlockSource — whether the earliest tracked block is a reviewed contract-specific bound or a blanket default coverage floor
  • startBlockConfidence — qualitative confidence on historical completeness (high, medium, low)

Current rule: the March 24 long-tail transfer wave that inherited the blanket 21_900_000 Ethereum floor is labeled startBlockSource = default-coverage-floor-2026-03-24 and startBlockConfidence = low, so the public API no longer implies contract-specific historical certainty where none exists.

Events are also classified by flow_type (standard, bridge_transfer, or atomic_roundtrip) so non-economic bridge transfers and same-tx roundtrip noise stay out of aggregate flow metrics.

Event Detection

Standard mint/burn: ERC-20 Transfer(address,address,uint256) events filtered by zero address.

  • Mint: topics[1] (from) = zero address
  • Burn: topics[2] (to) = zero address

USDT Ethereum special handling: The USDT contract uses custom Issue(uint256) and Redeem(uint256) events for treasury operations (issue() does NOT emit Transfer). These are tracked in addition to Transfer events.

EventTopic HashAmount Encoding
Transfer(address,address,uint256)0xddf252ad...transfer-value (data field)
Issue(uint256)0xcb8241ad...first-data-uint256
Redeem(uint256)0x702d5967...first-data-uint256

reUSD special handling: Re Protocol vault contracts emit custom deposit/redeem events instead of standard mint/burn Transfers. Deposits are decoded from Deposited(address,address,uint256) (dataSlot=2, 18-decimal amount), and burns from InstantRedemptionProcessed(address,uint256,uint256) (first-data-uint256, 18-decimal shares burned).

Custom counterparty encoding. For events whose relevant address is not in a standard topic slot, MintBurnEventDef now exposes an optional counterpartyEncoding override:

  • { source: "topic", index } — read log.topics[index] (must be ≥1).
  • { source: "data", slot } — read a 32-byte word from log.data at slot * 32 and take the low-20 bytes as the address. Implemented via the new readDataWord helper in worker/src/lib/evm-logs.ts.

When omitted, the default is the Transfer convention: mint → topics[2] (recipient), burn → topics[1] (sender).

reUSD's Deposited(address user, address token, uint256 amount) event has all three params unindexed, so its counterparty previously resolved to null. The mint config now sets counterpartyEncoding: { source: "data", slot: 0 } to correctly populate the depositor address.


Sync Algorithm

  1. Load sync state — batch query mint_burn_sync_state for all lane-selected contract keys. Falls back to startBlock - 1 for new configs.
  2. Apply runtime policy — filter disabled configs (MINT_BURN_DISABLED_IDS, MINT_BURN_DISABLED_SYMBOLS), select the requested lane (critical, extended, or all), rotate start index from the lane-specific mint_burn_run_state.job, front-load critical configs inside mixed/all runs, and assign a per-config request cap inside the global budget.
  3. Skip deferred configs — load active deferrals from mint_burn_config_deferral (rows with deferred_until > now) and remove them from the run. A config is deferred for a 1-hour grace period when it exits a run with apiErrors > 5 AND coverage < 0.8, so chronically failing configs cannot starve healthy ones of subrequest budget.
  4. Get chain head — Alchemy eth_blockNumber call per chain (cached per chain ID).
  5. Load price cache — query price_cache for all tracked stablecoin IDs (used for USD conversion).
  6. For each contract config:
    • Skip if fromBlock > chainHead or the lane/global budget is exhausted.
    • For each event definition, call Alchemy eth_getLogs with adaptive recursive block-range splitting on provider/range failures.
    • Enforce the per-config request cap while fetching logs, resolving timestamps, and classifying bridge activity so a single config cannot monopolize the lane.
    • Resolve block timestamps — batch eth_getBlockByNumber for all unique blocks in the returned logs, using local + persistent (block_timestamp_cache) caches.
    • Parse logs per event definition: decode amount (respecting decimals), derive counterparty address, compute amount_usd = amount * price (null if no price), and initialize flow_type='standard'.
    • Resolve transaction-context receipts for candidate bridge rows with bounded concurrency (limit=4 via runWithConcurrencyLimit) instead of a serial loop. This keeps 2 connections of headroom in Cloudflare's per-trigger 6-connection pool while still drastically speeding up classification for configs with many unique tx hashes.
    • Classify bridge transfers after all parsed rows for the config chunk are assembled so bridge-related mints and burns can be tagged together while still sharing the same transaction-context budget.
    • Detect atomic roundtrips after all event definitions for the config are parsed: group rows by (tx_hash, stablecoin_id) and flip the whole group to flow_type='atomic_roundtrip' when both mint and burn directions appear in the same transaction and their totals match within ROUNDTRIP_AMOUNT_TOLERANCE (0.5%). Rows with an empty tx_hash are defensively skipped.
    • Filter out dust events (amount < dustThreshold).
    • Batch INSERT OR IGNORE into mint_burn_events, track parsed vs inserted counts from D1 meta.changes.
    • Update mint_burn_sync_state.last_block:
      • If every event definition completed and timestamps are fully resolved:
        • If events found: advance to maxBlockSeen.
        • If no events: advance to chainHead - safetyMarginBlocks (avoids skipping not-yet-indexed events).
      • If any event definition was partial or any block timestamps were unresolved: advance only to the shared safe coverage frontier (min(scannedToBlock, earliestMissingTimestamp-1)).
      • If no safe frontier exists for the config in that run: do not advance.
  7. Recalculate affected hourly buckets — for each unique (stablecoinId, chainId, hourTs) touched, INSERT OR REPLACE into mint_burn_hourly by re-aggregating from mint_burn_events, counting only flow_type='standard' rows so bridge transfers and atomic roundtrips do not leak into flow statistics.
    • Recalc runs inside a finally block so it still fires after partial-run failures. If the recalc itself throws, the critical lane downgrades status=ok to status=degraded and surfaces recalcFailed: true plus recalcError: <message> in cron metadata (previously failures were only logged silently).
  8. Auto-heal recent NULL prices — on non-error runs, query up to 500 events with amount_usd IS NULL in the last 48 hours, resolve from price_cache, update amount_usd/price_* with price_source=price_cache_heal, and re-aggregate only newly affected hourly buckets.
    • Cron metadata now includes both nullPricesHealed and nullPriceBacklog (recent, historical) so operators can distinguish live healable gaps from older debt.
  9. Emit active progress — long runs call the shared cron reportProgress(...) hook so /api/status can surface the active stage, queue position, and budget heartbeat while the lease is still live.
  10. Escalate degraded runs — the critical lane emits status=degraded|error when sustained coverage/API thresholds are breached, with streak tracking in mint_burn_run_state. The extended lane keeps the same observability metadata but does not escalate long-tail backlog pressure to error.
  11. Sweep cross-run roundtrips — on non-error runs, query up to 200 (tx_hash, stablecoin_id) groups within the last 48 hours where both mint and burn directions exist but flow_type = 'standard'. Reclassify to atomic_roundtrip and re-aggregate affected hourly buckets. This catches roundtrips where the mint and burn were ingested in separate cron runs. The HAVING clause mirrors ROUNDTRIP_AMOUNT_TOLERANCE from the in-memory detector so partial same-tx groups (e.g. mint 100 / burn 50) are not mis-tagged as atomic roundtrips.
  12. Invalidate flow API caches — on successful runs (status ∈ {ok, degraded}), purge mint-burn-flows:* rows from the shared cache table using a PK-range predicate (key >= 'mint-burn-flows:' AND key < 'mint-burn-flows:\uffff'). This drops stale pre-sync aggregate payloads so the next /api/mint-burn-flows request rebuilds against the freshly written buckets.

Counterparty resolution: For mints, topics[2] (recipient). For burns, topics[1] (sender).

Event ID format: "{chainId}-{txHash}-{logIndex}" — deterministic, prevents duplicates via INSERT OR IGNORE.


Shared Ingestion Pipeline Boundaries

Cron (sync-mint-burn) and admin backfill (backfill-mint-burn) now share a single ingestion pipeline under worker/src/lib/mint-burn-pipeline/.

ModuleResponsibility
types.tsShared ingestion row/context/counter types and sync-state mode union
parse.tsparseMintBurnLogs() and event-level price resolution (supply-history then price_cache fallback)
roundtrip-detection.tsSame-transaction (tx_hash, stablecoin_id) atomic roundtrip detection for flow_type tagging
classification.tsBridge-aware burn classification and transaction-context loading
context.tsShared loaders for current prices and historical price series
persistence.tsINSERT OR IGNORE event writes, burn classification updates, affected-hour aggregation
price-heal.tsAuto-heal recent NULL-price rows from price_cache and return affected hours
roundtrip-sweep.tsPost-cron sweep for cross-run atomic roundtrip detection (48h window, 200 limit)
sync-state.tsSync-state key helpers plus mode-specific upserts (replace for cron, monotonic-max for backfill)

Implementation invariant: worker/src/api/backfill-mint-burn.ts does not import from worker/src/cron/sync-mint-burn.ts; both entrypoints import shared helpers from mint-burn-pipeline/*.

mint_burn_events.flow_type is orthogonal to burn_type: burn_type still classifies burns as economic vs bridge/review, while flow_type applies to both mints and burns and now marks tx-level bridge noise as bridge_transfer plus same-transaction mint+burn noise as atomic_roundtrip.

Cron metadata includes atomicRoundtripsDetected, an observability counter for how many rows were tagged during the run.

Bridge Classifier

Dispatch and per-protocol fingerprint logic now live in three co-operating modules instead of one monolith:

ModuleResponsibility
worker/src/lib/mint-burn-bridge-classifier.tsDispatcher: normalizes tx context, walks per-row fingerprints, and writes flow_type = 'bridge_transfer' plus (for burns) burn_type = 'bridge_burn'
worker/src/lib/mint-burn-bridge-classifier-protocols.tsPer-protocol helpers: CCIP/CCTP router matching, LayerZero OFT signal fingerprints, generic pool/router address heuristics
worker/src/lib/mint-burn-bridge-classifier-types.tsLeaf module with shared types (breaks the classifier ↔ protocols import cycle)

Key behavior changes forward-going:

  • Mint-side tagging for CCIP/CCTP. The classifier now tags bridge mints (not just burns) as flow_type='bridge_transfer'. Affects USDO, USD1, avUSD, ZCHF (CCIP) and USDC, EURC (CCTP). Previously only the burn side was filtered, so counted flow aggregates double-counted cross-chain hops.
  • LayerZero endpoint-only signal. The OFT/OAdapter path now accepts a third fingerprint (fingerprintC) that fires when the transaction context contains both a known LayerZero endpoint topic and an expected emitter address, even without the classic pool-address match (hasSignalTopic && hasExpectedEmitter && signalEmitterSet.size > 0). This catches LayerZero-Executor-only mints that previously slipped through. Tradeoff: known risk of shared-endpoint false positives is accepted to eliminate the prior false-negative backlog.
  • No more bridge-signal-with-unknown-pool review path. Rows that touch a recognized bridge-signal topic/emitter but not a tracked pool address now tag as bridge_transfer instead of flowing to a review queue. Policy: if a transaction carries a bridge signal, treat every mint/burn in it as bridge noise.
  • Load-time validation of bridge-detection configs. validateMintBurnBridgeDetection runs against every bridgeDetection config at module load. Address fields must match ADDRESS_RE, topics must match TOPIC_RE, and selectors must match SELECTOR_RE. Today the validator runs in audit-and-log mode (console.error(...) without aborting) so an unexpectedly malformed config cannot wedge the worker. Once two clean cron cycles confirm the log is silent, a follow-up commit will escalate to throw-on-error at module load and per-config resolution.

Atomic Roundtrip Detection

Same-transaction mint+burn pairs for one stablecoin are tagged flow_type='atomic_roundtrip' in two places: in-memory during ingestion (per config chunk) and via the post-run sweep (cross-run, 48h window). Both paths now share the same ROUNDTRIP_AMOUNT_TOLERANCE = 0.005 (0.5%) rule:

  • A group tags atomic only when |sum(mint) - sum(burn)| ≤ 0.005 × max(mintSum, burnSum).
  • Partial same-tx groups (e.g. mint 100 / burn 50) are no longer flagged atomic_roundtrip and stay as standard flow.
  • Rows with an empty tx_hash are defensively skipped by the in-memory detector.
  • The sweep SQL mirrors the tolerance via HAVING ... ABS(mint_amt - burn_amt) > 0.005 * (CASE WHEN mint_amt >= burn_amt THEN mint_amt ELSE burn_amt END) (SQLite has no two-arg MAX, so the explicit CASE is intentional).
  • A drift-guard unit test asserts ROUNDTRIP_AMOUNT_TOLERANCE === 0.005 so changes to the TS constant surface against the SQL literals that mirror it.

Scoring

File: worker/src/lib/mint-burn-scoring.ts

Pressure Shift vs 30D (Flow Intensity Formula)

The underlying scoring formula is unchanged, but the product now exposes it as the baseline-relative Pressure Shift vs 30D signal. Runs server-side in the /api/mint-burn-flows aggregate handler.

denominator = max(baselineDailyAbs * 0.3, $1M)
z = (currentDailyNet - baselineDailyNet) / denominator
pressureShift = clamp(-100, 100, z * 50)

Activity gate: If the coin's 24h absolute flow (mint volume + burn volume) is below MIN_ACTIVITY_USD ($50,000), pressure shift returns null (NR). This prevents misleading scores for dormant or low-activity coins.

  • Input: 24h net flow, 24h absolute flow (|mint| + |burn|), trailing 30 fully closed daily average net flow, trailing 30 fully closed daily average absolute flow, data age in days.

  • Output: -100 to +100 score, or null (NR) if fewer than 7 days of history, if 24h absolute flow is below $50,000, or if the coin has no 24h mint/burn activity.

  • Score of 0 = current flow matches baseline. Negative values = pressure is worse than baseline. Positive values = pressure is improving versus baseline.

Two-Signal Interpretation Model

Per-coin UI and API now answer two different questions explicitly:

  1. Net Flow 24h — current direction and magnitude from raw mint-minus-burn totals
    • minting: netFlow24hUsd > 0
    • burning: netFlow24hUsd < 0
    • flat: netFlow24hUsd = 0 with activity
    • inactive: no 24h activity
  2. Pressure Shift vs 30D — how unusual current pressure is versus the coin's own baseline
    • improving: score > 10 (strictly greater; score of exactly 10 is stable)
    • stable: score between -10 and +10 (inclusive on both boundaries)
    • worsening: score < -10 (strictly less; score of exactly -10 is stable)
    • nr: insufficient history or no current activity

Invariant: minting vs burning semantics now always come from raw net flow, never from score sign.

Shared Signal Helper

shared/lib/mint-burn-signals.ts centralizes interpretation logic used by worker responses and frontend fallbacks:

  • getNetFlowDirection24h()
  • getPressureShiftState()
  • getCoinFlowCompositeState()

Gauge Bands

BandRangeColorMeaning
CRISIS-100 to -70redMassive redemption pressure
STRESS-70 to -40orangeHeavy redemptions
CAUTIOUS-40 to -10amberElevated burns
NEUTRAL-10 to +10grayBalanced mint/burn
HEALTHY+10 to +40light-greenNet minting
CONFIDENT+40 to +70greenStrong demand
SURGE+70 to +100bright-greenExtreme minting demand

Boundary convention: each band is [min, max). The last band includes +100.

Bank Run Gauge (Composite)

Market-cap-weighted average of individual pressure-shift scores:

gauge_score = Σ(intensity_i * mcap_i) / Σ(mcap_i)
  • Skips coins with null intensity (insufficient data or NR no-activity window).
  • Returns null only when ALL tracked coins lack valid intensity.

Mcap weighting — tracked-chain scope. Each coin's weight is now its canonical tracked-chain circulating supply, not its global peg-bucket total. A coin is only scored against chains where we actually ingest mint/burn events, so omnichain tokens don't over-contribute via supply we don't observe.

Implementation (worker/src/lib/mint-burn-mcap-weighting.ts):

  • getMintBurnTrackedChains(stablecoinId) derives the active chainId set from MINT_BURN_CONFIGS.
  • sumMcapForTrackedChains(stablecoinId, chainCirculating, circulating) sums chainCirculating[chainId].current over those tracked chains, after canonicalizeChainCirculating(...) normalizes DefiLlama's capitalized keys (e.g. Ethereum) to canonical chain IDs.
  • Fallback policy (preserves legacy behavior where per-chain data isn't available):
    1. Coin with no tracked chains → sumPegBuckets(circulating).
    2. Canonicalized chainCirculating is empty → sumPegBuckets(circulating) (keeps CG-fallback assets alive).
    3. No tracked chain has an entry in the canonicalized map → sumPegBuckets(circulating).
    4. Otherwise sum current across tracked chains. current = 0 is treated as real data (zero supply) and does not trigger fallback.

Flight-to-Quality Detection

Detects simultaneous outflows from risky stablecoins and inflows to safe havens.

  • Activation: riskyNet24h < -$100M AND safeNet24h > +$100M
  • Intensity: min(100, |riskyNet24h| / $1B * 100)
  • Safe/risky cohorts come from the report-card cache: Safety Score >=65 is safe, <50 is risky, and 50–64 is ignored. If the report-card cache is unavailable, flight-to-quality classification is unavailable rather than falling back to hardcoded safe havens.

Database Schema

mint_burn_events (migration 0031)

CREATE TABLE mint_burn_events (
  id TEXT PRIMARY KEY,                 -- "{chainId}-{txHash}-{logIndex}"
  stablecoin_id TEXT NOT NULL,
  symbol TEXT NOT NULL,
  chain_id TEXT NOT NULL,
  direction TEXT NOT NULL,             -- "mint" or "burn"
  amount REAL NOT NULL,                -- Token-native amount
  amount_usd REAL,                     -- NULL if price unavailable at sync time
  price_used REAL,                     -- Price at resolution time
  price_timestamp INTEGER,             -- When the price was sourced (cache update time), NOT the event's block timestamp
  price_source TEXT,                   -- "supply-history-daily", "price-cache-current", "price_cache_heal", "backfill-supply-history-daily", or "backfill-derived-amount-usd"
  flow_type TEXT DEFAULT 'standard',   -- "standard", "bridge_transfer", or "atomic_roundtrip"
  counterparty TEXT,                   -- Address that received/sent tokens
  tx_hash TEXT NOT NULL,
  block_number INTEGER NOT NULL,
  timestamp INTEGER NOT NULL,          -- Unix seconds
  explorer_tx_url TEXT NOT NULL
);

CREATE INDEX idx_mbe2_ts ON mint_burn_events(timestamp DESC);
CREATE INDEX idx_mbe2_coin ON mint_burn_events(stablecoin_id, timestamp DESC);
CREATE INDEX idx_mbe2_chain ON mint_burn_events(chain_id, timestamp DESC);
CREATE INDEX idx_mbe_null_price_ts ON mint_burn_events(timestamp DESC) WHERE amount_usd IS NULL;

-- migration 0097: composite index to speed roundtrip sweep and future flow_type-filtered queries
CREATE INDEX idx_mbe_flow_type_ts ON mint_burn_events(flow_type, timestamp DESC);

mint_burn_hourly (migration 0031)

Pre-aggregated hourly flow buckets. Written by cron after each scan; also recalculated by the backfill admin endpoint.

CREATE TABLE mint_burn_hourly (
  stablecoin_id TEXT NOT NULL,
  chain_id TEXT NOT NULL,
  hour_ts INTEGER NOT NULL,            -- Unix seconds, truncated to hour: (timestamp / 3600) * 3600
  mint_count INTEGER NOT NULL DEFAULT 0,
  burn_count INTEGER NOT NULL DEFAULT 0,
  mint_volume_usd REAL NOT NULL DEFAULT 0,
  burn_volume_usd REAL NOT NULL DEFAULT 0,
  net_flow_usd REAL NOT NULL DEFAULT 0, -- mint_volume - burn_volume (positive = net mint)
  PRIMARY KEY (stablecoin_id, chain_id, hour_ts)
);

CREATE INDEX idx_mbh_ts ON mint_burn_hourly(hour_ts DESC);
CREATE INDEX idx_mbh_coin ON mint_burn_hourly(stablecoin_id, hour_ts DESC);

mint_burn_sync_state (migration 0031)

Incremental block tracking (same pattern as blacklist_sync_state).

CREATE TABLE mint_burn_sync_state (
  config_key TEXT PRIMARY KEY,         -- "{chainId}-{contractAddress}"
  last_block INTEGER NOT NULL DEFAULT 0
);

mint_burn_config_deferral (migration 0096)

Per-config deferral log. Configs that exit a run with apiErrors > 5 AND coverage < 0.8 are inserted here with deferred_until = now + 3600s. The next cron run skips any config whose deferred_until is still in the future, so chronically failing configs cannot starve healthy ones of subrequest budget.

CREATE TABLE mint_burn_config_deferral (
  config_key TEXT PRIMARY KEY,
  deferred_until INTEGER NOT NULL,
  reason TEXT NOT NULL,
  api_errors INTEGER NOT NULL DEFAULT 0,
  coverage REAL,
  created_at INTEGER NOT NULL
);

CREATE INDEX idx_mbcd_until ON mint_burn_config_deferral(deferred_until);

Migration history: Initial schema in 0019 was dropped in 0020. Current schema is v2 (migration 0031). Migration 0096 adds mint_burn_config_deferral; migration 0097 adds the (flow_type, timestamp) composite index on mint_burn_events.


API Endpoints

GET /api/mint-burn-flows

Two modes depending on whether stablecoin is provided.

Aggregate mode (no stablecoin param):

ParamTypeDefaultDescription
hoursint24Time window, 1–720 (up to 30 days)

Returns:

  • gauge — composite Bank Run Gauge: { score, band, flightToQuality, flightIntensity, trackedCoins, trackedMcapUsd }
  • coins[] — per-coin summaries: fixed 24h raw net flow, canonical pressureShiftScore, derived interpretation fields, baseline context, coverage metadata, and largest event
  • hourly[] — aggregate hourly timeseries: { hourTs, netFlowUsd, mintVolumeUsd, burnVolumeUsd }
  • updatedAt — Unix seconds of latest hourly bucket
  • windowHours — requested chart window for hourly[]
  • scope — current ingestion scope (for example Configured issuance chains, or Arbitrum-only on per-coin USDai views)
  • sync — latest critical-lane freshness and warning state

Per-coin mode (stablecoin param provided):

ParamTypeDefaultDescription
stablecoinstringStablecoin ID (required)
hoursint24Time window, 1–720

Returns:

  • stablecoinId, symbol
  • mintVolumeUsd, burnVolumeUsd, netFlowUsd, mintCount, burnCount
  • chains[] — per-chain breakdown
  • hourly[] — hourly timeseries
  • updatedAt
  • windowHours, scope, sync

Returns 404 if the stablecoin ID is not in the tracked set.

Contract note: aggregate hours only changes hourly[]. Coin-level netFlow24hUsd, mint/burn 24h volumes, counts, and pressure state remain fixed to the canonical 24-hour window.

Cache: CACHE_PROFILES.standard (~30-minute freshness keyed to successful critical-lane syncs)

GET /api/mint-burn-events

Paginated event feed for a single stablecoin.

ParamTypeDefaultDescription
stablecoinstringStablecoin ID (required)
directionstringFilter: "mint" or "burn"
chainstringFilter by chain ID within the stablecoin's tracked issuance scope (for example ethereum for most coins, arbitrum for USDai)
burnTypestringBurn-only filter: "effective_burn", "bridge_burn", or "review_required"
scopestring"all""all" returns the classified raw event stream; "counted" returns only rows that contribute to economic-flow aggregates (flow_type='standard' and mint/effective-burn semantics)
minAmountnumberMinimum USD amount; rows with amount_usd IS NULL are excluded when this filter is used
limitint50Page size, 1–500
offsetint0Pagination offset

Returns: { events[], total }. Events sorted by timestamp DESC.

Each event row includes valuation provenance fields (priceUsed, priceTimestamp, priceSource), flowType, plus burn classification fields (burnType, burnReviewReason).

Product note: stablecoin detail-page "Mint & Burn Flow History" uses the counted view so bridge transfers, review-required burns, and atomic roundtrips do not appear as ordinary economic flow.

Cache: CACHE_PROFILES.realtime (~900s freshness with 15-min staleness window)

POST /api/backfill-mint-burn-prices (admin)

Repairs incomplete mint/burn valuation metadata for historical rows. Requires Access service-token headers on ops-api.pharos.watch.

Note: cron now auto-heals recent NULL-price events (48h lookback). This endpoint remains the operator tool for broader historical backfills.

  1. Finds all mint_burn_events rows with incomplete valuation or audit fields (amount_usd, price_used, price_timestamp, price_source).
  2. For rows with amount_usd IS NULL, tries to value them from event-day supply_history prices.
  3. For rows that already have amount_usd, derives audit fields conservatively where possible without rewriting historical USD valuation from current spot prices.
  4. Rebuilds mint_burn_hourly only for coins whose amount_usd actually changed.

Important constraint: this endpoint no longer bulk-fills historical amount_usd from the current price_cache snapshot. Rows without a time-appropriate historical price remain unresolved and are reported as still unpriced.

Returns: { totalUpdated, rowsValued, rowsAudited, rowsStillUnpriced, rowsStillMissingAudit, coins: [{ id, updated, valued, audited, stillUnpriced, stillMissingAudit }] }

POST /api/backfill-mint-burn (admin)

Controlled ingestion backfill by explicit config/range/chunk, or by automatic config selection when configKey is omitted.

  • Auth: Access service-token headers
  • Idempotency: Idempotency-Key supported via admin idempotency middleware
  • Parameters: configKey, fromBlock, toBlock, chunkSize, maxChunks
  • Behavior:
    • If configKey is omitted, the worker auto-selects one tracked config using a critical-first / major-symbol-first / most-behind ordering and returns selectionMode="auto" plus the chosen configKey.
    • Uses the same shared parse/classification/context/persistence helpers as cron ingestion.
    • Advances mint_burn_sync_state with monotonic max semantics (never regresses on partial backfills).
    • Returns done=false with nextFromBlock when additional calls are needed.
  • Response includes a reclassified object exposing reclassification deltas for observability:
    • reclassified.flowTypeChanges — rows where flow_type flipped during this call (e.g. standard → bridge_transfer once the classifier saw a previously-unknown bridge signal).
    • reclassified.burnTypeChanges — rows where burn_type flipped (e.g. review_required → bridge_burn).
    • rowsReclassified — legacy scalar retained for backward compatibility; the exact count of unique rows whose classification columns were rewritten during this chunk. Prefer the nested reclassified.* fields for per-column accounting.

POST /api/reclassify-atomic-roundtrips (admin)

Retroactive cleanup endpoint for historical rows that predate shared roundtrip detection, were ingested before both sides of a transaction were visible to the detector, or were tagged atomic before the 0.5% amount-tolerance rule shipped.

  • Auth: Access service-token headers
  • Idempotency: Idempotency-Key supported via admin idempotency middleware
  • Behavior:
    • Forward pass — scans up to 1000 (tx_hash, stablecoin_id) groups per call where flow_type='standard' but both mint and burn directions exist, and flips all matching rows in each group to flow_type='atomic_roundtrip'.
    • Reverse pass (new). Scans up to 1000 groups currently tagged flow_type='atomic_roundtrip' that fail the 0.5% tolerance (|mint_amt - burn_amt| > 0.005 × max(mint_amt, burn_amt)) and flips them back to flow_type='standard'. The SQL mirrors ROUNDTRIP_AMOUNT_TOLERANCE in worker/src/lib/mint-burn-pipeline/roundtrip-detection.ts.
    • Recalculates the affected hourly buckets so downstream flow aggregates pick up both directions of reclassification immediately.
    • Returns done=true only when BOTH forward and reverse passes returned fewer than BATCH_SIZE groups.
  • Response fields:
    • toRoundtrip — forward-pass count (standard → atomic_roundtrip).
    • toStandard — reverse-pass count (atomic_roundtrip → standard).
    • updated — legacy scalar kept for backward compat: toRoundtrip + toStandard.
    • hoursRecalculated — number of distinct (stablecoinId, chainId, hourTs) buckets re-aggregated.
    • batchSize, done.

Cron Metadata Fields

syncMintBurn(...) stores a JSON metadata blob on every run row in cron_runs. Operators consume it via /api/status and the merge-gate metadata checks. Current notable fields:

FieldTypeMeaning
lane"critical" | "extended"Which lane produced this run
jobNamestringLane-specific run-state job key
rowsRead, rowsParsed, rowsInserted, rowsIgnored, rowsDroppednumberIngestion throughput counters
sourceCoverageobjectcontractsProcessed, contractsSkipped, contractsEnabled, contractsDisabled, contractsTotal
configBreakdown[], laggingConfigs[]arraysPer-config diagnostics
apiErrors, fallbackMode, validationFailuresmixedProvider-error observability
atomicRoundtripsDetectednumberRows tagged in-memory this run
nullPricesHealednumberRows auto-valued from price_cache this run (48h window)
degradedSignal, degradedStreak, coverageRatiomixedCritical-lane health signals
recalcFailedbooleantrue when recalcAffectedHours threw during the run's finally block; critical lane downgrades ok → degraded when this is set
recalcErrorstring (optional)Error message captured from the failed recalc call
nullPriceBacklogRecentnumberCount of amount_usd IS NULL rows inside the 48h auto-heal window still awaiting price resolution
nullPriceBacklogHistoricalnumberCount of amount_usd IS NULL rows older than the auto-heal window (debt that backfill-mint-burn-prices must address)
roundtripsBacklogSaturatedbooleantrue when the cross-run roundtrip sweep hit its per-run limit and more candidate groups likely remain in the 48h window
budgetUsednumberAlchemy subrequests consumed by this run (emitted via withBudgetMetadata)
budgetLimitnumberGlobal subrequest budget for the run (default 200)

Frontend

Page

Route: /flows File: src/app/flows/page.tsx Layout: src/app/flows/layout.tsx

Three sections:

  1. Hero Overview — net-direction hero with the baseline-relative Bank Run Gauge, a literal 24h Minting Pressure gauge, and flight-to-quality badge. Headline copy is derived from aggregate Net Flow 24h direction plus the Bank Run Gauge pressure state; it does not imply cross-asset breadth unless a separate breadth signal is added.
  2. Per-Coin Flows — sortable table with Pressure vs 30D, net 24h/7d, mint/burn volumes, largest event
  3. Aggregate Flows — Recharts composed chart (mint area, burn area, net flow line) with 24h/7d/30d toggle

Hooks

File: src/hooks/use-mint-burn-flows.ts

HookEndpointStale TimeNotes
useMintBurnFlows(hours?)/api/mint-burn-flowsCRON_MINT_BURNAggregate mode, no coin filter
useMintBurnFlowsCoin(id, hours?)/api/mint-burn-flows?stablecoin=CRON_MINT_BURNPer-coin mode, enabled only when ID truthy
useMintBurnEvents(id, opts?)/api/mint-burn-events?stablecoin=CRON_MINT_BURNPaginated event feed

All hooks use Zod schema validation for aggregate and per-coin responses (MintBurnFlowsResponseSchema, MintBurnPerCoinResponseSchema).

Components

ComponentFileDescription
FlowBrrrOverviewsrc/components/flow-brrr-overview.tsxShared overview shell used by /flows and the homepage snapshot; renders the Bank Run Gauge band returned by the API plus the literal 24h minting-pressure gauge, with inline methodology help on the Bank Run Gauge label
FlowChartsrc/components/flow-chart.tsxRecharts composed chart: mint (green area), burn (red area), net flow (blue line), hourly tooltip
FlowTablesrc/components/flow-table.tsxSortable per-coin table. Sort keys: net24h, mint24h, burn24h, net7d, largest, pressure. Responsive column hiding; Pressure vs 30D header uses the shared methodology-hint trigger
FlowEventFeedsrc/components/flow-event-feed.tsxPaginated event table: time, direction badge, amount USD, chain, tx link
MintingPressureGaugesrc/components/minting-pressure-gauge.tsxShared literal 24h mint-vs-burn gauge used by both the aggregate overview and stablecoin detail summary cards
HomepageFlowOverviewsrc/components/homepage-flow-overview.tsxHomepage snapshot wrapper: pulls 24h/7d aggregate flow data and renders the same net-direction hero used on /flows, including a headline keyed from aggregate net direction plus Bank Run Gauge pressure state, the Bank Run Gauge (pressure vs 30D), and the literal 24h Minting Pressure gauge
FlowSummaryCardsrc/components/flow-summary-card.tsxSummary card for stablecoin detail pages: explicit Net 24h, Pressure Shift vs 30D, and a literal Minting Pressure (24h) gauge, plus contextual methodology hints / footer links for the flow model

Dashboard Integration

FlowSummaryCard (src/components/flow-summary-card.tsx) now keys machine visuals from raw netFlow24hUsd and also renders the same literal Minting Pressure (24h) gauge used in the aggregate overview, while Bank Run Gauge band labels remain available for baseline-relative pressure semantics.


Error Handling & Edge Cases

ConditionBehavior
Price unavailable at sync timeamount_usd stored as NULL initially; cron auto-heals recent rows (48h window) and admin endpoint handles older history
Fewer than 7 days of flow historyPressure shift returns null; coin excluded from gauge weighting
No 24h mint/burn activity in a sparse windowPressure shift returns null (NR) for that window; coin excluded from gauge weighting
All coins have null pressure shiftGauge score returns null; frontend shows "Calibrating" state
Alchemy API error for a configapiErrors incremented; sync state NOT advanced (retried next cycle)
Incomplete timestamp resolutionconfigError = true; sync state not advanced, retried next cycle
Subrequest budget exhaustedRemaining configs skipped; picked up in next cron cycle
Block explorer indexing lag75-block safety margin prevents advancing past un-indexed blocks
Duplicate eventsINSERT OR IGNORE on deterministic id key prevents duplicates
Unknown stablecoin ID in per-coin APIReturns 404 with descriptive error
Missing stablecoin param in events APIReturns 400
Partial-coverage cron runHourly aggregation rebuilds from all DB events for affected hours; buckets may be temporarily incomplete for configs still catching up

Circuit Breaker Separation

Blacklist and mint/burn have independent circuit breakers:

  • CIRCUIT_SOURCE.ETHERSCAN — used by sync-blacklist (Etherscan REST API)
  • CIRCUIT_SOURCE.ALCHEMY — used by sync-mint-burn (Alchemy JSON-RPC)

An Alchemy outage does not block blacklist sync, and vice versa. Each circuit breaker opens after consecutive failures and probes independently.


Testing

Files:

  • worker/src/lib/__tests__/mint-burn-scoring.test.ts — pressure-shift formula, gauge bands, composite gauge, flight-to-quality
  • worker/src/lib/__tests__/mint-burn-pipeline.test.ts — shared parse/classification/persistence/sync-state behavior parity
  • worker/src/cron/__tests__/sync-mint-burn.test.ts — cron ingestion orchestration and degraded-mode handling
  • worker/src/api/__tests__/backfill-mint-burn.test.ts — admin backfill chunking, done/nextFromBlock, and sync-state progression
  • worker/src/api/__tests__/mint-burn-flows.test.ts — API response shape validation plus burning/improving regression coverage
  • shared/lib/__tests__/mint-burn-signals.test.ts — shared direction/pressure/composite interpretation coverage

Coverage:

  • Pressure shift: null for < 7 days, neutral at baseline, clamping at 0/100, floor denominator
  • Gauge bands: correct band for all score ranges
  • Composite gauge: mcap-weighted average, skips null, returns null when all null
  • Flight-to-quality: $100M activation, intensity formula, edge cases
  • Pipeline convergence: inserted-vs-ignored accounting, bridge/effective/review burn counters, affected-hour recomputation, sync-state mode semantics
  • Backfill chunking: done=false and nextFromBlock emitted when maxChunks stops before target range
  • API: aggregate vs per-coin response shapes against Zod schemas, 404 for unknown coin
  • Coverage/freshness: aggregate hours leaves 24h coin fields unchanged, current UTC day excluded from baseline, deterministic largest-event selection on ties

Future Work

Current production scope already spans configured issuance chains. Planned next expansions:

  • Additional EVM chains: add more native issuance configs + chain-specific scan policies after reliability gates are met
  • Tron support: USDT Issue/Redeem topic groundwork exists; ingestion path is not wired yet
  • Curve Finance detection: DEX-level flow tracking

File Index

FileRole
worker/src/cron/sync-mint-burn.tsCron job: critical + extended incremental event sync lanes, hourly aggregation, lane-specific run-state
worker/src/lib/mint-burn-pipeline/types.tsShared ingestion types for cron/backfill
worker/src/lib/mint-burn-pipeline/parse.tsShared log parsing and price resolution
worker/src/lib/mint-burn-pipeline/roundtrip-detection.tsShared same-transaction roundtrip tagging
worker/src/lib/mint-burn-pipeline/classification.tsShared bridge-burn classification
worker/src/lib/mint-burn-pipeline/context.tsShared current/historical price context loaders
worker/src/lib/mint-burn-pipeline/persistence.tsShared event write + hourly recompute helpers
worker/src/lib/mint-burn-pipeline/price-heal.tsShared NULL-price auto-heal helper
worker/src/lib/mint-burn-pipeline/roundtrip-sweep.tsPost-cron sweep for cross-run atomic roundtrip detection
worker/src/lib/mint-burn-pipeline/sync-state.tsShared sync-state read/init/upsert helpers
worker/src/lib/mint-burn-contracts.tsMint/burn event configs resolved from shared stablecoin contracts, plus explicit override addresses for special vault events
worker/src/lib/mint-burn-scoring.tsPure scoring functions: pressure shift (FIS), gauge, flight-to-quality
worker/src/api/mint-burn-flows.tsAPI handler: route-level aggregate + per-coin orchestration
worker/src/api/mint-burn-flows-shared.tsShared mint/burn cache fallback, cron snapshot, baseline, and coverage helpers
worker/src/api/mint-burn-events.tsAPI handler: paginated event feed
worker/src/api/backfill-mint-burn.tsAdmin endpoint: controlled event ingestion backfill
worker/src/api/backfill-mint-burn-prices.tsAdmin endpoint: backfill NULL amount_usd values
worker/src/api/reclassify-atomic-roundtrips.tsAdmin endpoint: retroactively tag same-tx mint/burn rows as atomic roundtrips
worker/migrations/0000_baseline.sqlBaseline mint/burn schema (3 tables, including the historical v2 layout)
src/hooks/use-mint-burn-flows.tsTanStack Query hooks (3 hooks)
src/app/flows/page.tsxFrontend page
src/app/flows/layout.tsxPage metadata/layout
worker/src/lib/mint-burn-scoring.tsPure Flow Intensity / Bank Run Gauge / flight-to-quality logic (getGaugeBand, computeGaugeScore, detectFlightToQuality)
src/components/flow-brrr-overview.tsxShared Bank Run Gauge overview shell for /flows and homepage snapshot
src/components/flow-chart.tsxRecharts flow chart
src/components/flow-table.tsxSortable per-coin table
src/components/flow-event-feed.tsxPaginated event table
src/components/minting-pressure-gauge.tsxShared literal 24h mint-vs-burn gauge
src/components/flow-summary-card.tsxSummary card for detail pages
shared/lib/mint-burn-signals.tsShared net-direction + pressure-state interpretation helpers
shared/types/index.tsTypeScript types + Zod schemas
worker/src/lib/__tests__/mint-burn-scoring.test.tsScoring unit tests
worker/src/lib/__tests__/mint-burn-pipeline.test.tsShared ingestion pipeline tests
worker/src/cron/__tests__/sync-mint-burn.test.tsCron ingestion tests
worker/src/api/__tests__/backfill-mint-burn.test.tsBackfill ingestion tests
worker/src/api/__tests__/mint-burn-flows.test.tsAPI contract tests