{"openapi":"3.1.0","info":{"title":"Signal Bureau API","version":"1.0.0","summary":"Machine-readable intelligence feed: current flags, calibrated Q&A, platform stats, and the public record datasets.","termsOfService":"https://signalbureau.ai/terms/","description":"Anonymous access works on every read surface — no key required to evaluate. A FREE identified key is self-issued at POST /api/keys (no human in the loop, no card): it gives a caller its own Answer Engine meter (free_key_daily_allowance/day) — the Answer Engine requires an identified key (universal keying, 2026-08-08; discovery and signal reads stay keyless), sent as `x-api-key` or `Authorization: Bearer sb_live_...`. The machine rate card is GET /api/tariff; POST /api/tariff-quote prices a basket of requirements. Honest limits instead of a key wall (see each operation's 429). Outputs are licensed for evaluation, individual use, and live consumption; systematic bulk collection, redistribution as a dataset, and use to train or calibrate models or competing signal products require a commercial license — see the Terms (§4a). All timestamps are ISO-8601 UTC unless a field says otherwise. Informational only: not investment, legal, or betting advice; prediction-market prices are cited as a signal of what the crowd believes, never a directional claim.\n\nTimeouts: cached reads answer from the edge in well under a second; a cache-miss read recomputes at the origin and can take a few seconds (each read endpoint's cache-control states its refresh window). POST /api/ask spends real model work, and sync and async are different contracts: Synchronous answers typically land in 5–30 seconds; set client timeouts to at least 60 seconds. Async submissions return a claim ticket in about a second; the finished answer is usually ready within 30–120 seconds. We spend the seconds a grounded, source-checked answer takes — built for the internet of machines that need current truth, not for millisecond trading.\n\nCORS: GET endpoints send `access-control-allow-origin: *`. POST /api/ask does not serve cross-origin browsers (server-to-server calls are unaffected).\n\nVersioning: response envelopes carry a `schema` tag where applicable (e.g. `sb.signals.v1`). Within a version, changes are additive only — fields are never renamed or removed.","contact":{"name":"Signal Bureau","url":"https://signalbureau.ai/connect/"}},"servers":[{"url":"https://signalbureau.ai"},{"url":"https://api.signalbureau.ai","description":"The machine door — same API, outside the edge's browser checks; any User-Agent welcome. Prefer this host for programmatic clients."}],"paths":{"/api/signals":{"get":{"operationId":"getSignals","summary":"The current-flags feed (same payload as the get_signals MCP tool)","description":"Every currently flagged entity with trajectory, domains, desk membership, and an explicit evidenceStatus per signal ('receipted' rows carry source-attributed evidence URLs; 'unreceipted' rows report the attention measurement without article receipts — filter on evidenceStatus for fully attributable rows). Snapshot-backed: refreshes nightly; edge-cached 10 minutes (`cache-control: public, max-age=600`). No pagination — the feed is a bounded set (all currently flagged entities, `totalFlagged` in the envelope); `limit` truncates from the top of the ranking. Sorting is fixed: the platform's priority ranking, highest first.\n\nRate limits: 30 requests/minute burst per network address (shared egress shares the allowance) and 500/day anonymous, plus a global daily breaker — enforced on ORIGIN reads. Edge-cached responses (10-minute TTL) serve without consuming quota, so short read bursts against a warm cache may all return 200; quota protects origin compute, not cached reads. Every 429 carries a `retry-after` header (seconds); daily-quota 429s additionally carry `requiresAuth: true`.","parameters":[{"name":"limit","in":"query","required":false,"description":"Cap the list. Clamped to 1..totalFlagged; values above the flagged count return the full feed. Default: all flagged entities.","schema":{"type":"integer","minimum":1}},{"name":"direction","in":"query","required":false,"description":"Filter by trajectory direction.","schema":{"type":"string","enum":["rising","fading","steady","new"]}}],"responses":{"200":{"description":"The feed envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalsFeed"},"examples":{"twoSignals":{"summary":"curl -s 'https://signalbureau.ai/api/signals?limit=1&direction=rising'","value":{"ok":true,"schema":"sb.signals.v1","asOf":"2026-07-11","generatedAt":"2026-07-11T13:05:00+00:00","generated_by":"Signal Bureau — signals for systems (https://signalbureau.ai/connect/)","methodology":{"attention_edge":"Measured association from the versioned regression result (see /api/record-data): flagged markets repriced materially at the published adjusted ratio vs matched controls, with 95% CI, cohort sizes, window, and as-of date stated in the live response. Never a directional claim.","not_a_claim":"No directional or trading claim.","provenance":"Every signal carries evidence with source name and URL."},"count":1,"totalFlagged":170,"signals":[{"name":"taiwan","label":"Taiwan","slug":"taiwan","url":"https://signalbureau.ai/entity/taiwan/","trajectory":{"direction":"rising","deltaPct":42,"trackedDays":61},"signalCount":18,"verticalCount":9,"domains":["defense_watch","semiconductors"],"baselineMultiple":3.1,"firstFlagged":"2026-05-11","why":"Cross-domain acceleration vs baseline","desks":["geopolitics"],"evidence":[{"title":"Example headline","source":"Example Wire","url":"https://example.com/story"}],"ask":"Taiwan: what happens next, and what would change the read?"}]}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/ask":{"post":{"operationId":"ask","summary":"The Answer Engine: one question in, a calibrated, source-checked read out","description":"Synthesizes an answer from tracked coverage, verifies it against sources with a second model pass, and joins live prediction markets. PERSISTENCE: each answer is stored and published at an unlisted public permalink (/s/<id>, returned as `id`); recent answers may surface on /api/recent. Do not send personal or confidential information in questions. Stale permalinks (>~36h) redirect to a fresh re-ask. Spends real model compute per call, so the limits are tighter: 8 requests/minute burst per network address (shared egress shares the allowance), 15/day anonymous (test drive), plus a global daily breaker. A free identified key — self-issued in one call at POST /api/keys, no human in the loop and no card — carries its own independent meter at 25/day, which is the reliable path for an agent on shared cloud egress. Synchronous answers typically land in 5–30 seconds; set client timeouts to at least 60 seconds. ASYNC OPTION: Async submissions return a claim ticket in about a second; the finished answer is usually ready within 30–120 seconds. Pass {\"async\": true} to receive the claim ticket (202: {status:\"working\", ticketId, get:\"/api/answer/<id>\"}) and collect the finished answer at GET /api/answer/{ticketId} — the question is metered once at submit, collecting is free. Falls back to a synchronous answer when the ticket store is unavailable; handle both shapes. SHARED STATE: the same question asked again while the record's answer is recent (~60 min) returns the SAME stored answer immediately — servedFrom:\"maintained-record\", same permalink id, original answeredAt disclosed — rather than a second improvisation; pass {\"fresh\": true} to force new synthesis. Not CORS-enabled for cross-origin browsers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"},"examples":{"simple":{"summary":"curl -s https://signalbureau.ai/api/ask -H 'content-type: application/json' -d '{\"question\":\"What is the outlook for Taiwan?\"}'","value":{"question":"What is the outlook for Taiwan?"}}}}}},"responses":{"200":{"description":"The structured answer. `verified` is the source-check verdict: true (every load-bearing claim located in article-level tracked coverage), false (some claims could not be located, or a hard claim's only support was a headline — both are listed in `unverified` and `confidence` is downgraded), or null (verification unavailable). A claim supported only by a headline is flagged \"supported only by a headline\" and caps the verdict: headline-attributed is not source-verified. Served-from-record answers additionally carry servedFrom/answeredAt/servedNote.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskAnswer"},"examples":{"answer":{"value":{"ok":true,"answer":"Two short paragraphs of analysis grounded in tracked coverage.\n\nSecond paragraph.","probability":0.35,"probabilityRationale":"What pushes the number up and what pushes it down, in one sentence.","confidence":"medium","whatWouldChange":["A named, observable signal"],"topics":["Taiwan"],"verified":true,"trendsUsed":2,"markets":[{"question":"Example market question?","impliedPct":32,"url":"https://polymarket.com/event/example"}],"marketLinks":[],"saved":true,"id":"b1e0…","lineage":{"forecaster":"signal-bureau-answer-engine","forecasterVersion":"ask-v1","forecastCapturedAt":"2026-07-14T12:00:00Z","selectedMarketId":null,"calibrationCohort":"ask-v1 — graded separately from engine_v1","forecastLedgerId":"b1e0…"}}}}}}},"400":{"description":"Invalid JSON body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Question missing or shorter than 5 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"askMethodNotAllowed","summary":"Not supported — POST a question instead","responses":{"405":{"description":"Use POST.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/stats":{"get":{"operationId":"getStats","summary":"Live platform metrics, assembled on demand by a stateless edge worker","description":"Edge-cached 5 minutes. Fields inside `live` are null when a live counter is unreachable (the platform degrades gracefully rather than erroring).","responses":{"200":{"description":"Platform picture.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"},"asOf":{"type":["string","null"]},"live":{"type":"object","description":"Moving counters (entitiesNow, verticalsWatched, marketsUniverse, dataPointsProcessed, questionsAnswered, …). Nullable per field.","additionalProperties":true},"platform":{"type":"object","description":"Slow-moving scale constants (lines of code, workflows, pipelines, …).","additionalProperties":true}}}}}}}}},"/api/recent":{"get":{"operationId":"getRecentQuestions","summary":"Recently answered public questions with permalinks","description":"Public-safe fields only; deduped to the latest row per question. Edge-cached 5 minutes. Returns `{ ok: true, items: [] }` when the store is unavailable — never an error.","responses":{"200":{"description":"Recent Q&A items.","content":{"application/json":{"schema":{"type":"object","required":["ok","items"],"properties":{"ok":{"type":"boolean"},"items":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/record-data":{"get":{"operationId":"getRecordData","summary":"RETIRED record + the surviving regression artifact","description":"The case-study Record was retired 2026-07-15 after internal re-measurement showed flag-to-window lead times below the publication bar; cases were withdrawn rather than relabeled. This endpoint returns the explicit retirement object plus the surviving population-level evidence: the flagged-vs-control repricing regression as an AGGREGATE SUMMARY (estimate, 95% CI, cohort sizes, window, as-of — no per-stratum table; the reproducibility bundle is a declared roadmap item) — the summary behind https://signalbureau.ai/system/. Edge-cached 24 h.","responses":{"200":{"description":"Retirement notice + regression artifact.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/calibration-data":{"get":{"operationId":"getCalibrationData","summary":"Calibration benchmark dataset (Brier, log loss, reliability bins)","description":"The dataset behind /record/calibration/ — forward (contamination-proof) and out-of-sample retrospective scores with confidence intervals. Freshness is PER-COMPONENT: the forward slice regenerates daily as markets mature; the retrospective slice is a fixed benchmark that re-runs on engine changes. Each slice carries its own generatedAt — read those, not the top-level asOf. Edge-cached 1 h.","responses":{"200":{"description":"Calibration dataset.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/mcp":{"post":{"operationId":"mcp","summary":"Model Context Protocol server (JSON-RPC 2.0 over plain HTTP POST)","description":"MCP over Streamable HTTP, stateless JSON-response mode. Protocol version is NEGOTIATED at `initialize` (supported: `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` — the server echoes the client's version when supported, else its newest; these are MESSAGE versions — only Streamable HTTP is served, and the deprecated 2024-era dual-endpoint HTTP+SSE transport is NOT provided, so `2024-11-05` is accepted only for clients that can initialize over this endpoint). Each JSON-RPC 2.0 message is its own HTTP POST; responses are plain JSON (no SSE stream, no long-lived connection, no session id issued). Standard lifecycle supported: `initialize` -> `notifications/initialized` (HTTP 202, no body; all `notifications/*` are accepted with 202) -> `tools/list` -> `tools/call`; `ping` also supported. As a documented Signal Bureau CONVENIENCE EXTENSION, direct `tools/call` without the handshake also works for lightweight stateless integrations. An `MCP-Protocol-Version` request header, when present, is validated: unsupported values receive HTTP 400. Unknown methods return JSON-RPC error -32601. GET on this endpoint returns HTTP 405 (Allow: POST) with a human-readable discovery body. Tools (21): ask, get_answer, get_signals, top_accelerating, search_entities, get_entity, get_truth_object, get_events, get_record, get_calibration, todays_brief, orient, propose_topic, get_quote, create_order, get_order, get_watch_feed, get_desk_feed, get_account, send_feedback, get_feedback_status. Prompts (3): morning-read, judge-us-first, coverage-quote. Resources: 7. Rate limits: the `ask` tool shares the Answer Engine's daily quota and adds a 6/minute burst at the MCP door.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"const":"2.0"},"id":{"type":["integer","string","null"]},"method":{"type":"string","enum":["initialize","notifications/initialized","tools/list","tools/call","ping"]},"params":{"type":"object","additionalProperties":true}}},"examples":{"listTools":{"value":{"jsonrpc":"2.0","id":1,"method":"tools/list"}},"callTool":{"value":{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_signals","arguments":{"limit":5}}}}}}}},"responses":{"200":{"description":"JSON-RPC result or error object.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"204":{"description":"Acknowledged notification (no body)."}}},"get":{"operationId":"mcpDiscovery","summary":"Discovery via 405 (POST-only endpoint)","responses":{"405":{"description":"Method Not Allowed — this endpoint is POST-only (MCP Streamable HTTP, stateless JSON; no standalone SSE stream). The 405 body still carries server name, supported protocol versions, and tool names for human discovery, plus an `allow: POST` header.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/registry":{"get":{"operationId":"getRegistry","summary":"Publication registry — authoritative active/dormant/retired state per event slug (deny-by-default: a slug absent from the registry is unknown and never a canonical record). Round-10 P2-02: now discoverable here.","parameters":[{"name":"slug","in":"query","required":false,"schema":{"type":"string"},"description":"Return one slug's state row ('unknown' if absent) instead of the full map."}],"responses":{"200":{"description":"Registry states. Full map: {asOf, version, policy, counts{activeEvents,dormantEvents,retiredEvents,activeEntities}, events{slug:{state,since|retiredAt,...}}, licensing}. With ?slug=: {ok, asOf, version, slug, state, detail}. Entity MEMBERSHIP is not yet published (count only) — entity rows are a declared roadmap item.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/answer/{ticketId}":{"get":{"operationId":"getAnswer","summary":"Collect an async Answer Engine result by claim ticket","description":"The other half of the async ask flow. Free and unmetered — the work was paid for once at submit. status values: working (poll again after pollAfter seconds), done (the complete answer rides in this response), failed (reason in error; ask again), lost (no progress past the 5-minute horizon; ask again). Tickets are unlisted uuid capability URLs, same model as /s/<id> share links. 30 requests/minute burst per network address (shared egress shares the allowance).","parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The uuid returned by POST /api/ask with async:true (or the MCP ask tool)."}],"responses":{"200":{"description":"Ticket state: {status:\"working\", pollAfter} or {status:\"done\", ...full answer} or {status:\"failed\"|\"lost\", error}."},"404":{"description":"No ticket with that id."},"422":{"description":"Malformed ticket id."},"429":{"description":"Polling faster than the stated burst limit."},"503":{"description":"Ticket store briefly unreachable; the question was not lost."}}}},"/api/feedback":{"post":{"operationId":"sendFeedback","summary":"File structured feedback: bug, improvement, complaint, praise, or question","description":"The desk's feedback window (also the MCP send_feedback tool). Free, no contact details required; include ref (a quoteId/ticketId/orderId) to tie the report to a specific interaction. A complaint that names a real defect becomes our work order. The response is honest about durability: recorded:true only when the store accepted the row; on store trouble you get a 503 asking for a retry rather than a silent loss. Limits: 5/minute burst, 20/day per network address (shared egress shares the allowance).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","summary"],"properties":{"kind":{"type":"string","enum":["bug","improvement","complaint","praise","question"]},"summary":{"type":"string","description":"One sentence: what happened or what you want"},"detail":{"type":"string","description":"Reproduction steps, expected vs observed (optional)"},"about":{"type":"string","description":"Which tool/endpoint/page this concerns (optional)"},"ref":{"type":"string","description":"A quoteId, ticketId, or orderId to tie this to (optional)"}}}}}},"responses":{"200":{"description":"{ok, feedbackId, recorded:true, note}"},"422":{"description":"Missing/invalid kind or summary."},"429":{"description":"Faster than the stated limits."},"503":{"description":"recorded:false — store briefly down; retry rather than silent loss."}}}},"/api/truth/{topic}":{"get":{"operationId":"getTruthObject","summary":"One public truth object: layered, source-backed current state for a maintained topic","description":"Serves the public truth-object record for a maintained topic — the same layered record the exhibit pages render. Free, no key, CORS-enabled (`access-control-allow-origin: *`). The contract is the LAYERS, kept separate so a machine never has to untangle them: primarySources (what authoritative sources state, with receipts), reporting (what current coverage says), coverageMetric (our own attention measurement — our coverage, never the subject's status), market (prediction-market belief, kept as belief, never fact and never advice), judgment (the desk's calibrated read, labeled as such), consensus (where the layers agree or disagree), and stateHistory (the record over time — never edited after the fact). The envelope carries schema `sb.truth.v1` plus `subject` and `asOf`; within the version, changes are additive only. Published topics today: `hormuz`. Unknown or unpublished topics return an honest 404 whose body names what IS published and how to request coverage — never a fabricated record. Rate limits: 30 requests/minute burst per network address (shared egress shares the allowance) and 500/day anonymous (a truth read spends no model tokens; limits protect the edge). Edge-cached 10 minutes (`cache-control: public, max-age=600`).","parameters":[{"name":"topic","in":"path","required":true,"schema":{"type":"string"},"description":"Topic slug, e.g. `hormuz`."}],"responses":{"200":{"description":"The truth object for a published topic.","content":{"application/json":{"schema":{"type":"object","required":["schema","subject","asOf"],"properties":{"schema":{"type":"string","description":"Envelope version tag: sb.truth.v1. Additive changes only within a version."},"subject":{"type":"string","description":"The maintained topic this record is about."},"asOf":{"type":["string","null"],"description":"When this state was last verified."},"layers":{"type":"object","description":"The layered content — the layers ARE the contract, kept separate so a machine never has to untangle them. Additional envelope fields (title, tracked, access, attribution) are descriptive and additive.","properties":{"primarySources":{"description":"Layer: what authoritative sources state, with receipts."},"reporting":{"description":"Layer: what current coverage reports."},"coverageMetric":{"description":"Layer: our own attention measurement — our coverage, never the subject's status."},"market":{"description":"Layer: prediction-market belief, kept as belief — never fact, never advice."},"judgment":{"description":"Layer: the desk's calibrated read, labeled as such."},"consensus":{"description":"Layer: where the layers agree or disagree."},"stateHistory":{"description":"Layer: the record over time — never edited after the fact."}}}}}}}},"404":{"description":"No published truth object for that topic — an honest miss, never a fabricated record. The body names the currently published topics and how to file a coverage request."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/tariff":{"get":{"operationId":"getTariff","summary":"The machine-readable rate card (the billing contract)","description":"Public tariff: units (Watch, Read), flat opening rates, free daily allowances (anonymous and identified-key), the money-back guarantee, and the free-key issuance path. The code that enforces allowances reads the SAME object this endpoint serves, so the stated allowance is the enforced allowance by construction. A dated decision history records every rate change.","responses":{"200":{"description":"The tariff object.","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string"},"units":{"type":"array","items":{"type":"object","properties":{"unit":{"type":"string"},"price":{"type":"number"},"free_daily_allowance":{"type":"number","description":"Anonymous per-network-address daily allowance (Read unit)."},"free_key_daily_allowance":{"type":"number","description":"Per-identified-key daily allowance (Read unit)."}}}},"guarantee":{"type":"object"},"free_key_how":{"type":"string"}}}}}}}}},"/api/tariff-quote":{"post":{"operationId":"tariffQuote","summary":"Quote a basket of requirements (the machine sales desk)","description":"Send `requirements` (free-form text or an array of concerns). The semantic engine decomposes them into well-formed, daily-reportable Watch topics BEFORE any purchase, each priced from the tariff single source. No contact details required; a quote is never a charge; validity is stated in the response.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"requirements":{"description":"Free-form text or an array of concerns to quote.","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}}}}}},"responses":{"200":{"description":"Enumerated topics with per-unit pricing from the tariff."},"422":{"description":"No `requirements`, or nothing quotable in them — the error says which."}}}},"/api/keys":{"get":{"operationId":"describeKeyIssuance","summary":"How to self-issue a free identified key","description":"Returns the issuance contract in prose: what a key is, why it exists (anonymous allowances meter per network address; shared cloud egress makes that unreliable), and the POST body shape.","responses":{"200":{"description":"Issuance guidance."}}},"post":{"operationId":"issueKey","summary":"Self-issue a free identified key (no human in the loop, no card)","description":"Body: `{label}` (required — name yourself; free text). The raw key (`sb_live_<32 hex>`) is returned exactly ONCE in this response; only its SHA-256 is stored, no contact or IP retained. The key carries its own free daily Answer Engine allowance and is presented as `x-api-key` or `Authorization: Bearer`. Issuance is bounded per network address per day.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","description":"Required. Name yourself (e.g. 'Acme research agent'). 120 chars."}}}}}},"responses":{"200":{"description":"The key (shown once), its hint, allowance, and what is stored."},"429":{"description":"Per-address daily issuance bound reached — the anonymous lane stays open meanwhile."}}}},"/api/release":{"get":{"operationId":"getRelease","summary":"Release identity of the running deploy","description":"The deploy's release manifest: `releaseId`, `builtAt`, and named source generations — the same identity every HTML surface carries in `meta[name=sb-release]` and every API response carries in `x-sb-release`. Lets a caller pin observations to an exact release.","responses":{"200":{"description":"Release manifest.","content":{"application/json":{"schema":{"type":"object","properties":{"releaseId":{"type":["string","null"]},"builtAt":{"type":["string","null"]}}}}}}}}},"/api/auth-config":{"get":{"operationId":"getAuthConfig","summary":"Public auth configuration for the account magic-link flow","responses":{"200":{"description":"Public configuration (no secrets)."}}}}},"components":{"responses":{"RateLimited":{"description":"Over a limit. Every 429 carries a `retry-after` header (seconds — for daily quotas, the time to the midnight-UTC reset); burst 429s also carry `retryAfter` in the body, and daily-quota 429s carry `requiresAuth: true` (the future sign-in hook).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","description":"Plain-English, reader-facing message."}}},"RateLimitError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Seconds until the burst window clears (burst 429 only)."},"requiresAuth":{"type":"boolean","description":"True when the daily anonymous quota is exhausted."}}},"Trajectory":{"type":"object","description":"Story momentum — how fast the narrative is moving. NOT an outcome view.","properties":{"direction":{"type":"string","enum":["rising","fading","steady","new"]},"deltaPct":{"type":["number","null"],"description":"Percent change vs the entity's trailing baseline."},"trackedDays":{"type":["integer","null"]}}},"Evidence":{"type":"object","properties":{"title":{"type":"string"},"source":{"type":["string","null"]},"url":{"type":["string","null"],"description":"Original-article URL. Check the claim against its origin."}}},"Signal":{"type":"object","required":["name","label","slug","url","trajectory"],"properties":{"name":{"type":"string","description":"Canonical (normalized) entity key."},"label":{"type":"string","description":"Display label."},"slug":{"type":"string"},"url":{"type":"string","description":"Entity dossier page."},"trajectory":{"$ref":"#/components/schemas/Trajectory"},"signalCount":{"type":["integer","null"]},"verticalCount":{"type":["integer","null"]},"domains":{"type":"array","items":{"type":"string"},"maxItems":12},"baselineMultiple":{"type":["number","null"],"description":"Attention vs the entity's own trailing baseline (ubiquity-normalized)."},"firstFlagged":{"type":["string","null"],"description":"YYYY-MM-DD."},"why":{"type":["string","null"]},"desks":{"type":"array","items":{"type":"string"}},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/Evidence"},"maxItems":3},"ask":{"type":"string","description":"A pre-phrased follow-up for POST /api/ask."},"evidenceStatus":{"type":"string","enum":["receipted","unreceipted"],"description":"receipted = evidence[] carries >=1 source-attributed URL to check the claim against; unreceipted = attention measurement without article receipts in this build (treat as a lead, not a sourced claim)."},"evidenceCount":{"type":"integer","description":"Number of evidence rows attached (0 for unreceipted)."}}},"SignalsFeed":{"type":"object","required":["ok","schema","count","signals"],"properties":{"ok":{"type":"boolean"},"schema":{"type":"string","description":"Envelope version tag, e.g. sb.signals.v1. Additive changes only within a version."},"asOf":{"type":["string","null"],"description":"Snapshot date (YYYY-MM-DD)."},"generatedAt":{"type":["string","null"]},"nextBuildAt":{"type":"string","format":"date-time","description":"OPTIONAL. When the NEXT scheduled data build is due (ISO-8601 UTC), derived from the build workflow's real cron schedule — never guessed. A quiet gap before nextBuildAt is scheduled rest, not staleness. Present from the first post-release data build; absent on snapshots that predate the field.","example":"2026-07-23T12:47:00+00:00"},"buildCadence":{"type":"string","description":"OPTIONAL. Plain-text statement of the data-build schedule (e.g. how many builds per day and over which UTC window), computed from the same parsed cron schedule as nextBuildAt. Present from the first post-release data build.","example":"8 scheduled data builds daily between 12:47 and 23:05 UTC; the market panel is sampled every 2 hours around the clock. A quiet gap between builds is scheduled rest, not staleness."},"generated_by":{"type":"string"},"methodology":{"type":"object","additionalProperties":{"type":"string"}},"count":{"type":"integer","description":"Signals in this response."},"totalFlagged":{"type":"integer","description":"Size of the full flagged set before limit/direction filters."},"coverage":{"type":["object","null"],"description":"Coverage counts for the vertical universe behind the feed — three DIFFERENT sets, never interchangeable. Served as an EXPLICIT null (with coverageUnavailableReason stating why) when the committed snapshot predates the coverage object; the next scheduled data build populates it. Counts are never fabricated.","properties":{"registered":{"type":["integer","null"],"description":"Every vertical in the coverage registry, including new ones in burn-in."},"activelyScraped":{"type":["integer","null"],"description":"Verticals producing articles on the current daily scrape cycle."},"spannedByFlaggedEntities":{"type":["integer","null"],"description":"Verticals the currently flagged entities draw evidence from — moves with the news."},"definitions":{"type":"object","additionalProperties":{"type":"string"},"description":"The same three definitions riding the payload, so integrators never have to guess which set a number names."}}},"coverageUnavailableReason":{"type":"string","description":"Present exactly when coverage is null: a plain-language reason the counts are unavailable (e.g. the committed snapshot predates the coverage object; populated by the next scheduled data build). Absent when coverage is populated.","example":"committed snapshot predates the coverage object; populated by the next scheduled data build"},"signals":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}}}},"AskRequest":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":5,"description":"A question about the future. Trimmed; must be at least 5 characters."},"context":{"type":"array","maxItems":12,"items":{"type":"object","additionalProperties":true},"description":"Optional entity context objects (the site UI passes matched entities). Server-side matching fills this for MCP callers; plain API callers can omit it."},"async":{"type":"boolean","description":"true → return a claim ticket immediately (202) and compute in the background; collect at GET /api/answer/{ticketId}. Default false (synchronous)."},"fresh":{"type":"boolean","description":"true → force a new synthesis instead of serving the maintained record's recent answer to the same question. Default false: a repeat of a recently-answered question returns the SAME stored state (servedFrom:\"maintained-record\", same permalink id) — the shared-state property, by design."}}},"Market":{"type":"object","properties":{"question":{"type":"string"},"impliedPct":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The crowd's implied YES probability, 0-100. Null for multi-outcome events (see outcomes)."},"outcomes":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"pct":{"type":"number"}}},"description":"Top outcomes for multi-outcome events (present only then)."},"url":{"type":"string"}},"additionalProperties":true},"AskAnswer":{"type":"object","required":["ok","answer"],"properties":{"ok":{"type":"boolean"},"answer":{"type":"string","description":"2–3 short paragraphs separated by blank lines."},"probability":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Calibrated probability when the question is binary and answerable; null otherwise."},"probabilityRationale":{"type":["string","null"]},"confidence":{"type":"string","enum":["low","medium","high"]},"whatWouldChange":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"string"},"description":"Canonical topics to track."},"verified":{"type":["boolean","null"],"description":"Source-check verdict from the second model pass; null when verification was unavailable."},"unverified":{"type":["array","null"],"items":{"type":"string"},"description":"Claims that could not be located in tracked coverage (present when verified=false); null when the verification pass did not produce a list (e.g. out-of-scope answers)."},"trendsUsed":{"type":"integer","description":"How many matched entities carried multi-week trend history."},"markets":{"type":"array","items":{"$ref":"#/components/schemas/Market"},"description":"Live prediction-market reads joined to the question (max 3)."},"marketLinks":{"type":"array","items":{"type":"object","additionalProperties":true}},"saved":{"type":"boolean","description":"Whether the answer was persisted (permalink available)."},"id":{"type":["string","null"],"description":"Shareable permalink id (/s/<id>); null if persistence was unavailable."},"lineage":{"type":["object","null"],"description":"Forecast lineage, present on every numeric answer: forecaster, forecasterVersion (ask-v1), forecastCapturedAt, selectedMarketId (null when no exact market is bound), calibrationCohort (ask-v1 — graded separately from engine_v1), forecastLedgerId."}}}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"OPTIONAL free identified key from POST /api/keys (also accepted as `Authorization: Bearer sb_live_...`). Absent or invalid keys fall open to the anonymous lane — a key is recognition by credential, never a wall."}}}}