Somnia Agents / Invoking agents

Custom consensus

Advanced: Threshold consensus for agents whose validators are expected to return different values.

Advanced. Most agents work with the default Majority consensus. Read this page if each validator is expected to return a different value.

By default, Somnia Agents uses Majority consensus — the request finalizes when a threshold of validators return byte-identical results. This works well for deterministic agents (math, API lookups with exact values, etc.) but breaks down when each validator is expected to return a different value.

Threshold consensus solves this. Instead of requiring matching results, it finalizes as soon as the threshold number of validators respond successfully — regardless of whether their results agree. Your callback receives all individual results, and you aggregate them however you like.

Consensus types

Type Finalization condition responses[] in callback
Majority (0) threshold validators return the same result Responds with all responses; at least threshold will match on the same .result value
Threshold (1) threshold validators return any successful result All responses (check .status to filter successful ones)

Using Threshold consensus

Call createAdvancedRequest instead of createRequest:

platform.createAdvancedRequest{value: deposit}(
    agentId,
    address(this),
    this.handleResponse.selector,
    payload,
    5,                       // subcommitteeSize
    3,                       // threshold — finalize after 3 of 5 respond
    ConsensusType.Threshold, // don't require matching results
    1 minutes                // timeout
);

The threshold parameter controls how many successful responses are needed before finalization. A typical choice is 3 of 5 — this tolerates up to 2 validator failures while still collecting enough data points for meaningful aggregation.

// Get the required deposit for a custom subcommittee size
uint256 deposit = platform.getAdvancedRequestDeposit(subcommitteeSize);

Example: price oracle (median)

Each validator fetches a price independently. Results may differ slightly. Your contract takes the median to filter outliers.

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

enum ConsensusType { Majority, Threshold }
enum ResponseStatus {
    None,       // 0 - Default zero value (uninitialized storage)
    Pending,    // 1 - Awaiting responses
    Success,    // 2 - Consensus reached normally
    Failed,     // 3 - Validators reported failure
    TimedOut    // 4 - Request timed out
}

struct Response {
    address validator;
    bytes result;
    ResponseStatus status;
    uint256 receipt;
    uint256 timestamp;
    uint256 executionCost;
}

struct Request {
    uint256 id;
    address requester;
    address callbackAddress;
    bytes4 callbackSelector;
    address[] subcommittee;
    Response[] responses;
    uint256 responseCount;
    uint256 failureCount;
    uint256 threshold;
    uint256 createdAt;
    uint256 deadline;
    ResponseStatus status;
    ConsensusType consensusType;
    uint256 remainingBudget;
    uint256 perAgentBudget;
}

interface IAgentRequester {
    function createAdvancedRequest(
        uint256 agentId,
        address callbackAddress,
        bytes4 callbackSelector,
        bytes calldata payload,
        uint256 subcommitteeSize,
        uint256 threshold,
        ConsensusType consensusType,
        uint256 timeout
    ) external payable returns (uint256 requestId);

    function getAdvancedRequestDeposit(uint256 subcommitteeSize) external view returns (uint256);
}

interface IPriceAgent {
    function getPrice(string calldata pair) external returns (uint256);
}

contract MedianPriceOracle {
    IAgentRequester public platform;
    uint256 public constant PRICE_AGENT_ID = 12345; // Replace with actual agent ID from the explorer
    uint256 public constant SUBCOMMITTEE_SIZE = 5;
    uint256 public constant COST_PER_AGENT = 0.0003 ether; // JSON Fetch (see Gas Fees)

    uint256 public latestPrice;

    constructor(address _platform) {
        platform = IAgentRequester(_platform);
    }

    function requestPrice(string calldata pair) external payable returns (uint256) {
        bytes memory payload = abi.encodeWithSelector(
            IPriceAgent.getPrice.selector,
            pair
        );

        // Safe deposit: contract floor + per-agent execution reward.
        uint256 reserve = platform.getAdvancedRequestDeposit(SUBCOMMITTEE_SIZE);
        uint256 reward  = COST_PER_AGENT * SUBCOMMITTEE_SIZE;
        uint256 deposit = reserve + reward;

        return platform.createAdvancedRequest{value: deposit}(
            PRICE_AGENT_ID,
            address(this),
            this.handlePrice.selector,
            payload,
            SUBCOMMITTEE_SIZE,
            3,                      // Finalize after 3 of 5 respond
            ConsensusType.Threshold,
            1 minutes               // Timeout
        );
    }

    function handlePrice(
        uint256 requestId,
        Response[] memory responses,
        ResponseStatus status,
        Request memory details
    ) external {
        require(msg.sender == address(platform), "Only platform");
        if (status != ResponseStatus.Success || responses.length == 0) return;

        // Decode all prices from successful responses and take the median
        uint256 count;
        uint256[] memory prices = new uint256[](responses.length);
        for (uint256 i = 0; i < responses.length; i++) {
            if (responses[i].status == ResponseStatus.Success) {
                prices[count] = abi.decode(responses[i].result, (uint256));
                count++;
            }
        }

        // Resize array to actual count
        assembly { mstore(prices, count) }
        if (count > 0) latestPrice = _median(prices);
    }

    function _median(uint256[] memory values) internal pure returns (uint256) {
        // Insertion sort (fine for small arrays)
        for (uint256 i = 1; i < values.length; i++) {
            uint256 key = values[i];
            uint256 j = i;
            while (j > 0 && values[j - 1] > key) {
                values[j] = values[j - 1];
                j--;
            }
            values[j] = key;
        }
        return values[values.length / 2];
    }

    receive() external payable {}
}

