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 − 1price ceiling (about 77M USD per unit at 1e18 scaling) is tighter thangetPrices.
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_computedstep per symbol for the batch functions, or - one
exchange_quote_readstep forgetExchangePrice.
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]
});