Rule Authoring Guide
This guide is for compliance officers and rule authors who define Merlon's CDD (customer due diligence) risk weights, country risk tables, and transaction-monitoring (TM) scenarios. Under the Configuration as the Product design principle, these rules are expressed as versioned JSON/YAML configuration rather than application code, so they can be authored, reviewed, and approved through your institution's own rule-governance process instead of a software release.
Where rules live
Rule content lives under content/ in the repository:
content/schema/— the JSON Schemas that validate each rule type.content/_sample/— Apache-2.0-licensed sample rules that demonstrate the format. They are not production-ready controls; the sample directory'sREADME.mdand every sample file mark this explicitly (is_sample: truewhere applicable).
The Rust Engine loads CDD weights, country risk tables, and TM scenarios
directly from operator-supplied files — for example, MERLON_CDD_WEIGHTS_PATH
and MERLON_TM_SCENARIOS_PATH (see
Configuration Reference). Because these files sit
outside the database-backed rule audit trail, your organization is
responsible for their source control, change approval, access control, and
backup, as described in ADR-0012 (Engine Configuration File Trust Boundary).
The Engine
emits a deterministic digest of the loaded configuration at startup and
through its ConfigService RPC so a deployed rule set can be verified after the
fact — this supports audit, but does not replace file-level access control.
CDD weight definitions
A CDD weight definition assigns weighted risk factors ("axes") to a customer
and derives a risk tier from the combined score. The schema is
content/schema/cdd_weight_v1.json
(reference). A minimal, real example is
content/_sample/cdd_basic_weights.yaml:
schema_version: cdd_weight_v1
id: cdd_basic_weights
name: Basic CDD Risk Weights
version: 1
is_sample: true
axes:
customer_attributes:
weight: 0.25
factors:
- name: customer_type
scores:
individual: 1
corporate_domestic: 2
corporate_foreign: 4
- name: account_age_months
ranges:
- max: 3
score: 4
- max: 12
score: 2
- min: 12
score: 1
geography:
weight: 0.30
factors:
- name: country_risk
scores:
low: 1
medium: 3
high: 5
tier_thresholds:
low:
max_score: 2.0
medium:
min_score: 2.0
max_score: 3.5
high:
min_score: 3.5
Required fields per the schema: schema_version (must be the literal
cdd_weight_v1), id (lowercase, ^[a-z][a-z0-9_]*$), name, version
(a positive integer), axes, and tier_thresholds (must define low,
medium, and high). Each entry under axes requires a weight (0–1) and a
factors array; factors may score a value directly (scores, keyed by an
observed value) or score a numeric range (ranges, each with min/max
bounds and a score).
A factor can also delegate scoring to the country risk table instead of an
inline scores map, by setting its source to country_risk_table
(see engine/crates/merlon-engine/src/scoring/country_risk.rs) — use this
when a factor's risk-by-country logic should be maintained in one place
rather than duplicated across every CDD weight definition.
Set is_sample: false (or omit it) once you replace a sample with a
production rule, per the guidance in content/README.md.
Country risk tables
A country risk table assigns a numeric risk score (1–5) per ISO country code,
with a default for countries not explicitly listed. The schema is
content/schema/country_risk_v1.json
(reference). Example, from
content/_sample/country_risk_sample.yaml:
schema_version: "1.0"
content_type: country_risk_table
name: Sample Country Risk Table
effective_date: 2026-07-01
default_score: 3
countries:
JP: { score: 1 }
US: { score: 1 }
KP: { score: 5, reason: "FATF blacklist / foreign exchange sanctions" }
IR: { score: 5, reason: "FATF blacklist" }
MM: { score: 4, reason: "FATF grey list" }
Required fields: schema_version, content_type (the literal constant
country_risk_table), effective_date (a date), default_score (1–5), and
countries. Each country key must match ^[A-Z]{2}$ (an ISO 3166-1 alpha-2
code) and its entry requires a score; reason is optional but recommended
for traceability of why a jurisdiction was scored the way it was, in keeping
with the Auditability First principle.
TM scenarios
A TM (transaction monitoring) scenario defines a detection rule — for
example, an aggregation-based structuring check — and the severity of an
alert it produces. Author new scenarios against the v2 schema,
content/schema/tm_scenario_v2.json
(reference). Example, adapted from
content/_sample/tm_scenarios/structuring_basic.yaml:
schema_version: "2.0"
scenario_id: tm_structuring_basic
name: "Structuring Detection (Basic)"
description: "Detects transaction splitting intended to evade reporting thresholds"
type: aggregation
conditions:
transaction_type:
- deposit
- transfer_in
aggregation:
field: amount
function: sum
period: 24h
group_by: customer_id
threshold:
by_customer_type:
individual:
by_risk_tier:
LOW: 2000000
MEDIUM: 1000000
HIGH: 500000
corporate_domestic:
by_risk_tier:
LOW: 2000000
MEDIUM: 1000000
HIGH: 500000
additional:
min_transaction_count: 3
evaluation_mode: both
severity: HIGH
tags:
- structuring
Required fields: schema_version, scenario_id, name, type (currently
only aggregation is defined), conditions, and severity. Under
conditions, threshold.by_customer_type lets a scenario set a different
threshold per customer type, and within each customer type,
by_risk_tier.{LOW,MEDIUM,HIGH} sets thresholds by the customer's CDD risk
tier — this is the concrete mechanism behind the Score-Driven Architecture
principle: a customer's CDD score determines which TM threshold applies to
them. evaluation_mode controls whether the scenario runs in the batch job,
inline, or both; if omitted, v2 scenarios default to batch.
v1 content and dual support
Existing tm_scenario_v1 content is not being force-migrated. Per
ADR-0006 (tm_scenario_v2 Schema and v1 Dual Support), the Engine
dual-supports v1 content for at least 12 months from the v2 rollout,
internally converting v1's flat risk_tier_adjustments into the equivalent
"same threshold for every customer type" v2 shape at load time, without
changing the resulting evaluation semantics. content/schema/tm_scenario_v1.json
(reference) remains available for that
compatibility period. New scenarios should still be authored directly against
v2 rather than v1.
Validating rule files
Validate every rule file against its schema before deploying it — this is a
mechanical check that should run as part of your review or CI process, ahead
of any organizational approval step. The schema reference pages under
Rule Schemas document every field and constraint for
cdd_weight_v1, country_risk_v1, tm_scenario_v1, and tm_scenario_v2 and
are generated directly from the JSON Schema files in content/schema/, so
they stay in sync with what the Engine actually enforces.
Score-driven rollout
Because CDD score is the central axis of the system (see Architecture and ADR-0004, Score-Driven Architecture), a change to a CDD weight definition can shift which risk tier a customer falls into, which in turn changes which TM thresholds apply to them, their case priority, and their screening frequency. Treat CDD weight changes with the same or greater caution as TM scenario changes, since their effect is indirect but system-wide rather than confined to a single detection rule.
The docs/configuration.md operational-thresholds guidance applies equally
here: the sample rules in content/_sample/ are examples, not production
defaults. Choose thresholds from your institution's own documented risk
assessment, test changes before deploying them, and route changes through
your rule-governance process. Under the Fail-Alert principle, resolve
uncertainty about a new or changed rule toward additional review rather than
suppressed alerts — for example, prefer a wider net (more matches reviewed
and dismissed) over a narrower one (fewer alerts but a higher chance of a
missed detection) when a threshold choice is ambiguous.