Expose money muling networks through graph analysis, cycle detection, and intelligent pattern recognition.
Upload a CSV of transactions β Engine builds a directed graph β Runs 3 parallel detection algorithms β Filters false positives β Renders an interactive D3.js force-directed network visualization.
The Forensiq Engine ingests raw transaction data and automatically identifies three classes of financial fraud:
| Pattern | What It Catches | Real-World Example |
|---|---|---|
| π Circular Fund Routing | Money cycling back to its origin through 3β5 accounts | A β B β C β A (layering loop) |
| πΈοΈ Smurfing (Fan-in / Fan-out) | 10+ accounts funneling into one aggregator, or one account dispersing to 10+ receivers within 72h | Structuring deposits below $10K reporting thresholds |
| π Layered Shell Networks | Chains of 3+ hops through intermediate accounts with only 2β3 total transactions | O1 β SH1 β SH2 β SH3 β E1 (shell layering) |
graph TB
subgraph Frontend["βοΈ Frontend β React + Vite + D3.js"]
UI[App.jsx<br/>Main Orchestrator]
FU[FileUpload.jsx<br/>CSV Drag & Drop]
SG[StatsGrid.jsx<br/>Summary Cards]
GV[GraphVisualization.jsx<br/>D3 Force Graph]
FRT[FraudRingTable.jsx<br/>Ring Details]
SAT[SuspiciousAccountsTable.jsx<br/>Account Details]
UI --> FU
UI --> SG
UI --> GV
UI --> FRT
UI --> SAT
end
subgraph Backend["π₯οΈ Backend β Express.js REST API"]
SRV[server.js<br/>CSV Parse + API Routes]
FE[ForensicsEngine<br/>Main Orchestrator]
subgraph Detection["π Detection Pipeline"]
GB["GraphBuilder<br/>Adjacency List + Metadata"]
CD["CycleDetector<br/>DFS Cycle Finding"]
SD["SmurfingDetector<br/>Fan-in / Fan-out"]
SND["ShellNetworkDetector<br/>Layered Chain Tracing"]
FPF["FalsePositiveFilter<br/>Merchant / Payroll / Exchange"]
end
SRV --> FE
FE --> GB
FE --> CD
FE --> SD
FE --> SND
FE --> FPF
end
UI -- "POST /api/analyze<br/>(multipart CSV)" --> SRV
SRV -- "JSON Results" --> UI
style Frontend fill:#1a1a2e,stroke:#6c63ff,color:#fff
style Backend fill:#16213e,stroke:#0f3460,color:#fff
style Detection fill:#0f3460,stroke:#e94560,color:#fff
Finds circular fund routing β money that loops back to its origin through 3β5 intermediate accounts.
A ββ$10KβββΈ B ββ$9.8KβββΈ C ββ$9.5KβββΈ A
β² β
βββββββββββββ cycle ββββββββββββββββ
Algorithm: Johnson's algorithm variant using bounded DFS with backtracking.
| Parameter | Value | Why |
|---|---|---|
MIN_CYCLE |
3 | Minimum meaningful cycle |
MAX_CYCLE |
5 | Longer cycles are too common in normal commerce |
MAX_OUT_DEGREE |
30 | Skip high-degree hub nodes (exchanges, merchants) |
MAX_RESULTS |
500 | Performance cap |
Scoring (0β100):
- Base: 50 (being a cycle is inherently suspicious)
- Cycle length: +15 (length 3) β +5 (length 5)
- Amount similarity: +15 (CV < 0.1) β low variance = structuring
- Temporal proximity: +15 (< 24h) β rapid cycling
- Low-activity nodes: +10 if >50% of cycle nodes have β€5 total txns
Detects structuring patterns where money is split or aggregated to avoid reporting thresholds.
Fan-In: Fan-Out:
S1 ββ$9KβββΈ βββ$9KβββΈ R1
S2 ββ$9KβββΈ AGGREGATOR βββ$9KβββΈ R2
S3 ββ$9KβββΈ DISPERSERββ$9KβββΈ R3
... (10+ senders) β (10+ receivers)
S10ββ$9KβββΈ βββ$9KβββΈ R10
Key rules:
- Fan-in: β₯10 unique senders β 1 receiver
- Fan-out: 1 sender β β₯10 unique receivers
- Combined: Same node receives from β₯10 AND sends to β₯10
- 72-hour temporal window: Sliding window clusters transactions; only counterparties within the same 72h window count toward the threshold
Scoring (0β100):
- Temporal cluster base: 55
- Counterparty count: +15 (β₯20) / +10 (β₯15) / +5 (β₯10)
- Structuring signal: +15 if >30% of amounts fall in $8Kβ$10K range
- Amount uniformity: +10 if coefficient of variation < 0.2
- Throughput ratio: +10 if node passes through ~100% of received funds
Traces layered shell chains β money hopping through 3+ intermediate accounts that exist solely as pass-throughs.
Exact Pass-Through:
O1 ββ$200KβββΈ SH1 ββ$200KβββΈ SH2 ββ$200KβββΈ SH3 ββ$200KβββΈ E1
Gradual Decay (fee-skimming):
O1 ββ$200KβββΈ SH1 ββ$198KβββΈ SH2 ββ$195KβββΈ SH3 ββ$190KβββΈ E1
Shell account criteria: β€3 total transactions, inDegree β₯1, outDegree β₯1
Key rules:
- Minimum 3 hops (4+ nodes in the chain)
- Maximum 7 nodes per chain
- Amount coherence: Drop between consecutive hops must be β€ $10,000
- Amount cannot increase: Next hop β€ previous hop (money doesn't appear from nothing)
Amount Pattern Classification:
| Pattern | Condition | Score Bonus |
|---|---|---|
exact_passthrough |
All hop amounts within 1% | +15 |
gradual_decay |
β₯50% of hops show 1β20% decrease | +20 |
mixed |
Coherent but no clear pattern | +10 |
Scoring (0β100):
- Base: 45
- Chain length: +20 (β₯6 nodes) β +5 (4 nodes)
- Amount pattern: +15 to +20 (see table above)
- Temporal sequence: +15 (all hops within 24h)
- Very low activity shells: +10 if >50% of intermediates have exactly 2 txns
Removes legitimate high-volume accounts that would otherwise trigger detection:
| Legitimate Type | Detection Criteria |
|---|---|
| πͺ Merchants | β₯15 unique senders, β€5 unique receivers, <20% sender-receiver overlap, high amount variance |
| πΌ Payroll | β₯10 unique receivers, β€5 unique senders, regular amounts, temporal regularity or repeat payments |
| π¦ Exchanges | β₯20 unique senders AND β₯20 unique receivers, <15% sender-receiver overlap |
| π₯ Counterparties | Low-activity accounts (β€5 txns) that mainly interact with legitimate hubs |
The filter also drops entire fraud rings if they're organized around a legitimate hub.
sequenceDiagram
participant U as π€ User
participant F as βοΈ Frontend
participant S as π₯οΈ Server
participant E as π¬ Engine
U->>F: Upload CSV
F->>S: POST /api/analyze (multipart)
S->>S: Parse CSV β Validate rows
S->>E: new ForensicsEngine(transactions)
rect rgb(15, 52, 96)
Note over E: Detection Pipeline
E->>E: 1. GraphBuilder β adjacency lists + metadata
E->>E: 2. CycleDetector β circular routes
E->>E: 3. SmurfingDetector β fan-in/fan-out
E->>E: 4. ShellNetworkDetector β layered chains
E->>E: 5. FalsePositiveFilter β remove legit accounts
E->>E: 6. Merge overlapping rings
end
E-->>S: Results JSON
S-->>F: { suspicious_accounts, fraud_rings, graph_data, summary }
F->>F: Render D3 force graph + tables
F-->>U: Interactive visualization
- Node.js β₯ 18
- npm β₯ 9
# Clone the repository
git clone <repo-url>
cd PW-hack
# Install backend dependencies
cd backend
npm install
# Install frontend dependencies
cd ../frontend
npm install# Terminal 1 β Start the backend (port 3001)
cd backend
npm start
# Terminal 2 β Start the frontend (port 5173)
cd frontend
npm run devOpen http://localhost:5173 in your browser.
The engine accepts CSV files with the following columns:
| Column | Required | Aliases Supported |
|---|---|---|
transaction_id |
β | transactionid, txn_id, id |
sender_id |
β | senderid, sender, from_id |
receiver_id |
β | receiverid, receiver, to_id |
amount |
β | β |
timestamp |
β | datetime, date, time |
Example:
transaction_id,sender_id,receiver_id,amount,timestamp
T001,ACC_A,ACC_B,50000,2026-01-15 08:30:00
T002,ACC_B,ACC_C,49500,2026-01-15 09:15:00
T003,ACC_C,ACC_A,49000,2026-01-15 10:00:00Validation rules:
- Rows with missing fields are skipped (with warnings)
- Self-transfers (
sender = receiver) are rejected - Amounts must be positive numbers
- Timestamps must be parseable by
Date.parse() - Max file size: 50 MB
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/analyze |
Upload CSV and run full analysis |
GET |
/api/results/:sessionId |
Retrieve stored results |
GET |
/api/download/:sessionId |
Download JSON report |
GET |
/api/health |
Health check |
Request: multipart/form-data with field file (CSV)
Response:
{
"success": true,
"sessionId": "m1abc123def",
"results": {
"suspicious_accounts": [
{
"account_id": "SH1",
"suspicion_score": 85.0,
"detected_patterns": ["shell_intermediary"],
"ring_id": "RING_001"
}
],
"fraud_rings": [
{
"ring_id": "RING_001",
"member_accounts": ["O1", "SH1", "SH2", "SH3", "E1"],
"pattern_type": "shell_network",
"risk_score": 85.0,
"chain_length": 5,
"amount_pattern": "exact_passthrough"
}
],
"summary": {
"total_accounts_analyzed": 150,
"suspicious_accounts_flagged": 23,
"fraud_rings_detected": 5,
"processing_time_seconds": 0.3
},
"graph_data": {
"nodes": [],
"edges": []
}
}
}PW-hack/
βββ backend/
β βββ server.js # Express server + CSV parsing + API routes
β βββ detection/
β β βββ forensicsEngine.js # Main orchestrator β runs all detectors
β β βββ graphBuilder.js # Builds adjacency lists + node metadata
β β βββ cycleDetector.js # DFS-based cycle finding (3β5 nodes)
β β βββ smurfingDetector.js # Fan-in / fan-out with 72h temporal windows
β β βββ shellDetector.js # Layered shell chain tracing (3+ hops)
β β βββ falsePositiveFilter.js # Merchant / payroll / exchange filtering
β βββ test_engine.js # Integration test with full CSV
β βββ test_shell_detection.js # Shell detection unit test
β βββ package.json
β
βββ frontend/
β βββ src/
β β βββ App.jsx # Main app β state management + layout
β β βββ main.jsx # React entry point
β β βββ index.css # Full design system
β β βββ components/
β β βββ FileUpload.jsx # Drag & drop CSV upload with progress
β β βββ StatsGrid.jsx # Summary statistics cards
β β βββ GraphVisualization.jsx # D3.js force-directed network graph
β β βββ FraudRingTable.jsx # Fraud ring details table
β β βββ SuspiciousAccountsTable.jsx # Suspicious accounts table
β βββ index.html
β βββ vite.config.js
β βββ package.json
β
βββ generate_test_data.py # Python script to generate test CSVs
βββ test_transactions.csv # Sample dataset (700+ transactions)
βββ test_transactions_10.csv # Minimal test dataset
All detection thresholds are configurable at the top of each detector class:
| Detector | Parameter | Default | Description |
|---|---|---|---|
| Cycle | MIN_CYCLE |
3 | Minimum cycle length |
| Cycle | MAX_CYCLE |
5 | Maximum cycle length |
| Smurfing | FAN_THRESHOLD |
10 | Minimum unique counterparties |
| Smurfing | Temporal window | 72h | Sliding window for clustering |
| Shell | MIN_CHAIN_LENGTH |
4 | Minimum nodes (= 3+ hops) |
| Shell | MAX_CHAIN_LENGTH |
7 | Maximum nodes in a chain |
| Shell | SHELL_TX_THRESHOLD |
3 | Max txns for shell classification |
| Shell | AMOUNT_COHERENCE_MAX_DROP |
$10,000 | Max $ drop between hops |
cd backend
# Run full integration test
node test_engine.js
# Run shell detection scenarios
node test_shell_detection.jspython generate_test_data.pypattern_type |
Description | Key Fields |
|---|---|---|
cycle |
Circular fund routing | cycle_length |
fan_in |
Multiple senders β one aggregator | aggregatorNode, temporalWindowHours |
fan_out |
One disperser β multiple receivers | disperserNode, temporalWindowHours |
fan_in_fan_out |
Same node aggregates AND disperses | aggregatorNode, disperserNode |
shell_network |
Layered chain through shell accounts | chain_length, amount_pattern |
This project was built for the PW Hackathon.
Built with π¬ by the RIFT team