Skip to content

C. Velocity and behaviour change

Fixed thresholds treat every customer the same. Velocity and behaviour rules do something smarter: they compare a customer with their own history. A GHS 20,000 week is routine for a wholesaler and alarming for a student.

These rules catch:

  • Accounts that have been taken over — a fraudster spends very differently from the owner.
  • Accounts that have been sold or rented to criminals — a quiet account suddenly becomes busy.
  • Pass-through accounts — money arrives and leaves almost immediately, which is how mule accounts are used.

SpeculaPro builds a profile for each customer and account — typical counts, typical amounts, typical hours — and keeps it up to date with every transaction. See Feature store for how this works.

ID Rule Mode Status
C1 Transaction count spike vs profile RT/PE Ships (core, post-event)
C2 Value spike vs profile RT/PE Ships (core, post-event)
C3 Dormant account reactivated with high activity PE/SCH Ships (core, post-event)
C4 New account, high value early RT/PE Ships (core, post-event)
C5 Rapid in-and-out (pass-through) PE Ships (core, post-event)
C6 Unusual hour for this customer RT Ships (fraud, real-time)
C7 Burst of payouts RT Ships (fraud, real-time)

What it looks for: an account making many more transactions in a day than it usually does: a takeover, a newly recruited mule, or a business that has outgrown its profile.

Example

With the defaults (multiplier 5, min_count 10, min_baseline_days 30), for an account that made 60 transactions in the 29 days before today (about 2 a day):

  • ✅ Fires: 12 transactions in the last 24 hours: at least 10, and more than 5 × 2.07.
  • ❌ Does not fire: 9 transactions in a day (under min_count), however quiet the account usually is.
  • ❌ Does not fire: an account opened 10 days ago (younger than min_baseline_days), or one with no opening date.

Parameters

Parameter Meaning How to tune
multiplier How many times the account’s average day counts as a spike Lower values catch more; 3–10 are typical values to test
min_count Transactions in the last 24 hours, at least Stops a jump from 1 to 6 transactions alerting
min_baseline_days How old the account must be before the rule applies, in days (7 to 3,650) 30 days or more gives a stable average

The windows are fixed: the last 24 hours, against the 29 days before them. Every transaction counts, in either direction.

Mode and pack: post-event, in core 1.1.0.

False positives: festive seasons, salary days and month-end, when everyone is busier. Watch alert volume around these dates during shadow testing.

Data it needs: the account’s opening date (opened_at on the account), and a continuous transaction history.


What it looks for: a customer moving a much larger total than usual in a period.

Example

With multiplier 5 and window 7 days, for a customer whose weekly total is usually about GHS 2,000:

  • ✅ Fires: GHS 14,000 moved this week (7× normal).
  • ❌ Does not fire: GHS 6,000 in a week when rent was paid (3×).

Parameters

Parameter Meaning How to tune
multiplier How many times the usual value counts as a spike As C1
window The period totalled Days to weeks

Mode and pack: real-time or post-event.

False positives: one-off life events — buying a car, paying a deposit on land, funeral contributions. These are exactly what an analyst should confirm and close quickly; record the explanation in the case.

Data it needs: amount (base currency) and a continuous history.


C3 Dormant account reactivated with high activity

Section titled “C3 Dormant account reactivated with high activity”

What it looks for: an account that has been inactive for a long time suddenly receiving or sending significant amounts. Dormant accounts are attractive to criminals: they are already verified, and their owner is not watching them.

Example

With the shipped rule, dormant_days 180 and min_amount GHS 10,000:

  • ✅ Fires: an account opened two years ago, with nothing in the last 90 days, receives GHS 25,000 and starts sending it on.
  • ❌ Does not fire: the same account receives GHS 300.
  • ❌ Does not fire: an account opened four months ago and unused since; it is younger than 180 days.

Parameters

Parameter Meaning How to tune
dormant_days How old the account must be (the quiet period itself is fixed at 90 days) At least the 90-day quiet period, so only established accounts count
min_amount The transaction needed after reactivation to fire Set so small “test” transactions don’t trigger alerts

Mode and pack: post-event, or as a scheduled sweep.

False positives: customers returning from abroad, seasonal workers, students. A check of the customer’s contact details and recent KYC usually resolves these.

Data it needs: full history (so the dormant period can be seen), account status.


What it looks for: a newly opened account moving large amounts straight away. Criminals open accounts specifically to receive fraud proceeds and use them before they are noticed.

Example

