Decimals & scaling
Claros never stores a floating-point number on-chain. Every feed value is an
integer amount plus a per-feed decimals exponent, exactly the model
Pyth uses for prices. The human value is
amount / 10^decimals. This page explains why, how the conversion works in
both directions, and the one constraint that follows from the integer type: the
amount is unsigned.
A reading has two halves, stored in two contracts and keyed by the same
feed_id: the integer amount lives in the AttestationRegistry, and the
decimals exponent lives in the FeedRegistry. You always pair them. See
Feeds & metadata.
The model
There are two directions, and they are inverses of each other.
| Direction | Formula | Who does it |
|---|---|---|
| Encode (human value to integer) | amount = round(value * 10^decimals) | the Claros agent, before it calls attest |
| Decode (integer to human value) | value = amount / 10^decimals | you, the consumer |
The agent encodes once when it attests. Everyone reading the feed decodes. Most
of the reading surfaces decode for you and hand back a
ready value, but the raw amount and decimals are always available so you
can redo the math yourself with full precision.
What is stored on-chain
The value and its exponent are deliberately split across two registries so a consumer can interpret a number entirely on-chain, the Pyth approach where a price ships with its exponent.
The latest value, in the AttestationRegistry:
#[odra::odra_type]
pub struct Attestation {
pub period: u64,
pub amount: U512, // the scaled integer
pub source_hash: String,
pub attester: Address,
pub timestamp: u64,
}The exponent and unit, in the FeedRegistry:
#[odra::odra_type]
pub struct Feed {
pub decimals: u8, // real value = amount / 10^decimals
pub unit: String, // e.g. "$/bbl", "MWh", "percent"
pub title: String,
pub source: String,
pub route: String,
pub frequency: String,
pub description: String,
}A reading pairs one Attestation.amount with one Feed.decimals for the same
feed_id.
Worked example: WTI crude
The EIA.PET.PRICE.WTI.DAILY feed reports the Cushing WTI spot price in
$/bbl with decimals = 6.
| Field | Value |
|---|---|
| Human value | 78.94 |
decimals | 6 |
amount (U512, on-chain) | 78940000 |
unit | $/bbl |
Encode (agent):
amount = round(78.94 * 10^6) = round(78.94 * 1000000) = 78940000Decode (consumer):
value = 78940000 / 10^6 = 78.94Why scaled integers, not floats
Determinism
Every Casper node validating a transaction must agree on the exact bytes stored in contract state. Floating-point results are not portable across platforms, so the on-chain value type is an integer.
The native type is U512
Casper’s value and amount type is U512, a 512-bit unsigned integer, and that
is what Attestation.amount is. It is wide enough that even a count-scale
metric multiplied by 10^decimals will not overflow in practice.
No float drift on encode
The agent does not multiply a JavaScript number. It scales the source decimal
string directly, so binary rounding (the 0.1 + 0.2 problem) never reaches the
amount.
The encoder splits the source on the decimal point, pads or trims the fractional
part to exactly decimals digits, then parses the joined digits as one big
integer:
// Scale a decimal string by 10^k without float drift.
function scaleDecimalString(s: string, k: number): bigint {
let neg = s.startsWith('-');
if (neg) s = s.slice(1);
let [int, frac = ''] = s.split('.');
frac = (frac + '0'.repeat(k)).slice(0, k);
const v = BigInt((int || '0') + frac);
return neg ? -v : v;
}Because decimals is chosen to be at least as precise as the source feed, the
fraction always fits and the scaling is exact, with no rounding loss in
practice.
decimals is the exponent (Pyth expo)
decimals is a per-feed u8, picked to fit the precision of the metric. It
plays the same role as Pyth’s expo for a price. Claros uses a small set of
conventions:
| Metric kind | decimals | Example feed | unit |
|---|---|---|---|
| Unit prices | 6 | EIA.PET.PRICE.WTI.DAILY | $/bbl |
| Percent | 4 | EIA.NUC.OUTAGE.US_PCT.DAILY | percent |
| Volumes and energy | 3 | EIA.ELEC.DEMAND.US48.HOURLY | MWh |
| Counts | 0 | EIA.ELEC.RETAIL.CUSTOMERS.US_RES.MONTHLY | count |
A count uses decimals = 0, so its amount equals the value exactly with no
scaling at all.
Each feed picks its own decimals. Never assume 6. Always read decimals
from the FeedRegistry (or the decimals field the SDK and REST return) before
you divide.
The amount is unsigned
U512 is an unsigned integer, so it cannot represent a negative number.
Claros only attests metrics that are non-negative: prices, volumes, counts,
revenue, and percentages. The off-chain encoder can represent a negative number
(it has a sign branch), but the on-chain type cannot hold one, so the live
catalog contains only non-negative feeds.
Signed metrics are out of scope for the current registry. A value that can drop below zero (for example a net storage change, or a temperature in degrees) is not published as a Claros feed today.
Reading a value back
Whichever surface you use, you can either take the pre-divided value or take
the raw amount plus decimals and divide yourself.
SDK
The SDK divides for you in getReading, and also returns
the raw amount as a BigInt and the decimals so you can redo it.
import { ClarosOracle } from 'claros-oracle';
const claros = new ClarosOracle();
const wti = await claros.getReading('EIA.PET.PRICE.WTI.DAILY');
// wti.value -> 78.94 (already amount / 10^decimals)
// wti.amount -> 78940000n (raw U512, as a BigInt)
// wti.decimals -> 6getValue returns only the raw amount; getReading is the helper that also
reads decimals and does the division.
Convert an amount by hand
Read decimals
Fetch the feed’s decimals from the FeedRegistry. It is per-feed metadata and
rarely changes, so you can cache it.
Read the latest amount
Fetch the latest amount (a U512) from the AttestationRegistry for the same
feed_id.
Divide
Compute value = amount / 10^decimals. For WTI that is
78940000 / 10^6 = 78.94.