Fayad K. HassanContact

An OSINT Tool Should Show Its Working

Storytime 2.1 is a free, self-hosted OSINT platform. It reads public sources only, runs a local agent that has to cite every claim it makes, and seals what it collects so the evidence can be checked later. Here it is on live data, including the places where it still needs a person.

The Storytime landing screen in the dark theme: the headline Trace anyone’s public story across the open web, a search box, and the platforms it reads
Contents 6 sections
  1. Every result carries its receipt
  2. The agent has to cite
  3. Looking at a place
  4. Unknown is an answer
  5. The timeline is still the core
  6. Run it yourself

The first version of Storytime was a sandbox. It put one account’s public activity from a few platforms onto a single timeline, and to do even that it needed a paid Twitter/X API key, a paid Pastebin PRO key and two database servers. I rebuilt it this month. Version 2.1 traces an account across fifteen platforms, runs sixteen intelligence connectors over infrastructure, public records, places and usernames, and ships as a single Docker image. It needs no paid key of any kind, and it is free under the MIT licence.

Most of the rebuild went into one idea. An intelligence tool is only as useful as your ability to check what it tells you, so Storytime has to show its working. Every result says where it came from, and every claim the agent makes points at the evidence behind it. When a source fails, the screen says so. The screenshots below are the real interface on live public data, captured on 26 September.

Every result carries its receipt

Each connector wraps what it returns in the same envelope: the source URL, the time of the fetch, a SHA-256 hash of the raw response, the licence the data is published under, and a tier that says whether it came from a primary source, an aggregator or an archive. The envelope prints as a line of small type under every block of results. It is the dullest line on the page and the one I would least like to lose.

Infrastructure recon on example.com: subdomains from certificate transparency logs and live DNS records, each block closed by its provenance line
Recon on a domain. Subdomains come from public certificate transparency logs and the records from a DNS-over-HTTPS lookup, and each block ends with its tier, source link, fetch time, hash and licence.

A whole sweep can be downloaded as an evidence bundle. The bundle is sealed with a Merkle root built from those hashes, and a small script that ships with the project, node scripts/verify-bundle.mjs, checks it offline with no dependencies. It recomputes the hash of every result set and rebuilds the root. Add a record, remove one or change a single value, and verification fails. Once the root has been shared, any later edit to the evidence shows.

The agent has to cite

Investigate mode puts a local agent on top of the connectors. Given a name, an organisation or a domain, it plans which sources apply, runs them cheapest first, and turns what comes back into findings. The plan is built with function calling rather than a free-running ReAct loop, so it is a fixed list of tool calls. Each call goes into a ledger with its source and a content hash, which means a run can be replayed and checked afterwards.

A critic pass then drops any finding whose citation does not resolve to something that was actually collected, and the run reports the result as a citation-validity score. The surviving findings are weighed against competing explanations using Analysis of Competing Hypotheses, the method analysts use to work against their own confirmation bias. The hypothesis that leads is the one with the fewest points against it, which is not always the one with the most points for it.

The investigator on Tesla Inc: the case file with its self-scored metrics, and three competing hypotheses scored for and against
An investigation of Tesla Inc with no model loaded, so every step is deterministic. Four of seven tools ran, citation validity is 100%, and “A legitimately registered organization” leads because nothing counts against it.

The same run shows why entity resolution has to be strict. GLEIF returned four legal entities with Tesla in the name: Tesla, Inc., Tesla Engineering Inc, Tesla General Insurance, Inc. and Tesla Prop & Cas Inc. Each holds its own Legal Entity Identifier, so Storytime keeps them as four. Records are linked firmly when they share an identifier and loosely when only the name matches, and a conflicting identifier blocks the merge outright. A tool that merged on name alone would fold four companies into one, and every finding after that would inherit the mistake.

The findings from the same run: a legal entity record, scholarly works, court records flagged for analyst review, and news volume
The findings, each with its method, its source and a confidence. The court-record match is not asserted. It carries a review flag and the note “Name match; confirm it is the same party.”

This screenshot also shows the limit of the approach, and I would rather point at it than crop it out. The second finding reports ten scholarly works, the most cited being a statistics paper on autoregressive time series. That paper has nothing to do with the car company. The critic passed it anyway, correctly by its own rules, because the citation does resolve: the paper really is in the OpenAlex response. It then went on to count as support for the third hypothesis. A citation check proves the evidence exists. It cannot prove the evidence is about the right subject. That judgement still belongs to a person, which is why the output is a draft for an analyst and never a verdict.

