The parts we refused to build, and why
No AI, no graph between users, no advertising technology, no media custody, no profiling or scores, no contact with Instagram itself — seven refusals, each verified against the repository, with the line of code or doc that enforces it.
Product pages list what a tool does. The more useful document, for anything that
handles a private archive of a life, is the list of what it declined to do —
because features can be added in a hurry, while refusals are architecture: they shape
the dependency list, the route table and the schema, and they leave visible scars in
the code where a feature would have been. This is that list for this product, seven
entries long, each one checked against the repository the day this was written rather
than against memory.
Every refusal here has the same shape: someone could reasonably want the feature, the
feature was not built, and a specific line of code, document section or absence is
what enforces that. Absences are checkable too — you do not have to believe a
negative; you can grep one.1
A finished refusal list needs the same citations as a feature list, so each entry
below states where the boundary is enforced — a file, a document section, a
dependency manifest — and a closing section reduces the whole thing to the searches
that re-prove it. Read it as a manifest, not a manifesto.
Seven things this product does not contain, each verified in-repo: no AI or
language model anywhere (the ask-anything engine is BM25 retrieval over your files);
no graph between users (the one control that would need one is left unbuilt with
the reason shown in the UI); no advertising technology — the sole outbound
analytics component is a page-view beacon passed through a URL redactor that rewrites
account paths to /[handle]; no media custody (photos never leave your device in
local mode, and the opt-in sync tier stores text blobs only, media never);
no profiling, prediction or 0–100 scores; no contact with Instagram itself
(the only network calls in the product are claim-layer email); and no account
automation — the sole input is an archive you obtained yourself.
- 2minthe seven refusals and where each is enforced.
- 5minthe two interesting ones: the tracker we do have (redacted), and the
- control we would not fake.
- ongoing — how to re-verify any entry yourself in an afternoon.
1. No model, anywhere
The feature that would have demanded one — asking your archive questions in plain
language — was built as retrieval instead: a BM25 index assembled in a browser
worker, deterministic context, answers that are pointers into your own text.2
No AI SDK appears in any package.json in the workspace; no completion endpoint
exists in the route table; the nav tip in the product itself says "no model,
nothing uploaded."3 Generation would have imported a foreign reasoning
process into a product whose entire value is that its arithmetic you can finish
reading — so it was not imported.
What retrieval buys in exchange is inspectability: the ranking is a scoring formula
over term frequencies, so a query's result changes exactly the way the arithmetic
says it should, and every answer carries the passage it came from. A wrong reply is a
wrong selection — corrected by reading the source paragraph it mispointed at —
rather than a wrong generation you must take on faith. For a product whose contract
is "nothing you did not record", only the first failure mode can honour the
contract.
2. No graph between users
There is no feature anywhere that connects two accounts on this platform: no
followers-only visibility, no mutual-discovery, no "who else you both follow"
surfaces — because all of them need a social graph the product does not have, and
would have to obtain by routes this product does not take. The clearest scar is in
the curation view: the followers-only page control was deliberately left unbuilt,
and the interface says so — a non-working control would be worse than a stated
gap.4 The public profile that does exist is the owner's own derived snapshot,
published by them, about them; nothing else in the product can display another
person's data.
3. No advertising technology — including the honest accounting of one beacon
The dependency list for the server side is four packages (argon2, drizzle-orm,postgres, zod) and there are no ad SDKs, marketing scripts or behavioural
tracking libraries in the codebase.5 The one component that could get lost in
a casual reading is Vercel Web Analytics — first-party to the deployment platform,
page views only — and it is worth describing precisely instead of waving away: it is
mounted as a single component and every event passes through a redactor before
transmission, which strips query strings and rewrites account-specific path
segments to the literal placeholder /[handle], so a real Instagram handle, a
snapshot token or any query token never reaches the beacon.6 No event
tracking, no cross-site identifier, no advertising use. That is the honest boundary
line: one redacted page-view counter for a deployed website, and nothing else — the
security doc's dependency section was amended in the same pass that verified this,
because a refusal list that rounds a presence into an absence is not a refusal list.
4. No media custody
In local mode — the default — your photos and videos are read in the browser and
never leave the device; there is no server component in the processing path to
receive them.7 The opt-in sync tier, the one feature that stores anything
server-side, keeps a single lean encrypted text blob and states its own refusal
plainly: media is never stored.8 What the product keeps is what text analysis
needs — your own archive's words, your own derived aggregates — and what it
categorically does not keep is the one thing that would turn any storage bucket into
a liability: other people's images and videos, sitting in a third-party store,
waiting for the security question that media always eventually asks.
5. No profiling, no prediction, no scores
The product analyses your archive; it does not build models of you, and it does
not rank you. Concretely: no composite 0–100 scores (the project's decision ledger
bans them outright, which is why counts, medians and deltas are everywhere and score
tiles are nowhere), no forecast rows (Compare states in its own copy that it shows a
snapshot, not a prediction of where your numbers will be), and no inference about
what anyone felt or intended.9 The dossier-section outputs you can read in
your export — interests, advertisers — are the platform's inferences about you,
delivered as data; this product reports them and does not add a second layer of
inference on top.
A score would have to assume what the numbers mean, and meaning changes by person:
a 200-post year is a collapse for one account and a debut for another. Numbers with
opinions attached are opinions wearing a widget.
6. No contact with Instagram
Nothing in the product talks to the platform. The analysis engine makes zero network
calls of any kind — no fetch, no sockets — and the only outbound requests in the
entire application source are claim-layer transactional email.10 The input is
an archive you downloaded yourself, through the platform's own export flow (the
request post's subject). No login automation, no session replay, no scraping of other
accounts, no background sync with Instagram's servers: the tool's relationship with
the platform begins and ends with a ZIP you handed it.
The request itself happens on the platform's own website — you ask for your data
there, and the resulting archive is the only artifact this product ever receives.
Nothing here replays a session, holds a password for later automation, or polls for
a newer export in the background: when the app is closed, nothing about it continues
to run anywhere.
7. No account automation
Closely related but distinct: there is no code that acts on your account — no
auto-poster, no auto-DM, no auto-follower management, nothing that would need your
password for anything other than the optional claim layer's own sign-in. The
practical consequence is worth stating as a user: this tool cannot get you action
taken, because it contains no action-taking paths. It reads and computes. The
decision ledger's whole shape follows from this — the product's job ends at
understanding, and doing is yours.
Declining automation also moves the risk somewhere else: any tool that acts on an
account invites credential handling, rate-limit gambling and platform enforcement
risk — costs that land on the account holder while the benefit accrues to the tool.
With none of that code present, the product's blast radius is zero: there is no
mode, misconfiguration or bug in this codebase that can post, message, follow or
unfollow anything on your behalf.
How to re-verify any entry
Every entry above reduces to a search you can run in an afternoon: search the
workspace for outbound network calls and see claim-layer email and nothing else;
read each package.json for an AI or advertising SDK; list the route table and
confirm there is no completion endpoint; open the curation view and read the gap
comment in context; trace one example path through the URL redactor by hand. The two
absences — no model, no automation — are provable in minutes; the two presences —
one beacon, one email sender — live in files smaller than this section. That
asymmetry is the point: what this product does do is small enough to audit
completely.
If there is no AI, how does ask-anything answer questions?
Retrieval: a BM25 index over your archive's text, built locally in a worker, with
deterministic context assembly. It finds and ranks passages from your own files — it
does not generate text, and it never sends your content anywhere to do it. The
counting post walks the full path.
There is an analytics package in the dependencies. Is that a tracker?
It is Vercel Web Analytics — the deployment platform's own page-view beacon, the
only such component in the app. Every event passes through a redactor that strips
query strings and rewrites account paths to /[handle] before transmission: no
handles, no tokens, no events, no advertising use. You can read the redactor inlib/analytics.ts; it is forty lines long.
Why not build followers-only visibility for my public page?
Because it requires a social graph of who follows whom — data the product does not
have and does not collect. The control is therefore left unimplemented, and the
settings UI states the reason rather than showing a switch that would do nothing. A
declared gap is the honest alternative to a decorative control.
Could a future version start storing my media?
The opt-in sync tier's design and security documentation state media is never
stored, and the local path has nowhere to store it. This is a source-available
product: the claim is re-checkable in the code at any version you run, which is the
only durable form such a promise has — not a policy page, a route table you can
read.
Does the product ever contact Instagram on my behalf?
No. There are zero calls to the platform anywhere in the code — the analysis engine
makes no network requests at all, and the only outbound requests in the application
are claim-layer emails. Your export is the entire input; the request post explains
how to obtain it yourself.
Is the list allowed to be incomplete?
It claims seven absences with a reproducible check attached to each, and a manifest
is falsifiable by design: find an eighth thing it should have admitted, or a seventh
it does not actually have, and the document is wrong in a way you can demonstrate
with a file path. The standard is not the tone — it is whether you can show the
line.
Questions this comes up
The counting post for how answers are computed once inputs arrive; the off-by-default
post for the only layer that touches a server; the economics post for why this
refusal list is financially sustainable; the security post for custody in full.
1: A refusal's verification form: dependency manifests (no AI SDK), the route
table (no completion endpoint), a network-call search over packages/shared/src
(zero fetch/socket usage) and apps/web-next/src (only claim-layer email), and
the quoted in-code gap comments — all reproducible with ordinary text search.
2: apps/web-next/src/lib/amaClient.ts (worker-built BM25 index) andpackages/shared/src/ama/context.ts (deterministic, memoised context assembly).
3: apps/web-next/src/components/Sidebar.tsx — the ask-anything nav tip:
"no model, nothing uploaded."
4: apps/web-next/src/views/Curator.tsx — the followers-only control exists as
a stated gap: it "would rather leave the gap than show a control that does not work",
because it needs a social graph the product does not have.
5: SECURITY.md dependency section (amended 2026-10-07) — claim layer runs on@node-rs/argon2, drizzle-orm, postgres, zod; no advertising or marketing
scripts; the sole analytics component is documented with its redaction path.
6: apps/web-next/src/components/RedactedAnalytics.tsx mounts the beacon
with beforeSend={redactAnalyticsEvent}; apps/web-next/src/lib/analytics.ts
implements sanitizeAnalyticsPath (query/hash stripped; accounts, heatmap,insights, content, relationships and unknown first segments collapsed to safe
roots or /[handle]).
7: SECURITY.md local-mode section — everything executes in the browser on
the device; no server component in the local path; media never leaves the device.
8: SECURITY.md sync-tier section — a single lean encrypted blob, media
(photos/videos/audio) never stored in the sync tier.
9: The project decision ledger (D5) bans composite scores; observable
consequence across Insights/Compare/Features — counts, medians and deltas only; the
Compare view's own copy disclaims prediction.
10: Network-call search: packages/shared/src contains no fetch,XMLHttpRequest or socket usage; apps/web-next/src outbound calls are the
claim-layer mail sender (lib/claim/mail.ts → api.resend.com).
Footnotes
- grep
- ama
- sidebar
- gap
- deps
- redact
- local
- media
- scores
- fetch