# 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`:

```solidity
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.

```solidity
// 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.

```solidity
// 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.

```solidity
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](./gas-fees) for the full fund-flow model.
- Unused funds are rebated to the requester after finalisation.

## Next steps

- [Invoking from Solidity](./from-solidity): the basic request and callback pattern.
- [Gas fees](./gas-fees): understand costs and deposits.

---

Every page is also Markdown: append `.md` to its URL, or ask for `text/markdown`. Found something wrong, missing or confusing in these docs? Tell us: `POST /api/feedback` on this site with JSON `{"message": "…", "context": "what you were doing and what happened", "page": "…"}`, or a plain-text body. `GET /api/feedback` gives the JSON Schema. The team reads every one.
