Third-party payment to high-risk jurisdiction
1. Introduction
Transaction monitoring is a fundamental pillar in retail banking, ensuring the integrity and safety of financial transactions. It plays a pivotal role in detecting and preventing financial fraud, money laundering, and other illicit activities, safeguarding the bank and its customers from potential threats and losses.
The "Third-party payment to high-risk jurisdiction" rule monitors transactions directed towards regions or countries categorised as high-risk for financial misconduct. By identifying such transactions, banks can scrutinise them more closely, ensuring they comply with regulatory compliances and aren’t a conduit for nefarious activities.
2. Rule Breakdown
-
Time Range:
-
Evaluate all data over a rolling 30-days (this can be any time period)
-
-
Catches:
-
Money Mules
-
-
Logic:
-
Aggregate total value of inflow payments by unique source accounts
-
Match transactions to high-risk jurisdiction
-
Where the value of an individual transaction is between 90% - 110% of the original inflow amount.
-
-
3. Modelling
This section will show examples of cypher queries on an example graph. The intention is to illustrate what the queries look like and provide a guide on how to structure your data in a real setting. We will do this on a small graph of several nodes. The example graph will be based on the data model below:
3.1. Data Model
3.1.1 Required Fields
Below are the fields required to get started:
Account
Node:
-
accountNumber
: Contains the account identifier. This could be changed for any other identifier you use for anAccount
.
Transaction
Node:
-
transactionId
: Unique system identifier for the transaction. -
amount
: Contains the amount of money transferred between accounts. -
date
: Contains the date the transaction occurred.
PERFORMS
Relationships:
-
No properties required
BENEFITS_TO
Relationships:
-
No properties required
3.2. Demo Data
The following Cypher statement will create the example graph in the Neo4j database:
// Create all accounts
CREATE (a1:Account:Internal {accountNumber: "ACC001"})
CREATE (a2:Account:Internal {accountNumber: "ACC002"})
CREATE (a3:Account:Internal {accountNumber: "ACC003"})
CREATE (a4:Account:Internal {accountNumber: "ACC004"})
CREATE (a5:Account:Internal {accountNumber: "ACC005"})
CREATE (a6:Account:Internal {accountNumber: "ACC006"})
CREATE (a7:Account:External:HighRiskJurisdiction {accountNumber: "ACC007"})
// Create valid transactions
CREATE (a2)-[:PERFORMS]->(:Transaction {transactionId: "TXN001", amount: 1100, date: datetime()-duration({days: 29})})-[:BENEFITS_TO]->(a4)
CREATE (a4)-[:PERFORMS]->(:Transaction {transactionId: "TXN002", amount: 100, date: datetime()-duration({days: 27})})-[:BENEFITS_TO]->(a6)
CREATE (a4)-[:PERFORMS]->(:Transaction {transactionId: "TXN003", amount: 200, date: datetime()-duration({days: 26})})-[:BENEFITS_TO]->(a6)
CREATE (a4)-[:PERFORMS]->(:Transaction {transactionId: "TXN004", amount: 600, date: datetime()-duration({days: 25})})-[:BENEFITS_TO]->(a6)
CREATE (a6)-[:PERFORMS]->(:Transaction {transactionId: "TXN005", amount: 500, date: datetime()-duration({days: 3})})-[:BENEFITS_TO]->(a7)
CREATE (a6)-[:PERFORMS]->(:Transaction {transactionId: "TXN006", amount: 500, date: datetime()-duration({days: 2})})-[:BENEFITS_TO]->(a7)
// Create invalid transactions (outside time window or not matching criteria)
CREATE (a1)-[:PERFORMS]->(:Transaction {transactionId: "TXN007", amount: 500, date: datetime()-duration({days: 60})})-[:BENEFITS_TO]->(a2)
CREATE (a1)-[:PERFORMS]->(:Transaction {transactionId: "TXN008", amount: 500, date: datetime()-duration({days: 60})})-[:BENEFITS_TO]->(a2)
CREATE (a3)-[:PERFORMS]->(:Transaction {transactionId: "TXN009", amount: 750, date: datetime()-duration({days: 28})})-[:BENEFITS_TO]->(a4)
CREATE (a5)-[:PERFORMS]->(:Transaction {transactionId: "TXN010", amount: 100, date: datetime()-duration({days: 24})})-[:BENEFITS_TO]->(a6)
CREATE (a5)-[:PERFORMS]->(:Transaction {transactionId: "TXN011", amount: 50, date: datetime()-duration({days: 24})})-[:BENEFITS_TO]->(a6)
4. Cypher Queries
4.1. Enhanced Graph Version
This is an enhanced version of the standard transaction monitoring rule, which is not achievable at scale and simplicity with the current system. Why?
-
The recursive traversed back through the relationships indefinitely can not be implemented in any of the current systems
-
The incredible performance by Neo4j was achieved by the fact we evaluated the following conditions at traversal time:
-
The value of the aggregated transactions is outside the bounds of 90% - 110% of the original transaction amount.
-
Where the dates of the transactions are outside the specified period. In this case, 30 days.
-
MATCH (l:Account)-[:PERFORMS]->(last_t:Transaction)-[:BENEFITS_TO]->(hrj:HighRiskJurisdiction)
WHERE last_t.date >= datetime()-duration({days: 30})
WITH l, hrj, SUM(last_t.amount) AS total_hrj_transactions
MATCH path=(first)((a1)-[:PERFORMS]->(t:Transaction)-[:BENEFITS_TO]->(a2)
WHERE COLLECT {
WITH a1, a2
MATCH (a1)-[:PERFORMS]->(some_t:Transaction)-[:BENEFITS_TO]->(a2)
WHERE some_t.date >= datetime()-duration({days: 30})
WITH SUM(some_t.amount) AS s
RETURN 0.9 * total_hrj_transactions <= s <= 1.1 * total_hrj_transactions
} = [TRUE]
)*(l)-[:PERFORMS]->(tx:Transaction)-[:BENEFITS_TO]->(hrj)
WHERE NOT EXISTS {
WITH first
MATCH (before)-[:PERFORMS]->(tx:Transaction)-[:BENEFITS_TO]->(first)
WHERE tx.date >= datetime()-duration({days: 30})
WITH SUM(tx.amount) AS sx, before
WHERE 0.9 * total_hrj_transactions <= sx <= 1.1 * total_hrj_transactions
RETURN before
} AND
tx.date >= datetime()-duration({days: 30})
RETURN path