2026-09-05. Rules version narratives-v1.
A narrative is a discovery label (PRD V4 §15): one primary, up to four secondaries, stored
in project_narratives. Until this change nothing wrote that table except the submission
form — production had 23 narratives and 0 links, so /narrative/<slug> pages were empty,
cards fell back to the project kind, and the Narrative filter matched nothing. This
document describes the deterministic assignment that fills it, what it trusts, how much it
covers, and how a person corrects it.
Nothing here is a score input. A narrative never touches HBM, Discovery Gap, Still Building or activity status, and no rule reads a price, a holder or a wallet.
1. What the rules read
Only evidence Dawnscan already stores; no provider is called and no model runs.
| Signal | Where it comes from | Rule kind |
|---|---|---|
| Launch origin | projects.launchpad_version (a factory id resolved through the registry, or a launchpad key) or the candidate's launchpad |
launchpad |
| Registry categories | token_candidates.categories — CoinGecko's categories from the coin detail read, kept since this change (new column, written by DISCOVER_LISTINGS); robinhood-stocks from the tokenized-stock category |
registry |
| Repository evidence | builder_candidates.topics / description for repositories matched to the project, plus owner/repo names from project_sources |
github |
| The project's own words | projects.name, the token symbol, short_description, long_description, and the candidate's description (usually the only text a discovered token has) |
keyword |
Code: packages/domain/src/narratives/rules.ts (the tables), assign.ts (pure logic),
service.ts (evidence loading, selection, persistence).
2. Rules and confidence
Signals are applied in order of trust; every candidate carries a confidence and a plain-language reason. A primary is assigned only at ≥ 0.6; everything else is kept in the reason as a candidate. Unknown stays unknown — no narrative is better than a guessed one.
| Rule | Evidence | Confidence |
|---|---|---|
launchpad |
Virtuals → AI Agents; PAIR (pairfund) → StockFi |
0.9 |
registry |
CoinGecko tokenized-stock category (robinhood-stocks) → StockFi |
0.95 |
registry |
CoinGecko categories (AI Agents, Decentralized Exchange (DEX), Lending/Borrowing, Meme, Cat-Themed, …) |
0.8–0.95 |
launchpad |
Memecoin launchers (Robinlaunch, hood.fun, Flap) → Meme, refined to Animal/Culture Meme by a second word | 0.7 |
github |
Repository topics (ai-agents, mcp, uniswap-v4, sdk, launchpad, …); generic topics (web3, defi, solidity) say nothing |
0.75, 0.8 with two topics |
github |
Repository description, by the keyword tables | 0.7, 0.75 with two naming phrases |
keyword |
One phrase that names the narrative outright in a description (decentralized exchange, tokenized stock, ai agent, launchpad, oracle, …) |
0.65, 0.7 with more support |
keyword |
Supporting words only (swap, liquidity, agent, ai) |
0.6 with three, 0.55 with fewer — below the threshold unless three agree |
keyword |
Name or symbol only | 0.5–0.55 — never assigned |
Launchpads with no genre (Pons, Pools, Clanker, Bankr, Hoodit, hood.dev, PonsPad, EasyA,
Robinpad) say nothing. other and robinhood-chain-native are never assigned by rule.
Ties are settled by confidence, then rule trust (launchpad > registry > GitHub > keyword), then by whichever narrative the description names first, then the slug — so the answer is stable and explainable. The same evidence always gives the same answer.
Memes
A meme is what a project says it is, never what it is called. The meme family is assigned
only from launch origin, a registry category, or an explicit self-description
(meme, memecoin, meme token, no utility, …). Names are ignored for the meme rules, so
"Agent Pepe" and "Doge Cat Meme" get nothing from their names. Phrases like "a DEX for
memecoins" or "meme token discovery" are negatives: a product that serves memes is the
product, and on a tie a keyword meme yields to any utility narrative that also qualifies.
Animal Meme needs an animal word beside the meme evidence; Culture Meme a culture word.
Secondaries
Any other candidate at ≥ 0.6 becomes a secondary (up to four), except the parent or child of a narrative already chosen — AI beside AI Agents adds nothing, AI Agents beside Agentic Finance does. Secondaries are visible on the project page; the narrative page, its header counts and the directory list primary narratives only, so they agree with each other.
3. Persistence and provenance
project_narratives gained:
| Column | Meaning |
|---|---|
assigned_by |
manual (submission form), admin (moderator), rule (this assignment). CHECK-constrained. |
confidence |
0–1 (already existed; now written) |
reason |
JSON: version, rule, why, the evidence used (launchpad, categories, topics, which texts), the candidates considered, evaluatedAt |
assigned_at |
When the link was set or last changed. A re-evaluation that agrees leaves it alone. |
projects.narrative_evaluated_at records when the rules last looked at a project. It is what
lets a project the rules could not place stay unplaced without being re-examined every hour.
Migration: packages/db/drizzle/0026_narrative_assignment.sql (also adds
token_candidates.categories).
Manual and admin rows are never overwritten. A project with any such row is not even
selected for evaluation. Rule rows are re-evaluated when the project's evidence moves (the
project row, its candidate record, or a matched repository changed), replaced when the answer
changes, and removed when the evidence no longer supports any narrative. The single-primary
index (project_narratives_one_primary_key) and the (project_id, narrative_id) key are
unchanged; replacement runs in a transaction that demotes before it promotes.
4. Running it
Hourly, the worker enqueues ASSIGN_NARRATIVES (apps/worker/src/jobs/narratives.ts):
a bounded batch (500) of projects that were never examined or whose evidence moved, published
and pending submissions first. Hidden candidates are left alone; a candidate gets its narrative
once it is qualified and published. Database reads only, idempotent, safe to repeat.
The backfill is the same code as a command:
pnpm data:assign-narratives --dry-run --limit=3000 # evaluate and print, write nothing
pnpm data:assign-narratives --limit=3000 # write; repeat until "examined 0"
pnpm data:assign-narratives --verbose # one line per project that changed
pnpm data:assign-narratives --include-hidden --limit=5000 # also the hidden catalogue (optional)
It prints how many were assigned, changed, unchanged, cleared and left unassigned; primaries by rule and by narrative; and the top reasons nothing was assigned.
DATA_QUALITY_AUDIT / pnpm data:quality-report now carry narrative_primary,
narrative_rule, narrative_manual and narrative_evaluated beside the existing
narrative counter, so coverage is a daily trend.
5. Correcting an assignment
- A project's owner picks a narrative on the submission form; that row is
manualand the rules never touch it. - A moderator can insert or update a
project_narrativesrow withassigned_by = 'admin'(andis_primaryas needed); from then on the rules leave that project alone entirely. The admin console does not expose this yet — it is a SQL change with the audit trail the moderation service already keeps for its own actions. - A wrong rule is fixed in
rules.tswith a fixture inassign.test.ts; the next hourly run re-evaluates projects whose evidence moved, andpnpm data:assign-narratives --include-hiddenafter bumpingupdated_at(or a full backfill) re-evaluates everything. BumpNARRATIVE_RULES_VERSIONwhen the semantics change soreason.versionsays which rules produced a row.
6. Coverage, measured on production (read-only, 2026-09-05)
The rules were run locally over the evidence of all 2,525 published projects, read from
production without writing anything (token_candidates.categories did not exist yet, so the
registry rule contributed nothing to this measurement).
| Published projects | 2,525 |
| Primary narrative assigned | 2,428 (96.2 %) |
| with at least one secondary | 1,211 |
| by rule | launchpad 2,158 · GitHub 147 · keyword 123 |
| by discovery source | Virtuals 2,158 / 2,158 · GitHub 148 / 174 · ecosystem 108 / 137 · DEX discovery 13 / 41 · hood.dev 1 / 12 |
Primary narratives: AI Agents 2,206 (the Virtuals catalogue), DEX 78, Launchpad 34, Infrastructure 16, Gaming 14, StockFi 13, Dev Tools 12, AI 8, Lending 7, Perps 7, RWA 7, Payments 6, Analytics 6, Meme 4, Agentic Finance 4, Data 4, Culture Meme 1, Animal Meme 1.
Unassigned (97): 45 have no classification evidence at all (no description, no launch
genre, no repository); 18 mention a DEX in passing (swap, liquidity) without naming
one; the rest are single supporting words below the threshold. They stay unknown.
Caveat worth stating: 85 % of the published catalogue is Virtuals launches, and the launch
origin files them under AI Agents at 0.9 as the brief asks. Some describe themselves as a
DEX, a payments rail or a casino; those narratives are kept as secondaries (the reason shows
them), and a person can override the primary with an admin row.
7. Read side
- Cards and
listProjectCardsread the primary narrative (unchanged). /narrative/{slug}lists primary narratives;narrativeOverviewnow counts primaries too, so its header agrees with the list beneath it./narrativescounts visible projects with the narrative as primary (it counted every link on every project, hidden ones included)./mappicks the primary first and falls back to a secondary;activeNarrativeson the pulse andweeklyAwardsread the rows as before.
8. What is deliberately not here
No LLM (CLAUDE.md: optional, asynchronous, disabled by default — and the rules cover 96 %
without one). No narrative from a name alone. No robinhood-chain-native by rule (almost
everything would qualify). No admin UI for narratives yet. No re-scoring: a narrative is a
label.