Skip to Content
ConceptsAttestation & freshness

Attestation & freshness

Every value Claros publishes is an attestation: a compact record written to the AttestationRegistry contract by the Claros agent, and only by the agent’s authorized attester key. An attestation pins a number to the real-world data period it covers, a provenance hash, the account that wrote it, and the chain time it was committed.

This page covers the record’s fields, how period encodes each cadence, and how to decide whether a reading is fresh enough to use.

The attestation record

On-chain, an attestation is this Odra struct (contracts/src/attestation_registry.rs):

#[odra::odra_type] pub struct Attestation { pub period: u64, pub amount: U512, pub source_hash: String, pub attester: Address, pub timestamp: u64, }
FieldTypeMeaning
periodu64The real-world data period the value covers, encoded as an integer (see What period means).
amountU512The integer value. The human value is amount / 10^decimals, where decimals comes from the feed’s metadata.
source_hashStringA deterministic provenance digest (a 32-character truncated SHA-256) of the canonical source row. Identical wherever the reading is computed, so the on-chain value and the served feed share one verifiable digest.
attesterAddressThe account that wrote the record. attest reverts Unauthorized for anyone but the configured attester (the Claros agent, account-hash 43d7dd06d5538e504e54a3f235f1596f7d2e803e9065bf3c0d040f5cd31a21d4).
timestampu64Epoch milliseconds, set by the chain via get_block_time() at inclusion. It is the commit time, not a value supplied by the agent.

timestamp is assigned by the network, not the caller. The agent can pick the period it attests, but it cannot backdate or forge the on-chain commit time. That makes timestamp the trustworthy signal for “when was this written?”.

How records are stored

attest(asset_id, period, amount, source_hash) writes three places at once: the latest value for the feed, an append-only history entry keyed "{asset_id}#{index}", and a per-feed count (the total across all feeds is also tracked). Read them back with these view methods:

MethodReturnsUse
get_latest(asset_id)Option<Attestation>The most recent attestation for a feed.
get_at(asset_id, index)Option<Attestation>A specific historical attestation (index 0 is the first).
get_count(asset_id)u64Number of attestations for a feed.
total_attestations()u64Total across all feeds.
get_attester()AddressThe authorized attester account.

For most consumers, get_latest (on-chain) or its off-chain equivalents are all you need: see the SDK, the REST API, and cross-contract reads.

Verifying provenance with source_hash

source_hash is computed from the stable source fields only (for EIA: route, frequency, sorted facets, period, value, unit; for civic data: asset, period, amount, count, source URL). Because the formula is deterministic, a consumer can recompute the digest from the raw upstream data and compare it to the on-chain source_hash. A match proves the attested number was derived from that exact source row and was not altered in transit.

What period means

period is an integer that names the interval the value describes. The encoding depends on the feed’s frequency (read it from the feed metadata). The agent strips the separators from the source’s period string, so a calendar period becomes a plain integer:

Cadence (frequency)period formatExampleCovers
dailyYYYYMMDD2026062323 Jun 2026
weeklyYYYYMMDD20260619The week the source dates 19 Jun 2026
monthlyYYYYMM202605May 2026
annualYYYY2025Calendar year 2025
hourlyepoch seconds1750680000One UTC hour

Hourly feeds (for example EIA.ELEC.DEMAND.US48.HOURLY) encode period as epoch seconds, not a date integer. Always branch on the feed’s frequency before you parse period, otherwise an hourly period will look like a nonsensical date.

A length-based decoder handles every cadence:

// period -> the UTC instant the value covers. `frequency` comes from the feed metadata. function decodePeriod(period: number, frequency: string): Date { if (frequency === 'hourly') return new Date(period * 1000); // epoch seconds const s = String(period); if (s.length === 8) return new Date(Date.UTC(+s.slice(0, 4), +s.slice(4, 6) - 1, +s.slice(6, 8))); // YYYYMMDD if (s.length === 6) return new Date(Date.UTC(+s.slice(0, 4), +s.slice(4, 6) - 1, 1)); // YYYYMM if (s.length === 4) return new Date(Date.UTC(+s, 0, 1)); // YYYY throw new Error(`unrecognized period: ${period}`); }

Checking freshness

A reading carries two independent time signals, and a robust staleness check uses both:

periodtimestamp / updated_at
What it isThe real-world interval the value coversThe chain time the attestation was committed
Set byThe agent (from the source row)The chain (get_block_time() at inclusion)
FormatYYYYMMDD / YYYYMM / YYYY (epoch seconds if hourly)Epoch milliseconds
Answers”What does this number describe?""When was it written?”

They can diverge. A value written minutes ago can still carry an old period if the upstream source has not published anything new, and a recent period written long ago means nothing has changed since. Check timestamp for write age, and decode period to confirm the underlying data is current for the cadence you expect.

timestamp (cross-contract field) and updated_at (SDK getReading and REST) are the same value: the on-chain commit time in epoch milliseconds. The SDK’s lower-level getValue exposes it as timestamp to match the contract.

A reasonable default tolerance, before you treat a reading as stale, scales with the cadence and the source’s own publishing lag:

CadenceSuggested max age
hourlya few hours
daily2 to 3 days (sources lag)
weeklyabout 10 days
monthlyabout 45 days
annualuntil the next release window

getReading returns period, frequency, and updated_at (epoch ms) in one call, so a freshness guard is a subtraction:

import { ClarosOracle } from 'claros-oracle'; const oracle = new ClarosOracle(); // testnet addresses baked in // Suggested wall-clock tolerance per cadence, in ms. const MAX_AGE: Record<string, number> = { hourly: 3 * 3_600_000, daily: 3 * 86_400_000, weekly: 10 * 86_400_000, monthly: 45 * 86_400_000, annual: 400 * 86_400_000, }; async function isFresh(feedId: string): Promise<boolean> { const r = await oracle.getReading(feedId); if (!r) return false; // never attested const ageMs = Date.now() - r.updated_at; // updated_at is epoch ms return ageMs <= (MAX_AGE[r.frequency] ?? 86_400_000); }

The agent re-attests only on new data

Claros runs an hourly heartbeat (agent/src/loop.ts), but a heartbeat is not an attestation. Each tick the agent checks whether the source has advanced, and runs a full cycle only when it has.

Heartbeat

The agent wakes on an interval (default CYCLE_INTERVAL_MS = 3_600_000, one hour) for each configured asset.

Read the latest period

It fetches the source’s most recent reading and reads off its period.

Compare to the last processed period

It keeps the last attested period per asset in a small state file. If period <= last, it logs “no new data” and skips: no transaction, no write.

Run the cycle

Only when period strictly advances does it run a cycle (the ZK eligibility gate, an anomaly check, then attest, then a treasury decision) and record the new period.

Casper testnet charges the gas limit you set, not the gas consumed, with no refund. Re-attesting an unchanged period would burn gas for an identical record, so the agent deduplicates by period. Each feed’s history therefore holds at most one attestation per genuinely new period, and stored periods strictly increase.

Two consequences for consumers:

  • A stale timestamp is itself a signal. Because the agent only writes on a new period, a large gap between now and timestamp means the upstream source has published nothing new, or the agent has paused. Your freshness check catches both.
  • New data is necessary, not sufficient. Even on a fresh period, the cycle declines to attest if the ZK eligibility credential is not confirmed or the reading fails the anomaly check. The on-chain value is always the last reading the agent judged eligible and clean.
Last updated on