Seshat

Documentation

Narratives

Trending Narratives

Most discovery is reactive — you wait for a token to show up, then evaluate it. The NarrativeScanner flips that: it clusters recently-scored tokens into the market stories they belong to, so Seshat can surface where attention is concentrating before any single token breaks out. A narrative gaining several launches in a few hours is a signal in itself.

Cadence: every 2 hoursModel: claude-haiku-4-5agents/narrativeScanner.ts

What it analyses

Each run pulls every token scored in the last 24 hours (up to 100), joined to its latest research report for a one-line summary. It needs at least 3 tokens to bother scanning — below that there isn't enough signal to call a narrative.

// getRecentTokensWithContext() — last 24h of scored tokens
SELECT t.address, t.symbol, t.name, t.market_cap_usd, t.volume_24h_usd,
       cs.composite_score, cs.scored_at,
       rr.summary, rr.key_findings          -- latest research report per token
FROM composite_scores cs
JOIN tokens t ON t.address = cs.token_address
LEFT JOIN research_reports rr ON <latest report for that token>
WHERE cs.scored_at > datetime('now', '-24 hours')
ORDER BY cs.scored_at DESC
LIMIT 100;

// Fewer than 3 tokens → skip the scan entirely.

Clustering with Claude

The compact token list (symbol, name, score, mcap, research summary) is handed to Claude Haiku, which groups tokens into trending narratives, scores each narrative's momentum 0–100, and writes a crisp description. Categories are constrained to a fixed set; output is capped at the top 8 narratives, ordered by momentum.

// Each narrative Claude returns:
{
  "label":          "AI Infrastructure",        // free-text narrative name
  "category":       "AI",                       // one of the fixed categories below
  "token_addresses":["0x...", "0x..."],
  "token_symbols":  ["SYMBOL1", "SYMBOL2"],
  "momentum_score": 78,                         // 0-100: launch count + avg score + mcap activity
  "summary":        "1-2 sentence description of the narrative",
  "key_signals":    ["6 launches in 24h", "avg score 71", "combined $240k mcap"]
}

// Fixed categories: AI | DeFi | Gaming | Meme | Infrastructure | Social | RWA | Other
// Momentum is clamped to 0-100; max 8 narratives, momentum-descending.

Deterministic fallback

With no Anthropic API key (or if the Claude call fails), the scanner still works via keyword matching — regex patterns bucket tokens by name/symbol/summary into the same category set, and a simple formula derives momentum from launch count and average score. No clustering call required.

// fallbackNarratives() — keyword buckets, no AI
const patterns = [
  { label: 'AI & Agents',     category: 'AI',   keywords: /\b(ai|agent|gpt|llm|neural|...)\b/i },
  { label: 'DeFi Primitives', category: 'DeFi', keywords: /\b(defi|swap|yield|liquidity|amm|...)\b/i },
  { label: 'Meme Culture',    category: 'Meme', keywords: /\b(meme|doge|pepe|frog|...)\b/i },
  // ... Gaming, Infrastructure, Social, RWA; unmatched → "Miscellaneous"
];

// momentum ≈ min(100, tokenCount * 10 + avgCompositeScore / 2)

Storage and freshness

Narratives are upserted into narrative_feeds, keyed by a unique narrative label so each scan refreshes the same label in place rather than piling up duplicates. Stale narratives not seen in 48 hours are pruned, so the feed always reflects what's live now.

-- narrative_feeds (migration 0005) — one row per narrative label
CREATE TABLE narrative_feeds (
  id                  TEXT PRIMARY KEY,
  narrative_label     TEXT NOT NULL UNIQUE,   -- UPSERT key
  narrative_category  TEXT NOT NULL,
  token_count         INTEGER NOT NULL DEFAULT 0,
  top_token_addresses TEXT NOT NULL DEFAULT '[]',  -- JSON, up to 5
  top_token_symbols   TEXT NOT NULL DEFAULT '[]',  -- JSON, up to 5
  momentum_score      REAL NOT NULL DEFAULT 0,     -- 0-100
  summary             TEXT,
  key_signals         TEXT NOT NULL DEFAULT '[]',  -- JSON
  scanned_at          TEXT NOT NULL DEFAULT (datetime('now'))
);

-- On each scan: INSERT ... ON CONFLICT(narrative_label) DO UPDATE (refresh in place)
-- Then: DELETE FROM narrative_feeds WHERE scanned_at < datetime('now','-48 hours')

How it surfaces

The result is exposed read-only via GET /api/narratives and rendered on the dashboard. Operators can also trigger a scan on demand withPOST /api/admin/scan-narratives. Like the other AI loops, narratives are contextual intelligence — they inform discovery, they don't feed the composite score.