DEV Community

Cover image for The risk isn't on the brochure: building a due-diligence agent on a graph
ibrahim
ibrahim

Posted on AI-assisted

The risk isn't on the brochure: building a due-diligence agent on a graph

TL;DR: I built Khoj, an AI agent that checks Bangalore property launches by following who is connected to whom in a FalkorDB graph. It scores risk as a sum over graph paths (Katz centrality), explains every score as a path you can see, and remembers each buyer across sessions. On 200 held-out projects that all look clean on paper, looking at each project's own records finds 0% of the hidden risk. The graph finds 81%, with zero false alarms.
Code: https://github.com/ibrxhimj/khoj


If you've looked at new residential launches in Bangalore, you know the brochure: rendered towers, a clubhouse, "RERA approved", a launch offer. What you don't see is the history of the people behind it.

Developers usually create a fresh company for each project. That's normal and legal, but it means a brand-new company with a clean record can be run by the same directors whose last company went insolvent and left buyers with half-built towers. The plot might have a previous owner tied up in a title suit. The contractor might be two years late on three other sites.

None of that is hidden. It sits in RERA filings, company records, court records and land records. It's just never connected for the buyer.

💡 The answer to "is this safe?" doesn't live in any single record. It lives in the relationships between them.

That's a graph problem. So I built Khoj (Hindi for "search").

The graph

The knowledge graph models what a careful property lawyer would look at:

  • Developers: brands CONTROL companies; people are DIRECTOR_OF companies.
  • Projects: a company DEVELOPS a project, which is BUILT_ON a land parcel that was PREVIOUSLY_OWNED_BY someone.
  • Execution: a contractor BUILDS projects, and approvals hang off each project.
  • Trouble: complaints point AGAINST projects; court cases INVOLVE land, companies, people or contractors.

Anything that's a warning sign on its own (a stalled project, an insolvent company, a pending court case, a B-khata plot) gets a :RedFlag label with a severity and a reason.

The data is synthetic but realistic: 91 projects, 46 companies and 100 people across real Bangalore localities, generated from a fixed seed. All names are fictional. My favourite planted story is Skyvista Altura: a brand-new launch in Sarjapur Road with RERA, A-khata land and zero complaints. It looks perfect, until you notice its company shares two directors with Northstar Habitats, which is in insolvency proceedings and left two projects stalled.

Turning paths into a score

I wanted a risk score that was explainable by construction, so that every point of it had a path behind it.

For each project, one Cypher query finds every path of up to four hops to a red flag:

MATCH path = (p:Project {id: $id})-[:DEVELOPS|DIRECTOR_OF|CONTROLS|BUILT_ON|
             PREVIOUSLY_OWNED_BY|INVOLVES|AGAINST|HAS_APPROVAL|BUILDS*1..4]-(f:RedFlag)
WHERE f <> p
  AND none(n IN nodes(path)[1..-1] WHERE n.kind = 'Project')
  AND size([r IN relationships(path) WHERE type(r) = 'PREVIOUSLY_OWNED_BY']) <= 1
RETURN f.severity AS severity, [r IN relationships(path) | type(r)] AS rels, nodes(path) AS nodes
Enter fullscreen mode Exit fullscreen mode

Each path contributes its flag's severity, weakened by distance and by how strong each link is:

K(p)  = Σ over paths P:  severity(f) · 0.6^(hops−1) · Π edge weights
score = 100 · (1 − e^(−K))
Enter fullscreen mode Exit fullscreen mode

If that looks familiar, it's Katz centrality, restricted to red-flag targets. Summing over all paths was deliberate: a company that shares two directors with a failed developer should look worse than one that shares one. For Skyvista Altura, the 10 paths add up to K = 1.87, which gives 84.6 out of 100.

Lesson 1: calibrate against the whole graph, not just your demo

My first version scored Skyvista Altura correctly, and also scored the median project at 94 out of 100. Contractors and land aggregators are hubs. Paths ran from a project to its contractor, to another project, to that project's 30 complaints, and everything looked dangerous.

