{"openapi":"3.1.0","info":{"title":"Signal Bureau API","version":"1.0.0","summary":"Machine-readable intelligence feed: current flags, grounded source-checked Q&A, platform stats, and the public record datasets.","termsOfService":"https://sigb.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 (25/day) — the Answer Engine requires an identified key (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://sigb.ai/connect/"}},"servers":[{"url":"https://api.signalbureau.ai","description":"DEFAULT for programmatic clients — the machine door. Same API, outside the edge's browser checks; any User-Agent is welcome, and you never need to disguise your client as a browser. Generated clients take servers[0], which is why this entry is first."},{"url":"https://sigb.ai","description":"The apex — the human site, which also answers the API. It runs the edge's browser checks, so a bare programmatic User-Agent can be refused with a 403 naming Cloudflare error 1010. If you see that, switch to the machine door above and it clears."}],"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://sigb.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://sigb.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://sigb.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 grounded, 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: 12 requests/minute burst per key, 25/day on a free self-issued key, plus a global daily breaker. Anonymous ask is not served — an unkeyed call meets an 8 requests/minute per-address burst guard and returns the one-step self-issue instruction. The key is issued in one call at POST /api/keys (no human in the loop, no card) and carries its own independent meter, never shared with cloud or office neighbors. 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://sigb.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. RESPONSE-SHAPE CHANGE (2026-08-19): answers carry no probability of Signal Bureau's own — the former `probability`, `probabilityRationale`, and `lineage` fields were removed by operator ruling. Prediction-market prices remain on `markets[]` rows, quoted and attributed to their market.","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.","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…"}}}}}},"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 platform scale constants (coverage universe, market universe, vertical registry).","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":"The correction record — the flagged-vs-control repricing regression","description":"The population-level evidence: the flagged-vs-control repricing regression as an AGGREGATE SUMMARY (estimate, 95% CI, cohort sizes, window, as-of), with the full reproducibility bundle at https://sigb.ai/reproducibility/repricing-regression.json. Also carries the dated retirement object for the case-study track record withdrawn in 2026 after re-measurement put it under our own publication bar — appended, never edited. 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 of the market sensor we cite (Brier, log loss, reliability bins)","description":"The grading dataset for the market sensor we cite, and the published home of these numbers — 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. Rows: `crowd` (the prediction-market price) and `base_rate` (the naive null it is scored against), in both slices. No forecaster of ours is published — we sell a maintained, source-traced record and cite market prices as one graded sensor, never a forecast of ours (operator ruling, 2026-08-19). The payload states this in `whatThisCovers`.","responses":{"200":{"description":"Calibration dataset for the cited market sensor.","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).","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 rows are served as counts.","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 derived reading over the layers above — analysis labeled as ours, never a probability of ours), 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: two full-tier records (`hormuz`, `interest-rates`) plus a standard-tier record for every tracked event with live coverage — an unknown topic's 404 body names the current published list. 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 derived reading over the layers above — analysis labeled as ours, never a probability of ours."},"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, the independent per-key meter it carries, 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 (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. Carries no probability of Signal Bureau's own (removed 2026-08-19 by operator ruling); any percentage in the prose is the market's or a source's, attributed."},"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."}}}},"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."}}}}