Read-only FalkorDB tools for cross-bank AML investigations: rings, money paths, echo-proof claims.
Many banks, one ring, every claim proven.

BankWeave is a team of AI agents that investigates money-laundering alerts across several banks, with FalkorDB as its shared brain. It clears innocent alerts fast, exposes laundering rings that no single bank can see, and backs every conclusion with a graph path you can click through. It also knows how many independent reasons it has for each belief, notices when one of them dies, and refuses to file a report it can't prove.
io.github.LSUDOKO/bankweave (v3.0.0, streamable HTTP)claude mcp add --transport http bankweave https://bankweave-api.onrender.com/mcp, or as a stdio package: claude mcp add bankweave -- docker run -i --rm ghcr.io/lsudoko/bankweave-mcp (17 read-only FalkorDB tools, each returning the Cypher it ran; docs/MCP.md)video/build.py --longdocs/BLOG.mdRunning now on real data. The production instance on FalkorDB Cloud holds IBM's labelled AML transactions for 6 banks: 3,024 accounts, 24,990 transfers, 36 graphs (6 bank graphs, the consortium graph with 10,178 nodes and 37,448 edges, 27 case graphs, agent memory and the typology knowledge base). The agents have triaged 1,208 alerts: 1,113 cleared with a stated reason and 95 escalated into cases, each case with claims, evidence, the Cypher behind every piece of evidence, a provenance-gated report and a signed Merkle seal.
BankWeave is an independent project built for FalkorDB Graph Hacks. It is not a FalkorDB product and is unrelated to FalkorDB's QueryWeaver or Weights & Biases Weave.
case_<id> graph, where claims are linked to evidence and the query that
produced it.| Area | What you can do | Where |
|---|---|---|
| Console | A sidebar listing all fourteen pages with live counts, Ctrl K search over pages and cases, and one card system across every page: stat cards from live queries, status chips, rounded charts | every page |
| Dashboard | Open cases, alerts decided, independent claims, actions awaiting a human; claim support for the latest cases; the next approval; the agent team; an alert-triage gauge; FalkorDB Cloud latency, graphs and memory | /command |
| Graph explorer | A FalkorDB-browser-style page: pick a graph, write Cypher (GRAPH.RO_QUERY, writes rejected by the engine), see an Obsidian-style force graph with labels, hover neighbourhoods and a property panel. Twelve presets, each a FalkorDB feature on our data: cross-bank ring, algo.pageRank, algo.labelPropagation, algo.wcc, algo.SPpaths, Double Metaphone full-text, HNSW vector KNN, shared identity, the agents' alert-to-claim chain, a SAR's provenance, the typology KB, learned procedures | /explorer |
| 3D explainer | Scroll through eight scenes on a three.js stage: six bank slices and per-graph ACL, hashed ids stitching the consortium, algo.wcc clearing an alert, the ring one bank sees 37.5% of, PageRank hub and algo.SPpaths, six paths collapsing to four origins, a KYC cascade, a Merkle root signed by a hardware key. Every number comes from the API; each scene shows the Cypher it ran | /explainer |
| Hosted MCP | claude mcp add --transport http bankweave https://bankweave-api.onrender.com/mcp: 17 read-only tools (cases, echo-proof claims, money paths, rings, hubs, components, vector and phonetic search, triage, typologies, guarded Cypher), 3 prompts, 2 resources | /mcp, docs/MCP.md |
| Landing | Centred hero over glossy blue bubbles with word-by-word type, the live console rising out of it, a "what's new" bento, the evaluation shown as highlighted screenshots of the live page, the real-tool recordings and the FalkorDB Browser proof | / |
| Real tools, recorded | The ESP32 sketch running in Wokwi posts a real card tap to the live API, and EMVLab decodes the field 55 it printed | Hardware Lab, below |
| Landing | Dashboard |
|---|---|
![]() | ![]() |
| Graph explorer: cross-bank ring on the consortium graph | 3D explainer: the ring as one bank sees it |
|---|---|
![]() | ![]() |
Graph explorer: algo.pageRank hubs (hover focuses one community) | 3D explainer: six bank slices, each sees only its own graph |
|---|---|
![]() | ![]() |
| Alert queue | Case workspace |
|---|---|
![]() | ![]() |
| Approvals | Hardware Lab |
|---|---|
![]() | ![]() |
The same data the agents use, opened in FalkorDB's own browser (browser.falkordb.com, connected
to our FalkorDB Cloud instance: FalkorDB v4.22, 36 graphs, about 81 MB). Every query below is
read-only and was typed into the browser; the timings are the engine's own RT readout. The
connection bar with the instance hostname is cropped out.

