Provenance & source hash
Every value Claros publishes carries a source_hash: a deterministic SHA-256 fingerprint
of the exact upstream row it was derived from. The hash is computed once, from the
substantive identity of the row (route, frequency, facets, period, value, unit for energy
data, or asset, period, amount, count, source URL for civic data), and it travels with the
value everywhere it goes. The agent attests it on-chain, and the paid x402 feed serves the
same digest, so a buyer can reconcile the two without trusting Claros.
If the source_hash you receive over the x402 feed equals the
source_hash stored in the on-chain attestation, the value you paid for is byte-for-byte
the value Claros committed on-chain. A matching hash is the trust boundary: you do not
have to trust the server, only the chain and the algorithm below.
What source_hash commits to
The hash is the first 32 hex characters (128 bits) of a SHA-256 digest over a canonical string built from stable fields only. It deliberately excludes upstream cosmetics: human-readable labels, API version strings, request echoes, and response metadata are not hashed, so a harmless change upstream (a relabeled column, a new API version) does not break the digest. Only the data identity of the row is committed.
Claros has two data families, and each canonicalizes the row in its own fixed way.
EIA energy feeds
EIA feeds are reached through one uniform faceted API, so the hash commits to the request coordinates plus the value. Facets are sorted by id, and the value is serialized as a string. See the data model for what route, frequency, and facets mean.
| Field | Where it comes from | Example (WTI) |
|---|---|---|
route | feed mapping | petroleum/pri/spt |
frequency | EIA response (falls back to the feed config) | daily |
facets | feed mapping, sorted by facet id | [["series","RWTC"]] |
period | the row’s raw EIA period string | 2026-06-26 |
value | the data column, as a string | "78.94" |
unit | EIA row units (falls back to the feed config) | $/bbl |
The hashing code, from agent/src/eia.ts:
function sourceHash(f: EiaFeed, row: Record<string, any>, frequency: string, unit: string): string {
const canonical = JSON.stringify({
route: f.route,
frequency,
facets: Object.entries(f.facets).sort(([a], [b]) => a.localeCompare(b)),
period: row.period,
value: String(row[f.data_col]),
unit,
});
return createHash('sha256').update(canonical).digest('hex').slice(0, 32);
}One hash, two homes: on-chain and over x402
The point of a deterministic digest is that it can be computed independently in more than one place and still agree. Claros computes it in exactly two:
- The agent, each cycle, reads the latest upstream row, computes
source_hash, and attests it on-chain. The agent’s instructions are explicit: attest with the exactperiod,amount, andsource_hashfrom the reading, never recomputed or rounded. - The x402 oracle server, when it sells the feed, recomputes the reading with the same
function (
latestRevenuefor civic feeds,fetchLatestfor EIA), so thesource_hashin the paid response is the identical digest the agent attested.
Because both call the same canonicalization over the same upstream row, the two digests are equal by construction. There is no separate “served” value and “attested” value to drift apart.
On-chain: the attestation record
The agent signs an attest call into AttestationRegistry, which stores the digest verbatim
in the attestation and emits it as an event. Reading it back gives you the committed hash
(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,
}
// get_latest(asset_id: String) -> Option<Attestation>The deployed registry is package
236b510436c60b6a797d175c72c6014de367d43f1de1ca45f580d112f98116cc on casper-test. The
first-party attester account-hash is 43d7dd06d5538e504e54a3f235f1596f7d2e803e9065bf3c0d040f5cd31a21d4;
operator-claimed feeds carry their own operator’s key in attester.
Over x402: the paid payload
The x402 endpoint (GET /oracle/feed?asset_id=OP-1, gated by HTTP 402) returns the reading
together with provenance pointers back to that same registry. The source_hash field is the
reconciliation anchor (services/oracle-server/index.ts):
{
"asset_id": "OP-1",
"period": 20260623,
"amount_cents": 273420,
"txn_count": 412,
"latest_date": "2026-06-23",
"source_hash": "71db7d2dd6edd2a47f3fe27c4c693fa1",
"provenance": {
"network": "casper-test",
"registry_package_hash": "236b510436c60b6a797d175c72c6014de367d43f1de1ca45f580d112f98116cc",
"attester": "account-hash-43d7dd06d5538e504e54a3f235f1596f7d2e803e9065bf3c0d040f5cd31a21d4",
"source": "City of San Diego: parking-meter daily transactions",
"source_url": "https://seshat.datasd.org/parking_meters_transactions_daily/treas_meters_2026_pole_by_mo_day_datasd.csv",
"verify": "https://testnet.cspr.live/contract-package/236b510436c60b6a797d175c72c6014de367d43f1de1ca45f580d112f98116cc"
},
"served_at": "2026-06-23T18:04:11.000Z"
}The provenance block is not the proof, it is a set of pointers. The proof is that
source_hash here equals the source_hash at that registry. The block just tells you which
registry, attester, and network to check, and gives you a verify link to the explorer.
Verify a reading
Reconciliation is a string comparison. Read the committed hash from the chain, read the hash from the payload, and confirm they are identical. The strongest check goes one step further and recomputes the hash from the raw upstream source.
Read the committed source_hash from the chain
Use whichever read path you already have. All of them surface the same field that
AttestationRegistry stores.
SDK
import { ClarosOracle } from 'claros-oracle'
const oracle = new ClarosOracle()
const reading = await oracle.getReading('OP-1')
console.log(reading.source_hash) // committed on-chainSee the SDK for the full reading shape.
Read the source_hash from the x402 payload
Buy or fetch the feed and pull source_hash out of the response body. The
x402 flow returns the reading and provenance after settlement:
# after the 402 -> X-PAYMENT -> 200 handshake
curl ... 'http://localhost:4021/oracle/feed?asset_id=OP-1' | jq -r .source_hash
# -> 71db7d2dd6edd2a47f3fe27c4c693fa1Compare the two strings
Both are 32-character lowercase hex. They must be byte-for-byte identical:
on-chain : 71db7d2dd6edd2a47f3fe27c4c693fa1
x402 : 71db7d2dd6edd2a47f3fe27c4c693fa1 ✓ reconciledA match means the paid value is exactly what Claros attested. A mismatch means the payload does not correspond to the on-chain attestation, and you should reject it.
Reproduce the hash from source
The chain proves the served value equals the attested value. To also prove the attested value
faithfully represents the real world, recompute the digest yourself from the upstream row and
confirm it equals both. The algorithm is the canonical string plus sha256 truncated to 32
hex characters, nothing else.
Civic (shell)
# asset_id | period | cents | txn_count | source_url
printf '%s' "OP-1|20260623|273420|412|https://seshat.datasd.org/parking_meters_transactions_daily/treas_meters_2026_pole_by_mo_day_datasd.csv" \
| sha256sum | cut -c1-32
# -> 71db7d2dd6edd2a47f3fe27c4c693fa1Use printf '%s', not echo: a trailing newline would change the digest. Plug in the
period, cents, and txn_count from your own reading.
The values above are sample inputs chosen to make the worked digests reproducible. To check a specific live attestation, use that attestation’s own fields. Reproduction is exact only if you match the canonicalization byte-for-byte:
- hex is lowercase and truncated to the first 32 characters;
- civic fields are joined with literal
|in the orderasset_id | period | cents | txn_count | source_url, with no trailing newline; - EIA facets are sorted by id and the
valueis serialized as a string ("78.94", not the number78.94).
Any single differing byte produces a completely different hash.