Skip to content

SynapCores / Recipe

Fraud ring detection in under 50ms

Detect circular money-laundering patterns up to length 4 by finding cycles in a transaction graph

All recipes· graph· 7 minutesintermediatecypher
Instance: localhost:8080

Opens your running SynapCores (Fraud ring detection in under 50ms will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

⚠️ Known issue (2026-10-16): on the current shipped engine builds (v2.1.3-ce and v2.2.0-ce) this recipe returns INCORRECT results and should not be relied on yet. Closed-path cycle matching is not correct: a repeated node variable (e.g. (x)…->(x)) is not bound to the same node, so the query returns open chains instead of the real ring and drops the true cycle. Separately, MERGE on a relationship currently ignores its properties, so distinct transfers can collapse into one edge and a ring hop is lost. The detected ring and its totals are therefore wrong. A fix is tracked with engineering; the recipe will be re-certified once it lands.

Try it in your browser: the SynapCores Playground runs a Cypher ring walk over a live identity graph. No install.

Objective

Fraud rings move money in circles — A pays B pays C pays A — to make stolen funds look like legitimate revenue. The pattern is invisible to row-level rules but obvious as a cycle in the transaction graph. This recipe builds a small money-flow graph with one embedded ring and finds it with a single MATCH on a closed path.

Step 1: Create the graph

MERGE (a:Account {id: "ACC-001", holder: "Sarah Chen",     country: "US"})
MERGE (b:Account {id: "ACC-002", holder: "Acme Logistics", country: "US"})
MERGE (c:Account {id: "ACC-003", holder: "Bay Tech LLC",   country: "US"})
MERGE (d:Account {id: "ACC-004", holder: "Raj Patel",      country: "US"})
MERGE (e:Account {id: "ACC-005", holder: "Mia Rossi",      country: "IT"})
MERGE (f:Account {id: "ACC-006", holder: "Leo Park",       country: "KR"})
MERGE (g:Account {id: "ACC-007", holder: "Zoe Williams",   country: "UK"})
MERGE (h:Account {id: "ACC-008", holder: "Eli Tanaka",     country: "JP"})

// Legitimate-looking cash flow
MERGE (a)-[:TRANSFER {amount: 1200,  date: "2026-04-02"}]->(b)
MERGE (b)-[:TRANSFER {amount: 980,   date: "2026-04-03"}]->(g)
MERGE (g)-[:TRANSFER {amount: 600,   date: "2026-04-05"}]->(h)
MERGE (d)-[:TRANSFER {amount: 1500,  date: "2026-04-06"}]->(a)

// THE RING: c -> d -> e -> f -> c, all over 50k, within a tight window
MERGE (c)-[:TRANSFER {amount: 78000, date: "2026-04-10"}]->(d)
MERGE (d)-[:TRANSFER {amount: 76500, date: "2026-04-11"}]->(e)
MERGE (e)-[:TRANSFER {amount: 74000, date: "2026-04-12"}]->(f)
MERGE (f)-[:TRANSFER {amount: 71500, date: "2026-04-13"}]->(c)

// Some additional decoy flow
MERGE (h)-[:TRANSFER {amount: 250,   date: "2026-04-14"}]->(a)
MERGE (b)-[:TRANSFER {amount: 410,   date: "2026-04-15"}]->(h);

Step 2: Find suspicious cycles

// Closed cycles of 3 to 4 hops with every hop above $50k:
// walk 2-3 hops out, then require a transfer from the last account back to the start.
MATCH p = (start:Account)-[t:TRANSFER*2..3]->(last:Account)
MATCH (last)-[closing:TRANSFER]->(back:Account)
WHERE back.id = start.id
  AND ALL(r IN t WHERE r.amount > 50000)
  AND closing.amount > 50000
  AND ALL(n IN nodes(p) WHERE start.id <= n.id)   // report each ring once
RETURN [n IN nodes(p) | n.id]                     AS ring_accounts,
       [r IN t | r.amount]                         AS path_hops,
       closing.amount                              AS closing_hop,
       reduce(s = 0, r IN t | s + r.amount) + closing.amount AS total_moved;

Expected: one ring — ACC-003 → ACC-004 → ACC-005 → ACC-006 and back to ACC-003, hops 78,000, 76,500 and 74,000 plus the closing 71,500, 300,000 moved in total. Nothing else qualifies: the other loop in the data (Sarah Chen → Acme → Eli Tanaka → Sarah Chen) only moves a few hundred dollars per hop.

What's happening

  • The variable-length pattern *2..3 walks out from each account; the second MATCH requires a transfer from the last account back to the start (back.id = start.id), which closes the cycle.
  • ALL(n IN nodes(p) WHERE start.id <= n.id) keeps one copy of each ring instead of one per starting account.
  • ALL(r IN t WHERE r.amount > 50000) filters out rings of trivial amounts at traversal time.
  • reduce() totals the laundered amount inline, no second query.
  • In SQL this needs a 4-way self-join with equality on the start/end account plus AML thresholds — N^4 cost on the transaction table. On the graph it's local: only edges from start are visited.
  • Tight latency (<50ms on millions of edges) means rules can run on every transfer in real-time.

Try this next

MATCH (a:Account)-[t:TRANSFER]->(b:Account)
RETURN a.country AS from_country, b.country AS to_country,
       count(t) AS transfers, sum(t.amount) AS volume
ORDER BY volume DESC;
// Every account that sits on any loop of 2-4 hops, whatever the amounts.
MATCH p = (a:Account)-[:TRANSFER*1..3]->(last:Account)
MATCH (last)-[:TRANSFER]->(back:Account)
WHERE back.id = a.id
RETURN DISTINCT a.id, a.holder;

All eight accounts come back: money moves in loops in ordinary business too. The amount threshold in Step 2 is what separates a laundering ring from a normal payment loop.

MATCH (a:Account)-[t:TRANSFER]->(b:Account)
WHERE t.amount > 70000
RETURN a.holder AS sender, b.holder AS receiver, t.amount AS amount, t.date AS date
ORDER BY date;

Tags

graphcypherfraudsecurityintermediate