const DEFAULT_RECENT_LIMIT = 50; export function createAlertEngine({ activePair, priceStaleMs, inventoryStaleMs, fundingCreditPendingMs, fundingStuckMs, evaluationIntervalMs, recentTransitionLimit = DEFAULT_RECENT_LIMIT, }) { const state = { latest_price: null, latest_inventory: null, latest_liquidity_action: null, latest_trade_result: null, funding_observations: {}, active_alerts: {}, runtime_alert_keys: new Set(), recent_transitions: [], last_evaluated_at: null, }; return { applyEvent(topic, payload, now = new Date().toISOString()) { switch (topic) { case 'ref.market_price': state.latest_price = payload; break; case 'state.intent_inventory': state.latest_inventory = payload; break; case 'ops.liquidity_action': state.latest_liquidity_action = payload; break; case 'ops.funding_observation': if (payload?.funding_observation_id) { state.funding_observations[payload.funding_observation_id] = payload; } break; case 'exec.trade_result': state.latest_trade_result = payload; break; default: break; } return evaluateAlerts({ state, activePair, priceStaleMs, inventoryStaleMs, fundingCreditPendingMs, fundingStuckMs, recentTransitionLimit, now, }); }, evaluate(now = new Date().toISOString()) { return evaluateAlerts({ state, activePair, priceStaleMs, inventoryStaleMs, fundingCreditPendingMs, fundingStuckMs, recentTransitionLimit, now, }); }, applyRuntimeAlerts(desiredAlerts = [], now = new Date().toISOString()) { return reconcileRuntimeAlertState({ state, desiredAlerts, now, recentTransitionLimit, }); }, getState(now = new Date().toISOString()) { return summarizeState({ state, evaluationIntervalMs, now, }); }, }; } function evaluateAlerts({ state, activePair, priceStaleMs, inventoryStaleMs, fundingCreditPendingMs, fundingStuckMs, recentTransitionLimit, now, }) { const desired = new Map(); const nowValue = timestampValue(now); const priceAgeMs = ageMs(state.latest_price?.observed_at || state.latest_price?.ingested_at, nowValue); if (priceAgeMs == null || priceAgeMs > priceStaleMs) { desired.set( buildAlertKey({ alertCode: 'reference_price_stale', serviceScope: 'market-reference-ingest', pair: activePair, }), { alert_code: 'reference_price_stale', severity: 'warning', reason: priceAgeMs == null ? 'no reference price has been observed' : `reference price age ${priceAgeMs}ms exceeds ${priceStaleMs}ms`, service_scope: 'market-reference-ingest', pair: activePair, asset_id: null, tx_hash: null, details: { last_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null, age_ms: priceAgeMs, stale_after_ms: priceStaleMs, }, }, ); } const inventoryAgeMs = ageMs( state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at, nowValue, ); if (inventoryAgeMs == null || inventoryAgeMs > inventoryStaleMs) { desired.set( buildAlertKey({ alertCode: 'inventory_snapshot_stale', serviceScope: 'inventory-sync', }), { alert_code: 'inventory_snapshot_stale', severity: 'warning', reason: inventoryAgeMs == null ? 'no inventory snapshot has been observed' : `inventory snapshot age ${inventoryAgeMs}ms exceeds ${inventoryStaleMs}ms`, service_scope: 'inventory-sync', pair: null, asset_id: null, tx_hash: null, details: { last_inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null, age_ms: inventoryAgeMs, stale_after_ms: inventoryStaleMs, }, }, ); } for (const observation of Object.values(state.funding_observations)) { const observationAgeMs = ageMs(observation.last_seen_at, nowValue); const baseDetails = { funding_observation_id: observation.funding_observation_id, funding_handle: observation.funding_handle, confirmations: observation.confirmations, amount: observation.amount, observation_status: observation.status, first_seen_at: observation.first_seen_at, last_seen_at: observation.last_seen_at, age_ms: observationAgeMs, bridge_deposit_tx_hash: observation.bridge_deposit_tx_hash, bridge_status: observation.bridge_status, }; if (observation.status === 'SEEN_UNCONFIRMED') { desired.set( buildAlertKey({ alertCode: 'funding_seen_unconfirmed', serviceScope: 'liquidity-manager', assetId: observation.asset_id, txHash: observation.tx_hash, }), { alert_code: 'funding_seen_unconfirmed', severity: 'info', reason: `funding tx ${observation.tx_hash} is visible before confirmations`, service_scope: 'liquidity-manager', pair: null, asset_id: observation.asset_id, tx_hash: observation.tx_hash, details: baseDetails, }, ); } if ( observation.status === 'CREDIT_PENDING' || (observation.status === 'SEEN_CONFIRMED' && observationAgeMs != null && observationAgeMs >= fundingCreditPendingMs) ) { desired.set( buildAlertKey({ alertCode: 'funding_confirmed_credit_pending', serviceScope: 'liquidity-manager', assetId: observation.asset_id, txHash: observation.tx_hash, }), { alert_code: 'funding_confirmed_credit_pending', severity: 'warning', reason: `funding tx ${observation.tx_hash} is confirmed but not spendable yet`, service_scope: 'liquidity-manager', pair: null, asset_id: observation.asset_id, tx_hash: observation.tx_hash, details: { ...baseDetails, credit_pending_after_ms: fundingCreditPendingMs, }, }, ); } if ( observation.status === 'FAILED_OR_STUCK' || ( observation.status !== 'CREDITED' && observation.status !== 'SEEN_UNCONFIRMED' && observationAgeMs != null && fundingStuckMs != null && observationAgeMs >= fundingStuckMs ) ) { desired.set( buildAlertKey({ alertCode: 'funding_stuck', serviceScope: 'liquidity-manager', assetId: observation.asset_id, txHash: observation.tx_hash, }), { alert_code: 'funding_stuck', severity: 'critical', reason: `funding tx ${observation.tx_hash} is failed or stuck before credit`, service_scope: 'liquidity-manager', pair: null, asset_id: observation.asset_id, tx_hash: observation.tx_hash, details: baseDetails, }, ); } } const latestTradeResult = state.latest_trade_result; if (latestTradeResult && isSubmissionFailure(latestTradeResult)) { desired.set( buildAlertKey({ alertCode: 'executor_submission_failed', serviceScope: 'trade-executor', pair: latestTradeResult.pair || activePair, }), { alert_code: 'executor_submission_failed', severity: 'critical', reason: `executor submission failed for command ${latestTradeResult.command_id}`, service_scope: 'trade-executor', pair: latestTradeResult.pair || activePair, asset_id: null, tx_hash: null, details: { command_id: latestTradeResult.command_id, quote_id: latestTradeResult.quote_id, result_code: latestTradeResult.result_code, error: latestTradeResult.error || null, status: latestTradeResult.status, }, }, ); } const transitions = reconcileAlertState({ state, desired, now, recentTransitionLimit, }); state.last_evaluated_at = now; return transitions; } function reconcileAlertState({ state, desired, now, recentTransitionLimit }) { const transitions = []; for (const [key, next] of desired.entries()) { const existing = state.active_alerts[key]; if (!existing) { const raised = { ...next, status: 'raised', first_raised_at: now, raised_at: now, cleared_at: null, last_evaluated_at: now, }; state.active_alerts[key] = raised; transitions.push(raised); continue; } state.active_alerts[key] = { ...existing, ...next, status: 'raised', raised_at: existing.raised_at || existing.first_raised_at || now, first_raised_at: existing.first_raised_at || existing.raised_at || now, cleared_at: null, last_evaluated_at: now, }; } for (const [key, existing] of Object.entries(state.active_alerts)) { if (desired.has(key)) continue; const cleared = { ...existing, status: 'cleared', raised_at: existing.raised_at || existing.first_raised_at || now, cleared_at: now, last_evaluated_at: now, }; delete state.active_alerts[key]; transitions.push(cleared); } if (transitions.length > 0) { state.recent_transitions.unshift(...transitions); state.recent_transitions = state.recent_transitions.slice(0, recentTransitionLimit); } return transitions; } function reconcileRuntimeAlertState({ state, desiredAlerts, now, recentTransitionLimit, }) { const transitions = []; const desired = new Map(); const nextRuntimeKeys = new Set(); for (const alert of desiredAlerts || []) { const key = buildAlertKey({ alertCode: alert.alert_code, serviceScope: alert.service_scope, pair: alert.pair, assetId: alert.asset_id, txHash: alert.tx_hash, }); desired.set(key, alert); nextRuntimeKeys.add(key); } for (const key of state.runtime_alert_keys) { if (desired.has(key)) continue; const existing = state.active_alerts[key]; if (!existing) continue; const cleared = { ...existing, status: 'cleared', raised_at: existing.raised_at || existing.first_raised_at || now, cleared_at: now, last_evaluated_at: now, }; delete state.active_alerts[key]; transitions.push(cleared); } for (const [key, alert] of desired.entries()) { const existing = state.active_alerts[key]; if (!existing) { const raised = { ...alert, status: 'raised', first_raised_at: now, raised_at: now, cleared_at: null, last_evaluated_at: now, }; state.active_alerts[key] = raised; transitions.push(raised); continue; } state.active_alerts[key] = { ...existing, ...alert, status: 'raised', raised_at: existing.raised_at || existing.first_raised_at || now, first_raised_at: existing.first_raised_at || existing.raised_at || now, cleared_at: null, last_evaluated_at: now, }; } state.runtime_alert_keys = nextRuntimeKeys; if (transitions.length > 0) { state.recent_transitions.unshift(...transitions); state.recent_transitions = state.recent_transitions.slice(0, recentTransitionLimit); } state.last_evaluated_at = now; return transitions; } function summarizeState({ state, evaluationIntervalMs, now }) { const activeAlerts = Object.values(state.active_alerts) .sort((left, right) => timestampValue(right.first_raised_at) - timestampValue(left.first_raised_at)); const nowValue = timestampValue(now); return { active_alerts: activeAlerts, recent_transitions: state.recent_transitions, last_evaluated_at: state.last_evaluated_at, stale: ageMs(state.last_evaluated_at, nowValue) > (evaluationIntervalMs * 2), latest_inputs: { market_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null, inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null, liquidity_action_at: state.latest_liquidity_action?.observed_at || null, trade_result_at: state.latest_trade_result?.ingested_at || null, funding_observation_count: Object.keys(state.funding_observations).length, }, }; } function buildAlertKey({ alertCode, serviceScope, pair = null, assetId = null, txHash = null }) { return [alertCode, serviceScope, pair || '', assetId || '', txHash || ''].join('|'); } function isSubmissionFailure(result) { return result?.status === 'failed' || result?.result_code === 'submission_failed'; } function ageMs(value, nowValue) { const start = timestampValue(value); if (!Number.isFinite(start) || !Number.isFinite(nowValue)) return null; return Math.max(0, nowValue - start); } function timestampValue(value) { if (!value) return NaN; const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : NaN; }