Scripts and settings
Every script, setting, port and endpoint, with defaults.
Scripts
Section titled “Scripts”| Script | Runs on | Does |
|---|---|---|
provision.sh | your machine | Creates the firewall, volume and server on Hetzner with hcloud. Checks the price first. Prints the IPv4 address |
node.sh | VM (ssh … bash -s < node.sh) | all (default): checks, Docker, disk helpers on small disks, official bootstrap, wait for sync, health check. Also wait, health, helpers |
deploy-client.sh | your machine | Builds the app on your computer, copies it and Caddy to /opt/snapchain-client |
teardown.sh | your machine | Deletes the server, volume and firewall; lists what is left. Safe to run twice |
watchdog.sh | your machine | Runs teardown.sh before the bill reaches MAX_USD, after MAX_WALL_SECS, or after HANG_SECS with no progress |
disk-guard.sh | VM (systemd timer; node.sh installs it) | install, check, status, uninstall. Stops the node at 90% disk |
snapshot-space-saver.sh | VM (systemd; node.sh installs it) | install, run, once, uninstall. Deletes snapshot files the node has finished with; stops when the restore is done |
provision.sh, node.sh, deploy-client.sh, teardown.sh and the two VM helpers take DRY_RUN=1: they print what they would do and change nothing. watchdog.sh does not. Tokens come from the environment and are never printed.
Environment variables
Section titled “Environment variables”| Variable | Script | Default | Notes |
|---|---|---|---|
HCLOUD_TOKEN | provision, teardown, watchdog | (required) | Hetzner API token, read/write |
SSH_KEY | provision | (required) | Name or ID of an SSH key in your Hetzner project |
SERVER_TYPE | provision | cx43 | Tested with cax31. Empty = auto-pick from SERVER_TYPE_CANDIDATES. If it isn’t available to rent in LOCATION, the error lists what is, cheapest first |
SERVER_TYPE_CANDIDATES | provision | cx43 cax31 cpx42 ccx23 | Cheapest first |
VOLUME_GB | provision | 2000 (REQUIRED_DISK_GB) | 1000 fits a new account’s quota |
LOCATION | provision | fsn1 | fsn1, nbg1, hel1, ash, hil or sin. Server and volume share it |
MAX_USD | provision, watchdog | 8 | Price limit for the check before creating anything; the watchdog deletes everything at it. In your Hetzner account’s currency |
EXPECTED_HOURS | provision | 24 | Hours the price check multiplies by. Our cax31 sync took 14 h 06 m |
WATCHDOG | provision | 1 | 0 = no watchdog; the server runs until you delete it. The Full run uses 0 |
ESTIMATE_ONLY | provision | 0 | 1 = price check only, creates nothing |
SSH_SOURCE_CIDRS | provision | 0.0.0.0/0,::/0 | Set to "$MY_IP/32" (from curl -4 -s https://ifconfig.me) |
NODE_API_CIDRS | provision | empty | Addresses allowed to reach 3381 and 3383. Empty keeps both closed |
PORT_PROFILE | provision | docs | readme also opens the README’s TCP+UDP superset |
NAME | provision, teardown, watchdog | snapchain-read-node | Volume $NAME-data, firewall $NAME-fw. The Full run exports it, so every step and teardown.sh use the same name |
REUSE | provision | 0 | If the server $NAME, the volume $NAME-data or an applied firewall $NAME-fw already exists, provision.sh stops before it creates or changes anything. 1 continues with them (your own earlier run) |
VOLUME_QUOTA_GB | provision | 1024 | Account-wide volume limit (new Hetzner accounts: 1024 GB across all projects). Before creating anything, provision.sh adds the volumes in this project to VOLUME_GB and stops if the sum is over it. Volumes in other projects are not visible, so Hetzner can still refuse. Set it if Hetzner raised your limit |
WATCHDOG_MAX_WALL_SECS | provision | 86400 | Time limit provision.sh passes to the watchdog (as MAX_WALL_SECS; MAX_WALL_SECS on provision.sh works too). 172800 = 48 hours |
CONFIRM | teardown | (prompt) | Must equal NAME |
OTHER_SERVER_OK | teardown | 0 | teardown.sh stops and deletes nothing if the server named $NAME has a different ID from the one provision.sh recorded in .provision-state/server-$NAME.id. 1 deletes it anyway |
SNAPCHAIN_AGREE_NO_REWARDS | node.sh | (prompt) | yes records the no-rewards agreement |
OPERATOR_FID | node.sh | unset | Enables the weekly auto-upgrade cron; unset = SKIP_CRONTAB=true |
MIN_FREE_GB | node.sh | 1500 | 900 for a 1000 GB volume |
SYNC_TIMEOUT_SECS | node.sh | 86400 | 24 hours; our cax31 sync took 14 h 06 m |
DISK_HELPERS | node.sh | auto | auto installs disk-guard and the space saver when the disk is under DISK_HELPERS_BELOW_GB; 1 always; 0 never |
DISK_HELPERS_BELOW_GB | node.sh | 1500 | |
SPACE_SAVER_WAIT_SECS | node.sh | 7200 | How long the space saver waits for the snapshot folder to appear |
SNAPSHOT_REPAIR | node.sh | auto | If a shard’s snapshot download fails with no upstream retry, restore only that shard. 0 only prints the command |
REPAIR_ATTEMPTS / REPAIR_PARALLEL | node.sh | 3 / 4 | Fresh snapshot lists per repair; parallel chunk downloads |
SYNC_MAX_BLOCK_DELAY / SYNC_STABLE_CHECKS / SYNC_POLL_SECS | node.sh | 10 / 3 / 60 | Sync criterion |
NODE_UPGRADE | node.sh | 0 | 1 re-runs the official bootstrap (upgrade) |
SSH_OPTS | deploy-client | empty | Extra ssh/scp options, e.g. -i key |
LIMIT_PCT | disk-guard | 90 | |
HANG_SECS / MAX_WALL_SECS | watchdog | 7200 / 86400 | No-progress limit; total time limit. When provision.sh starts the watchdog, set the time limit with WATCHDOG_MAX_WALL_SECS |
SSH_KEY_FILE | watchdog | empty | Private key for the progress check. Empty turns the check off |
PROGRESS_MIN_BYTES | watchdog | 1048576 | Database growth that counts as progress; any block height increase also counts |
UNREACHABLE_IS_HANG | watchdog | 0 | 1 counts lost SSH as no progress |
| Port | Purpose | Open in the firewall |
|---|---|---|
| 3381/tcp | Snapchain HTTP API | No, unless you set NODE_API_CIDRS. Caddy reaches it on 127.0.0.1 |
| 3382/udp | Gossip (libp2p QUIC) | Yes |
| 3383/tcp | Snapchain gRPC API | No, unless you set NODE_API_CIDRS |
| 80/tcp, 443/tcp | Caddy: Let’s Encrypt and the HTTPS client | Yes |
| 22/tcp | SSH | Yes, limit with SSH_SOURCE_CIDRS |
| 3000/tcp | Grafana (default admin:admin) | No. Keep it closed |
| 8125/udp, 8126/tcp | statsd | No |
Docker publishes 3381, 3383, 3000 and the statsd ports on every interface of the server, so the Hetzner firewall is what keeps them private. The app reaches the node through Caddy on 443. To read the API from your computer, use https://<name>/v1/... or ssh -N -L 3381:127.0.0.1:3381 root@$IP.
HTTP endpoints the client uses
Section titled “HTTP endpoints the client uses”/v1/info, /v1/castsByFid, /v1/castById, /v1/linksByFid, /v1/linkById, /v1/reactionById, /v1/userDataByFid, /v1/signersByFid, /v1/signer, /v1/onChainIdRegistryEventByAddress, POST /v1/submitMessage. Reference: HTTP API.
Repo layout
Section titled “Repo layout”| Path | What |
|---|---|
client/ | The web app: Vite and TypeScript, @farcaster/core 0.20.0 |
deploy/ | Caddyfile, Caddy compose file, Hetzner firewall rules |
test/, e2e/ | Tests for the scripts; browser tests against a mock node and the live node |
site/ | This site (Astro Starlight) |
MEASUREMENTS.md, run-log.md | Numbers and log from the mainnet run |
DOCS_ISSUES.md, SOURCES.md | Problems found in the official docs; the source for each fact |
SECURITY.md, VOUCHER.md | How keys are handled; the checklist for vouching |
Sources
Section titled “Sources”- Snapchain v0.14.3, commit
fc87f26: github.com/farcasterxyz/snapchain - Gasless app keys: writing-messages; live since engine version V16 on 2026-05-07 (
src/version/version.rs); the guide was updated in docs PR #989 - Read nodes pass messages on: node-types (documented in docs PR #1034); code:
submit_message_internalinsrc/network/server.rs,ReadNodeMempoolinsrc/mempool/mempool.rs - Getting Started, managing-a-node
- Full list:
SOURCES.md