With account_age_days 30 and threshold_amount GHS 20,000:

  • ✅ Fires: an account opened 9 days ago receives GHS 35,000.
  • ❌ Does not fire: an account opened 9 days ago receives a GHS 2,500 salary.

Parameters

Parameter Meaning How to tune
account_age_days How long an account counts as “new” 30–90 days are common
threshold_amount Amount that is unusual for a new account Lower for retail, higher for business onboarding

Mode and pack: real-time or post-event.

False positives: new business accounts receiving their opening balance, customers moving their salary to a new bank. Segment business customers separately.

Data it needs: account opening date (opened_at), amount.


What it looks for: money that arrives and leaves again almost immediately, with nearly the whole amount passed on. The account is a pipe, not a destination — the defining behaviour of a money mule.

Example

With out_pct_of_in 90%, window 24 hours and min_amount GHS 5,000:

  • ✅ Fires: GHS 18,000 arrives at 10:05; GHS 17,500 is sent to three other wallets by 13:30.
  • ❌ Does not fire: GHS 18,000 arrives and GHS 4,000 is spent over the next week.

Parameters

Parameter Meaning How to tune
out_pct_of_in Share of incoming money that leaves 80–95% are typical values to test
window How quickly it must leave Hours to a day or two
min_amount Minimum amount passed through Ignores small everyday transfers

Mode and pack: post-event.

False positives: customers who move their own salary from one account to another on payday, and payment aggregators. Check whether the outgoing destination is the customer’s own account.

Data it needs: amount, direction, counterparties.


What it looks for: a payout at an hour this customer does not use, for example 03:00 for someone who only ever pays during the day. Payouts while the owner sleeps are a common sign that someone else is using the account.

Example

With the defaults (min_history 30, max_prior_at_hour 0, min_amount 1,000), for a customer with 80 transactions in 90 days, all between 07:00 and 21:00:

  • ✅ Fires: a GHS 4,000 transfer at 02:40. None of the 80 was in the 02:00 hour, so the customer is asked to prove it’s them.
  • ❌ Does not fire: a GHS 50 airtime purchase at 02:40 (below min_amount).
  • ❌ Does not fire: a customer with only 12 transactions in 90 days (not enough history).
  • ❌ Does not fire, for now: any customer in the first weeks after the upgrade that introduced the hour counters, until those counters have seen 30 of their transactions.

Parameters

Parameter Meaning How to tune
min_history Transactions in the last 90 days before it applies, at least Higher means a better-known pattern and fewer alerts
max_prior_at_hour Earlier transactions at this hour of the day, at most 0 means “never at this hour”; 1 tolerates a single earlier one
min_amount Payout at or above (base currency) Keeps small late-night purchases out

An hour means a clock hour (02:00 to 02:59), and every transaction counts towards the history, in either direction.

Mode and pack: real-time, added in fraud 1.1.0 and made like-for-like in fraud 1.2.0. Like every real-time rule, it only sees payments sent to the decision API.

False positives: shift workers, and travellers in other time zones. A step-up is a light answer: the genuine customer confirms and pays.

Data it needs: accurate transaction timestamps (occurred_at) and enough history.


What it looks for: many outgoing payments in a few minutes. A genuine customer rarely sends five payments in ten minutes; someone emptying a wallet they’ve taken over, or a script, often does. It’s a velocity rule built for real time: it has to act while the burst is still happening.

Example

With max_payouts 5 and min_total GHS 1,000:

  • ✅ Fires: six transfers of GHS 400 to different wallets between 22:01 and 22:07. The fifth already fires (five payouts, GHS 2,000).
  • ❌ Does not fire: five airtime purchases of GHS 10 in ten minutes (total under min_total).
  • ❌ Does not fire: four payouts of GHS 3,000 (fewer than max_payouts).

Parameters

Parameter Meaning How to tune
max_payouts Payouts in 10 minutes, this one included, from which it fires Above what your busiest genuine customers do: merchants and agents may need a separate rule
min_total Smallest 10-minute total that counts Keeps bursts of tiny payments (airtime, transport) quiet

Mode and pack: real-time, fraud pack. Ships today. It answers step_up, so a genuine customer passes with an OTP while a fraudster without the phone is stopped.

False positives: businesses paying staff or suppliers in a batch, and customers splitting a bill. Consider a suppression for a known business, or a segment check in a custom version.

Data it needs: direction and amount, with payouts sent through a real-time decision as they happen. A backlog sent late isn’t counted; see Late and backdated data.