Detect the ring
while it's still forming.
A fraud ring is a graph shape on a clock. OriginChainDB runs connected components, bidirectional all-paths and triangle enumeration against the very same store the transfers commit to — the detector queries the write the instant it lands, with no copy in between.
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, the transfer write, the SQL rules and the graph traversals all land on and read from the same store. Zero replication lag between them, because there is nothing to replicate. The detector and the data are the same surface.
graph view: a sync lag behind reality
graph view: the write that just committed
Write, then walk the graph. One store, no hand-offs.
The loop that usually spans three systems and an ops team is two HTTP requests here: the write that lands the transfer, and a graph query your handler fires when a rule hit warrants a look.
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 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 GET /v1/tenants/:t/graph/:schema/triangles
{
"schema": "transfers",
"min_amount": 5000,
"since": "PT2H"
}
runs against the store the transfer just landed in 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.
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.
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.
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.
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.
A detector can't chase an edge that isn't there yet.
When a transfer commits, the row, its forward edge and its reverse edge 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.
> 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) ✓
a concurrent traversal sees all of it — or none SELECT from_acct, COUNT(*) AS hits,
SUM(amount) AS total
FROM transfers
WHERE amount > 5000
GROUP BY from_acct
HAVING COUNT(*) > 3; 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.