Date: 2026-05-09 UTC
Refs: docs/architecture/reasonator/design.md, docs/architecture/reasonator/api-contract.md, #1385
All times UTC.
A Pro user views their AAPL position history. Raptor's background scheduler fills in the sentiment window for that position asynchronously.
sequenceDiagram
autonumber
participant User as User (Pro)
participant Antlers as Antlers (frontend)
participant Raptor as Raptor (backend)
participant Sched as Raptor Scheduler
participant Reasonator as Reasonator service
participant DB as sentiment_events (Raptor DB)
User->>Antlers: Open positions page
Antlers->>Raptor: GET /api/positions
Raptor-->>Antlers: positions[] (with sentiment_status: "pending" or "available")
Antlers-->>User: Show positions; sentiment badge shows "Loading..."
Note over Sched: Scheduler runs every N minutes for unscored events
Sched->>DB: SELECT id, headline FROM sentiment_events<br/>WHERE scorer_model_version IS NULL AND symbol IN (user_symbols)
DB-->>Sched: [unscored rows]
Sched->>Reasonator: POST /v1/score/batch<br/>X-Raxx-Tier: pro<br/>{ job_id, model_sha, headlines: [...] }
Reasonator-->>Sched: 202 Accepted { job_id, status: "queued" }
loop Poll every 30s
Sched->>Reasonator: GET /v1/score/batch/{job_id}
Reasonator-->>Sched: { status: "processing", progress: 0.42 }
end
Reasonator-->>Sched: { status: "complete", results: [...] }
Sched->>DB: UPDATE sentiment_events SET<br/>sentiment_label, sentiment_score,<br/>sentiment_confidence, scorer_model_version<br/>WHERE id IN (scored_ids)
Sched->>DB: INSERT INTO sentiment_score_audit<br/>(event_id, scored_at, model_sha, label, score, source: "rack_batch")
Note over User,Antlers: User refreshes or next poll cycle
Antlers->>Raptor: GET /api/sentiment/AAPL?start=2026-04-14T10:00:00Z&end=2026-04-18T16:00:00Z
Raptor->>DB: Aggregate scored events in window
DB-->>Raptor: { score: 0.08, label: "neutral", count: 12, events: [...] }
Raptor-->>Antlers: Sentiment summary + top headlines
Antlers-->>User: Show sentiment badge "Neutral (+0.08)<br/>12 articles during your hold"
Key invariants preserved: - I-1: Score describes the window — not a forecast. - I-2: Reasonator is read-only. No order state touched. - I-3: If Reasonator is down, Sched retries later; positions page still loads (no sentiment badge). - I-6: All timestamps UTC.
A Pro+ user opens a trade ticket for AAPL. Raptor synchronously fetches the current sentiment snapshot from Reasonator, which the user can review before deciding.
sequenceDiagram
autonumber
participant User as User (Pro+)
participant Antlers as Antlers (frontend)
participant Raptor as Raptor (backend)
participant Reasonator as Reasonator service
participant DB as sentiment_events (Raptor DB)
participant CB as Circuit Breaker (Raptor-side)
User->>Antlers: Open trade ticket for AAPL
Antlers->>Raptor: GET /api/trade-context/AAPL
Note over Raptor: Check circuit breaker state
Raptor->>CB: Is Reasonator circuit open?
CB-->>Raptor: closed (Reasonator is healthy)
Raptor->>DB: SELECT headlines from sentiment_events<br/>WHERE symbol='AAPL' AND published_at > NOW()-4h<br/>AND scorer_model_version IS NOT NULL
alt Scored events exist in DB (cache hit)
DB-->>Raptor: [pre-scored rows]
Note over Raptor: Skip Reasonator call — use cached scores
Raptor->>Raptor: Aggregate cached scores
else Unscored events or cache miss
DB-->>Raptor: [unscored or stale rows]
Raptor->>Reasonator: POST /v1/score<br/>X-Raxx-Tier: pro_plus<br/>{ model_sha, headlines: [last N hours] }
alt Reasonator responds within 2s (p99 target)
Reasonator-->>Raptor: 200 { results: [...] }
Raptor->>DB: Write scores to sentiment_events + audit log
else Reasonator timeout or 5xx
Reasonator-->>Raptor: timeout / 503
Raptor->>CB: Record failure
CB-->>Raptor: Circuit still closed (< 5 failures)
Raptor->>Raptor: Serve stale cache or null sentiment
end
end
Raptor-->>Antlers: { trade_context: { symbol, price, ... },<br/>sentiment: { score: 0.42, label: "positive",<br/>count: 8, events: [...], stale: false } }
Antlers-->>User: Trade ticket with sentiment panel:<br/>"AAPL sentiment: Positive (+0.42) — 8 articles in last 4h<br/>Here are the headlines [...]"
Note over User: User reviews facts. User decides.<br/>Raxx never proposes the trade.
opt User configures Pro+ alert rule
User->>Antlers: "Alert me when AAPL sentiment flips negative<br/>on positions > $10K"
Antlers->>Raptor: POST /api/alert-rules<br/>{ symbol: "AAPL", condition: "sentiment_label=negative",<br/>position_threshold: 10000 }
Raptor-->>Antlers: Rule saved
Note over Raptor: Next time Reasonator returns negative for AAPL...
Raptor->>Raptor: Evaluate rule against new score
Raptor->>User: Email: "AAPL sentiment is currently negative.<br/>You have $X in open AAPL positions.<br/>[No recommendation. No trade proposal.]"
end
Key invariants preserved: - I-1: Score is current historical state of sentiment — not a prediction. - I-2: Raptor evaluates alert rules; no order fires. User decides. - I-3: Circuit breaker shields trade ticket from Reasonator failures. Ticket still opens. - I-7: No broker names in the sentiment panel.
The operator updates the pinned FinBERT model SHA. A sweep job re-scores all historical sentiment_events rows and writes the updated scores alongside the old ones for provenance.
sequenceDiagram
autonumber
participant Operator as Operator
participant Vault as Infisical Vault
participant Reasonator as Reasonator service
participant Sched as Raptor Re-score Sweep Job
participant DB as Raptor DB<br/>(sentiment_events + audit)
Operator->>Vault: Update FINBERT_MODEL_SHA=def789abc012<br/>at /reasonator/prod/FINBERT_MODEL_SHA
Operator->>Reasonator: SIGHUP (or redeploy)<br/>→ Reasonator reloads model with new SHA
Reasonator->>Reasonator: Download + load ProsusAI/finbert@def789abc012<br/>(~30–120s model load time)
Reasonator-->>Operator: GET /v1/health → { model_sha: "def789abc012", model_loaded: true }
Operator->>Sched: Trigger rescore sweep<br/>(manual or scheduled job)
loop Pages of 500 rows
Sched->>DB: SELECT id, headline, symbol, published_at,<br/>sentiment_score AS prev_score, scorer_model_version AS prev_sha<br/>FROM sentiment_events<br/>WHERE scorer_model_version != 'def789abc012'<br/>ORDER BY published_at DESC<br/>LIMIT 500 OFFSET :page
DB-->>Sched: [500 rows with prev scores]
Sched->>Reasonator: POST /v1/score/rescore<br/>X-Raxx-Tier: pro<br/>{ new_model_sha: "def789abc012",<br/> headlines: [{ id, text, symbol, published_at,<br/> previous_score, previous_model_sha }] }
Reasonator-->>Sched: 200 { results: [{ id, label, score, confidence,<br/> previous_score, score_delta }] }
Sched->>DB: UPDATE sentiment_events<br/>SET sentiment_label=new, sentiment_score=new,<br/>sentiment_confidence=new,<br/>scorer_model_version='def789abc012'<br/>WHERE id IN (batch_ids)
Sched->>DB: INSERT INTO sentiment_score_audit<br/>({ event_id, scored_at, scorer_model_version: "def789abc012",<br/> label, score, confidence, score_source: "rack_rescore",<br/> previous_score, previous_model_sha: "abc123def456" })
Note over Sched: Wait 100ms between pages<br/>to avoid starving Pro+ sync requests
end
Sched-->>Operator: Sweep complete.<br/>N rows rescored. M rows unchanged.<br/>Score drift summary: mean |delta| = 0.03
Key invariants preserved:
- I-5: Every score write (including re-scores) is audited in sentiment_score_audit with old and new SHA.
- I-4: Model SHA is env-driven (Vault); no SHA hardcoded in code.
- I-3: Re-score sweep uses X-Raxx-Tier: pro so it does not starve Pro+ sync requests.
Reasonator becomes unavailable (dyno restart, deploy, crash). Raptor's circuit breaker opens and all in-flight requests return gracefully without blocking users.
sequenceDiagram
autonumber
participant User as User (Pro+)
participant Antlers as Antlers
participant Raptor as Raptor
participant Reasonator as Reasonator service (DOWN)
participant CB as Circuit Breaker
participant Cache as Response Cache (Raptor)
User->>Antlers: Open trade ticket for AAPL
Antlers->>Raptor: GET /api/trade-context/AAPL
Raptor->>CB: Is Reasonator circuit open?
CB-->>Raptor: closed
Raptor->>Cache: Any cached sentiment for AAPL in last 15m?
Cache-->>Raptor: cache miss
Raptor->>Reasonator: POST /v1/score (timeout: 3s)
Reasonator-->>Raptor: [no response — dyno down]
Raptor->>CB: Record failure #1
Note over CB: Threshold: 5 failures in 60s
Note over Raptor: Retries 1-4 (other requests in flight)<br/>Each timeout → CB records failure
Raptor->>CB: Record failure #5
CB->>CB: Circuit opens. State: OPEN.<br/>Reset timer: 120s
Note over CB,Raptor: All subsequent Reasonator calls<br/>short-circuit immediately
Raptor-->>Antlers: { trade_context: { ... },<br/>sentiment: null,<br/>sentiment_reason: "rack_unavailable" }
Antlers-->>User: Trade ticket loads normally.<br/>Sentiment panel: "Sentiment data temporarily unavailable"
Note over User: Trade ticket still fully functional.<br/>User can trade normally.<br/>Reasonator being down NEVER blocks execution.
Note over CB: After 120s, circuit enters HALF-OPEN
CB->>Reasonator: Probe: GET /v1/health
Reasonator-->>CB: 200 { status: "ok", model_loaded: true }
CB->>CB: Circuit closes. State: CLOSED.
Note over Raptor: Normal Reasonator calls resume
Key invariants preserved: - I-3: Trade ticket opens. Order entry is available. Reasonator is always optional. - I-2: The execution path is completely independent of sentiment availability.