Somnia Agents / Base agents

Price Oracle

Median token prices across multiple exchanges, served from a continuously updated cache.

Tracks a configured set of tokens by polling their prices across multiple exchanges via ccxt, holding each exchange's latest price in memory, and serving the median across exchanges on request. No network calls happen while serving a request, and no GPU or LLM is involved.

Property Value
Agent ID 9911223344556677889 (mainnet and testnet)
Version 1.0.0
Author Somnia Team
Per-agent price 0.0003 SOMI (STT on testnet). See Gas fees.
Explorer somnia-platform-explorer.vercel.app/agents/9911223344556677889

Methods

Function Inputs Output Use case
getPrices symbols, decimals uint256[], uint8[], uint64[] Median price, source count and update time per symbol
getPricesPacked symbols, decimals uint256[] Same medians, two symbols per 256-bit word
getPricesSlots symbols, decimals bytes Same medians, one 48-bit slot per symbol
getExchangePrice symbol, exchange, decimals, maxAgeMillis uint256, uint64 One exchange's latest quote, with caller-defined freshness

The three batch functions share the same aggregation, freshness rules, min-source floor and receipts. Only the return encoding differs.

Parameters

Input Type Description
symbols string[] Symbols to price, e.g. BTC/USDC or BTC/USDC:PERP. Outputs are in the same order.
symbol string A single symbol, as above
exchange string A ccxt exchange id this agent polls, e.g. binance
decimals uint8 Prices are scaled by 10^decimals and rounded to the nearest integer
maxAgeMillis uint64 getExchangePrice only: the oldest quote, in milliseconds, the caller will accept

Symbols are ccxt unified pairs. Case and surrounding whitespace don't matter: symbols are upper-cased and exchange ids lower-cased before lookup.

Outputs

Output Type Description
prices uint256[] Weighted median price, scaled by 10^decimals
numSources uint8[] Number of exchanges that contributed to the median
lastUpdated uint64[] Most recent quote time among the contributing exchanges, in unix milliseconds
packed uint256[] getPricesPacked encoding, see below
slots bytes getPricesSlots encoding, see below
price uint256 getExchangePrice: that exchange's latest price, scaled by 10^decimals
lastUpdatedMillis uint64 getExchangePrice: the quote's update time, in unix milliseconds

getPrices

Returns the median price for each symbol in one call, with the output arrays aligned to the input order.

getPrices never fails the whole call. A symbol with no fresh quote, fewer fresh quotes than its min-source floor, or that isn't tracked returns price=0, numSources=0, lastUpdated=0. Check numSources[i] > 0 to tell a real price from a missing one.

getPricesPacked

Packs two symbols per uint256 word. The symbol at position p lives in word p / 2, lane p % 2 (the low lane is even p). The array holds ceil(nSymbols / 2) words. Each 128-bit lane, from low to high bits:

Bits Field
[0..85] price: the weighted median scaled by 10^decimals, in (0, 2^86 − 1]
[86..127] lastUpdatedMs: the most recent contributing quote time (unix ms), masked to 42 bits

The word is lowLane | (highLane << 128). For an odd symbol count the final word's high lane is zero.

A lane of all zeros means "no price". That happens when a symbol has no fresh quote, is below its min-source floor, or its scaled price is 0 or above 2^86 − 1. Decode with:

lane  = (word >> (128 * (p % 2))) & (2**128 - 1)
price = lane & (2**86 - 1)

Note. The 2^86 − 1 price ceiling (about 77M USD per unit at 1e18 scaling) is tighter than getPrices.

getPricesSlots

Returns one bytes blob of tightly packed 48-bit slots, with no ABI element padding. The symbol at position p occupies bytes [6p, 6p+6), big-endian, and the blob is exactly 6 * nSymbols bytes long. Each slot, from low to high bits:

Bits Field
[0..29] mantissa (30 bits)
[30..34] decimalExp (5 bits), 0 to 31
[35..47] sourceAgeMs (13 bits): how many ms the contributing quote trailed the agent's clock at response time. 8191 means unknown.

