Skip to main content

Anomaly insights: detector shapes, baselines, and the alert handoff

The anomaly-insights page (Insights → Anomalies) is a time-stamped ledger of everything the platform’s detectors have raised against your traffic — split into a wallet scope (spend channels) and a webhook scope (inbound and delivery-signal hints) — with per-scope totals, open counts, and a day-by-day trend. It is the read model of the insights rollup model: no new table, no write-path change — every anomaly the detectors already record becomes a query. The endpoint surface is three GET routes under /insights:
  • GET /insights/anomaly-insights/by-scope — per-scope totals (wallet, webhook, and the combined count), open counts, and high/critical counts over the window, with the newest detection timestamp.
  • GET /insights/anomaly-insights/history — the windowed ledger rows themselves: id, category, severity, scope, scrubbed summary, and triage/resolve timestamps.
  • GET /insights/anomaly-insights/timeseries — a zero-filled per-(scope, UTC day) histogram for the trendline, so a flat day reads as zero, not as a gap.
All three resolve their window the same way the other insights surfaces do: ?days= (default 30, capped at 365) or an explicit from/to ISO pair. History pages at 100 rows; the aggregate and trendline reads are scan-bounded underneath and surface a truncated marker if a pathological window ever hits that bound, so a misreport is never silent.

Detector families — what shapes an anomaly

Each ledger entry carries a detector family — the shape of anomaly that fired. One row per family: The wallet/webhook split is a classification over the entry’s details, not two separate detectors: entries whose channel is a wallet spend channel (sms, whatsapp, email, rcs, voice, agents, or the synthetic all) land in wallet scope; entries carrying an inbound phone or webhook-delivery hint land in webhook scope. The filter (?scope=) never restricts what is scanned — it only reclassifies the rows.

Baseline window — trailing, per family

Every detector builds its baseline over a trailing window defined by its family, and the anomaly-insights view shows you what those comparisons recorded. For the spend-velocity family the baseline is the average daily spend over the trailing 14 complete UTC days, with operator wallet adjustments excluded from both sides so bookkeeping never reads as usage. Other families keep their own windows — the point of the ledger is that each recorded entry tells you which comparison it failed. Cold-start semantics. A tenant with no baseline history at all is treated as a surge the moment an absolute floor clears — for spend velocity, the projected daily rate must also clear a 250/dayfloorsoa250/day floor so a 0.30/day tenant never pages on a “3x” spike; the surge ratio reports null instead of a meaningless multiple. An empty window reads as no data, never as zero — the same convention the insights ratio surfaces follow.

Threshold → breach → alert

A breach is recorded once, then handed off. The platform’s detector writers record each anomaly through one choke point (the fraud-review writer), then:
  1. Ledger row — the anomaly-insights page you are on: an append-only row with open status for triage.
  2. Customer alert fan-out — the same detection reaches the tenant whose traffic tripped it as an account.fraud.alert webhook event, an in-app notification (security-critical: owners and admins get it even when they opted out of routine bells), and an email to owners/admins.
  3. Severity tierlow / medium read as info/warning; high / critical read as an error badge and are what the dashboard high_or_critical_count strip counts.
For the spend-velocity family the severity escalates to critical when the surge reaches 25x baseline (2.5x the 10x flag threshold) or $1,250/day absolute (5x the floor). That family’s graduated auto-mitigation — 10/min clamp at high, 0/min pause at critical, self-expiring within 6 hours — is owned by the spend-anomaly model; the anomaly-insights page records which of those mitigations applied on the spend-velocity entries.

What a breach carries

Each ledger item answers six questions an operator asking “what fired” needs:
  • category — which detector family (the table above).
  • severity + status — how bad, and whether anyone has triaged or resolved it.
  • scopewallet vs webhook.
  • summary — a scrubbed one-liner (phone numbers and emails are masked on write, and the read re-scrubs the structured details as defence in depth, so this view never widens into a PII viewer).
  • detected/triaged/resolved timestamps — the breach timeline.
  • details — the structured context the detector passed (channel, phone prefix, correlation id), never raw PII.
The suggested next step is encoded in the category + details: a spend-velocity breach points at the channel’s send rate; a destination breach points at the prefix; a SIM-swap or deepfake breach points at the specific recipient or call in the correlation id.

Boundary with spend anomalies

Two pages deliberately do NOT duplicate each other:
  • Spend alerts, burn rate, and the velocity-anomaly model owns the wallet-burn detection math — thresholds, the burn-rate projection, the graduated auto-mitigation.
  • This page owns the traffic/quality anomaly ledger — every detector family’s recorded breaches, across scopes, with triage status.
The spend page explains the math; this page is where the breach lands.

Two worked examples

A delivery-rate cliff on one destination. Your account sends SMS at a steady pace into a domestic prefix. A campaign starts bursting sends into a premium-rate international prefix. The spend-velocity detector compares today’s projected rate against the 14-day baseline per channel; if it clears the $250/day floor and 10x the baseline, it records a spend_velocity_anomaly at high. If the prefix is on the blocked list, the destination detectors record irsf_blocked instead. Either way the entry lands on this page with the channel, the surge ratio, and the prefix hint — and, on the spend-velocity family, the throttle or pause that mitigated it. A conversation-mix shift across scopes. A tenant whose traffic was historically SMS + WhatsApp starts seeing SIM-swap flags on new verify traffic. Each flag lands as a webhook-scope entry. The by-scope strip shows the shift: open webhook-scope entries rising while wallet stays quiet, so you recognise “inbound/verify anomalies” are the new shape instead of reading each one in isolation.

Tenant-owned response — flag, never gate

An anomaly-insights entry is a flag for your operators, not a platform-mandated gate. The two controls exist on purpose:
  • Triage status flips between open, triaged, dismissed, and escalated on your own operators’ review — the page never presumes the right posture for you.
  • Auto-mitigation is opt-in to the detector, not to this page — only the spend-velocity family applies one, and it is always self-expiring and tenant-clearable.
Wallets, conversations, and sends keep their own controls; the ledger never drains a wallet, blocks a destination, or pauses a channel itself. That separation is what makes the ledger safe to keep: it records what the detectors judged, and it never decides.

See also