Skip to content

Custom Price & Yield Alert Rules #289

Description

@robertocarlous

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'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.

Proposed Solution

Data Model

model AlertRule {
  id              String        @id @default(uuid())
  userId          String
  metric          AlertMetric   // PROTOCOL_APY, PORTFOLIO_VALUE, POSITION_DRAWDOWN
  protocolName    String?       // required when metric = PROTOCOL_APY
  comparator      Comparator    // LT, GT, LTE, GTE
  threshold       Decimal
  deliveryChannel DeliveryChannel // WEBHOOK, WHATSAPP, BOTH
  cooldownMinutes Int           @default(60)
  lastFiredAt     DateTime?
  isActive        Boolean       @default(true)
  createdAt       DateTime      @default(now())
  user            User @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@index([userId])
  @@index([isActive, metric])
}
enum AlertMetric { PROTOCOL_APY PORTFOLIO_VALUE POSITION_DRAWDOWN }
enum Comparator { LT LTE GT GTE }
enum DeliveryChannel { WEBHOOK WHATSAPP BOTH }

API Surface

  • 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/*.ts setInterval 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 and lastFiredAt 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.
  • NLP: add alert create/list/delete intents to 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

  • 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

  1. AlertRule model, migration, CRUD API with ownership checks.
  2. Evaluator job with cooldown logic; unit tests for cooldown suppression and threshold-crossing detection in isolation from the scheduler.
  3. Webhook + WhatsApp delivery wiring, including the explicit decision on failed-delivery retry behavior.
  4. 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
  • docs/openapi.yaml updated

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions