townhall
No app can ban you.
No app between you and Farcaster: your own client and Snapchain read node, in three commands.
The demo is the author's own node and client, running from these exact scripts.
Quickstart
From your computer, with a Hetzner Cloud API token:
curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/provision.sh | HCLOUD_TOKEN=your_token bash
It prints your server's IP. Then:
ssh root@YOUR_IP 'curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/node.sh | bash'
ssh root@YOUR_IP 'curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/client.sh | bash'
Open the https://…sslip.io address the last command prints. The client shows your node's progress in the top bar. Done testing? One more command deletes the server and the volume so billing stops:
curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/destroy.sh | HCLOUD_TOKEN=your_token bash
What to expect, measured
From the demo node: Hetzner cx43 (8 vCPU, 16 GB) in Falkenstein with a 1 TB volume, October 2026, Snapchain 0.14.3.
| Measured | |
|---|---|
| Hands-on time | About 10 minutes |
| Snapshot download and unpack | About 11 hours. The snapshot is about 5,700 chunks, roughly 445 GB compressed. |
| Catching up after the snapshot | About 10 hours. The node replays the blocks made since the snapshot, around 290 blocks a minute faster than the chain grows. |
From node.sh to in sync | Under 22 hours. Start in the evening, use it the next afternoon. |
| Disk | Peak about 800 GB while unpacking. About 560 GB once synced: 914 million messages from 3.36 million FIDs. |
| Adding the app key | One KEY_ADD, active about 10 seconds later. No gas. |
Prefer to read before you run? Every script is short and commented: install/. The client is built from source on your server, so what runs is what is in the repo.

