Somnia Agents / Invoking agents

Invoking from Solidity

Call agents from smart contracts and receive results through callbacks.

This guide shows how to invoke Somnia Agents from smart contracts. Your contracts can call agents and receive responses via callbacks.

Generate code from the explorer

The quickest way to start is from an agent's page in the Somnia Explorer:

  1. Open somnia-platform-explorer.vercel.app/agents and pick the network with the chain selector.
  2. Select an agent from the list.
  3. Find the method you want. Each method shows its inputs and outputs and a generated Solidity snippet.
  4. Copy the snippet into your project.

The generated snippet covers:

  • Agent method encoding (the payload)
  • Sizing the deposit
  • The createRequest call
  • The callback that handles the response

To invoke an agent directly from the browser instead, use the standalone Agent Explorer app at https://agents.somnia.network (mainnet) or https://agents.testnet.somnia.network (testnet).

How it works

Step From → to Call or event
1 Your contract → SomniaAgents createRequest{value: deposit}(agentId, callback, selector, payload)
2 SomniaAgents → validators RequestCreated(..., perAgentBudget, subcommittee) event
3 Validators → SomniaAgents submitResponse(result, receipt, success, executionCost)
4 SomniaAgents → validators Gas refund from the operations reserve
5 SomniaAgents Reaches consensus
6 SomniaAgents → your contract handleResponse(requestId, responses[], status, details)
7 SomniaAgents → committee deposit(subcommittee, [median × subSize])
8 SomniaAgents → your contract Rebate of unused budget
9 SomniaAgents RequestFinalized event

In words:

  1. Your contract calls createRequest() with the agent ID, encoded payload, and a deposit.
  2. The platform splits the deposit into an operations reserve (minPerAgentDeposit × subSize, which funds gas refunds, the callback and upkeep) and an agent reward pot (the rest). perAgentBudget = rewardPot / subSize is emitted in RequestCreated so runners know their cap.
  3. Elected validators execute the agent and submit responses. Each submission is gas-refunded from the operations reserve.
  4. Once consensus is reached, the platform calls your callback, then pays every elected subcommittee member the median of the reported executionCost values via the committee.
  5. Any unused funds are rebated to the requester.

See Gas fees for the full fund flow and details on sizing msg.value.

Platform interface

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;   // escrow remaining at any point in the lifecycle
    uint256 perAgentBudget;    // max each elected member can claim (set at creation)
}

interface IAgentRequester {
    // Events
    event RequestCreated(uint256 indexed requestId, uint256 indexed agentId, uint256 perAgentBudget, bytes payload, address[] subcommittee);
    event RequestFinalized(uint256 indexed requestId, ResponseStatus status);
    event SubcommitteePaid(uint256 indexed requestId, uint256 totalPaid, uint256 perMember);
    event CommitteeDepositFailed(uint256 indexed requestId, uint256 attemptedAmount);

    // Request Creation (payable - sends budget upfront)
    function createRequest(
        uint256 agentId,
        address callbackAddress,
        bytes4 callbackSelector,
        bytes calldata payload
    ) external payable returns (uint256 requestId);

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

    // Query Functions
    function getRequest(uint256 requestId) external view returns (Request memory);
    function hasRequest(uint256 requestId) external view returns (bool);

    // Deposit helpers — query the exact msg.value required
    function getRequestDeposit() external view returns (uint256);
    function getAdvancedRequestDeposit(uint256 subcommitteeSize) external view returns (uint256);
}

interface IAgentRequesterHandler {
    function handleResponse(
        uint256 requestId,
        Response[] memory responses,
        ResponseStatus status,
        Request memory details
    ) external;
}

SomniaAgents / AgentRequester addresses:

Network Address
Mainnet (chain ID 5031) 0x5E5205CF39E766118C01636bED000A54D93163E6
Testnet (chain ID 50312) 0x037Bb9C718F3f7fe5eCBDB0b600D607b52706776

The examples below use the testnet address; swap in the mainnet address for production.

Basic example

// 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;   // escrow remaining at any point in the lifecycle
    uint256 perAgentBudget;    // max each elected member can claim (set at creation)
}

interface IAgentRequester {
    function createRequest(
        uint256 agentId,
        address callbackAddress,
        bytes4 callbackSelector,
        bytes calldata payload
    ) external payable returns (uint256 requestId);

    function getRequestDeposit() external view returns (uint256);
}

interface IJsonApiAgent {
    function fetchUint(string calldata url, string calldata selector, uint8 decimals)
        external returns (uint256);
}

