Skip to Content
ConceptsProvenance & source hash

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 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.

FieldWhere it comes fromExample (WTI)
routefeed mappingpetroleum/pri/spt
frequencyEIA response (falls back to the feed config)daily
facetsfeed mapping, sorted by facet id[["series","RWTC"]]
periodthe row’s raw EIA period string2026-06-26
valuethe data column, as a string"78.94"
unitEIA 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:

  1. 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 exact period, amount, and source_hash from the reading, never recomputed or rounded.
  2. The x402 oracle server, when it sells the feed, recomputes the reading with the same function (latestRevenue for civic feeds, fetchLatest for EIA), so the source_hash in 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.

import { ClarosOracle } from 'claros-oracle' const oracle = new ClarosOracle() const reading = await oracle.getReading('OP-1') console.log(reading.source_hash) // committed on-chain

See 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 # -> 71db7d2dd6edd2a47f3fe27c4c693fa1

Compare the two strings

Both are 32-character lowercase hex. They must be byte-for-byte identical:

on-chain : 71db7d2dd6edd2a47f3fe27c4c693fa1 x402 : 71db7d2dd6edd2a47f3fe27c4c693fa1 ✓ reconciled

A 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.

# 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 # -> 71db7d2dd6edd2a47f3fe27c4c693fa1

Use 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 order asset_id | period | cents | txn_count | source_url, with no trailing newline;
  • EIA facets are sorted by id and the value is serialized as a string ("78.94", not the number 78.94).

Any single differing byte produces a completely different hash.

Last updated on