---
title: "Follow the money: a connection graph for fraud and AML investigators"
slug: build-fraud-investigation-graph
date: 2026-10-12
author: Semurg
section: how-to
tags: [build-on-semurg, fraud, aml, graph, blueprint]
summary: A blueprint for an investigations team: accounts, owners, companies and transfers as one graph, so a flagged account leads straight to where the money went, who controls each hop, and the loop that brings it back.
status: published
covers: [route:POST /v1/memory/remember, route:POST /v1/memory/recall, route:POST /v1/memory/context, route:POST /v1/memory/relate]
---
Money laundering hides in the connections. An account on its own looks ordinary; the same account, two transfers away from a company
whose director also controls the account the money came from, does not. Investigators reconstruct those connections by hand from
exports, one join at a time. This blueprint keeps them as a graph from the moment the data arrives, so a flagged account leads
straight to where the money went, who sits behind each hop, and whether it comes back.

> Diagram case-graph: Money moves from Account Alpha to Account Bravo to Account Charlie and back to Alpha; Dana Kovac holds Alpha and is a director of Harbour Holdings, which holds Bravo, so one person sits on both sides of the loop.

It is a tool for investigators, not a screening or reporting system: it holds what your monitoring and case work put into it, and it
shows the connections. Deciding what is suspicious stays with the investigator.

Everything below uses only what the published release does.

## The shape of the data

- **One scope per investigation**, so a case's working graph stays apart from every other case, and one shared scope for what the whole team has established.
- **Entities**: accounts, the people who hold or control them, companies, addresses.
- **Relationships**: `TRANSFERRED_TO` between accounts, `HOLDS` from a person or company to an account, `DIRECTOR_OF` from a person to a company, `REGISTERED_AT` to an address.
- **Detail as passages**: the amounts, dates and references of each transfer go into a passage that names the accounts involved, so the detail comes back with the connection.

A person who controls ten accounts is one entity with ten edges. An address shared by four companies is one entity, and the moment
the fourth company is written, the walk from any of them reaches the other three.

## Step one: write what monitoring found

The transfers your monitoring flagged, with their owners. The call lists both ends of each relationship, so it stands on its own:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"case-harbour","date":"2026-09-30","source":"transaction monitoring alert batch",
       "facts":{"entities":[{"name":"Account Alpha","type":"Account"},{"name":"Account Bravo","type":"Account"},
                            {"name":"Account Charlie","type":"Account"},{"name":"Harbour Holdings","type":"Company"},
                            {"name":"Dana Kovac","type":"Person"}],
                "relations":[{"source":"Account Alpha","target":"Account Bravo","type":"TRANSFERRED_TO"},
                             {"source":"Account Bravo","target":"Account Charlie","type":"TRANSFERRED_TO"},
                             {"source":"Account Charlie","target":"Account Alpha","type":"TRANSFERRED_TO"},
                             {"source":"Harbour Holdings","target":"Account Bravo","type":"HOLDS"},
                             {"source":"Dana Kovac","target":"Account Alpha","type":"HOLDS"}]},
       "passages":[{"text":"Account Alpha sent 48,500 to Account Bravo on 2026-09-22; Account Bravo sent 47,900 to Account Charlie on 2026-09-23; Account Charlie returned 47,100 to Account Alpha on 2026-09-25.",
                    "mentions":["Account Alpha","Account Bravo","Account Charlie"],"key":"alert-ring-1"}]}'
```

```expect
expect: a JSON answer with "ok": true
assert: json:.ok==true
```

## Step two: follow the money

Start from the flagged account. Context finds every entity the question names, walks out from them up to three steps, and returns the
connections with the passages that describe them:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/context \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"case-harbour","query":"Where did money from Account Alpha go?","hops":3}'
```

```expect
expect: "state": "answered": the transfers from Account Alpha to Account Bravo, on to Account Charlie and back to Account Alpha, with the passage holding the amounts and dates
assert: json:.state=="answered" and ([..|strings|ascii_downcase|select(contains("returned 47,100"))]|length>0)
```

The loop is visible in what comes back: Alpha to Bravo, Bravo to Charlie, Charlie back to Alpha. Who sits behind each hop is one
recall away. Recall returns every connection of one entity, in both directions:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"case-harbour","subject":"Account Bravo"}'
```

```expect
expect: "state": "answered": money in from Account Alpha, out to Account Charlie, and Harbour Holdings as the holder
assert: json:.ok==true and .state=="answered" and ([..|strings|ascii_downcase|select(contains("harbour holdings"))]|length>0)
```

## Step three: add what the investigator finds

A company registry search shows that Dana Kovac, who holds Account Alpha, is also a director of Harbour Holdings, which holds Account
Bravo. One relationship, added on its own:

> Diagram shared-address: Four companies registered at one address point at a single address entity, so the walk from any one of them reaches the other three.

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/relate \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"case-harbour","from":"Dana Kovac","to":"Harbour Holdings","type":"DIRECTOR_OF"}'
```

```expect
expect: a JSON answer with "ok": true
assert: json:.ok==true
```

From now on, a walk from either account reaches the same person on both sides of the transfer, and so does every investigator who
opens the case.

## An agent on the case

An investigation agent can call the same memory over MCP, with the case's scope in the `X-Semurg-Scope` header: it reads what the team
has established before it proposes the next step, and it writes what it confirms. The [agents page](/agents) has the configuration
for each agent and framework we test.

## Scale and cost

A single node is free, with no licence key, and it is the full engine. A cluster is licensed per node; talk to us about the one you
need. Our graph results are being re-measured on new hardware and land on the [benchmark page](/benchmark) with the release they ran on. We have not measured
this blueprint's own workload.

## What it does not do today

- Walks on this surface go at most three steps from the names in the question. A chain longer than that is found by starting a second walk from where the first one ended. The engine's deep traversal, which goes much further, answers only with counts over the public interface today, not with the paths.
- A relationship carries a type, not an amount or a date. The amounts and dates live in the passage that names the accounts, and the date of the write comes back with every fact it sourced.
- A name is matched exactly, by letter case or by a close spelling, so two very different spellings of one person stay two entities until an investigator relates them.
- It does not score risk, screen against sanctions lists or file reports. It holds the graph your monitoring and your investigators build, and it shows the connections.

## When something is refused

- `401`: the token is missing or wrong; `422` `missing 'scope'` on a call without a scope.
- A passage whose mention does not appear in its text is refused, naming the mention.
- A relationship whose far end is not listed in the same call is not written on today's release, although the call answers ok: list both accounts every time.
- `"state": "nothing_stored"` means nothing in the case matched the names in the question; it is an answer, not an error.

## Next

- [Give your agent persistent memory with MCP in five minutes](/blog/agent-memory-mcp-five-minutes): the node and the seven memory tools.
- [A research graph of claims and papers](/blog/build-research-assistant-claims-graph): another graph that follows the connections.
