{"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":"This document covers the REST paths. The MCP door at POST https://api.signalbureau.ai/api/mcp offers 29 tools, listed by tools/list and at https://sigb.ai/machine/. Some of those tools have a REST twin here; the rest are MCP-only, and some paths here (keys, tariff, stats, release) have no tool. Anonymous access works on every read surface — no key required to evaluate — and the Answer Engine answers 25 questions a day per network address with no key (metered on the address only, no cookie, no fingerprint, shared by everyone behind that address, resetting at midnight UTC); past them, ask rides a key. 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), 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 24–31 seconds (p50–p90) over 75 live asks in the last 14 days, as of 2026-10-16; set client timeouts to at least 70 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 state, 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 — a field is never removed, and a renamed field keeps riding beside its successor.\n\nRenamed names: Renamed public names keep their earlier names as PERMANENT aliases — nothing a caller already uses stops answering, and each answer says which name is current. Paths: `GET /api/truth/{topic}` (now `GET /api/record/{topic}`, since 2026-10-01; answers carry `Link: <current path>; rel=canonical` and `x-sigb-current-path`). Fields, still served beside their successors with the same value: `signals` (now `counts.attention.current.documents.count`, tool:get_entity), `verticals` (now `counts.attention.current.verticals.count`, tool:get_entity), `matches[].signals` (now `matches[].counts.attention.current.documents.count`, tool:search_entities), `matches[].verticals` (now `matches[].counts.attention.current.verticals.count`, tool:search_entities), `entities[].signals` (now `entities[].counts.attention.current.documents.count`, tool:top_accelerating), `entities[].verticals` (now `entities[].counts.attention.current.verticals.count`, tool:top_accelerating), `signals[].signalCount` (now `signals[].counts.attention.current.documents.count`, tool:get_signals), `signals[].verticalCount` (now `signals[].counts.attention.current.verticals.count`, tool:get_signals), `clusters[].independentOrigins` (now `clusters[].knownOriginCount`, tool:get_corroborated), `recordState` (now `state`, GET /api/record/{topic}), `maintainedRecord` (now `record`, GET /api/record/{topic}). Schema tags, listed in `schemaAliases`: `sb.truth.v1` (now `sb.record.v1`, since 2026-10-06). MCP tools: `orient` (now `start_here`, since 2026-09-07), `get_truth_object` (now `get_topic_record`, since 2026-09-07), `get_truth_record` (now `get_topic_record`, since 2026-10-01). The whole table is machine-readable under `x-sigb-aliases` at the root of this document.","contact":{"name":"Signal Bureau","url":"https://sigb.ai/machine/"}},"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."}],"x-sigb-aliases":{"policy":"Permanent and forward-only: every earlier public name keeps answering, returns what its current name returns, and says which name is current. Rows are appended, never removed.","tools":{"orient":{"use":"start_here","since":"2026-09-07"},"get_truth_object":{"use":"get_topic_record","since":"2026-09-07"},"get_truth_record":{"use":"get_topic_record","since":"2026-10-01"}},"prompts":{"judge-us-first":{"use":"look-around","since":"2026-10-06"}},"routes":{"/api/truth/{topic}":{"use":"/api/record/{topic}","since":"2026-10-01","operationId":"getTruthObject","useOperationId":"getTopicRecord"}},"fields":[{"door":"tool:get_entity","field":"signals","use":"counts.attention.current.documents.count"},{"door":"tool:get_entity","field":"verticals","use":"counts.attention.current.verticals.count"},{"door":"tool:search_entities","field":"matches[].signals","use":"matches[].counts.attention.current.documents.count"},{"door":"tool:search_entities","field":"matches[].verticals","use":"matches[].counts.attention.current.verticals.count"},{"door":"tool:top_accelerating","field":"entities[].signals","use":"entities[].counts.attention.current.documents.count"},{"door":"tool:top_accelerating","field":"entities[].verticals","use":"entities[].counts.attention.current.verticals.count"},{"door":"tool:get_signals","field":"signals[].signalCount","use":"signals[].counts.attention.current.documents.count"},{"door":"tool:get_signals","field":"signals[].verticalCount","use":"signals[].counts.attention.current.verticals.count"},{"door":"tool:get_corroborated","field":"clusters[].independentOrigins","use":"clusters[].knownOriginCount","note":"Mirrors knownOriginCount (origins whose upstream is known, the count the tier stands on); never larger than it. Before 2026-10-12 it mirrored publisherCount, which could read as more support than the tier stood on."},{"door":"GET /api/record/{topic}","field":"recordState","use":"state","since":"2026-09-09"},{"door":"GET /api/record/{topic}","field":"maintainedRecord","use":"record","since":"2026-09-09"}],"schemas":{"sb.truth.v1":{"use":"sb.record.v1","since":"2026-10-06"}}},"paths":{"/api/v1/watches/standard":{"get":{"operationId":"listStandardWatches","tags":["Watches"],"summary":"The free standard Watches and the published terms for saving one","description":"Lists the standard Watches with every coordinate explicit, plus the terms for saving a Watch (price read from GET /api/tariff, what it includes, its limits). Free, no key. Same content as the MCP tool `list_standard_watches`.","responses":{"200":{"description":"The standard Watches and the saving terms.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."}}}},"/api/v1/watches/unfurl":{"post":{"operationId":"unfurlWatch","tags":["Watches"],"summary":"Configure a Watch and run it once, unsaved","description":"Returns the governed reading (`sb.watch-reading.v1`) plus one line per coordinate stating whether it was applied. Free, no key, nothing saved or charged. Same content as the MCP tool `unfurl_watch`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["base"],"properties":{"base":{"type":"string","description":"The standard Watch to start from; GET /api/v1/watches/standard lists the ids."},"coordinates":{"type":"object","description":"Coordinates to change from the base Watch; each one omitted takes the base value. The reading states, per coordinate, whether it was applied."},"label":{"type":"string","description":"Your own name for the Watch (save only)."},"watchId":{"type":"string","description":"Save only: edit this saved Watch. An edit is a new version at no extra charge."}}}}}},"responses":{"200":{"description":"The reading and the per-coordinate application report.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The body is not JSON or names an unknown base or coordinate; the error names the field."},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."}}}},"/api/v1/watches":{"get":{"operationId":"listSavedWatches","tags":["Watches"],"summary":"Your saved Watches","description":"The newest version of each Watch saved to your key, active and retired. Same content as the MCP tool `list_saved_watches`.","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string"},"description":"Your self-issued key (POST /api/keys, free). `Authorization: Bearer sb_live_...` also works."}],"responses":{"200":{"description":"Your saved Watches.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No key: `{state: \"key-required\", error, howToGetOne}`. Configuring and unfurling need no key; saving, listing, recalling and retiring do."},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."}}},"post":{"operationId":"saveWatch","tags":["Watches"],"summary":"Save a Watch to your key (the paid step)","description":"Saves a Watch into a slot: a paid slot at the published Watch rate per saved Watch per month (GET /api/tariff, units[watch].price, which also serves units[watch].opening_offer), or, while the opening offer is open, an opening slot on a verified email address. Your first five Watches are on us while we open the doors. Save up to 5 Watches at no charge through December 31, 2026, one allowance per verified email address. No card is needed to start. After that a Watch is $20 a month, and only if you choose to keep it. Self-issue a key (POST /api/keys, free, instant), then confirm an email address for it: POST /api/keys/verify with {\"email\"} and your key, and press the button in the email we send. Your next saves go into the opening slots. Paid slots are used first. With no free slot of either kind the answer is 402 with the next step, and nothing is saved or charged. Pass `watchId` to edit a saved Watch: a new version, no extra charge. Same content as the MCP tool `save_watch`. Buying steps: GET /api/tariff, units[watch].how_to_buy.","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string"},"description":"Your self-issued key (POST /api/keys, free). `Authorization: Bearer sb_live_...` also works."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["base"],"properties":{"base":{"type":"string","description":"The standard Watch to start from; GET /api/v1/watches/standard lists the ids."},"coordinates":{"type":"object","description":"Coordinates to change from the base Watch; each one omitted takes the base value. The reading states, per coordinate, whether it was applied."},"label":{"type":"string","description":"Your own name for the Watch (save only)."},"watchId":{"type":"string","description":"Save only: edit this saved Watch. An edit is a new version at no extra charge."}}}}}},"responses":{"200":{"description":"Saved; the body carries the watchId and version.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"bad-request: the body is not a JSON object, or names an unknown base or coordinate; the body names the parameter."},"401":{"description":"No key: `{state: \"key-required\", error, howToGetOne}`. Configuring and unfurling need no key; saving, listing, recalling and retiring do."},"402":{"description":"payment-required: no free paid slot on this key and no free opening slot on its verified email (or the opening offer has closed). Nothing was saved or charged; the body names the next step (create_order item 'watch')."},"403":{"description":"not-yours: the Watch is held by a different key and was not shared with this one; nothing was read, changed or charged."},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."},"503":{"description":"cannot-verify: we could not confirm the Watch is yours (key or Watch store did not answer, or the record names no owner); refused, retry shortly."}}}},"/api/v1/watches/{id}":{"get":{"operationId":"recallWatch","tags":["Watches"],"summary":"Recall a saved Watch: its reading now and what changed since your last delivery","description":"Returns the reading (`sb.watch-reading.v1`) and the change since the last delivery to your key. This call records a delivery, which is what makes the next recall's change exact. Same content as the MCP tool `recall_watch`.","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string"},"description":"Your self-issued key (POST /api/keys, free). `Authorization: Bearer sb_live_...` also works."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The saved Watch's id (sw_...)."},{"name":"expand","in":"query","required":false,"schema":{"type":"string"},"description":"Override the Watch's depth for this call: 0, 1, 2 ... or full."}],"responses":{"200":{"description":"The reading and its change since your last delivery.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No key: `{state: \"key-required\", error, howToGetOne}`. Configuring and unfurling need no key; saving, listing, recalling and retiring do."},"403":{"description":"not-yours: the Watch is held by a different key and was not shared with this one; nothing was read, changed or charged."},"404":{"description":"not-found: `{state: \"not-found\", watchId}` — no Watch with that id is saved to your key."},"410":{"description":"retired: the Watch was retired and is no longer recallable; the body carries its saved view. Its versions and deliveries are kept."},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."},"503":{"description":"cannot-verify: we could not confirm the Watch is yours (key or Watch store did not answer, or the record names no owner); refused, retry shortly."}}},"post":{"operationId":"retireWatch","tags":["Watches"],"summary":"Retire a saved Watch","description":"Body `{\"action\":\"retire\"}`. The Watch stops being recallable and its paid slot is freed. Nothing is deleted: its versions and deliveries are kept. Same content as the MCP tool `retire_watch`.","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string"},"description":"Your self-issued key (POST /api/keys, free). `Authorization: Bearer sb_live_...` also works."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The saved Watch's id (sw_...)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["retire"]}}}}}},"responses":{"200":{"description":"Retired.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"bad-request: `action` is not `retire`; the body names the accepted value."},"401":{"description":"No key: `{state: \"key-required\", error, howToGetOne}`. Configuring and unfurling need no key; saving, listing, recalling and retiring do."},"403":{"description":"not-yours: the Watch is held by a different key and was not shared with this one; nothing was read, changed or charged."},"404":{"description":"not-found: `{state: \"not-found\", watchId}` — no Watch with that id is saved to your key."},"409":{"description":"retired or conflict: the Watch is already retired, or the change conflicts with its current state; the body states the reason."},"429":{"description":"slow-down: past the published burst limit for this door. The `retry-after` header gives the seconds to wait."},"503":{"description":"cannot-verify: we could not confirm the Watch is yours (key or Watch store did not answer, or the record names no owner); refused, retry shortly."}}}},"/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 regime. insufficient_history marks entities too new to have earned a regime. An unknown value is refused with 400 (code invalid_param) and the accepted list — never answered with an empty feed.","schema":{"type":"string","enum":["rising","fading","steady","new","insufficient_history"]}}],"responses":{"200":{"description":"The feed envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalsFeed"},"examples":{"twoSignals":{"summary":"curl -s 'https://api.signalbureau.ai/api/signals?limit=1&direction=rising'","value":{"ok":true,"schema":"sb.signals.v1","asOf":"2026-07-11","generatedAt":"2026-07-11T13:05:00+00:00","generated_by":"Signal Bureau — signals for systems (https://sigb.ai/machine/)","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?"}]}}}}}},"400":{"description":"Unknown `direction` value: {ok:false, code:\"invalid_param\", field, accepted, error}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"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. With no key, ask answers 25 questions a day per network address (metered on the address only — no cookie, no fingerprint — so everyone behind one shared address shares it; resets at midnight UTC), behind an 8 requests/minute per-address burst guard. Every keyless answer carries `allowance` {state, remaining, limit, resetsAt, ownMeter}; `state` is cannot-measure (remaining null, never 0) when the meter could not be read. Once the day's keyless answers are spent the call returns 401 key_required whose FIRST field, `selfIssue`, is the ready-to-run POST /api/keys call (body, curl), followed by `thenRetry` and `keylessAllowance` {limit, used, remaining: 0, resetsAt}. 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 24–31 seconds (p50–p90) over 75 live asks in the last 14 days, as of 2026-10-16; set client timeouts to at least 70 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://api.signalbureau.ai/api/ask -H 'content-type: application/json' -d '{\"question\":\"What is the outlook for Taiwan?\"}'","value":{"question":"What is the outlook for Taiwan?"}}}}}},"responses":{"200":{"description":"The structured answer. `verified` is the source-check verdict: true (every load-bearing claim located in article-level tracked coverage), false (some claims could not be located, or a hard claim's only support was a headline — both are listed in `unverified` and `confidence` is downgraded), or null (verification unavailable). A claim supported only by a headline is flagged \"supported only by a headline\" and caps the verdict: headline-attributed is not source-verified. Served-from-record answers additionally carry servedFrom/answeredAt/servedNote. RESPONSE-SHAPE CHANGE (2026-08-19): answers carry no probability of Signal Bureau's own — the former `probability`, `probabilityRationale`, and `lineage` fields were removed. 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; we serve current state, not forecasts (since 2026-08-19, restated 2026-10-07). 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 (29), in the order `tools/list` serves them, grouped by job: Orient first: start_here; Walk the record: traverse, search_entities, get_entity, get_topic_record, get_events, get_signals, top_accelerating, todays_brief; Trace a claim: ask, get_answer, get_corroborated, get_record, get_calibration; Quote and order: get_quote, create_order, get_order, get_account; Keep a Watch: list_standard_watches, unfurl_watch, save_watch, list_saved_watches, recall_watch, retire_watch, get_watch_feed, get_desk_feed; Feedback and coverage requests: send_feedback, get_feedback_status, propose_topic. Every tool in tools/list carries MCP annotations. Read-only, no side effects (readOnlyHint true): start_here, traverse, search_entities, get_entity, get_topic_record, get_events, get_signals, top_accelerating, todays_brief, get_answer, get_corroborated, get_record, get_calibration, get_order, get_account, list_standard_watches, unfurl_watch, list_saved_watches, get_watch_feed, get_desk_feed, get_feedback_status. Writes state (readOnlyHint false): ask, get_quote, create_order, save_watch, recall_watch, retire_watch, send_feedback, propose_topic; of these, only retire_watch cannot be undone (destructiveHint true). Call `start_here` first, then walk the record with `traverse`. To see how the record fits together, start at an entity or event (e.g. Hormuz) and traverse its maintained relationships; each step returns its relation type, receipts, standing and time. traverse opens one node of the record per call: an entity, a situation, a claim or a relation. Each node carries its standing as five separate records (evidence, admission, disposition, validity, measurement), its receipts with source urls, and its neighbours as counts plus typed ids, with a cursor for the next page. One level per call; follow any id it returns to take the next step. Free, no key. The REST twin is GET /api/v1/node/{kind}/{hex}. A first walk in three calls: (1) Open an entity by name. traverse {\"id\":\"entity:hormuz\"}, or GET /api/v1/node?id=entity:hormuz: the entity, its standing, its receipts (children), and the situations and claims it is named in (relations). (2) Follow a claim. traverse {\"id\":\"<a relations.items[].id whose kind is claim>\"}, or GET /api/v1/node/p/{hex}: the claim in its sources' words, whether it is corroborated, single source or contested, and who carried it. (3) Open its sources. traverse {\"id\":\"<the same claim id>\",\"level\":\"children\"}, or GET /api/v1/node/p/{hex}?level=children: every receipt as an observation with the publisher, the date and the url; open the url to read the source itself. Earlier tool names are PERMANENT aliases, not a deprecation queue — machine callers hold tool names in configs we do not control: `orient` (now `start_here`, since 2026-09-07), `get_truth_object` (now `get_topic_record`, since 2026-09-07), `get_truth_record` (now `get_topic_record`, since 2026-10-01). Each keeps answering, returns the current tool's work, and carries a `deprecated` marker naming the current name. Earlier prompt names answer the same way: `judge-us-first` (now `look-around`, since 2026-10-06), naming the current name in the reply's `_meta.alias`. An alias never reshapes a payload underneath a caller who never asked for the new name. Prompts (3): morning-read, look-around, 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. Every JSON-RPC error carries a machine-readable `error.data.code` (parse_error, invalid_request, method_not_found, invalid_params, rate_limited, cannot_measure, data_unavailable, internal_error, or the upstream REST code such as key_required) beside the human sentence in `error.message`; rate limits add `error.data.retryAfter` and a Retry-After header. A deploy whose data could not be read answers every method with -32000 / `data.state: \"cannot-measure\"` (HTTP 503) rather than an empty result. JSON-RPC batches are refused with -32600.","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: 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. Plan ONE request per report. Caps: summary 4,000 characters (overflow moves to detail), detail 40,000 characters, the report's text 64,000 bytes, up to 5 text attachments (text/markdown, text/plain, application/json) of 64,000 bytes each stored with the report — an attachment over its cap refuses the request with the reason named, never cut. A report larger than that is split: later parts carry continues (part 1's feedbackId) and part/of, and the parts merge into one work order. reporter (product, developer, model, version — any may be \"unexposed\") labels the work order by product. 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. Every receipt carries stored (sent vs stored character counts, sha256 of the stored detail, each attachment's bytes and sha256), resend (the exact instruction when anything did not fit) and workOrder (linked at the next sweep, 07:46 / 19:46 UTC). 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)"},"reporter":{"type":"object","description":"Who is reporting (optional; every field optional, any may be \"unexposed\")","properties":{"product":{"type":"string","maxLength":100},"developer":{"type":"string","maxLength":100},"model":{"type":"string","maxLength":100},"version":{"type":"string","maxLength":100}}},"attachments":{"type":"array","maxItems":5,"description":"Up to 5 text files stored with the report, each at most 64,000 bytes of UTF-8 content (optional). One over its cap or of another type refuses the request with the reason named.","items":{"type":"object","required":["content"],"properties":{"name":{"type":"string"},"mimeType":{"type":"string","enum":["text/markdown","text/plain","application/json"]},"content":{"type":"string","description":"The file's text (not base64)"}}}},"continues":{"type":"string","format":"uuid","description":"Part 1's feedbackId, when this request carries a later part of a split report (optional)"},"part":{"type":"integer","minimum":1,"maximum":50,"description":"This part's number (optional)"},"of":{"type":"integer","minimum":1,"maximum":50,"description":"How many parts the report has (optional)"}}}}}},"responses":{"200":{"description":"{ok, feedbackId, recorded:true, truncated, movedToDetail, notFiled, stored, resend, reporter, workOrder, note}"},"413":{"description":"Too large: the report's text over 64,000 bytes, the whole request over its ceiling, or an attachment over 64,000 bytes (code attachment_too_large, naming the attachment). Nothing was stored."},"422":{"description":"Missing/invalid kind or summary, or an attachment of an unaccepted type or shape, or more than 5 (the code and message name which)."},"429":{"description":"Faster than the stated limits."},"503":{"description":"recorded:false — store briefly down; retry rather than silent loss."}}}},"/api/record/{topic}":{"get":{"operationId":"getTopicRecord","summary":"One maintained record: layered, source-backed current state for a maintained topic, with what sources corroborate, contradict and answer differently","description":"Serves the maintained record for a topic — the CANONICAL address since 2026-10-01; `GET /api/truth/{topic}` is a permanent alias that returns the identical body (one shared handler, one rate-limit bucket) — 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.record.v1` plus `subject` and `asOf`, and `schemaAliases` lists the earlier tag `sb.truth.v1`, permanently; 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 record 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 maintained record for a published topic.","content":{"application/json":{"schema":{"type":"object","required":["schema","subject","asOf"],"properties":{"schema":{"type":"string","description":"Envelope version tag: sb.record.v1. Additive changes only within a version."},"schemaAliases":{"type":"array","items":{"type":"string"},"description":"Every earlier tag this same object answered to (today `sb.truth.v1`). Permanent and append-only: a reader holding an earlier tag finds it here."},"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 record 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/truth/{topic}":{"get":{"operationId":"getTruthObject","summary":"One public topic record: layered, source-backed current state for a maintained topic (earlier address, still answers)","description":"Permanent alias of `GET /api/record/{topic}` (the canonical address since 2026-10-01): the same handler, the identical body, kept answering forever because callers hold this path in configs we do not control. Serves the public topic 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.record.v1` plus `subject` and `asOf`, and `schemaAliases` lists the earlier tag `sb.truth.v1`, permanently; 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 record 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 topic record for a published topic.","content":{"application/json":{"schema":{"type":"object","required":["schema","subject","asOf"],"properties":{"schema":{"type":"string","description":"Envelope version tag: sb.record.v1. Additive changes only within a version."},"schemaAliases":{"type":"array","items":{"type":"string"},"description":"Every earlier tag this same object answered to (today `sb.truth.v1`). Permanent and append-only: a reader holding an earlier tag finds it here."},"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 topic record 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/v1/node/{kind}/{hex}":{"get":{"operationId":"getNode","summary":"Open one node of the maintained record (traversal, sb.node.v1)","description":"To see how the record fits together, start at an entity or event (e.g. Hormuz) and traverse its maintained relationships; each step returns its relation type, receipts, standing and time. Walk the maintained record node to node. Returns ONE node in schema `sb.node.v1` — an entity (`e`), a situation or corroborated claim (`p`), or a co-mention relation (`r`) — with its identity, five separate standing records, the knowledge cutoff it was read at, provenance (`build`), and its neighbours folded: `children` (observations, each with its source url) and `relations` (situations, claims and co-mentions it takes part in), each as a count plus the first page of typed refs and a cursor. Follow any ref's id, or pass a cursor, to open the next level. Same object as the MCP tool `traverse`. The same door also answers by query at `GET /api/v1/node?id=…` with a full id (`sb:e/…`), a starting point by name (`entity:hormuz`, `event:strait-of-hormuz`) or a cursor alone. Free, no key, CORS-enabled. PUBLISHED LIMITS (also stated in every node's `limits`): one level per call (`depth` must be 1; there is no whole-graph dump); `pageSize` 1-50, default 10; members listed up to 50; 60 calls a minute per caller (runaway-loop protection, not a meter); one knowledge cutoff, the deploy's. THREE STATES, NEVER TWO: a level we read and found empty has `count: 0`; a level we could not read has `count: {state: \"cannot-measure\", reason}`; a level the projection cannot hold has `count: {state: \"not-built\", reason}`. An unknown id is a 404 with code `node_not_found`; a store we could not read is a 503 `cannot_measure` (the node may exist). Version 1 is PROJECTED from today's graphs (`build.plane`), not read from the maintained world state. Claim ids are provisional (a wording change mints a new id); relation ids use the proposed letter `r`. Edge-cached 5 minutes.","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","enum":["e","p","r"]},"description":"The id's kind letter: sb:e/… → e."},{"name":"hex","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{12,64}$"},"description":"The id's hex. Legacy 12-hex entity ids resolve to their 24-hex successor."},{"name":"level","in":"query","required":false,"schema":{"type":"string","enum":["node","children","relations"]},"description":"node (default) or one level to page."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"A cursor from a previous response, exactly as returned."},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50},"description":"Items per level (default 10)."},{"name":"edgeKind","in":"query","required":false,"schema":{"type":"string","enum":["event","claim","co-mentioned"]},"description":"Only relations of this kind."},{"name":"depth","in":"query","required":false,"schema":{"type":"integer","enum":[1]},"description":"Must be 1; anything else is refused with depth_limit."},{"name":"asOf","in":"query","required":false,"schema":{"type":"string"},"description":"Only the deploy's knowledge cutoff is served; another value is refused with as_of_unavailable."}],"responses":{"200":{"description":"The node.","content":{"application/json":{"schema":{"type":"object","description":"sb.node.v1. Full JSON Schema: lib/schemas/sb.node.v1.json in the repository.","required":["schema","id","kind","label","asOf","lens","build","standing","up","children","relations","edge","actions","links","limits"],"properties":{"schema":{"type":"string","enum":["sb.node.v1"]},"id":{"type":"string","pattern":"^sb:[a-z]/[0-9a-f]{12,64}$"},"kind":{"type":"string","enum":["entity","relation","event","claim","observation","source","watch","reading"]},"grain":{"type":"string","enum":["occurrence","situation"]},"label":{"type":"object","properties":{"text":{"type":"string"},"locale":{"type":"string"}}},"identity":{"type":"object","description":"How the id was minted (anchor) and how strong it is."},"asOf":{"type":"object","properties":{"knowledgeCutoff":{"type":"string"},"snapshot":{"type":"string"}},"description":"The one knowledge cutoff this node was read at. Every cursor carries it."},"lens":{"type":"object","properties":{"kind":{"type":"string","enum":["public","watch"]}}},"build":{"type":"object","properties":{"plane":{"type":"string","enum":["projected","maintained"]},"builtFrom":{"type":"array","items":{"type":"string"}},"rule":{"type":"string"}}},"standing":{"type":"object","description":"Five separate records: evidence, admission, disposition, validity, measurement. Any may be {state, reason}."},"dimensions":{"type":"object"},"members":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string"},"position":{"type":"integer"},"id":{"type":"string"},"label":{"type":"string"},"resolved":{"type":"boolean"}}}},"receipt":{"type":"object"},"up":{"type":"array","items":{"type":"object","required":["id","kind","label"],"properties":{"id":{"type":"string"},"kind":{"type":"string"},"label":{"type":"string"},"role":{"type":"string"},"relationKind":{"type":"string"},"members":{"type":"integer"},"url":{"type":"string","description":"Observation refs: the source document."}}}},"upNote":{"type":"string"},"children":{"type":"object","required":["count"],"properties":{"count":{"description":"An integer, or {state, reason} when the level could not be read (cannot-measure), was never measured (unknown) or needs the maintained world state (not-built). Never a zero standing in for blindness.","oneOf":[{"type":"integer","minimum":0},{"type":"object","required":["state","reason"],"properties":{"state":{"type":"string","enum":["unknown","cannot-measure","not-applicable","not-built"]},"reason":{"type":"string"}}}]},"byKind":{"type":"object","additionalProperties":{"type":"integer"}},"items":{"type":"array","items":{"type":"object","required":["id","kind","label"],"properties":{"id":{"type":"string"},"kind":{"type":"string"},"label":{"type":"string"},"role":{"type":"string"},"relationKind":{"type":"string"},"members":{"type":"integer"},"url":{"type":"string","description":"Observation refs: the source document."}}}},"cursor":{"type":["string","null"],"description":"Opaque. Pass it back (with no other paging params) for the next page of this level. null when the level is complete."},"order":{"type":"string"}}},"relations":{"type":"object","required":["count"],"properties":{"count":{"description":"An integer, or {state, reason} when the level could not be read (cannot-measure), was never measured (unknown) or needs the maintained world state (not-built). Never a zero standing in for blindness.","oneOf":[{"type":"integer","minimum":0},{"type":"object","required":["state","reason"],"properties":{"state":{"type":"string","enum":["unknown","cannot-measure","not-applicable","not-built"]},"reason":{"type":"string"}}}]},"byKind":{"type":"object","additionalProperties":{"type":"integer"}},"items":{"type":"array","items":{"type":"object","required":["id","kind","label"],"properties":{"id":{"type":"string"},"kind":{"type":"string"},"label":{"type":"string"},"role":{"type":"string"},"relationKind":{"type":"string"},"members":{"type":"integer"},"url":{"type":"string","description":"Observation refs: the source document."}}}},"cursor":{"type":["string","null"],"description":"Opaque. Pass it back (with no other paging params) for the next page of this level. null when the level is complete."},"order":{"type":"string"}}},"edge":{"type":"object","properties":{"state":{"type":"string","enum":["continues","source","observation","cannot-see","withheld","not-built"]},"reason":{"type":"string"},"url":{"type":"string"}}},"links":{"type":"object","properties":{"rest":{"type":"string"},"mcp":{"type":"string"},"page":{"type":["string","null"]}}},"limits":{"type":"object","properties":{"levelsPerCall":{"type":"integer","enum":[1]},"pageSize":{"type":"integer"},"maxPageSize":{"type":"integer"},"membersMax":{"type":"integer"},"statement":{"type":"string"}}},"actions":{"type":"array","description":"The control panel: 14 verbs, each lit (with the next call) or dim (with the reason). Every verb only reads or declares a Watch.","items":{"type":"object","properties":{"verb":{"type":"string"},"lit":{"type":"boolean"},"effect":{"type":"string","enum":["read","watch-declaration"]},"layer":{"type":"string"},"call":{"type":"string"},"reason":{"type":"string"}}}}}}}}},"400":{"description":"bad_id, bad_param, depth_limit, page_size_limit or bad_cursor — the message names the parameter and the accepted values.","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}},"404":{"description":"node_not_found: the id is well-formed and the store was read, and no such node exists at this cutoff.","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}},"409":{"description":"cursor_stale or as_of_unavailable: the walk would mix two knowledge cutoffs.","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}},"429":{"description":"slow_down: more than 60 calls in a minute from one caller; retry-after header set.","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}},"501":{"description":"kind_not_served: a valid id letter this version does not open (observations are leaves listed with their url; Watches wait on a ruling).","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}},"503":{"description":"cannot_measure: a store needed to answer could not be read; the node may exist. Retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["schema","error"],"properties":{"schema":{"type":"string","enum":["sb.node-error.v1"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_id","bad_param","depth_limit","page_size_limit","bad_cursor","node_not_found","kind_not_served","cursor_stale","as_of_unavailable","cannot_measure","slow_down"]},"message":{"type":"string"},"id":{"type":"string"},"limit":{"type":"integer"},"served":{"type":"string"},"issuedAt":{"type":"string"}}}}}}}}}}},"/api/corroborated":{"get":{"operationId":"getCorroborated","summary":"What several separate origins carried in the published window","description":"The corroboration layer of the maintained record: events in the published window that publishers carried from more than one separate origin. Free, no key, CORS-enabled (`access-control-allow-origin: *`). Each entry carries the claim as its sources state it, the corroboration tier, two exact counts — `publisherCount` (distinct publishers that carried it, after wire copies, republished copies and same-portal duplicates are collapsed) and `originCount` (separate origins left after publishers behind one address, and publishers relaying one named upstream, are collapsed too) — the publishers by name with wires marked, the number of republished copies collapsed, the entities, and the earliest report date when the producing run carries one. The tier is computed from `knownOriginCount` (`originCount` minus `originsUnknownUpstream`): `strong` = carried by publishers from 4 or more separate origins, `moderate` = from 2 or 3. NEITHER COUNT IS EVIDENTIARY INDEPENDENCE: an outlet that names no upstream still counts as its own origin (`originsUnknownUpstream` says how many could not be checked), and many outlets working from one official statement is wide carriage, not verification. The `method` object states the counting rules (`publisher_rule`, `origin_rule`, `aggregator_rule`) and the engine's own words verbatim under `engineMethod`. `knownOriginCount` is the separate origins whose upstream is known, the count the tier stands on. DEPRECATED ALIAS: `independentOrigins` mirrors `knownOriginCount` (identical value, null when the origins were not measured), kept so existing readers do not break and never larger than the count the tier stands on; it is not dependency-aware evidentiary independence — read `publisherCount`, `originCount` and `knownOriginCount`. NO PROBABILITY HERE IS OURS: prediction-market prices appear only where the match was claim-direction verified, and are a cited external sensor; we serve current state, not forecasts. 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 counting rules: `publisher_rule` (what publisherCount counts), `origin_rule` (what originCount counts and how the tier is derived from it), `aggregator_rule` (portals), `caveat`, and the engine's own block verbatim under `engineMethod`. `independence_rule` is a DEPRECATED alias of `publisher_rule` with identical text, kept for existing readers; despite its name it describes a publisher count, not evidentiary independence."},"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, meanings and count rules. Labels describe what was measured (carried by publishers from N separate origins); none asserts independent verification.","items":{"type":"object"}},"clusters":{"type":"array","description":"The published events, ranked by tier then 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 (`strong` | `moderate`), a reader label that describes what was measured, meaning, rank and countRule. Computed from originCount; never a claim of independent verification."},"publisherCount":{"type":"integer","description":"Distinct publishers that carried this event, after wire copies, republished copies and same-portal duplicates are collapsed. Not a measure of evidentiary independence."},"originCount":{"type":["integer","null"],"description":"Separate origins after publishers behind one canonical address, and publishers relaying one named upstream, are collapsed into one (rule origins@1; three outlets relaying what one named outlet reported count once). The tier is computed from this number. Not dependency-aware evidentiary independence: an outlet that names no upstream still counts as its own origin. Null only on a row from a shard older than the rule."},"originsUnknownUpstream":{"type":["integer","null"],"description":"How many of the counted origins had no readable text beyond a headline, so whether they relay someone else could not be checked. Null when the origin pass did not run."},"originState":{"type":"string","description":"`measured` when originCount was computed from origin chains, otherwise the state that explains why it was not."},"knownOriginCount":{"type":["integer","null"],"description":"Separate origins whose upstream is KNOWN: originCount minus originsUnknownUpstream. The tier stands on this count; origins whose text could not be read are shown in originCount and never counted. Null when the origin chains were not measured (originCount is then only a ceiling)."},"independentOrigins":{"type":["integer","null"],"deprecated":true,"description":"DEPRECATED ALIAS of knownOriginCount — identical value, kept so existing readers do not break, and never larger than the count the tier stands on. Before 2026-10-12 it carried the publisher count, which could read as more support than the tier stood on. Read knownOriginCount, originCount and publisherCount instead."},"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":"Keyless Answer Engine answers per day per network address (25/day anonymous, with no key, since 2026-10-06, revision keyless-daily-allowance; it was 0 from 2026-08-08 under universal keying). Resets at midnight UTC. A key's own meter is `free_key_allowance`."},"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/sources":{"get":{"operationId":"getSources","summary":"The source roster, by class","description":"The roster the /sources/ page shows, as data: each source class with its sources' names and publisher homepages, plus the domains and regions we cover. `asOf` is the date the roster last changed, not the time of the request. Names and homepages only: feed addresses, per-feed volume and per-source grading are not published (the `policy` field says so). Feed health by lane is served to identified callers at /api/source-health. Free, no key.","responses":{"200":{"description":"The roster (schema `sb.sources.v1`).","content":{"application/json":{"schema":{"type":"object","required":["schema","asOf","classes","domains","regions","policy"],"properties":{"schema":{"type":"string","const":"sb.sources.v1"},"asOf":{"type":"string","format":"date"},"classes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"sources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"homepage":{"type":["string","null"],"format":"uri"}}}}}}},"domains":{"type":"array","items":{"type":"string"}},"regions":{"type":"array","items":{"type":"string"}},"policy":{"type":"string"},"page":{"type":"string"}}}}}}}}},"/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."},"code":{"type":"string","description":"Machine-readable error code (snake_case, e.g. invalid_body, invalid_param, key_required, cannot_measure, rate_limited, internal_error). Present on every error from the public API; the human sentence rides `error` (or `message` on the key_required/draw_ceiling refusals)."},"state":{"type":"string","description":"Present as \"cannot-measure\" when the server could not read what it would need to answer: blindness, never a statement that the thing is absent. Retry after `retry-after`."}}},"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","description":"A discovery input; it may change. canonicalId is the durable identity."},"url":{"type":"string","description":"Entity dossier page."},"trajectory":{"$ref":"#/components/schemas/Trajectory"},"signalCount":{"type":["integer","null"],"deprecated":true,"description":"Deprecated alias of counts.attention.current.documents.count (kept for existing callers): article signals the tracked feeds carried on the detection day."},"verticalCount":{"type":["integer","null"],"deprecated":true,"description":"Deprecated alias of counts.attention.current.domains.count (kept for existing callers): coverage verticals those signals came from on the detection day."},"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)."},"canonicalId":{"type":["string","null"],"pattern":"^sb:e/[0-9a-f]{12,64}$","description":"The durable identity: the entity graph id the node door serves (GET /api/v1/node/e/{hex}, MCP traverse). Null with canonicalIdState when the graph does not carry the subject."},"canonicalIdState":{"$ref":"#/components/schemas/CannotMeasure"},"aliases":{"type":"array","items":{"type":"string"},"description":"Curated, unambiguous shorthand that resolves to this subject in search; empty means none is curated."},"aliasesBasis":{"type":"string"},"addresses":{"type":"object","description":"Where the subject lives on each door.","properties":{"page":{"type":"string"},"rest":{"type":["string","null"],"description":"The node-door URL; null without a canonicalId."},"mcp":{"type":"object","description":"{tool:'get_entity', arguments:{name}}"},"traverse":{"type":"object","description":"{tool:'traverse', arguments:{id: canonicalId}}; present with a canonicalId."}}},"identityNote":{"type":"string","description":"slug is a discovery input and may change; canonicalId (sb:e/...) is the durable identity."},"counts":{"$ref":"#/components/schemas/EntityCounts"}}},"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."},"observer":{"type":"string","maxLength":80,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,79}$","description":"Optional. The observer this question is asked as (the O of Time, Observer, Watch, Rule). Recorded as declared on the answer's `replay` block; default \"public\". A malformed value is named in `replay.declared.rejected`, never applied."},"watch":{"type":"string","maxLength":80,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,79}$","description":"Optional. The Watch this question is asked under. Recorded as declared on the answer's `replay` block; default none."}}},"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); 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, confidence: 'low' (no response carries a probability field — answers state no probability of ours). Verifier language reaches a caller only inside the labeled abstention, never spliced into reporting. Independently of the four values, every served factual sentence has passed the sentence-grounding gate: it is carried by one row of the record the writer saw (its key terms inside one source sentence of that row; its numbers, names and dates in that row), or it was removed whole — see `sentenceGrounding`. A verifier verdict never lets a sentence through on its own."},"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\"."},"sentencesServed":{"type":["integer","null"],"description":"How many sentences of `answer` were served after the sentence-grounding gate. Null when the gate did not grade this prose (a house withholding, or no answer was written) — never a zero that reads as graded."},"sentencesDropped":{"type":["integer","null"],"description":"How many sentences of the drafted prose the sentence-grounding gate removed because no row of the grounding record carried them. The removed text is never published; the count is disclosed here and in words on `unverified`. Null when the gate did not grade this prose."},"sentenceGrounding":{"type":["object","null"],"description":"The sentence-grounding gate's report (schema `sb.sentence-grounding.v1`). `state` is 'grounded' (at least one factual sentence carried by a row), 'absence-only' (no factual sentence carried; only verified absence sentences served), 'ungrounded' (rows existed, none carried the prose: the answer abstained), 'cannot-measure' (no rows to check against, or the gate could not run: the answer abstained), 'not-graded' (the prose was already a house withholding or was never written; `reason` says which) or, on a replay of a stored answer, 'not-recorded' when the row predates the gate. `sentences[]` lists each served sentence with `factual` and `rows` — the grounding row ids it stands on: a source URL, or `ctx:<kind>:<hash>` for a line of the record the writer saw (market, trend, entity, record, corpus, headline). Every factual entry carries at least one row id. `droppedReasons` counts removals by the closest failure: number, name, terms (a row held at least half the key terms the test needed: a paraphrase near miss), uncited (no row came near), absence-contradicted, and dangling (a sentence opening with a continuation or anaphoric opener from a closed list, such as 'adds that', 'also', 'it', 'however', 'the company', removed because the sentence before it in the same paragraph was removed). `abstainReason` says why an abstained answer was withheld: 'no-factual-sentence' (no factual sentence survived; a non-factual remainder is never served alone), 'framing-only' (the only factual sentences left described our own evidence, with no number, name or date) or 'no-rows'; null when served. `citationsReconciled` reports how the response's `citations` were matched to the served sentences after the gate (state 'reconciled' with served/added/removed counts; every served citation is a row a served sentence stands on), or null when not applicable. `verificationScope` says which text `verified` was judged on: 'as-served', or 'before-sentence-gate' when the gate removed sentences or re-matched the citations after the verifier read the prose (it is not re-run; `verificationScopeNote` says so in words, and the same sentence rides `unverified`); null when the gate did not grade the prose or the answer was withheld. ABSENCE sentences (a closed list of forms saying the record holds nothing on X: 'the record cannot speak to X', 'we hold no reporting on X', 'none of the sources mention X', 'the coverage we hold is about Y rather than X', ...) are kept only when code verifies the absence: no row contains any of X's key terms, names or numbers (else dropped, absence-contradicted). A kept absence is listed with `kind: \"absence\"` and the `terms` it was checked against, is not factual, and is counted in `absencesServed`. State 'absence-only' means no factual sentence was carried but verified absences were: the answer is served (not abstained) with verified false, verificationReason 'ungrounded' and confidence low. Deterministic: re-run over the stored prose against a bundle whose SHA-256 matches `replay.generation.groundingBundleSha256`, it reproduces every decision."},"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","ungrounded"]},"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. `ungrounded` is contributed by the sentence-grounding gate: no factual sentence of the prose was carried by a row of the grounding record, so the answer was withheld (`servedAs: \"abstained\"`, `verificationReason: \"ungrounded\"`) and `verified` is false — or null when the gate had no rows to check against (`verificationState: \"cannot-measure\"`)."},"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. When it reads 'ungrounded' the sentence-grounding gate found no factual sentence of the drafted prose carried by any row of the grounding record: servedAs is 'abstained', the labeled withholding is served, verified is false and verificationState 'unverified' — or verified null and verificationState 'cannot-measure' when the gate had no rows to check against or could not run."},"recordVersion":{"type":["string","null"],"description":"The version of the maintained record this answer was written against — a 16-hex-character content fingerprint over the topic record 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 topic record (schema tag `sb.record.v1`; the tag itself is not hashed, so the 2026-10-06 rename from `sb.truth.v1` moved no version) 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.record.v1`."},"schemaAliases":{"type":"array","items":{"type":"string"},"description":"Earlier tags of the same record schema (today `sb.truth.v1`). Permanent."},"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","null"],"items":{"$ref":"#/components/schemas/Market"},"description":"Live prediction-market reads joined to the question (max 3). An empty list means none were joined. null means not recorded: a repeat served from an answer stored before markets were kept, listed in notRecorded."},"notRecorded":{"type":"array","items":{"type":"string"},"description":"On a repeat served from a stored answer: the fields that answer never recorded. Each reads null, never as empty."},"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."},"degraded":{"type":"boolean","description":"true when the answer was produced on a degraded path (a dependency was unavailable); the reason rides degradedReason."},"degradedReason":{"type":"string","description":"Why the answer was produced on a degraded path."},"replay":{"type":"object","description":"Replay coordinates for this answer (schema `sb.answer-replay.v1`). `state` is available (re-execution under the recorded rule over the recorded snapshot reproduces it), partial (rule and snapshot recorded, but part of the answer is only recovered: model-written prose is never re-executed, and a live read is not re-read) or unavailable (the rule that produced it was not recorded; `reason` says why). Time, Observer and Watch are declared by the asker; Rule is recorded by the station (the release manifest id). Asking again is a reassessment under the current rule, not a replay. The same object rides POST /api/ask, GET /api/answer/{ticketId} and the MCP ask and get_answer tools.","required":["schema","state","T","rule","observer","watch","snapshot","scope","reason"],"properties":{"schema":{"type":"string","enum":["sb.answer-replay.v1"]},"state":{"type":"string","enum":["available","partial","unavailable"]},"T":{"type":["string","null"],"description":"Knowledge cutoff: the latest build time among the committed bundles read (or the bound record's asOf). Null when not recorded."},"rule":{"type":["object","null"],"description":"The pinned rule: the release manifest id and build time of the deploy that ran the answer. Null when not recorded.","properties":{"releaseId":{"type":"string"},"builtAt":{"type":["string","null"]},"source":{"type":"string"}}},"observer":{"type":"string","description":"The declared observer, or \"public\"."},"watch":{"type":["string","null"]},"declared":{"type":"object"},"snapshot":{"type":"object","description":"The committed bundles read (each with sha256 and build time), the bound record version, and the corpus copy's provenance."},"scope":{"type":"string","description":"What re-execution reproduces and what is only recovered, in words."},"reason":{"type":["string","null"],"description":"Why the state is not available; null when it is."},"replayable":{"type":"array","items":{"type":"string"}},"recoveredOnly":{"type":"array","items":{"type":"string"}},"missingInputs":{"type":"array","items":{"type":"string"}},"answeredAt":{"type":["string","null"]},"url":{"type":["string","null"],"description":"Where this answer can be fetched again (path on this API)."},"reassess":{"type":"string"}}}}},"CannotMeasure":{"type":"object","required":["state","reason"],"properties":{"state":{"type":"string","enum":["cannot-measure"]},"reason":{"type":"string"}}},"CountWindow":{"description":"Either the measured window (start/end, YYYY-MM-DD, inclusive) or cannot-measure with the reason — never a guessed date.","oneOf":[{"type":"object","required":["start","end"],"properties":{"start":{"type":"string"},"end":{"type":"string"}}},{"$ref":"#/components/schemas/CannotMeasure"}]},"CountFact":{"type":"object","required":["count","unit","window","scope"],"description":"One self-describing count. A count not taken is null with state cannot-measure and a reason, never 0.","properties":{"count":{"type":["integer","null"]},"unit":{"type":"string","enum":["documents","domains","publishers"]},"window":{"$ref":"#/components/schemas/CountWindow"},"scope":{"type":"string","enum":["tracked-coverage","retained-corpus"]},"state":{"type":"string","enum":["cannot-measure"]},"reason":{"type":"string"}}},"EntityCounts":{"type":"object","description":"Two count namespaces that answer different questions — never compare or add them.","properties":{"attention":{"type":"object","properties":{"current":{"type":"object","description":"scope tracked-coverage: documents (legacy signalCount) and coverage-vertical domains (legacy verticalCount) on the row's detection day.","properties":{"scope":{"type":"string","enum":["tracked-coverage"]},"window":{"$ref":"#/components/schemas/CountWindow"},"windowBasis":{"type":"string"},"documents":{"$ref":"#/components/schemas/CountFact"},"domains":{"$ref":"#/components/schemas/CountFact"},"definition":{"type":"string"}}}}},"evidence":{"type":"object","properties":{"total":{"type":"object","description":"scope retained-corpus: documents deduped by url and distinct publishers over firstSeen..lastSeen (legacy observed.stories / observed.outlets).","properties":{"scope":{"type":"string","enum":["retained-corpus"]},"window":{"$ref":"#/components/schemas/CountWindow"},"windowBasis":{"type":"string"},"documents":{"$ref":"#/components/schemas/CountFact"},"publishers":{"$ref":"#/components/schemas/CountFact"},"definition":{"type":"string"}}}}},"note":{"type":"string"}}}},"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."}}}}