Rules engine
The Anti-Fraud rules engine is similar in shape to the AML rules engine but distinct in vocabulary and action verbs. Rules here output allow, flag, review, or block — not just severity scores.
Anatomy
{
"id": "rul_01HXY...",
"code": "VL-003",
"name": "High amount unverified counterparty",
"description": "Transactions ≥ IDR 50M to a counterparty not in the customer's known-good list.",
"lane": "transaction",
"category": "velocity",
"type": "threshold",
"severity": "high",
"action": "review",
"bypassMl": false,
"score": 20,
"conditions": { ... },
"status": "active",
"approvedById": "usr_01HXY...",
"createdAt": "...",
"updatedAt": "...",
"fireStats30d": {
"fired": 8421,
"truePositives": 412,
"falsePositives": 8009
}
}Action verbs
| Action | Effect |
|---|---|
allow | Reduces fraudScore (negative contribution). Used for "known good" rules. |
flag | Adds to score; if final decision is allow, the alert is still recorded. |
review | Adds to score; if final decision is flag/allow, upgrade to review. |
block | Force-block regardless of score (subject to bypassMl policy). |
bypassMl
If bypassMl: true, the rule's action is never downgraded by ML suppression. Use for hard regulatory or known-bad rules (sanctioned country, internal blocklist). Default: false (ML can suppress).
fireStats30d
A rolling 30-day window of true/false positive counts based on analyst dispositions on the resulting alerts. Use it to tune your rule library — high-FP rules should have their score lowered or be retired.
List + create
/api/rulesFilters: lane, status, type, category (tag-based), search (free-text against name and description).
/api/rulesNew rules start in draft. They require approval before going active (see below). Admin-only.
Approval
/api/rules/{id}/approve{
"decision": "approve | reject",
"notes": "Validated against last 90 days; FP rate 8.2% acceptable."
}Four-eyes enforced: approver must differ from drafter when the rule's action is block (configurable for other actions).
Backtest
/api/rules/{id}/backtest{
"lookbackDays": 90,
"sampleSize": 50000
}Returns:
{
"data": {
"totalEvaluated": 412875,
"wouldHaveFired": 3284,
"estimatedFalsePositiveRate": 0.082,
"byLane": { "onboarding": 412, "transaction": 2872 },
"topMatchingCustomers": [
{ "customerId": "cus_01HXY...", "fireCount": 14 }
]
}
}Version history
/api/rules/{id}/versionsReturns every prior version with the editor + timestamp. Required for examiner audit ("what was the rule on date X?").
Categories we ship by default
| Category | Sample rules |
|---|---|
| Velocity | High amount, repeat-merchant burst, daily-sum exceed |
| Device | Jailbroken, rooted, emulator, attestation-fail |
| Geography | High-risk-country IP, geo-distance vs registered address |
| Synthetic identity | Email-domain young, phone-recently-ported, name-DOB mismatch |
| Account-takeover | New device + sensitive action, IP change + immediate transfer |
| Mule / structuring | Funnel-account pattern, fast-pass-through, sudden activity |
| Card / QRIS | High-risk MCC, prepaid-instrument burst, declined-then-approved |
| Onboarding | NIK-on-blocklist, duplicate-NIK-different-account, sanctioned country |
Out-of-box ~120 rules across these categories. You can disable, edit, score-tune, or fork any of them.
Status flow
draft → pending_approval → active
↓
paused (admin emergency-off)
↓
retired (terminal)Retiring a rule keeps it in audit logs forever but stops it from firing on new evaluations. Use this rather than deletion.
Right-size scores by elasticity, not gut
We strongly recommend that all new rules be tested via backtest before promotion. The reflex is to give important-feeling rules high scores; backtest lets you see what the rule actually does to your block rate and FP rate before it touches production traffic.