{"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 free asks per key, for the life of the key (not per day; they do not reset; a used-up allowance answers 402 with the metered next step) — 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 measured 25–29 seconds (p50–p90) over 70 live asks in the last 14 days, as of 2026-09-26; 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 free asks per key for the life of a free self-issued key (not per day — they do not reset; a used-up allowance answers 402 naming the metered next step, never a dead end), a per-key daily ceiling that is runaway-loop protection, 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 measured 25–29 seconds (p50–p90) over 70 live asks in the last 14 days, as of 2026-09-26; 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. METERING: An identical question repeated within 10 minutes against an unchanged record is one Read. A repeat of your own question inside that window comes back servedFrom:\"recent-identical-ask\" with metering.read.charged=false and the rule in metering.read.reason; anyone else's identical question is their own Read. 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"}}}},"402":{"description":"The key's free allowance (25 asks per key, lifetime — never per day) is used up AND the key has no balance left to draw from. Body: `error: \"allowance_exhausted\"`, `allowance` {asks, used, remaining: 0, resets: 'never'}, `balance` {state, remaining, rate}, `owed: 0`, and `nextSteps` — TWO ways on, both as machine-followable programs. Step 0 is a BALANCE on the key (MCP create_order, item 'balance', with an `amount` from $5 to $500): your principal completes one checkout, ever, and after it the key draws each Read from the balance at the published rate with no human in the loop. Step 1 is a monthly metered arrangement (create_order item 'api', or POST /api/feedback naming the key). Each step carries the Read price, the refund path and the guarantee, and the body lists what stays free. A door, never a dead end; no Retry-After, because neither the allowance nor a balance resets."},"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 (22), in the order `tools/list` serves them: start_here, top_accelerating, search_entities, get_entity, get_truth_record, ask, get_answer, get_signals, get_record, get_calibration, get_events, get_corroborated, todays_brief, propose_topic, get_quote, create_order, get_order, get_watch_feed, get_desk_feed, get_account, send_feedback, get_feedback_status. Two tools were renamed on 2026-09-07 and their earlier names are PERMANENT aliases, not a deprecation queue — machine callers hold tool names in configs we do not control, so `orient` (now `start_here`) and `get_truth_object` (now `get_truth_record`) keep answering, return the canonical tool's work, and carry a `deprecated` marker naming the current name. An alias never reshapes a payload underneath a caller who never asked for the new name. Prompts (3): morning-read, judge-us-first, coverage-quote. Resources: 8. 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 AFTER a re-drive was also tried; ask again). A ticket still working past the published 120-second bound is re-driven once, automatically — the response then carries redriven:true and redrivenAt, the answer still lands under the same ticket, and the lost clock restarts from the re-drive. 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}.","content":{"application/json":{"schema":{"type":"object","description":"Shape varies by `status`; the done/failed/lost shapes are summarized in this response's own description above. Documented here is the `working` shape specifically — the one that changed (#782): a run still in progress now says whether it is on schedule, instead of reading identically to one that just started.","properties":{"status":{"type":"string","enum":["working","done","failed","lost"],"description":"The fields below (elapsedSeconds, late, message) ride only when status is \"working\"."},"elapsedSeconds":{"type":"number","description":"Present when status is \"working\" — seconds since the ticket was created; the same value as `ageSeconds`, under the name a poller reaches for (`ageSeconds` stays present too, for every client already reading it)."},"late":{"type":"boolean","description":"Present when status is \"working\". True once `elapsedSeconds` passes the published 30–120 second range for a typical async answer — the honest middle state between \"working\" and \"lost\" (a run is only reported lost past 300 seconds): a run behind schedule now says so instead of reading identically to one that just started."},"message":{"type":"string","description":"Present only when `late` is true — states the elapsed seconds, the published range, whether the run was re-driven, and that it has not been dropped (lost is declared only 300 seconds after the re-drive)."},"redriven":{"type":"boolean","description":"Present (and always true) once this ticket has been re-driven. A run still working past the published 120-second bound is re-fired exactly once — a compare-and-set in the ticket store, so polling repeatedly cannot fire it twice and concurrent pollers fire it once between them. The answer lands under the same ticketId; nothing is re-metered."},"redrivenAt":{"type":"string","format":"date-time","nullable":true,"description":"Present with `redriven` — when the re-drive was claimed. Null in the rare case where a concurrent poller claimed it in the same instant and this response did not re-read the row; the next poll carries the timestamp."}}}}}},"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/corroborated":{"get":{"operationId":"getCorroborated","summary":"What independent sources agreed on in the published window","description":"The corroboration layer of the maintained record: events in the published window that more than one INDEPENDENT origin reported. Free, no key, CORS-enabled (`access-control-allow-origin: *`). Each entry carries the claim as its sources state it, the corroboration tier (`strong` = independently corroborated, `moderate` = corroborated), the independent-origin count, the origins by name with wires marked, the number of republished copies collapsed, the entities, and the earliest report date when the producing run carries one. Syndication is collapsed, never summed: a wire story republished by fifteen outlets is ONE origin. Corroboration counts independent origins, not truth — many outlets echoing one official statement is consensus, not verification, and the `method` object states the engine's own rules and caveat verbatim. NO PROBABILITY HERE IS OURS: prediction-market prices appear only where the match was claim-direction verified, and are a cited external sensor, never a forecast of ours. THREE STATES, NEVER TWO: `state` is `measured` (the pass ran — an empty `clusters` list then means nothing cleared the bar) or `cannot-measure` with `cannotMeasureReason` naming which pass failed. A cannot-measure reading is served as a 200, not an error, so a client can distinguish a quiet week from a blind instrument. Rate limits: 30 requests/minute burst per network address and 500/day anonymous (this read spends no model tokens; the limits protect the edge). Cached 10 minutes at the edge.","responses":{"200":{"description":"The corroborated record for the published window. Served as 200 in both states — read `state` before `clusters`.","content":{"application/json":{"schema":{"type":"object","required":["schema","state","asOf","clusters"],"properties":{"schema":{"type":"string","description":"Envelope version tag: sb.corroborated.v1. Additive changes only within a version."},"state":{"type":"string","enum":["measured","cannot-measure"],"description":"Whether the corroboration pass could be read. `measured` with an empty clusters list is a real reading (nothing cleared the bar); `cannot-measure` means we could not look."},"cannotMeasureReason":{"type":["string","null"],"description":"Names which pass failed when state is cannot-measure. Null when measured."},"asOf":{"type":"string","description":"The date this shard was built."},"window":{"type":["object","null"],"description":"The publication window the corroboration run covers.","properties":{"start":{"type":"string"},"end":{"type":"string"}}},"source":{"type":["object","null"],"description":"Which artifact was read (`kind`: nightly, or compare-fallback while the nightly artifact is absent) and when it was generated."},"method":{"type":["object","null"],"description":"The corroboration engine's own independence rule, clustering rule, and caveat, carried verbatim."},"marketJoin":{"type":["string","null"],"description":"States when the prediction-market join was not run for this artifact, rather than leaving the absence unexplained."},"counts":{"type":["object","null"],"description":"clustersTotal, corroborated, published — the denominators behind the list."},"tiers":{"type":"array","description":"The tier vocabulary with reader-facing labels and meanings.","items":{"type":"object"}},"clusters":{"type":"array","description":"The published events, ranked by tier then independent-origin count.","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable cluster key for this event within the run."},"claim":{"type":"string","description":"The claim as its sources state it — HTML entities decoded, never truncated."},"claimUrl":{"type":["string","null"],"description":"Source URL when the run carries one for the representative document."},"tier":{"type":"object","description":"id, reader label, meaning, rank, and the engine's own tier string."},"independentOrigins":{"type":"integer","description":"Distinct independent origins after syndication collapse."},"articles":{"type":"integer","description":"Documents in the cluster, before collapse."},"origins":{"type":"array","description":"The origins by name, each flagged wire / press.","items":{"type":"object"}},"wires":{"type":"array","items":{"type":"string"}},"republishersCollapsed":{"type":"integer","description":"How many republished copies were collapsed rather than counted as agreement."},"earliestDate":{"type":["string","null"],"description":"Earliest report date among the corroborating documents, when the producing run carries it."},"earliestDateReason":{"type":["string","null"],"description":"Why earliestDate is null — absent field, never inferred from the window."},"entities":{"type":"array","items":{"type":"string"}},"entitiesPromoted":{"type":"array","description":"Canonical promoted entities with ids and categories.","items":{"type":"object"}},"market":{"type":["object","null"],"description":"The matched prediction-market question with its claim-direction alignment flag, or null. A price is only meaningful where claim_aligned is true."},"bodyStatus":{"type":"string","description":"Body-capture reading for this event — our instrument's state, for auditors."}}}}}}}}}}}},"/api/tariff":{"get":{"operationId":"getTariff","summary":"The machine-readable rate card (the billing contract)","description":"Public tariff: units (Watch, Read), flat opening rates, the free allowance on a self-issued key (25 asks per key for the life of the key since 2026-09-05 — not per day; `before` states the per-day terms keys issued earlier keep), 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_allowance":{"type":"object","description":"The free allowance on a self-issued key (Read unit), LIFETIME not daily since 2026-09-05: {asks, per: 'key', resets: 'never', effective, note, daily_ceiling, daily_ceiling_note, before: {per_day, resets, note}} — `before` states the per-day terms keys issued earlier keep. Replaced free_key_daily_allowance (schema_version 1 -> 2). The sibling `balance` object on the same unit states the top-up terms: {min_usd, max_usd, per: 'key', drawn_at: 'read' (the rate is the Read price above, by reference), expires: 'never', order, refundable, refund_path}."}}}},"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), plus optional `contact`, `purpose`, and `preferredRail` (one of card-via-principal | agent-token | stablecoin | other — how you would rather pay, recorded as profiling ONLY: it gates nothing, changes no price, and an unrecognized value is filed as 'other' with the text dropped). 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 a free Answer Engine allowance of 25 asks for its lifetime (stated in the response with whether it resets — it does not — and the price after it) and is presented as `x-api-key` or `Authorization: Bearer`. The response also states the BALANCE terms: a key can be funded once with any amount from $5 to $500 and every Read past the allowance draws from it at the published Read rate — this meter funded in advance at list, never a discounted prepaid pack. 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). On /api/ask a funded key can also meet `error: \"draw_ceiling_reached\"` — a published ceiling on how much of its BALANCE one key may draw per UTC day (GET /api/tariff, read unit, `balance.daily_draw_ceiling_usd`). It is runaway-loop and stolen-key protection on the RATE, never a claim on the balance: the body carries `dailyDrawCeiling` {usd, reads, rate, resets, why}, `drawnToday`, `balance` {intact: true, remaining}, `owed: 0` and `nextSteps` (wait for the midnight-UTC reset, or ask the desk to raise it on your account). Nothing is drawn on a refused Read, the balance never expires, and what the ceiling holds back today is drawn tomorrow.","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."},"dailyDrawCeiling":{"type":"object","description":"draw_ceiling_reached only: the published per-key, per-UTC-day ceiling on balance draws — {usd, reads, rate, resets, why}. Stated in the tariff before you fund a balance, so it is never a surprise at the door."},"drawnToday":{"type":"object","description":"draw_ceiling_reached only: what this key has already drawn from its balance today — {usd, reads}."},"balance":{"type":"object","description":"draw_ceiling_reached only: {intact: true, remaining, rate, refund}. The ceiling never touches the balance — a refused Read draws nothing, and unused balance is returned in full on request."}}},"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. An identical question repeated within 10 minutes against an unchanged record is one Read. — so passing true also opts out of that, buying new work at the published rate."}}},"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"],"description":"Capped by verification, never independent of it (2026-09-07). Unless `verified` is exactly true AND `verifiedBlockedBy` is empty, this reads \"low\" and `unverified` carries the reason in words — a cannot-measure verdict may never render as the strongest trust signal we emit. The synthesis model's own self-assessment is an input to that ceiling, never the output."},"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. Three states, never two: true = checked and located, false = checked and not located, null = could not check. Both false and null cap `confidence` at low; only null sets `verificationAbstained`."},"servedFrom":{"type":"string","enum":["maintained-record","recent-identical-ask"],"description":"Present only when this reply was served FROM the record instead of synthesized fresh. 'maintained-record': this question was answered recently (~60 min) and you are reading that SAME state under the same permalink id — the shared-state property, and a Read like any other. 'recent-identical-ask': YOUR OWN key asked this exact question inside the published window and the record has not moved, so no work was done and no Read was receipted — An identical question repeated within 10 minutes against an unchanged record is one Read. It is scoped to your key: one key is never served, and never spared a charge, from another key's answers. Pass {\"fresh\": true} for new work, metered as new work."},"servedFromAt":{"type":"string","format":"date-time","description":"When this reply was served from the record. Its companion `answeredAt` is when the answer it serves was produced — the gap between them is the age, with no arithmetic."},"metering":{"type":["object","null"],"description":"What this request did to your meter. Null when no identified key was presented.","properties":{"charged":{"type":"boolean","description":"Billed to your account in money. False on a free-allowance key, and false on a repeat inside the published window."},"read":{"type":"object","description":"Whether this request cost you a Read AT ALL, and if not, why. Deliberately distinct from `charged` above, which is about money: a free-allowance ask is charged:false with read.charged:true — it spends one of the free asks, at $0.00.","properties":{"charged":{"type":"boolean","description":"False when no Read was receipted for this request."},"reason":{"type":["string","null"],"description":"Why no Read was receipted, in the words the tariff publishes. An identical question repeated within 10 minutes against an unchanged record is one Read."},"servedFrom":{"type":"string","description":"The rule that spared the receipt — matches the reply's own servedFrom."},"of":{"type":["string","null"],"description":"Permalink id of the answer this repeat was served from."},"at":{"type":["string","null"],"description":"When that answer was produced."}}}}},"servedAs":{"type":"string","enum":["verified","as-written","evidence-bounded","abstained"],"description":"What happened to the prose. 'verified': the verdict cleared and the text is untouched. 'as-written': the verdict did not clear but nothing in the text states more than the sources establish, so it is served unchanged. 'evidence-bounded': every sentence stating more than the cited sources establish was removed whole and the rest served verbatim, with a disclosure line saying a statement was withheld — and also the state served when the verification pass could not run at all (verificationReason 'verifier-unavailable') over an answer that has evidence: the mechanical strength check still bounds the prose, so the text is held to what the citations carry, but no verifier read it. 'abstained': the conclusion was withheld and a labeled statement served in its place (opens 'Withheld:'). Guarantee: when verificationReason is 'overstated' this is never 'verified' or 'as-written'; when the verifier did not run it is never 'verified' or 'as-written' either; and an abstention always carries abstained: true, probability: null, confidence: 'low'. Verifier language reaches a caller only inside the labeled abstention, never spliced into reporting."},"abstained":{"type":"boolean","description":"Whether the conclusion was withheld and an evidence summary served instead of a synthesized answer. Always a boolean, never null — this is OUR editorial decision taken at synthesis, not a reading of the world, so it has no cannot-measure state to report; see `verificationState` below for the field that does carry one. True exactly when `servedAs` is \"abstained\"."},"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). Also carries the confidence-ceiling reason in plain words when a blocking condition capped the answer."},"verifiedBlockedBy":{"type":"array","items":{"type":"string","enum":["no-citations","headline-only","self-citation-only","overstated","verifier-flagged","verifier-unavailable","conjunction-unsupported","conjunction-unjudgeable","market-disagreement","market-unreceipted"]},"description":"Every condition that capped this answer, machine-readable. Empty means nothing capped it. Non-empty always implies confidence \"low\". The first eight are properties of the CITATIONS and are exactly the conditions the machine-acceptance verdict is computed from, so any of them appearing means `verified` is not true. The last two are properties of the PAYLOAD's internal agreement rather than of the evidence — `market-disagreement` (the prose stated a market number this response's own `marketReceipt` or `markets` array contradicts) and `market-unreceipted` (a market-pricing claim carried no receipt on this answer a reader could check). Both REMOVE the offending sentence before the answer is served and disclose the removal on `unverified`; neither changes `verified`, because the claims that remain are sourced exactly as well as the verifier found them. `verified: true` beside one of these two is therefore coherent, and means: the sources checked out, and a sentence that disagreed with this payload's own market data was taken out."},"unverifiedReason":{"type":["string","null"],"description":"The single condition that set the confidence ceiling (the first entry of `verifiedBlockedBy`, or `verifier-unavailable` when the verdict itself is null). Null when nothing capped the answer."},"verificationAbstained":{"type":"boolean","description":"The abstention, recorded (2026-09-07). True only for the cannot-measure verdict — `verified: null`, a check that could not run. A verdict of false is a check that ran and said no: a finding, not an abstention. Counting these rows gives the null-verification rate directly."},"verificationState":{"type":"string","enum":["verified","unverified","cannot-measure"],"description":"The same tri-state verdict as `verified` (true/false/null), spelled in words a machine can log or branch on without a lookup table: \"verified\", \"unverified\", or \"cannot-measure\" — the last meaning the check itself could not run, never that nothing was found. Additive; `verified` keeps its boolean-or-null shape unchanged."},"verificationReason":{"type":["string","null"],"description":"The single token naming the pass that failed or could not run, drawn from the same vocabulary as `verifiedBlockedBy` (falling back to \"verifier-flagged\" or \"verifier-unavailable\" when the blocking list is empty). Null exactly when `verificationState` is \"verified\": a passing check has no reason to report. It also carries `subject-untracked` — emitted when nothing in the maintained record names the question's subject, so the answer was withheld rather than written from general knowledge (`servedAs: \"abstained\"`, `citations: []`, `verified: false`). When it reads 'overstated', servedAs is 'evidence-bounded' or 'abstained' — the served text was re-graded before it shipped and contains no sentence above what the sources establish. When it reads 'verifier-unavailable' the verification pass did not run — the call is retried once inside its existing deadline before that state is declared — and the answer is served deterministically: verified null, verificationState 'cannot-measure', servedAs 'evidence-bounded' when the record holds evidence for the question and 'abstained' when it holds none. The same outage returns the same contract on every retry. When it reads 'writer-unavailable' the writing pass itself did not complete — the answer was never synthesized — so nothing was served in its place: servedAs is 'abstained', abstained true, verified null, verificationState 'cannot-measure', and any citations still listed are the evidence the answer would have been written from, named as such in the withholding text rather than offered as a checked basis. A writer outage is never reported as 'verifier-unavailable': no answer existed for the verifier to read."},"recordVersion":{"type":["string","null"],"description":"The version of the maintained record this answer was written against — a 16-hex-character content fingerprint over the truth object the answer cites as its principal receipt. Null when the question matched no published record, which is an honest \"nothing to bind to\" and never a placeholder that would compare equal to a real version later."},"recordBinding":{"type":"string","enum":["fresh","unbound","stale","unknown"],"description":"Whether this answer is still bound to the record state it cites. `fresh` = the record has not moved; `unbound` = no published record covers the question. A served answer whose record HAS moved (`stale`) or whose binding could not be computed (`unknown`) is never redistributed — it is re-synthesised against the record as it stands today, so these two values are visible on stored rows and audit events rather than on a served reply."},"recordTopic":{"type":["string","null"],"description":"The published record slug `recordVersion` is computed over (e.g. \"hormuz\"). Null when nothing bound."},"recordVersionBasis":{"type":["object","null"],"description":"What `recordVersion` is a version OF. The hash is taken over the LAYERED truth object (`sb.truth.v1`) this answer cites as its principal receipt. The generated record registry publishes a `contentHash` over a DIFFERENT object under the SAME 16-hex format, so a caller comparing the two without reading this field would conclude the answer and the record describe different worlds every time. Null when nothing bound.","properties":{"schema":{"type":"string","description":"The record schema the hash covers, e.g. `sb.truth.v1`."},"algorithm":{"type":"string","description":"`sha256-16` — SHA-256, first 16 hex characters."},"over":{"type":"string","description":"URL of the exact object hashed."},"comparableTo":{"type":"string","description":"What this value may and may not be compared against."}},"additionalProperties":true},"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}},"marketReceipt":{"type":"object","description":"The receipt for the market price this answer's prose is entitled to state. ALWAYS present, in one of three states, with the same keys in every state — an absent receipt beside a cited market is a silence a caller cannot act on. The prose, the `markets` array and this object are held to one set of numbers: a sentence stating a figure this object contradicts is removed before the answer is served, the removal is disclosed on `unverified`, and `market-disagreement` appears on `verifiedBlockedBy`.","properties":{"status":{"type":"string","enum":["live","resolved","none"],"description":"`live` — a market is bound and `pricePct` is the number the prose may state. `resolved` — the exact contract settled at the venue; `resolution` is the answer and `pricePct` is the last PRE-resolution capture, never a fresh forecast. `none` — no market is bound to this question, and `reason` says so in words."},"question":{"type":["string","null"],"description":"The bound market's own question — the exact proposition the price is about."},"url":{"type":["string","null"],"description":"The venue page for that contract."},"pricePct":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The implied YES probability this answer is entitled to state, 0–100. Null in the `none` state — never 0, because a price of 0 reads as \"the crowd says no\"."},"source":{"type":["string","null"],"enum":["venue-live","registry-receipt","stored-answer",null],"description":"WHICH BUS the number came off. `venue-live` — read from the venue's public index during this request. `registry-receipt` — Signal Bureau's committed nightly capture of venue receipts, used when a contract has left the venue's live index. `stored-answer` — the price captured with this answer when it was written and replayed with it. These are different claims about how fresh a number is, so they are named rather than blended."},"sourceNote":{"type":["string","null"],"description":"The same fact in plain words, for a human reading the payload."},"seenAt":{"type":["string","null"],"description":"The BUS's own stamp for this price — when it was read or captured, never when this response was assembled."},"resolution":{"type":["string","null"],"description":"The venue's settled outcome. Present in the `resolved` state."},"reason":{"type":["string","null"],"description":"Why there is no price to check, in words. Present in the `none` state, and on a link-only receipt whose price was never recorded."},"lastPreResolutionPct":{"type":["number","null"],"description":"Alias of `pricePct` in the `resolved` state, kept for clients written against the earlier resolved-contract shape. Null in every other state — a live price is not a pre-resolution capture and must not read as one."},"lastSeen":{"type":["string","null"],"description":"Alias of `seenAt`, kept for the same reason."},"arc":{"type":["object","null"],"description":"This same market's movement off Signal Bureau's nightly per-entity series — a SECOND clock on ONE contract, published with its own dates and bus instead of arriving unattributed in the prose. `endpointSupersededBy` is set when the nightly endpoint and the live read differ by a point or more: the capture is history at that point, the live number is what the crowd prices now, and the endpoint stops being sayable as a current price.","properties":{"question":{"type":["string","null"]},"fromPct":{"type":["number","null"]},"toPct":{"type":["number","null"]},"fromDate":{"type":["string","null"]},"toDate":{"type":["string","null"]},"source":{"type":["string","null"]},"sourceNote":{"type":["string","null"]},"endpointSupersededBy":{"type":["number","null"]}},"additionalProperties":true}},"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."}}}}