Olivier Couthaud

WorldGraph

An investigator that maps what is publicly known about a company, a website or a public figure, with the source of every fact attached.

Drawing of a graph: three large hubs, each ringed by dozens of small dots on thin lines, joined to each other by chains of black lines. Most lines are pale grey, some black, a few red.
A drawing, not a real case: a few busy hubs joined by thinner chains, which is roughly what the graph of a company looks like. Black lines stand for facts several places agree on, grey for one source, red for disputed.

Say you want to know who is behind a website, or how a company is put together. You can search for it, and an hour later you have forty tabs open and a vague feeling. I wanted a tool that does that hour of work and then shows its work, so I built one. It is a project I did on my own, start to finish.

You give WorldGraph a starting point: a company, a website, a public figure. An AI agent takes it from there. It searches, reads pages, and looks things up in public records such as company registries, court filings and domain registrations. As it goes it draws a map: people, companies, places and documents, joined by lines that say how they are related. Think of a detective's corkboard with string between the photos.

What I care about is the string. Every line on my board has a card pinned to it with the sentence the AI read it from and the page that sentence was on. The card is not checked word for word, so now and then the AI's quote is a paraphrase, but every line has one. Each line is also rated: how trustworthy is whoever published it, and do other, unrelated places say the same thing? One page saying so is a single source. Two independent ones is much stronger. You can try that below.

One fact, with its receipt

Fact
Acme Ltd was founded in 1998
Quote
"Acme Ltd has been making garden tools since 1998."
Sources
Acme Ltd's own About page
Rating
Single source

Acme Ltd is invented. A company's own page is good evidence about itself, but it is still one voice. If a trade magazine with no link to Acme said the same, the rating would become Established: two sources that do not depend on each other.

The other thing I care about is not mixing things up. Two companies can share a name. WorldGraph keeps them apart until something real joins them, like a registration number. A small model I trained suggests pairs that might be the same, a second AI checks, and nothing is joined just because two records look alike. Every join and every split is written down, and any of them can be undone.

You can talk to it while it works, and ask questions of the finished map. The answers are short and say where they come from. It never logs into anything and does not touch anyone's systems. The few optional connectors are off unless I switch them on.

The early versions made mistakes I had to fix. At one point it merged two unrelated websites that happened to share a name into one thing. For a while a run died whenever my phone left the page. A full run on a small public website took about eleven minutes and cost under two dollars. What limits me is the free usage quotas on the search and records services. It is only mine so far, and it still has rough edges.

What's next. I'm building a shared map that every investigation adds to, with watches that re-check things on a schedule and tell me what changed. That is in progress and the rest of this does not depend on it. I also once had face search on my list, a way to match a photo to a person. I never built it. It would have been a big step up in what the tool can do, and I didn't think that was worth the complexity or the ethics. The code isn't public.

WorldGraph is TypeScript on Postgres. You give it a seed, which can be an organization, a domain, a URL or a public figure, and one agent investigates it and builds a graph where every link points back to the page and the quote it was read from. I started it at the end of August 2026 and built it on my own, because I wanted the hour of tab-juggling done for me with the receipts kept. It has one user, me, and it works end to end with rough edges.

What I wanted

Most tools in this space hand you a pile of links. Finding pages is the easy part. The hard parts are knowing how far to believe each fact, and not welding two different things together because they share a name. Four rules hold the rest up. Every fact keeps the quote it came from. Facts are graded by who published them and by how many independent places agree. Identity never merges on a name alone. And every edit can be undone. It never logs into anything and does not touch anyone's systems. It reads public sources, and the few optional connectors are off unless I turn them on.

How it works

Postgres holds everything, and a local blob store archives the raw pages. A run goes through five stages. Click or tap a box to see what it does.

planagent + tools acquirepages, search, APIs extractpages to claims knowledgeclaims to graph appscli + web app the agent reads the graph back and picks its next step Postgres: sources, claims, decisions, graph. Raw pages in a local blob store.
Diagram. The five stages of a run.

The five stages

  • plan. The investigator is one agent, Claude Sonnet 5 on low effort by default, with about 35 tools and a notebook. It decides what to search, read and look up next, and when it is done. You can talk to it while it works.
  • acquire. Fetching pages (Firecrawl first, then plain HTTP with Readability, then the Wayback Machine), searching several engines, and about 30 connectors for public records and infrastructure.
  • extract. A model, Gemini 3.7 Flash by default, reads a page and writes claims. Each claim is typed against a registry of entity and relationship types (with room for new ones) and carries the quote the model says it came from. A separate vision step, also Gemini 3.7 Flash, reads photos for text and place, never identity.
  • knowledge. Claims become a graph by a deterministic rebuild: resolve who is who, grade every link, and keep the whole thing regenerable from claims plus the decisions on top of them.
  • apps. A command line, wg, and a phone-friendly web app: a chat with the investigator, and a force-directed graph where tapping a node shows its evidence.

