A local-first tracker for where your screen time actually goes.
It watches the sites you already use, records how long you spent and on what, and sorts that time into bands so the number you look at first is the one worth acting on. Not "6 hours of screen time", which only tells you the laptop was on, but "3h 27m of brain rot, up 4h from last week".
Everything runs on your machine: a browser extension that measures, a local
server that stores, and a dashboard at 127.0.0.1:4321 that reads.
Seven platforms across ten hosts, plus any site you add yourself:
| Platform | How it is recorded |
|---|---|
| YouTube | long-form, Shorts, and the feed as one sitting |
| YouTube Music | its own platform, because background audio is not watching |
| Netflix, JioHotstar, Prime Video | per title, with completion percentage |
| a text feed with no video player at all | |
| per creator, because Reels are not uniformly junk | |
| Your own sites | one at a time, with Chrome asking permission for each |
Six bands (plus unclassified, which is a question rather than a verdict),
and the split between two of them is the whole design:
brain_rot— what you want to drive toward zeroentertainment— a film is not a failuretextertainment— text read for pleasure, LinkedIn and its kindnourishing— worth the timebackground— tracked, but never competed for your attentionsleep_debt— derived, never chosen: entertainment that ran past midnight
content_band records what something was; effective_band records what it
cost. A film watched until 2am stays a film and also costs you a night, and
collapsing those two facts would throw away the only information that makes the
number actionable.
Nothing is classified by guesswork. An unrecognised site lands in a queue and asks you once, then a rule covers it forever.
The short version is that this exists to be run by one person on one machine, and it is built that way. The long version has three parts, because "nothing leaves your machine" is close to true and not exactly true:
1. The server is localhost-only. It binds 127.0.0.1 and needs a pairing
token. There is no account, no sync, no telemetry, and no hosted component.
2. Enrichment is optional and makes two outbound calls. If you supply a
YouTube or TMDB API key, the worker sends a title or a video id to look up
genres. It never sends a timestamp, a duration, or anything about when you
watched. Leave both keys unset and everything works with genre = unknown.
3. The extension can be granted access to sites you name. Out of the box it loads on ten hosts and nowhere else. Adding your own site asks Chrome for that one origin, and Chrome prompts you each time. Nothing is read from a site you have not added, removing a site hands the permission back, and the extension tells you if a permission is revoked behind its back.
Two deliberate refusals worth knowing about. Messaging is never tracked, on any platform: a tool that logged time spent talking to people would be a different kind of surveillance than the one being asked for. And user-added sites record no page titles and no URLs beyond the origin, because the project knows nothing about a site you added and a path there could be a document name, a search term or a message thread.
Node 24 is pinned in .nvmrc because better-sqlite3 needs a matching prebuild.
Nothing else is unusual.
nvm use # 24.20.0
corepack enable pnpm
pnpm installCreate a .env and generate a token:
cp .env.example .env
# Replaces the placeholder in place. Appending would leave two MINDFEED_TOKEN
# lines and it is not defined which one wins.
sed -i '' "s|^MINDFEED_TOKEN=.*|MINDFEED_TOKEN=$(openssl rand -hex 32)|" .envOn Linux that is sed -i without the ''.
Build the dashboard and start the server:
pnpm dash:build
pnpm start # http://127.0.0.1:4321Build the extension and load it:
pnpm ext:buildThen chrome://extensions → enable Developer mode → Load unpacked →
packages/extension/dist/chrome-mv3.
Finally, pair them. Click MindFeed in the extensions menu (the puzzle-piece icon
next to the address bar) to open its settings, and paste the token from your
.env. pnpm token prints it. Until the token is set the extension buffers
everything and sends nothing, which is deliberate: a missing token must never
silently discard data.
Open 127.0.0.1:4321, paste the same token, and reload any tab you already had
open on a tracked site. A reload is needed because loading the extension cannot
reach into tabs that were open before it existed.
Open the extension's settings, type a hostname, and accept Chrome's prompt. The site is timed the same way as the built-in ones, appears in every chart, and shows up once in the label queue so you can say what kind of time it is.
Sites already covered by a built-in adapter are refused, because a second content script in the same tab would count every minute twice.
Phone viewing is not captured. It was investigated and dropped. Phone time is self-reported through a form on the dashboard, and every total says so rather than quietly presenting a laptop-only figure as a complete one.
Time is measured, not estimated. Browser seconds and hand-entered phone minutes are never added into one number, because presenting a guess and a measurement as the same figure is the one thing this is built not to do. A session closed by timeout stays marked as such.
Limits, if you want them. Per band or per platform, with an optional nudge that can pause playback. Knowing you are at 70 of 120 minutes at four in the afternoon is the point; being told at 120 is too late to act on.
When something looks wrong, run pnpm doctor. It checks the capture path end
to end, including whether the browser is running an older build than the one on
disk, which is the failure most likely to look like a broken adapter.
pnpm test # 769 tests
pnpm typecheck
pnpm doctorpackages/shared schema, diet classification, pure logic, the wire contract
packages/server Hono + SQLite, ingest, queries, enrichment worker
packages/extension MV3 content script, service worker, per-platform adapters
The interesting code is packages/shared/src/diet.ts, which decides what a band
is and is deliberately pure so it can be re-run over all of history whenever a
rule changes, and packages/extension/src/session.ts, a state machine that
owns the clock and is written to survive an MV3 service worker being evicted
mid-session.
Adapter knowledge for Netflix and JioHotstar is credited in ATTRIBUTION.md.
The commit history is unusually long-form on purpose. Most non-obvious decisions here were expensive to arrive at, and the reasoning is in the commit that made them rather than lost.
MIT. See LICENSE.