Aller au contenu

Explanations

Ce contenu n’est pas encore disponible dans votre langue.

Every rule has an explanation: one sentence, with the real numbers filled in, telling the analyst why it fired. It is stored with each hit, and the latest one becomes the summary on the alert. A good explanation saves the analyst from rebuilding the rule’s arithmetic in their head.

Template: {{features.cash_in_count_48h}} cash-ins totalling {{txn.base_currency}} {{features.cash_in_sum_48h}} in 48h; this one ({{txn.base_currency}} {{txn.amount}}) is just below the {{txn.base_currency}} {{params.threshold}} threshold.
Analyst: 4 cash-ins totalling GHS 37,600 in 48h; this one (GHS 9,400) is just below the GHS 10,000 threshold.

Write {{ … }} around one of seven kinds of value, or {{= … }} around an expression (computed placeholders, below):

Placeholder Fills in Example
{{txn.<field>}} A field of the transaction, from the CEL reference {{txn.amount}}, {{txn.channel}}, {{txn.counterparty_country}}, {{txn.hour}}
{{features.<name>}} A feature this rule declares {{features.senders_7d}}
{{params.<name>}} A parameter of this version {{params.threshold}}
{{customer.<field>}} A field of the customer, from the CEL reference {{customer.risk_rating}}, {{customer.segment}}, {{customer.designations}}
{{account.<field>}} A field of the account, from the CEL reference {{account.type}}, {{account.age_days}}
{{counterparty.<field>}} A field of the other party, from the CEL reference {{counterparty.country}}, {{counterparty.provider}}
{{device.<field>}} A field of the device, from the CEL reference {{device.customer_age_hours}}
  • Label amounts with {{txn.base_currency}}, not a fixed currency: it gives your workspace’s base currency, the currency every amount is in, so {{txn.base_currency}} {{txn.amount}} reads GHS 9,400 in a Ghanaian workspace and NGN 9,400 in a Nigerian one. Every shipped pack does this from core 1.1.0, fraud 1.1.0 and gh-psp 1.2.0.
  • Spaces inside the braces are allowed: {{ txn.amount }} works.
  • One value from a map: add its key after a dot. {{customer.hours_since.sim_swap}} gives the hours since the customer’s latest SIM swap, as the shipped rule G3 does. The key must be written in lowercase letters, digits and underscores. A map on its own, such as {{customer.hours_since}}, prints the raw map, which reads badly.
  • Any other variable is not replaced: it appears in the alert exactly as typed. To work something out instead, use a computed placeholder.
  • A field, name or key that doesn’t exist shows as —, for example {{customer.hours_since.sim_swap}} for a customer with no SIM swap on record.
  • The template can be up to 500 characters.

Sometimes the value you want isn’t a single field but something worked out from several: which of the configured events happened, or how many hours ago. Put an equals sign after the opening braces and write a CEL expression, the same language as the rule itself:

{{= <CEL expression> }}

The expression sees exactly what the rule’s expression sees: txn, customer, account, counterparty, device, features, params and lists. Its result is formatted like any other value (see How values are formatted), so a list comes out separated by commas.

Example: H5 names the changes that fired it

Section titled “Example: H5 names the changes that fired it”

H5 fires on a payout soon after a profile or credential change. Its explanation, in the fraud pack 1.1.0, is:

Payout of {{txn.base_currency}} {{txn.amount}} after {{= params.change_events.filter(e, e in customer.hours_since && customer.hours_since[e] < double(params.window_hours)).map(e, e.replace('_', ' ') + ' ' + (customer.hours_since[e] < 1.0 ? string(int(customer.hours_since[e] * 60.0)) + 'm ago' : string(int(customer.hours_since[e])) + 'h ago')).join(', ') }} (rule window: {{params.window_hours}} hours).

Read the computed part from left to right:

  1. params.change_events.filter(…) keeps only the changes this rule is set to watch (by default phone, email, password reset, password change and device added) that happened within the rule’s window.
  2. .map(…) turns each one into words: phone_change becomes phone change 20h ago. A change less than an hour ago is given in whole minutes, email change 35m ago; an older one in whole hours.
  3. .join(', ') puts them in one line.

A customer who changed their phone number 20 hours ago and their email 35 minutes ago, and had a SIM swap an hour ago, sends GHS 2,400. The analyst reads:

