---
title: "The Answer Engine: one question in, a grounded, source-checked read out"
method: POST
path: "/api/ask"
---

# The Answer Engine: one question in, a grounded, source-checked read out

`POST /api/ask`

## Request

- Base URL: `https://api.signalbureau.ai`
- URL: `https://api.signalbureau.ai/api/ask`
- Auth: none declared

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 78 live asks in the last 14 days, as of 2026-10-19; 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.

## Request body

- AskRequest
  - `question` string, required: A question about the future. Trimmed; must be at least 5 characters.
  - `context` object[]: 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` boolean: true → return a claim ticket immediately (202) and compute in the background; collect at GET /api/answer/{ticketId}. Default false (synchronous).
  - `fresh` boolean: 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` string: 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` string: Optional. The Watch this question is asked under. Recorded as declared on the answer's `replay` block; default none.

## Response `200`

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.

- AskAnswer
  - `ok` boolean, required
  - `answer` string, required: 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` 'low' | 'medium' | 'high': 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` string[]
  - `topics` string[]: Canonical topics to track.
  - `verified` boolean, nullable: 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` 'maintained-record' | 'recent-identical-ask': 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` string, date-time: 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` object, nullable: What this request did to your meter. Null when no identified key was presented.
    - `charged` boolean: Billed to your account in money. False on a free-allowance key, and false on a repeat inside the published window.
    - `read` object: 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.
      - `charged` boolean: False when no Read was receipted for this request.
      - `reason` string, nullable: 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` string: The rule that spared the receipt — matches the reply's own servedFrom.
      - `of` string, nullable: Permalink id of the answer this repeat was served from.
      - `at` string, nullable: When that answer was produced.
  - `servedAs` 'verified' | 'as-written' | 'evidence-bounded' | 'abstained': 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` boolean: 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` integer, nullable: 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` integer, nullable: 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` object, nullable: 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` string[], nullable: 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` string[]: 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` string, nullable: 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` boolean: 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` 'verified' | 'unverified' | 'cannot-measure': 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` string, nullable: 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` string, nullable: 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` 'fresh' | 'unbound' | 'stale' | 'unknown': 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` string, nullable: The published record slug `recordVersion` is computed over (e.g. "hormuz"). Null when nothing bound.
  - `recordVersionBasis` object, nullable: 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.
    - `schema` string: The record schema the hash covers, e.g. `sb.record.v1`.
    - `schemaAliases` string[]: Earlier tags of the same record schema (today `sb.truth.v1`). Permanent.
    - `algorithm` string: `sha256-16` — SHA-256, first 16 hex characters.
    - `over` string: URL of the exact object hashed.
    - `comparableTo` string: What this value may and may not be compared against.
  - `trendsUsed` integer: How many matched entities carried multi-week trend history.
  - `markets` Market[], nullable: 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.
    - `question` string
    - `impliedPct` number, nullable: The crowd's implied YES probability, 0-100. Null for multi-outcome events (see outcomes).
    - `outcomes` object[]: Top outcomes for multi-outcome events (present only then).
      - `label` string
      - `pct` number
    - `url` string
  - `notRecorded` string[]: On a repeat served from a stored answer: the fields that answer never recorded. Each reads null, never as empty.
  - `marketLinks` object[]
  - `marketReceipt` object: 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`.
    - `status` 'live' | 'resolved' | 'none': `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` string, nullable: The bound market's own question — the exact proposition the price is about.
    - `url` string, nullable: The venue page for that contract.
    - `pricePct` number, nullable: 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` 'venue-live' | 'registry-receipt' | 'stored-answer' | 'null', nullable: 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` string, nullable: The same fact in plain words, for a human reading the payload.
    - `seenAt` string, nullable: The BUS's own stamp for this price — when it was read or captured, never when this response was assembled.
    - `resolution` string, nullable: The venue's settled outcome. Present in the `resolved` state.
    - `reason` string, nullable: 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` number, nullable: 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` string, nullable: Alias of `seenAt`, kept for the same reason.
    - `arc` object, nullable: 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.
      - `question` string, nullable
      - `fromPct` number, nullable
      - `toPct` number, nullable
      - `fromDate` string, nullable
      - `toDate` string, nullable
      - `source` string, nullable
      - `sourceNote` string, nullable
      - `endpointSupersededBy` number, nullable
  - `saved` boolean: Whether the answer was persisted (permalink available).
  - `id` string, nullable: Shareable permalink id (/s/<id>); null if persistence was unavailable.
  - `degraded` boolean: true when the answer was produced on a degraded path (a dependency was unavailable); the reason rides degradedReason.
  - `degradedReason` string: Why the answer was produced on a degraded path.
  - `replay` object: 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.
    - `schema` 'sb.answer-replay.v1', required
    - `state` 'available' | 'partial' | 'unavailable', required
    - `T` string, nullable, required: Knowledge cutoff: the latest build time among the committed bundles read (or the bound record's asOf). Null when not recorded.
    - `rule` object, nullable, required: The pinned rule: the release manifest id and build time of the deploy that ran the answer. Null when not recorded.
      - `releaseId` string
      - `builtAt` string, nullable
      - `source` string
    - `observer` string, required: The declared observer, or "public".
    - `watch` string, nullable, required
    - `declared` object
    - `snapshot` object, required: The committed bundles read (each with sha256 and build time), the bound record version, and the corpus copy's provenance.
    - `scope` string, required: What re-execution reproduces and what is only recovered, in words.
    - `reason` string, nullable, required: Why the state is not available; null when it is.
    - `replayable` string[]
    - `recoveredOnly` string[]
    - `missingInputs` string[]
    - `answeredAt` string, nullable
    - `url` string, nullable: Where this answer can be fetched again (path on this API).
    - `reassess` string

## Other responses

- `400`: Invalid JSON body.
- `402`: 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`: Question missing or shorter than 5 characters.
- `429`: 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.

---

[API](https://skmtc.dev/sigb/apis/signal-bureau-api.md) · [All operations](https://skmtc.dev/sigb/apis/signal-bureau-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sigb/apis/signal-bureau-api/revisions/290c9c6ca0d0?raw)
