---
title: "Feed oracle"
description: "EXTERNAL = any IOracle (recommended). INTERNAL = cash-collateral peg helper. NX Rates push API."
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-03"
publish: true
---
# Feed Oracle

> The file and URL for this page still read `internal-oracle`, a legacy slug. The page is the
> consumer-side reference for the **external** feed; `INTERNAL` mode is the small par helper below.

Every AIMM leg prices off a mark supplied by the `IOracle` its `OracleConfig`
names. This page is the consumer-side reference for that feed: the read shape
`getFeed` returns, the state each push overwrites, the bounds a push must clear,
and the wire format of the signed push API. The one exception is the internal par
helper, which returns a fixed mark of 1.0 for cash-collateralized 1:1 tokens,
where the peg is the honest price and a live feed would only add a manipulable
input. Security properties and the mode reference live in
[Oracles](/docs/3-4-oracles).

---

## 1. Oracle modes

`OracleConfig.mode` picks one of two modes:

- **`EXTERNAL`**: the mark is whatever the `IOracle` at `primary` returns (an NX Rates push feed, a Chainlink adapter, a view-only reader over another pool or a vault NAV, so the book can be fully on-chain).
- **`INTERNAL`**: the par helper above, legal only on cash-collateral 1:1 spokes and never on the base.

