> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use oracle data

> Read and validate stored oracle values for applications and contracts

> [!NOTE]
>
> **What you'll be able to do**
>
> * Read current oracle values.
> * Use validated oracle data in your smart contracts.
> * Retrieve an earlier observation retained by the oracle.

> [!WARNING]
>
> The oracle contract is [not on Metal Testnet yet](https://docs.metalntwx.com/reference/contract-addresses.md#contracts-not-yet-on-metal-testnet).
> Follow this guide on a network that has code at its address.

Metal's oracle makes data such as exchange rates and asset prices available to
applications and smart contracts. Validators combine observations from the
configured data sources and publish the results. A payment contract can use an
exchange rate to calculate an amount, while an application can use an asset
price to calculate a valuation.

This guide reads values stored in the oracle contract at
`0x00000000000000000000000000000000000000fd` and checks them before use.
For signed observations supplied with a transaction, see the formats and
verification interface in [Oracle reports](https://docs.metalntwx.com/reference/data-and-signatures/oracle-reports.md).

## Confirm the data source

Check that `client.getCode({ address: oracle })` returns nonempty runtime bytecode.

Get the item name, units, quote direction, and acceptable bounds from the feed
provider, then match them to the catalog's scale and aggregation settings. Define
the catalog and observation reads with viem's ABI parser:

```typescript theme={null}
import { formatUnits, keccak256, parseAbi, toBytes } from 'viem'

const oracle = '0x00000000000000000000000000000000000000fd' as const
const oracleAbi = parseAbi([
  'struct Item { uint256 value; uint256 conf; uint64 observedAtMs; uint64 writtenAtMs; uint64 height; uint16 nSigners; uint16 minNSources; uint32 flags; }',
  'struct CatalogItem { bytes32 id; string name; uint8 decimals; uint64 heartbeatMs; uint32 deviationBps; uint16 minSources; uint16 minQuorum; uint8 mode; bool suspends; bool catalog; bytes32 controls; uint64 emaWindowMs; }',
  'function catalogSize() view returns (uint256)',
  'function catalogItem(uint256 index) view returns (CatalogItem)',
  'function latest(bytes32 id) view returns (Item)',
  'function at(bytes32 id, uint64 height) view returns (Item)',
  'error UnknownItem()',
  'error NotInHistory()',
])
```

Read the catalog and observation at the same finalized block, using `itemName`
from the feed configuration. Save the block hash, item ID, decimals, and unit
definition with the result.

```typescript theme={null}
const block = await client.getBlock({ blockTag: 'finalized' })
const atBlock = { address: oracle, abi: oracleAbi, blockNumber: block.number }
const count = await client.readContract({ ...atBlock, functionName: 'catalogSize' })
const catalog = await Promise.all(Array.from({ length: Number(count) }, (_, i) =>
  client.readContract({ ...atBlock, functionName: 'catalogItem', args: [BigInt(i)] }),
))
const item = catalog.find(row => row.name === itemName)
if (!item || item.id !== keccak256(toBytes(itemName))) throw new Error('Missing item')
if (item.mode !== 0) throw new Error('Expected a medianConf valuation item')
const observation = await client.readContract({
  ...atBlock, functionName: 'latest', args: [item.id],
})
```

`mode` is `0` median with confidence, `1` quorum-identical, `2` bit vote, `3`
minimum, or `4` maximum. This valuation procedure expects mode `0`. Choose
checks suited to the data when using another mode. `heartbeatMs` controls
periodic writes in block time, so updates can stop during a chain halt.

## Validate a value

The confidence half-width (`conf`) describes the uncertainty on either side of
the reported value. Set the maximum age (`maxAgeMs`), maximum confidence
half-width in basis points (`maxConfBps`), and rejected flags (`stopFlags`)
before reading. `acceptPrice` accepts a positive price within those bounds,
including values at the limits. It checks observation and write times against
the application's current clock to detect stale data even during a chain halt.

```typescript theme={null}
type Observation = {
  value: bigint
  conf: bigint
  observedAtMs: bigint
  writtenAtMs: bigint
  flags: number
}
type Bounds = { maxAgeMs: bigint, maxConfBps: bigint, stopFlags: number }

function acceptPrice(value: Observation, bounds: Bounds, nowMs: bigint) {
  if (bounds.maxAgeMs < 0n || bounds.maxConfBps < 0n || bounds.maxConfBps > 10_000n) {
    throw new Error('Invalid bounds')
  }
  if ((value.flags & bounds.stopFlags) !== 0) throw new Error('Flagged')
  for (const time of [value.observedAtMs, value.writtenAtMs]) {
    if (time === 0n || time > nowMs || nowMs - time > bounds.maxAgeMs) {
      throw new Error('Stale or invalid timestamp')
    }
  }
  if (value.value <= 0n || value.conf * 10_000n > value.value * bounds.maxConfBps) {
    throw new Error('Uncertain or nonpositive price')
  }
  return value.value
}

const bounds: Bounds = { maxAgeMs: 30_000n, maxConfBps: 100n, stopFlags: 31 }
const raw = acceptPrice(observation, bounds, BigInt(Date.now()))
const decimalPrice = formatUnits(raw, item.decimals)
```

The configured bounds allow an age of up to 30 seconds and a confidence
half-width of up to 1% of the price, while rejecting all five flags. Adjust them
to fit the feed and business action. `value` and `conf` share the catalog's
fixed-point scale, so validate them with integer arithmetic and convert to a
decimal string for display.

| Flag | Mask | Meaning |
| - | - | - |
| Disputed | `1` | Validators did not agree on an identical value |
| Halted | `2` | New writes are stopped |
| Frozen | `4` | Consumers should stop acting while writes may continue |
| Market closed | `8` | Last value from closed venues |
| Ripcord | `16` | Source operator stopped normal use |

A control update can change flags while the stored value and height stay the
same, so check the flags on every read. Stop the operation on `UnknownItem`,
missing code, transport errors, or a rejected value.

## Use a value in a contract

The `BoundedPrice` contract validates the observation during the transaction
that uses it, so it checks the value and flags present at execution. Its item,
scale, and bounds are fixed at deployment. Confirm the constructor inputs
against the catalog, then follow
[Deploy an application contract](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md).

> [!NOTE]
>
> Oracle timestamps use milliseconds. The contract checks them against
> `block.timestamp * 1000 + 999`, the last millisecond of the current second.
> This accepts observations from within that second and can reject a value up
> to 999 milliseconds before its age limit.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IMetalOracle {
    struct Item {
        uint256 value;
        uint256 conf;
        uint64 observedAtMs;
        uint64 writtenAtMs;
        uint64 height;
        uint16 nSigners;
        uint16 minNSources;
        uint32 flags;
    }
    function latest(bytes32 id) external view returns (Item memory);
}

contract BoundedPrice {
    bytes32 public immutable itemId;
    uint8 public immutable decimals;
    uint64 public immutable maxAgeMs;
    uint16 public immutable maxConfBps;
    uint32 public immutable stopFlags;

    constructor(bytes32 id, uint8 scale, uint64 age, uint16 confidence, uint32 flags) {
        require(confidence <= 10_000 && age > 0, "bounds");
        itemId = id;
        decimals = scale;
        maxAgeMs = age;
        maxConfBps = confidence;
        stopFlags = flags;
    }

    function read() external view returns (uint256) {
        IMetalOracle.Item memory x = IMetalOracle(
            0x00000000000000000000000000000000000000fd
        ).latest(itemId);
        uint256 nowMs = block.timestamp * 1000 + 999;
        require(x.observedAtMs > 0 && x.writtenAtMs > 0, "unavailable");
        require(x.observedAtMs <= nowMs && x.writtenAtMs <= nowMs, "future");
        require(nowMs - x.observedAtMs <= maxAgeMs, "stale observation");
        require(nowMs - x.writtenAtMs <= maxAgeMs, "stale write");
        require((x.flags & stopFlags) == 0 && x.value > 0, "flagged or zero");
        uint256 limit = (x.value / 10_000) * maxConfBps
            + ((x.value % 10_000) * maxConfBps) / 10_000;
        require(x.conf <= limit, "uncertain");
        return x.value;
    }
}
```

Have your application contract call `read()` and use the returned value in the
same transaction, allowing a failed check to revert the action. The contract
checks age against `block.timestamp`, while the application checks its current
clock before submission.

## Verify the integration

Save an accepted value with its decimal string, units, block hash, observation
and write times, and configured limits. Test values at and just beyond each
age and confidence limit, each rejected flag, an unknown item, and RPC failure.
Each rejected case must stop the valuation or action.

If you use the contract, compile it and verify a transaction that uses an
accepted observation. Include timestamps within the current second and just
beyond it, and age limits measured from that second's final millisecond.

## Read an earlier observation

Use an earlier observation to check a past valuation or reconcile an
application record. `observation.height` identifies the block that wrote the
observation. Request that height at the same pinned state:

```typescript theme={null}
const historical = await client.readContract({
  ...atBlock, functionName: 'at', args: [item.id, observation.height],
})
if (historical.height !== observation.height || historical.value !== observation.value) {
  throw new Error('Historical write mismatch')
}
```

History has limited retention, and newer writes can replace earlier entries.
`at` requires the exact height of a retained write. Otherwise it returns
`NotInHistory`. Save observations you need to retain with their block numbers
and hashes, or use an archive RPC provider that supports reads at older blocks.

For a historical valuation, evaluate the observation using the valuation time
and policy recorded for that action. For a current payment, check its age
against the current time. Test both a retained observation and `NotInHistory`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.