Live demo · Fraud-ring search

One shared phone number.
A whole ring behind it.

Fraud rings reuse phones, inboxes and addresses, but rarely write them the same way twice. Chalk normalizes an applicant's PII with native expressions, then a Python resolver searches around it in a graph with openCypher, for rings you have already labelled and for dense clusters of high-risk accounts you haven't.

Eight applications, one book of 29,967

Pick an applicant. Chalk normalizes their phone, email and address, searches two hops around them in the graph, and decides live. Leave out an attribute to see how much it carries. Normalization alone collapses 28,986 raw phone strings into 25,745 numbers and 26,659 raw addresses into 20,235: the links exact matching misses.

Applications · Brightwater Lending
29,967 synthetic people, 3 labelled rings, one ring nobody has labelled, and plenty of families and apartment buildings that share contact details for ordinary reasons.
APP-101 · No matches
Wilf Thistledown
A first-time applicant whose phone, email and address appear nowhere in the book.
approve
risk 0.00 · recorded · 6201 ms
UseRaw, as submittedNormalized by Chalk
(989) 555-0100
+19895550100
Digits only, country code 1, E.164
wilf.thistledown.new@example.net
wilf.thistledown.new@example.net
Lowercase, drop +tag, fold mbx.test into mailbox.test, drop dots there
4410 Lanternfield Court, New Larchmere, ZZ 00074
4410 lanternfield ct new larchmere zz 00074
Lowercase, fold punctuation, USPS abbreviations, units as apt
building: 4410 lanternfield ct new larchmere zz 00074
0
Links, normalized
0
Links, raw strings
0
People within 2 hops
0
High-risk among them
applicantno shared values in the book
PhoneEmailAddressBuildingTie between risky people Labelled ring High riskDashed: found only after normalization
Labelled rings in reach
None within two hops.
Score
Known ring0.00
No labelled ring within two hops.
Unlabelled cluster0.00
0 high-risk people one hop away and 0 two hops away (weighted 0.0); 0% of them are tied to each other.
Risk 0.00: review at 0.35, decline at 0.7. Graph walk 2385 ms · query cmv8kpd3m008l0hp3ygwvuudz.

Brightwater Lending, its customers and applicants are fictional: phones are in the reserved 555-01xx range, emails use example and .test domains, and the streets are invented. The population is stored as Vortex, SpiralDB's open columnar format, in a Chalk Volume, and loaded from there into a real Neo4j database running as a Chalk scaling group; it is not read through SpiralDB itself. Every decision above is a real Chalk online query against a branch that holds these feature definitions, and each applicant shows a recorded run of the same query instantly while the live one runs.

How it works

Normalize first, so the graph links people who really share a value. Then search around the applicant, and let the shape of what you find decide.

Normalize the PII

Native Chalk expressions turn every phone into E.164, fold email aliases, plus-tags and ignored dots, and standardize addresses USPS-style. The same expressions normalize the whole book before it is loaded.

Search around in the graph

A Python resolver sends openCypher to a graph of people and their normalized phones, emails, addresses and buildings: who shares a value with the applicant, and who shares one with them.

Score rings, labelled or not

A labelled ring close by is decisive. Without a label, a dense knot of high-risk accounts with chargebacks, tied to each other, is the signal. Families and busy buildings are not.

Decide with evidence

Approve, review or decline comes back with the subgraph that justifies it: which values matched, through whom, and which ring or cluster they lead to.

Getting started

Bring your own book

Clone the example, deploy the features to a branch, normalize and load the population, and screen an applicant. The book here is synthetic; point the build at your customer table and the normalization, the search-around and the scoring do not change.

The graph walk is ordinary Python with an HTTP client, so the same pattern works with any graph database that speaks openCypher.

chalk-ai/fraud-ring-search
git clone https://github.com/chalk-ai/fraud-ring-search
cd fraud-ring-search

# Features, normalization and scoring on a branch.
chalk apply --branch fraud-rings

# Normalize the population with the same Chalk expressions, store it as Vortex in a Chalk Volume.
./build_population.py normalize --branch fraud-rings
./build_population.py upload

# Neo4j loads the Vortex tables and serves the PII graph read-only, inside your cluster.
./deploy_graph.py
chalk secret set FRAUD_GRAPH_URL=http://<graph service>:7474

chalk query --branch fraud-rings --in applicant.id=APP-106 \
  --out applicant.decision --out applicant.risk_score

What Chalk handles

Normalization, the graph query and the decision in one feature graph, live per application.

One normalization, everywhere

The applicant and the population go through the same feature expressions, so a match never depends on which side wrote the phone number with dashes.

Graph inside your cluster

The graph database runs as a Chalk scaling group, loaded from Vortex files in a Chalk Volume, read-only and reachable only from inside the environment.

openCypher from Python

Resolvers are Python functions, so a graph query is just another feature source, next to your SQL tables and your models.

Weights you can read

A shared phone outweighs a shared building. The weights and thresholds sit in the feature definitions, versioned with the code.

Compose with more signal

Add device fingerprints, IP history or bureau data as more features, and the same query returns them next to the graph context.

Branches for new rules

Try new weights or a second hop through buildings on a branch, replay yesterday's applications, and compare decisions before shipping.

Find the ring before it finds you

Identity graphs, device signals and bureau data: talk to an engineer about turning them into features your onboarding decisions can act on.