| Query | What it proves |
|---|---|
| Cross-bank ring: the case spanning the most banks, its accounts, banks and transfers (12 nodes, 166 edges, 1.85 ms) | one Cypher query sees the whole ring that no single bank graph contains |
CALL algo.pageRank('Account','TRANSFERS') top 12 (3.55 ms) | the hub detection the Investigator calls is native GraphBLAS, not Python |
(:Account)-[:CARD_TXN]->(:Terminal) (5 nodes, 29 edges, 0.41 ms) | the POS/ATM taps, including the one sent from the Wokwi ESP32, are real edges in the consortium graph; the Terminal node carries POS-MUM-…, Mumbai, GPS 19.076, 72.8777 |
(:SAR)-[:CONTAINS]->(:Sentence)-[:CITES]->(:Claim)-[:SUPPORTED_BY]->(:Evidence) in case_C-70431130 (0.61 ms) | every report sentence cites a claim, and every claim rests on evidence: the provenance gate is a graph pattern |
db.idx.vector.queryNodes('Account','behavior_vec',10, …) (0.36 ms) | HNSW vector search finds behaviour twins at five different banks |
Accounts at different banks sharing a Device or IP (291 nodes, 220 edges, 2.79 ms) | the identity links the consortium uses to join bank slices |
| Cross-bank ring | algo.pageRank |
|---|---|
![]() | ![]() |
| Card taps into terminals (the Wokwi tap included) | The Mumbai POS node and its properties |
|---|---|
![]() | ![]() |
| SAR → sentence → claim → evidence | HNSW vector KNN |
|---|---|
![]() | ![]() |
| Shared devices and IPs across banks | FalkorDB Cloud console: the instance (Free tier, ap-south-1, v4.22.0, healthy) |
|---|---|
![]() | ![]() |
Hosted MCP, called live (no install, plain HTTP):
$ curl -s -X POST https://bankweave-api.onrender.com/mcp -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
17 tools: engine_overview, list_graphs, graph_schema, list_cases, get_case, explain_claim,
account_network, trace_money_path, find_rings, top_hubs, connected_components, similar_accounts,
search_customers, list_alerts, triage_alert, typology_guidance, run_readonly_cypher
$ ... "method":"tools/call","params":{"name":"top_hubs","arguments":{}}
{"graph": "consortium", "result": [{"account": "e8ccd1faa8d3a4ba", "bank": "001", "score": 0.0027...}, ...
The top account matches row 1 of the PageRank query in the FalkorDB Browser above.
| Area | What you can do | Where |
|---|---|---|
| Command Center | Live alerts, open cases, independently supported claims, evidence awaiting review, FalkorDB health (memory, graphs, engine) in one view | / |
| Golden scenarios | A: an innocent alert cleared by algo.wcc. B: a ring across 4 banks (best single bank holds 50% of it; E2: 43.8% on average, 0% of rings whole). C: a shared 200-unit building behind a claim, with the cascade a KYC update would trigger. Computed live; bundled snapshots load instantly when the free-tier API sleeps | /scenarios |
| Mode badge | Every page says whether it is live with LLM agents, live with deterministic agents, or a seeded replay | rail, page headers |
| FalkorDB Lab | Every graph with counts, indexes and GRAPH.MEMORY USAGE; 17 one-click runs (10 algorithms, phonetic and stemmed full-text, vector KNN, JS UDFs, FLEX, a tri-modal query); a read-only GRAPH.PROFILE/GRAPH.EXPLAIN inspector; the MCP tool explorer with schemas, live payloads, the Cypher each tool ran and an Engine X-Ray of it; per-bank ACLs; procedural memory as a graph | /lab |
| Case workspace | Evidence Independence Explorer ("15 paths → 12 origins"), revoke any evidence and watch the cascade, provenance DAG (React Flow), Replay Investigation, bi-temporal time machine, Simulate Freeze on a GRAPH.COPY sandbox, sanctions screening, crypto off-ramp linking, agent trace waterfall | /cases/<id> |
| Signed exports | FinCEN-style SAR XML and a printable audit dossier: every sentence, the evidence it cites, each query with an execution hash, and an Ed25519-signed Merkle root over the case | /cases/<id>/dossier |
| 3D consortium | Bank slices walled in by external stubs, the consortium where they join (flow particles, PageRank hub), cases and claims on top; fly-through, echo collapse, invalidation shockwave, kill-and-resume with a heartbeat countdown | /consortium-3d |
| Evaluation | E2 hero, E1 table plus a threshold explorer, E6 scorecard, E3-E5 with misses labelled, E7 badged as a local benchmark | /evaluation |
| Hardware Lab (device twins, real cryptography) | POS/ATM digital twin emitting ISO 8583 with real EMV TLV into the graph; a Wokwi ESP32 terminal; approvals signed with WebAuthn or a virtual security key over the Merkle root ("Merkle seal broken" if evidence changes later); an enclave with PCR attestation that quarantines itself when tampered with; ECDH-PSI between banks | /hardware-lab, hardware/wokwi/ |
| Integrations | OpenSanctions screening (live with an API key, bundled sample list without one), on-chain wallet facts, OpenTelemetry-shaped spans with OTLP export | /integrations/*, case tabs |
The card terminals, enclave and security key run as software device twins (the ESP32 terminal in Wokwi), producing real EMV/ISO 8583 data, real signatures, real key exchange and real graph writes through the same API a physical device would call.
Captured on https://bankweave.vercel.app against the Render API and FalkorDB Cloud (ap-south-1), agents on Groq's free tier; the mode badge reads "Live · LLM + FalkorDB".
Golden scenarios: A cleared by algo.wcc, B the ring across banks, C the cascade | Bank vs consortium: one bank's fragments against the consortium |
|---|---|
![]() | ![]() |
| Cases | Case workspace: evidence independence ("15 paths → 14 origins") |
|---|---|
![]() | ![]() |
| Provenance DAG | Simulate Freeze on a GRAPH.COPY sandbox |
|---|---|
![]() | ![]() |
| Agent trace waterfall (OpenTelemetry-shaped spans from the live agents) | Sanctions screening and crypto off-ramp bridge |
|---|---|
![]() | ![]() |
| The SAR: every sentence cites its evidence | Audit dossier with the signed Merkle root |
|---|---|
![]() | ![]() |
| Trust layer: echo-proof confidence and the cascade | 3D consortium |
|---|---|
![]() | ![]() |
| FalkorDB Lab: graphs, nodes, edges, memory | Algorithms and indexes, one click each |
|---|---|
![]() | ![]() |
GRAPH.PROFILE / GRAPH.EXPLAIN inspector | MCP tools with live payloads and the Cypher each ran |
|---|---|
![]() | ![]() |
| Engine features: JS UDFs, tri-modal query, per-bank ACL | Evaluation |
|---|---|
![]() | ![]() |
Measured, with the evidence highlighted. The landing page's "The numbers, not the pitch" shows these captures of the live Evaluation page:
| E2: best single bank 43.8%, rings seen whole 0 of 36, consortium 100% | E1: calibrated graph score F1 0.205 vs 0.063 escalating everything |
|---|---|
![]() | ![]() |
| E6: 27.8% echo caught, 25 of 28 SARs through the gate, 100% cascade recall | E7: vector KNN 0.4 ms, PageRank over the whole consortium 57.5 ms |
|---|---|
![]() | ![]() |
Banks don't let software act alone: terminals produce the device evidence, HSMs and enclaves guard identity hashing, and a human signs off with a security key. BankWeave runs all three as device twins in software and wires each into FalkorDB.
Recorded on the real tools. The sketch in hardware/wokwi/ running in the Wokwi ESP32
simulator: it joins Wokwi-GUEST, Card A is tapped on the Mumbai POS, the board posts it over
HTTPS to the live API, which answers 00 APPROVED, writes the CARD_TXN edge into FalkorDB
Cloud and returns the ISO 8583 field 55 printed on the serial monitor. EMVLab.org then decodes
that exact TLV (amount, currency, IFD serial D0006301, ATC, cryptogram, PAN).
| Wokwi ESP32 → live API → FalkorDB | EMVLab decodes the tap's field 55 |
|---|---|
![]() | ![]() |
The Hardware Lab on the live site:
| Terminals: POS and ATM twins with their last taps, EMV TLV, verify on EMVLab | Security key: sign the case's Merkle root with WebAuthn or the virtual VaultKey |
|---|---|
![]() | ![]() |
| Enclave: attestation document, PCRs, HSM signer, bank links | ESP32 on Wokwi: the real sketch, diagram and live serial log |
|---|---|
![]() | ![]() |
| ECDH-PSI between two bank graphs (X25519) | Wokwi tap as an edge in FalkorDB Cloud (FalkorDB Browser) |
|---|---|
![]() | ![]() |
What each device twin produces:
| Piece | Real output | Runs as |
|---|---|---|
| Card terminals | BER-TLV encoding (verified by EMVLab), ISO 8583 field layout, graph writes (CARD_TXN, USES_DEVICE {emv_atc, terminal_id, gps, nfc_uid}), alerts the agents triage, impossible-travel and ATC-replay detection | the terminals and cards; 9F26 is an HMAC, not a chip's 3DES/AES MAC |
| Wokwi ESP32 | the circuit runs in Wokwi and makes real HTTPS posts | ESP32 board in the Wokwi simulator |
| Approval keys | WebAuthn ES256 verification, signature counter, Merkle root over claims/evidence/sentences, seal break on revocation | VaultKey is a WebCrypto key in the tab |
| Enclave / HSM | HMAC-SHA256 blinding, SHA-384 PCRs, ES384-signed Nitro-style document, (:EnclaveAttestation) nodes, ingestion refused with 423 while quarantined | no TEE hardware or AWS Nitro PKI chain |
| PSI | X25519 scalar multiplication, commutative blinding | both banks run in one process |
External verifiers: EMVLab TLV decoder · EMVLab cryptogram tool · WebAuthn.me debugger · Chrome virtual authenticator · Wokwi ESP32 · OpenSanctions · FalkorDB Cloud.
Turn FalkorDB off and BankWeave can't do any of its core work:
| Without the graph… | because |
|---|---|
| no ring is visible | the ring only exists as a path between banks; the consortium graph is where it connects (see E2) |
| triage can't clear anything | components, motif scores, cross-bank links and shared identity are all graph queries |
| no claim has evidence | evidence is a path, an algorithm score or a shared attribute in the graph, stored with its query |
| confidence can't be computed | echo-proof confidence counts distinct origins over SUPPORTED_BY edges |
| invalidation can't cascade | dependencies are typed edges (SUPPORTED_BY, DERIVED_FROM, JUSTIFIED_BY, CITES) |
| agents can't coordinate or resume | tasks, heartbeats and the case timeline live in the graph |
FalkorDB features in use: multigraph (per-bank, consortium, per-case, procedures, typology KB
graphs in one instance), native algorithms (algo.wcc, algo.labelPropagation,
algo.pageRank, algo.betweenness, algo.HarmonicCentrality, algo.SPpaths, algo.SSpaths,
algo.maxFlow, algo.bfs, algo.MSF), full-text indexes with Double Metaphone phonetic
matching and English stemming, a vector index (HNSW) on behaviour profiles, JavaScript
UDFs (FalkorDB's FLEX library and our own aml_temporal), GRAPH.COPY sandboxes,
GRAPH.PROFILE/GRAPH.EXPLAIN, GRAPH.MEMORY USAGE, ACLs scoped to one graph, and
the Redis protocol for live pub/sub.
| Feature | Where in the code | Evidence it runs |
|---|---|---|
| Multigraph (isolated graphs in one instance) | consortium, bank_<id>, case_<id>, procedures, typology_kb (backend/app/db.py) | Browser shows 43-188 graphs during a run; FalkorDB Cloud holds consortium + 6 bank graphs + typology_kb |
algo.wcc | precompute component sizes; triage islands (detection/precompute.py, detection/algorithms.py) | E7 timing; triage reasons "isolated cluster" in the alert queue |
algo.labelPropagation | communities (detection/precompute.py) | Account.community on every account |
algo.pageRank | hub finding in case graphs; SPNSA intermediaries | on FalkorDB Cloud (3.55 ms); E7 timing |
algo.betweenness (seeded) | mule bridges; SPNSA | ![]() |
algo.HarmonicCentrality | orchestrator candidates | find_orchestrator tool, rebuttal rounds |
algo.SPpaths | money paths: the primary evidence of a claim | path evidence in every case, with its query |
algo.SSpaths, algo.bfs | reach, bfs_reach tools (case graph) | available to the Investigator and over MCP |
algo.maxFlow | money at risk over collapsed FLOWS edges | hub claims cite max-flow evidence |
algo.MSF | backbone tool: a ring's spanning backbone | available to the Investigator, Reporter and over MCP |
| Full-text index + Double Metaphone | Person.name, Company.name (detection/identity.py) | test_identity.py: a spelling variant finds its original identity |
| Full-text index + English stemming | typology knowledge graph (knowledge/typology_kb.py) | cited answers; abstains on unrelated questions |
| Vector index (HNSW, cosine) | 16-dim behaviour profiles (app/services/loader.py) | behavior_neighbors, triage features, clue expansion |
| Range indexes incl. relationship property index | TRANSFERS.ts (the txn_id edge index was dropped in 2.0: nothing looks it up, and it cost ~8 MB) | CALL db.indexes() |
JavaScript UDFs (GRAPH.UDF LOAD) | aml_temporal.timeRespectingReach walks TRANSFERS with getNeighbors, following only later transfers (backend/udf/aml_temporal.js) | test_lab.py::test_udf_libraries_load_and_run; Lab → Engine features |
| FLEX (FalkorDB's UDF library, vendored 1.0.1) | flex.text.jaroWinkler, flex.text.levenshtein for names, flex.sim.jaccard for shared identity attributes | Lab runs flex_names, flex_jaccard |
GRAPH.COPY | Simulate Freeze copies the case graph, deletes the account in the copy, reruns algo.maxFlow + algo.wcc (app/routers/workspace.py) | test_workspace.py: flow 80 → 0, sandbox deleted |
GRAPH.PROFILE / GRAPH.EXPLAIN | read-only inspector and Engine X-Ray per tool call; a query must pass GRAPH.RO_QUERY first (app/services/engine.py) | test_lab.py::test_inspect_refuses_writes |
GRAPH.MEMORY USAGE | per-graph memory in the Lab and Command Center; capacity guard for the 100 MB cloud tier (app/services/capacity.py) | /lab/graphs |
ACL (~graph key patterns, ACL LOG) | a bank user that can only GRAPH.RO_QUERY its own graph (/lab/acl-demo) | consortium, other banks and writes denied, logged (screenshot above). The FalkorDB Cloud user cannot manage ACLs, so this runs on Docker |
Several CALLs in one query | tri-modal query: phonetic full-text → HNSW vector twins → algo.SPpaths over FLOWS | Lab trimodal (about 10 ms) |
Parameterised Cypher, ro_query for reads | every query (docs/QUERIES.md: 204 registered + 67 inline) | read-only MCP tool cypher_readonly |
| Concurrent writers on one graph | task blackboard (agents/blackboard.py) | test_blackboard.py: 6 threads, 20 tasks, each claimed once |
| FalkorDB Browser | visual inspection of every graph | screenshots below |
| FalkorDB Cloud | live instance (ap-south-1) seeded with the IBM slice | scripts/make_slice.py + seed log |
Screenshots from FalkorDB Browser connected to the production instance the live site uses
(docs/img/cloud/):
| What | Screenshot |
|---|---|
| Multigraph: consortium, six bank graphs and case graphs in one instance | ![]() |
| Index types in use: 1 vector (HNSW), 2 full-text (phonetic), 14 range | ![]() |
| Every index, by label and property | ![]() |
algo.pageRank on the consortium (4.3 ms) | ![]() |
| Vector similarity over behaviour profiles (0.5 ms) | ![]() |
| Double Metaphone full-text: "Hana Sharma" finds "Hannah Sharma" | ![]() |
| A ring the live agents opened: one hub paying accounts at four banks (consortium graph, 0.98 ms) | ![]() |
That case's own graph: claims, SUPPORTED_BY and Skeptic CHALLENGED_BY evidence; SAR, Sentence, Event and Action live here too | ![]() |
| The consortium ring in FalkorDB Browser | A case graph: claims and their evidence |
|---|---|
![]() | ![]() |
| Bank vs consortium on the live site | Golden scenario B: the ring one bank holds 37.5% of |
|---|---|
![]() | ![]() |
| Alert queue on real IBM data | Case workspace |
|---|---|
![]() | ![]() |

flowchart TB
UI[Next.js UI: queue, network weave, case workspace, SAR, trust, approvals, scoreboard] -- REST / WebSocket --> API
API[FastAPI + WebSocket bridge] --> W
subgraph W[Agent worker: LangGraph flow]
O[Orchestrator] --> T[Triage] --> I[Investigator] <--> S[Skeptic] --> R[Reporter] --> A[Auditor]
H[Human approval] --> AR[Archivist]
end
W -- tools --> TOOLS[Tool registry]
MCP[MCP server: 34 tools] --> TOOLS
TOOLS -- Cypher / algo.* --> F[(FalkorDB: consortium, bank_*, case_*, procedures, typology_kb)]
TOOLS -- SQL --> PG[(Postgres KYC)]
W -- events --> RD[(Redis pub/sub)] --> API
W -- LiteLLM --> LLM[Groq free tier: writer qwen3.8-27b + different, larger auditor gpt-oss-120b]
The LLMs plan, choose tools and write prose. Cypher and algo.* do the computation. With no
API key the agents run a deterministic policy (labelled as such in the UI), so the system is
always runnable.

Full schema: docs/GRAPH_MODEL.md. In short:
consortium: Account (hashed id, bank, degrees, PageRank, component, community, motif
score, pass-through stats, 16-dim behaviour vector) -[:TRANSFERS]-> Account, a collapsed
-[:FLOWS]-> edge per account pair, the identity layer (Person/Company -[:OWNS]->;
USES_DEVICE, USES_IP, REGISTERED_AT), Alert, Decision, blackboard Task, case mirrors.bank_<id>: the bank's own Accounts plus ExternalAccount stubs; only transfers touching
the bank.case_<id>: Claim -[:SUPPORTED_BY|CHALLENGED_BY]-> Evidence {origin, cypher},
Claim -[:DERIVED_FROM]-> Claim, Action -[:JUSTIFIED_BY]-> Claim,
SAR -[:CONTAINS]-> Sentence -[:CITES]-> Evidence|Claim, Evidence -[:SUPERSEDES]-> Evidence,
Case -[:HAD_EVENT]-> Event -[:NEXT]-> Event.Every query the product runs is listed in docs/QUERIES.md, generated from
the code. A few that carry the product:
// echo-proof confidence: count independent origins, not paths
MATCH (c:Claim {id:$id})-[:SUPPORTED_BY]->(e:Evidence)
WHERE e.state IN ['active', 'needs_verification']
RETURN count(e) AS paths, count(DISTINCT e.origin) AS independent_sources
// money path between two accounts: the primary evidence for a claim
MATCH (s:Account {id:$src}), (d:Account {id:$dst})
CALL algo.SPpaths({sourceNode:s, targetNode:d, relTypes:['TRANSFERS'],
maxLen:$max_len, pathCount:$path_count, relDirection:$direction})
YIELD path, pathWeight
RETURN [n IN nodes(path) | n.id] AS accounts, [t IN relationships(path) | t.txn_id] AS txn_ids
// money at risk: max flow from flagged accounts to the hub (collapsed FLOWS edges)
CALL algo.maxFlow({sourceNodes:srcs, targetNodes:tgts, relationshipTypes:['FLOWS'],
capacityProperty:'capacity', nodeLabels:['Account']})
YIELD maxFlow, edges, edgeFlows
// premise check: does this action rest on overturned evidence?
MATCH (a:Action {id:$id})-[:JUSTIFIED_BY]->(c:Claim)
OPTIONAL MATCH (c)-[:SUPPORTED_BY|DERIVED_FROM*1..5]->(e:Evidence {state:'superseded'})
RETURN c.id, collect(e.id) AS stale
Things FalkorDB doesn't ship natively, done in plain Cypher (and said so): degree (precomputed), time-ordered cycles (iterative time-respecting expansion, one query per hop), 3-cycles (motif score), eigenvector centrality (PageRank used instead in SPNSA).

| Agent | Does |
|---|---|
| Orchestrator | turns open alerts into blackboard tasks, resets stale claims |
| Triage | clears an alert with a reason, or opens a case graph |
| Investigator | chooses graph tools (LLM) or follows a learned procedure; writes evidence-linked claims; cites named typologies |
| Skeptic | argues the innocent explanation with KYC facts and attribute-sharing context; at most 2 rounds |
| Reporter + Auditor | SAR through the provenance gate; a different, stronger model audits |
| Archivist | saves the tool sequence that worked (procedural memory), closes the case |
Hosted MCP (read-only, no install): claude mcp add --transport http bankweave https://bankweave-api.onrender.com/mcp.
It serves 17 read-only tools over Streamable HTTP from the same Render API; every tool runs through
GRAPH.RO_QUERY and returns the graph, Cypher, parameters and time. Install lines for Claude
Desktop and Cursor, the tool table and a real transcript are in docs/MCP.md.
The full local MCP server (python -m mcp_server.server, stdio or --http PORT) exposes all 34 tools (32 agent tools plus cypher_readonly and list_graphs),
including trace_flow, find_hub, find_bridges, money_at_risk, typology_patterns,
passthrough_check, clue_expand, hunt_regions, resolve_entity, behavior_neighbors,
kyc_lookup, kb_query, echo_check, invalidate_evidence, premise_check, plus
cypher_readonly and list_graphs. Every result includes the Cypher it ran.
Claude Desktop config:
{ "mcpServers": { "bankweave": {
"command": "/path/to/BankWeave/backend/.venv/bin/python",
"args": ["-m", "mcp_server.server"], "cwd": "/path/to/BankWeave/backend" } } }

| Feature | Failure it prevents | Source |
|---|---|---|
| Echo-proof confidence | counting one fact many times because many paths lead to it | GraphEcho, arXiv 2609.17695 |
| Cascading invalidation + premise check | acting on beliefs whose evidence was overturned | PACMI, arXiv 2610.05732 |
| Provenance gate + divergence report | a fluent report inventing agreement the evidence doesn't support | Active Provenance Gate, arXiv 2609.31422 |
Evidence carries an origin (e.g. attr:D00063, flow:a->b, typ:cycle:a).
Confidence is computed over distinct origins; a 200-unit building address is down-weighted.
The UI shows "N paths, K independent sources" and the naive figure beside it.active < needs_verification < historical_only < superseded); actions resting on stale
evidence need an explicit override note to approve.Task nodes; claiming is one atomic query (verified with
6 concurrent workers, each task done exactly once).Event timeline (HAD_EVENT, NEXT), replayable in the UI.beliefs_as_of).procedures graph stores tool sequences that led to approved outcomes.Account <-[:ABOUT]- Claim).
4,000 alerted accounts from the later half of the period (129 illicit, base rate 3.2%); weights and thresholds from the earlier half only.
| Method | Precision | Recall | F1 | Accounts escalated | Alerts not escalated |
|---|---|---|---|---|---|
| Rule-based: escalate everything | 3.2% | 100.0% | 0.062 | 4,000 | 0.0% |
| Rules without the graph (≥3 rules) | 33.3% | 7.8% | 0.126 | 30 | 99.2% |
| BankWeave graph score, hand weights (no labels) | 18.9% | 14.0% | 0.161 | 95 | 97.6% |
| BankWeave graph score, calibrated on dev split | 17.8% | 24.0% | 0.205 | 174 | 95.7% |
eval/results/E1_alert_triage.json, commit 49f9388, HI-Small_Trans.csv (20 banks, graph prefix 'ibm_')
Across 36 laundering groups that span banks (on average 3.5 banks each), the best-placed single bank holds 43.8% of the ring's accounts and holds a whole ring in 0.0% of cases. It does observe 95.7% of the laundering transfers, but most counterparties are other banks' customers it can't investigate. The consortium graph holds 100% of every ring.
eval/results/E2_consortium_vs_single_bank.json, commit 49f9388, HI-Small_Trans.csv (20 banks, graph prefix 'ibm_')
| Seed feed | Subgraph accounts | Illicit ratio | Paper (HI-Small) |
|---|---|---|---|
| Random (mean of 10) | 250 | 3.4% | 2.4% |
| Degree imbalance | 336 | 17.0% | 13.7% |
| Motif-coherent (paper parameters) | 280 | 7.5% | 22.6% |
| Motif, no degree penalty (post-hoc ablation) | 378 | 11.6% | — |
Base rate in this graph: 2.5%. We did not reproduce the paper's ranking: with its parameters the degree penalty inverts the motif score on our 20-bank slice (account-level AUC 0.3743, vs 0.638 without the penalty), so degree imbalance wins here. All feeds beat random.
eval/results/E3_spnsa_hunt.json, commit cc7037a, HI-Small_Trans.csv (20 banks, graph prefix 'ibm_')
708 accounts flagged; 4.8% of them illicit vs a 2.5% base rate. Chain-deduplicated value: 74,961,854,871 (IBM amounts are in many currencies, so this is a raw sum, not a USD figure).
eval/results/E4_passthrough.json, commit 154672b, HI-Small_Trans.csv (20 banks, graph prefix 'ibm_')
From 2 clue members of 12 laundering groups: best round 2 reaches precision 33.3%, recall 3.4%. This is weak; the graph-signal version (no GNN) mostly finds the clue's immediate partners.
eval/results/E5_clue_expansion.json, commit 706ae01, HI-Small_Trans.csv (20 banks, graph prefix 'ibm_')
Over 28 cases the agents built from the live IBM slice:
eval/results/E6_trust_layer.json, commit f3cff38, IBM_SLICE_Trans.csv (6 banks, graph prefix '')
Consortium graph: 28,307 accounts, 1,315,597 transfers.
| Operation | Median | 95th percentile |
|---|---|---|
| expand neighborhood | 3.1 ms | 6.1 ms |
| typology patterns | 12.5 ms | 674.0 ms |
| cross bank links | 0.6 ms | 0.9 ms |
| shared identity | 0.7 ms | 0.9 ms |
| behavior neighbors | 0.4 ms | 1.0 ms |
| passthrough check | 2.8 ms | 10.5 ms |
| triage signals hub accounts | 1.1 ms | 2.3 ms |
| triage signals regular accounts | 15.4 ms | 58.0 ms |
| pagerank consortium | 57.5 ms | |
| wcc consortium | 39.5 ms |
eval/results/E7_performance.json, commit b8a1292, ibm_
| Piece | Where | How |
|---|---|---|
| Graph database | FalkorDB Cloud, AWS ap-south-1 | seeded with scripts/make_slice.py + scripts/seed_all.sh (real IBM slice: 24,990 transactions, 6 banks) |
| API + agent worker | Render web service bankweave-api (Singapore) | root Dockerfile; created with the Render CLI; agents run in the API process |
| Live events | in-process event bus (no Redis needed on a single service) | backend/app/events.py |
| KYC Postgres | Neon (aws-ap-southeast-1) | python -m app.services.kyc_synth --postgres-only |
| UI | Vercel bankweave.vercel.app | vercel deploy --prod from frontend/ |
| LLMs | Groq free tier (writer qwen/qwen3.8-27b, auditor openai/gpt-oss-120b), OpenRouter free models as fallback | backend/agents/llm.py |
Render's free plan sleeps after 15 minutes idle; the first request wakes it (about a minute) and
the agents resume from the graph blackboard. A GitHub Actions job pings /health every 10 minutes
to keep it warm, and while it wakes the main pages stay available from captured real API responses
(scripts/capture_snapshots.py), labelled on screen.
FalkorDB Cloud's free tier has 100 MB and noeviction. Since 2.0 the graphs load in compact form
(KYC_ARGS=--compact, no unused txn_id edge index, no id indexes on ~100-node case graphs),
/health reports used memory, and scripts/cloud_reseed.sh rebuilds the cloud graphs while keeping
pinned cases. Old case graphs are pruned automatically only when CAPACITY_PRUNE=true.
Optional environment variables: OPENSANCTIONS_API_KEY (live sanctions screening),
HELIUS_API_KEY (Solana wallet lookups), EVM_RPC_URL, OTEL_EXPORTER_OTLP_ENDPOINT and
OTEL_EXPORTER_OTLP_HEADERS (export agent spans to Langfuse, Jaeger or any OTLP collector),
DOSSIER_SIGNING_KEY (hex Ed25519 seed for dossier signatures), PINNED_CASES, CAPACITY_PRUNE.
Prebuilt API image: docker pull ghcr.io/lsudoko/bankweave-api:latest (published on every release).
docker compose up -d # FalkorDB (6390, Browser 3030), Redis (6391), Postgres (5440)
cp .env.example .env # add GROQ_API_KEY (free) for LLM agents (optional)
cd backend && uv sync && cd ..
scripts/seed_all.sh # sample dataset -> bank graphs, consortium, identity, precompute
cd backend && uv run python -m app.services.baseline && uv run uvicorn app.main:app --port 8000
cd frontend && pnpm install && pnpm dev -p 3001
Open http://localhost:3001, go to Demo control and press Start agents (the agents begin triaging alerts and opening cases; the dashboard updates live).
IBM data for evaluation: scripts/download_ibm.sh (Kaggle token) or the public mirror
hf.co/datasets/eexzzm/IBM-Transactions-for-Anti-Money-Laundering-HI-Small-Trans, then
GRAPH_PREFIX=ibm_ scripts/seed_all.sh data/raw/HI-Small_Trans.csv --top-n 20 and
GRAPH_PREFIX=ibm_ eval/run_all.sh.
Tests: cd backend && uv run pytest (55 tests, against the local FalkorDB).
Hardware: the ESP32 terminal in hardware/wokwi/ runs in Wokwi and posts card taps over HTTPS to
POST /hardware/pos-tap (see its README).
eval/. The live instance runs a 6-bank slice of it
(3,024 accounts, 24,990 transfers).backend/app/services/kyc_synth.py, seed 42): people,
name variants, devices, IPs, addresses and a Postgres KYC table. Generation rule, reported so
leakage can be audited: attempt members share a device with P=0.40, innocent accounts with
P=0.03; 6% of innocent accounts share a family address/IP; one 200-unit building (A-77) houses
about 15 accounts including some attempt members.data/demo, seed 42): IBM-format transactions with innocent
background and 16 cross-bank laundering schemes in the 8 IBM shapes.Built with help from Claude Code (Anthropic), which wrote much of the code under the
team's direction; every module was reviewed and is understood by the team. At runtime the
agents use free-tier LLMs through Groq (writer qwen/qwen3.8-27b, auditor openai/gpt-oss-120b,
a different and larger model from a different vendor), with OpenRouter free models as fallback.
scripts/seed_all.sh takes any IBM-format CSV).MIT, see LICENSE.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
docker run -i --rm ghcr.io/lsudoko/bankweave-mcp:3.0.0Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-lsudoko-bankweave": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/lsudoko/bankweave-mcp:3.0.0"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceghcr.io/lsudoko/bankweave-mcp:3.0.0dockerBankWeave works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.