Docs

Guide

On-chain oracle

A smart contract asks a question, a Silicon Network model answers it, and the answer comes back on chain with proof of which machine produced it.

Not deployed yet. The contract and the worker that answers are built and tested, but no oracle contract is live on any chain, so there is no address to call. This page describes how it will work; the address will be published here once it is deployed.

What it does

The oracle is a contract on Robinhood Chain (chain id 4663, an Arbitrum Orbit chain with gas in ETH). Your contract calls it with a model, a prompt and a token limit, and pays in ETH. An oracle worker run by Silicon Network picks the request up, runs it through the same API every other caller uses, and posts the answer back to the oracle contract, which calls your contract with it.

Every answer carries a receipt bundle: the gateway's signed verifiable receipt for that inference (which node served it, its operator, region and attestation) together with the exact request and response bytes the receipt hashes. Anyone can check it against the gateway's published keys.

How a request flows

Request lifecycle
StepWhoWhat happens
1. requestyour contractCalls request(model, prompt, maxTokens, callback, callbackGasLimit) with at least quote(model, maxTokens, promptBytes, callbackGasLimit) in ETH. The callback is your contract itself or none. The oracle emits Requested and holds the payment. While no oracle signer is active the request is refused (NoActiveSigner), since nobody could answer it.
2. inferenceoracle workerReads the event, sends the prompt to the gateway asking for a receipt, and checks the receipt before posting anything.
3. fulfilloracle workerCalls fulfill with the answer and the receipt bundle. The oracle checks that the signer is an active oracle signer and that the answer is for exactly this request.
4. callbackoracle contractCalls onSiliconAnswer on your contract with the callback gas limit your request chose and paid for. If your callback reverts, the answer still stands and stays in the Fulfilled event.
refundanyoneA request still unanswered after its deadline (the oracle's timeout, one hour by default) can be refunded with refund(id): the whole payment is credited back to the requester.
rejectoracle workerA request that can never be answered (a prompt URI that does not match its hash, a prompt too large to answer on chain, a prompt host too slow to answer) is rejected before its deadline: a reject fee (the model's base price) is kept and the rest credited back at once; onSiliconRejected tells your contract, with at most 100,000 gas. After the deadline no rejection is possible, only the full refund.

Credited ETH (refunds, rejections minus the reject fee, and anything paid above the price) is withdrawn with claim(to) by the address that is owed it. When your contract is the requester, give it a function that calls claim.

From a contract

Implement the two callbacks, check that msg.sender is the oracle and that the id is an open question your contract asked, and send the quoted price with the request. The oracle only calls back the contract that made the request, so the callback is always address(this); choose its gas (20,000 to 2,000,000), which the price includes. Keep the callback cheap: store a hash or a short prefix and read the full answer from the event.

AskOnce.sol (example)
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;

interface ISiliconOracle {
    function quote(string calldata model, uint32 maxTokens, uint32 promptBytes, uint32 callbackGasLimit)
        external view returns (uint256);
    function request(
        string calldata model,
        string calldata prompt,
        uint32 maxTokens,
        address callback,
        uint32 callbackGasLimit
    ) external payable returns (uint256 id);
    function claim(address payable to) external;
}

interface ISiliconOracleConsumer {
    function onSiliconAnswer(uint256 id, bytes calldata answer, bytes32 answerHash, string calldata answerUri) external;
    function onSiliconRejected(uint256 id, string calldata reason) external;
}

contract AskOnce is ISiliconOracleConsumer {
    uint32 constant CALLBACK_GAS = 200_000;
    ISiliconOracle public immutable oracle;
    mapping(uint256 => bool) public open;
    mapping(uint256 => bytes32) public answerHashes;

    constructor(ISiliconOracle oracle_) {
        oracle = oracle_;
    }

    function ask(string calldata question) external payable returns (uint256 id) {
        // send at least oracle.quote("llama-3.2-3b", 128, uint32(bytes(question).length), CALLBACK_GAS);
        // the callback must be this contract: the oracle only calls back the requester
        id = oracle.request{value: msg.value}("llama-3.2-3b", question, 128, address(this), CALLBACK_GAS);
        open[id] = true;
    }

    function onSiliconAnswer(uint256 id, bytes calldata, bytes32 answerHash, string calldata) external {
        require(msg.sender == address(oracle), "not the oracle");
        require(open[id], "not a question this contract asked");
        open[id] = false;
        answerHashes[id] = answerHash; // the full answer is in the oracle's Fulfilled event
    }

    function onSiliconRejected(uint256 id, string calldata) external {
        require(msg.sender == address(oracle), "not the oracle");
        require(open[id], "not a question this contract asked");
        open[id] = false;
    }
}

A fuller example, with state per request and a credit withdrawal, is ExampleConsumer.sol in the repository, next to the oracle itself.

Without a callback

Pass address(0) as the callback and 0 as its gas, and read the answer from the oracle's Fulfilled(id, signer, answerHash, answer, answerUri, receipt) event instead, off chain. statusOf(id) tells you where a request is: 1 pending, 2 fulfilled, 3 rejected, 4 refunded.

Long prompts

A prompt too large for calldata can be kept off chain: requestWithUri(model, promptHash, promptUri, promptBytes, maxTokens, callback, callbackGasLimit) with an https:// or ipfs:// URI, the sha256 of the prompt bytes and their length, which the price counts. The worker fetches it (uncompressed) and answers only if it is no longer than declared and the hash matches; otherwise the request is rejected, the reject fee kept and the rest credited back.

What it costs

Requests are paid in ETH on Robinhood Chain. The price covers the gas the oracle spends posting the answer and the inference itself, and is set per model by the oracle's owner: a base price per request, plus a price per token of maxTokens, per prompt byte, and per unit of the callback gas you ask for. Read it on chain with quote(model, maxTokens, promptBytes, callbackGasLimit) right before you request; a model the oracle does not offer reverts. The price of a request is fixed when it is made. A rejected request keeps the base price as its reject fee; a request that times out is refunded in full.

The oracle has no connection to SIL. Payment is in ETH only, for the gas and the inference.

Checking an answer

The receipt is carried on chain, not verified there: the gateway signs with ed25519, which the EVM cannot check cheaply. What the contract does check is that the answer came from an active oracle signer and is for exactly the request that was made, not that the answer is right. If your contract moves value on an answer, verify the receipt off chain first, or hold the value behind a dispute window. To check where an answer came from, take the receipt bytes of the Fulfilled event. They are this JSON:

receipt bundle
{
  "v": 1,
  "kind": "silicon.oracle.receipt",
  "chain_id": 4663,
  "oracle": "0x<the oracle contract>",
  "request_id": "1",
  "request_tx": "0x<the transaction that made the request>",
  "gateway_request_id": "<Silicon-Request-Id>",
  "request": "<base64 of the exact JSON body sent to the gateway>",
  "response": "<base64 of the exact JSON body the gateway answered>",
  "receipt": { "v": 1, "node": { ... } | null, "gateway": { ... } }
}

Then:

  1. Verify receipt with the SDKs' receipt verifier against GET https://api.siliconnetwork.io/v1/receipts/keys, passing request and response (decoded) as the request and response bytes. See Verifiable receipts.
  2. Check that request is the question that was asked: it is canonical JSON with the event's model, prompt and maxTokens, temperature 0, for example {"max_tokens":128,"messages":[{"content":"<prompt>","role":"user"}],"model":"llama-3.2-3b","stream":false,"temperature":0}.
  3. Check that the answer on chain is response's choices[0].message.content.
  4. Read the machine from the gateway statement's route (node id, operator, region, attestation) and, when present, the node's own signed receipt.

Receipts are software attestation, as everywhere on the network: the gateway's statement is the platform's, the node's is its operator's. Neither is a hardware root of trust.

Who you trust: the oracle's owner chooses its signers, and a signer could post any bytes as an answer. A signer the owner adds later becomes active only two days after, and never for a request made before that, so adding one cannot reach the payments of requests already made. signers(), isSigner(address) and signerActiveAt(address) show the signers, pending ones included.

Limits

Limits
WhatLimit
Inline promptSet by the owner (8 KB at deployment by default, 64 KB at most)
Prompt by URIhttps:// or ipfs://, public addresses only, uncompressed, 64 KB and at most the size declared
maxTokensPer model, set by the owner
DeadlineThe oracle's timeout after the request (one hour by default)
Callback gasChosen per request, 20,000 to 2,000,000, and priced; at most 100,000 for a rejection
AnswersOne per request, at most once; inline in the transaction

The oracle can be paused by its owner, which stops new requests and answers. Refunds and claims keep working while paused, so ETH held for a request always comes back.

Silicon Network›Docs›Guides›On-chain oracle46 items