Somnia Markets / TypeScript SDK
Price feeds
A realtime index price for an asset (BTC, ETH) from the on-chain EMA price
oracle — streamed the same way the order books are: hydrate a snapshot to get
roughly up to speed, then tail live. It's a read-only feed (an index price, not
a tradable market), so there are no orders here — just watch/fetch. The
shared client mechanics (read tiers, the reactive store, React wiring) are in
the engine guide; how the number itself is produced — venue
sourcing, weighted median, quantization, validator consensus, the EMA mark — is
Methodology at the bottom of this page.
The mental model
- A separate service. Prices come from a standalone EMA price-feed indexer
(Hasura GraphQL), not the markets indexer — its own endpoint, its own
WebSocket. So price watches are independent of
watchMarket: their own store, their ownsubscribePricessignal, their own health. - One endpoint, every asset. A single endpoint (
config.priceFeed) serves every tracked asset ("BTC","ETH", …); callers select an asset by symbol (case-insensitive). Discover what's tracked withlistPriceFeeds(). With nopriceFeedconfigured, the price methods throw with guidance. - Snapshot, then live tail. A watch first pulls an HTTP snapshot (current
price and, when requested, recent ticks), then Hasura subscriptions stream updates. The two heal
differently on a reconnect. The current price is a full-state subscription:
every push carries the current row, so re-subscribing is all it needs. The tick
tape is a stream cursored on
requestId— it is seeded from the newest tick in the snapshot and advances on every push, so a reconnect resumes from the last tick delivered rather than replaying the window. Both are handled with backoff internally; neither is anything a caller has to drive. - One socket and batched snapshots per client. Every watched asset on one
endpoint shares one Hasura socket in the client. Each asset keeps its own
current-price subscription and, when requested, tape subscription and cursor. Watches that start in the same tick (for
example
watchPrices, or several hooks that mount together) share snapshot requests of up to 10 assets each, grouped by tape demand. Each tape reader receives up to 200 recent ticks; latest-only readers receive none. A failed snapshot request rejects only the watches it carried. Two clients never share a socket. - Human numbers + exact raw. Every price is a 1e18-scaled integer on the
wire; the structs carry a display
number(price,ema) and the exact 1e18 string inraw— never round-trip money through thenumber. Timestamps are unix seconds (blockTimestampis chain time, monotonic — use it as a series x-axis;observedAtis the oracle's own source time and can drift).
Configuration
import { SomniaMarkets, SOMNIA_TESTNET_PRICE_FEED } from "@somnia-chain/markets-sdk";
const exchange = new SomniaMarkets({
indexerUrl,
chain,
wsRpcUrl,
priceFeed: SOMNIA_TESTNET_PRICE_FEED, // { url, quote } - the deployed dev feed, all assets
// or your own: priceFeed: { url: "https://…/v1/graphql", quote: "USDC" }
});
priceFeed is { url, wsUrl?, quote? }; wsUrl is derived from url
(http→ws, https→wss) when omitted. Live subscriptions use the exchange's
WebSocket selection: the SDK-managed ws transport on Node, the platform
constructor in browsers, or rpcWebSocketConstructor when you set it.
quote is required to watch. The feed indexes several quotes per base
(BTC/USDC, BTC/USDT), so base alone is not a unique feed key. The tick tape
streams on a requestId cursor, and one oracle request prices every symbol in
the tick, so across quotes that cursor is not unique and a batch boundary between
two rows sharing one would advance past the second and lose the tick. Rather than
drop data quietly, watchPrice/watchPrices (and the price hooks) reject an
unpinned feed with a NotConfiguredError naming config.priceFeed.quote.
The one-shot reads (fetchPrice, fetchPriceHistory, fetchPriceOHLCV,
fetchPrices, listPriceFeeds) still work unpinned, where matching every quote
only double-counts a base (duplicate candle buckets, an arbitrary row per asset).
Pin it anyway unless you want that. Both shipped constants are pinned to USDC, so
anything built on SOMNIA_TESTNET_PRICE_FEED or SOMNIA_MAINNET_PRICE_FEED
needs no change.
Reading — live
Direct client.watchPrice and client.watchPrices calls include the tick tape
by default. Pass { ticks: false } for current price and metadata only. Reads
of the tick tape return an empty array until a tick reader acquires it.
Latest/info React hooks (useLivePrice, useLivePriceFeedInfo, useWatchPrice
and useWatchPriceResult) acquire no historical ticks or tape stream.
useLivePriceTicks acquires the tape for its asset. Adding that reader hydrates
only its asset. Removing its last reference releases the tape stream and cached
ticks after 30 seconds, while latest/info readers continue. A remount during
this linger reuses the tape and its cursor. Owner close releases everything
immediately, including pending snapshots.
Current-only and tape snapshots batch separately. A tape request failure does not reject a simultaneous current-only acquisition. A tape upgrade also keeps the current-price stream and its newer metadata intact.
const watch = await client.watchPrice("BTC"); // snapshot + subscribe; ref-counted
const now = client.getLivePrice("BTC"); // LivePrice | null — { price, ema, blockTimestamp, raw, … }
const tape = client.getLivePriceTicks("BTC", { limit: 120 }); // recent ticks, newest first
const info = client.getLivePriceFeedInfo("BTC"); // decimals, symbol, description, latest
const state = client.getPriceStatus("BTC"); // "unwatched" | "hydrating" | "live" | "error"
watch.stop(); // release (the last release ends this asset's subscriptions; the shared socket closes when no asset uses it)
Batch variants take/return arrays and are the ergonomic choice for a multi-asset ticker — one watch, one read:
const handle = await client.watchPrices(["BTC", "ETH", "SOL"]); // snapshot + subscribe all; ref-counted
const rows = client.getLivePrices(["BTC", "ETH", "SOL"]); // (LivePrice | null)[], index-aligned
const status = client.getPriceStatus("BTC"); // "live" after snapshot/subscription setup
handle.stop(); // release all in one call
Reads are synchronous, zero-round-trip, and current to the last pushed tick;
subscribe to changes with client.subscribePrices(listener) (independent of the
order-book subscribeLive). In React the hooks auto-watch while mounted:
import { useLivePrice, useLivePriceTicks, useWatchPrice } from "@somnia-chain/markets-sdk/react";
function Ticker() {
const price = useLivePrice("BTC"); // updates the moment a tick lands
const status = useWatchPrice("BTC"); // "hydrating" | "live" | "error" — render off this
const ticks = useLivePriceTicks("BTC", 120); // for a sparkline
if (status === "error") return <span>price feed stopped</span>;
return <span>{price ? `$${price.price.toLocaleString()}` : "…"}</span>;
}
The "error" state
The status is "error" when the price-feed server rejected or terminated the
asset's subscription. This happens on a validation failure after a schema change,
or on a permission denial. The subscription will not heal by itself, so stop the
watch and start a new one. Only the rejected asset enters "error". Other assets
on the shared socket keep streaming. A dropped connection is different: every
asset on that socket becomes stale and heals on reconnect.
A failed stream keeps its last values. If a tape fails while latest-only
readers share the asset, the current-price stream continues. The default
getPriceStatus and getPriceHealth report the tape error. Pass { ticks: false }
to either read for current-price state alone. useWatchPrice reads that state.
With only default/direct readers, a rejection still stops both streams.
Start a new tick reader to hydrate and subscribe the failed tape again. Each rejection is also emitted as a
warn event on the client's debug channel with the server payload.
Reading — one-shot
No watch, one HTTP round-trip each — for history, charts, or a server render:
const info = await client.fetchPriceFeedInfo("BTC"); // metadata + current price
const price = await client.fetchPrice("BTC"); // LivePrice | null
const history = await client.fetchPriceHistory("BTC", { limit: 500, from, to }); // ticks, newest first
const candles = await client.fetchPriceCandles("BTC", "M1", { from, to }); // OHLC, oldest first
Candle resolutions are "M1" / "H1" / "D1" (60s / 3600s / 86400s). count
on a candle is the number of oracle updates in the bucket (update density), not
trade volume. Like every indexer read these throw on request failure — an
empty result means "no rows", never "request failed".
From the exchange
The unified SomniaMarkets surface exposes prices by asset
(these don't need loadMarkets — a price feed isn't a symbol-keyed market):
const px = await exchange.fetchPrice("BTC"); // { symbol, price, ema, timestamp, … } | null
for await (const _ of forever) {
const tick = await exchange.watchPrice("BTC"); // resolves on each new tick
}
const ohlcv = await exchange.fetchPriceOHLCV("BTC", "1m"); // [ms, o, h, l, c, count][] — "1m"/"1h"/"1d"
The native LivePrice (raw strings + block metadata) rides on each struct's
info.
Methodology
Everything above is the read surface. This section is how the number itself is
produced: exactly what is measured, what is discarded, how it is aggregated, how
it is encoded, and what a consumer is guaranteed when it reads a cell. Every
constant is the value in the shipped code (impl v9).
One-line version. A weighted median across seven venue mids, quantized to nine significant figures, medianed again across three independent validators, packed with its own provenance into one 256-bit storage word — every second, paid for by the contract itself.
| Cadence | 1 s |
| Scale | 18 decimals |
| Sources | 7 centralised exchanges, top-of-book mid |
| On-chain aggregation | 3-validator subcommittee, 2-of-3 threshold |
| Networks | Somnia mainnet (5031) 0x1998C88D54a240b8C671493f27f58eC416b81D7F · testnet (50312) 0x7ccAE31E45693475be1e4BAa2360D6997eB2D32B |
flowchart TB
V["7 venues<br/>top-of-book mid"] --> G["4 freshness gates<br/>+ per-symbol quorum floor"]
G --> M["weighted median<br/>over the fresh set"]
M --> Q["quantize<br/>30-bit mantissa + 5-bit exponent"]
Q --> B["48-bit slot per symbol<br/>packed blob"]
B --> C["3 validators submit<br/>independently"]
C --> M2["second median<br/>per symbol, on-chain"]
M2 --> E["EMA mark<br/>alpha = 0.125"]
E --> S["one 256-bit cell<br/>spot + mark + provenance"]
S --> R["consumer read<br/>pull, never reverts"]
01 — Source: seven venue mids, streamed
A Go process — the price-oracle agent — connects to seven centralised
exchanges through a pruned fork of ccxt. It does not fetch on demand. One
goroutine per venue runs continuously, writing the latest quote for every
tracked pair into an in-memory store; a price request is a read of that store,
so it answers in about a millisecond with zero network I/O on the request path.
The quantity collected is the top-of-book mid, (bid + ask) / 2, spot
markets only. Four venues stream over WebSocket; three fall back to bulk REST
polling on a 1-second ticker.
| Venue | Weight | Transport | Why |
|---|---|---|---|
| binance | 3 | ws stream | |
| okx | 2 | ws stream | |
| bybit | 2 | rest poll | wss endpoint geoblocked from the runners' datacentre IPs |
| kucoin | 1 | ws stream | |
| kraken | 1 | ws stream | |
| gateio | 1 | rest poll | stream unimplemented in the fork |
| mexc | 1 | rest poll | stream connects, never delivers |
Total weight 11. Weights are static configuration — not measured volume or depth. They are placeholders intended to be tuned against real per-venue liquidity. Measured source freshness: 70–220 ms on the streaming venues, 225 ms–3.3 s on REST.
internal/exchange/ccxt.go,oracle.json· 61 tracked pairs ·POLL_INTERVAL1 s ·PER_CALL_TIMEOUT8 s · reconnect backoff doubles to 30 s, jittered
02 — Admission: four gates, then a quorum floor
Staleness is the first and strictest gate. A quote that sat in the cache longer than three seconds is not aggregated — it is not even reported as a number.
| Gate | When | Rule | Effect |
|---|---|---|---|
| Book sanity | ingest | both sides present, bid > 0, ask > 0, not crossed (ask ≥ bid) |
quote discarded, never cached |
| Spread | ingest | (ask − bid) / mid ≤ 0.02 |
quote discarded, never cached |
| Dwell | aggregation | now − UpdatedAt > STALE_AFTER (3 s) |
exclude, excludedReason: "stale" |
| Never seen | aggregation | configured exchange has never returned this pair | exclude, excludedReason: "no-cell" |
The first two run in midPrices as each venue's ticker batch is reduced to a
mid, so a bad book never reaches the store at all — there is no exclusion
reason for it, and a venue that only ever produces bad books is
indistinguishable from one that has never returned the pair. "stale" and
"no-cell" are the only two exclusion reasons that exist.
- The spread bound is loose on purpose. Liquid USDT pairs run around 1 bp, so 2% only catches a book wide enough to be a manipulation surface.
- Dwell is measured on the agent's own ingest clock (
now − UpdatedAt), not the exchange's timestamp, so it is immune to venue clock skew. The comparison is strictly greater: exactly 3.000 s still counts.
Then a per-symbol quorum floor. With fewer than minSources fresh venues
the agent refuses to publish. The default floor is 3, with a per-token
override table (SOMI/USDC: 1, because it does not list on three of these
venues). Below the floor the symbol's slot is written as all zeroes — not a
small number, not a stale number, nothing.
internal/store/store.go:136,198·internal/server/handlers.go:98–112, 406–437·maxMidSpreadFrac = 0.02·STALE_AFTER = 3s
03 — Aggregation: a weighted median, not an average
Sort the fresh quotes by price. Walk the weights. The first price whose cumulative weight crosses half the total is the index. One venue can be the whole answer.
All seven venues fresh, total weight 11, half is 5.5:
| Venue | Price | Weight | Cumulative |
|---|---|---|---|
| kraken | 107,373.90 | 1 | 1 |
| mexc | 107,374.05 | 1 | 2 |
| okx | 107,374.18 | 2 | 4 |
| binance | 107,374.21 | 3 | 7 ← first to cross 5.5 |
| bybit | 107,374.44 | 2 | 9 |
| kucoin | 107,374.60 | 1 | 10 |
| gateio | 107,375.02 | 1 | 11 |
The index is 107,374.21. Drop binance (stale) and the total falls to 8,
half is 4, and bybit at 107,374.44 becomes the index — a 23-cent step with no
bad data anywhere.
There is no outlier or deviation rejection anywhere in the agent. Robustness comes entirely from the choice of median over mean.
04 — Quantization: nine significant figures in six bytes
The float becomes an 18-decimal integer, then is re-encoded as a 30-bit mantissa and a 5-bit exponent. Everything past the ninth digit was never real.
Two lossy steps, in order:
- Scale-up. The price is a
float64the whole way through the agent, so multiplying by10^18in abig.Floatpinned at a 53-bit mantissa re-rounds rather than shifting exactly. This is why BTC arrives on-chain looking like63420.750000000000327680— that tail isfloat64residue, not market data. - Wire encoding. Pick the smallest exponent whose mantissa still fits 30 bits, rounding half up.
One symbol's wire slot is exactly 48 bits and exactly full:
| Field | Bits | Range | Example — BTC at 107,374.21 |
|---|---|---|---|
mantissa |
30 | 0 … 2^30 − 1 |
107374210 |
exp |
5 | 0 … 31 |
15, so the value is 107374210 × 10^15 |
sourceAgeMs |
13 | 0 … 8191 |
120 |
05 — Transport: the packed blob
The contract calls getPricesSlots(string[],uint8) and gets back a single
bytes value of exactly 6 × nActive bytes — no ABI element padding at
all. The symbol at active position p lives at bytes [6p, 6p+6), big-endian.
Slots straddle 32-byte word boundaries by design.
- 30 active symbols → blob length must equal 180 bytes, exactly.
- A whole-zero slot means "this validator did not price this symbol".
- A nonzero slot with a zero mantissa is malformed and is dropped the same way.
- Any other length drops the validator entirely — a stale cell, never a wrong price.
06 — Consensus: three validators, then a second median
Nobody trusts one agent. A subcommittee of three runs the same code in three sandboxes against three separate caches, and the contract medians them again.
Each elected subcommittee member detects the request, runs the agent in its own Docker sandbox, and submits its own blob on-chain. Because the agent answers from a continuously-updated cache, the three answers are legitimately different — which is why the platform finalises on Threshold consensus (2-of-3), not Majority, which would require identical result hashes.
| Live parameter | Value | Consequence |
|---|---|---|
subcommitteeSize |
3 | three independent agent runs per tick |
threshold |
2 | two contributors is the ordinary steady state; one is reachable |
consensusType |
1 (Threshold) | non-identical results are expected, not a fault |
timeout |
15 s | must exceed 10 s — the runner cancels at deadline − 10s |
decimals |
18 | the scale the agent is asked to pre-apply |
Four gates on the response, before a single cell moves.
| Rejection | Reason code | Why |
|---|---|---|
| not in the inflight ring | not-inflight |
ring lookup and consume, so a duplicate callback is idempotent |
status ≠ Success |
AgentRequestFailed |
no cells written |
block.timestamp − kickedAt > 3 s |
stale-response |
the platform's timeout upkeep can deliver a quorum-met partial as Success hours late; ingesting it would sawtooth the feed and poison the EMA |
symbolsVersion moved |
symbols-version-mismatch |
responses carry no symbol identity, only array position — if the symbol list changed since the kick, position i may be a different asset |
Any symbol-list mutation invalidates every inflight request; the next tick refills everything within one interval.
The second median — per validator, per symbol.
For each symbol the fill loop does one calldataload per validator, gates it,
and medians the survivors. The gate is per-validator, which is the whole
point: one garbage validator drops out instead of shifting the median.
| Symbol | Validator A | Validator B | Validator C | Cell written |
|---|---|---|---|---|
| BTC/USDC | 107,374.18 | 107,374.21 | 107,374.30 | 107,374.21 · vc 3 |
| ETH/USDC | 3,142.07 | slot = 0 | 3,142.11 | 3,142.11 · vc 2 |
| SOL/USDC | slot = 0 | mantissa = 0 | 184.62 | 184.62 · vc 1 |
| FARTCOIN/USDC | slot = 0 | slot = 0 | slot = 0 | untouched — omitted from the batch |
Insertion sort, then values[n / 2] — the upper middle on even counts,
which at the ordinary 2-of-3 threshold means the higher of the two prices. On a
2-validator tick the median is not a blend; it is a pick. vc is
validatorCount, the surviving contributor count, saturating at 7, with 0
meaning unknown — never "none".
07 — Mark price: the same number, smoothed
Mark is not a different data source. It is an EMA over the spot series, computed from the previous cell, and it costs no extra storage.
Each tick the mark moves emaAlpha = 0.125e18 — one eighth — of the way from
the previous mark toward the new spot. At the 1 s cadence that is a smoothing
horizon of τ ≈ 8 s.
Response to a genuine move.
After n ticks the fraction of a step captured is 1 − 0.875^n. The
conventional time constant τ is where that crosses 63%, and it lands exactly
where interval / alpha predicts — 8 ticks:
| Ticks | 1 | 2 | 4 | 6 | 8 (= τ) | 16 | 24 |
|---|---|---|---|---|---|---|---|
| Captured | 12.5% | 23.4% | 41.4% | 55.1% | 65.6% | 88.2% | 95.9% |
Alpha is per tick, not per second. τ ≈ 8 s here, but τ ≈ 80 s in the DEX's
oracles — same emaAlpha = 0.125e18, copied from their production values, but
their cadence is 10 s. Rescale alpha if the cadence changes and the smoothing
horizon should stay put. emaAlpha = 1e18 disables smoothing entirely — mark
becomes spot.
08 — Storage: everything in one slot
Spot, mark, timestamp, source age and provenance share a single 256-bit word.
One cold SSTORE per symbol per tick, one SLOAD to read all of it.
| Field | Bits | Encoding, and what 0 means |
|---|---|---|
price |
86 | spot median at 18 dp. Ceiling ≈ 77 M per unit. |
quality |
14 | packed sub-fields, below |
mark |
86 | EMA mark; always between the previous mark and this spot |
| reserved | 14 | |
updatedAtMs |
42 | block.timestamp × 1000 — second-granular, shared by every symbol written in the same tick. A maxAgeMs bound below the tick cadence is not meaningful. Good to year 2109. |
sourceAgeMs |
14 | a delta below updatedAtMs, not an absolute stamp — capped at 16.383 s. The top value is the sentinel and reads back as 0 = unknown, never "current", so a real age clamps one below it. |
The 14-bit quality field expands to:
| Sub-field | Bits | Meaning |
|---|---|---|
validatorCount |
3 | contributors to the median, saturating at 7. 0 = unknown (a cell written before the field shipped), never "none" — a written cell always had ≥ 1. |
medianControlCount |
3 | reserved. Always reads 0. No room on the wire. Do not branch on it. |
flags |
2 | bit 0 FLAG_MARK_RESYNCED; bit 1 FLAG_DEVIATION reserved and never set. |
| reserved | 6 |
Removing a symbol deletes its cell. Indices are append-only and never
reused, so an on-chain consumer can resolve index → symbol once off
SymbolAdded and read by index forever — one SLOAD cheaper than the
string-keyed path, with no risk that a removal elsewhere silently shifts the
mapping.
contracts/feed/lib/PriceCell.sol·lib/FeedHealth.sol·lib/ActiveIndexSet.sol
09 — Consumption: pull, and decide staleness yourself
Reads never revert. Staleness is the caller's decision, always — the feed will not decide on your behalf what is too old.
| Interface | Keyed by | Gives you |
|---|---|---|
IPriceFeed |
symbol string | spot, cell age, source age, tracked-symbol registry |
IMarkPriceFeed |
symbol string | EMA mark (separate so IPriceFeed's id never moved) |
IIndexedPriceFeed |
stable uint256 |
the same data, one SLOAD cheaper |
IQualifiedPriceFeed |
either | quorum, flags, feed liveness, and the safe reads |
Off-chain: one batched event, and history.
Each tick emits a single
PricesUpdated(requestId, updatedAtMs, indices[], prices[], marks[], sourceUpdatedAtsMs[], resyncedBits)
— one event for the whole batch, carrying only stable indices. An Envio + Hasura
stack indexes it into Feed (latest per symbol, a Chainlink-ish
latestRoundData analogue), PricePoint (the per-tick firehose), Candle
(OHLC at M1 / H1 / D1) and Symbol (the index → pair registry). That GraphQL
surface is what the price methods above read.
GraphQL is for humans, not for settlement. Testnet only, and observability only. It lags the chain, can be resynced, and is not part of the support contract. Never settle, liquidate, or price anything off it.
End-to-end latency.
| Leg | Time |
|---|---|
| 1 — exchange → agent | ~0.1 s (ws) |
| 2 + 4 + 5 — cache dwell + kick→response + cell write | ~0.8 s |
| 6 — wait for your next read | 0–~1 s |
Measured on testnet over 150 consecutive BTC/USDC ticks: cell cadence median 1 s (max 2 s), kick→cell median ~0.8 s ≈ 7 blocks at ~116 ms. A consumer acts on a price 1–2 s old; at the instant of the cell write it is ~0.9 s. The dominant and most actionable leg is kick→cell. REST-only venues are the other lever — they inflate leg 1 and can leave source age unmeasurable entirely.
Sources
Contracts and scheduler:
smart-contracts/contracts/feed/PriceFeedScheduler.solsmart-contracts/contracts/feed/lib/PriceCell.solsmart-contracts/contracts/feed/lib/SlotResponse.solsmart-contracts/contracts/feed/lib/FeedHealth.solsmart-contracts/docs/price-feed-scheduler.md,price-feed-latency.md,price-feed-design-decisions.mdsmart-contracts/script/config/feed-testnet-development.jsondocs/price-feed-integration.mdprice-feed/indexer/schema.graphql
Agent:
somnia-agents/agents/price-oracle/internal/store/store.gosomnia-agents/agents/price-oracle/internal/server/handlers.gosomnia-agents/agents/price-oracle/internal/exchange/ccxt.gosomnia-agents/agents/price-oracle/oracle.json,agent.json
Delivery state and acquisition errors
The price service is separate from the main indexer. Its results do not carry main-indexer watermarks. hydrated means the HTTP snapshot completed. streaming requires valid initial results from every acquired stream. Current-only health requires only current-price delivery. Empty arrays confirm delivery without creating a price. A connection acknowledgement alone does not establish streaming.
stale means known delivery loss. It does not infer an expected oracle tick frequency. The asset retains its last values while reconnecting, and every acquired stream must deliver again before streaming returns. error means hydration failed or a subscription was rejected. Each asset reports its own state, so a healthy sibling cannot conceal a stale one.
Existing getPriceStatus and useWatchPrice keep unwatched | hydrating | live | error. The legacy live state means snapshot/subscription setup succeeded; it does not confirm operation delivery. A disconnect does not change that legacy state.
Opt into client.getPriceHealth(asset) or client.listPriceHealth() for the precise states above. They share the existing price watches and subscribePrices notifications. No extra socket opens. Automatic hooks continue reporting acquisition failures through the debug/console warning channel. Use useWatchMarketResult, useWatchUserResult, or useWatchPriceResult for typed inline acquisition UI. Effects ignore superseded failures and release handles acquired after unmount.