Lux Docs
Compliance

AML Engine

Real-time AML/CFT transaction monitoring — configurable rules DSL, weight-of-evidence scoring, sanctions screening, and case management in a single Go binary

The Lux AML engine (github.com/luxfi/aml, the amld service) is a real-time AML/CFT transaction-monitoring engine. It evaluates every transaction synchronously against a configurable rules DSL and a weight-of-evidence scoring model, screens parties against sanctions and PEP lists, opens cases for human review, and emits signed webhooks to downstream systems.

It is pure Go, ships as a single binary with an embedded admin UI, and persists rules, alerts, and cases through a Hanzo Base–backed SQLite store (per-org). No external services, no message broker, no ML runtime required to run.

Architecture

   POST /v1/aml/transactions
            |
   +--------v---------+     rules DSL (expr-lang)      +-------------+
   |  Rule evaluator  +-------------------------------->  20 starter |
   |  (per-tx, sync)  |                                 |  typologies |
   +--------+---------+                                 +-------------+
            |
   +--------v---------+     weight-of-evidence [0,1]
   |     Scorer       |     (pure Go; ScorerPlugin
   |                  |      interface for Zen ML)
   +--------+---------+
            |
   +--------v---------+     highest-severity action wins
   | resolveAction()  +--> allow | review | flag | block | report
   +--------+---------+
            |
     block/report  -->  auto-create Case  -->  signed webhook (HMAC-SHA256)
ConcernPackageNotes
Domain typespkg/typesTransaction, Entity, Rule, Alert, Case
Enginepkg/engineevaluate → alerts + score + action
Rules DSLpkg/engine/evaluator.goexpr-lang compiler + helpers
Scoringpkg/engine/scoring.goweight-of-evidence; ScorerPlugin interface
Rule librarypkg/rules20 starter typologies
Sanctionspkg/sanctionsJaro-Winkler + token fuzzy match; OFAC SDN XML ingest
Casespkg/casescreate, assign, status, events, resolve
Webhookspkg/webhooksigned delivery + retry + dead-letter
HTTP APIpkg/api/v1/aml/*

Transaction Ingest Flow

Every transaction is evaluated synchronously; the response carries the decision.

POST /v1/aml/transactions
  -> Parse + validate
  -> Resolve entity from user_id
  -> EvalAll(rules, {tx, entity})
       -> filter by jurisdiction, asset_class, enabled
       -> compile + run each rule DSL via expr-lang
  -> Score(hits) -> weight-of-evidence in [0,1]
  -> resolveAction(alerts) -> highest-severity action wins
  -> if block/report: auto-create Case
  -> return { action, score, alert_ids, case_id? }
curl -X POST http://localhost:8090/v1/aml/transactions \
  -H "X-Org-Id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u_123","notional":12500,"currency":"USD","asset_class":"fiat"}'
# -> {"action":"report","score":0.82,"alert_ids":["al_..."],"case_id":"case_..."}

All endpoints require the X-Org-Id header, which the gateway sets from the JWT owner claim.

Rules DSL

Rules are expressions compiled and evaluated with expr-lang against the transaction and its resolved entity. Each rule declares a jurisdiction/asset-class filter, a severity, and an action. Test any expression before enabling it:

curl -X POST http://localhost:8090/v1/aml/rules/test \
  -H "X-Org-Id: $ORG_ID" \
  -d '{"expr":"notional > 10000 && currency == \"USD\"","tx":{"notional":12500,"currency":"USD"}}'
# -> {"matched":true}

Starter Typologies

amld ships with 20 baseline rules covering the standard AML typologies:

#RulePatternAction
1CTR Thresholdnotional > 10000 && USDreport
2Structuringnotional in [9000, 10000)flag
3Velocity (daily)sum_last_24h > 50000review
4Velocity (monthly)sum_last_30d > 500000review
5First-Time Largefirst_tx && notional > 25000review
6Round-Tripis_round_trip 24hflag
7Sanctioned Jurisdictionjurisdiction in sanctionedblock
8PEP LargePEP && notional > 10000review
9Unusual Hourhour in [2,3,4]flag
10New Counterparty Largefirst_counterparty && notional > 5000review
11Dormant Reactivationlast_tx > 180d && notional > 10000review
12Crypto Mixeris_mixer_addressblock
13Darknet Marketis_darknet_marketblock
14IP/Geo Mismatch!geo_matchflag
15Layeringlayering_score > 0.8review
16Smurfingsmurfing_detected 7dreview
17Wash Tradingwash_trade_detected 1hflag
18Sanctions (direct)sanctions_hit(entity)block
19Sanctions (counterparty)sanctions_hit(counterparty)block
20Travel Rulenotional > travel_rule_thresholdreport

Scoring

Rule hits feed a weight-of-evidence model that produces a risk score in [0,1] (pure Go math — no external ML runtime). The ScorerPlugin interface lets you swap in a model served by the Hanzo Zen gateway without changing the engine. The final action is the highest-severity action among all matched rules.

ActionMeaning
allowNo material risk; proceed
reviewManual review queued
flagRecorded for periodic review
blockReject the transaction; case opened
reportReject + file (e.g. CTR/SAR); case opened

Sanctions Screening

Names are screened against configurable lists (OFAC SDN, UN, EU, HMT) using Jaro-Winkler similarity plus token-based fuzzy matching to catch transliteration and spelling variants. The OFAC SDN XML feed is parsed and fetched natively.

curl -X POST http://localhost:8090/v1/aml/sanctions/search \
  -H "X-Org-Id: $ORG_ID" \
  -d '{"name":"Vladimir Ivanov"}'
# -> {"matches":[{"list":"ofac_sdn","score":0.94,"ref_id":"..."}]}

Cases & Alerts

Any block or report action auto-opens a case. Alerts and cases follow the lifecycle open -> investigating -> escalated -> closed/filed. Cases carry an event log (notes, status changes, assignment) for the audit trail.

curl http://localhost:8090/v1/aml/cases?status=open -H "X-Org-Id: $ORG_ID"
curl -X POST http://localhost:8090/v1/aml/cases/$CASE_ID/events \
  -H "X-Org-Id: $ORG_ID" \
  -d '{"type":"note","body":"Escalated to compliance officer"}'

API

MethodPathDescription
POST/v1/aml/transactionsIngest a transaction; sync rule eval; returns action
GET/v1/aml/transactions/{id}/alertsAlerts for a transaction
GET/v1/aml/casesList cases (filter by status)
POST/v1/aml/cases/{id}/eventsAdd a case event (note, status change)
GET/v1/aml/rulesList all rules
POST/v1/aml/rules/testDry-run a DSL expression
POST/v1/aml/sanctions/searchSearch sanctions lists by name
GET/v1/aml/healthHealth check

Webhooks

Downstream systems subscribe to signed (HMAC-SHA256) webhook deliveries with retry and dead-letter handling. Events: aml.flagged, aml.cleared, kyc.approved, trade.executed.

Admin UI

A dark-themed admin dashboard is embedded in the binary (via go:embed) at /_/aml/ — Dashboard, Cases, Rules, Alerts, and Sanctions search, served with a hash router so no server rewrites are needed.

Deployment

amld serve --http=0.0.0.0:8090        # single binary
  • Image: ghcr.io/luxfi/aml:{env}
  • K8s: Deployment, replicas=1 (SQLite single-writer), PVC for /data
  • Replication: hanzoai/replicate sidecar streams the age-encrypted WAL to S3

The engine is consumed in-process by luxfi/compliance and over HTTP by regulated products such as an ATS — set AML_ENGINE_URL to the amld address and treat an unreachable engine as fail-closed for regulated flows.

On this page