Somnia Oracle

Sources

The three source types, Website, JSON and Contract, what each one returns and how to encode its params.

A source is where the oracle looks for the answer. Each question has 1 to 20 of them, in any mix of three types, and each source is one vote.

struct QuestionSource {
    QuestionSourceType sourceType; // 0 = Website, 1 = JSON, 2 = Contract
    bytes params;                  // abi.encode(...) of the fields for that type
}
Type sourceType Read by params
Website 0 An agent subcommittee that reads the page with an LLM abi.encode(string url, bool websiteSearch, uint8 decimals)
JSON 1 An agent subcommittee that fetches the API and follows a path abi.encode(string url, string jsonQuery, uint8 decimals)
Contract 2 OracleCore, with a staticcall at resolution time abi.encode(address target, string signature, bytes args, uint8 decimals)

Every type carries decimals: the scale the source's value is expressed at. The oracle rescales each value from the source's decimals to the question's numericDecimals before aggregating. Discrete questions ignore it; pass 0.

Website

A Website source sends the question to the LLM Parse Website agent. Each agent fetches the page, converts it to text and extracts the answer to questionText.

Field Meaning
url The page to read, or a domain to search when websiteSearch is true.
websiteSearch false: read url directly. true: search url's domain using the question text and read the page it finds. Use it when you know the site but not the page, for example espn.com.
decimals The scale of the extracted number.
  • Discrete: agents are given the outcome labels and must pick one.
  • Numeric: agents return a whole, non-negative number. If you need fractions, ask for a whole-number unit in the question text ("in US cents") and set decimals to match (2).
  • Agents only answer when they are at least 70% confident; below that, the agent counts as failed.
bytes memory params = abi.encode("https://www.bbc.com/sport/football/teams/arsenal", false, uint8(0));

JSON

A JSON source sends the URL and a path to the JSON API Request agent. Each agent fetches the URL and returns the value at the path. There is no LLM involved, so results are exact.

Field Meaning
url A JSON endpoint the agents can fetch, with no API key.
jsonQuery A plain dot path into the response: data.price, rates.EUR, data[0].intensity.actual, or [0][4] into nested arrays. It is not JSONPath: a $. prefix fails.
decimals For numeric questions, the agent multiplies the value by 10^decimals, so 97123.45 at decimals = 2 becomes 9712345. Numbers sent as strings, such as "97123.45", work too.
  • Discrete: the value at the path is read as a string and must equal one of the labels exactly.
  • Numeric: negative values are supported.
// Binance's one-minute BTC/USDT candle starting at 2026-12-01 00:00 UTC; [0][4] is its close price.
bytes memory params = abi.encode(
    "https://data-api.binance.vision/api/v3/klines?symbol=BTCUSDT&interval=1m&startTime=1796083200000&endTime=1796083259999&limit=1",
    "[0][4]",
    uint8(2)
);

Prefer endpoints with the time baked into the URL, like the candle above, over live tickers. They return the same answer whenever they're read, which helps agents agree and keeps the question auditable afterwards.

Contract

A Contract source calls a view function on any contract on the same chain, at the moment the question resolves. No agents are involved, so it costs nothing extra and is trusted as-is.

Field Meaning
target The contract to call.
signature The canonical function signature, such as "latestAnswer()" or "balanceOf(address)". Must not be empty. The selector is bytes4(keccak256(signature)).
args The ABI-encoded arguments, without the selector. Empty bytes for no arguments.
decimals The scale of the returned number. For an 8-decimal price feed, 8.

How the return value is read:

  • Numeric: the first 32-byte word, as an int256. Functions that return uint256 or int256 work. For a function that returns a tuple, the first field is used: Chainlink's latestRoundData() would give the round ID, so call latestAnswer() instead.
  • Discrete: the return data must be a single ABI-encoded string that equals one of the labels.
  • A revert, empty return data or a value of implausible magnitude counts as a failed source. The call gets a gas limit of 200,000.
bytes memory params = abi.encode(
    address(0x1234567890123456789012345678901234567890), // your token
    "balanceOf(address)",
    abi.encode(address(0xAbCdEf0123456789aBcDeF0123456789AbCdEf01)), // the holder
    uint8(18)
);

Encoding with viem

The same encodings in TypeScript:

import { encodeAbiParameters, encodeFunctionData, parseAbi } from "viem"

const website = encodeAbiParameters(
  [{ type: "string" }, { type: "bool" }, { type: "uint8" }],
  ["espn.com", true, 0],
)

const json = encodeAbiParameters(
  [{ type: "string" }, { type: "string" }, { type: "uint8" }],
  ["https://api.frankfurter.dev/v1/2026-12-01?base=USD&symbols=EUR", "rates.EUR", 4],
)

// args are the encoded arguments only: encodeFunctionData's output without its 4-byte selector.
const call = encodeFunctionData({
  abi: parseAbi(["function balanceOf(address) view returns (uint256)"]),
  args: ["0xAbCdEf0123456789aBcDeF0123456789AbCdEf01"],
})
const contract = encodeAbiParameters(
  [{ type: "address" }, { type: "string" }, { type: "bytes" }, { type: "uint8" }],
  ["0x1234567890123456789012345678901234567890", "balanceOf(address)", `0x${call.slice(10)}`, 18],
)

Sources are shared

A source's ID is its content hash: uint256(keccak256(abi.encode(sourceType, params))). The same URL, path and decimals used by many questions is one source, with one usage and reliability record. The Sources page in the Somnia Explorer shows that record. Each question still sends its own agent requests; results are never reused between questions.

The same source can't appear twice in one question, so one read can't be counted as two votes.

Edit this page