Lesson 70Compliance flowsAdvanced

AML transaction monitoring: structuring + smurfing detection

When you can't prove it's clean, you treat it as suspicious. The ledger reflects the decision.

By Solomon Ajayi · Free to read, no signup

Lesson 34 introduced AML transaction monitoring at the surface level. This lesson zooms in on TWO specific patterns your engine has to detect: STRUCTURING (a single user splits one large transaction into many small ones to stay under a reporting threshold) and SMURFING (multiple users coordinate to do the same, many small inbounds to a single beneficiary). Both are LITERAL crimes (BSA in the US, similar in CBN guidelines). Your monitor needs windowed aggregates and graph queries. The DECISION it produces, flag, hold, or pass, drives ledger entries that move flagged-user funds into the AML-Hold bucket pending review.

Structuring and smurfing both attack the same weakness: a reporting threshold that looks at one transaction at a time. Structuring is one user splitting a large amount into many deposits each just under the limit. Smurfing is many unrelated users feeding small amounts into one beneficiary to the same effect. Neither pattern is visible in any single transaction; the crime only appears when you look across a window of time or a graph of accounts.

That is why detection runs in two loops. A per-transaction loop is synchronous and blocks an obvious hard fail at the moment of the payment. A windowed-aggregate loop is asynchronous, summing deposits over a rolling window to catch structuring, and running concentration queries over the account graph to catch smurfing. The detection fires after the individual deposits have already posted normally, so the resulting action reaches back and moves the flagged sum from User Wallet (Available) into User Wallet (AML Hold).

The journal entries are almost identical for both patterns: Available down, AML Hold up for a flag, the reverse for a clear, and AML Hold to Blocked Funds for a confirmed bad actor. What distinguishes them is the metadata, structuring points its source event at a windowed-aggregate alert, smurfing points at a graph-query alert, so the investigation evidence differs even though the bookkeeping does not.

Worked example, step by step

User makes 5 deposits of ₦9,000 in 3 days (structuring pattern)

Cash Transaction Report threshold is ₦10,000 in Nigeria (varies). User's 5 deposits sum to ₦45,000, far over the threshold, but each is individually under. Classic structuring. Your windowed aggregate over a 7-day rolling window catches it.

Aggregate detection: 5×₦9k structuring → AML Hold
AccountDebitCredit
User Wallet (Available) (2000)₦45,000.00
User Wallet (AML Hold) (2170)₦45,000.00

Each individual deposit IS posted normally (we won't repost; lesson 1 shape × 5). The detection fires AFTER the aggregates trip. The action: move the full sum (₦45,000) to AML Hold pending compliance review.

10 unrelated users send ₦5,000 each to one beneficiary in 2 hours (smurfing)

Different pattern. The CONCENTRATION (10 small inbounds from unrelated accounts in 2 hours) flags the beneficiary's account. Total: ₦50,000. The beneficiary's available wallet moves to AML Hold pending review.

Aggregate detection: smurfing concentration → AML Hold
AccountDebitCredit
User Wallet (Available) (2000)₦50,000.00
User Wallet (AML Hold) (2170)₦50,000.00

Same shape as structuring. The DIFFERENCE is what the metadata records, sourceEventId points to the graph-query alert, reasonCode is 'smurfing_concentration_detected'. The bookkeeping is identical; the investigation evidence differs.

Takeaway

AML transaction monitoring runs in TWO loops: per-transaction (synchronous, blocks if hard-fail) and windowed-aggregate (asynchronous, fires on rolling windows for structuring/smurfing/velocity patterns). The journal entries are simple, Available → AML Hold for a flag, Hold → Available for a clear, Hold → Blocked Funds for a confirmed bad actor. The hard work is in the DETECTION layer (Esper/Flink for streaming aggregates, graph queries for concentration patterns) and the INVESTIGATION workflow (case management for compliance officers). Build both, and your sponsor bank's regulator-facing reports practically write themselves.

The code behind it

Structuring detection: a rolling 7-day window that catches many just-under-threshold deposits summing past the report limit.

-- Amounts are in kobo (₦1 = 100). CTR threshold = ₦10,000 = 1_000_000 kobo.
-- Structuring = several deposits each UNDER the threshold whose 7-day rolling sum is OVER it.
WITH windowed AS (
  SELECT
    user_id,
    posted_at,
    amount_kobo,
    -- Trailing 7-day rolling sum of this user's deposits, inclusive of the current row.
    -- RANGE (not ROWS) + an INTERVAL offset gives a true time window: every row whose
    -- posted_at falls in [current.posted_at - 7 days, current.posted_at] is summed.
    SUM(amount_kobo) OVER w   AS rolling_sum_kobo,
    COUNT(*)         OVER w   AS deposits_in_window
  FROM deposits
  WHERE amount_kobo < 1000000   -- only individually under-threshold deposits qualify
  WINDOW w AS (
    PARTITION BY user_id
    ORDER BY posted_at
    RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
  )
)
SELECT user_id,
       MAX(rolling_sum_kobo)   AS peak_window_sum_kobo,  -- e.g. 4_500_000 = ₦45,000
       MAX(deposits_in_window) AS peak_window_count
FROM windowed
WHERE rolling_sum_kobo > 1000000   -- aggregate crosses the threshold the splits dodged
  AND deposits_in_window >= 3
GROUP BY user_id;                  -- each flagged user -> Available (2000) to AML Hold (2170)

Practice this on a real ledger

Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.

More in this section

Search lessons

Type to find any of the 85 lessons. Press Enter to open.