# Self Hosting

You can run an Etchd calendar on your own Cloudflare account. It answers on your domain and anchors with your key, which pays its own gas, and it deploys with the script that put etchd.app live on 2026-10-21. We haven't yet deployed a second calendar on another account ourselves, and the steps below come from the scripts and from our own deploys.

## What Runs Where

Etchd is built on Cloudflare Workers, Durable Objects, D1, R2, KV and Workflows. Of the two Workers in a calendar, `etchd` serves the API, the explorer and the calendar host, while `etchd-signer` holds the anchor key and answers only the main Worker's service binding. No standalone server binary exists.

For local experiments, the Docker Compose devnet in [docker/README.md](https://github.com/classix-dev/etchd/blob/main/docker/README.md) runs both Workers under `wrangler dev` against Anvil with no Cloudflare account at all.

You need Workers Paid. By our arithmetic at one tick a minute, the Durable Objects take about 97,000 requests a day against the Free allowance of 100,000, and the Anchor and each BeaconGroup stay awake around the clock at about 11,000 GB-s a day each, while a Free account gets 13,000 GB-s for everything it runs. Free allows 50 subrequests per invocation. Our beacon alarm plans up to 100. We took those limits from Cloudflare's pricing pages and haven't tried Etchd on a Free account ([Workers Free and Workers Paid](https://github.com/classix-dev/etchd/blob/main/DEPLOY.md#workers-free-and-workers-paid)).

## The Anchor Contract

You don't need a contract of your own. EtchdAnchor at [`0xa6f241d2`](https://etc.blockscout.com/address/0xa6f241d25efa4e8582b808fa44cda1a68cd69fed) has no owner and [takes posts from any sender](https://etchd.app/docs/anchor#noticeboard), logging each one under the sender's address. Your signer address tells your anchors from ours.

A copy is optional. [scripts/deploy-contract.ts](https://github.com/classix-dev/etchd/blob/main/scripts/deploy-contract.ts) deploys one at 1 gwei from the key in `ETCHD_SIGNER_KEY`, and with `--dry-run` it prints the gas estimate and the address the contract will get. Ours took 68,141 gas. Give the script a separate deployer key if you can, because the Anchor halts when another transaction uses one of its nonces, and the script refuses a key that has sent transactions before unless you pass `--force`.

Optional, your own copy

```
ETCHD_SIGNER_KEY=0x... node scripts/deploy-contract.ts --dry-run
ETCHD_SIGNER_KEY=0x... node scripts/deploy-contract.ts
```

## Before You Start

You need these:

- A Cloudflare account on Workers Paid, with your domain as a zone in it.
- An API token. [deploy.ts](https://github.com/classix-dev/etchd/blob/main/scripts/deploy.ts) calls the D1, KV and R2 APIs itself and wrangler does the rest, which comes to the account permissions D1 Edit, Workers KV Storage Edit, Workers R2 Storage Edit and Workers Scripts Edit, plus Workers Routes Edit on your zone. We haven't tried a narrower token.
- Node 22.18 or later and pnpm, or Nix. We run our deploys in `nix shell nixpkgs#nodejs_22 nixpkgs#pnpm nixpkgs#foundry` from the repo root, after `pnpm install`.
- An ETC key used for nothing else, holding a few days of gas.

One anchor takes about 23,100 gas. At 1 gwei and 1,440 anchors a day, one for each UTC minute, that comes to 0.033264 ETC a day or about 1 ETC a month.

## Configure

The deploy script holds the production configs to the constants in [scripts/lib/prod.ts](https://github.com/classix-dev/etchd/blob/main/scripts/lib/prod.ts), and the Worker runs with the `production` env of [packages/worker/wrangler.jsonc](https://github.com/classix-dev/etchd/blob/main/packages/worker/wrangler.jsonc). Change these:

| File | Setting | Set to |
| --- | --- | --- |
| `scripts/lib/prod.ts` | `CLOUDFLARE_ACCOUNT` | Your account id |
| `scripts/lib/prod.ts` | `ZONE` | Your domain |
| `scripts/lib/prod.ts` | `ETC_RPCS` | The RPCs deploy.ts checks the contract on |
| `packages/worker/wrangler.jsonc` | `routes` | Your domain and a.etc.calendar.<your domain> |
| `packages/worker/wrangler.jsonc` | `CALENDAR_URI` | https://a.etc.calendar.<your domain> |
| `packages/worker/wrangler.jsonc` | `ETC_RPCS` | The RPCs your Anchor reads |
| `packages/worker/wrangler.jsonc` | `IA_ITEM_PREFIX` | A prefix of your own, with archive.org keys |

`deploy.ts` refuses a `CLOUDFLARE_ACCOUNT_ID` other than the one in prod.ts. The guard takes the calendar URI only in the form `https://a.etc.calendar.<zone>`, with `ETC_QUORUM` at 2 and `CAL` at `a`. Item names at archive.org are global, and without `IA_ITEM_PREFIX` yours would be named `etchd-a-<date>` like ours. Chain 61, the 1 gwei gas price, 60 s ticks, the 0.05 ETC daily cap and the internal depths of 12 and 400 are fixed, while the depth at which proofs are served (`PROOF_DEPTH`, default 1) is a var ([Fixed Parameters](https://github.com/classix-dev/etchd/blob/main/DEPLOY.md#fixed-parameters)).

The chain registry and the witness sources live in KV. The deploy seeds the `registry` key from [packages/core/src/chains/prod.ts](https://github.com/classix-dev/etchd/blob/main/packages/core/src/chains/prod.ts) when it is missing, and `--reseed` overwrites it. Without a `witness_sources` key the Scheduler captures the list in [packages/core/src/witness/registry.ts](https://github.com/classix-dev/etchd/blob/main/packages/core/src/witness/registry.ts), the RSS front pages of 80 news outlets among them, and publishers' [takedown requests](https://github.com/classix-dev/etchd/blob/main/DEPLOY.md#takedowns) would come to you. An empty list turns captures off.

No witness captures

```
wrangler kv key put witness_sources '{"env":"production","sources":[]}' --namespace-id <id> --remote
```

The `config` key holds the [live settings](https://github.com/classix-dev/etchd/blob/main/docs/operations.md#live-settings), read every 30 s. On a new calendar we would set `alertWebhook` first. Setting `btcBridge` to `false` stops the hourly Bitcoin roll-ups, 24 digests a day to the OpenTimestamps aggregators.

## Secrets

Keys and tokens come from the environment. The script hands each secret to `wrangler secret put` on stdin without printing it, and a secret missing from the environment keeps its old value.

| Variable | Goes to | Needed |
| --- | --- | --- |
| `CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID` | deploy.ts | Yes, except for --dry-run |
| `ANCHOR_CONTRACT` | A var on both Workers | Yes |
| `ETCHD_SIGNER_KEY` | Signer secret | Yes |
| `ETCHD_SIGNER_KEY_NEXT` | Signer secret | For a key rotation |
| `ADMIN_TOKEN` | Worker secret | Yes, for /v1/admin |
| `ALERT_WEBHOOK` | Worker secret | No |
| `IA_ACCESS_KEY, IA_SECRET_KEY` | Worker secrets | No, and both or neither |
| `FILEBASE_RPC_TOKEN` | Worker secret | No |

`ANCHOR_CONTRACT` is our address or your copy. The signer keys are `0x` and 64 hex characters.

## Deploy

Start with a dry run. It needs no token and calls no Cloudflare API, and it bundles both Workers with `wrangler deploy --dry-run`.

Dry run, then deploy

```
node scripts/deploy.ts --dry-run

CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... \
ANCHOR_CONTRACT=0x... ETCHD_SIGNER_KEY=0x... ADMIN_TOKEN=... \
node scripts/deploy.ts
```

The real run, in order:

1. checks the configs against the guards
2. confirms that at least two ETC RPCs serve the anchor runtime at `ANCHOR_CONTRACT`
3. creates the missing D1 databases, KV namespace and R2 buckets, with r2.dev access off
4. writes `wrangler.production.generated.jsonc` next to each config and applies the D1 migrations
5. seeds KV and builds the explorer
6. deploys `etchd-signer`, then `etchd`, and sets the secrets

Run it again whenever you like, since it creates only what is missing. `--skip-secrets` leaves the secrets alone and `--skip-build` reuses `packages/web/dist`. Through pnpm it is `pnpm run deploy`, because `pnpm deploy` is a pnpm command of its own.

## Smoke Test

[scripts/smoke.ts](https://github.com/classix-dev/etchd/blob/main/scripts/smoke.ts) exits 1 if any check fails. From outside, it stamps one random digest and finds it again on both hostnames, makes sure the explorer comes from Static Assets while the API paths reach the Worker, and hashes a sample of published witness bodies. It defaults to our hosts. Name yours.

Smoke test

```
node scripts/smoke.ts https://example.com --calendar https://a.etc.calendar.example.com
```

## Stamping Against It

The [CLI](https://etchd.app/docs/cli) installs from the GitHub release with one line, once we have pushed the first `cli-v*` tag. With the clone you deploy from, `pnpm cli:pack` builds the same tarball.

Install the CLI

```
npm i -g https://github.com/classix-dev/etchd/releases/latest/download/etchd-cli.tgz
# or, from the repo root
pnpm cli:pack && npm i -g ./packages/cli/dist/etchd-cli.tgz
```

Without `-c` the CLI stamps on our calendar. `ETCHD_CALENDAR` sets yours for every command. A proof names the calendar it came from, and `upgrade` asks that one with no flag.

Stamp, upgrade, verify

```
etchd stamp report.pdf -c https://a.etc.calendar.example.com
etchd upgrade report.pdf.ots --wait
etchd verify report.pdf.ots --signers https://example.com/v1/signers --require-known-signer
```

## Your Signer List

`/v1/signers` on your calendar lists each [signer epoch](https://etchd.app/docs/signers) with the time and tick it started at, the launch key as epoch 1. The Anchor records them itself. The verifier fetches that list from the calendar a proof names, and whoever hands over a proof picks that calendar. Publish your signer addresses in a second place under your name, such as a repo, where a reader can copy them into a file for `--signers`.

## Running It

Alerts go as a JSON POST to `ALERT_WEBHOOK` or `alertWebhook`. The Anchor holds back a repeat of the same alert key for 10 min, which keeps a stuck condition from flooding the hook.

`low-balance` fires when the hot wallet holds less than one day of spend, 0.033264 ETC by default, and the watchdog reads the balance every five minutes. `/v1/status` shows it as `balanceWei`. We top ours up by hand. The signer stops for the rest of a UTC day after 0.05 ETC of gas, counting the full 30,000 gas limit of each signature, which reserves 0.0432 ETC on a normal day.

To rotate, put the new key on the signer as `ETCHD_SIGNER_KEY_NEXT`, fund it with at least one day of spend, and call the route below. Proofs from the old epoch stay valid. Leave about 0.001 ETC on the old address for 30 days, because the reorg audit can send an old-epoch transaction back to the mempool. After that, promote the secret as [Key Rotation](https://github.com/classix-dev/etchd/blob/main/DEPLOY.md#key-rotation) in DEPLOY.md describes.

Rotate

```
curl -X POST https://example.com/v1/admin/rotate -H "authorization: Bearer $ADMIN_TOKEN" \
  -H "content-type: application/json" -d '{"reason": "move to the long-term key", "waitSeconds": 60}'
```

Beacon rows go to one database per UTC year, and a calendar still running on 2027-01-01 needs a `BEACON_DB_2027` binding added in December 2026.

Daily exports start by themselves for each UTC day that ends after the deploy. The backfill loops in DEPLOY.md rebuild ticks sealed before a feature shipped, and a calendar started today needs none of them until an upgrade brings one. For a copy elsewhere, run `etchd mirror -c https://example.com --dir ./mirror` on another machine ([Mirrors](https://etchd.app/docs/mirrors)).

A halted Anchor stays halted until `POST /v1/admin/resume`. Durable Object storage keeps 30 days of point-in-time recovery, and the full runbooks for etchd.app are in [DEPLOY.md](https://github.com/classix-dev/etchd/blob/main/DEPLOY.md) and [docs/operations.md](https://github.com/classix-dev/etchd/blob/main/docs/operations.md).

## Costs

Cloudflare charges $5 a month for Workers Paid, plus usage past what the plan includes. Usage depends on your traffic, and [Cloudflare's pricing page](https://developers.cloudflare.com/workers/platform/pricing/) has the current rates. We haven't checked Workflows step pricing on our own account yet. Our daily exports take about 60 MB a day in R2, under 2 GB a month, and at $0.015 per GB-month that stays under $3 in the first year.

Gas is 1 gwei × about 23,100 gas × 1,440 transactions, or 0.033264 ETC a day. A contract of your own adds about 0.00007 ETC once.

## Known Limits

Parts of the code still name etchd.app. The CLI's default calendar, the defaults of `smoke.ts`, the `--status-url` default of `deploy-contract.ts` and the line `deploy.ts` prints when it finishes all point at our hosts. The explorer you deploy carries these docs, and they describe our calendar and our contract from the constants at the top of [packages/web/src/pages/docs/Doc.tsx](https://github.com/classix-dev/etchd/blob/main/packages/web/src/pages/docs/Doc.tsx). The witness fetchers send a User-Agent with our URL in it. Change them in your fork.
