Why we refuse to draft AI citations into archived threads
Most visibility tools draft a Reddit comment for a thread that's been read-only for six months. We classify before we generate. Here's the framework — not the code.
Generative-engine visibility tools have a tempting failure mode: AI cites a Reddit thread for a query where your brand is missing, the tool sees the gap, the tool feeds the thread to an LLM and asks for a comment. The draft comes out the other end. The user pastes it. The post gets removed in 90 seconds because the thread is half a year old and Reddit auto-archived it long ago.
This is not an edge case. On a representative production dataset (one paying customer, several weeks of citations across ChatGPT, Perplexity, and Google AI Mode) we found that roughly one in five cited URLs is unreachable for any kind of comment, pitch, or PR — archived Reddit, locked Hacker News, defunct domains, login-only Twitter and YouTube, plus a handful of canonical-knowledge sites that simply do not have a comment venue.
Drafting into any of these is a category trust failure: the tool charges for a draft that will not stick.
The right response is not "drafts work most of the time, deal with the rest." The right response is a classifier that runs before any LLM call and refuses to spend a token on a closed venue. This article is the framework behind that classifier — the design choices, the trade-offs, and the bug stories. Not the code; the code is the easy part once you've made the right calls upstream.
We are publishing the framework because the trade-offs are more interesting than the implementation, and because every visibility tool we audited still ships drafts into closed venues. That category default deserves to change.
Methodology & sources
Editorial review for factual claims (as of 2026-05-02).
- All numbers come from a single production dataset. Statistics generalise as patterns, not as benchmarks for any other tool.
- This article describes the conceptual framework. It is intentionally not a build guide. Specific thresholds, hostname lists, and detection regex live in our codebase and are not reproduced here — they are the result of empirical work that does not generalise without that work being repeated.
- "States" are written
Live,Limited,Frozen,Manualto match the dashboard UI badges.
The shape of the problem
- A single binary "actionable / not actionable" misses the most useful states. We need four.
- The states must be derived from already-extracted signals — extra round-trips kill the ROI.
- Replacement strategy for closed venues is a separate rules table, not part of the classifier.
Why it pays off
- Roughly one in five cited URLs lands in a closed-venue state on a typical Pro-tier dataset. Those skip LLM entirely.
- The bigger saving is not money; it is UI trust. A draft you cannot use teaches the user the tool is unreliable.
How this fits into the broader GEO discipline
If you are still calibrating definitions, start with our What Is GEO? primer. The action layer described here is the missing half of every visibility tool we audited — we mapped that gap in 22 AI Visibility Tools, $25 to $699 a Month. For the broader cross-engine picture (how ChatGPT, Perplexity, Google AI Mode, and others surface citations differently), read AI Visibility Is Not One Channel. And the May 6, 2026 Google announcement that put Reddit threads directly into AI Mode is unpacked in Reddit Is Now Inside Google's AI Mode — relevant because half the draftable citations our classifier touches live on Reddit.
The four states (and why three was not enough)
The first version of this idea had three states: Ready, Limited, Frozen. We shipped it and ran it for a week. Two patterns showed up immediately:
- Twitter / X replies and YouTube comments kept landing in
Limitedbecause the venue was technically open. The user clicked the action card, got an "we cannot draft here, but go reply manually" message, and asked the obvious question: why is this not a different state from "old listicle, low-yield"? - Old listicles and old podcast feeds kept landing in
Frozenif we were strict about staleness, or inLimitedif we were not. Neither felt right. A 14-month-old listicle's author can reply to a sequel pitch — that's not a dead end. A YouTube comment we cannot post for the user IS a dead end for our automation, even though the comment box is open.
The third state was conflating two genuinely different problems:
- The venue is degraded (old article, low-traffic thread, news comments closed but author reachable). Action is still possible, sometimes useful, just lower yield.
- The venue requires a logged-in identity we cannot drive (Twitter, YouTube). Action is possible for the user, impossible for us to automate.
We split them. The four-state model:
| State | Meaning | What we generate |
|---|---|---|
Live | Open venue + recent | Full draft on demand |
Limited | Open but degraded | Draft with a yellow warning |
Frozen | No venue ever (locked / archived / defunct / informational ref) | No draft. Replacement strategy card. |
Manual | Open venue, login required | No draft. Hand-off link with hint. |
Splitting Manual out of Limited made the UX honest. A Limited Reddit thread tells the user "reply within 48h, fewer eyes on it". A Manual Twitter source tells them "open it, we cannot draft here, here is what good looks like for tone". Different actions, different copy, different outcome expectations.
This is the framework. The rest of this article is the trade-offs we made fitting it to real source types.
What the classifier actually consumes
The classifier is a single pure function. Its inputs are signals already extracted by the previous step in the pipeline — fetch outcome, parsed metadata, hostname, dates, source type. Its output is the state plus a one-line plain-English reason that the dashboard surfaces in a tooltip:
type ActionabilityStatus = 'ready' | 'limited' | 'frozen' | 'out_of_scope'
interface ClassifyArgs {
// Already-extracted signals; the classifier never fetches.
sourceType: CitationSourceType
hostname: string
fetchOk: boolean
fetchStatus: number | null
publishedAt: string | null
// Per-platform parsed flags, when available.
redditFlags?: { archived: boolean; locked: boolean; removed: boolean }
}
interface ActionabilityResult {
status: ActionabilityStatus
reason: string | null
}
The signature does the marketing for us: there is no fetch inside, no LLM, no remote call. Pure data in, structured decision out. The function is small, exhaustively unit-tested, and a regression is a one-test-plus-one-line change.
What the article does not include — and what we will not be reproducing here — are the specific thresholds, the host blocklists, the regex patterns, the per-platform JSON-flag interactions, and the order in which the rules fire. Those are the part that took time. Engineering teams that want to ship something similar are better served writing them from scratch against their own dataset; copying ours would replicate our bugs and miss the empirical adjustments we have made over months of operation.
The categories of decision the classifier has to make
What we can describe usefully is the shape of the decision space. There are six categories of input the classifier folds into the four output states. Each category is its own small reasoning problem.
Hard HTTP signals
Some HTTP statuses are unambiguous and should short-circuit before any per-platform logic. A page that no longer exists is not a degraded venue; it is a different problem and benefits from a different replacement strategy ("AI engines re-index every few weeks; this citation will fade on its own"). Other 4xx statuses are more ambiguous because the page may load fine in a real browser even when our crawler is blocked — those tilt toward Limited rather than Frozen so the user can verify manually.
Per-platform structured flags
For a small number of platforms, the source itself publishes its actionability. Reddit's public metadata exposes flags for archived, locked, and removed states. Hacker News documents an auto-lock window in its FAQ. GitHub repositories expose an archived boolean via API. When such flags exist, they are authoritative — guessing actionability from age when the platform tells you the thread is closed is engineering malpractice.
The non-obvious lesson here was that platforms publish multiple signals for the same outcome and you should OR them, not pick one. Reddit reports removed content in several distinct fields depending on which actor removed it (mod, auto-mod, original poster, automoderator-from-subreddit-rule). Treating any one as canonical means you miss most removals. Treating any of them as ground truth gives you a clean signal.
Hostname blocklists for canonical-knowledge sites
A small set of hostnames are cited by AI engines because the content is canonical, not because the publisher accepts contributions. Wikipedia, MDN, the IETF datatracker, RFC editors, W3C, and a few similar sites belong here. There is no comment box; there is no editorial pitch path; some have explicit conflict-of-interest policies that turn brand-side editing into a reputational liability. We classify these Frozen and route the replacement strategy to "build a canonical resource on your own domain."
The list is short, hand-curated, and grows slowly. We have rejected proposals to widen it with rule-based heuristics ("any site whose hostname starts with docs."). The cost of one false positive — telling a customer their brand cannot pitch a real publisher — is much higher than the cost of missing a few sites and falling through to the default state.
Age-based degradation
For source types where staleness reduces yield without closing the venue (legacy listicles, news articles past the comment window, podcast feeds without a recent episode), we apply per-platform age thresholds. The thresholds came from empirical observation, not from a single survey, and have been adjusted twice from customer data. The exact numbers are not in this article because they are not the lesson — having different windows per source type is the lesson, and we got that wrong on the first three attempts before realising news, podcasts, and listicles have genuinely different shelf lives.
What works is to publish the rule and the threshold internally so customers who disagree can read it and tell us why. What does not work is treating staleness as a single global cliff.
Out-of-scope venues
Twitter / X and YouTube are open venues that require a logged-in identity our automation does not have. Drafting text we cannot submit teaches the user the outputs are unreliable; we instead surface the URL and a short tone hint, and route the user to do it manually. This is the case that earned the fourth state.
Default-actionable
Anything not matched above defaults to Live. Generic forum threads, GitHub repositories whose archived flag we have not checked, source types we have not yet specialised. The default has to be permissive — the cost of false-frozen (no draft for an open venue) is product-killing, while the cost of false-live (a draft that gets pushed back politely) is recoverable.
Edge cases that bit us
The classifier itself is small. The runway leading up to it is not. Three of the bugs we paid for in production:
A platform reported the same outcome via multiple distinct fields
The first version of our Reddit removal-detection used a single field. We missed roughly a third of removed threads because the platform populates different fields depending on which actor removed the content. The fix was to read four signals and OR them — and to add a regression test for each of the four cases so the next refactor cannot silently break one.
The lesson generalises: when a platform exposes structured metadata, do not pick the prettiest-looking field. Look at every field that could indicate the state you care about and treat any of them as truth.
A cited PDF broke our database insert pipeline
A PDF surfaced as a citation. Our enricher fetched it, decoded the bytes as UTF-8 with permissive error handling, and the resulting plain-text excerpt contained null bytes — which Postgres text columns refuse, with an error message that does not mention the PDF, the null byte, or the field that failed. The insert failed silently against our enrichment retry loop and the source stayed unclassified for two weeks while we hunted the cause across error reports.
The fix is a deterministic sanitiser at the persist boundary: strip control characters that databases refuse, on every text field, before insert. The classifier never produces hostile bytes itself, but it inherits the upstream sanitisation guarantee — and the test that asserts the guarantee.
A bot wall returned 200 OK with valid HTML
One of the platforms we crawl serves a small HTML page from its anti-bot infrastructure when a server-to-server fetch hits a region or fingerprint it does not trust. The page is well-formed HTML, returns HTTP 200, has a real <title> element. Our classifier had no rule for "200 OK with bot-wall body," so the page was treated as a successful fetch, the title extractor ran, and a handful of source rows landed in production with the bot-wall page's title as the article title. Drafts generated for those sources cited a thread the LLM had never read.
The fix is a defence-in-depth detector at the fetch layer that recognises common interstitial patterns and downgrades the fetch result to not ok. The classifier sees a fetch failure and returns the right state. We use the platform's structured API path where available, but the bot-wall detector is a safety net for hosts that newly start serving challenges.
The pattern across all three: the classifier was not the bug. The bug was upstream. The classifier's job is to refuse to operate on inputs it cannot trust, and to express that refusal as a structured state the rest of the pipeline can read.
What we deliberately do not solve yet
Three categories the classifier does not touch in v1, with the engineering reasoning:
Per-API archived flags for repository platforms. GitHub exposes an archived boolean. Calling the API on every cited repo URL on every cron run is not justified by the rate at which repos go archived in our dataset. Roadmap: add when the share crosses a meaningful threshold.
Paywall detection on news articles. A composite of about six heuristics could give us a paywalled-vs-open signal. Today we skip and rely on the human reading the draft and noticing the source is paywalled before reaching out. We accept some false-actionable rate as a trade for not maintaining a fragile detector.
Per-platform forum APIs. Some forum platforms expose a public API; most do not. We could special-case the ones that do and gain a few percent. Until forum threads become a meaningful share of the citation surface, this is not worth the maintenance cost.
The pattern across these: a rule is worth shipping when it pays for at least 1% of cited URLs in production. Specificity is cheap; overhead from a rarely-firing rule is permanent.
Cost impact
The model we use for action drafts is in the cheapest tier — fractions of a cent per draft. Cheap, but cheap multiplied by every cited URL is not free. On a hypothetical thousand-URL run, a fifth of those URLs never reaching the model is a small dollar saving.
The real saving is product-shaped. A fifth of UI surface area is not a generated draft the user has to evaluate, fail to use, and lose trust over. The cheapest action draft is the one you do not generate.
What this opens up
The classifier output is structured enough that the rest of the product reads it without case analysis. The recommendation generator early-exits on closed states before any LLM call. The dashboard renders the four states as four-colored badges directly from the column. The replacement-strategy module — a separate rules table — takes the source row and produces a one-click URL where applicable.
The whole architecture has been stable for months with no rewrites. We expect this to be the part of the product we ship most often without changing any UI — every time a customer reports a miscategorised source, the fix is one new test plus a one-line rule change.
If you are building visibility tooling, we recommend writing your own version against your own dataset. The category of bugs that comes from skipping this layer (drafting into archived threads, generating PDF-derived comments, treating canonical references as comment venues) is large enough to be worth the rules engine even if you never publish the rules.
Closing
The classifier is the boring part. It runs in milliseconds, on signals already extracted by the previous step in the pipeline, and refuses to spend a token on a closed venue. It is also the part of the product we expect to age the best — the architecture has been stable for months and the per-platform rules grow slowly.
If you are evaluating AI visibility tools, the question that separates the category is not "how many engines do you track" — almost everyone tracks the same five. The question is "what does your tool do when AI cites a source you cannot act on?" Most tools do not have an answer. We do.
Sources and official documentation
- Hacker News — FAQ on auto-locked threads: news.ycombinator.com/newsfaq.html
- Wikipedia — Conflict-of-interest editing policy: en.wikipedia.org/wiki/Wikipedia:Conflict_of_interest
- Reddit — Help: archived posts: support.reddithelp.com/hc/articles/360038586312
- PostgreSQL docs — Character data types and null bytes: postgresql.org/docs/current/datatype-character.html
Related articles
Visibility baseline
Establish an AI mention baseline you can defend
GEO Tracker AI runs repeatable checks for supported engines so you can see whether your brand is mentioned, what context shows up, and how that changes week over week — complementary to Search Console, not a replacement for it.