Taking the median of independent price observations naturally filters outliers and manipulation — with 3 of 5 results, the median remains accurate even if one validator returns a garbage value.

Example: random oracle (XOR)

Each validator returns a random uint256. Your contract XORs all values together to produce a combined random number. This is secure as long as at least one responding validator provides a truly random value — even if the others are compromised or predictable, the XOR output remains unpredictable.

interface IRandomAgent {
    function random() external returns (uint256);
}

contract RandomOracle {
    IAgentRequester public platform;
    uint256 public constant RANDOM_AGENT_ID = 67890; // Replace with actual agent ID from the explorer
    uint256 public constant SUBCOMMITTEE_SIZE = 5;
    uint256 public constant COST_PER_AGENT = 0.0003 ether; // adjust to match the agent type (see Gas Fees)

    mapping(uint256 => address) public requesters;

    event RandomGenerated(uint256 indexed requestId, uint256 value);

    constructor(address _platform) {
        platform = IAgentRequester(_platform);
    }

    function requestRandom() external payable returns (uint256 requestId) {
        bytes memory payload = abi.encodeWithSelector(
            IRandomAgent.random.selector
        );

        // Safe deposit: contract floor + per-agent execution reward.
        uint256 reserve = platform.getAdvancedRequestDeposit(SUBCOMMITTEE_SIZE);
        uint256 reward  = COST_PER_AGENT * SUBCOMMITTEE_SIZE;
        uint256 deposit = reserve + reward;

        requestId = platform.createAdvancedRequest{value: deposit}(
            RANDOM_AGENT_ID,
            address(this),
            this.handleRandom.selector,
            payload,
            SUBCOMMITTEE_SIZE,
            3,                       // Finalize after 3 of 5
            ConsensusType.Threshold,
            1 minutes                // Timeout
        );
        requesters[requestId] = msg.sender;
    }

    function handleRandom(
        uint256 requestId,
        Response[] memory responses,
        ResponseStatus status,
        Request memory details
    ) external {
        require(msg.sender == address(platform), "Only platform");
        if (status != ResponseStatus.Success || responses.length == 0) return;

        // XOR all successful values — secure if at least one provider is honest
        uint256 combined = 0;
        for (uint256 i = 0; i < responses.length; i++) {
            if (responses[i].status == ResponseStatus.Success) {
                combined ^= abi.decode(responses[i].result, (uint256));
            }
        }

        emit RandomGenerated(requestId, combined);
    }

    receive() external payable {}
}

When to use which

Use case Consensus Why
Deterministic computation (math, hashing) Majority All validators should return the exact same result
API lookups with exact values Majority Same endpoint, same response
Deterministic LLM inference Majority Fixed seed + temperature 0 produces identical outputs across validators
Price feeds / continuous values Threshold Slight differences expected; median filters outliers
Random number generation Threshold Values must differ; XOR combines them securely
Weather data / sensor readings Threshold Approximate values; average or median for accuracy
Non-deterministic LLM inference Threshold Natural variance in outputs; application decides how to reconcile

Threshold tips

  • A threshold of 3 out of 5 is a good default — it tolerates up to 2 failures while still collecting enough data for aggregation.
  • Higher thresholds (e.g., 4 of 5) give more data points but are more likely to time out if a validator fails.
  • Avoid setting threshold = subcommitteeSize — a single validator failure would prevent finalization entirely.
  • The responses[] array in your callback contains all validator responses — check each responses[i].status to filter for successful ones.
  • getAdvancedRequestDeposit(subSize) returns the operations-reserve floor (minPerAgentDeposit × subSize). That's the minimum msg.value the contract will accept, and it only covers gas/callback/upkeep — the agent pot is msg.value − floor. Deposit more than the floor to actually pay runners for execution work. See Gas fees for the full fund-flow model.
  • Unused funds are rebated to the requester after finalisation.

Next steps

Edit this page