Skip to content

Repository files navigation

Ledgerline — GST procurement collusion detection

A graph-powered console that surfaces hidden collusion in GST procurement data: employees quietly approving invoices for vendors they're secretly tied to, and clusters of "different" vendors that all funnel money into one bank account. Built on CognoDB (managed graph database, openCypher over Bolt) with the official Neo4j driver.

A non-technical reviewer opens the app, sees a ranked list of alerts in plain English, clicks one, and watches the collusion loop light up in the network.

Live demo: <your-vercel-url> Screen recording: <your-loom-or-video-link>


Why a graph database?

The whole product is one question: given an invoice, who is secretly connected to whom, and how? That question is about paths and cycles, not rows.

The core fraud signal is a loop:

Employee ──APPROVED──▶ Invoice ◀──SUBMITTED── Vendor
   ▲                                              │
   └──────────── shares bank account ─────────────┘

An employee approves an invoice for a vendor who shares a bank account (or address, or phone) with that same employee. Detecting it means walking a variable-length path that returns to where it started.

  • In SQL, this is a recursive self-join across five tables (employees, approvals, invoices, vendors, bank_accounts) with a join condition that ties the two ends of the chain back together. Adding "…or a shared address, or a shared phone, up to N hops apart" multiplies the join permutations and the query becomes unreadable and slow.

  • In Cypher, it's one pattern that reads like the fraud itself:

    MATCH (e:Employee)-[:APPROVED]->(i:Invoice)<-[:SUBMITTED]-(v:Vendor)
    MATCH (v)-[:HAS_ACCOUNT|LOCATED_AT|HAS_PHONE]->(s)<-[:HAS_ACCOUNT|LIVES_AT|HAS_PHONE]-(e)
    RETURN e, v, s, collect(i)

The relationships are first-class, so traversals are index-free adjacency hops rather than joins. Shell-cluster detection (many vendors → one account) and shortest-path explanations ("how is this employee connected to this vendor?") fall out of the same model for free. That's a graph database genuinely earning its place, not a relational schema in disguise.


Data model

Six labeled node types and typed relationships:

graph LR
  E[Employee]
  V[Vendor]
  I[Invoice]
  B[BankAccount]
  A[Address]
  P[Phone]

  E -- APPROVED --> I
  V -- SUBMITTED --> I
  E -- HAS_ACCOUNT --> B
  V -- HAS_ACCOUNT --> B
  E -- LIVES_AT --> A
  V -- LOCATED_AT --> A
  E -- HAS_PHONE --> P
  V -- HAS_PHONE --> P
Loading
Node Key properties
Employee id, name, department, role
Vendor id, name, gstin, registeredDate
Invoice id, number, amount, date, status
BankAccount id, accountNumber, ifsc
Address id, line, city, pincode
Phone id, number

The fraud signals are shared identity nodes: when an Employee and a Vendor point at the same BankAccount / Address / Phone, that shared node is the smoking gun. The seed data is mostly clean, with 3 shared-identity rings and 1 shell cluster deliberately planted so detection has something to find.


Main queries

All queries are parameterised through the official driver (see lib/queries.ts). None are string-concatenated.

  1. Shared-identity collusion (multi-hop cycle, SQL-awkward) — the loop above. Returns each (employee, vendor, shared-node) triple plus the invoices and total exposure.
  2. Shell-vendor cluster — one bank account, many vendors:
    MATCH (b:BankAccount)<-[:HAS_ACCOUNT]-(v:Vendor)
    WITH b, collect(DISTINCT v) AS vendors
    WHERE size(vendors) > 1
    RETURN b, vendors
  3. Vendor risk scoring — a single query aggregates each vendor's invoice volume, whether it shares identity with an approver, and whether it shares an account with other vendors; the score is derived from those graph signals.
  4. Vendor neighbourhood (variable-length traversal)(v)-[*1..2]-(m) expands a vendor's two-hop world for the drill-down view.

Architecture

app/
  page.tsx              Console: alerts, watchlist, interactive graph
  layout.tsx            Fonts + shell
  components/
    GraphView.tsx       Force-directed canvas (react-force-graph-2d)
    ui.tsx              Risk meter, legend, loading / empty / error states
    graph-style.ts      Node colours + sizes
  api/
    health/route.ts     DB liveness probe → drives the Live/Offline badge
    rings/route.ts      Detected collusion rings + shell clusters
    graph/route.ts      Overview graph (union of all ring subgraphs)
    vendors/route.ts    Ranked vendor watchlist
    vendor/[id]/route.ts  Two-hop neighbourhood for drill-down
lib/
  neo4j.ts              Driver singleton (serverless-safe) + run() helper
  queries.ts            All Cypher + result mappers
  types.ts              Shared types
scripts/
  seed.ts               Generates realistic data + planted fraud, loads via UNWIND

Notes worth calling out:

  • Serverless-safe driver. The Neo4j driver is cached on globalThis with a small connection pool (lib/neo4j.ts), so Vercel's many function instances don't exhaust the free tier's 200-connection ceiling.
  • Secrets from the environment only. The URI and password are read from NEO4J_* env vars and never committed. .env.example is the template; .env.local is git-ignored.
  • Graceful degradation. Every API route returns a 503 with a helpful hint when CognoDB is unreachable; the UI shows an Offline badge and a retryable error state instead of a blank screen.
  • Server-only DB access. neo4j-driver runs only in Node route handlers; nothing touches the database from the browser.

Setup

1. Create a CognoDB instance

  1. Sign up at https://console.cognodb.com/signup (free tier, no credit card).
  2. Create a free c0 instance and pick a region — it provisions in under a minute.
  3. Copy the connection URI (bolt+s://<instance-id>.databases.cognodb.cloud) and the generated password for user cognodb. The password is shown once — copy it now.

2. Configure the app

git clone <your-repo-url>
cd gst-fraud-graph
npm install
cp .env.example .env.local
# edit .env.local with your CognoDB URI, username (cognodb), and password

3. Seed and run

npm run seed     # loads ~45 vendors, 18 employees, ~250 invoices + planted fraud
npm run dev      # http://localhost:3000

.env.local:

NEO4J_URI=bolt+s://<instance-id>.databases.cognodb.cloud
NEO4J_USERNAME=cognodb
NEO4J_PASSWORD=<your-password>

Deploy (Vercel free tier)

  1. Push to GitHub and import the repo in Vercel.
  2. Add NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD as environment variables.
  3. Deploy. Run npm run seed locally once against the same instance so the hosted app has data.

Screenshots

Overview — map of suspicious activity Ring selected — collusion loop traced
docs/overview.png docs/ring.png

(Add screenshots after your first run.)


Tech

Next.js 14 (App Router, TypeScript) · CognoDB via neo4j-driver (Bolt) · Tailwind CSS · react-force-graph-2d.

Releases

Packages

Contributors

Languages