The mode reference is [Oracles §1](/docs/3-4-oracles#1-overview); per-pool selection is [Curation §3.2](/docs/5-1-2-pool-deployment-curation#32-oracle-configuration).

---

## 2. Unified trust schema

NX Rates attests **levels** (mark, σ, confidence) and signs them; the chain **stores them directly** and bounds each push by a volatility-adaptive deviation band. No on-chain EMA smoothing (that moved to the source).

| Field | NXR attests | Chain stores | Swap consumes |
|-------|-------------|--------------|---------------|
| Price | Fresh mark (NXR 30s close, θ + heartbeat) | Raw mark; per-push move bounded by the adaptive band | **Raw mark** (`mark1e18`, 1e18 WAD) |
| Volatility | **Parkinson σ** (48×30m NXR bars) | Signed σ **direct**; floored at realized `\|Δmark\|/mark` where the σ word is loaded | **`sigmaPbps`** |
| Uncertainty | Mark CI (bps); interim keeper = max(1, 25%·σ) until NXR dispersion | Caps only | Raw `confidenceBps` |

**Price authority:** k-of-n distinct granted NXR signers over one EIP-712 digest, enforced on-chain on the push path (minimum 2-of-3, `MAX_SIGNERS = 16`); the submitting keeper is unpermissioned on the signed path. On the live V4 the entry points are `pushSignedV4(blob, sigs)` and the session variant `pushV4(blob)`, which pins the sender to a quorum-granted relay for at most an hour. Set installation and rotation: [Oracles §4.2](/docs/3-4-oracles#42-signed-push-path-batchpushsigned-k-of-n-quorum).

---

## 3. FeedData (read shape)

```solidity
struct FeedData {
    uint256 mark1e18;       // fresh mark, 1e18 WAD. QUOTE SOURCE. (storage encoding is version-specific; see Oracles §2.)
    uint32 sigmaPbps;       // stored σ (PBPS): signed sample floored at |Δmark|/mark. PRICING INPUT.
    uint32 updatedAtSecs;   // observation timestamp (s). On V4 this IS the source time, see below.
    uint16 ttlSecs;         // freshness window (s).
    uint16 confidenceBps;   // mark 1σ CI (bps), decoupled from σ.
    uint16 flags;           // feed flags. bit0 = paused (guardian fast-freeze, fail-closed).
    uint16 maxDeviationBps; // per-push deviation-band FLOOR (bps), not its max: band ceiling = 10x this. Mandatory non-zero.
    uint48 sourceTsMs;      // NXR-signed source time (ms). Monotonic replay guard + data-age.
}
```

`getFeed` returns this shape; the mark is a plain 1e18 WAD. The on-chain **storage** differs by oracle version. V1 packs one feed per 256-bit slot (B64 mark), V2 packs 8 feeds per slot: the two memory layouts are set side by side in [Oracles §2](/docs/3-4-oracles#2-feeddata-and-the-two-storage-layouts); the live V4 packs 8 lanes of 29 bits with one clock per slot ([Oracles §11.2](/docs/3-4-oracles#112-storage-29-bit-lanes-8-per-slot)). `sigmaPbps` despite its name is **not** an on-chain EMA: it is the NXR-signed σ stored directly, floored at the realized `\|Δmark\|/mark` where the σ word is loaded (a compromised-signer backstop; §4).

Two V4 specifics a consumer must not read past:

- **`updatedAtSecs` and `sourceTsMs` are the same instant.** V4 stores one reconstructed source time per slot and no relay landing time, so `updatedAtSecs == sourceTsMs / 1000`. Freshness is source age either way. Outside the 6 h acceptance window the read fails closed: `updatedAtSecs` comes back as `now − 21601`, which is stale under every deployed ttl.
- **Age is per slot, not per feed.** Eight lanes share one clock, so a lane whose own value did not change still reports the slot's age. Class-pure slot assignment is what keeps that honest; it is not a per-feed freshness guarantee.

V1 emitted no push event. **V4 emits** `SlotsPushed(seq, sourceTsDs, acceptedMask, blobHash)` on both push paths, plus `LanesSkipped(slotId, laneMask)` when a lane fail-softs.

---

## 4. Stored state (no on-chain recurrences)

No price EMA, no σ-EMA fold. Each push overwrites the mark (`mark1e18`) and `sigmaPbps` directly, so stored state is exactly what was signed; all smoothing lives at the source (NX Rates), where the full tape is available to do it well.

- **σ floor (compromised-signer backstop):** the stored σ is `max(signedσ, |Δmark|/mark in PBPS)`, so a mark move forces a proportional spread. A signer cannot sign σ = 0 to make a mark-then-self-swap round trip spread-free. **V4 applies it only where the σ word is already loaded** — the deviation-band slow path, or a slot the blob carries σ entries for. σ/conf elision is total, so a sub-band move in a σ-less blob leaves the stored σ untouched.
- **Per-push deviation band:** each push must satisfy `|Δmark|/mark <= maxDeviationBps + min(6·(σ_pbps/100)·√(dtSource/1800), 9·maxDeviationBps)` in bps (volatility-adaptive, using the **stored prior** σ, stored in PBPS and entering in bps). On V4 a breach **skips that lane** — it keeps its previous value and its bit appears in `LanesSkipped` — rather than reverting the push. A lane with no previous mark, i.e. a feed just registered, has nothing to band against and its first push is unbanded. Full derivation: [Oracles §8.3](/docs/3-4-oracles#83-deviation-bounds).
- **`maxDeviationBps` is the band floor, not its maximum.** The ceiling is exactly `10 × maxDeviationBps`, so at the shipped stable floor of 50 bps a single push may move a stable's mark by up to 500 bps - the whole width of `BASE_DEPEG_HALT_BPS = 500`. Size the floor against that ceiling.

---

## 5. Push API

> **V1 wire.** The live instances run **wire v5** on `ExternalOracleV4`: an 11-byte header
> (`ver:u8(=5) | seq:u32 | sourceTsDs:u24 | nP:u8 | nS:u8 | nC:u8`) followed by positional entries —
> price 5 B (`gi:u8 | lane:u32`), σ 5 B, confidence 3 B — in strictly ascending `gi`, with no
> `tickerId` on the wire and registration through `registerFeed(feedId, globalIndex, expBias,
> maxDeviationBps, ttlSecs)`, which seeds **no mark and no σ**. Byte-exact layout and golden vectors:
> [Oracles §11.5](/docs/3-4-oracles#115-wire-v5). What follows is the V1 format, kept for readers of
> the retired instance.

```solidity
function addFeed(uint64 tickerId, address base, address quote, uint64 price, uint32 sigmaSamplePbps,
                 uint16 confidenceBps, uint16 maxDeviationBps, uint16 ttlSecs) external; // owner-only
// tickerId: NXR/MITCH instrument id keying the signed records (append-only feedIdOf[tickerId]); the
// same call validates the per-push bounds (deviation band, TTL, σ seed) the feed will be held to.
function batchPushSigned(bytes calldata blob, bytes calldata sigs) external; // k-of-n signed
```

`blob` = header(8) || record(22)×n, big-endian.

- **header** = `version(u8) | sourceTsMs(u48) | reserved(u8)`: one attested source time for the whole batch, reserved MUST be 0, `BLOB_VERSION = 1`.
- **record**: 22 bytes, keyed by the NXR/MITCH `tickerId` (`ExternalOracle.sol`), resolved through the append-only `feedIdOf[tickerId]` map; an unregistered ticker reverts.
- **`sigs`** = k concatenated 65-byte ECDSA signatures over the EIP-712 digest of `keccak256(blob)`, sorted by recovered address ascending.

```bitfield 176
0..63     tickerId   (u64)
64..127   priceB64   (u64)
128..159  sigmaPbps  (u32)
160..175  confBps    (u16)
```

Byte-exact wire layout: decoded by `@btr-protocol/sdk` (`decodeBlob`), mirrored 1:1 by the keeper encoder.

---

## 6. Consumer guide

| Need | EXTERNAL | INTERNAL |
|------|----------|----------|
| **Quote** | `mark1e18` | constant 1.0 (`FeedMathLib.getPegFeed`) |
| **Pricing σ** | `sigmaPbps` | `STABLE_SIGMA_PBPS` = 1000 |
| **CI** | `confidenceBps` | gate feed only |

---

## 7. Related

- [Oracles](/docs/3-4-oracles): security + k-of-n signed push path
- [Price-Push Security](/docs/3-6-oracle-price-push-security): NXR provenance, whitelisting ceremony, signature-scheme rationale, transparency
- [Spread & Fees](/docs/1-1-4-spread-fees): σ/confidence/staleness
- [Parametrization](/docs/1-1-7-parametrization): θ, heartbeat, ttl