The price at your requested decimals is mantissa * 10^decimalExp. Worst-case relative quantisation error is about 4.66e-9.

A slot of all zeros means "no price": no fresh quote, below the min-source floor, or a scaled price above 2^86 − 1. The ceiling isn't implied by the field widths, so range-check the reconstructed price yourself.

The age is measured on the agent's clock, not the chain's.

getExchangePrice

Returns one named exchange's latest quote for a symbol. There is no median and no cross-exchange aggregation. The exchange must be one this agent polls.

Freshness is set by the caller: the quote is returned only if it is at most maxAgeMillis old. Otherwise the agent returns an error, and the call reverts. It also errors if the exchange has no quote for the symbol yet, or if the venue's own timestamp shows the quote is older than the agent's maximum source age.

How prices are computed

Weighted median

Each exchange carries a weight. The returned price is the point at which the cumulative weight of the fresh sources, sorted by price, first reaches half the total. A median (not a mean) keeps the result robust to a single bad print. Equal weights reduce to a plain median.

Weights are summed over the exchanges that are fresh at read time. A stale or missing source simply drops out.

Freshness

By default, a quote older than 3 seconds is excluded from the median. A quote is also dropped if the exchange's own timestamp shows it is older than that cap, however recently it arrived. A streamed quote whose connection is still live but quiet can stay in the median for up to 10 seconds by default.

Min-source floor

A median across too few exchanges is easy to manipulate. Each symbol has a minimum number of fresh exchanges (3 by default, lower for some thinly traded tokens). Below the floor, the symbol returns the same zero result as "no price", so consumers pause rather than act on a thin price. The receipt still records the would-be median.

Spot prices and perp composites

  • Spot symbols are BASE/USDC: the weighted median across exchanges' spot USDC markets.
  • Perp composites are BASE/USDC:PERP: the weighted median of the top-of-book mids of each venue's USDC-margined perpetual (BASE/USDC:USDC). They are read through the same four functions.

All prices are quoted in USDC, with no currency conversion step.

Validators can disagree slightly

Each validator answers from its own continuously updated cache, so different validators can return slightly different prices for the same request. Aggregate across responses in your callback (for example, take the median) using Threshold consensus. See Custom consensus.

Exchanges and tokens

The tracked set comes from the agent's shipped configuration and can change between versions.

Exchange Spot weight Perp weight
binance 3 3
okx 2 —
bybit 2 2
kucoin 1 2
gateio 1 —
mexc 1 2
kraken 1 —
hyperliquid — 2
bitget — 2
lighter — 2

Spot symbols (BASE/USDC), by min-source floor:

Floor Tokens
3 BTC, ETH, SOL, XRP, DOGE, BNB, ADA, LINK, AVAX, NEAR, LTC, UNI, XMR, SUI, WLD, TAO, FIL, ENA, ONDO, PEPE, BCH, ARB, OP, INJ, ZRO
2 XLM, PUMP, ASTER, HYPE, ZEC, AAVE, XPL, TRUMP, LIT
1 SOMI, FARTCOIN, TON

Crypto perp composites (BASE/USDC:PERP), by min-source floor:

Floor Tokens
3 BTC, ETH, SOL, XRP, DOGE, BNB, ADA, LINK, AVAX, XLM, NEAR, LTC, UNI, SUI, ZEC, WLD, FIL, AAVE, ENA, ONDO, 1000PEPE, TRUMP, FARTCOIN, BCH
2 XMR, ASTER, HYPE, TAO, XPL
1 PUMP, ZRO, LIT, VVV

PEPE's perp composite is 1000PEPE/USDC:PERP, on the 1000-token contract's scale. Spot stays PEPE/USDC.

Real-world-asset perp composites (BASE/USDC:PERP), sourced from hyperliquid and/or lighter:

