# Mirrors

A mirror holds a copy of the [daily bundles](https://etchd.app/docs/data) on storage we do not control, and with a few of them around the history outlives the calendar. Anyone can run one.

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](https://etchd.app/docs/mirrors#boundary)).

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`](https://github.com/classix-dev/etchd/blob/main/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](https://etchd.app/docs/data#ipfs) 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`](https://github.com/classix-dev/etchd/blob/main/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`](https://github.com/classix-dev/etchd/blob/main/deploy/mirror/Dockerfile) and runs [`deploy/mirror/mirror.sh`](https://github.com/classix-dev/etchd/blob/main/deploy/mirror/mirror.sh) on a timer.
