Your own Farcaster, in three commands
Rent a server, run a Snapchain read node on it, and use a client that reads from and posts through that node. No API keys from anyone. Every protocol concept is explained at the moment you need it.
Quickstart
You need a terminal with bash, curl, ssh and jq (a stock Mac has all four), a Hetzner Cloud or DigitalOcean account, and a test Farcaster account that already has storage.
git clone https://github.com/warfield2016/homestead && cd homestead./homestead provision # 0. rent a server
./homestead node # 1. start a read node
./homestead client # 2. start your client- provision finds the cheapest server with 4+ vCPU, 16 GB RAM and a 2 TB disk, shows the live price, and creates it only after you type
PROVISION. - node installs Farcaster's Snapchain as a read node. It restores a snapshot, then catches up: a few hours, unattended.
- client builds the client on your server and prints
https://<your-ip>.sslip.ioand a login passphrase.
Open the URL on your laptop or phone, log in, and connect your test account. The client gets its own app key (no gas, no transaction), and then you can read your feed, cast, follow, like, recast, and choose which apps' casts you see.
When you're done: ./homestead destroy. A one-day test on Hetzner costs roughly €4–7, depending on which server types are in stock.
This page follows those commands in order. Each step says what runs, what it costs, how long it takes, and which parts of the protocol it touches. If you only want to get it running, the boxes are optional. If you want to understand Farcaster well enough to build your own client, read them.
Before you start
What you need
| A laptop or desktop | macOS or Linux. Windows works inside WSL. Nothing to install on a Mac. |
| A cloud account | Hetzner Cloud (cheapest) or DigitalOcean. Or any Linux machine you already have with 4 cores, 16 GB RAM and 2 TB free disk (bring your own). |
| A Farcaster test account with storage | Any account you'd be fine losing. See Getting a test account. |
| Time | About 15 minutes of your attention, plus a few hours of waiting while the node syncs. |
| Money | Hetzner, at its October 2026 prices: about €130–200/month depending on stock, billed hourly, so €4–7 for a one-day test. Most of it is the 2 TB volume. The script prints the live price and waits for your yes. |
Getting a test account
Homestead imports an existing account; it doesn't create one, because creating an account needs an onchain transaction and a little ETH on OP Mainnet. Two easy ways to get one:
- Farcaster app. Make a second account in the Farcaster app. It comes with storage. Its recovery phrase is in the app's settings.
- Yourself, onchain. Register an FID with the IdGateway contract on OP Mainnet; registration includes one storage unit. On 2026-10-19 that cost 0.0000841 ETH, about $0.20 (read it yourself:
IdGateway.price()). The official walkthrough has the code.
Farcaster in one picture
0. Provision a server
./homestead provision # Hetzner Cloud (default)
./homestead provision digitalocean
./homestead provision byo root@203.0.113.7What a node needs, and why
These are Snapchain's published requirements: 4 CPU cores, 16 GB of RAM, 2 TB of free disk, and ports 3381–3383 reachable.
The disk is the interesting one. On 2026-10-18 the mainnet snapshot was 5,691 chunks of about 70 MB, roughly 400 GB compressed, and the unpacked database was about 550 GB. You need more than the sum because of how the restore works. From snapshot.rs, for each shard the node:
- downloads every compressed chunk to
.rocks.snapshot/, - un-gzips them all into one
.tarfile, also in.rocks.snapshot/, - unpacks the tar into the database directory,
and it deletes .rocks.snapshot/ only after the last shard. At the peak you hold the compressed chunks, the uncompressed tars and the database at once: about 1.5 TB. Then the database keeps growing.
What the command does
On Hetzner, ./homestead provision:
- asks for an API token (hidden input, never written to disk). Create one under Security → API tokens in a new, empty project, so the token can't touch anything else;
- lists server types that are in stock in your location right now, keeps those with ≥4 vCPU and ≥16 GB RAM, and picks the cheapest. Snapchain publishes
arm64andamd64images, so ARM servers qualify. At Hetzner's published October 2026 prices the cheapest fits are CX43 (€15.99/month) and CAX31 (€20.99), but those lines have been stock-limited since September; when they're gone, the next fit is CCX23 (€85.99). The 2 TB volume adds about €114; - reads live prices from the API and prints the monthly total and the cost of one day, then waits for you to type
PROVISION; - creates, in order: an SSH key just for Homestead (in
.homestead/ssh/, not your~/.ssh), a firewall, a 2 TB volume, and an Ubuntu 24.04 server with both attached; - waits until it can SSH in, then checks CPU and RAM.
Everything it created is remembered in .homestead/state (no secrets in it), which is how commands 1, 2 and destroy find the server.
The firewall opens exactly these ports:
| Port | What it's for | Who connects |
|---|---|---|
| 22/tcp | SSH | you |
| 80/tcp, 443/tcp | the client over HTTPS (80 is for the certificate challenge) | you, from any device |
| 3382/udp | Snapchain gossip (libp2p over QUIC) | other nodes. Required. |
| 3381/tcp | Snapchain HTTP API | anyone. Recommended by the Snapchain docs. |
| 3383/tcp | Snapchain gRPC API | anyone. Recommended by the Snapchain docs. |
To try it without creating anything, run HOMESTEAD_DRY_RUN=1 ./homestead provision: it shows the plan and prints the API calls it would make.
Bring your own machine
Any Linux box with Docker support, 4+ cores, 16 GB RAM and 2 TB free works: a home server, a Netcup or OVH dedicated machine, an EC2 instance.
./homestead provision byo root@your.server # uses your normal SSH setup
./homestead provision byo ubuntu@your.server ~/.ssh/key.pemOpen 3382/udp (required) and 80/443/tcp (for the client) on your router or cloud firewall yourself.
1. Run a Snapchain read node
./homestead nodeIt asks you two things, the same two Farcaster's installer asks: to acknowledge that you won't get rewards for running a node, and, optionally, your FID, which is used only to pick the weekday your node auto-upgrades so that not every node upgrades at once.
What Snapchain is
What the command does
On the server, as root, ./homestead node:
- mounts the 2 TB volume at
/root/snapchain(formatting it only if it has no filesystem); - installs Docker using Docker's official convenience script;
- writes
/root/snapchain/.envwith your two answers, so the official installer doesn't stop to ask; - writes a
docker-compose.override.ymlthat binds Grafana (default passwordadmin) and statsd to127.0.0.1. The official compose file publishes them on every interface. Overrides are the documented way to change it; - downloads and runs Farcaster's own
snapchain.sh upgrade, which fetches the mainnet compose file and validator list, sets up Grafana, installs a weekly auto-upgrade cron job, and starts the node.
Homestead doesn't fork or patch Snapchain. When Farcaster ships a new version, your node's weekly cron job picks it up.
Watching it sync
./homestead status --watchSync has two phases.
1. Snapshot restore (most of the wait). The node downloads shard 1, then 2, then 0, and unpacks each. The HTTP API isn't up yet, so status reads progress from the node's logs. Output looks like this (the numbers are an example):
node: container running (up since 2026-10-19T09:12:04)
snapshot: downloading shard 1: chunk 1204 of 2672 Shard ETA: 0h41m
(shards restore one after another: 1, 2, then 0; each is unpacked right after it downloads)
disk: 141G used, 1.7T free2. Catch-up. Once restored, the node replays every block decided since the snapshot was taken, fetching them from peers. Now the API answers, and blockDelay (seconds between each shard's newest block and now) counts down. For example:
shard 0: height 48337984 catching up: 5120s behind
shard 1: height 49013206 in sync (1s behind)
shard 2: height 48848731 in sync (0s behind)When every shard is in single digits, you're synced. On a Hetzner server, expect a few hours in total, mostly download.
Your first queries
The node's HTTP API is plain JSON over GET. On the server (./homestead ssh):
curl -s localhost:3381/v1/info | jq '.dbStats'{
"numMessages": 914249478,
"numFidRegistrations": 3355205,
"approxSize": 548710481099
}curl -s 'localhost:3381/v1/castsByFid?fid=1&pageSize=1&reverse=true' | jq '.messages[0]'That's a raw message, the thing everything else is built from:
{
"data": {
"type": "MESSAGE_TYPE_CAST_ADD",
"fid": 1,
"timestamp": 182710221,
"network": "FARCASTER_NETWORK_MAINNET",
"castAddBody": {
"text": " ",
"mentions": [14206, 19960],
"mentionsPositions": [0, 1],
"parentCastId": { "fid": 852612, "hash": "0x9d37ece822109756e40b8624abdb0c88e9e8a5d5" },
"embeds": [],
"type": "CAST"
}
},
"hash": "0xa7cfccb85b71ecd53945dec4c53161a8a4aed77b",
"hashScheme": "HASH_SCHEME_BLAKE3",
"signature": "3muv+tUjyXsmLWMy8Js55K3Iu9dWVtXl3qqBgcn0eJefvYgzj6rA2ih9Ebp+oaH6vrslaowGNDQnV2Pi/peABQ==",
"signatureScheme": "SIGNATURE_SCHEME_ED25519",
"signer": "0xb85cf7feef230f30925b101223fd3e3dc4e1120bacd677f5ad3523288f8f7102"
}2. Create the client
./homestead clientYour client: https://203-0-113-7.sslip.io
Passphrase: k7pqm-2xw9v-hc4rt-b8nzf (shown once — save it in your password manager)You can run this while the node is still syncing. The client starts, and its pages tell you what they're waiting for.
What the command does
- Copies
client/anddeploy/from this repo to/opt/homesteadon the server. - Builds the client's Docker image on the server, from that source, and starts it with Docker Compose, next to (not inside) Farcaster's compose project, so
snapchain.sh upgradecan never overwrite it. - Starts Caddy in front of it for HTTPS. You don't need a domain:
203-0-113-7.sslip.iois a sslip.io name that resolves to203.0.113.7, and Caddy gets a Let's Encrypt certificate for it automatically. Bring your own name with--domain. - Generates a login passphrase (about 95 bits), sends it to the server over SSH's stdin so it never appears in a process list, and stores only its scrypt hash.
--private skips Caddy and binds the client to 127.0.0.1 on the server. Reach it with ./homestead tunnel at http://localhost:8080.
Connect your account
Log in, and the client asks you to connect an account. This is where Farcaster's key model matters.
Step 1: find your account. You either paste a recovery phrase or connect a browser wallet. In both cases your browser works out the custody address and sends only the address to your server. The server asks your node:
curl -s 'localhost:3381/v1/onChainIdRegistryEventByAddress?address=0x6b0bda3f2ffed5efc83fa8c024acff1dd45793f1' | jq '{fid, to: .idRegisterEventBody.to}'and then double-checks that the address is still the current custody address (accounts can be transferred) and that the account has storage. It shows you the FID, username and storage it found.
Step 2: sign two messages. The server generates a fresh Ed25519 key pair and asks the browser to sign two EIP-712 payloads with the custody key. The page shows them before you sign. They look like this:
[
{
"domain": { "name": "Farcaster KeyAdd", "version": "1", "chainId": 1 },
"primaryType": "SignedKeyRequest",
"message": { "requestFid": "1234567", "key": "0x7f3c…a21e", "deadline": "182796043" }
},
{
"domain": { "name": "Farcaster KeyAdd", "version": "1", "chainId": 1 },
"primaryType": "KeyAdd",
"message": {
"fid": "1234567", "key": "0x7f3c…a21e", "keyType": 1,
"scopes": [1, 2, 3, 4, 5, 6], "ttl": 2592000,
"nonce": 1, "deadline": "182796043"
}
}
]- SignedKeyRequest says app FID
requestFidis requesting keykey. Homestead uses your own FID as the app; a branded client would register its own app FID and sign this with that FID's custody key instead. - KeyAdd says FID
fidauthorizeskeyto sign these message types, for this long.scopes1–6 are cast add/remove, reaction add/remove and link (follow) add/remove.ttlis 30 days.nonceis one more than the counter your node reports, so the authorization can't be replayed.
Step 3: the server builds and submits a KEY_ADD. It verifies both signatures recover to your custody address, packs them into a KEY_ADD message, signs the message envelope with the new app key itself (proof that whoever submits it holds that key), and submits it to your node. Then it waits until your node reports the key:
curl -s 'localhost:3381/v1/signersByFid?fid=1234567' | jq '.signers[] | {source, key, requestFid, scopes, addedAt, expiresAt}'{
"source": "SIGNER_SOURCE_OFFCHAIN",
"key": "0x7f3c…a21e",
"requestFid": 1234567,
"scopes": ["MESSAGE_TYPE_CAST_ADD", "MESSAGE_TYPE_CAST_REMOVE", "MESSAGE_TYPE_REACTION_ADD", "MESSAGE_TYPE_REACTION_REMOVE", "MESSAGE_TYPE_LINK_ADD", "MESSAGE_TYPE_LINK_REMOVE"],
"addedAt": 1792350494,
"expiresAt": 1794942494
}One trap when reading these records: addedAt, lastUsedAt and expiresAt are Unix seconds, while message timestamps count from the Farcaster epoch. expiresAt − lastUsedAt is the TTL.
No transaction, no gas, about a second.
Recovery phrases, honestly
Balaji's clarification put it well: a developer tutorial should treat a recovery phrase the way a Unix tutorial treats rm -rf. So, plainly:
- Use a test account. Not your main account, not an account whose username you care about, not one whose wallet holds anything.
- Only paste a phrase into software whose code you've read or whose developers you trust. That applies to Homestead too. Don't take this page's word for it.
- Prefer delegated signing. The client's Sign with a browser wallet option sends the same two signature requests to a wallet you already trust (a hardware wallet behind it is better still). The phrase never touches Homestead at all. Use it whenever your custody address lives in such a wallet.
What Homestead does with a phrase, so you can check it:
- The phrase is typed into a page served by your server. Its handling lives in one file,
client/src/web/signing.ts, about 110 lines. - The browser derives the custody account in memory, signs the two payloads above, and clears the text box. The phrase is never sent anywhere: open your browser's network tab and you'll see only an address going out, then two signatures.
- Before signing, that file checks the payload is one of exactly those two types on the
"Farcaster KeyAdd"domain, and refuses anything else. A compromised server can't trick the page into signing, say, a transfer of your account. - The server stores only the new app key, never the phrase or the custody key.
And if something goes wrong, the app key is the only thing exposed: it can post as you until you revoke it, but it can't take your account. The client's Keys page has a revoke button that works without your phrase (below).
Reading: how the feed works
There's no feed API on Farcaster. A client builds one from raw messages. Homestead does it in three moves, all against your node:
- Who do you follow?
GET /v1/linksByFid?fid=<you>&link_type=follow, every page. - Backfill. For each account you follow,
GET /v1/castsByFid?fid=<them>&reverse=true&pageSize=10, eight requests at a time. Against a node on localhost this takes seconds even for thousands of follows. - Stay live. For each shard, read the node's event stream starting at the current block:
GET /v1/events?shard_index=1&from_event_id=<height × 2^14>. EveryMERGE_MESSAGEevent is a message your node just accepted from a block. Casts from people you follow go into the feed; deletes come out; your own follows and likes, even from other apps, update the client's state.
The Live tab shows step 3 raw: every cast your node merges, as it arrives. It's the network as your node sees it, unranked, and it's the best way to find people to follow from a fresh test account.
Writing: where your cast goes
When you press Cast:
- The server builds a
CAST_ADDwith your text, mentions (resolved to FIDs on your node) and embeds, signs it with the app key, andPOSTs the protobuf bytes to/v1/submitMessageon your node. - Your node checks it as a validator would: valid signature, active key with the right scope, storage available. If it passes, the node gossips it on the
mempooltopic to validators. A read node has no mempool of its own and stores nothing yet. - A validator puts it in a block. Validators agree. Your node receives that block and merges the message, which appears on the event stream.
The client shows this as a receipt in the corner, for example accepted by your node in 40 ms → in block 49,013,227 (shard 1) after 1.6 s: the first number is your node's validation, the second is the time until your node saw the message in a decided block.
Don't use /v1/validateMessage to predict whether a write will land: it checks encoding, hash and signature only. On 2026-10-19 it answered "valid": true for a cast signed by a key that wasn't authorized for that FID.
Follows (LINK_ADD), likes and recasts (REACTION_ADD), and their removals work the same way. Deleting a cast sends a CAST_REMOVE; the original becomes a tombstone that still counts against your storage until it's pushed out.
Storage rent
curl -s 'localhost:3381/v1/storageLimitsByFid?fid=1' | jq '{units, unitDetails, casts: (.limits[] | select(.name == "CASTS"))}'{
"units": 22,
"unitDetails": [
{ "unitType": "UnitTypeLegacy", "unitSize": 19 },
{ "unitType": "UnitType2024", "unitSize": 3 },
{ "unitType": "UnitType2025", "unitSize": 0 }
],
"casts": { "name": "CASTS", "limit": 101000, "used": 3797 }
}19 legacy units × 5,000 + 3 units from 2024 × 2,000 = 101,000 casts. A brand-new account's single 2025 unit holds 100 casts, so on a test account, expect old casts to be pruned quickly.
Choosing which apps' casts you see
Every cast in Homestead says which app posted it: via Warpcast, via Firefly App, via Neynar, or own key when someone runs their own client, as you now do.
There's no "client" field in a message. Homestead works it out from the app key, as described in the concept box above:
cast.signer → GET /v1/signer?fid=<author>&signer=<key> → requestFid → that FID's profileFor gasless keys the node returns requestFid directly. For older onchain keys it's inside the ABI-encoded SignedKeyRequestMetadata that was submitted with the KeyRegistry transaction, so Homestead decodes it. For example, the metadata on one of @dwr's keys decodes to requestFid: 9152, the Farcaster app. A key's app never changes, so each lookup is cached forever.
On the Filter apps page you can hide casts from apps you check, or only show casts from them. Hide automated posters, or read only what people wrote in small independent clients. Your node gives you the data; the policy is yours.
Revoking the key
Three ways, from gentlest:
- Stop using it. A gasless key expires after its TTL (30 days here) without use.
- The panic button. Keys → Revoke this client's key sends a
KEY_REMOVEsigned by the key itself. Gasless keys can revoke themselves without the custody key, using a separate nonce counter per requesting app, so a compromised app can kill its own keys fast. - From the custody address. A
KEY_REMOVEsigned by the custody key removes a gasless key; a KeyRegistryremovetransaction removes an onchain one (that's what "connected apps" screens in Farcaster wallets do).
The Keys page also lists every key on your account, onchain and gasless, with the app that requested it. It's worth looking at once: most people have more connected apps than they remember.
Day to day
| Command | What it does |
|---|---|
./homestead status | sync state of each shard |
./homestead logs node · logs client | follow logs |
./homestead ssh | shell on the server |
./homestead tunnel | localhost:8080 → client, localhost:3000 → Grafana (login admin/admin; change it) |
./homestead client | redeploy after git pull; keeps your passphrase and key (--new-passphrase rotates it) |
./homestead destroy | delete server, volume, firewall and key. Billing stops. |
Upgrades. The node upgrades itself weekly (snapchain.sh autoupgrade, from cron). Protocol upgrades are scheduled by engine version and timestamp, so a node that stops upgrading eventually stops following the chain. Check ./homestead status after long breaks.
Backups. The node database can always be rebuilt from a snapshot. The only state worth backing up is the client's Docker volume (homestead_client-data): the app key and passphrase hash. Losing it just means connecting again.
Cost. Hetzner bills by the hour up to a monthly cap. The biggest line is the 2 TB volume (about €114/month at October 2026 prices). Delete everything with ./homestead destroy when you're done testing.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
provision: no server type available | That location is out of stock. Try HOMESTEAD_LOCATION=nbg1 (or hel1, ash, hil). |
status shows the same chunk for a long time | Download stalled. ./homestead logs node; restarting the container resumes it. |
| Out of disk during restore | Under ~1.5 TB free. Grow the volume or start over with a bigger one (HOMESTEAD_DISK_GB). |
| HTTPS never comes up | Ports 80/443 blocked, or Let's Encrypt rate-limited. ./homestead logs client. Use --private meanwhile. |
| No Farcaster account for this address | Wrong phrase or account index, or the node isn't synced yet. The Farcaster app uses index 0. |
| Signatures don't match this account's custody address | The wallet signed with a different address than the one looked up. Switch accounts in the wallet and retry. |
| A cast never reaches a block | Lost in gossip. Submit again; the receipt shows when it lands. |
| Wrong passphrase repeatedly | After 10 tries the login pauses for 10 minutes. ./homestead client --new-passphrase sets a new one. |
Reference
Glossary
| Term | Meaning |
|---|---|
| FID | Farcaster ID: an account's permanent number, issued by IdRegistry on OP Mainnet. |
| Custody address | The Ethereum address that owns an FID. Its key is the master key. |
| Recovery phrase | The 12 or 24 BIP-39 words the custody key is derived from. |
| Recovery address | An address allowed to move an FID to a new custody address if the old one is lost. |
| App key / signer | An Ed25519 key authorized to sign messages for an FID. Onchain (KeyRegistry) or gasless (KEY_ADD). |
| requestFid | The FID of the app that asked for an app key. |
| Message | A signed unit of activity: CAST_ADD, REACTION_ADD, LINK_ADD, USER_DATA_ADD, … |
| Storage unit | Rented capacity per FID, per message type. One year, about $0.20. |
| fname | An offchain username like @alice, issued by Farcaster's name server. Nodes store a signed proof of it, so any client can resolve it. |
| Snapchain | The network that orders and stores messages in blocks. |
| Shard | A partition of Snapchain. Shard 0 orders the rest; message shards hold accounts' data. |
| Validator | A node in the permissioned set that proposes and votes on blocks. |
| Read node | A node that follows the chain without voting. Validates and relays writes. |
| blockDelay | How many seconds a shard's newest block on your node is behind now. |
| Farcaster epoch | 2021-01-01T00:00:00Z. Message timestamps count seconds from it. |
Node endpoints Homestead uses
Every read is a GET you can run yourself against localhost:3381:
| Endpoint | Used for |
|---|---|
/v1/info | sync status, shard heights |
/v1/onChainIdRegistryEventByAddress?address= | custody address → FID |
/v1/onChainEventsByFid?fid=&event_type=EVENT_TYPE_ID_REGISTER | is that address still the custody address? |
/v1/storageLimitsByFid?fid= | storage units and limits |
/v1/signersByFid?fid= | all app keys, onchain and gasless, plus the nonce for the next KEY_ADD |
/v1/signer?fid=&signer= | one key, with its requestFid |
/v1/linksByFid?fid=&link_type=follow | who you follow |
/v1/castsByFid, /v1/castById, /v1/castsByParent, /v1/castsByMention | casts, threads, mentions |
/v1/reactionsByFid, /v1/reactionsByCast | likes and recasts |
/v1/userDataByFid?fid= | profiles |
/v1/userNameProofByName?name= | @name → FID, for mentions |
/v1/events?shard_index=&from_event_id= | the live stream |
POST /v1/submitMessage | every write, as protobuf bytes |
Things we found out of date
Building this meant checking every claim against the Snapchain source. These are the places the docs had drifted, with the fixes we prepared (details and status):
- The Snapchain HTTP API reference doesn't document
/v1/signersByFidor/v1/signer, the endpoints the gasless-signer guide tells you to use. - Its JavaScript pagination example passes
nextPageToken(the parameter ispageToken) and readsresponse.messagesinstead ofresponse.data.messages. signersByFidtakesrequesterFidsasrequesterFids[]=123(the node parses queries withserde_qs). The repeated formrequesterFids=1&requesterFids=2that the source comments suggest is rejected, and percent-encoded brackets are silently ignored, which returns no nonces at all./v1/eventsdoesn't serve shard 0 (Shard not found), so gaslessKEY_ADDs can't be watched there; poll/v1/signerinstead.- docs.farcaster.xyz still lists only the legacy storage limits (5,000 casts per unit) and a $7 unit price; 2025 units hold 100 casts and cost about $0.20.
- docs.farcaster.xyz says a cast can have 2 embeds; since engine V20 (2026-08-13) every account can use 4.
- docs.farcaster.xyz's hello-world still registers the app key with an onchain transaction and points at a "hub"; the Snapchain version of the guide uses gasless keys.
Sources
- Snapchain docs: getting started, managing a node, node types, writing messages, HTTP API
- Snapchain source, especially
src/storage/db/snapshot.rs,src/mempool/routing.rs,src/storage/store/stores.rs,src/network/http_server.rs @farcaster/core0.20.0: message builders,gaslessKeys.ts- Farcaster docs: architecture, accounts, messages
- The bounty thread: balajis.eth/0x50184b62
What has been tested
- The client against a live mainnet node (feed, live tail, threads, client attribution, node status) and its unit tests, including an offline end-to-end check that a
KEY_ADDbuilt from browser signatures passes@farcaster/corevalidation. Seedocs/TESTING.md. - Command 0 against a mock of the Hetzner API (resource order, firewall rules, price arithmetic,
destroy). - Full fresh-machine runs of all three commands are logged in
docs/test-runs/, with the date and Snapchain version of each.
Built against Snapchain 0.14.2–0.14.3 (engine V20), @farcaster/core 0.20.0 and Ubuntu 24.04. Last checked 2026-10-19.