The investigator searches (Google through Serper, with other engines as fallbacks, paging and operators), reads pages, looks at photos, and calls roughly 30 connectors: DNS, RDAP, certificate transparency, passive DNS, the Wayback Machine, GitHub, FEC filings, SEC EDGAR, GLEIF, Wikidata, court records and company registries. It keeps a notebook and a hypothesis board, and it decides for itself when it is done.

What is RDAP?

The structured successor to WHOIS: the public record of who registered a domain, with which registrar, when, and on which name servers. Much of a website's identity is thin, and registration data is one of the few hard facts about it.

Pages become claims. A claim says one thing, such as who runs what, and stores the quote it was read from. Quotes are no longer checked against the page. They are there so I can walk a fact back to its source, and a model sometimes paraphrases, which the verbatim column below shows. Nothing in the graph is edited by hand. Entities, links and grades are computed from the claims plus the decisions recorded on top of them, so I can throw the graph away, rebuild it, and get the same graph back.

Why rebuild instead of edit?

If a graph is only ever edited, you can't tell why a fact is in it. If it is always computed from claims and decisions, a bad source can be retracted and everything that rested on it re-derives. Asking "why is this here" is a walk back to the quote and the page.

There is a chat. You talk to the investigator while it works and it replies as it goes. The conversation is the report. You can also ask the finished graph a question and get a short answer with its evidence attached.

How a fact gets graded

Every link gets two things I never fold into one. The first is reliability, a ladder over who published it. The second is credibility, which counts independent origins. Independence is about provenance, not wording: one page is one origin, one publisher is one origin, and a fact that says "according to X" counts as coming from X wherever I read it. A publisher the graph has never seen starts low and climbs as it learns who they are, and it only has to learn that once.

Reliability: who published it A Authorityissues the record B Selfsubject on itself C Independentreporter, witness D Platformhosts others' posts E Unknownanonymous or new more trustedless trusted Credibility: how many independent origins Same pagecounts once Same publishercounts once "According to X"counts as X
Diagram. The reliability ladder is the definition of the A to E scale, not a judgment about any particular site.
What is the Admiralty scale?

A grading system from intelligence work: one letter for how reliable the source is, one number for how well the information is confirmed by others. A1 is a completely reliable source and a fact confirmed elsewhere. Two axes are used because a good source can report something nobody else has seen, and a bad one can repeat something true.

The label (established, corroborated, single source, unverified, disputed) is only a summary of the letter and the number, and the letter and number are always shown next to it. This is the rule WorldGraph uses to grade facts, with two extras: a fact whose quote could only be matched loosely never rises above unverified, and a source it cannot place at all is marked unjudgeable. Try it.

Grade a fact

Reliability letter, credibility number, then the label. Credibility 1 is confirmed by independent origins, 2 a single good origin, 3 a single weak one, 4 contested.

The same rule as a table, for a fact nobody contradicts. Best publisher among the sources on the left.
Best publisherOne originTwo or more origins
A, issues the recordA2 corroboratedA1 established
B, the subject itselfB2 single sourceB1 established
C, independentC3 single sourceC1 corroborated
D, platformD3 unverifiedD1 corroborated
E, unknownE3 unverifiedE1 corroborated

Who is who

A name is never identity. People and organizations stay separate records until an identifier, a judgment with evidence, or I join them. Organizations resolve per registrable domain, so two sites that happen to share a brand name don't fuse, while investors.example.com and example.com do. Name matching misses variants and nicknames, so a small model proposes the pairs it would miss. An LLM judge (Haiku 4.5) then reads both records and confirms or declines, and every merge and split is a recorded decision that can be reversed.

What is an embedding?

A list of numbers that places a piece of text near other text with a similar meaning. Records about the same entity, written differently, should land close together. I fine-tuned a sentence-embedding model on the system's own casework, where the judge's earlier verdicts act as free labels, and exported it to ONNX so it runs inside the TypeScript process with no Python at runtime.

Mentionsnames and hints Name blockingids, domains, names Learned proposerproposes only LLM judgereads both records Decision logreversible Owner (me) A merge needs a shared identifier, a judgment with evidence, or me. Never a similarity score alone.
Diagram. Name blocking and the learned proposer each nominate pairs. Only the judge, an identifier or I can merge them.

I ruled out merging automatically on a score. Two sibling investment funds, one the next vintage of the other, scored 0.94 on similarity and are different entities.

Results

The learned resolver. Untrained, a sentence model already separates same-entity records from different ones: ROC-AUC 0.80 over all 519 records from three test cases, and 0.98 on the held-out entities. Fine-tuned, it reached 1.00 on those held-out entities, and the similarity of same-entity pairs went from 0.73 to 0.96. That 1.00 is soft. There are only 111 training and 174 test positive pairs, and the labels are my own judge's decisions, so it measures agreement with my casework, not truth. I don't have a human-reviewed set yet. As a proposer it did its job: on its first run it surfaced 13 candidate pairs, some of which name matching never compared, the judge merged one (a company and its short name) and declined the rest, including the sibling funds. One merge, no bad ones, $0.02.

