Skip to content

Fraud Detection with Uni (Rust)

Detecting money laundering rings (3-cycles) and shared device anomalies using Uni's native Rust API.

:dep uni-db = { path = "../../../crates/uni" }
:dep tokio = { version = "1", features = ["full"] }
:dep serde_json = "1"
use uni_db::{Uni, DataType, IndexType, ScalarType, VectorMetric, VectorAlgo, VectorIndexCfg};
use std::collections::HashMap;
use serde_json::json;

// Helper macro to run async code in evcxr
macro_rules! run {
    ($e:expr) => {
        tokio::runtime::Runtime::new().unwrap().block_on($e)
    };
}
let db_path = "./fraud_db";

// Clean up any existing database
if std::path::Path::new(db_path).exists() {
    std::fs::remove_dir_all(db_path).unwrap();
}

let db = run!(Uni::open(db_path).build()).unwrap();
println!("Opened database at {}", db_path);

1. Schema

run!(async {
    db.schema()
        .label("User")
            .property("name",  DataType::String)
            .property("email", DataType::String)
            .property_nullable("risk_score", DataType::Float32)
        .label("Device")
            .property("device_id", DataType::String)
        .edge_type("SENT_MONEY", &["User"], &["User"])
            .property("amount", DataType::Float64)
        .edge_type("USED_DEVICE", &["User"], &["Device"])
        .apply()
        .await
}).unwrap();

println!("Fraud detection schema created");

2. Ingestion

5 named users, 3 devices, a money ring, and suspicious cross-device links.

// 5 users: 3 in a ring, 2 high-risk fraudsters
let users = vec![
    HashMap::from([("name".to_string(), json!("Alice")),  ("email".to_string(), json!("alice@example.com")),  ("risk_score".to_string(), json!(0.10))]),
    HashMap::from([("name".to_string(), json!("Bob")),    ("email".to_string(), json!("bob@example.com")),    ("risk_score".to_string(), json!(0.15))]),
    HashMap::from([("name".to_string(), json!("Carlos")), ("email".to_string(), json!("carlos@example.com")), ("risk_score".to_string(), json!(0.20))]),
    HashMap::from([("name".to_string(), json!("Dana")),   ("email".to_string(), json!("dana@example.com")),   ("risk_score".to_string(), json!(0.92))]),
    HashMap::from([("name".to_string(), json!("Eve")),    ("email".to_string(), json!("eve@example.com")),    ("risk_score".to_string(), json!(0.88))]),
];

let user_vids = run!(db.bulk_insert_vertices("User", users)).unwrap();
let (alice, bob, carlos, dana, eve) =
    (user_vids[0], user_vids[1], user_vids[2], user_vids[3], user_vids[4]);

// 3 devices
let devices = vec![
    HashMap::from([("device_id".to_string(), json!("device_A"))]),
    HashMap::from([("device_id".to_string(), json!("device_B"))]),
    HashMap::from([("device_id".to_string(), json!("device_C"))]),
];
let device_vids = run!(db.bulk_insert_vertices("Device", devices)).unwrap();
let (device_a, device_b, device_c) = (device_vids[0], device_vids[1], device_vids[2]);

// Money ring: Alice -> Bob -> Carlos -> Alice
run!(db.bulk_insert_edges("SENT_MONEY", vec![
    (alice,  bob,    HashMap::from([("amount".to_string(), json!(9500.0))])),
    (bob,    carlos, HashMap::from([("amount".to_string(), json!(9000.0))])),
    (carlos, alice,  HashMap::from([("amount".to_string(), json!(8750.0))])),
    (dana,   eve,    HashMap::from([("amount".to_string(), json!(15000.0))])),  // Suspicious
])).unwrap();

// Device sharing: Alice+Dana on device_A, Bob+Eve on device_B, Carlos alone on device_C
run!(db.bulk_insert_edges("USED_DEVICE", vec![
    (alice,  device_a, HashMap::new()),
    (dana,   device_a, HashMap::new()),
    (bob,    device_b, HashMap::new()),
    (eve,    device_b, HashMap::new()),
    (carlos, device_c, HashMap::new()),
])).unwrap();

run!(db.flush()).unwrap();
println!("Fraud data ingested");

3. Ring Detection

Find 3-cycles in the money transfer graph. Deduplication: a._vid < b._vid AND a._vid < c._vid.

let query_ring = r#"
    MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
    WHERE a._vid < b._vid AND a._vid < c._vid
    RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
           COUNT(*) AS rings
"#;

let results = run!(db.query(query_ring)).unwrap();
println!("Money laundering rings detected:");
for row in &results.rows {
    println!("  {:?}", row);
}
assert!(results.rows.len() == 1, "Expected 1 ring, got {}", results.rows.len());

4. Ring with Transfer Amounts

Same pattern, but also retrieve edge properties to show total cycled money.

let query_amounts = r#"
    MATCH (a:User)-[r1:SENT_MONEY]->(b:User)-[r2:SENT_MONEY]->(c:User)-[r3:SENT_MONEY]->(a)
    WHERE a._vid < b._vid AND a._vid < c._vid
    RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
           r1.amount AS leg1, r2.amount AS leg2, r3.amount AS leg3,
           r1.amount + r2.amount + r3.amount AS total_cycled
"#;

let results = run!(db.query(query_amounts)).unwrap();
println!("Ring with transfer amounts:");
for row in &results.rows {
    println!("  {:?}", row);
}

5. Shared Device Risk

Find users who share a device with a high-risk user (risk > 0.8). Carlos should NOT appear.

let query_shared = r#"
    MATCH (u:User)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
    WHERE fraudster.risk_score > 0.8 AND u._vid <> fraudster._vid
    RETURN u.name AS user, d.device_id AS device, fraudster.name AS flagged_contact
    ORDER BY user
"#;

let results = run!(db.query(query_shared)).unwrap();
println!("Users sharing device with high-risk account:");
for row in &results.rows {
    println!("  {:?}", row);
}
// Carlos should not appear - he only uses device_C alone
println!("Note: Carlos uses device_C alone and should not appear above");

6. Combined Alert: Ring + Device Sharing

Users appearing in BOTH a money ring AND sharing a device with a fraudster.

let query_combined = r#"
    MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
    WHERE a._vid < b._vid AND a._vid < c._vid
    MATCH (a)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
    WHERE fraudster.risk_score > 0.8
    RETURN DISTINCT a.name AS high_priority_user
"#;

let results = run!(db.query(query_combined)).unwrap();
println!("HIGH PRIORITY targets (ring + device-sharing):");
for row in &results.rows {
    println!("  {:?}", row);
}
// Alice is in the ring AND shares device_A with Dana (high risk)
assert!(!results.rows.is_empty(), "Expected at least one combined alert");