Appearance
Transaction velocity
Monitoring on facts that arrive after onboarding, decided by rules, a model and a person together: payments are ingested as events, a workflow evaluates velocity rules over a subject's recent events, and when the score calls for a person, a model triages the alert (whether the merchant's own description of its business accounts for the payments, and how urgent the review is) and writes the analyst a note before a case opens. The model never clears or blocks an alert. Every rule result, model answer and decision is on the run's record. Pack: examples/policies/transaction-velocity/.
A pattern for expressing a monitoring policy, not compliance guidance: the windows, thresholds, the choice of signals and the model's questions are placeholders; a real policy comes from the business's own loss data. Its model steps call one install, chatgpt (The chatgpt install).
The decision it automates
Whether a subject's recent payment behavior is fine, needs a person, or should be blocked, as of the moment the workflow runs. Today the run is started by the integrating system (for example after every batch of events, or from a scheduler it owns); events as workflow triggers are on the roadmap. The rules decide; on the way to a person, the model sets how urgent the review is and writes the analyst a note, and the analyst decides.
Data expected
- A subject (any kind) and events for it ingested through
POST /events: typepayment, anamountandcurrency, anoccurredAt, a caller-chosenidempotencyKey, the promotedcardFingerprintkey when known, andattributes.status(approved,declined). See Events. subject.data.businessDescription(optional): how the merchant describes its business, such as what it sells and its usual order size, from onboarding. The model reads it against the numbers; without it, the model reads the numbers alone.- An install named
chatgptof the platform'schatgptapp, with your OpenAI API key.
Steps and rules
| Step | Type | Why |
|---|---|---|
rules | evaluate_rules | The velocity_monitoring rule set; every rule aggregates the subject's events as of now. |
route | branch | rules.outcome == manual_review goes to triage; an approval or a rejection goes straight to the decision, without the model. |
triage | call_app | decide through chatgpt: two questions about the alert, answered under triage. |
narrative | call_app | generate through chatgpt: three sentences for the analyst, under narrative. |
review | create_case | velocity_review, 24-hour SLA: money may be moving. Its priority is the triage's (priorityFrom: triage.answers.priority.value), critical or high. |
decide | emit_decision | From the rules, or the reviewer's decision. |
The rule set velocity_monitoring (useCase: monitoring) sums weights into bands 0-40 approve, 40-90 manual_review, 90-100 reject:
| Rule | Aggregate | Severity, weight | Fails when |
|---|---|---|---|
velocity_payment_sum_24h | sum of amount over 24 hours, type payment | warn, 50 | more than 5,000 |
velocity_payment_count_1h | count over 1 hour, type payment | warn, 40 | 5 or more |
velocity_declined_count_24h | count over 24 hours, type payment, status: declined | block, 100 | 3 or more (card testing) |
velocity_distinct_cards_24h | distinct_count of card_fingerprint over 24 hours | warn, 40 | 3 or more distinct cards |
Volume alone (50) is a review; volume with a burst or many cards (90) rejects; repeated declines block on their own. Each result's details carries the computed value, the key and the window, so a reviewer sees the number, not only the verdict. Windows end at the evaluation time, which is what makes backtests reproduce live results.
The model steps
triage writes the model an account of the alert (its stateTemplate) from two variables: the merchant's description (subject.businessDescription, (no description on file) when there is none) and the velocity results (rules.results, each with the value it computed over its window). It asks two questions:
| Question | Answer | Asks | Read by |
|---|---|---|---|
explained | yes or no | Does the merchant's own description of its business account for payments like these: their amounts, how many came in how short a time, the declines and the number of cards? | The analyst, in the run context |
priority | critical or high | How soon an analyst should look: critical at once (declines across several cards, or a burst far beyond what the business describes, as card testing or a taken-over account would look); high within the review time. | review: the case's priority |
The options run from the most urgent and stop at high, the step's own priority: the triage can raise an alert to critical, never lower one below high, and an answer that is no priority leaves the case at the step's high. Through the adapter, the install's default way of answering decisions, a choice comes without a probability, so the urgency is not gated on confidence; a person reviews every alert either way.
narrative asks for three sentences from the same two variables: what the payments show, with the numbers from the results; whether the description accounts for them; and what to look at first in the merchant's timeline. Its instructions keep it to the results and the description, with no advice on the outcome and no finding of fraud or wrongdoing: the analyst decides, on the results and the timeline, which stay the evidence.
What the vendor sees
Through the install, OpenAI receives the steps' own text, the merchant's description and the rule results as the run records them: each rule's key, version, severity, weight, outcome and reason, with the value it computed, the threshold, the window and the group's key, which is the subject's id. Nothing else reaches it: not the merchant's name or country, and not the events themselves; no document or image reaches a model (Data protection). Only an alert on its way to a case is sent: an approval or a rejection never calls the model. The one-click install calls OpenAI's Responses API with storing off, for the triage as well as the note.
The description is the merchant's own text and may hold instructions: at worst, an answer the model was talked into leaves an alert at high or colors the note; it never closes a case or changes the rules' outcome (Prompt injection).
The chatgpt install
A pack never carries an install: its steps call one by name, so the pack declares it, "installs": { "chatgpt": "chatgpt" }, the platform's ChatGPT (OpenAI API). The import reports chatgpt missing (--publish publishes all the same), and the admin console's import result installs it in one click, with a preset configuration (an OpenAI connection through the Responses API, the console's first suggested model, OpenAI's hosts approved) and a sample key in place of yours. Store your OpenAI API key on the install's page (Configuration › Keys › openaiApiKey) before the pack monitors anything. Through the API, create the install as Installing a model app shows, named chatgpt.
Until you do, the run fails: OpenAI refuses the sample key, so every alert on its way to a case fails at triage, with no case and no decision, and no analyst sees it; approvals and rejections are decided by the rules alone. A model call that fails once the key is in does the same: an outage that outlasts the retries, an exhausted quota or a refusal fails the run at triage or narrative, and so does a workspace without any install named chatgpt. Failed runs show under Operate › Runs (Admin console: runs), and the vendor's answer shows in the invocation record and under the run's app calls; once the call can succeed, replay the run from triage (Replay from step), which starts from the rule results the run had. To ask Claude or Gemini instead, install that app under a name of your own and name it in the steps' app and in the pack's installs: every model app answers decide and generate in the same shape.
What a reviewer sees
A velocity_review case at the priority the triage gave, with the four results and their values, and the triage's answers (triage) and the note (narrative.text) in the run context; the model's calls are under Vendor checks. The subject's timeline in the admin console lists the events behind the results. The reviewer's reject becomes the decision; what the integrating system does with a rejected subject (hold payouts, close the account) is its call.
How to adapt it
Group by a promoted key instead of the subject (groupBy: ip with groupValuePath) to catch one device behind many accounts; add avg or max rules for unusually large single payments; use attributes filters for your own event vocabulary (channel, country). Dry-run a rule against a stored subject (POST /rule-definitions/:key/dry-run) and backtest a draft over recorded evaluations before publishing a new threshold. Ask the triage about your own signals, keep its options running from the most urgent down to the priority every alert needs (an option below high would let the model lower an alert), and point triage's next at review if your analysts want no note.
Run it
bash
pnpm policy:import examples/policies/transaction-velocity --publishThe check runs two fixtures against the model stub (scripts/ai-stub.mjs), whose answers a cue word in the merchant's description picks. Both ingest four payments of $1,500 over the last 90 minutes (two cards, one declined): the 24-hour sum of $6,000 fails, the burst, decline and card rules pass, and the score of 50 sends the alert to the model. expect.json is an online store in Nashville for band merchandise with most orders under $50; cued low, the stub answers explained no at 0.05 and priority critical and writes the note, a velocity_review case opens at critical, the check rejects it and the run completes with a manual reject. expect-explained.json is a Seattle coffee roaster whose wholesale customers pay invoices of $1,000 to $2,000 by card; cued high, the stub answers explained yes at 0.95 and priorityhigh, the case opens at high, the check approves it and the run completes with a manual approve.