Skip to content

DocsReading Dawnscan

Narratives

How a project gets its discovery labels, and why a label is never a rank.

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 manual and the rules never touch it.
  • A moderator can insert or update a project_narratives row with assigned_by = 'admin' (and is_primary as 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.ts with a fixture in assign.test.ts; the next hourly run re-evaluates projects whose evidence moved, and pnpm data:assign-narratives --include-hidden after bumping updated_at (or a full backfill) re-evaluates everything. Bump NARRATIVE_RULES_VERSION when the semantics change so reason.version says 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 listProjectCards read the primary narrative (unchanged).
  • /narrative/{slug} lists primary narratives; narrativeOverview now counts primaries too, so its header agrees with the list beneath it.
  • /narratives counts visible projects with the narrative as primary (it counted every link on every project, hidden ones included).
  • /map picks the primary first and falls back to a secondary; activeNarratives on the pulse and weeklyAwards read 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.