Reading pages. I ran eight models on the same prompt and schema over the same ten real pages (a biography, a patent, a court opinion, a campaign-finance record, a forum thread and so on), and a second model judged up to 60 items per page against the text. Each model ran once, and the judge was Opus 5, which is also one of the eight, so small gaps mean nothing. "Supported" only says the page states the item, not that it was worth extracting.

Extraction benchmark, 10 pages, one run per model. Verbatim is the share of quotes found word for word on the page. Cost is in dollars and time in seconds per page. The number of items each model extracted is in the chart below. * Only 9 of the 10 pages came back.
ModelSup­portedVer­batimCost, time per page
Gemini 3.7 Flash99.6%91.6%$0.023, 10.2 s
Gemini 3.1 Pro97.5%77.0%$0.190, 104.5 s
Opus 5 (low)97.2%91.9%$0.178, 43.2 s
Sonnet 5 (low)97.2%90.9%$0.069, 33.2 s
Haiku 4.595.4%87.4%$0.038, 31.7 s
Sonnet 5 (medium) *94.4%97.1%$0.075, 37.7 s
Gemini 3.5 Flash-Lite92.0%93.6%$0.012, 8.9 s
Gemini 2.5 Flash *90.8%79.7%$0.049, 68.9 s

Driving an investigation. Three models each investigated the same small public website from scratch, with the same extractor (Gemini 3.7 Flash), tools and a $1.50 cost ceiling. Sonnet 5 on low effort ended with 54 corroborated facts (two or more independent origins, or an authoritative record), from 39 sources. Haiku 4.5 got 19 from 20 sources. Sonnet 5 on medium effort got 31 and stopped on its own: its extra thinking went into deciding it was done sooner, not into collecting more. So Sonnet 5 on low effort drives the loop, and Haiku is kept for narrower jobs such as judging identity pairs. One run each, so the same caveat applies. The earlier full run on that case took 130 tool calls, 11 minutes and $1.82. An early automated check, where a model compared a sample of extracted facts with their quotes, came out at about 85% supported, on an earlier version.

Compare the models

Reading pages: one run per model over 10 pages. * 9 of 10 pages came back.

What didn't work

Two websites became one. Organizations were grouped by name before the resolver ran, so two unrelated sites with the same brand name fused into a single entity. Organizations are now keyed by registrable domain, and a mention with no domain only attaches when exactly one record carries that name.

An account that wasn't there. A username check reported a profile on a site that returns a normal-looking page for any name at all. Every hit now gets a control request with a random username, and catch-all sites are dropped. The control is conservative: when a site refuses it, the hit is dropped too, so the check misses some real accounts on the big social platforms.

Two thousand wrong domains. A reverse-IP lookup on a site behind a CDN returned about 2,000 unrelated domains hosted on the same address. The lookup now checks the network first, refuses to list siblings on a CDN, and returns only a count above 40 tenants.

It died with my phone. The first version of the app tied a run to the browser connection, so the investigator stopped whenever the phone left the page. I found that by using it. Runs are now server-side jobs.

I deleted a lot. After using the first full version I rewrote the design. Out went the check that rejected claims whose quote wasn't found verbatim (it was finicky), a classifier for source types, a table of website tiers, and text-similarity tests for independence. In came the publisher ladder, provenance-based independence, and agents that read whole pages. I'm also retiring the hand-tuned resolution weights in favour of the learned model, though they still score pairs today. One run also spent about a third of its budget tidying duplicates after every batch, so the agent now collects first, tidies once it has enough, then looks at what is missing. A long network outage still kills a run, and I restart it from the app.

What is unfinished

Only I use it, and it is early. It needs a Postgres, an LLM key and a search key, and many connectors need their own, often paid, keys. Free-tier quotas are the real limit, which is why results are cached. Face search was on the roadmap for a while and I never built it. It would have been a big step up from everything else here, and I didn't think that was worth the complexity or the ethics, so nothing in the tool tries to identify anyone from a face. There is an optional connector that links identifiers through breach records. It does not keep passwords, and it is off unless I add its key. I haven't built the analyst layer that would pick out the lead finding, the timeline and the contradictions on its own.

Next is a shared world graph that every investigation feeds into, with standing watches that re-run on a schedule and a short digest of what changed. It is in progress and none of the above relies on it. Precision matters more there: a bad merge in one case hurts one case, and in a shared graph it spreads.

Stack
TypeScript, Node, Postgres. Gemini 3.7 Flash extracts claims, Claude Sonnet 5 drives the investigator and Haiku 4.5 judges identity pairs by default. Search through Serper with fallbacks, a fine-tuned sentence model served through ONNX.
Status
Working
Scope
Public entities, read-only, with a few optional connectors that are off by default. Single user, early, rough edges.
Code
Not public

← All work