Mirrors

A mirror holds a copy of the daily bundles on storage we do not control, and with a few of them around the history outlives the calendar. Anyone can run one.

Diagram. The etchd mirror command checks each daily CAR four ways, then stores it in a folder, a Kubo node run by Docker Compose or a pinning service. A day that fails is stored nowhere. The etchd mirror command checks each day four ways before it stores anything, then writes it to a folder, a Kubo node or a pinning service. A day that fails a check is stored nowhere.

The Etchd Mirror Command

etchd mirror copies the exports to a directory, a Kubo node or a pinning service, and checks each day before it stores it. The file has to match the index, each block has to hash to its CID, tick bodies must rebuild their own roots, and the anchored tick roots must match the calldata of their ETC transactions. After that it looks up 10 random anchored ticks a day on the ETC RPCs (--verify all looks up every one). A failed day is stored nowhere.

The CLI lives in the repo and runs on Node 22.

Install
git clone https://github.com/classix-dev/etchd
cd etchd
pnpm install
alias etchd="node $PWD/packages/cli/src/bin.ts"
To a directory
etchd mirror -c https://etchd.app --dir ./etchd

That writes <date>.car files and an index.json into ./etchd.

To your own Kubo node
etchd mirror -c https://etchd.app --ipfs --kubo-api http://127.0.0.1:5001

Each CAR goes through Kubo's dag/import with its root pinned. When that pin fails on the one tick body from before the record, the mirror pins each block of the day on its own instead (The Start of the Record).

To a pinning service
FILEBASE_TOKEN=... etchd mirror -c https://etchd.app --pin-to filebase --token-env FILEBASE_TOKEN
PINATA_JWT=... etchd mirror -c https://etchd.app --pin-to pinata --token-env PINATA_JWT

Filebase takes the CAR through its Kubo-compatible RPC API with a bucket token, and Pinata through its v3 upload API with car=true, which needs a paid plan. The token stays in your environment and goes only to the service you name.

A second run fetches only the missing days. The mirror starts at the first day of the record, 2026-10-24, and a --since before it is raised to it. --etc-rpc points the checks at your own ETC node. The exit codes, set in packages/cli/src/cmd/mirror.ts, are 0 when every day is there, 3 when a day failed a check and 4 when a calendar, an RPC or a target was unreachable or refused a CAR.

The Start of the Record

Each tick body links to the tick before it through prev_body_cid. The mirror checks those links. Within a day each body has to name the body before it, and the first tick of a day has to name the last tick of the calendar day before it when the same run read both. The first tick of 2026-10-24 names a body from 2026-10-23, which we deleted with the rest of the days before the record started. The mirror accepts that link on that tick and nowhere else, and reports it as verify.boundary with the tick and the CID, plus a line in the text output.

The same link reaches into IPFS pins. A recursive pin needs every block its root links to, and through the chain of ticks every day from 2026-10-24 on links back to the deleted body. Kubo's dag/import pins offline, and on a fresh node the recursive pin of each day fails on that block. The mirror then pins every block of the day's CAR directly, with pin/add and recursive=false, and reports the target stored with pin: "direct". On the next run a root pinned either way counts as already there. Pinata takes the CAR as an upload and is not affected.

Our Copy on Filebase

We run one mirror ourselves. The calendar imports each day's CAR into Filebase with its root pinned, checks the pin and reads the root block back from the Filebase gateway. The day's entry in /v1/exports then carries filebase.cid and filebase.url, and the day can be fetched by that CID without etchd.app.

Fetch a pinned day by CID
curl -s "https://ipfs.filebase.io/ipfs/<cid>?format=car" > day.car
ipfs pin add <cid>     # on your own Kubo node

That copy lasts as long as our Filebase account does, and a mirror you run yourself does not depend on it. The public ipfs.io and dweb.link gateways no longer fetch blocks they do not hold, as the Daily Bundles page explains, and we have not yet checked whether Filebase announces the blocks to the DHT. Nor have we checked whether Filebase pins a day whose tick chain reaches back to the deleted body. If it does not, days from 2026-10-24 on get no filebase field.

A Kubo Node in Docker Compose

deploy/mirror/docker-compose.yml runs a Kubo node next to a small container that calls etchd mirror --ipfs every six hours, keeps a copy of each CAR in a volume and pins each day on the node. Start it from the repo root.

docker compose -f deploy/mirror/docker-compose.yml up -d --build
docker compose -f deploy/mirror/docker-compose.yml logs -f mirror

Kubo publishes only its swarm port, 4001. That is enough for the blocks to reach the IPFS DHT and for other nodes to fetch them, and the RPC API and the gateway stay inside the compose network. The mirror checks a sample of 10 anchored ticks a day unless you set MIRROR_VERIFY to all, and ETCHD_ETC_RPC points it at your own ETC node in place of the three public defaults. The container image is built from deploy/mirror/Dockerfile and runs deploy/mirror/mirror.sh on a timer.