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.

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 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.

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.

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.

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.

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.

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.

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.

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.


Fayad K. Hassan