Seshat

Documentation

Outcome Research

Outcome Research

Scoring a token is only half the loop. The OutcomeResearcher closes it — it measures what actually happened to each scored token afterwards and writes a labelled outcome that the self-learning model trains on. It is the agent that turns Seshat from a static scorer into a system that gets better.

Cadence: every 2 hoursTiered: rules → Claude (Haiku)agents/outcomeResearcher.ts

What it picks up

Every 2 hours it pulls up to 25 composite scores that are at least 2 hours old, have a market cap recorded at scoring time (the baseline), and don't yet have an outcome row. For each, it fetches current market data and computes the multiple vs the baseline.

// Candidate query (simplified):
SELECT cs.id, cs.token_address, cs.composite_score,
       cs.fee_opportunity_score, cs.developer_score,
       cs.tech_research_score,   cs.novelty_score,
       cs.market_cap_usd_at_scoring AS mcap_at_score,
       <hours since scored> AS hours_elapsed
FROM composite_scores cs
WHERE cs.market_cap_usd_at_scoring > 0
  AND <hours since scored> >= 2
  AND NOT EXISTS (              -- not already labelled
    SELECT 1 FROM learning_outcomes lo WHERE lo.composite_score_id = cs.id
  )
ORDER BY cs.scored_at ASC
LIMIT 25;

const ratio = currentMcap / mcap_at_score;   // the core signal

Two tiers

The agent is cost-aware: obvious outcomes are decided instantly from market data, and only genuinely ambiguous cases spend a Claude call.

Tier 1 — obvious (no AI, instant)

Three rules catch the clear-cut cases without any model. They never call Claude.

// checkTier1() — returns a label immediately, or null to escalate
// 1. Dead — no market data 24h+ after scoring
if (currentMcap <= 0 && hoursElapsed >= 24) → 'dump'  // presumed dead/abandoned

// 2. Fast rug — lost >80% within the first 4 hours
if (ratio > 0 && ratio <= 0.2 && hoursElapsed <= 4) → 'dump'

// 3. Undeniable winner — 5× or more
if (ratio >= 5.0) → 'strong_positive'

// Everything else (≈ 0.2× to 5×) returns null → goes to Tier 2.

Tier 2 — ambiguous (Claude researches with tools)

The middle ground is where context matters — a token that 10×'d then settled at 4× is not a dump; a flat token with an active dev and growing holders is not neutral-bad. So Seshat hands the case to claude-haiku-4-5 with three tools and a bounded reasoning loop (up to 6 tool rounds, then a forced final answer).

get_price_trajectory

Current mcap, multiple vs baseline, 24h volume, and a liquidity-health verdict (critical / low / healthy). Re-reads Dexscreener.

search_developer_activity

Scrapes DuckDuckGo for recent mentions of the developer's handle + token name to gauge whether the dev stayed active or went quiet.

get_holder_data

Unique holder count via Basescan (when a key is configured) — growing vs fleeing community. Capped at the 100-per-page API limit.

Claude returns structured JSON — the label, a trajectory classification, a narrative explaining why, and key factors. The trajectory vocabulary is part of what makes the labels richer than a price check.

// Required JSON from the Tier-2 agent:
{
  "outcome_label": "strong_positive|mild_positive|neutral|mild_negative|dump",
  "trajectory":    "steady_rise|peak_then_settle|spike_and_crash|
                    slow_decline|flat|immediate_dump|unknown",
  "peak_multiple_estimate": <number|null>,
  "narrative":   "3-5 sentences: WHY this label? dev? community? liquidity?",
  "key_factors": ["factor1", "factor2", "factor3"],
  "confidence":  "low|medium|high"
}

The outcome labels

Both tiers produce one of five labels. These are the categories the self-learning model maps to numeric targets when it retrains.

strong_positive

Clear sustained upside — dev active, liquidity stable, holders growing.

mild_positive

Positive but not exceptional, or peaked high then settled above baseline.

neutral

Trading flat, or mixed signals (some growth, some decline).

mild_negative

Declining but not dead — liquidity still present, some activity.

dump

Liquidity pulled, >70% loss maintained, or developer abandoned the project.

Graceful degradation

If no Anthropic API key is configured, Tier 2 doesn't stall — it falls back to a deterministic ratio-based label, bucketed by hours elapsed, so the learning loop keeps producing training data with zero model calls. The same fallback catches a malformed Claude response.

// ratioLabel() — the no-AI fallback (bucketed by age)
if (hoursElapsed < 24) {
  if (ratio >= 3.0) return 'strong_positive';
  if (ratio >= 1.5) return 'mild_positive';
  if (ratio >= 0.7) return 'neutral';
  if (ratio >= 0.3) return 'mild_negative';
  return 'dump';
}
// older than 24h — wider bands (more time to move)
if (ratio >= 5.0) return 'strong_positive';
if (ratio >= 2.0) return 'mild_positive';
if (ratio >= 0.5) return 'neutral';
if (ratio >= 0.2) return 'mild_negative';
return 'dump';

What gets written

Each outcome is inserted into learning_outcomes, linked back to the exact composite score it grades. The narrative, trajectory and key factors are bundled into signal_scores_at_alert as JSON for later audit. INSERT OR IGNORE keeps it idempotent — a re-run never double-labels a score.

INSERT OR IGNORE INTO learning_outcomes (
  token_address,            composite_score_id,
  composite_score_at_alert, signal_scores_at_alert,  -- JSON: scores + narrative + trajectory + key_factors
  price_at_alert,           measured_at,
  hours_elapsed,            price_at_measure,
  price_change_pct,         outcome_label,
  outcome_magnitude                                  -- the ratio (current / baseline)
) VALUES (...);

Where it sits in the loop

Composite scoring records a baseline mcap → the OutcomeResearcher grades each score 2h+ later → the weekly ridge regression in the self-learning model trains on the accumulated labels → dimension weights update → the next scores are sharper. Operators can also seed known historical outcomes via POST /api/admin/bootstrap to warm-start the loop. See Self-Learning for the training side.