Somnia Oracle
Asking questions
Write questions that resolve cleanly: question text, resolution time, answer types, agreement rules, costs and why questions void.
A question is a QuestionDefinition: the question text, the sources that answer it, the set of valid answers, when to resolve, and how much agreement is needed. This page covers how to fill each part in so the question resolves the way you expect. For the call itself, see From Solidity.
| Field | Type | What it is |
|---|---|---|
questionText |
string |
The question, in plain language. Must not be empty. |
sources |
QuestionSource[] |
1 to 20 sources. Each one is one vote. See Sources. |
validAnswers |
ValidAnswers |
The outcomes the question can resolve to: discrete labels or numeric intervals. |
resolutionTime |
uint256 |
Unix seconds. Sources are read at this time, not at submission. |
minAgreement |
uint256 |
How many sources must produce a usable result. From 1 up to the number of sources. |
subcommitteeSize |
uint256 |
Agents queried per Website or JSON source. At least 3. |
subcommitteeThreshold |
uint256 |
How many of those agents must agree for the source to count. At least 2, at most subcommitteeSize. |
Ask about a moment, not a period
The oracle reads every source once, at resolutionTime. It does not watch a value over time. So:
- "What is the price of BTC in USD at 2026-12-01 00:00 UTC?" resolves: it is one reading at one instant.
- "Did Arsenal beat Chelsea on 2026-11-14?" resolves: the final result is fixed by
resolutionTime. - "Did BTC trade above $120,000 at any point in November?" does not resolve as written. Reduce it to one reading first, such as an API that reports the month's high, or a contract that tracks it.
Pick sources that still show the answer at resolutionTime. A live ticker shows the price when it is read; a historical endpoint with the timestamp in its URL, such as a one-minute candle, always shows the price at that minute.
Write the question text
questionText is the prompt the Website agents answer, and the term they search with when a source asks them to search. JSON and Contract sources ignore it, but the moderation and naming committees and the Caliber rating all read it, so write it for every source.
- Name the instant and the time zone. "at 2026-12-01 00:00 UTC", not "on December 1st".
- Name the unit. "in USD", "in degrees Celsius", "in millimetres".
- Name the exact thing. "the closing price of AAPL on NASDAQ", not "Apple's stock".
- Say what counts. "including extra time", "as first published", "the official result".
- Avoid judgement words such as "significant", "popular" or "likely". Agents can only report what the page says.
Pick the resolution time
Set resolutionTime after the answer is published, with some margin: after the final whistle, after the data release, after the candle closes. If the answer isn't on the page yet, the agents can't find it.
A resolutionTime in the past is accepted; the question fires about a second after submission. When many questions share the same second, the trigger can move a few seconds later.
Answer types
validAnswers.answerType is 0 for Numeric or 1 for Discrete. Fill in only the fields for your type; the other list must be empty.
struct ValidAnswers {
AnswerType answerType; // 0 = NumericAnswer, 1 = DiscreteAnswer
string[] discreteOutcomes; // Discrete only
Interval[] numericIntervals; // Numeric only: { int256 low; int256 high; }
uint64 numericDecimals; // Numeric only, at most 18; 0 for Discrete
}
Discrete
List 1 to 64 labels in discreteOutcomes, for example ["Yes", "No"] or ["Home", "Draw", "Away"]. The answer's outcomeIdx is the index of the winning label.
- Website agents are given the list and must pick one of the labels.
- JSON and Contract sources must return a string that matches a label exactly, byte for byte.
"yes"does not match"Yes". - A source that returns anything else doesn't count.
- The winner is the label with the most votes, as long as it has at least
minAgreementvotes and no other label ties it. A tie voids the question.
Numeric
List 1 to 64 intervals in numericIntervals. Each is an inclusive [low, high] range of int256 values, with low <= high, and no two ranges may overlap. The answer's outcomeIdx is the index of the interval that contains the result, and numericValue holds the result itself.
numericDecimalsis the fixed-point scale of the bounds and the result. AtnumericDecimals = 2, the bound10050means 100.50.- Each source declares its own
decimals, and the oracle rescales its value tonumericDecimalsbefore aggregating. Scaling down truncates toward zero. - The result is the median of the values from every usable source. With an even count, it is the mean of the middle two.
- For open-ended ranges, use
type(int256).minas the lowest bound ortype(int256).maxas the highest. - If the median falls outside every interval, the question voids with
NoBucketMatch. Cover the whole range you expect, and make adjacent intervals meet: atnumericDecimals = 0,[0, 99999]and[100000, max]leave no gap.
getOutcomeLabels(questionId) renders the intervals as text: "<= X" and ">= X" for open-ended ranges, the bare value for a single-value range, and "low - high" otherwise, with numericDecimals applied.
Agreement
Agreement works at two levels.
Within a source. Each Website or JSON source is answered by a subcommittee of subcommitteeSize agents. The source counts only if at least subcommitteeThreshold of them succeed: for numbers the source's value is the median of their answers, and for labels at least that many must return the same label. The protocol minimum is a subcommittee of 3 with a threshold of 2 (read minSubcommitteeSize() and minSubcommitteeThreshold() on OracleCore); the maximum size is 32. Contract sources are a single deterministic read with no subcommittee.
Across sources. minAgreement is how many sources must produce a usable result, from 1 up to the number of sources. Below it, the question voids with InsufficientSources. For discrete questions, the winning label also needs at least minAgreement votes.
A good default is three independent sources with minAgreement of 2: one broken page or API can't void the question, and one wrong source can't decide it.
Why questions void
A voided question has voided = true and outcomeIdx = 255 on its Answer. The AnswerPosted event carries the reason.
| Reason | What happened |
|---|---|
InsufficientSources |
Fewer than minAgreement sources produced a usable result. The usual causes are an unreachable URL, a JSON path that doesn't match the response, or a page that doesn't show the answer. |
NoBucketMatch |
The numeric median fell outside every interval. |
NoDiscreteWinner |
Labels tied, or the leading label had fewer than minAgreement votes. |
Expired |
Nobody resolved the question within the void timeout after resolutionTime, and someone called voidExpired. |
Administrative |
The protocol owner force-voided the question. |
To find out why a source failed, open the question in the Somnia Explorer. Each agent request links to every validator's receipt, which records each step (fetch, extract, respond) and any error.
Costs
You pay the whole fee in the native token (SOMI on mainnet, STT on testnet) with submitQuestion. Ask the contract for the live quote with getSubmissionCost(def) and send at least that much. Any excess is refunded in the same transaction.
The fee has these parts. The figures are the current mainnet and testnet settings, with a 3-agent subcommittee:
| Part | Charged | Current cost |
|---|---|---|
| Moderation | Always | 0.24 |
| Display name | Always | 0.24 |
| Website source | Per source | 0.33 (agent platform deposit + 3 × 0.10) |
| JSON source | Per source | 0.12 (agent platform deposit + 3 × 0.03) |
| Contract source | Per source | 0 |
| Reactivity trigger | Always | 5,000,000 gas × (basefee + 10 gwei) × 1.2, about 0.096 at a 6 gwei basefee |
So a question with one JSON source costs about 0.70, one Website source about 0.91, and three JSON sources about 0.94. Larger subcommittees cost more per source. The reactivity part follows the basefee at submission, so quote close to when you submit. The fee isn't refunded if the question voids.
Timing and stuck questions
Contract-only questions resolve in the same transaction as the trigger. Questions with Website or JSON sources resolve once every subcommittee has answered, usually within seconds to a few minutes. Agent requests time out after about 10 minutes, and a timed-out source counts as failed.
If the Reactivity trigger is ever missed, the question stays Scheduled. Anyone can fix that:
triggerMissedSchedule(questionId)onOracleCore, onceresolutionTimehas passed, dispatches the question as the trigger would have.voidExpired(questionId)onOracleCorevoids a question that is still unresolved one hour (voidTimeout()) afterresolutionTime, so no question stays open forever.
The Status page lists questions whose trigger has failed.
Moderation and display names
When you submit, a moderation committee returns an advisory Accepted or Rejected verdict, and a naming committee writes a short display name. Neither affects resolution: a rejected question still resolves. Consumers decide whether to honour the verdict. Read them with getModerationVerdict(questionId) and getName(questionId) on the entry point.
Metadata
submitQuestion(def, metadata) takes an extra bytes argument for your own data, such as a category tag or an image hash. The oracle stores and emits it but never reads it, and it adds nothing to the fee. Read it back with getQuestionMetadata(questionId).