Looking at a place

Give Recon a pair of coordinates and it switches to geospatial sources. For 48.8584, 2.2945 it listed 28 aircraft nearby from live ADS-B data and 60 named places from OpenStreetMap. Nominatim turned the point into a street address on Avenue Gustave Eiffel, in the 7th arrondissement of Paris, and Sentinel-2 supplied the recent satellite scenes of the spot.

GEOINT on a pair of coordinates: the reverse-geocoded address, then eight Sentinel-2 satellite scenes with the cloud cover of each
Eight Sentinel-2 scenes from 11 to 24 September. Five of them are over 80% cloud, and only two are under a quarter.

The cloud figure is there on purpose. Satellite imagery looks authoritative, and a scene that is 96% cloud tells you almost nothing about the ground. Printing the number beside every thumbnail stops anyone reading an empty frame as evidence of an empty place.

Unknown is an answer

The easy way to check a username is to request the profile address and count any successful response as a hit. Plenty of platforms answer every request with a page, or put a bot wall in front of their profiles, so that method reports accounts that do not exist. Storytime uses three states instead of two. An account is present only on a positive signal, such as a clean response from an address that returns 404 for a missing profile, or a marker in the page that only a real profile carries. It is absent only on a definite negative. Everything else is unknown. A false “present” does more damage than an honest “unknown”, so the check leans that way deliberately.

Username presence for torvalds across eight platforms, and the Tor hidden-service index reporting that it would not return a listing to this network
A username sweep. “Found” means an account with that name exists. It does not mean the same person owns it, and that link needs other evidence.

The same sweep shows the other half of the rule. A failure is reported as a failure. SEC’s full-text search refused the request with a 403, OpenSanctions asked for its free API key, and the Tor index would not return a listing to this network. Each appears on screen in red, with the reason. None of them is shown as “nothing found”, because “I could not look” and “I looked and there was nothing” are different statements, and quietly swapping one for the other is how a tool misleads you. The legal-entity search, which did run and came back empty, says exactly that: nothing found.

Three records searches for torvalds: sanctions screening waiting on an API key, a legal-entity search that ran and found nothing, and an SEC search refused with a 403
Two sources that could not answer, and one that answered with nothing. The screen keeps them apart.

The timeline is still the core

Trace is what Storytime was first built for, and it is still the front door. Give it a handle and it pulls that account’s public activity from up to fifteen platforms onto one timeline, then reads the patterns in it: the hours the account keeps, and the themes it keeps returning to.

The trace for torvalds: 50 GitHub events over ten days, the profile card, and the most active hours, primary platform and leading theme
Fifty public GitHub events for torvalds between 15 and 25 September, read into active hours, a primary platform and a leading theme.

Below the summary, events cluster into dated story arcs, and a small knowledge graph records facts such as “works on torvalds/linux” with the date and source behind each one. You can search the timeline by meaning rather than keyword, or ask it a question; with a local model running, the answer comes back with citations to the events it was drawn from. The whole dossier exports as Markdown, JSON, CSV, XML or HTML. Compare mode puts two accounts side by side on the same measures.

Two GitHub accounts compared side by side: events, active hours, primary platform, overall tone, leading theme and the span of activity
The torvalds and gaearon accounts: 51 events to 100, and working hours that overlap for six of their eight.

Run it yourself

Storytime runs on your own machine. With Docker installed, one command starts it:

docker run -p 3000:3000 \
  ghcr.io/elfayadk/storytime:latest

Then open localhost:3000. Storytime has no account of its own to create and no telemetry, and nothing leaves the machine except the requests to the sources you query. The one source that wants a key, OpenSanctions, gives it away free. A local model through Ollama is optional. When one is present it sharpens the agent’s plan and writes a narrative assessment. Everything above works without it, and the Tesla investigation ran with no model at all. The interface has a dark theme and a light one.

The Storytime landing screen in the light theme
The same landing screen in the light theme.

Collection is passive and public. Storytime never gets past a login, a CAPTCHA or a paywall, never scans a target, and treats each source’s rate limit as a hard limit, including Nominatim’s one request a second. The dark-web connector searches a public index of Tor hidden services and does not fetch onion content, which would need a Tor circuit and its own authorisation. Like any OSINT tool it is dual-use, so the project’s responsible-use notes rule out employment, tenant and credit screening, and profiling, locating or unmasking private individuals against their will.

The code is on GitHub, and it is open to anyone who wants to push it further.