You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Users have no way to be proactively notified about conditions they care about — "tell me if any protocol's APY drops below 5%," "alert me if my portfolio drops 10% in a day." This is new, user-defined-rule functionality. It is distinct from the platform's existing internal alerting (Prometheus/Grafana thresholds documented in docs/OBSERVABILITY.md, e.g. agent_loop_status, cursor_lag_ledgers, dlq_size), which watches system health for operators, not portfolio conditions for end users.
Current State
The building blocks for delivery already exist: src/services/webhookDispatcher.ts's dispatchWebhookEvent(event, data) fans out to any WebhookSubscription matching an event name, with HMAC signing (src/utils/webhookSignature.ts) and a 3-attempt exponential backoff (1s/2s/4s). Note this retry only happens synchronously within the initial dispatch call — a delivery that ends up FAILED after all 3 attempts is never retried later by any sweep job. For user-facing alerts (arguably more time-sensitive/important to the user than they'd tolerate silently dropping), this issue should decide explicitly whether to reuse dispatchWebhookEvent as-is (accepting that gap) or extend it with an actual retry sweep — this decision should be made deliberately, not by default.
POST /api/alerts / GET /api/alerts/:userId / PATCH /api/alerts/:id / DELETE /api/alerts/:id — standard requireAuth + ownership-check pattern established in the other issues in this batch.
Integration Points
A new scheduled job, src/jobs/alertRules.ts, following the existing src/jobs/*.tssetInterval shape (register/clear in src/index.ts, wrap with runWithCorrelationIdAsync and job metrics like the other jobs in that directory). On each tick: load ACTIVE rules, evaluate each against current ProtocolRate (for PROTOCOL_APY), Position/YieldSnapshot (for PORTFOLIO_VALUE/POSITION_DRAWDOWN), and fire only if the comparator condition is met andlastFiredAt is outside cooldownMinutes — the cooldown is essential; without it a rule sitting right at its threshold would fire on every single tick.
Delivery: add a new webhook event (e.g. alert_rule.triggered) to the WEBHOOK_EVENTS const in src/validators/webhook-validators.ts and reuse dispatchWebhookEvent for the WEBHOOK/BOTH channels; reuse src/whatsapp/formatters.ts for a new formatAlertTriggeredReply for the WHATSAPP/BOTH channels.
A rule's condition remains true across many consecutive ticks — cooldown prevents repeat notification spam; the rule should still be "active" and fire again once the cooldown elapses if the condition is still true, not require the condition to flip false and true again.
PROTOCOL_APY rule references a protocol that gets delisted/removed — the rule should be flagged or auto-deactivated with a clear message, not silently evaluate against stale/missing data.
POSITION_DRAWDOWN requires a "peak value" reference point — decide and document explicitly what window that's measured against (e.g. rolling 30-day high) since "drawdown" is ambiguous without one.
User deletes a rule while the evaluator job is mid-tick — the in-flight evaluation for that tick should simply not fire (no error), since the rule is gone by the time delivery would happen.
Security & Privacy Considerations
Rule CRUD must use the same ownership-check pattern as the other :id-keyed resources in this batch (compare rule.userId to req.auth.userId), not a new auth path.
Webhook delivery of alert data goes through the existing HMAC-signed mechanism — no new unsigned delivery path should be introduced for this feature.
Out of Scope
Compound/multi-condition rules (e.g. "APY below X AND portfolio value above Y") — single-condition rules only for v1.
A generic user-facing rule-builder UI — this issue is the API, evaluator, and WhatsApp/webhook delivery only.
Suggested Implementation Plan
AlertRule model, migration, CRUD API with ownership checks.
Evaluator job with cooldown logic; unit tests for cooldown suppression and threshold-crossing detection in isolation from the scheduler.
Webhook + WhatsApp delivery wiring, including the explicit decision on failed-delivery retry behavior.
NLP intents for conversational rule management.
Acceptance Criteria
AlertRule model + migration and CRUD API with ownership checks
Evaluator job fires correctly on threshold crossing for all three metric types
Cooldown correctly suppresses repeat notifications while the condition remains true, and allows re-firing once the cooldown elapses
Deleting/deactivating a rule stops future alerts, including for an evaluation already in flight
Alerts delivered via both webhook (HMAC-signed, existing dispatcher) and WhatsApp paths
POSITION_DRAWDOWN's reference-point window is explicitly documented
Explicit, documented decision on whether failed webhook deliveries for alerts get any retry beyond the existing dispatcher's synchronous 3 attempts
Problem Statement
Users have no way to be proactively notified about conditions they care about — "tell me if any protocol's APY drops below 5%," "alert me if my portfolio drops 10% in a day." This is new, user-defined-rule functionality. It is distinct from the platform's existing internal alerting (Prometheus/Grafana thresholds documented in
docs/OBSERVABILITY.md, e.g.agent_loop_status,cursor_lag_ledgers,dlq_size), which watches system health for operators, not portfolio conditions for end users.Current State
The building blocks for delivery already exist:
src/services/webhookDispatcher.ts'sdispatchWebhookEvent(event, data)fans out to anyWebhookSubscriptionmatching an event name, with HMAC signing (src/utils/webhookSignature.ts) and a 3-attempt exponential backoff (1s/2s/4s). Note this retry only happens synchronously within the initial dispatch call — a delivery that ends upFAILEDafter all 3 attempts is never retried later by any sweep job. For user-facing alerts (arguably more time-sensitive/important to the user than they'd tolerate silently dropping), this issue should decide explicitly whether to reusedispatchWebhookEventas-is (accepting that gap) or extend it with an actual retry sweep — this decision should be made deliberately, not by default.Proposed Solution
Data Model
API Surface
POST /api/alerts/GET /api/alerts/:userId/PATCH /api/alerts/:id/DELETE /api/alerts/:id— standardrequireAuth+ ownership-check pattern established in the other issues in this batch.Integration Points
src/jobs/alertRules.ts, following the existingsrc/jobs/*.tssetIntervalshape (register/clear insrc/index.ts, wrap withrunWithCorrelationIdAsyncand job metrics like the other jobs in that directory). On each tick: loadACTIVErules, evaluate each against currentProtocolRate(forPROTOCOL_APY),Position/YieldSnapshot(forPORTFOLIO_VALUE/POSITION_DRAWDOWN), and fire only if the comparator condition is met andlastFiredAtis outsidecooldownMinutes— the cooldown is essential; without it a rule sitting right at its threshold would fire on every single tick.alert_rule.triggered) to theWEBHOOK_EVENTSconst insrc/validators/webhook-validators.tsand reusedispatchWebhookEventfor theWEBHOOK/BOTHchannels; reusesrc/whatsapp/formatters.tsfor a newformatAlertTriggeredReplyfor theWHATSAPP/BOTHchannels.src/nlp/parser.ts's closed union, same two-tier regex/Claude pattern and manual-sync caveat as issues Goal-Based Investing ("Savings Goals") #281/Recurring Deposits (Dollar-Cost Averaging Scheduler) #282.Edge Cases & Failure Modes
PROTOCOL_APYrule references a protocol that gets delisted/removed — the rule should be flagged or auto-deactivated with a clear message, not silently evaluate against stale/missing data.POSITION_DRAWDOWNrequires a "peak value" reference point — decide and document explicitly what window that's measured against (e.g. rolling 30-day high) since "drawdown" is ambiguous without one.Security & Privacy Considerations
:id-keyed resources in this batch (comparerule.userIdtoreq.auth.userId), not a new auth path.Out of Scope
Suggested Implementation Plan
AlertRulemodel, migration, CRUD API with ownership checks.Acceptance Criteria
AlertRulemodel + migration and CRUD API with ownership checksPOSITION_DRAWDOWN's reference-point window is explicitly documenteddocs/openapi.yamlupdated