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.
- Java
- Python
- Rust
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
}
with client.subscriptions.listen(["flagged_payments"]) as listener:
for msg in listener:
investigate(account_from(msg.payload)) # expand its network (step 2)
listener.ack([msg.message_id])
let mut listener = api.subscriptions.listen(&["flagged_payments"]).await?;
while let Some(Ok(msg)) = listener.next().await {
investigate(account_from(&msg.payload)).await?; // expand its network (step 2)
listener.ack(&[msg.message_id.as_str()]).await?;
}
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.
- Java
- Python
- Rust
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));
ring = client.resources.fetch_related(external_id="account_77310", depth=5)
linked = [n.external_id for n in ring.nodes if n.external_id.startswith("account_")]
print("ring accounts:", linked)
use dataplatform_rust_sdk::resources::RelatedResourcesForm;
let ring = api.resources.fetch_related(
&RelatedResourcesForm::from_external_id("account_77310").with_depth(5)).await?;
for node in ring.nodes() {
if node.external_id.starts_with("account_") {
println!("linked account: {}", node.external_id);
}
}
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
- Consume live data — reacting to flagged payments.
- Correlate alarms with the graph — expanding the network.
- Turn readings into events — escalating a cluster.
- Fraud classification (advanced) — score each cluster with graph features + a model so investigators work the riskiest first.