solution · fraud detection

Detect the ring
while it's still forming.

A fraud ring is a graph shape on a clock. OriginChain runs connected components, bidirectional all-paths and triangle enumeration against the same store that /watch streams — the detector sees every write the instant it commits.

GET /watch?prefix=transfers/ data: {"op":"put","from":"acct_51","to":"acct_19"} component #1 · clear just landed acct_19 acct_51 c_91 shared card connected_components → ring flagged · 5 accts
the problem it kills

Fraudsters exploit the gap between your systems.

Split the stack into Kafka + Neo4j + Postgres and your graph view runs a sync lag behind reality. A ring spinning up inside that window is invisible to the detector that has the right algorithm — the data it needs exists, but not where the algorithm runs.

On one substrate, reactive /watch and graph queries run against the same store. Zero replication lag between them, because there is nothing to replicate. The detector and the data are the same surface.

split stack
Postgres Kafka Neo4j sync detector

graph view: a sync lag behind reality

OriginChain
one substrate · writes + stream + graph

graph view: the write that just committed

the detection loop

Write → stream → graph. One store, no hand-offs.

The loop that usually spans three systems and an ops team is two HTTP requests here: a standing /watch subscription and a graph query your handler fires when an event warrants a look.

01
A transfer lands

The row and its relation edges — both directions — commit in one atomic write. There is no CDC pipeline copying it anywhere, because there is nowhere else it needs to go.

row + edges, one commit
02
/watch fires

Every write puts an event on the stream by construction. Subscribe to the transfers/ prefix and the filtering happens server-side — your detector sees transfers, nothing else.

SSE · server-side filtered
03
The graph answers on current state

Components, triangles and bidirectional all-paths run against the same store the write just hit. There is no index that hasn't caught up and no replica that's behind.

query the write you just saw
subscribe · every transfer
curl -N \
  -H 'Authorization: Bearer ...' \
  'https://<tenant>.db.originchain.ai/v1/tenants/:t/watch?prefix=transfers/'

data: {"op":"put","schema":"transfers","row":{"from":"acct_51",…}}
data: {"op":"put","schema":"transfers","row":{"from":"acct_19",…}}
react · triangle query
POST /v1/tenants/:t/graph/triangles
{
  "schema":     "transfers",
  "min_amount": 5000,
  "since":      "PT2H"
}

runs against the store the event just came from
the shapes fraud takes

Four fraud shapes, four graph primitives.

Each one runs against the direction-tagged relation keys your rows already have — no export, no second engine — and every traversal endpoint accepts ?explain=true for per-hop cost, so a slow investigation query tells you which hop exploded. The full catalogue is on the graph page.

rings Connected components
Union-Findcomponent per account

Accounts sharing a device, an address, a card collapse into one component. The ring surfaces as a component id — no path query to write, no pattern to guess in advance.

money trails All simple paths · bidirectional
depth capmax_paths5–20× vs forward

Every node-disjoint route between two flagged accounts, bounded by depth and max_paths. The bidirectional variant walks forward and reverse edges in the same pass and meets in the middle — same answer, 5–20× faster on real graphs.

collusion Triangle enumeration
3-cliqueforward-prefix scan

Three accounts moving money in a cycle is the smallest laundering loop. Triangle enumeration finds every 3-clique — the mule triangles that component analysis alone won't isolate.

influence PageRank
power methoddeterministic ties

Inside a mule network, PageRank scores who the money concentrates through. Ties break by primary key, so a rerun for casework reproduces the same ordering.

why the substrate matters

A detector can't chase an edge that isn't there yet.

When a transfer commits, the row, its forward edge, its reverse edge and the stream event land together — one atomic write. A traversal running in another connection cannot observe a state where the row exists but the reverse edge is missing; it is structurally impossible to see a half-written transfer.

That is the property fraud tooling quietly depends on. A path query that walks money backward — who fed this account? — needs the reverse edge at the same instant the forward one exists. On a synced graph copy that guarantee needs a reconciler; on one substrate it holds by construction.

one transfer, one commit
> POST /v1/tenants/:t/rows/transfers
> { "from": "acct_51", "to": "acct_19", "amount": 8200 }

committed atomically
  row      transfers/t_9041             
  edge     acct_51 → acct_19            
  edge     acct_19 ← acct_51 (reverse)  
  event    watch?prefix=transfers/      

a concurrent traversal sees all of it — or none
velocity rule
SELECT from_acct, COUNT(*) AS hits,
       SUM(amount)      AS total
FROM transfers
WHERE amount > 5000
GROUP BY from_acct
HAVING COUNT(*) > 3;
rules still run

The rule engine reads the same rows the graph walks.

Not every check is a traversal. Velocity thresholds, amount limits, per-account aggregates — plain SQL over the customer and transaction tables, with predicates pushed down to secondary indexes so rule evaluation stays fast.

Because rules and graph share one store, a rule hit can hand its account id straight to a components or all-paths call — no id mapping between systems, no window where the rule engine and the graph engine disagree about what happened.

measured
< 5 ms
per-hop traversal (small frontier)
5–20×
bidirectional vs forward all-paths
26.0 ms
4-hop trace · 35,745 edges considered
0
replication lag, detector ↔ data
1
HTTP GET to subscribe to every write
next

The stream and the graph, in detail.