Payout of GHS 2,400 after phone change 20h ago, email change 35m ago (rule window: 24 hours).

The SIM swap isn’t listed, because it isn’t one of the changes H5 watches. It is G3’s business.

When a version is saved, validated or tested, each computed placeholder is compiled like the rule’s expression. An unknown name or a syntax error is refused, and the problem names the placeholder, for example:

explanation {{= no_such_var }}: ERROR: <input>:1:1: undeclared reference to 'no_such_var' (in container '')

Some mistakes only show up with real data. txn, customer and the other variables hold values of any type, so an expression such as {{= txn.amount + 'x' }} is accepted on save and fails when it runs. A placeholder that fails, or that exceeds the same cost limit rules run under, shows as —, and the rest of the explanation is still filled in. The hit itself still counts: only the text is affected. Before you submit a version, select Test in the rule builder with sample data, and read the explanation it returns.

Each failure is also counted, by rule key, in the metric speculapro_rule_explanation_errors_total, so whoever runs SpeculaPro can see a broken placeholder without waiting for an analyst to notice dashes. See Monitoring.

  • The expression can’t contain }}, because that ends the placeholder.
  • The 500-character limit covers the whole template, expressions included.

A scheduled rule doesn’t fire on one transaction, so its explanation has a different set of values:

Placeholder Fills in Example
{{features.<name>}} The figures the run found for this subject. Each scheduled rule lists them in Pack contents {{features.senders}}, {{features.in_amount}}
{{params.<name>}} A parameter of this version {{params.window_hours}}
{{subject.<name>}} Text about the subject the run found, such as an agent’s peer group or the branches used {{subject.peer_group}}, {{subject.branch_list}}
{{txn.base_currency}} Your workspace’s base currency GHS

Other txn, customer, account and counterparty fields aren’t available and show as —. Computed placeholders are refused: scheduled rule explanations use {{features.x}}, {{params.x}}, {{subject.x}} and {{txn.base_currency}}, not computed {{= }} placeholders.

Value Shown as
A whole number With thousands separators: 50000 → 50,000
A number with a fraction Two decimals, no separators: 4950.5 → 4950.50
Text As is: momo, NG
A list Items separated by commas: high, unacceptable. An empty list shows as nothing at all
Yes/no (for example customer.is_pep) yes or no
Nothing (missing) —

Two things to watch:

  • “Unknown” numbers show as -1. customer.kyc_level, customer.age_days, account.age_days and customer.hours_since_last_change are -1 when not known, and the explanation prints -1. Avoid them in explanations unless the rule only fires when the value is known.
  • An empty list leaves a gap. “Designations: {{customer.designations}}” reads “Designations: “ for a customer with none. Use list placeholders in rules that only fire when the list has something in it.

So most amounts read naturally (GHS 49,500), but an amount with pesewas reads GHS 12345.67. Keep that in mind when you word the sentence. Putting the currency in front ({{txn.base_currency}} {{txn.amount}}) makes it clear either way.

Hours from customer.hours_since are rarely whole, so they read like 3.42 (hours, not 3 hours 42 minutes). Word the sentence so a decimal makes sense: “SIM swapped 3.42 hours before…”.

❌ Weak ✅ Strong Why
Rule B1 matched. {{features.cash_in_count_48h}} cash-ins totalling {{txn.base_currency}} {{features.cash_in_sum_48h}} in 48h; this one is just below {{txn.base_currency}} {{params.threshold}}. The analyst sees the pattern and the numbers at once
Suspicious activity detected. {{features.senders_7d}} different senders paid this account in 7 days. Says what is suspicious
Amount {{txn.amount}} > {{params.threshold}} Transaction of {{txn.base_currency}} {{txn.amount}} is at or above the {{txn.base_currency}} {{params.threshold}} threshold. Plain words, with the currency
Customer may be laundering money via agents. Cash-ins via {{features.agents_24h}} different agents totalling {{txn.base_currency}} {{features.cash_in_24h}} in 24h. States facts, not conclusions. The analyst decides
  • Does it make sense to someone who has never seen the rule’s expression?
  • Does it include the number that crossed the line, and the line itself?
  • Does it name the time window (“in 24h”, “in 7 days”)?
  • Would it still read well if a value were missing (—)?
  • If it has a computed placeholder, did you Test it with sample data?