What you get
- A read node. A full, verified copy of every cast, reaction, follow and profile on Farcaster, kept in sync with the network. Anyone can run one.
- A client. Home feed of the people you follow, profiles, threads, posting, replies, likes, recasts, follows. It also shows which app signed each cast and lets you hide or show casts by app.
- Your own app key. The client signs with a key that belongs to you and that you added to your account yourself. Nobody in the middle can post for you, hide your feed or block your writes, as long as your node is up.
Before you start
| You need | Why | How |
|---|---|---|
| A server with 4 vCPU, 16 GB RAM and a 1 TB disk | Snapchain documents 2 TB. 1 TB is enough to sync today: the demo node peaked at about 800 GB (see measured). Use 2 TB if you keep the node for months, since the network keeps growing. | Step 0 creates one on Hetzner. Create an account, a project, then Security → API tokens → Generate with Read & Write. |
| A Farcaster test account that has storage, and its recovery phrase | Only to post. The phrase signs one KEY_ADD message that adds your app key. No gas. Read-only mode needs just a username. | In the Farcaster app settings, under Advanced → Recovery phrase. Check its storage on your node: /v1/storageLimitsByFid?fid=…. |
A terminal with curl, jq, ssh | To run the three commands. | macOS: brew install jq. Ubuntu: sudo apt install jq. |
Hands-on time is about 10 minutes. The node then syncs on its own, for most of a day.
Step 0 · Get a server
curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/provision.sh | HCLOUD_TOKEN=your_token bash
What it does, in order:
- Uses your SSH key (
~/.ssh/id_ed25519.pub, created if missing) and uploads it to Hetzner, so only you can log in. - Picks the cheapest server type in the location with at least 4 vCPU and 16 GB RAM.
- Creates a firewall that only opens SSH (22), HTTP/HTTPS for your client (80, 443) and UDP 3382, the port Snapchain nodes use to talk to each other.
- Creates the server with Ubuntu 24.04 and attaches a 2,000 GB volume for the node's data.
- Waits until it can SSH in, then prints the next two commands with your IP filled in.
Options: LOCATION=nbg1, SERVER_TYPE=…, VOLUME_GB=…. A server is billed while it exists, see cost & cleanup.
Step 1 · Run a Snapchain read node
ssh root@YOUR_IP 'curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/node.sh | bash'
- Installs Docker.
- Finds the disk with the most free space (your 2 TB volume) and puts the node there.
- Downloads the official
docker-compose.mainnet.ymlandvalidators.tomlfrom the latest Snapchain release, unchanged. That file already configures a read node (read_node = true). - Starts the node. It first downloads a snapshot of the network, then replays recent blocks from peers until it catches up.
- Starts a small snapshot guard. On first start the node keeps every downloaded snapshot file until the very end, which is why the docs ask for 2 TB. The guard deletes each file as soon as the node has finished with it, so the first sync should peak around 850 GB instead of about 1.5 TB (our estimate from the snapshot sizes in October 2026). It stops by itself when the sync is done.
- Installs
townhall-status, a one-word command that shows how far behind each shard is.
It prints one line per shard with its height and how many blocks it is behind. The node is in sync when every shard is within a few blocks.
The node answers on port 3381 with Snapchain's HTTP API. Try it on the server: curl -s localhost:3381/v1/castsByFid?fid=1 | jq '.messages[0].data.castAddBody.text'
Step 2 · Create your client
ssh root@YOUR_IP 'curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/client.sh | bash'
- Clones this repository and builds the client inside a throwaway Node container.
- Starts Caddy, which gets an HTTPS certificate for
YOUR-IP.sslip.io(a free wildcard DNS name that points at your IP; setDOMAIN=your.domainto use your own). - Serves the client and forwards
/v1/*to your node. The browser only ever talks to your server: same origin, no CORS, no third-party API.
Re-run the same command any time to update the client.
Using the client
Read-only
Type your username. The client resolves it to your FID on your node, loads who you follow, and builds your feed from their latest casts. Nothing is signed and nothing is sent anywhere.
Connect your account (to post)
- Paste your recovery phrase and press Check. The page derives your custody address from the phrase and reads your FID from the IdRegistry contract on Optimism. Nothing is sent yet.
- Press Add app key. The page generates a new Ed25519 keypair (your app key) and reads your current key nonce from your node (
/v1/signersByFid→currentUserNonce). Your custody account then signs two EIP-712 messages: a SignedKeyRequest (which app asks for the key, here your own FID) and a KeyAdd (the key, what it may sign, how long it lives, the next nonce). - It goes to your node as a message. Both signatures ride in a
KEY_ADDmessage whose envelope is signed by the new key itself, proof that you hold it. Your node checks it and relays it like a cast. About a second later the key is active. The page clears the phrase and keeps only the app key in this browser.
Post, reply, like, recast, follow
Each action builds a protocol message, signs it with your app key in the browser, and submits the raw bytes to your node. Mentions like @alice are resolved on your node and stored as protocol mentions.
Include or exclude casts by app
Every cast is signed by an app key, and every app key records which app requested it (the app's own FID). The client looks this up on your node for each author and shows it as via farcaster, via own key and so on. Tap an app in the bar above the feed to hide or show its casts. Your choice is remembered in this browser.
Live: the network as your node sees it
Live shows every message the network accepts, a second or two after your node receives it: casts, likes, follows, profile changes, and messages pruned when someone goes over their storage limit. It reads your node's event log (/v1/events, one log per shard) and its block heights (/v1/info). Nothing here comes from anyone else's server.
Inspect a cast
Tap { } under any cast to see the protocol message behind it: its BLAKE3 hash, the Ed25519 key that signed it, which app requested that key, and a link to the raw JSON on your node. Your node stored the cast only after checking all three.
Search
Type a username or an FID. Your node resolves names with /v1/userNameProofByName; follower and following counts come from its link store.
Voters only
People who accepted the social contract in the Farcaster network vote hold a VOTE NFT on Base whose token id is their FID (contract). The client asks Base directly, ownerOf(fid), and marks those authors voted. Turn on Voters only for a feed of people who showed up. Declining minted nothing, so only "yes" is visible onchain.
Replies, from mutuals
Replies collects replies to your recent casts and casts that mention you, all read from your node. By default it shows only mutuals, people you follow who follow you back. Switch to everyone when you want the full picture.
On your phone
Open your client address and choose Add to Home Screen (Safari: Share menu; Chrome: ⋮ menu). It opens full screen like an app, still talking only to your node.
Concepts, in plain words
- FID
- Your Farcaster ID, a number. It lives in the
IdRegistrycontract on Optimism. Usernames, keys and every message point to it. - Custody address and recovery phrase
- The Ethereum account that owns your FID. Your recovery phrase is the private key to it. Whoever has the phrase owns the account, which is why it is only used once here, locally, to add an app key.
- App keys (signers)
- Ed25519 keys that sign your messages. You can have many, one per app. This client adds a gasless key: a
KEY_ADDmessage authorized by your custody address, with no transaction. Three rules come with it. Scopes: the key lists the message types it may sign (here casts, reactions, follows, profile), and can never add or remove other keys. Sliding TTL: it expires after a period of no use (up to 90 days; this client asks for the maximum), and every message it signs resets the clock. Nonces: eachKEY_ADDcarries a number higher than the last one for your FID, so an old authorization can't be replayed. - The older way still works: calling
KeyGateway.addon Optimism, with a transaction and gas. Those onchain keys have no scopes or TTL. Your node lists both kinds at/v1/signersByFid. - Messages
- Everything you do is a small signed message:
CAST_ADD,CAST_REMOVE,REACTION_ADD(like or recast),LINK_ADD(follow),USER_DATA_ADD(profile). Each has a BLAKE3 hash and an Ed25519 signature, so any node can check it without trusting anyone. - Snapchain
- The network that orders and stores those messages. Validators run consensus and produce blocks. Messages are split across shards by FID; shard 0 orders the others.
- Read nodes vs validators
- A read node follows the chain, verifies every block's signatures from the validators, stores the full state and serves the API. It does not vote and keeps no mempool. Anyone can run one; validators are a fixed set of operators.
- Storage
- Storage is rent, paid onchain on Optimism (the
StorageRegistrycontract) and counted in units. Each unit expires after a period, so rent is renewed, not bought once. A unit sets a separate limit per kind of message (casts, reactions, follows, profile data). Go over a limit and nothing fails: your oldest message of that kind is pruned to make room, which suits a timeline but means a very active account can lose old follows or casts. If your rent runs out, the network rejects new messages from you. This is how Snapchain keeps its state bounded and stops spam without per-message fees. - Check yours on your node:
/v1/storageLimitsByFid?fid=YOUR_FIDlists each store'slimitandused, plus your units and when they expire. Rent and key events are onchain events your node indexes too:/v1/onChainEventsByFid?fid=YOUR_FID&event_type=EVENT_TYPE_STORAGE_RENT. - Usernames (fnames)
- Human-readable names mapped to FIDs by a signed proof. Your node answers
/v1/userNameProofByName?name=…. - Clients
- Apps are just programs that hold an app key for you and read from some node. The Farcaster app is one; this is another. They all see the same network.
How a cast travels
- You press Cast. The browser builds a
CAST_ADDmessage with your FID, the text and mentions, hashes it, signs it with your app key, and encodes it as protobuf. - It is posted to
/v1/submitMessageon your server, which Caddy forwards to your read node. - Your node validates it: FID exists, the key is active for that FID, hash and signature are correct, you have storage. If anything is wrong it answers with an error right away.
- A read node keeps no mempool, so it relays the valid message to the validators over gossip (the
mempooltopic). - A validator includes it in a block. Your node syncs that block and the cast shows up in your feed, usually within seconds.
submitMessage. A validator puts the message in its own mempool; a read node keeps no mempool, so it validates the message, gossips it on the mempool topic, and keeps nothing. That is why a 200 means "valid and relayed", not "in a block yet": if the gossip is lost under heavy load, resubmit. To confirm inclusion, read the message back from your node. Key messages (KEY_ADD, KEY_REMOVE) are routed to shard 0; casts, reactions and follows go to a message shard picked from your FID. Source: Validators vs. read nodes.Security model
- Treat a recovery phrase like
rm -rf. It controls the whole account. Test this tutorial with a test account. In general, only paste a phrase into software you run yourself or from developers you trust, and prefer delegated sign-in (an app that holds your account approves a key for you) whenever it is available. - Your recovery phrase never leaves the page. It is used in your browser to sign the two key authorizations, then cleared. It is not sent to your server or stored. The only thing your node receives is the signed
KEY_ADD. - The app key lives in this browser (local storage). Its scopes let it sign casts, reactions, follows and profile changes, never move your FID or add keys. To stop, open Settings → Revoke this key on the network: the key signs its own
KEY_REMOVE, no recovery phrase needed. If you lose the device, revoke from your custody address, or just stop: an unused key expires on its own when its TTL runs out. - Your node's API is public through your client address: anyone can read through it (it is public data anyway) and submit validly signed messages, which your node only relays.
What "no app can ban you" means, and where it stops
Your account belongs to your custody key, not to any app. If an app hides you or shuts down, your casts, follows and profile are still on the network, and this client reads and posts them through your own node. What it does not change: blocks are produced by a small, fixed set of Snapchain validators, which in principle could refuse your messages, and the network only accepts messages from FIDs with storage rent paid. Your node makes you independent of apps and API providers, not of the protocol's own rules.
What broke when we built this
Every one of these happened on the way to the demo node. Each is now handled by the scripts, but it helps to know why.
- Hetzner said "error during placement". The datacenter had no capacity for that server type. Step 0 now tries Falkenstein, then Nuremberg, then Helsinki.
- Hetzner refused a 2 TB volume. New accounts are capped at 1 TB of volumes, and a limit increase was refused because the account was too new. Step 0 now falls back to the largest size allowed, and the guard below makes 1 TB enough.
- The snapshot did not fit. Snapchain keeps every downloaded chunk, writes a full tar per shard, then unpacks it, and only cleans up at the very end. On one shard that peaks at roughly 1.5 TB.
node.shnow starts a small guard that deletes chunks and tars as soon as the node is done with them, so the peak stays near 800 GB. - The node crash-looped with a metrics error. The official mainnet compose file points metrics at a
statsdservice that has to be started too.node.shstarts both. - A
dfflag mix-up (-Pwith--output) stopped the disk check on Ubuntu. Fixed. - App keys changed mid-build. The first version added the key with an Optimism transaction and asked for $1 of ETH. Snapchain now accepts gasless keys (
KEY_ADD), so that step and the ETH are gone. - Recovery phrase confusion. The Farcaster app has two phrases: the account's recovery phrase and the wallet's. Only the account one owns your FID; the client shows the custody address and FID so you can tell before anything is signed.
Troubleshooting
| You see | Do this |
|---|---|
| "node unreachable" in the client | The node is still downloading the snapshot. townhall-status and the node logs show progress. |
| "blocks behind" stays high for hours | Check UDP 3382 is open in your firewall and the disk is not full (df -h). |
| The client address doesn't load over HTTPS | Ports 80 and 443 must be open for the certificate. If sslip.io is rate limited, point your own domain at the IP and re-run step 2 with DOMAIN=your.domain. |
| "nonce is not greater than stored nonce" | Another key was added or removed for your FID in the meantime. Press Add app key again; it re-reads the nonce. |
| Casts rejected after adding a key | Your FID may have no storage. Check /v1/storageLimitsByFid?fid=YOUR_FID on your node. |
| "invalid signer" when posting | Your node hasn't processed the key event yet, usually because it is still syncing. Wait until it is in sync. |
| "volumes size limit exceeded" in step 0 | New Hetzner accounts have a cap on total volume size. Step 0 then attaches the largest volume allowed and tells you. To grow it later, request a higher limit in the Hetzner console (Limits) and resize the volume. |
| "error during placement" in step 0 | That location is out of the server type for now. Step 0 tries the other EU locations automatically. |
| Messages rejected for storage | Your FID has no storage units left. Rent storage in any Farcaster app. |
Cost & cleanup
Hetzner Cloud bills by the hour, for the server and for the volume. The demo node (server plus 1 TB volume) came to about $2 for its first 12 hours, so a full test run costs a few dollars. Running it for a month is roughly a hundred-odd dollars; check current prices in the Hetzner console. A dedicated server with large disks is cheaper if you keep the node for good. Adding the app key costs nothing.
To stop paying, delete the server and the volume. This does it and asks you to confirm first:
curl -fsSL https://raw.githubusercontent.com/townhall-fc/townhall/main/install/destroy.sh | HCLOUD_TOKEN=your_token bash
Your Farcaster account is unaffected. To also revoke the app key, use Settings → Revoke in the client before you delete the server.
Testing this tutorial for someone?
Thank you. A useful test report is honest, including what failed. Please:
- Use a test Farcaster account that has storage, never one that matters to you.
- Run the three commands from a clean machine and note the times.
- Once the client says the node is in sync: post a cast, follow someone, like something, and hide one app's casts with the filter.
- Open it on your phone too.
- Run
destroy.shso you stop paying. - Post on Farcaster what happened, good and bad, and how long it took.
Never send your recovery phrase or Hetzner token to anyone, the author included. If you were reimbursed for server costs or your time, say so in your post.