contract PriceOracle {
    IAgentRequester public platform =
        IAgentRequester(0x037Bb9C718F3f7fe5eCBDB0b600D607b52706776);

    uint256 public constant JSON_API_AGENT_ID = 12345678901234567890; // Replace with actual agent ID from the explorer
    uint256 public constant SUBCOMMITTEE_SIZE = 3;             // matches the platform default
    uint256 public constant JSON_FETCH_COST_PER_AGENT = 0.0003 ether; // see Gas Fees → Current Per-Agent Prices

    uint256 public latestPrice;
    mapping(uint256 => bool) public pendingRequests;

    event PriceRequested(uint256 indexed requestId);
    event PriceReceived(uint256 indexed requestId, uint256 price);

    function requestBitcoinPrice() external payable returns (uint256 requestId) {
        // Encode the agent function call
        bytes memory payload = abi.encodeWithSelector(
            IJsonApiAgent.fetchUint.selector,
            "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
            "bitcoin.usd",
            uint8(8)
        );

        // Safe deposit: contract floor + per-agent execution reward.
        // Floor only would satisfy the contract but runners would skip the
        // request because perAgentBudget < scheduledExecutionCost.
        uint256 reserve = platform.getRequestDeposit();                       // = minPerAgentDeposit × subSize
        uint256 reward  = JSON_FETCH_COST_PER_AGENT * SUBCOMMITTEE_SIZE;
        uint256 deposit = reserve + reward;
        requestId = platform.createRequest{value: deposit}(
            JSON_API_AGENT_ID,
            address(this),
            this.handleResponse.selector,
            payload
        );
        pendingRequests[requestId] = true;

        emit PriceRequested(requestId);
    }

    // Callback function - called by the platform when consensus is reached
    function handleResponse(
        uint256 requestId,
        Response[] memory responses,
        ResponseStatus status,
        Request memory details
    ) external {
        require(msg.sender == address(platform), "Only platform can call");
        require(pendingRequests[requestId], "Unknown request");

        delete pendingRequests[requestId];

        if (status == ResponseStatus.Success && responses.length > 0) {
            latestPrice = abi.decode(responses[0].result, (uint256));
            emit PriceReceived(requestId, latestPrice);
        }
    }

    // Allow receiving rebates
    receive() external payable {}
}

Callback function signature

Your callback function must match this signature:

function handleResponse(
    uint256 requestId,
    Response[] memory responses,
    ResponseStatus status,
    Request memory details
) external;
  • requestId: The ID returned from createRequest()
  • responses: Array of Response structs from validators (each contains .result bytes, .status, .validator, etc.)
  • status: The overall request status — one of Success (2), Failed (3), or TimedOut (4)
  • details: The full Request struct with metadata (subcommittee, threshold, deadline, etc.)

You can name the function anything, but the parameter types must match. The selector you pass to createRequest must match your function.

Response status

Status Value Meaning
None 0 Default zero value (uninitialized storage)
Pending 1 Awaiting responses
Success 2 Validators reached consensus
Failed 3 Validators reported failure (success became impossible)
TimedOut 4 Request timed out before consensus was reached

Decoding responses

The responses array contains Response structs from validators. Each response has a .result field with ABI-encoded bytes and a .status field. The callback receives all responses submitted up to finalization. For Majority consensus, at least threshold responses will have identical .result bytes (the consensus value). For Threshold consensus, results may differ.

// Decode from the first successful response
if (status == ResponseStatus.Success && responses.length > 0) {
    // Single uint256
    uint256 value = abi.decode(responses[0].result, (uint256));

    // Single string
    string memory text = abi.decode(responses[0].result, (string));

    // Multiple values
    (uint256 value, string memory message) = abi.decode(responses[0].result, (uint256, string));
}

// Iterate all responses (useful for Threshold consensus)
for (uint256 i = 0; i < responses.length; i++) {
    if (responses[i].status == ResponseStatus.Success) {
        uint256 value = abi.decode(responses[i].result, (uint256));
        // Process each validator's result...
    }
}

Multiple callbacks

Use different callbacks for different response types:

contract MultiAgentConsumer {
    IAgentRequester public platform =
        IAgentRequester(0x037Bb9C718F3f7fe5eCBDB0b600D607b52706776);

    // Request with uint256 response
    function requestPrice() external payable {
        bytes memory payload = abi.encodeWithSelector(...);

        platform.createRequest{value: msg.value}(
            PRICE_AGENT_ID,
            address(this),
            this.handlePriceResponse.selector,
            payload
        );
    }

    // Request with string response
    function requestText() external payable {
        bytes memory payload = abi.encodeWithSelector(...);

        platform.createRequest{value: msg.value}(
            TEXT_AGENT_ID,
            address(this),
            this.handleTextResponse.selector,
            payload
        );
    }

    function handlePriceResponse(
        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) {
            uint256 price = abi.decode(responses[0].result, (uint256));
            // Process price...
        }
    }

    function handleTextResponse(
        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) {
            string memory text = abi.decode(responses[0].result, (string));
            // Process text...
        }
    }

    receive() external payable {}
}

Error handling

Handle all response statuses gracefully:

function handleResponse(
    uint256 requestId,
    Response[] memory responses,
    ResponseStatus status,
    Request memory details
) external {
    require(msg.sender == address(platform), "Only platform");

    if (status == ResponseStatus.Failed) {
        emit AgentFailed(requestId);
        // Validators reported execution failure
        return;
    }

    if (status == ResponseStatus.TimedOut) {
        emit AgentTimedOut(requestId);
        // Request timed out - maybe retry
        return;
    }

    // Success
    if (responses.length == 0) {
        emit EmptyResponse(requestId);
        return;
    }

    // Decode and process response...
}

Security considerations

  1. Always verify the caller: Check that msg.sender is the platform contract
  2. Track pending requests: Use a mapping to validate incoming callbacks
  3. Handle all statuses: Check status — requests can succeed, fail, or time out
  4. Validate responses length: Check responses.length before decoding
  5. Accept rebates: Implement receive() so your contract can receive rebated funds

Next steps

Edit this page