Somnia Price Feeds

Reading prices

The four read interfaces of Somnia Price Feeds, what each quality field means, and the one case a fresh timestamp does not cover.

The four read interfaces

The feed is split into four interfaces, so each one's ERC-165 id stays stable as the feed grows. The same proxy implements all four.

Interface Keyed by Gives you
IPriceFeed Pair string Spot price, cell age, source age, the tracked-pair registry
IMarkPriceFeed Pair string EMA mark price
IIndexedPriceFeed Stable uint256 index The same data, one SLOAD cheaper
IQualifiedPriceFeed Either Quality flags, validator count, feed liveness, safe reads

Which to use. If you read onchain every block, use the indexed path: resolve index → pair once from the SymbolAdded event, then read by index from then on. Indices are append-only and never reused, so a removal elsewhere can't shift your mapping. If you read occasionally or offchain, the string-keyed path is simpler and costs one extra SLOAD.

Whichever you pick, prefer latestOrRevert and latestByIndexOrRevert over the raw latest (see Read a price safely). Every call and its return values are in the reference.

If you liquidate on mark, require both mark and spot to be fresh, and also reject a mark that diverges from spot by more than your own bound (see the mark hazard).

Quality fields

latestWithQuality(pair) and latestByIndexWithQuality(index) return everything the feed knows about one cell in a single SLOAD. They cost no more than the plain read, because the extra fields live in bits the price lanes don't use:

(price, markPrice, updatedAtMs, sourceUpdatedAtMs, validatorCount, medianControlCount, flags)

sourceUpdatedAtMs is the data's age: when the agent took in the quotes behind the price (the median across validators). updatedAtMs is when the cell was written. It is the agent's ingest time, not the exchange's own timestamp. It is computed as the request's start time minus the agent's reported age, so it errs old and never exceeds updatedAtMs. Consecutive ticks step about a second apart, at millisecond precision. Zero means unknown, never "fresh". It also reads zero when the age exceeds the onchain cap of about 16.4 seconds.

validatorCount is how many validators' responses went into the median, saturating at 7. The platform finalises on a 2-of-3 threshold, so two is the normal steady state and one is possible: at one, a single validator set the price alone. Zero means unknown, not "none": cells written before this field shipped carry no count.

medianControlCount is the minimum number of venues whose combined weight controlled the cross-venue median. It counts control, not venues: the agent takes a weighted median, so one venue holding more than half the total weight decides the price outright, even when several venues were fresh. The weights are fixed per-exchange constants in the agent's config, a hand-set proxy for reliability and liquidity, not a measure of book depth at tick time.

Warning

medianControlCount always reads 0. Don't gate on it. It returns 0 ("unknown") for every pair on every network, and there is no date for that to change: carrying the count onchain would widen the response, and those bytes are paid for on every tick. The field stays on the ABI, so reviving it won't break your integration.

If it is ever populated, read it this way:

  • Zero means unknown, never "no venues". Don't branch on 0.
  • Don't write a >= 3 gate. Control counts are much smaller than venue counts: with the live weights, the three heaviest venues all fresh is control 2, and all seven is 3. A >= 3 gate leaves pairs unpriced almost always, which on a one-second feed means voiding constantly. 1 is the signal to act on: one exchange decided this price, and no other fresh venue could have moved it.

flags is a bitfield covering both published prices; each bit belongs to one of them. Ignore bits you don't recognise, so new flags stay additive.

Bit Constant Price Set today? Meaning
0 FLAG_MARK_RESYNCED (1) mark Yes This tick's mark was written equal to spot instead of blended: there was no earlier mark, or it was older than emaMaxAgeMs. A resynced mark is fresher than a blended one.
1 FLAG_DEVIATION (2) spot No, reserved For a future per-tick deviation flag on spot. It will only ever be a flag, never a rejection: rejecting a large move would freeze the feed during exactly the crash you need it for.

FLAG_MARK_RESYNCED is computed onchain, but it is rare by design. At a one-second cadence with emaMaxAgeMs at five minutes, a pair resyncs only when it is first added or after a gap of several minutes in its own pricing. A long run of flags == 0 is the healthy case, not a dead field.

The mark hazard

updatedAtMs describes the cell, not whether the mark has caught up. After a gap shorter than emaMaxAgeMs, the mark resumes blending from its value before the gap, and takes roughly 1e18 / emaAlpha ticks to converge again. During that time it carries a fresh updatedAtMs and no FLAG_MARK_RESYNCED. No flag covers this. Detect it yourself with a divergence bound: reject a mark that differs from spot by more than you are willing to accept. The smoothing parameters are readable as emaAlpha() and emaMaxAgeMs().

Copied from somnia-oracle's price feed integration guide, until the oracle's docs manifest publishes it.Edit this page

Send feedback

Agents: POST the same to /api/feedback.