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

The Anchor Contract

You don't need a contract of your own. EtchdAnchor at 0xa6f241d2 has no owner and takes posts from any sender, logging each one under the sender's address. Your signer address tells your anchors from ours.

A copy is optional. 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 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, and the Worker runs with the production env of packages/worker/wrangler.jsonc. Change these:

FileSettingSet to
scripts/lib/prod.tsCLOUDFLARE_ACCOUNTYour account id
scripts/lib/prod.tsZONEYour domain
scripts/lib/prod.tsETC_RPCSThe RPCs deploy.ts checks the contract on
packages/worker/wrangler.jsoncroutesYour domain and a.etc.calendar.<your domain>
packages/worker/wrangler.jsoncCALENDAR_URIhttps://a.etc.calendar.<your domain>
packages/worker/wrangler.jsoncETC_RPCSThe RPCs your Anchor reads
packages/worker/wrangler.jsoncIA_ITEM_PREFIXA 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).

The chain registry and the witness sources live in KV. The deploy seeds the registry key from 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, the RSS front pages of 80 news outlets among them, and publishers' takedown requests 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, 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.

VariableGoes toNeeded
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_IDdeploy.tsYes, except for --dry-run
ANCHOR_CONTRACTA var on both WorkersYes
ETCHD_SIGNER_KEYSigner secretYes
ETCHD_SIGNER_KEY_NEXTSigner secretFor a key rotation
ADMIN_TOKENWorker secretYes, for /v1/admin
ALERT_WEBHOOKWorker secretNo
IA_ACCESS_KEY, IA_SECRET_KEYWorker secretsNo, and both or neither
FILEBASE_RPC_TOKENWorker secretNo

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

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 and 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 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. The witness fetchers send a User-Agent with our URL in it. Change them in your fork.