Crowd Swarm
Multi-Persona Swarm
For high-conviction tokens, Seshat runs a small swarm of opinionated Claude personas — a degen, a risk manager, a tech analyst, a momentum hunter, and a contrarian bear — and aggregates their votes into a sentiment read and a single 0–100 crowd score.
Honest scope: this is a captured signal, not a scoring input.
The crowd score is deliberately not part of the composite formula and is not a dimension the self-learning model trains on. It is captured alongside each score as a candidate feature so future learning can correlate it with real outcomes — it cannot be recovered retroactively. Today it is read-only.
Why a swarm
The intended backend is MiroShark, a heavy self-hosted scenario-simulation service that is long-running and async — too slow for a synchronous Worker request, with no REST API to call in-line. So the working implementation is a 5-persona Claude mini-swarm that runs immediately and cheaply, with an async seam left in place for the real service to plug in later via webhook.
The five personas
All five run in parallel as cheap Haiku calls. Each gets the same token brief and returns its own verdict — sentiment, conviction, reasoning, and red flags.
Degenerate Trader
High-risk Base degen. Focuses on narrative strength, community hype, short-term upside.
Risk Manager
Cautious DeFi risk analyst. Hunts rug signals, contract safety issues, anonymous devs, thin liquidity.
Tech Analyst
Evaluates real technical substance — architecture, contracts, whether it solves a real problem.
Momentum Hunter
Trend-follower tracking social velocity, holder growth, volume spikes, narrative alignment.
Contrarian Bear
Fundamental sceptic — assumes every launch is a cash grab until proven otherwise.
// Each persona returns:
{
"sentiment": "bullish" | "bearish" | "neutral",
"conviction": <0-100>,
"reasoning": "<1-2 sentences>",
"red_flags": ["<risk1>", "<risk2>"]
}
// All 5 fire via Promise.allSettled — a failed persona is dropped, not fatal.Aggregation
Votes are tallied into bullish / bearish / neutral percentages, an overall sentiment (majority wins, >50%), and a consensus score measuring how aligned the swarm was. Risks are deduplicated across personas; the majority group's lead reasoning becomes the top thesis.
// analyze() aggregation bullPct = round(bullish / total * 100); // etc. for bear / neutral overall = bullPct > 50 ? 'bullish' : bearPct > 50 ? 'bearish' : 'neutral'; // Consensus = size of the largest group / total → 100 if unanimous, lower if split consensusScore = round(max(bullish, bearish, neutral) / total * 100);
The crowd score (0–100)
The full analysis is distilled into one number via a pure, deterministic function. It blends two ingredients: directional lean (how bullish-vs-bearish) and consensus (how aligned). A confident lean from a divided swarm shouldn't be trusted, so the lean is pulled toward neutral 50 in proportion to how low the consensus is.
// computeCrowdScore() — pure, no I/O const lean = clamp(50 + (bullish_pct - bearish_pct) / 2); // 0..100 const crowd = 50 + (lean - 50) * (consensus_score / 100); return round(clamp(crowd, 0, 100)); // • strong, aligned bullishness → toward 100 // • strong, aligned bearishness → toward 0 // • split / low-consensus swarm → near neutral 50
When it runs
The swarm is gated on conviction. After a research report completes, if the token's latest composite is at least the alert threshold (≥ 65) and no swarm has run in the last 24 hours, the research queue fires the swarm and persists the result. It can also be run on demand for any token via POST /api/admin/swarm/:address.
// queues/researchQueue.ts (after the report is written)
const threshold = parseInt(env.ALERT_SCORE_THRESHOLD ?? '65', 10) || 65;
if (composite >= threshold) {
// dedup: skip if any swarm_results row exists for this token in last 24h
if (!recentSwarm) {
const analysis = await swarm.analyze({ ...tokenContext });
await persistSwarm(env, token_address, analysis, 'claude', 'complete');
}
}The async seam
kickoffExternalRun() is the forward-looking path. When an external service URL is configured it records apending swarm row with an external run id and returns — deliberately not calling a fabricated endpoint. The real service would complete out-of-band and POST back to/api/webhook/swarm, which fills in the result and computes the crowd score. The provider type allowsclaude | miroshark | aeon as sources; only claude is live today.
Storage and exposure
Results are persisted to swarm_results with the sentiment percentages, top thesis, deduplicated risks, per-persona votes (JSON), the crowd score, source, and lifecycle status. The latest complete result is read-only viaGET /api/tokens/:address/swarm. Persistence is fail-soft — a DB error never breaks the pipeline or request path.
-- swarm_results (key columns) INSERT INTO swarm_results ( id, token_address, source, status, -- source: claude|miroshark|aeon; status: pending|complete|failed overall_sentiment, consensus_score, bullish_pct, bearish_pct, neutral_pct, top_thesis, key_risks, persona_votes, -- key_risks + persona_votes as JSON crowd_score, -- 0-100, null until an analysis exists external_run_id, completed_at ) VALUES (...);