Skip to main content

Banking — payments & anti-money-laundering

The problem. Money laundering hides in the shape of a transaction network, not in any single payment. Funds fan out through mule accounts, loop through shell counterparties, and reconverge — a pattern invisible to a per-transaction rule but obvious on a graph. A bank's financial-crime team needs to catch a suspicious payment as it happens and immediately see the web of accounts around it, in time to hold the funds.

The problem here is finding hidden structure fast enough to act — react to a live payment, then expand its network.

Set up demo data

New workspace? Run this once (Python) to create the flagged-payments stream (subscription created before we listen) and the account network the flagged account sits in — a loop of transfers, i.e. a laundering ring. Safe to re-run.

import datahub_sdk, pandas as pd

client = datahub_sdk.DataHubClient.from_env()

client.timeseries.create([datahub_sdk.TimeSeries(external_id="flagged_payments_feed", name="Flagged payments", unit="count", value_type="float")])
client.subscriptions.create([datahub_sdk.Subscription(
external_id="flagged_payments", name="Flagged payments", timeseries=["flagged_payments_feed"])])
client.timeseries.insert_from_lists(timestamps=[pd.Timestamp.now(tz="UTC")], values=[1.0], ts="flagged_payments_feed")

client.resources.create(
[datahub_sdk.Resource(external_id=f"account_{i}", name=f"Account {i}", labels=["Account"]) for i in [77310, 8841, 8842, 8843]],
[datahub_sdk.RelForm.by_external_ids("account_77310", "account_8841", "sent_to"),
datahub_sdk.RelForm.by_external_ids("account_8841", "account_8842", "sent_to"),
datahub_sdk.RelForm.by_external_ids("account_8842", "account_8843", "sent_to"),
datahub_sdk.RelForm.by_external_ids("account_8843", "account_77310", "sent_to")])

1. React to a flagged payment live

A rules engine publishes flagged payments; subscribe and pick them up the moment they land. See Consume live data.

import ai.intellistream.datahub.sdk.subscriptions.SubscriptionMessage;

try (var stream = client.subscriptions().listen(List.of("flagged_payments"))
.stream((SubscriptionMessage msg) -> { // auto-acks after each message
investigate(accountFrom(msg.payload())); // expand its network (step 2)
})) {
awaitShutdown(); // your app lifecycle; closing the stream ends delivery
}

2. Expand the account into its network

Accounts, transactions and counterparties are a graph. Walk out from the flagged account and the returned sub-graph is the cluster — the mules and shells it moves money through. Other flagged accounts in the same cluster are the ring.

ResourceNetwork ring = client.resources().fetchRelated("account_77310", 5);

ring.nodes().stream()
.map(Resource::getExternalId)
.filter(id -> id.startsWith("account_"))
.forEach(a -> System.out.println("linked account: " + a));

Same shared-node reasoning as alarm correlation, here turning one flagged payment into the whole laundering ring.

3. Escalate and watch the trend

Raise a sar_candidate event on the cluster for the investigations queue, and track account_77310_outflow as a series — a sudden outflow spike is structuring in progress.

See the result

Expanding the flagged account reveals the ring it moves money through:

ring accounts: ['account_8841', 'account_8842', 'account_8843']

See also