# 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](https://github.com/somnia-chain/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](../invoking-agents/gas-fees#current-per-agent-prices). |
| Explorer | [explorer.somnia.network/agents/9911223344556677889](https://explorer.somnia.network/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](../invoking-agents/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

```solidity
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](../invoking-agents/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

```javascript
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

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

```javascript
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]
});
```

---

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.
