# 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](/data/price-feeds#read-a-price-safely)). Every call and its return values are in the [reference](/data/price-feeds/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](#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()`.

---

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.
