Inspiration

Most fraud-detection tools hand back a black-box score with no explanation, which makes them hard to trust and hard to act on. We wanted to build something that shows its reasoning the way a human investigator would check a few different angles, then explain the verdict in plain language.

What it does

ChainSentry reviews financial transactions and explains why one looks risky. Three independent agents each check something different structural fraud patterns, statistical drift from normal behavior, and proximity to known-illicit accounts and a synthesis agent turns their findings into a plain-English risk report. It runs on real data from the Elliptic Data Set (~20,000 real Bitcoin transactions, partially labeled illicit/licit). On our own data, the pattern agent caught a transaction that received one deposit and immediately fanned it out into 288 separate payments a textbook layering pattern, found automatically, not hand-picked.

How we built it

  • Backend: FastAPI + Supabase (Postgres) for data storage and the transaction/risk-report API
  • Agents: A custom multi-agent pipeline in Python pattern, velocity, and network-proximity agents run independently, then a synthesis agent (Qwen3.7-max via QwenCloud) combines their findings into a final report
  • Frontend: Next.js + Tailwind, styled as a "case file" dashboard a risk-ranked feed, two summary charts, and a detail page per transaction showing the full agent reasoning trace with stamped verdicts
  • Data: Elliptic Data Set (Kaggle) real on-chain Bitcoin transaction data with partial illicit/licit labeling

Challenges we ran into

Early on, ranking transactions purely by statistical drift surfaced mostly licit transactions, not illicit ones a real finding that raw numbers alone don't reliably separate fraud from normal activity in this dataset. That's exactly why we built independent pattern and network-proximity checks rather than relying on one model to do everything. We also hit intermittent dropped connections to our database mid-batch, which pushed us to add retry logic so a flaky connection never surfaces as a broken result.

What we learned

Explainability isn't a UI feature bolted onto a model's output afterward designing for it from the start changed how we approached the whole architecture, and it directly surfaced a real limitation in our own data that a single black-box score would have hidden.

What's next

  • Compute the drift baseline from licit-only transactions for a cleaner signal
  • Load the full ~200k-row dataset for denser graph connectivity
  • Have the synthesis agent return a structured score instead of parsing it from free text

Built With

Share this project:

Updates

Submission history