Anti-money-laundering layering detection

Surface multi-hop transaction chains that obscure source-of-funds, longer than 3 hops with high values

All recipes· graph· 9 minutesadvancedcypher

Anti-money-laundering layering detection

Objective

Layering is the AML stage where dirty money is moved through a long chain of accounts to obscure its origin. Compliance teams need to find chains longer than three hops where the value preserved end-to-end is high. The graph view turns this into a single MATCH that traces money flow across banks, jurisdictions, and shell companies in one shot.

Step 1: Create the graph

// Accounts (with bank + jurisdiction)
MERGE (a:Account {id: "ACC-A", holder: "Sarah Chen",     bank: "Bay Trust",    country: "US"})
MERGE (b:Account {id: "ACC-B", holder: "Helios Holdings", bank: "Bay Trust",   country: "US"})
MERGE (c:Account {id: "ACC-C", holder: "Vortex Trading",  bank: "Cayman Bank", country: "KY"})
MERGE (d:Account {id: "ACC-D", holder: "BlueRock Ltd",    bank: "Riga Bank",   country: "LV"})
MERGE (e:Account {id: "ACC-E", holder: "Atlas Shell",     bank: "Sofia Bank",  country: "BG"})
MERGE (f:Account {id: "ACC-F", holder: "Mia Rossi",       bank: "Banca Roma",  country: "IT"})
MERGE (g:Account {id: "ACC-G", holder: "Polar Imports",   bank: "Tallin Bank", country: "EE"})
MERGE (h:Account {id: "ACC-H", holder: "Leo Park",        bank: "Seoul Bank",  country: "KR"})
MERGE (i:Account {id: "ACC-I", holder: "Eli Tanaka",      bank: "Tokyo Bank",  country: "JP"})

// Transfer edges (amount in USD, date)
// Layered chain: A -> B -> C -> D -> E -> F (5 hops, high preserved value)
MERGE (a)-[:TRANSFER {amount: 250000, date: "2026-04-01"}]->(b)
MERGE (b)-[:TRANSFER {amount: 245000, date: "2026-04-03"}]->(c)
MERGE (c)-[:TRANSFER {amount: 240000, date: "2026-04-05"}]->(d)
MERGE (d)-[:TRANSFER {amount: 235000, date: "2026-04-07"}]->(e)
MERGE (e)-[:TRANSFER {amount: 230000, date: "2026-04-09"}]->(f)

// Second suspicious chain: G -> H -> I (only 2 hops; should NOT trigger)
MERGE (g)-[:TRANSFER {amount: 180000, date: "2026-04-02"}]->(h)
MERGE (h)-[:TRANSFER {amount: 178000, date: "2026-04-04"}]->(i)

// Decoy noise transfers
MERGE (a)-[:TRANSFER {amount: 1200,  date: "2026-04-10"}]->(g)
MERGE (h)-[:TRANSFER {amount: 800,   date: "2026-04-12"}]->(b)
MERGE (f)-[:TRANSFER {amount: 50,    date: "2026-04-15"}]->(i);

Step 2: Detect layering chains

// Look for chains of length >3 where every hop is over $50k AND >85% of the value
// is preserved end-to-end (a hallmark of pass-through layering).
MATCH path = (src:Account)-[t:TRANSFER*4..6]->(dst:Account)
WHERE ALL(r IN t WHERE r.amount > 50000)
WITH src, dst, path, t,
     head(t).amount AS source_amount,
     last(t).amount AS final_amount
WHERE final_amount * 1.0 / source_amount > 0.85
WITH src, dst, path,
     [n IN nodes(path) | n.id]              AS account_chain,
     [n IN nodes(path) | n.country]         AS country_chain,
     length(path)                            AS hop_count,
     reduce(s = 0, r IN relationships(path) | s + r.amount) AS total_volume
RETURN account_chain, country_chain, hop_count, total_volume
ORDER BY hop_count DESC, total_volume DESC;

What's happening

  • TRANSFER*4..6 only matches chains of at least four hops — exactly where layering lives. Direct transfers and 2-hop "round-trip" patterns are ignored.
  • head(t) and last(t) access the first and last edges of the path to compute value retention, the strongest layering signal: if 90% of $250k arrives at the destination, it likely passed through unchanged.
  • country_chain reveals jurisdiction-hopping (US → KY → LV → BG → IT here) — auditors immediately see the geographic footprint.
  • In SQL this is essentially impossible without an iterative loop in application code — recursive CTEs that emit path arrays are a Postgres-specific extension and still require complex window arithmetic for retention scoring.
  • Run this incrementally on every new transfer to alert the FIU within seconds, not after a batch.

Try this next

MATCH (a:Account)-[t:TRANSFER]->(b:Account)
WHERE a.country <> b.country AND t.amount > 100000
RETURN a.country AS from_country, b.country AS to_country,
       count(t) AS transfers, sum(t.amount) AS volume
ORDER BY volume DESC;
MATCH path = (a:Account)-[:TRANSFER*3..]->(z:Account)
WHERE NOT (z)-[:TRANSFER]->()
RETURN a.holder AS originator, z.holder AS terminus, length(path) AS hops
ORDER BY hops DESC LIMIT 5;
MATCH (a:Account)-[t:TRANSFER]->(b:Account)
WITH a, count(t) AS out_degree
WHERE out_degree >= 3
RETURN a.id, a.holder, a.country, out_degree
ORDER BY out_degree DESC;

Tags

graphcypheramlfraudadvanced

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE