Somnia Oracle

From Solidity

Submit questions and consume answers from a contract, or from TypeScript with viem, using the OracleEntryPoint interface.

Everything goes through one contract, OracleEntryPoint, at 0x5D7AA25129FD7a3712fd11e4FaEeE9034c2aC2AB on both mainnet and testnet. You submit a question with the fee, keep the questionId it returns, and read the answer once the question has resolved. There is no callback: answers are pulled.

The interface

Copy these types and this interface into your project. The types match OracleTypes.sol, including the order of enum values and struct fields, which the ABI depends on. The interface is the subset of IOracleEntryPoint most integrations need.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

enum AnswerType { NumericAnswer, DiscreteAnswer }
enum QuestionSourceType { Website, JSON, Contract }
enum QuestionStatus { None, Scheduled, AwaitingAnswer, Resolved, Voided }

struct Interval {
    int256 low;
    int256 high;
}

struct ValidAnswers {
    AnswerType answerType;
    string[] discreteOutcomes;
    Interval[] numericIntervals;
    uint64 numericDecimals;
}

struct QuestionSource {
    QuestionSourceType sourceType;
    bytes params;
}

struct QuestionDefinition {
    string questionText;
    QuestionSource[] sources;
    ValidAnswers validAnswers;
    uint256 resolutionTime;
    uint256 minAgreement;
    uint256 subcommitteeSize;
    uint256 subcommitteeThreshold;
}

struct Answer {
    uint64 timestamp;
    bool voided;
    uint8 outcomeIdx;
    int256 numericValue;
    uint256[] receiptIds;
}

interface IOracleEntryPoint {
    event QuestionSubmitted(uint256 indexed questionId, address indexed creator);

    function submitQuestion(QuestionDefinition calldata def) external payable returns (uint256 questionId);
    function getSubmissionCost(QuestionDefinition calldata def) external view returns (uint256);

    function getStatus(uint256 questionId) external view returns (QuestionStatus);
    function getAnswer(uint256 questionId) external view returns (Answer memory);
    function getOutcome(uint256 questionId)
        external
        view
        returns (uint8 outcomeIdx, string memory label, int256 numericValue, uint256 timestamp);
    function getOutcomeLabels(uint256 questionId) external view returns (string[] memory);
}

The full interface also has submitQuestion(def, metadata), getQuestionSources, getQuestionMetadata, getName, getModerationStatus, getModerationVerdict, getNameStatus and getOutcomeLabel. See Asking questions for what each field of QuestionDefinition means.

Submit from a contract

This contract asks whether it rained in London on a given day, then settles on the answer:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IOracleEntryPoint.sol";

contract RainBet {
    IOracleEntryPoint public constant ORACLE = IOracleEntryPoint(0x5D7AA25129FD7a3712fd11e4FaEeE9034c2aC2AB);

    uint256 public questionId;
    bool public settled;
    bool public rained;

    /// The entry point refunds overpayment to msg.sender, which is this contract.
    receive() external payable {}

    function ask(string calldata date, uint256 resolutionTime) external payable {
        require(questionId == 0, "already asked");

        QuestionSource[] memory sources = new QuestionSource[](1);
        sources[0] = QuestionSource({
            sourceType: QuestionSourceType.Website,
            params: abi.encode("https://www.timeanddate.com/weather/uk/london/historic", false, uint8(0))
        });

        string[] memory outcomes = new string[](2);
        outcomes[0] = "Yes";
        outcomes[1] = "No";

        QuestionDefinition memory def = QuestionDefinition({
            questionText: string.concat("Did it rain in London, UK on ", date, "?"),
            sources: sources,
            validAnswers: ValidAnswers({
                answerType: AnswerType.DiscreteAnswer,
                discreteOutcomes: outcomes,
                numericIntervals: new Interval[](0),
                numericDecimals: 0
            }),
            resolutionTime: resolutionTime,
            minAgreement: 1,
            subcommitteeSize: 3,
            subcommitteeThreshold: 2
        });

        uint256 cost = ORACLE.getSubmissionCost(def);
        require(msg.value >= cost, "send at least the submission cost");
        questionId = ORACLE.submitQuestion{ value: msg.value }(def);
    }

    function settle() external {
        require(!settled, "settled");
        QuestionStatus status = ORACLE.getStatus(questionId);
        require(status == QuestionStatus.Resolved || status == QuestionStatus.Voided, "not final yet");

        Answer memory answer = ORACLE.getAnswer(questionId);
        settled = true;
        if (answer.voided) {
            return; // refund both sides, ask again, etc.
        }
        rained = answer.outcomeIdx == 0; // index into discreteOutcomes: 0 = "Yes"
    }
}

Things to get right:

  • Pay at least getSubmissionCost(def). Less reverts with IncorrectMsgValue. The reactivity part of the fee follows the basefee, so sending a little extra is safer; the excess comes back.
  • Accept refunds. The entry point sends any excess back to msg.sender in the same transaction, and reverts with RefundFailed if that transfer fails. A contract that submits needs a receive function, or must send exactly the cost.
  • Set a generous gas limit. Submitting writes the question and subscribes to Reactivity, which takes a lot of gas: recent mainnet submissions with three to six sources used 19 to 24 million gas. Gas estimation can come in low, so set an explicit limit with headroom, such as 30,000,000. You pay only for the gas used.
  • Invalid definitions revert the whole call, with errors such as InvalidAgreement, TooManySources, OverlappingIntervals, DuplicateSource or EmptyQuestionText, and no fee is taken.

Read the answer

Function Returns Reverts
getStatus(questionId) 0 None, 1 Scheduled, 2 AwaitingAnswer, 3 Resolved, 4 Voided QuestionNotFound for an unknown ID
getAnswer(questionId) The Answer struct Until the question is Resolved or Voided
getOutcome(questionId) (outcomeIdx, label, numericValue, timestamp) Until Resolved, and always for a voided question
getOutcomeLabels(questionId) Every outcome's label, in index order Never, for a known question

The Answer fields:

  • voided: true if the question voided. outcomeIdx is then 255.
  • outcomeIdx: the index of the winning label (Discrete) or interval (Numeric).
  • numericValue: for Numeric, the aggregated median at numericDecimals scale; 0 for Discrete.
  • timestamp: when the answer was posted.
  • receiptIds: one per source, in source order. For Website and JSON sources it is the receipt ID of a successful agent response; 0 for contract sources and for sources with no successful response.

Consuming answers

Because answers are pulled, a contract that depends on one checks the status before reading, as settle() does above. Typical patterns:

  • Settle on demand. Anyone calls a settle function after resolutionTime; it reverts until getStatus is 3 or 4.
  • Handle voids. Decide up front what a void means for your contract: refund, ask again, or fall back to a default. A void is a final state, so a contract that only handles Resolved can get stuck.
  • Check the type of answer. For Numeric questions, compare numericValue yourself if you need more precision than the interval gives.

Off chain, or with Somnia Reactivity on chain, you can react to the answer instead of polling. OracleCore (not the entry point) emits both events in the same transaction:

// Full answer, with the void reason. Its shape may grow in future versions.
event AnswerPosted(
    uint256 indexed questionId,
    uint8 outcomeIdx,
    string outcomeLabel,
    int256 numericValue,
    bool voided,
    VoidReason reason,
    uint256[] receiptIds
);

// Minimal notification whose signature never changes. Subscribe to it, then call getAnswer.
event AnswerFinalized(uint256 indexed questionId);

VoidReason is None, InsufficientSources, NumericOverflow, NoBucketMatch, NoDiscreteWinner, Administrative or Expired, in that order (0 to 6). See Why questions void.

From TypeScript with viem

viem's human-readable ABI understands the structs, so the ABI fits in a few lines:

import { createPublicClient, createWalletClient, custom, encodeAbiParameters, http, parseAbi, parseEventLogs } from "viem"
import { somnia } from "viem/chains"

const ENTRY_POINT = "0x5D7AA25129FD7a3712fd11e4FaEeE9034c2aC2AB"

const oracleAbi = parseAbi([
  "struct Interval { int256 low; int256 high; }",
  "struct ValidAnswers { uint8 answerType; string[] discreteOutcomes; Interval[] numericIntervals; uint64 numericDecimals; }",
  "struct QuestionSource { uint8 sourceType; bytes params; }",
  "struct QuestionDefinition { string questionText; QuestionSource[] sources; ValidAnswers validAnswers; uint256 resolutionTime; uint256 minAgreement; uint256 subcommitteeSize; uint256 subcommitteeThreshold; }",
  "struct Answer { uint64 timestamp; bool voided; uint8 outcomeIdx; int256 numericValue; uint256[] receiptIds; }",
  "function getSubmissionCost(QuestionDefinition def) view returns (uint256)",
  "function submitQuestion(QuestionDefinition def) payable returns (uint256 questionId)",
  "function getStatus(uint256 questionId) view returns (uint8)",
  "function getAnswer(uint256 questionId) view returns (Answer)",
  "function getOutcomeLabels(uint256 questionId) view returns (string[])",
  "event QuestionSubmitted(uint256 indexed questionId, address indexed creator)",
])

const publicClient = createPublicClient({ chain: somnia, transport: http() })
const walletClient = createWalletClient({ chain: somnia, transport: custom(window.ethereum!) })

// Will BTC close above $100,000 in the 00:00 UTC minute on 1 December 2026?
const at = 1796083200 // 2026-12-01T00:00:00Z
const def = {
  questionText: "What is the BTC/USDT price at 2026-12-01 00:00 UTC?",
  sources: [
    {
      sourceType: 1, // JSON
      params: encodeAbiParameters(
        [{ type: "string" }, { type: "string" }, { type: "uint8" }],
        [
          `https://data-api.binance.vision/api/v3/klines?symbol=BTCUSDT&interval=1m&startTime=${at * 1000}&endTime=${at * 1000 + 59_999}&limit=1`,
          "[0][4]",
          2, // keep cents: 97123.45 becomes 9712345
        ],
      ),
    },
  ],
  validAnswers: {
    answerType: 0, // Numeric
    discreteOutcomes: [],
    numericIntervals: [
      { low: -(2n ** 255n), high: 10_000_000n }, // "<= 100000.00"
      { low: 10_000_001n, high: 2n ** 255n - 1n }, // ">= 100000.01"
    ],
    numericDecimals: 2n,
  },
  resolutionTime: BigInt(at + 120), // two minutes after the candle closes
  minAgreement: 1n,
  subcommitteeSize: 3n,
  subcommitteeThreshold: 2n,
}

const cost = await publicClient.readContract({
  address: ENTRY_POINT,
  abi: oracleAbi,
  functionName: "getSubmissionCost",
  args: [def],
})

const [account] = await walletClient.requestAddresses()
const hash = await walletClient.writeContract({
  account,
  address: ENTRY_POINT,
  abi: oracleAbi,
  functionName: "submitQuestion",
  args: [def],
  value: (cost * 11n) / 10n, // 10% headroom; the excess is refunded
  gas: 30_000_000n,
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
const [submitted] = parseEventLogs({ abi: oracleAbi, eventName: "QuestionSubmitted", logs: receipt.logs })
const questionId = submitted.args.questionId

// Later, once the question has resolved:
const status = await publicClient.readContract({ address: ENTRY_POINT, abi: oracleAbi, functionName: "getStatus", args: [questionId] })
if (status === 3 || status === 4) {
  const answer = await publicClient.readContract({ address: ENTRY_POINT, abi: oracleAbi, functionName: "getAnswer", args: [questionId] })
  const labels = await publicClient.readContract({ address: ENTRY_POINT, abi: oracleAbi, functionName: "getOutcomeLabels", args: [questionId] })
  console.log(answer.voided ? "voided" : `${labels[answer.outcomeIdx]} (${answer.numericValue})`)
}

For testnet, use somniaTestnet from viem/chains. The entry point address is the same.

From the command line with cast

Reads work with Foundry's cast:

EP=0x5D7AA25129FD7a3712fd11e4FaEeE9034c2aC2AB
RPC=https://api.infra.mainnet.somnia.network

cast call $EP "getStatus(uint256)(uint8)" 72711 --rpc-url $RPC
cast call $EP "getAnswer(uint256)((uint64,bool,uint8,int256,uint256[]))" 72711 --rpc-url $RPC
cast call $EP "getOutcomeLabels(uint256)(string[])" 72711 --rpc-url $RPC

Without a contract

To see what a question looks like before writing code, start from a template in the Somnia Explorer and submit it from your wallet. Any question can then be read from your contract by its ID.

Edit this page