# Records

A record is one fact the calendar captured, such as a gold price, a block head, a signed tree head, a NIST pulse or a headline, written as a typed envelope of bytes. Every tick commits its records in branches, one Merkle tree per branch, and the branches in one map whose root is part of the tick root. A contract on ETC proves any record with one call, and the [Records](https://etchd.app/records) pages of the explorer list them by branch.

Every byte table, JSON form and rule of the branches is in [Records](https://github.com/classix-dev/etchd/blob/main/SPEC.md#records) in SPEC.md.

## The Envelope

Every record of every branch starts with the same 11-byte header.

Every record starts with the same 11 bytes, the tag 0x03, the branch as 4 bytes, the schema as 2 and the payload length as 4. The payload follows in the layout of its branch and schema. A record's leaf is the sha256 of the whole envelope. No envelope is exactly 64 bytes long, so a record can never pass for an inner node of a tree.

| Offset | Length | Field | Encoding |
| --- | --- | --- | --- |
| 0 | 1 | `tag` | 0x03, the record tag |
| 1 | 4 | `branch` | u32, the branch id, 1 or more |
| 5 | 2 | `schema` | u16, the payload layout version of that branch |
| 7 | 4 | `length` | u32, the payload length, the record length minus 11 |
| 11 | length | `payload` | as the branch and schema define |

```text
leaf = sha256(envelope)              the tag is byte 0, no other prefix
```

A record is 11 to 65,536 bytes long and never exactly 64. Sixty-four bytes is the size of an inner node of a Merkle tree, two 32-byte hashes side by side, so a 64-byte record would pass for one. A layout whose bytes would come to 64 adds one `0x00` pad byte, which the decoder drops, and a decoder refuses any other trailing byte. One record therefore has one encoding and one leaf.

The header carries everything a reader needs to split and hash records without knowing their schema. That is how a mirror running an older CLI still checks every root of a day that holds a branch it has never heard of.

## Branches and the Map

A tick sorts its records by branch. Inside a branch the envelopes are sorted bytewise and hashed into a Merkle tree, the branch root. The envelopes themselves go out concatenated in the same order as one raw block, the pack, and each envelope carries its own length, so a pack splits one way only. A branch with no records in a tick has no pack and no leaf in the map.

The records of a tick are sorted bytewise and folded into one Merkle tree per branch. Each branch root becomes a leaf of the branches map, the sha256 of 0x04, the branch id as 4 bytes and the root. The map's root is the branches root, the third of the four 32-byte parts the tick root hashes, at offset 64. A branch with no records in a tick has no leaf.

```text
branch_root   = merkle_tree([sha256(e) for e in the branch's envelopes, sorted bytewise])
branch_leaf   = sha256(0x04 || u32be(branch_id) || branch_root)        37 bytes in
branches_root = merkle_tree(branch leaves sorted by branch_id)         ZERO with no records
tick_root     = sha256(prev_tick_root || calendar_root || branches_root || tick_header)
```

The branches root sits at offset 64 of the 128 bytes the tick root hashes, next to the calendar root that holds the stamps ([Ticks](https://etchd.app/docs/ticks#root)). The branch table is append-only and an id is never reused. The record started with these six.

| Id | Name | Layout | Fed by | Trust |
| --- | --- | --- | --- | --- |
| 1 | [`price`](https://etchd.app/records/price) | fixed, 89 bytes | Kraken, Coinbase, Swissquote, gold-api and Chainlink on Ethereum each minute, the ECB once a day | O, Chainlink rechecked on Ethereum |
| 2 | [`chain-head`](https://etchd.app/records/chain-head) | 72 bytes, then the hash | the beacon, when a chain's committed head changes | C where a header is kept, O otherwise |
| 3 | [`ct-head`](https://etchd.app/records/ct-head) | 101 bytes, then the signature | the signed tree heads of the Certificate Transparency logs in force, every 5 minutes | S |
| 4 | [`nist-pulse`](https://etchd.app/records/nist-pulse) | 109 bytes, then NIST's fields and signature | the NIST Randomness Beacon 2.0, each minute | S |
| 5 | [`headline`](https://etchd.app/records/headline) | variable | the items of about 80 news feeds and Hacker News, each distinct item once a day | O |
| 6 | [`front-page`](https://etchd.app/records/front-page) | variable, references to headlines | one per feed capture | O |

Trust says how a reader checks a record without trusting us. S is signed by the source, C is content-addressed (a block hash whose header is in the day's bundle), P is a publisher hash a reader rechecks while the object exists, and O rests on the operator alone. An O record is the operator's account of what a provider said. Providers sign nothing there, and the record shows only that the operator committed those bytes by the anchor time.

Each branch has its own uniqueness rule, such as one price per pair per slot or one pulse per pulse index, and `etchd records verify` checks it for every day. The rules are versioned, and every registry snapshot names the version it ran, so an old day is judged by the rule it was written under.

## Price

A price record is 89 bytes. The price is a uint128 with 18 decimals, so a contract compares and multiplies prices with no exponent per record. Every price source runs once a minute at second 1, and the ECB reference rates once a day at 15:30 UTC. The [Prices](https://etchd.app/prices) page shows them minute by minute.

A price record is 89 bytes. After the 11-byte header come the pair at offset 11, the price at 43 as a uint128 with 18 decimals, quoted_at at 59, captured_at at 67, the parser version at 75, the kind at 77, the flags at 78 and ref at 79, the round id of an on-chain oracle answer. A contract reads each field with one calldata slice.

| Offset | Length | Field | Encoding |
| --- | --- | --- | --- |
| 11 | 32 | `pair` | ASCII provider:BASE/QUOTE, padded with 0x00, like kraken:BTC/USD |
| 43 | 16 | `price` | uint128, quote units per base unit times 10^18, above 0 |
| 59 | 8 | `quoted_at` | uint64, the provider's time for the quote, read with the flags |
| 67 | 8 | `captured_at` | uint64, the second the capture started, inside its slot |
| 75 | 2 | `parser` | uint16, the version of the parser that made the record |
| 77 | 1 | `kind` | 0 mid of bid and ask, 1 last trade, 2 daily reference rate, 3 on-chain oracle answer, 4 aggregated spot, 5 official close |
| 78 | 1 | `flags` | bit 0 quoted_at is Date minus Age, bit 1 an update time, bit 2 a trade date, bit 3 revises a value already committed |
| 79 | 10 | `ref` | uint80, the oracle round id for kind 3, zero otherwise |

Solidity reads, after the verifier call

```solidity
bytes32 pair       = bytes32(r[11:43]);
uint256 price      = uint128(bytes16(r[43:59]));
uint64  quotedAt   = uint64(bytes8(r[59:67]));
uint64  capturedAt = uint64(bytes8(r[67:75]));
uint8   kind       = uint8(r[77]);
uint8   flags      = uint8(r[78]);
```

Kraken and Coinbase give no quote time, so `quoted_at` is the response's `Date` header minus its `Age`, with flag bit 0 set. A cache in front of a provider would make that time too young, so a capture refuses a pair with the reason `cached` when `Age` is above 5 s or Cloudflare reports a cache hit, and the retries at seconds 21 and 41 try again. Parsing is exact decimal arithmetic from the source text, never a float. A record is one per pair per source slot, and a fixing is named by the start of its slot, so the 12:00 fixing is the slot that starts at 12:00:01 UTC. The [gold escrow](https://etchd.app/docs/gold-escrow) reads these records.

JSON form

```json
{
  "pair": "kraken:BTC/USD",
  "price": "67123450000000000000000",
  "quoted_at": "1767225599",
  "captured_at": "1767225601",
  "parser": 1,
  "kind": 0,
  "flags": 1,
  "ref": "0"
}
```

`etchd at` prints the price of a pair that was current at a moment, the last record captured at or before it, in the quote currency and checked against ETC first ([The Record at a Moment](https://etchd.app/docs/cli#at)). `records list --format csv` with a pair prints the whole series.

Gold at 05:00 UTC

```bash
etchd at "2026-10-28 05:00" swissquote:XAU/USD
```

The same on the local devnet

```bash
records current at 2026-10-26T13:38:30Z
id              branch  key                 time                  fields
179302190-1-21  price   swissquote:XAU/USD  2026-10-26T13:38:21Z  4,096.474 USD, mid, quoted 2026-10-26T13:38:20Z
2026-10-26T13-38  root bafyreiaxtqfbhb3rxtospd6tk3suqer7al6btfhvcmu5v57fnvjqiipntq  verified against ETC block 52, from the local copy
```

## Chain Head

The beacon follows about 35 chains ([The Beacon Chains](https://etchd.app/docs/ticks#beacon)). At each close the calendar compares the block a quorum of sources agreed on for each chain with the last one it committed, and writes a head record when the hash changed or when the chain has no record yet in that UTC day. Every day's bundle therefore holds the head of every chain that had a quorum that day. A Bitcoin or an Ethereum record is 104 bytes.

| Offset | Length | Field | Encoding |
| --- | --- | --- | --- |
| 11 | 32 | `chain` | sha256 of the CAIP-2 chain id |
| 43 | 1 | `kind` | 0 head, 1 final |
| 44 | 8 | `height` | uint64, the chain's own height |
| 52 | 8 | `seen_at` | uint64, when the calendar's quorum agreed on the block |
| 60 | 8 | `block_time` | uint64, the block's own time, 0 where a chain has none |
| 68 | 1 | `n_agree` | sources that named this block |
| 69 | 1 | `n_sources` | sources asked |
| 70 | 1 | `header_format` | 0 none, 1 the 80-byte Bitcoin header, 2 an RLP header |
| 71 | 1 | `hash_len` | 1 to 64 |
| 72 | hash_len | `hash` | the block hash as the chain shows it |

A `head` record names the newest block the sources reported, and a `final` one the newest block the chain's own protocol has finalized. For the UTXO and EVM chains the tick stores the raw block header as an evidence block in the day's bundle, so a reader checks the hash offline. When one block has several head records of a kind, readers and contracts take the earliest anchored one. The [Bitcoin swap](https://etchd.app/docs/bitcoin-swap) uses the Bitcoin head records.

`etchd at "2026-10-28 05:00" bip122:000000000019d6689c085ae165831e93` prints the Bitcoin head that was current at 05:00 UTC, its height, hash, kind and block time, from a day it checked against ETC first ([The Record at a Moment](https://etchd.app/docs/cli#at)).

## CT Head

Certificate Transparency logs sign a tree head over every certificate they hold. Every 5 minutes the source `ct-heads` asks each log shard in force for its signed tree head, and a record is written once per log, tree size and root hash in a day, so a log that has not moved keeps one record a day.

| Offset | Length | Field | Encoding |
| --- | --- | --- | --- |
| 11 | 32 | `log_id` | the RFC 6962 log ID |
| 43 | 8 | `tree_size` | uint64 |
| 51 | 8 | `timestamp_ms` | uint64, the tree head's own time |
| 59 | 32 | `root_hash` | the tree head |
| 91 | 8 | `captured_at` | uint64 |
| 99 | 2 | `sig_len` | uint16 |
| 101 | sig_len | `signature` | the log's signature as served, about 72 bytes for ECDSA P-256 |

The record keeps the log's own signature, and `etchd records verify --signatures` checks it with the log key the CLI build pins (or one you add with `--trust`), never a key only the registry snapshot names. ETC has no P-256 precompile, so no example contract checks it on chain.

## NIST Pulse

Each minute the NIST Randomness Beacon publishes a pulse of 512 random bits, signed with NIST's RSA key. The record keeps NIST's signed fields and the signature unchanged, with copies of the pulse index, time and output at fixed offsets. A record is 1,430 bytes today.

| Offset | Length | Field | Encoding |
| --- | --- | --- | --- |
| 11 | 8 | `captured_at` | uint64 |
| 19 | 8 | `chain_index` | uint64, NIST's chain |
| 27 | 8 | `pulse_index` | uint64 |
| 35 | 8 | `pulse_time` | uint64, the pulse time in Unix seconds |
| 43 | 64 | `output_value` | sha512(fields \|\| signature) |
| 107 | 2 | `fields_len` | uint16, 807 today |
| 109 | fields_len | `fields` | the pulse as NIST signs it |
| 109 + fields_len | 2 | `sig_len` | uint16, 512 |
| 111 + fields_len | sig_len | `signature` | NIST's RSA signature |

A pulse is named by its chain and pulse index, since NIST restarts the index with each new chain. The capture checks the signature with the key the pulse's certificate names and refuses a signature at or above the modulus, which would also pass the RSA check and give an output NIST never published. [NIST Randomness on ETC](https://etchd.app/docs/nist-randomness) checks the same bytes inside the EVM.

## Headline

A headline is one item of a news feed or of Hacker News, as its source id and up to four short fields, the title, the link, the published date and the guid. Each field has a length prefix and a bit in a presence byte. A headline holds no time and no rank, so one item gives the same bytes on every capture, and a calendar commits each distinct headline once per UTC day.

We read only capped metadata from a feed, the first 100 items with titles cut at 300 code points, links and guids at 500 and dates at 64, and from Hacker News the first 30 stories. No description, body, image or author is ever read. Text is normalized the same way everywhere, with entities decoded, white space folded, NFC applied and an over-long value cut to its cap with a trailing ellipsis.

## Front Page

A front page is one capture of one feed, its source id, the capture time, the parser version and its entries in rank order. Each entry is the 32-byte leaf of a headline, and for Hacker News also the score and the comment count. Every entry resolves inside the same day's bundle to a headline record, so a day stands alone.

A front page and the headlines it brings are sealed together in one tick, so a front page captured at 23:59:55 and sealed after midnight writes its headlines again into the new day. About 80 feeds are read every two hours, each at its own minute, and Hacker News every hour. [What We Publish](https://etchd.app/docs/what-we-publish) lists the feeds.

`etchd at` prints the front page of a source that was current at a moment, with its headlines expanded by rank to title and link, after it has checked the day against ETC ([The Record at a Moment](https://etchd.app/docs/cli#at)). The long form is `records list` with `--at` and `--resolve`.

The BBC front page at 05:00 UTC

```bash
etchd at "2026-10-28 05:00" bbc-front
etchd records list etchd-exports --key bbc-front --at "2026-10-28 05:00" --resolve --format table
```

On the local devnet, first lines

```bash
records current at 2026-10-26T13:38:10Z
id             branch      key        time                  fields
179302188-6-0  front-page  bbc-front  2026-10-26T13:38:05Z  12 headlines
     1  Garden allotment society finds a lost comet catalogue  http://fixtures.devnet/bbc/story/29914392?src=rss&n=0
     2  Northern ferry line delays the spring timetable  http://fixtures.devnet/bbc/story/29914397
     3  Island weather station opens a second reading room  http://fixtures.devnet/bbc/story/29914402?src=rss&n=2
```

## The Decoder

[decodeRecord](https://github.com/classix-dev/etchd/blob/main/packages/core/src/records/decode.ts) turns an envelope into its fields and its JSON form, with one decoder per branch and schema. The CLI, the record API and the explorer all use it, and no root check runs it. Large integers come out as decimal strings and byte strings as lowercase hex. A branch, schema or rule version the decoder does not know is reported as not decoded, and the day still passes when every root matches.

Decode one record

```bash
etchd records decode 0x0300000001000100...
etchd records show day.car 29882000-1-3
curl -s https://etchd.app/v1/records/29882000-1-3 | jq .decoded
```

## The Generic Record Check

One call of EtchdVerifier v2, `recordExistedAtBy(operator, branchId, record, recordPath)`, proves a record of any branch, including one added after the contract was deployed. It checks the envelope header, folds the record into its branch root, the branch leaf into the branches root, hashes the 128-byte tick root, folds the batch and asks EtchdRegistry v2 when that operator sent the result. It returns that time and block, or reverts.

The path cannot be bent. Every hop is 64 bytes, the branch step is 37 bytes and the tick step 128, so a forged path has to follow the real trees down to a real record of that branch. A user's stamp sits in the calendar root at offset 32 of the tick root, where no record path goes, so a stamped digest never passes as a record. [Proofs in Contracts](https://etchd.app/docs/contracts#check) walks through the call, its gas and the operator address a contract has to pin. `etchd records proof` prints its input ([Proofs for Contracts](https://etchd.app/docs/cli#records-proof)).

## Branches 7 to 29

A new branch takes the next id, a layout and a parser, and needs no new tick version and no new contract. Branches 7 to 29 are built in the same release and enter one family at a time after the record starts, from drand rounds and signed log checkpoints to official figures, quakes, sanctions files and model catalogs. Their layouts are under [Branches 7 to 29](https://github.com/classix-dev/etchd/blob/main/SPEC.md#branches-7-to-29) in SPEC.md, and [Record Sources](https://etchd.app/docs/sources) gives each family's sources, credits and trust class.