Two structural rules fixed it:

  • Paths can't pass through another project. A troubled project is already a red flag; its complaints shouldn't count again for its neighbours.
  • A path can use at most one land-ownership edge, so a land aggregator that once owned many plots doesn't connect unrelated projects.

The median dropped to 28, clean projects scored 0, and the planted stories still stood out.

Graph algorithms as agent tools

Two questions every buyer should ask, "who is really behind this brand?" and "is anyone here a serial operator?", map directly onto FalkorDB's built-in algorithms:

  • Weakly Connected Components on the director–company graph. Companies in the same component form one hidden group, whatever brand they trade under.
  • Betweenness centrality finds directors who bridge otherwise separate companies. In the demo, one person sits on six boards across four brands, two of which failed, and ranks first of all the directors in the market.

Lesson 2: algorithms have opinions about direction

Betweenness in FalkorDB follows edge direction. My edges all pointed Person → Company, so no shortest path ever passed through a person, and every score came back zero. Adding a reverse HAS_DIRECTOR edge for each directorship fixed it. I've documented this, with a minimal reproduction, in the repo.

The agent doesn't write Cypher

It's tempting to hand the model the schema and let it write queries. I didn't, following the organizers' own guide to building graph agents. The agents get a fixed catalogue of eight human-written, parameterised tools served over MCP. The model picks a tool and fills in arguments, and every query runs read-only.

Every result also says what kind of answer it is:

Status Meaning
ANSWERED_WITH_PATH the graph held the relationships; here are the paths
ANSWERED_FROM_MEMORY an earlier finding, with its date
NOT_IN_GRAPH no known connection, which is not the same as safe
UNKNOWN_ENTITY the name doesn't resolve, so no query runs

🧱 Agents propose, code disposes. Before a finding is saved, a deterministic verifier recomputes the score from the graph, checks the verdict against fixed rules, and rejects any evidence that names an entity the tools never returned. The model's own number is always replaced by the computed one.

Memory, and two agents that coordinate through the graph

A due-diligence assistant that forgets your budget every time isn't much use. FalkorDB can hold many graphs in one instance, so each buyer gets their own memory graph and nothing leaks between buyers. It holds:

  • preferences, with history (changing your budget creates a new node that SUPERSEDES the old one);
  • standing rules, like "always check the land title";
  • sessions, a shortlist, and case files.

There are two agents. The Concierge talks to you and owns memory. The Investigator owns the market graph. They hand off through the graph itself: the Concierge opens a Case node, the Investigator writes Finding nodes into it, and the Concierge reads it back.

The moment I like most: shortlist two clean projects, then add some market events to the graph (a contractor's insolvency petition, a new title suit). When you come back, Khoj greets you with "Whispering Woods went from 0 to 52" and the exact new path, before you've asked anything.

Proving the graph does the work

"Remove the database and it breaks" is easy to claim, so I measured it. A benchmark injects 200 new projects into a copy of the graph, every one with a clean surface. Half hide one of four risk patterns; half are clean controls with traps, like sharing a locality and a bank with a stalled project.

The surprising row is the ablation without my two structural rules: it finds nearly everything but falsely flags 93% of clean projects. The graph matters, but so does how you walk it. I also recomputed every score with a plain-Python path enumeration: 0 mismatches with FalkorDB.

Lesson 3: let the benchmark embarrass you

A title suit naming only the plot's previous owner scored just under the threshold, because the model decayed it like any other hop. In property law that's wrong: you inherit your vendor's title. So title-chain hops now carry no decay. I made that change after seeing the benchmark, and the report says so.

What I'd do next

The schema maps directly onto real public sources: K-RERA, MCA company filings, eCourts and Bhoomi land records. With real outcomes, the hand-set severities could be fitted instead of chosen.

The graph isn't just storage for an agent. It's what makes the agent's answers checkable. When Khoj says "avoid", it shows you the path, and you can verify it yourself.


*Khoj is open source: https://github.com/ibrxhimj/khoj. Built with FalkorDB, MCP, Streamlit.

Top comments (0)