{"slug": "the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph", "title": "The risk isn't on the brochure: building a due-diligence agent on a graph", "summary": "A developer built Khoj, an AI agent that scores risk in Bangalore property launches by traversing a FalkorDB knowledge graph of directors, companies, land parcels, contractors and court cases rather than reading each project's own records. Using Katz centrality restricted to red-flag targets, with path scores decayed by hop distance and edge weights, the graph surfaced 81% of hidden risk across 200 held-out projects with zero false alarms, versus 0% for record-only checks. Two structural rules cut the median project score from 94 to 28 while keeping planted cases like Skyvista Altura at 84.6 out of 100.", "body_md": "**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**.\n\nCode: [https://github.com/ibrxhimj/khoj](https://github.com/ibrxhimj/khoj)\n\nIf 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.\n\nDevelopers 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.\n\nNone of that is hidden. It sits in RERA filings, company records, court records and land records. It's just never *connected* for the buyer.\n\n💡 **The answer to \"is this safe?\" doesn't live in any single record. It lives in the relationships between them.**\n\nThat's a graph problem. So I built **Khoj** (Hindi for \"search\").\n\nThe knowledge graph models what a careful property lawyer would look at:\n\n`CONTROL` companies; people are `DIRECTOR_OF` companies.`DEVELOPS` a project, which is `BUILT_ON` a land parcel that was `PREVIOUSLY_OWNED_BY` someone.`BUILDS` projects, and approvals hang off each project.`AGAINST` projects; court cases `INVOLVE` land, companies, people or contractors.\nAnything 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.\n\nThe 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.\n\nI wanted a risk score that was explainable by construction, so that every point of it had a path behind it.\n\nFor each project, one Cypher query finds every path of up to four hops to a red flag:\n\n```\nMATCH path = (p:Project {id: $id})-[:DEVELOPS|DIRECTOR_OF|CONTROLS|BUILT_ON|\n             PREVIOUSLY_OWNED_BY|INVOLVES|AGAINST|HAS_APPROVAL|BUILDS*1..4]-(f:RedFlag)\nWHERE f <> p\n  AND none(n IN nodes(path)[1..-1] WHERE n.kind = 'Project')\n  AND size([r IN relationships(path) WHERE type(r) = 'PREVIOUSLY_OWNED_BY']) <= 1\nRETURN f.severity AS severity, [r IN relationships(path) | type(r)] AS rels, nodes(path) AS nodes\n```\n\nEach path contributes its flag's severity, weakened by distance and by how strong each link is:\n\n```\nK(p)  = Σ over paths P:  severity(f) · 0.6^(hops−1) · Π edge weights\nscore = 100 · (1 − e^(−K))\n```\n\nIf 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**.\n\nMy 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.\n\nTwo structural rules fixed it:\n\nThe median dropped to 28, clean projects scored 0, and the planted stories still stood out.\n\nTwo 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:\n\nBetweenness 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.\n\nIt'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.\n\nEvery result also says what kind of answer it is:\n\n| Status | Meaning | \n|---|---|\n| `ANSWERED_WITH_PATH` | the graph held the relationships; here are the paths | \n| `ANSWERED_FROM_MEMORY` | an earlier finding, with its date | \n| `NOT_IN_GRAPH` | no known connection, which is **not** the same as safe | \n| `UNKNOWN_ENTITY` | the name doesn't resolve, so no query runs | \n\n🧱 **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.\n\nA 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:\n\n`SUPERSEDES` the old one);\nThere 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.\n\nThe 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.\n\n\"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.\n\nThe 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.\n\nA 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.\n\nThe 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.\n\n**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.\n\n*Khoj is open source: [https://github.com/ibrxhimj/khoj](https://github.com/ibrxhimj/khoj). Built with FalkorDB, MCP, Streamlit.", "url": "https://wpnews.pro/news/the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph", "canonical_source": "https://dev.to/ibrxhimj/the-risk-isnt-on-the-brochure-building-a-due-diligence-agent-on-a-graph-3lon", "published_at": "2026-10-18 17:14:10+00:00", "updated_at": "2026-10-18 17:17:51.560427+00:00", "lang": "en", "topics": ["ai-agents", "artificial-intelligence", "ai-tools"], "entities": ["Khoj", "FalkorDB", "Bangalore", "Skyvista Altura", "Northstar Habitats"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph", "markdown": "https://wpnews.pro/news/the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph.md", "text": "https://wpnews.pro/news/the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph.txt", "jsonld": "https://wpnews.pro/news/the-risk-isn-t-on-the-brochure-building-a-due-diligence-agent-on-a-graph.jsonld"}}