Skip to main content

Documentation Index

Fetch the complete documentation index at: https://glide-9da73dea.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

OSS reference implementation of the anomaly detector documented in the Glide OSS plan §M4. Deterministic, explainable signals; no classifier, no ML; every heuristic is a pure function over a facts snapshot the operator passes in.

Install

npm install @glideco/anomaly
npmjs.com/package/@glideco/anomaly

Why no ML?

Per the OSS plan §“Critical architectural commitments”:
Anomaly signals are heuristics, not a classifier.
Signals are inputs to UI decision aids — the policy engine has the only hard veto. Heuristics are explainable, version-controllable, testable in isolation, and don’t drift between training runs. ML is the wrong tool for the “why did this fire” review surface.

Built-in heuristics

HeuristicDefault severityWhat it catches
newRecipientHeuristicnoticeFirst payment to a counterparty
makeAmountDeviationHeuristicwarn at 3×, critical at 10×Amounts well above baseline median
makeVelocityHeuristicwarn at 2×, critical at 5×Burst of tool calls
allowlistBypassHeuristiccriticalAttempt to pay outside the allowlist
timeWindowHeuristicwarnTransactions outside business hours

Storm suppression

A burst of similar signals (e.g., 50 “new-recipient” events in 60 seconds during a payroll batch) should NOT fan out as 50 push notifications. The suppressor returns at most maxPerWindow signals per (kind, agentId) per windowMs; overflow signals are still recorded — caller’s choice what to do with them (typically routed to an in-app aggregated feed).
import { StormSuppressor } from '@glideco/anomaly';

const suppressor = new StormSuppressor({
  maxPerWindow: 1,
  windowMs: 60 * 60 * 1000, // 1 hour
});

const decision = suppressor.shouldPush(signal, agentId);
// → { pass: true } | { pass: false, reason: 'storm-suppressed', overflowCount: N }

Sentry sink

Optional adapter that routes signals to Sentry as messages tagged with severity + kind. The Sentry instance is operator-supplied so the package doesn’t take a hard dependency on @sentry/nextjs (any object with captureMessage() works — @sentry/node, Glitchtip, custom shims).
import * as Sentry from '@sentry/nextjs';
import { makeSentrySink } from '@glideco/anomaly';

const sink = makeSentrySink({ Sentry, project: 'glide-prod' });

for (const signal of signals) {
  const decision = suppressor.shouldPush(signal, agentId);
  if (decision.pass) {
    sink.emit(signal, agentId);
  } else {
    appendToInAppFeed(signal); // overflow goes here
  }
}
Severity mapping:
Anomaly severitySentry level
info, noticeinfo
warnwarning
criticalerror
Failures emitting to Sentry never bubble — the sink swallows them so a Sentry outage can’t break the calling pipeline.

Adding your own heuristic

Heuristics are just (ctx) => signals[]. Wire one for your domain:
import { Heuristic } from '@glideco/anomaly';

interface CrossBorderFacts {
  recipientCountry: string;
  agentHomeCountry: string;
}

export const crossBorderHeuristic: Heuristic<CrossBorderFacts> = (ctx) => {
  if (ctx.facts.recipientCountry === ctx.facts.agentHomeCountry) {
    return [];
  }
  return [
    {
      kind: 'cross-border',
      severity: 'notice',
      message: `Payment to ${ctx.facts.recipientCountry} (agent home: ${ctx.facts.agentHomeCountry})`,
      details: ctx.facts,
      timestamp: ctx.now,
    },
  ];
};

Reading list