Floor Tokens
2 AAPL, NVDA, MSTR, INTC, TSLA, MRVL, LITE, COIN, AMD, HOOD, RKLB, GOOGL, SMSN (Samsung), HYNX (SK Hynix), XAU, XAG, WTI, COPR (copper), PAXG, SPY, QQQ
1 IREN, GLW, SMIC, XIAO (Xiaomi)

Method signatures

function getPrices(string[] symbols, uint8 decimals) returns (uint256[] prices, uint8[] numSources, uint64[] lastUpdated)
function getPricesPacked(string[] symbols, uint8 decimals) returns (uint256[] packed)
function getPricesSlots(string[] symbols, uint8 decimals) returns (bytes slots)
function getExchangePrice(string symbol, string exchange, uint8 decimals, uint64 maxAgeMillis) returns (uint256 price, uint64 lastUpdatedMillis)

Receipts

Every request emits the standard steps (request_received, request_decoded, handler_started, handler_completed, response_encoded), plus:

  • one median_computed step per symbol for the batch functions, or
  • one exchange_quote_read step for getExchangePrice.

A median_computed step records:

Field Meaning
symbol, kind The symbol, and spot or perp
priceKind last or mid: which quantity the deployment reports
medianPrice The unscaled median, recorded even when the floor suppressed it
numSourcesUsed Number of fresh sources included
minSourcesRequired, belowMinSources The symbol's floor, and true when the price was withheld for being below it
medianControlCount The smallest number of exchanges whose combined weight decides the median
staleAfterMs, maxSourceAgeMs The freshness bounds used
sources Every configured exchange, with its price, ages, transport, and included / excludedReason (stale, starved, source-stale or no-cell)
sourceSymbol Perp steps only: the swap market the quotes came from, e.g. BTC/USDC:USDC

An exchange_quote_read step has a single source instead of sources, no medianPrice, and records the caller's maxAgeMillis as staleAfterMs.

See Receipts for how to fetch them.

Example use cases

  • Push price feeds that write a set of token prices on-chain on a schedule
  • Index prices for perp PnL and liquidations, with the min-source floor guarding against thin markets
  • Checking one exchange's quote with a strict freshness bound

Usage examples

Fetch several prices

import { encodeFunctionData } from 'viem';

const abi = [{
  type: 'function',
  name: 'getPrices',
  inputs: [
    { name: 'symbols', type: 'string[]' },
    { name: 'decimals', type: 'uint8' }
  ],
  outputs: [
    { name: 'prices', type: 'uint256[]' },
    { name: 'numSources', type: 'uint8[]' },
    { name: 'lastUpdated', type: 'uint64[]' }
  ]
}];

// BTC and ETH spot medians with 8 decimal places
const calldata = encodeFunctionData({
  abi,
  functionName: 'getPrices',
  args: [['BTC/USDC', 'ETH/USDC'], 8]
});

Decode in a Solidity callback

interface IPriceOracleAgent {
    function getPrices(string[] calldata symbols, uint8 decimals)
        external returns (uint256[] memory prices, uint8[] memory numSources, uint64[] memory lastUpdated);
}

// In handleResponse, for each successful response:
(uint256[] memory prices, uint8[] memory numSources, uint64[] memory lastUpdated) =
    abi.decode(responses[i].result, (uint256[], uint8[], uint64[]));

for (uint256 j = 0; j < prices.length; j++) {
    if (numSources[j] == 0) continue; // no price for this symbol
    // use prices[j] ...
}

Read one exchange

const abi = [{
  type: 'function',
  name: 'getExchangePrice',
  inputs: [
    { name: 'symbol', type: 'string' },
    { name: 'exchange', type: 'string' },
    { name: 'decimals', type: 'uint8' },
    { name: 'maxAgeMillis', type: 'uint64' }
  ],
  outputs: [
    { name: 'price', type: 'uint256' },
    { name: 'lastUpdatedMillis', type: 'uint64' }
  ]
}];

// Binance's BTC/USDC quote, no older than 1 second
const calldata = encodeFunctionData({
  abi,
  functionName: 'getExchangePrice',
  args: ['BTC/USDC', 'binance', 8, 1000n]
});

Edit this page