# Your own Farcaster node and client Source: https://castyfun.github.io/casty/ ## Quickstart ```sh git clone https://github.com/castyfun/casty.git && cd casty ./scripts/0-provision.sh # (0) compute: a server with a 2 TB volume, or --host user@ip for your own ./scripts/1-node.sh # (1) a Snapchain read node: installed, synced, with a tunnel to it ./scripts/2-client.sh # (2) a client: casty, an app key for your account, your feed from your node ``` Three commands: (0) compute, (1) a read node, (2) a client. `./scripts/deploy.sh` runs all three in one go, and [The whole way](https://castyfun.github.io/casty/guide/) explains every step and the protocol behind it. casty puts a [Snapchain](https://snapchain.farcaster.xyz) read node, a full copy of Farcaster, on a host you control, and gives you a client that talks to nothing else: a terminal command and a page for the browser and the phone. Your feed, mentions and threads are read from your node; your casts, replies, likes, recasts and follows go out through it. No hosted API stands between you and the network, so there is no API key to get and no rate limit to meet. ## Start with one command ### A new node of your own A server with a 2 TB volume and a firewall on Hetzner or AWS, Snapchain on it, the client on your machine, and the page on the host behind HTTPS. The node syncs for a few hours. ```sh wrap git clone https://github.com/castyfun/casty.git && cd casty ./scripts/deploy.sh --fid --domain node.example.org ``` Already have a Linux machine with 2 TB free? Add `-- --host user@ip`. ### casty on a node that already runs No server: casty connects to any Snapchain node, one of yours on another machine or a public one, and remembers it. Ready in a minute. ```sh wrap git clone https://github.com/castyfun/casty.git && cd casty ./scripts/2-client.sh --node https://snap.farcaster.xyz:3381 --fid ``` Then `./bin/casty web` and, under you, "add a key with your wallet". ### Let an agent do it Paste [one prompt](https://castyfun.github.io/casty/agents/) into Claude Code, Codex, Cursor or another coding agent. It reads [llms.txt](https://castyfun.github.io/casty/llms.txt), asks which of the two you want, and never asks for your seed phrase. ```text wrap Set up casty for me: https://github.com/castyfun/casty. Read https://castyfun.github.io/casty/llms-full.txt first. ... ``` ## How it fits together Reads come from your node, so the feed is as fast as the machine it runs on. Writes are signed on your machine with an app key only casty holds, and your node relays them to the validators. The key is added with one signed message, approved by your wallet in the browser: no transaction, no gas, no Ethereum RPC. ## What you get - **Three scripts and one command.** `0-provision.sh` rents the server or checks yours, `1-node.sh` installs Snapchain and waits for its sync, `2-client.sh` installs `casty` and adds a key. `deploy.sh` runs them in order and picks up where a run stopped. - **A terminal client.** `casty feed`, `casty cast`, `casty like 2`: plain text for you, `--json` for scripts, numbers that carry over from one command to the next. - **A page for the browser and the phone.** The feed, threads, mentions, profiles and a compose box, light and dark, with keyboard shortcuts on a laptop and an install-to-home-screen on a phone. - **Your node, anywhere.** `casty serve` puts the page on the node's host behind HTTPS, with a feed index that answers at once and push notifications for mentions and replies. - **Keys you can see.** Every app key of your account, what it may sign, when it lapses, and revocation from the terminal or with the key itself. - **Tested without a node.** A mock Snapchain node checks every signature, so the client, the scripts and the page run their checks offline in CI. ## In the terminal ```text $ casty feed --limit 3 1 @casty-test · 0s · via casty Reading the feed from my own node. No API key, no rate limit. 2 @user-2000000002 · 22d · via app-alpha @casty-test thanks for the pointer, it worked. https://example.com/2000000002/3 3 @user-2000000011 · 22d · via app-beta Reading the Snapchain docs on a train. A good way to spend an hour. $ casty like 2 liked 2000000002:0x973d398ad75e1f25fa780085f3c22d30b01d2504 ``` ## In the browser and on the phone ## What you need - A Farcaster test account: an fid with at least one storage unit, and a wallet that holds the address it is registered to, or that address's seed phrase. casty uses the phrase once, to sign the key request, and never writes it to disk. - A host. Either credentials for Hetzner Cloud (`HCLOUD_TOKEN` and the `hcloud` CLI) or AWS (the AWS CLI v2 and a profile or access key), or SSH access to a machine you already have that can run Docker. See [A node at home](https://castyfun.github.io/casty/home-node/) for the last case. - An SSH key pair, by default `~/.ssh/id_ed25519` and `~/.ssh/id_ed25519.pub`. - On your machine: `ssh`, `curl` and `jq`. Node.js 22.12 or newer only to build the client yourself (`2-client.sh --from-source`). macOS and Linux work; on Windows, run the scripts in WSL2. The node host needs what Snapchain asks for: 16 GB RAM, 4 vCPU, 2 TB of free disk and a public IP ([getting started](https://snapchain.farcaster.xyz/getting-started)). On a rented host, step 0 picks the server and the volume, and step 1 checks the host before it installs anything. ## How long it takes Step 1 takes the longest: on its first start the node downloads a snapshot of several hundred GB, then catches up on the blocks since that snapshot. Expect hours. `1-node.sh` waits and prints how far behind the node is after each check. Pressing Ctrl-C ends only the wait; the node keeps syncing, and `./scripts/1-node.sh status --wait` picks the wait up again. ## What it costs casty does not know your provider's prices. A real `0-provision.sh` run lists the server, volume and firewall it is about to create, says they are billed until you delete them, and goes ahead only once you type the server name back. `./scripts/0-provision.sh --destroy` deletes all three. ## Try it without a node The repository ships a mock Snapchain node that serves the same HTTP API from fixtures and checks the signatures on every message submitted to it. With it, the whole client runs offline: ```sh pnpm install --frozen-lockfile pnpm demo ``` `pnpm demo` starts the mock on a free port, adds an app key with the BIP-39 test phrase `test test test test test test test test test test test junk`, reads the feed with and without one app's casts, publishes a cast, likes one, and deletes its throwaway config at the end. The fids in the fixtures start at 2000000001 and belong to no one. `pnpm demo:web` does the same setup, then serves [casty web](https://castyfun.github.io/casty/web/), the browser and phone page, on a free port until you press Ctrl-C. ## One command ```text Usage: deploy.sh [--domain | --tailscale] [--read-only] [--fid ] [--from-source] [--env ] [--yes] [--dry-run] [-- <0-provision.sh options>] Runs, in order: 0-provision.sh (unless --env's file already describes a host; with --domain, again on that host to open 80 and 443), 1-node.sh (the node, the wait for its sync, the tunnel), 2-client.sh (the client, an app key, the feed) and, with a front named, 1-node.sh serve. Every step is safe to run again: the same command picks up where a run stopped. --domain serve the page on the host as https:// (Caddy; opens 80 and 443) --tailscale serve it as the host's name on your tailnet instead --read-only a page that reads only: no app key or passphrase leaves this machine --fid no seed phrase: 2-client.sh --fid; add the key in ./bin/casty web with your wallet, then run the same command again for the page on the host --from-source build the client here with Node and pnpm instead of downloading it --env node.env to write or read (default: node.env) --yes do not ask before creating anything --dry-run print what provisioning and the node would run; the client is skipped -- passed to 0-provision.sh, e.g. -- --provider aws --region eu-west-1 Exit codes: 0 ok, 2 bad arguments, otherwise the code of the step that stopped. ``` ## Where to go next Start with [0. Provision a host](https://castyfun.github.io/casty/0-provision/). If you want to know what the scripts set up before you run them, read [Read nodes and validators](https://castyfun.github.io/casty/concepts/read-nodes-vs-validators/) and [Custody address and app keys](https://castyfun.github.io/casty/concepts/custody-and-app-keys/). [Security](https://castyfun.github.io/casty/security/) lists every secret involved and how to revoke each one. --- # The whole way Source: https://castyfun.github.io/casty/guide/ This page walks the whole way once, in the order you do it, and links to the page with the details of each step. Commands run from a checkout of the casty repository unless they say "on the host". ## 0. Before you start A seed phrase controls the whole account. Use a test account for this tutorial, paste a seed phrase only into software from developers you trust, and prefer a key authorised by your wallet: `casty init --fid `, then `casty web` and, under you, "add a key with your wallet". You need: - **A Farcaster test account**: its fid, at least one storage unit ([Storage rent](https://castyfun.github.io/casty/concepts/storage-rent/)), and a wallet that holds the address the fid is registered to (or, as the fallback, that address's seed phrase). `casty whoami` and `casty storage` show both once casty is set up. - **A host for the node**: an account at Hetzner Cloud or AWS, which casty rents a server from, or a machine of your own with 16 GB RAM, 4 cores, 2 TB of free disk and Docker ([A node at home](https://castyfun.github.io/casty/home-node/)). A rented server and its 2 TB volume are billed until you delete them (step 9). - **On your machine**: `ssh`, `curl` and `jq`, and an SSH key pair, by default `~/.ssh/id_ed25519`. Until the project has a release, Node.js 22.12 or newer too, since step 2 then builds the client here. On Windows, use WSL2. - **Time**: the node's first sync downloads a snapshot of several hundred GB and takes hours. Nothing is lost if you stop waiting; every step can be run again and picks up where it was. ```sh git clone https://github.com/castyfun/casty.git && cd casty ls ~/.ssh/id_ed25519.pub || ssh-keygen -t ed25519 # a key to log in to the node with ``` ## 1. Give casty a cloud, or a host Set the credentials of one provider; the scripts read them from your environment and never write them anywhere ([0. Provision a host](https://castyfun.github.io/casty/0-provision/)). ```sh export HCLOUD_TOKEN= # Hetzner: Cloud console, your project, Security, API tokens (read/write) # or, for AWS: aws configure # or AWS_PROFILE, or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY ``` For a machine you already have, skip this and pass `--host user@ip` in step 2. Read what a run would do before anything is created; a dry run needs no credentials: ```sh ./scripts/deploy.sh --dry-run -- --provider hetzner ``` It prints every provider and SSH command as a `DRY:` line and creates nothing. ## 2. Run it ### In one command ```sh ./scripts/deploy.sh --fid ``` This runs the three steps below in order and stops at the first that fails, with that step's exit code. Run the same command again to go on: the first sync takes hours, and Ctrl-C during it loses nothing. Add `--domain ` (or `--tailscale`) to also put the page on the host, step 6. Without `--fid`, step 2c asks for the seed phrase instead. What each step asks of you: - **A host** (`0-provision.sh`): it prints what it will create and bill, and goes ahead only once you type the server name back (`--yes` skips the question). It writes `node.env`, which every later step reads and which deleting the node needs: keep it. With `--host user@ip` nothing is created; the host is checked over SSH. - **The node** (`1-node.sh`): nothing to answer. It checks the host (RAM, disk, Docker, the clock), starts Snapchain, prints the sync, and opens an SSH tunnel to `127.0.0.1:3381` once the node has caught up. - **The client** (`2-client.sh`): with `--fid`, nothing; casty is set up for your fid with no key yet, and the feed is read. Without it, a passphrase for the key store (twice), then the seed phrase, at a hidden prompt. ### Or step by step The same thing, a step at a time: ```sh ./scripts/0-provision.sh # a: a server, a 2 TB volume and a firewall; or --host user@ip ./scripts/1-node.sh # b: the read node, the wait for its sync, the tunnel ./scripts/2-client.sh --fid # c: ./bin/casty, set up for your fid; the seed phrase path without --fid ``` While the node syncs, `./scripts/1-node.sh status` shows how far each shard is behind, and `./scripts/1-node.sh status --wait` waits until it is synced. Details: [1. Run the read node](https://castyfun.github.io/casty/1-node/), [2. Install the client](https://castyfun.github.io/casty/2-client/). ## 3. Add an app key casty writes with an app key: an Ed25519 key your fid's custody address authorises, added with a gasless `KEY_ADD` message through your node, with no transaction and no gas ([Custody address and app keys](https://castyfun.github.io/casty/concepts/custody-and-app-keys/)). ### With your wallet ```sh ./bin/casty web ``` 1. `casty web` asks for a passphrase for the key store it is about to make, twice, and prints a link with a token. Open it in a browser that has your wallet (an extension such as MetaMask). 2. Under you, choose "add a key with your wallet". 3. The wallet is asked for its account; pick the one the fid is registered to. If it sits on another chain, it is asked to switch to Ethereum mainnet, which costs nothing. 4. Sign the two requests the wallet shows: the key request, then the key add. 5. The page checks both signatures, sends the `KEY_ADD` through your node, waits until the node lists the key, and stores it encrypted in `~/.config/casty/keys.enc.json`. On a phone whose wallet is an app, start `./bin/casty web --listen 0.0.0.0:3390` (or use the page on the host, step 6), open the page on the phone, and under you use the link that opens it in your wallet's browser; the button next to it copies the pairing link to paste there. ### With the seed phrase, as the fallback ```sh ./bin/casty init ``` It asks for the passphrase, then for the phrase at a hidden prompt; the phrase is used once to sign the key request and is never written to disk. `--address-index` picks another address of the phrase. Either way, the key may sign casts, reactions and follows (`--scopes` adds profile changes), and it lapses after 30 days without use (`--ttl`, at most `90d`); every message it signs renews it. `./bin/casty whoami` shows the key, the custody address and your storage. ## 4. Use it in a terminal `./bin/casty --help` lists every command; ` --help` its flags. To type `casty` instead, link it once: `ln -s "$PWD/bin/casty" ~/.local/bin/casty`. | To | Run | | --- | --- | | read your feed | `casty feed`, `casty feed --since 24h --no-replies --limit 20` | | read what others said to you | `casty mentions` | | open a cast with what it answers and its replies | `casty thread 2` | | post | `casty cast "hello from my own node"`, `--channel /name`, `--embed ` | | answer, like, recast | `casty reply 1 "agreed"`, `casty like 1`, `casty recast 1` | | undo | `casty unlike 1`, `casty unrecast 1`, `casty delete 1` | | follow | `casty follow @name`, `casty unfollow @name` | | see the apps behind a fid's keys | `casty clients --fid ` | | check your storage | `casty storage` | | check the node, the clock and the setup | `casty doctor`, `casty node status` | The number in front of a cast in the last list is what `like`, `reply`, `recast`, `delete` and `thread` take; so do `:` and a cast's farcaster.xyz link. `via` names the app a cast was posted through; `--only-app` and `--exclude-app` filter on it. Every write waits until the node has the message, and says so when it does not. ## 5. Use it in a browser `./bin/casty web` serves the same commands as a page on `http://127.0.0.1:3390` and prints a link that pairs a browser ([In a browser and on a phone](https://castyfun.github.io/casty/web/)). - **On a phone at home**: `./bin/casty web --listen 0.0.0.0:3390` prints a link per network address and a QR code; scan it on the same network. The connection is plain HTTP, so use a network you trust. Add the page to the home screen to open it full screen. - **Read only**: `--read-only` asks for no passphrase, and the page cannot write. - **A new token**: `--new-token` makes every link printed before stop working. Ctrl-C stops `casty web`; the machine has to stay on while a phone uses it. ## 6. The page from any network To reach the page from a mobile network, run it on the node's host behind HTTPS ([From any network](https://castyfun.github.io/casty/serve/)). It needs an app key here first (step 3): the key store and its passphrase go to the host as a Docker secret, unless `--read-only`. With a domain: 1. Point a DNS `A` record of a name you own, `node.example.org` say, at the node's IP (`NODE_IP` in `node.env`). 2. Run: ```sh ./scripts/deploy.sh --fid --domain node.example.org ``` On a node casty rented, this opens 80 and 443 on its firewall; on a host of your own, open them yourself. Then it builds casty on the host, starts it behind Caddy, which gets the certificate, asks for the key store passphrase, and prints `https://node.example.org/#token=...`. The QR code of that link is in `./scripts/1-node.sh logs casty`. With Tailscale instead, install it on the host and log it in, put the phone on the same tailnet, and run `./scripts/deploy.sh --fid --tailscale`; no port opens to the internet. On the host the feed comes from an index that follows the node's events. Under you, "notifications" turns on a push for each mention or reply, naming the sender and nothing else; on an iPhone, add the page to the home screen first (iOS 16.4 and later). After a change to casty, run `./scripts/1-node.sh serve --domain ` again: it rebuilds and restarts the page. ## 7. Run the node | To | Run | | --- | --- | | see the sync | `./scripts/1-node.sh status` (exit code 1 while behind) | | follow its logs, or the page's | `./scripts/1-node.sh logs`, `./scripts/1-node.sh logs casty` | | reopen the tunnel after a restart of your machine | `./scripts/1-node.sh tunnel` (`--stop` closes it) | | move to the newest Snapchain release | `./scripts/1-node.sh upgrade` | | stay on one release | `./scripts/1-node.sh upgrade --release 0.14.3` | | upgrade every week by itself | `./scripts/1-node.sh --autoupgrade --fid `: it runs `docker system prune --volumes -f` on the whole host | | keep fewer blocks | `./scripts/1-node.sh --block-retention 30` (days) | | see Grafana | on the host, `docker compose up -d grafana` in `~/snapchain`; here, `ssh -L 3000:127.0.0.1:3000 @` and `http://127.0.0.1:3000` | | stop the node, and the page | `./scripts/1-node.sh down`; the database stays on the volume | From another network, the firewall still allows only the address you had: run `./scripts/0-provision.sh` again, and it moves the rules to your current address, on the same node. ## 8. Keys and safety | To | Run | | --- | --- | | list the keys of your fid | `casty key list` | | remove this casty's key with the key itself | `casty key revoke --self` | | remove any key | `casty key revoke `, with the seed phrase | | unpair every browser | `casty web --new-token`; on the host, delete `~/snapchain/casty/config/web-token` and `docker compose restart casty` | A key that lapsed, or was removed elsewhere, is skipped; add another as in step 3. [Security](https://castyfun.github.io/casty/security/) lists every secret involved, where it lives and what to do if one leaks. ## 9. Delete everything ```sh ./bin/casty key revoke --self # first, while the node runs: the key stops working everywhere ./scripts/1-node.sh down # stops the node and the page; nothing is deleted ./scripts/0-provision.sh --destroy # deletes the server, its volume and its firewall, after you type the name rm -rf ~/.config/casty # the config and the encrypted key store here ``` `--destroy` reads `node.env` to know what to delete and removes it afterwards; a new node syncs from the beginning. On a host of your own there is nothing to destroy: delete `~/snapchain` there. ## Without a node `pnpm install`, then `pnpm demo`, runs the client against a mock node with the test phrase and removes what it made; `pnpm demo:web` serves the page the same way until Ctrl-C. ## When a step stops Every script prints what failed and what to do, and exits with a code that says what kind of problem it was: | Code | `0-provision.sh` | `1-node.sh` | `2-client.sh` | | --- | --- | --- | --- | | 1 | | not synced, or the node did not answer | | | 2 | bad arguments | bad arguments | bad arguments | | 3 | a missing tool or credential | a missing tool | a missing tool, or Node too old | | 4 | the provider refused | no SSH connection, or a download failed | the node unreachable (open the tunnel), or no good download | | 5 | the host falls short | the host falls short | a key problem: passphrase, seed phrase, storage | [Troubleshooting](https://castyfun.github.io/casty/troubleshooting/) has each message with its fix. `deploy.sh` stops with the code of the step that stopped, and the same command goes on from there. --- # Commands Source: https://castyfun.github.io/casty/commands/ Two kinds of commands: the scripts in `scripts/`, which set up the host, the node and the client, and `casty`, the client itself. Every script and command takes `--help` and prints its full list of options. ## Quick start You need the fid of a Farcaster test account and a wallet that holds its custody address; for a new node also a Hetzner token, AWS credentials or a Linux host with 16 GB RAM and 2 TB free disk. An account made in the Farcaster app keeps that address in the app, not in a browser wallet: show its recovery phrase in the app on your phone and import it once into MetaMask or Rabby, or type it into `./bin/casty init` in your own terminal instead of using `casty web`. ```sh # a new node of your own git clone https://github.com/castyfun/casty.git && cd casty export HCLOUD_TOKEN= # Hetzner; or AWS credentials, or 0-provision.sh --host user@ip ./scripts/0-provision.sh # (0) compute: a server, a 2 TB volume, a firewall ./scripts/1-node.sh # (1) a Snapchain read node, synced (hours), and a tunnel ./scripts/2-client.sh --fid # (2) a client, ./bin/casty, reading from your node ./bin/casty web # open the link; under "you": "add a key with your wallet" ./bin/casty feed # read; ./bin/casty cast "hello" writes ./scripts/0-provision.sh --destroy # when you are done: server, volume and firewall go ``` Or with no node of your own, on one that already runs: ```sh # casty on a node that already runs git clone https://github.com/castyfun/casty.git && cd casty ./scripts/2-client.sh --node https://snap.farcaster.xyz:3381 --fid ./bin/casty web # open the link; under "you": "add a key with your wallet" ./bin/casty feed # read; ./bin/casty cast "hello" writes ``` ## The scripts | Command | What it does | | --- | --- | | `./scripts/deploy.sh` | Runs steps 0, 1 and 2 in order, and with `--domain` or `--tailscale` puts the page on the host. Safe to run again: it picks up where a run stopped. | | `./scripts/0-provision.sh` | Creates a server, a 2 TB data volume and a firewall on Hetzner or AWS, whichever credentials are set, and writes `node.env`. Asks before creating anything that costs money. | | `./scripts/0-provision.sh --host user@ip` | Uses a machine you already have: creates nothing, checks RAM, disk, Docker and the clock over SSH. | | `./scripts/0-provision.sh --dry-run` | Prints every provider call instead of making it. | | `./scripts/0-provision.sh --https` | Also opens ports 80 and 443, for the page on the host. | | `./scripts/0-provision.sh --destroy` | Deletes the server, then its volume, then its firewall. | | `./scripts/1-node.sh` | Checks the host, installs Snapchain in `~/snapchain`, starts it, waits for it to sync and opens the tunnel. | | `./scripts/1-node.sh status [--wait]` | How far behind each shard is; `--wait` polls until the node has caught up. | | `./scripts/1-node.sh tunnel [--stop]` | Forwards local ports 3381 and 3383 to the node over SSH, so casty reaches it at `127.0.0.1:3381`. | | `./scripts/1-node.sh logs [casty]` | Follows the node's logs, or those of casty on the host. | | `./scripts/1-node.sh upgrade` | Fetches the latest Snapchain release, or `--release `, and restarts the node. | | `./scripts/1-node.sh serve --domain ` | Runs `casty serve` on the host behind Caddy with a Let's Encrypt certificate; `--tailscale` serves it on your tailnet instead. | | `./scripts/1-node.sh down` | Stops the node, and casty on the host if it runs there. | | `./scripts/2-client.sh` | Downloads casty for your platform, checks it against the release's checksums, runs `casty doctor`, then `casty init`. | | `./scripts/2-client.sh --fid ` | Sets up without a seed phrase; you then add the key with your wallet in `casty web`. | | `./scripts/2-client.sh --from-source` | Builds casty here with Node 22.12 or newer instead of downloading it. | Exit codes of the scripts: 0 done, 1 not synced or the node did not answer, 2 bad arguments, 3 a prerequisite is missing, 4 the provider, SSH or a download failed, 5 the host falls short. ## casty ### Options for every command | Option | What it does | | --- | --- | | `--node ` | The node's HTTP API. Default `http://127.0.0.1:3381`, the tunnel from step 1. `casty init` remembers the one it is given, for later commands. | | `--json` | Prints the result as JSON, for scripts. | | `--quiet` | Prints the result only, no progress notes. | | `--config ` | Where casty keeps its config and keys. Default `~/.config/casty`. | ### Setting up | Command | What it does | | --- | --- | | `casty doctor` | Checks that the node answers, how far behind it is, your clock against the node's, and what is set up locally. | | `casty init --fid ` | Sets up for your account with no seed phrase and no key; `casty web` then adds a key with your wallet. | | `casty init` | Adds a gasless app key from your seed phrase, typed at a hidden prompt and never stored. `--ttl 90d` for the longest lifetime, `--address-index ` for another account of the same phrase. | | `casty whoami` | Your fid, username, custody address, active app key and storage. | | `casty key list` | The keys registered to your fid, marking the ones this casty holds. | | `casty key revoke --self` | Removes this casty's key, signed by the key itself. | | `casty key revoke ` | Removes any key of your fid; asks for the seed phrase. | ### Reading | Command | What it does | | --- | --- | | `casty feed` | The newest casts from you and the accounts you follow, numbered. `--limit`, `--since 24h`, `--no-replies`, `--channel /name`, `--only-app` and `--exclude-app` narrow it; `--fids 2,@alice` reads other accounts. | | `casty mentions` | Casts that mention you and replies to your casts, newest first. | | `casty thread ` | A cast, what it answers, and its replies. | | `casty clients [--fid ]` | The app keys of an account and which app requested each. | | `casty storage [--fid ]` | Storage units and how full each message store is. | | `casty node status` | The node's sync only; exit code 1 until it has caught up. | ### Writing | Command | What it does | | --- | --- | | `casty cast ""` | Publishes a cast: up to 320 bytes, or 1024 as a long cast. `--reply `, `--channel /name`, `--embed ` (twice at most); `@names` become mentions. | | `casty reply ""` | Replies to a cast. | | `casty delete ` | Deletes one of your casts. | | `casty like ` / `casty unlike ` | Likes a cast, or takes the like back. | | `casty recast ` / `casty unrecast ` | Recasts a cast to your followers, or takes it back. | | `casty follow ` / `casty unfollow` | Follows an account, or stops. | Every write goes to your node, which relays it to the network; casty waits until it can read the message back, and says so if it cannot. ### In a browser | Command | What it does | | --- | --- | | `casty web` | The feed, threads, mentions, profiles and a compose box at `http://127.0.0.1:3390`, paired by the link it prints. `--listen 0.0.0.0:3390` lets a phone on the same network in; `--read-only` leaves the key locked. | | `casty serve` | The same page over an index of the feed, with push notifications, for the node's host behind HTTPS. `1-node.sh serve` installs it. | ### Naming a cast Wherever a command takes ``, any of these work: - its number in the last list shown by `casty feed`, `mentions`, `thread` or the page: `casty like 2`; - `:<0x hash>`: `casty like 2000000001:0x1a2b...`; - its farcaster.xyz link. ### Exit codes | Code | Meaning | | --- | --- | | 0 | Done. | | 1 | Not synced (`casty node status`), or another failure. | | 2 | Bad arguments. | | 3 | The node refused the message, or it did not merge. | | 4 | The node cannot be reached. | | 5 | A problem with the config, the key store or the seed phrase. | [Troubleshooting](https://castyfun.github.io/casty/troubleshooting/) lists the messages behind each code and what to do. --- # Deploy with an agent Source: https://castyfun.github.io/casty/agents/ A coding agent that can run shell commands (Claude Code, Codex, Cursor, Gemini CLI, Aider and the like) can do the whole setup from one prompt. It reads the documentation from two plain-text files made for agents: - [llms.txt](https://castyfun.github.io/casty/llms.txt): what casty is, the commands, and links to every page; - [llms-full.txt](https://castyfun.github.io/casty/llms-full.txt): every page of this documentation in one file. The repository carries the same `llms.txt` at its root. ## The prompt Paste it as it is. The agent asks for your fid and for the way you want: a new node of your own, or casty connected to a node that already runs. ```text Set up casty for me: https://github.com/castyfun/casty. Read https://castyfun.github.io/casty/llms-full.txt first. 1. Clone the repository and cd into it. 2. Ask me for my Farcaster fid, and which of these I want: a) a new node of my own: I give you HCLOUD_TOKEN or AWS credentials, or user@ip of a Linux host with 16 GB RAM and 2 TB free disk; b) no node of my own: connect casty to an existing Snapchain node URL (default https://snap.farcaster.xyz:3381). 3. For a), first run ./scripts/deploy.sh --fid --dry-run (add -- --host user@ip for my own host), show me what it would create and what it costs, and only after my "yes" run the same command with --yes instead of --dry-run. The node sync takes hours: if the wait stops, run ./scripts/1-node.sh status --wait, then run deploy.sh again; it resumes. For b), run ./scripts/2-client.sh --node --fid . 4. Never ask me for my seed phrase or recovery phrase and never type one anywhere. The app key is added with my wallet: tell me to run ./bin/casty web in my own terminal (it asks me for a new passphrase), open the link it prints, go to "you" and press "add a key with your wallet". If my account was made in the Farcaster app, no browser wallet holds its custody key: tell me to show the recovery phrase in the app on my phone and import it once into a browser wallet (MetaMask or Rabby), or to run ./bin/casty init in my own terminal and type it there. 5. Check the result with ./bin/casty doctor and ./bin/casty whoami, then show me ./bin/casty feed --limit 5. If a step fails, read its error line and hint, look the message up in https://castyfun.github.io/casty/troubleshooting/, fix the cause and run the same command again. ``` ## What stays with you - **Money.** Nothing that costs money is created before you have seen the plan from `--dry-run` and said yes. `./scripts/0-provision.sh --destroy` deletes it again. - **The seed phrase.** The prompt forbids the agent to ask for it. The app key is authorised by your wallet in `casty web`, which you start yourself, so the key store passphrase is yours too. - **Cloud credentials.** The agent needs `HCLOUD_TOKEN` or AWS credentials only for a new node. Give it a token you can revoke, and revoke it when the node is up; casty needs it again only to delete the server. See [Security](https://castyfun.github.io/casty/security/) for every secret involved and how to revoke each one. --- # 0. Provision a host Source: https://castyfun.github.io/casty/0-provision/ Step 0 gets you a host that can run a Snapchain read node and writes `node.env`, the file step 1 reads to find it. ```sh ./scripts/0-provision.sh ``` ## Which provider Without `--provider`, the script decides from what you have set: | You set | Provider | | --- | --- | | `--host user@ip` | `existing`: nothing is created, the host is checked over SSH | | `HCLOUD_TOKEN` | `hetzner` | | `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, or a `~/.aws/credentials` or `~/.aws/config` file | `aws` | When both Hetzner and AWS credentials are set it stops and asks you to pass `--provider`. When neither is, it stops and says what to set. Cherry Servers is there for `--dry-run` only; a live run stops and suggests creating the server by hand, then using `--host`. ## Look before you create ```sh ./scripts/0-provision.sh --provider hetzner --dry-run ``` A dry run prints every provider call as a `DRY:` line and creates nothing. It needs no token, no provider CLI and no SSH key, so you can read what a run would do before you sign up anywhere. It still writes `node.env`, with placeholder values such as the documentation address `192.0.2.1`, but never over a `node.env` that describes a real node. `1-node.sh` takes that file only with `--dry-run`, and the next step the dry run prints says so. ```text dry run: printing provider calls, creating nothing; node.env gets placeholder values DRY: curl -s --max-time 10 https://api.ipify.org DRY: hcloud ssh-key list -o noheader -o columns=fingerprint,name DRY: hcloud ssh-key create --name fc-node --public-key-from-file /home/you/.ssh/id_ed25519.pub DRY: hcloud firewall list -o noheader -o columns=name DRY: hcloud firewall create --name fc-node-fw --rules-file ./fc-node.firewall.json DRY: hcloud volume list -o noheader -o columns=name DRY: hcloud volume create --name fc-node-data --size 2000 --location fsn1 --format ext4 ... ``` ## Hetzner Install the [hcloud CLI](https://github.com/hetznercloud/cli#installation) and create a read/write API token in the Hetzner Cloud console (your project, then Security, then API tokens). ```sh export HCLOUD_TOKEN= ./scripts/0-provision.sh ``` It creates, in `fsn1` unless you pass `--region`: - the server `fc-node`, Ubuntu 24.04, of the first type that can be created out of `cpx41`, `ccx23` and `cpx51` (pass `--type` to choose); - the volume `fc-node-data`, 2000 GB, formatted ext4; - the firewall `fc-node-fw`; - the SSH key `fc-node`, unless the project already has your key under some other name. ## AWS Install the [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and give it credentials: `AWS_PROFILE`, or `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, or `aws configure` (`aws configure sso` and `aws sso login` for SSO). The region is `--region`, else `AWS_REGION`, else `AWS_DEFAULT_REGION`, else `us-east-1`. In zone `a` it creates: - the instance `fc-node`, `m6i.xlarge` or else `m5.xlarge`, from Canonical's current Ubuntu 24.04 image, with a 40 GB root disk; - the gp3 volume `fc-node-data`, 2000 GB, attached once the instance runs; - the security group `fc-node-fw` and the key pair `fc-node`. The node's user is `ubuntu`. ## A host you already have ```sh ./scripts/0-provision.sh --host user@203.0.113.7 ``` Nothing is created. The script logs in with your key and runs the same checks step 1 runs. See [A node at home](https://castyfun.github.io/casty/home-node/) for what such a host needs. ## Confirming A real run prints its plan and waits for you to type the server name: ```text On hetzner this creates, or reuses when they already exist under these names: server fc-node: cpx41 ccx23 cpx51 (the first one available), ubuntu-24.04, fsn1 firewall fc-node-fw: ssh, 3381 and 3383 from 203.0.113.10/32; gossip 3382 from anywhere volume fc-node-data, 2000 GB The server and the volume are billed until you run this again with --destroy. Type fc-node to create them: ``` Anything else, or a closed stdin, stops the run with nothing created. `--yes` skips the question. ## The firewall With the default `--allow-ip auto`, the script asks `api.ipify.org` for your public IP and opens SSH (22), the node's HTTP API (3381) and its gRPC API (3383) to that one address. Gossip (3382, UDP and TCP) is open to everyone, because other nodes connect to it. The client never needs 3381 to be public: it goes through the SSH tunnel step 1 opens. Pass `--allow-ip 198.51.100.0/24` to allow a range instead. Running the script again from another network moves the rules to your new address, on Hetzner and on AWS. On AWS only the rules casty made move, told apart by their descriptions; a rule you added to the group yourself stays. `--https` also opens 80 and 443 to everyone, which `./scripts/1-node.sh serve --domain` needs for its certificate and for a phone on a mobile network ([From any network](https://castyfun.github.io/casty/serve/)). A rerun without `--https` closes them again. ## What the server does on first boot The cloud-init file, written next to `node.env` as `fc-node.cloud-init.yaml`: - installs `chrony` and `jq` and raises the open-file limit; - waits for the data volume, formats it only if it is blank, and mounts it at `/data`; - links `~/snapchain` to `/data/snapchain`, so the database lands on the volume; - installs Docker with Docker's install script and adds the user to the `docker` group. It does not reboot the server. ## node.env ```text NODE_PROVIDER=hetzner NODE_NAME=fc-node NODE_REGION=fsn1 NODE_IP=192.0.2.1 NODE_USER=root NODE_SSH_KEY=/home/you/.ssh/id_ed25519 NODE_SERVER_ID=dry-run NODE_VOLUME_ID=dry-run-volume NODE_DATA_DIR=/data NODE_CREATED_AT=dry-run NODE_STARTED_AT=dry-run ``` It holds names, ids, the IP and the path of your private SSH key, never a token or a key. `NODE_STARTED_AT` is when a run last created, started or powered on the server: step 1 waits up to twenty minutes for ssh on a server that started in the last 15, a minute or two otherwise. Keep it: step 1 reads it, and `--destroy` needs it to know what to delete. `--json` prints it as JSON as well, and `--out ` writes it somewhere else. ## Running it again Everything is found by name, so running the same command again reuses what exists and creates only what is missing. A rerun takes the provider, the name and the region from `node.env`, so `--https` or a new `--allow-ip` later lands on the same node; a flag that names another provider, name or region stops the run, and `--out ` is the way to a second node. If a run stops half way, on an error or Ctrl-C, `node.env` still records what was made so far. Run the same command again to finish, or add `--destroy` to remove it. ## Deleting the node ```sh ./scripts/0-provision.sh --destroy ``` It reads `node.env`, asks you to type the name, then deletes the server, the volume and the firewall, in that order, and removes `node.env`. The volume holds the synced database, so a new node starts the sync from the beginning. `--destroy --dry-run` prints the calls instead. On AWS it deletes the instance and volume ids `node.env` records; it looks one up by its Name tag only when `node.env` has no id for it, after a run that stopped half way. ## Options ```text Usage: 0-provision.sh [--provider hetzner|aws|cherry|existing] [options] --provider hetzner, aws, cherry (dry run only for now) or existing; without it, existing with --host, else the one whose credentials are set --name server name; also -data and -fw (default: fc-node) --region provider region (default: fsn1, us-east-1, eu_nord_1) --type server type (default: cpx41 then ccx23, cpx51; m6i.xlarge then m5.xlarge) --disk-gb data volume size, at least 2000 (default: 2000) --ssh-pubkey key to log in with (default: ~/.ssh/id_ed25519.pub) --allow-ip auto| who may reach ssh, 3381 and 3383 (default: auto, your current IP) --https also open 80 and 443 to everyone, for 1-node.sh serve --domain; a rerun without it closes them again --host the host, for --provider existing --dry-run print every provider call instead of making it --destroy delete the server, then its volume, then its firewall --yes do not ask before creating or deleting anything --json print node.env as JSON --out where to write node.env (default: node.env) Exit codes: 0 ok, 2 bad arguments, 3 missing prerequisite, 4 provider error, 5 preflight failed. ``` Next: [1. Run the read node](https://castyfun.github.io/casty/1-node/). --- # 1. Run the read node Source: https://castyfun.github.io/casty/1-node/ Step 1 installs a Snapchain read node on the host in `node.env`, waits until it has caught up with the network, and opens an SSH tunnel so `http://127.0.0.1:3381` on your machine reaches the node's HTTP API. ```sh ./scripts/1-node.sh ``` ## What it does on the host 1. Waits until the host answers SSH. A new server needs to boot and run cloud-init first; the script tries 60 times, 5 seconds apart. 2. Waits for cloud-init to finish, if the host uses it. 3. Checks the host and prints one row per check: | Check | Needs | | --- | --- | | `ram` | 15 GiB (Snapchain asks for 16 GB; the kernel keeps some) | | `disk` | 1.8 TiB free in the data directory, skipped once `~/snapchain/.rocks` exists | | `docker` | a Docker server the SSH user can talk to | | `compose` | Docker Compose 2.24.4 or newer, which the override below needs | | `ports` | 3381 to 3383 free; shown, never fails, since on a rerun the node holds them | | `clock` | within 60 seconds of your machine's clock | A `no` in any row stops the run with exit code 5. 4. Writes `~/snapchain/.env` with `AGREE_NO_REWARDS_FOR_ME=true`, `HUB_OPERATOR_FID` (your `--fid`, 0 by default), `SKIP_CRONTAB=true` unless you pass `--autoupgrade`, one random Grafana password as `GRAFANA_CREDS` (which Snapchain's own script reads) and `GRAFANA_PASSWORD` (which the override gives Grafana), and `STATSD_PUBLISH` and `STATSD_ADMIN_PUBLISH` set to the host's loopback, where the compose file publishes statsd. It only adds keys the file does not have yet, and never changes a value that is already there. 5. Downloads `docker-compose.mainnet.yml` (saved as `docker-compose.yml`), `validators.toml`, Grafana's `grafana.ini` and the Snapchain dashboard from the Snapchain repository at the `@latest` tag, or at `v` with `--release`. 6. Writes `docker-compose.override.yml`, which Docker Compose merges over the downloaded file and which Snapchain upgrades never replace. It publishes Grafana on the host's loopback only, gives it the password from `.env` and its datasource and dashboard, and, with `--block-retention`, sets block pruning. 7. Starts the `snapchain` and `statsd` services with `docker compose up -d`. 8. Waits for the sync, up to `--timeout` seconds (12 hours by default). 9. Opens the tunnel, unless you pass `--no-tunnel`. Steps 4 to 7 are the non-interactive parts of Snapchain's own `snapchain.sh upgrade`; the [node guide](https://snapchain.farcaster.xyz/guides/managing-a-node) describes the `.env` keys and the override file. ## Waiting for the sync The first start downloads a snapshot of several hundred GB, then syncs the blocks since that snapshot. Expect hours. The script prints the full status once: ```text snapchain , synced: no shard 0 height behind s size GiB mempool n/a shard 1 height behind s size GiB mempool n/a shard 2 height behind s size GiB mempool n/a ``` then one line per check, with the time, how far behind the slowest shard is, and an estimate from the rate since the first check: ```text h m behind, ETA ``` The checks start 10 seconds apart and back off to one a minute. The node counts as synced when every shard is less than 10 seconds behind; the full status is printed again then. `behind` is the `blockDelay` the node reports in `/v1/info`. Read nodes report no mempool size for some shards, which shows as `n/a`. Pressing Ctrl-C ends only the wait. The node keeps syncing on the host; `./scripts/1-node.sh status --wait` watches it from where it is. Running `./scripts/1-node.sh` again works too, but repeats every step, downloading the compose file again (the latest release's unless you pass the same `--release`). ## The tunnel ```sh ./scripts/1-node.sh tunnel # forward 127.0.0.1:3381 and :3383 to the node ./scripts/1-node.sh tunnel --stop # close it ``` The tunnel is an `ssh -N` process in the background, found and stopped through its control socket in `~/.ssh`. It does not come back by itself after the connection drops or your machine restarts: run `tunnel` again. If something else already listens on local port 3381 or 3383, the tunnel does not come up and the script says so. ## Other commands | Command | What it does | | --- | --- | | `status` | the sync status once; exit code 0 when synced, 1 otherwise | | `status --wait` | poll until synced, as `up` does | | `logs` | follow the `snapchain` container's logs; `logs casty` the page's | | `upgrade` | fetch the latest release's compose file, or `--release`'s, then pull and restart | | `serve` | run casty on the host behind HTTPS, with `--domain` or `--tailscale`: [From any network](https://castyfun.github.io/casty/serve/) | | `down` | stop the node, and casty if it serves; the database stays on the volume | `status` asks the node over SSH, so port 3381 never has to be public for it. ## Pinning a release and upgrades The compose file of every Snapchain release names the image `farcasterxyz/snapchain:latest`. `--release 0.14.3` downloads the `v0.14.3` files and rewrites that line to `farcasterxyz/snapchain:0.14.3`; if the file no longer names `:latest`, the script stops rather than run something else. `./scripts/1-node.sh upgrade` without `--release` goes back to the latest release. `--autoupgrade --fid ` installs `snapchain.sh`'s weekly upgrade job instead, on a weekday derived from your fid the way `snapchain.sh` derives it. That job runs `docker system prune --volumes -f`, which affects every Docker project on the host, not only Snapchain. `--release` and `--autoupgrade` cannot be combined, and `--release` removes an upgrade job it finds, since the job would replace the pinned version. ## Block retention `--block-retention ` sets `SNAPCHAIN_PRUNING__BLOCK_RETENTION` so the node prunes blocks older than `n` days. Snapchain allows this on read nodes only and leaves it off by default. The value is a duration, read as humantime by Snapchain's config, so the `d` casty writes is `30d` for 30 days. ## Grafana Snapchain's compose file includes Grafana. casty starts only `snapchain` and `statsd`, but prepares Grafana so that one command starts it with the Snapchain dashboard in place: on the host, run `docker compose up -d grafana` in `~/snapchain`. The override publishes it on the host's loopback, so reach it through SSH: ```sh ssh -L 3000:127.0.0.1:3000 @ # then open http://127.0.0.1:3000 ``` The dashboard opens without a login (Snapchain's `grafana.ini` lets anyone view); to edit, log in as `admin` with the password `GRAFANA_PASSWORD` in `~/snapchain/.env` holds. Grafana reads that password on its first start only: if you set `GRAFANA_CREDS` yourself before casty ran, set `GRAFANA_PASSWORD` to match it. ## Options ```text Usage: 1-node.sh [up|status|logs|tunnel|upgrade|serve|down] [options] (no command) up, wait for sync, then open the tunnel up check the host, write ~/snapchain, start the node, wait for sync status [--wait] sync progress per shard; --wait polls until synced logs [casty] follow the node's logs, or the casty service's tunnel [--stop] forward local ports 3381 and 3383 to the node over SSH upgrade fetch the latest release's compose file (or --release) and restart serve run casty on the host, reached over HTTPS from any network: needs --domain (Caddy gets the certificate; open 80 and 443 with 0-provision.sh --https) or --tailscale (tailscale serve on the host) down stop the node, and casty if it serves --fid your fid, recorded as HUB_OPERATOR_FID (default: 0) --autoupgrade install snapchain.sh's weekly auto-upgrade (needs --fid). It runs docker system prune --volumes -f on the whole host. --block-retention prune blocks older than n days (read nodes only) --release run this Snapchain release, such as 0.14.3, instead of the latest --no-tunnel do not open the tunnel after sync --timeout how long status --wait waits (default: 43200, 12 hours) --env node.env from step 0 (default: node.env) --dry-run print the ssh commands instead of running them serve: --domain the name the page is reached at; must point at the host --tailscale reach it as the host's name on your tailnet instead --read-only a page that reads only: no app key or passphrase leaves this machine --config casty's config directory here (default: $XDG_CONFIG_HOME/casty) Exit codes: 0 ok, 1 not synced or the node did not answer, 2 bad arguments, 3 missing prerequisite, 4 no ssh connection or download failed, 5 preflight failed. ``` Next: [2. Install the client](https://castyfun.github.io/casty/2-client/). --- # 2. Install the client Source: https://castyfun.github.io/casty/2-client/ ```sh ./scripts/2-client.sh ``` ## What it does 1. Downloads `casty--` from the project's latest GitHub release, checks its sha256 against the `SHA256SUMS` published next to it, and installs it as `./bin/casty`: one file of about 77 MB on macOS and 97 MB on Linux, with the client, its dependencies and the web page inside, built with bun. It needs no Node.js. `THIRD_PARTY_LICENSES` next to it names every package inside the file with its license. Releases are built for linux-x64, linux-arm64 and darwin-arm64; on any other machine, or with `--from-source`, the script instead checks for Node.js 22.12 or newer, installs the client's dependencies from the lockfile with pnpm through corepack (Node 25 and later no longer bundle corepack; `npm install -g corepack`), builds the client and writes `./bin/casty` as a wrapper that runs it with Node. 2. Runs `casty doctor` against `http://127.0.0.1:3381`. If nothing answers, it stops with exit code 4 and tells you to open the tunnel. 3. Runs `casty init --fid ` with `--fid`, otherwise `casty init`, which asks for your seed phrase. It skips this when casty already holds a key the node lists for your fid, so running the script again never adds a second key. 4. Prints your feed and a few commands to try; after `--fid`, how to add the key in `casty web` with your wallet. `--node ` points it at another node. `--no-init` stops after `casty doctor`. Anything after `--` is passed to `casty init`, for example `./scripts/2-client.sh -- --address-index 1`. `CASTY_RELEASE_URL` names another place to download from, for a fork or a mirror. When there is nothing to download, a checkout that was never released or no connection, the script says so and builds casty from source itself, as `--from-source` would. ## casty init A seed phrase controls the whole account. Use a test account for this tutorial, paste a seed phrase only into software from developers you trust, and prefer a key authorised by your wallet: `casty init --fid `, then `casty web` and, under you, "add a key with your wallet". With your wallet: `./bin/casty init --fid ` writes the config from the node, the custody address included, and makes no key; `./bin/casty web` then adds one with the wallet in the browser ([In a browser and on a phone](https://castyfun.github.io/casty/web/)). With the seed phrase: `casty init` asks for a passphrase for the key store, twice the first time, then prints the warning above and asks for your seed phrase. Neither is echoed. ```text A seed phrase controls the whole account. Use a test account to try casty, and paste a seed phrase only into software from developers you trust. Prefer a key authorised by your wallet: casty init --fid , then casty web and, under you, "add a key with your wallet". custody address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 holds fid 2000000001 the custody address signs one KEY_ADD message, and only this: fid 2000000001 new app key 0x4224ff3035db6350cfae06e0ad54b79d98a6bbcb962488e0c090c24bd54e6190 may sign cast, reaction, link lapses 30 days after its last use requested by fid 2000000001, this account adding a gasless app key (no transaction, no gas) fid 2000000001 custody 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 app key 0x4224ff3035db6350cfae06e0ad54b79d98a6bbcb962488e0c090c24bd54e6190 scopes cast, reaction, link expires 2026-11-17, after 30 days without use next: casty feed, then casty cast "hello from my own node" ``` That run used the test phrase against the mock node; yours shows your address and fid. In order, casty: 1. derives the custody address at index 0 of the seed phrase (`--address-index` picks another); 2. asks the node which fid that address holds; 3. refuses an fid without a storage unit, since the network would not accept its messages; 4. makes a new Ed25519 key and has the custody address sign a request for it; 5. stores the key encrypted, with the signed `KEY_ADD` message, and submits that message to your node; 6. waits until the node lists the key, then drops the stored message. The seed phrase is never written to disk. The key may sign casts, reactions and follows; add `--scopes cast,reaction,link,user-data` if it should also change your profile. It lapses after 30 days without use (`--ttl`, at most `90d`), and every message it signs renews that. [Custody address and app keys](https://castyfun.github.io/casty/concepts/custody-and-app-keys/) explains the request. If the node accepts the key but casty stops waiting before it shows up, the key is kept. Running `casty init` again sends the same signed message once more and needs no seed phrase. If the node does not know your address yet because it is still syncing, `--op-rpc ` reads the fid from the IdRegistry contract on OP Mainnet instead. ## Reading `casty mentions` lists what others said to you: casts that mention you and replies to your newest 25 casts, numbered like the feed, so `casty reply 1 "thanks"` answers the first. `casty thread 2` shows the second cast of the last list with the cast it answers and its replies, oldest first, numbered again, so `casty like 3` likes the first reply. ```text $ casty feed --limit 3 1 @casty-test · 1s · via casty hello from casty 2 @user-2000000002 · 17d · via app-alpha @casty-test thanks for the pointer, it worked. https://example.com/2000000002/3 3 @user-2000000011 · 17d · via app-beta Reading the Snapchain docs on a train. A good way to spend an hour. ``` The feed is your own casts and those of the accounts you follow, newest first. Each cast shows its author, its age, the app that posted it, what it replies to and the channel it is in. `via` names the app that requested the key the cast was signed with; the list of keys and apps for any fid is `casty clients --fid `. | Flag | Shows | | --- | --- | | `--limit 20` | at most 20 casts (default 50) | | `--since 24h` | casts newer than a duration: `45m`, `24h`, `7d` | | `--no-replies` | no replies | | `--channel /name` | casts in one channel, named as the feed shows it, or by its parent URL | | `--only-app ` | casts posted through this app only; repeatable | | `--exclude-app ` | everything except casts posted through this app; repeatable | | `--fids 2,@alice` | these accounts instead of your follows, by fid or @name | casty reads at most `--limit` casts (up to 100) from each account, five at first and more only where they could be among the newest, so a filter can return fewer casts than `--limit`. ## Writing ```sh ./bin/casty cast "hello from my own node" ./bin/casty reply 2 "agreed" ./bin/casty like 2 ./bin/casty recast 2 ./bin/casty delete 1 ./bin/casty follow @name ``` The number is a cast's position in the last list shown, by `casty feed`, `casty mentions`, `casty thread` or casty web. A cast can also be named as `:<0x hash>`, or by the farcaster.xyz link `casty cast` prints. A cast takes up to 320 bytes, or up to 1024 as a long cast, which casty picks by itself. `casty cast "text" --channel /name` posts in a channel. A channel's name is not on the node, only its parent URL, so casty takes `/name` once a list it printed has shown a cast in that channel, and the parent URL itself at any time. `casty cast "gm @alice"` mentions alice: her app shows the cast among her notifications. casty looks each @name up as `follow` does, on your node and then on the public fname registry (`--no-allow-fname-server` keeps it to the node), and takes it out of the text with her fid and where it stood, which is how mentions travel. A name nobody holds goes out as text, and casty says so; `--no-mentions` sends every @name as text. A cast mentions at most 10 accounts. Every write waits until the node has the message, since a read node only passes it on. [Read nodes and validators](https://castyfun.github.io/casty/concepts/read-nodes-vs-validators/) says how long casty waits and what it does when the message does not show up. ## Where casty keeps its files In `~/.config/casty`, or `$XDG_CONFIG_HOME/casty`, or the directory given with `--config`: | File | What | | --- | --- | | `config.json` | your fid, custody address and the public halves of casty's app keys | | `keys.enc.json` | the app keys, encrypted with your passphrase (scrypt, XChaCha20-Poly1305), mode 0600 | | `last-feed.json` | the casts the last list shown numbered, for `casty like 2` and the like | | `web-token` | the token that pairs a browser with `casty web`, mode 0600 | | `cache/` | follow lists, app labels and usernames, so the next feed asks the node less, the channels lists have shown, and the newest mention you were shown, for the count in `casty web` | Commands that only read never ask for the passphrase. To type `casty` instead of `./bin/casty`: ```sh ln -s "$PWD/bin/casty" ~/.local/bin/casty ``` `--json` on any command prints its result as JSON, and `--quiet` drops the progress notes. ## Options ```text Usage: 2-client.sh [--node ] [--fid ] [--from-source] [--no-init] [-- ] --node the node's HTTP API (default: http://127.0.0.1:3381, the tunnel from step 1); any Snapchain node will do, and casty remembers it for later commands --fid no seed phrase: casty init --fid, then add the key in casty web with your wallet --from-source build casty here with Node 22.12 or newer and pnpm, instead of downloading it --no-init stop after casty doctor; run ./bin/casty init yourself later -- passed to casty init, e.g. -- --address-index 1 Without --fid, casty init asks for your seed phrase. Use a test account for this. The download is casty-- and SHA256SUMS from CASTY_RELEASE_URL, by default this project's latest GitHub release, which has linux-x64, linux-arm64 and darwin-arm64. Exit codes: 0 ok, 2 bad arguments, 3 missing prerequisite, 4 node unreachable or no good download, 5 key problem. ``` ```text Usage: casty [options] [command] Read and write Farcaster through your own Snapchain node. Options: --node node HTTP API; casty init remembers the one it is given (default: "http://127.0.0.1:3381") --json print results as JSON --config config directory (default: $XDG_CONFIG_HOME/casty) --quiet print results only, no progress notes -h, --help display help for command Set up: doctor check the node, its sync, the local clock and the local setup init [options] add a gasless app key for your fid: with --fid and your wallet in casty web, or from your seed phrase whoami your fid, username, custody address, active app key and storage key your app keys Read: feed [options] newest casts from you and the accounts you follow, read from your node mentions [options] casts that mention you and replies to your casts, newest first thread a cast with what it answers and its replies, numbered as the feed is clients [options] the app keys of a fid and which app requested each storage [options] storage units and how full each message store is node about the node itself Write: cast [options] publish a cast (up to 320 bytes, or 1024 as a long cast) reply [options] reply to a cast; same as cast --reply delete delete one of your casts like like a cast unlike take a like back recast recast a cast to your followers unrecast take a recast back follow [options] follow an account unfollow [options] stop following an account In a browser: web [options] the feed, threads and a compose box in a browser, on this machine or a phone serve [options] the page over an index of the feed, with push; on the node's host behind HTTPS, which 1-node.sh serve installs Commands: help [command] display help for command Examples: casty feed --since 24h --no-replies casty feed --exclude-app leave out casts posted through one app casty like 2 the second cast of the last casty feed casty reply 2 "agreed" casty follow @name casty web the feed in your browser, or on your phone ``` --- # In a browser and on a phone Source: https://castyfun.github.io/casty/web/ ```sh ./bin/casty web ``` `casty web` asks for the key store passphrase, then serves a page on `http://127.0.0.1:3390` that reads from and writes through the same node as the other commands. It prints a link with a token; open it to pair the browser. ```text casty web on http://127.0.0.1:3390 open to pair a browser: http://127.0.0.1:3390/#token=<43 characters> Ctrl-C stops it; until then the app key stays unlocked in memory ``` To see it without a node, `pnpm demo:web` starts the mock node, adds a key with the test phrase, posts one cast and serves the page on a free port until you press Ctrl-C. ![The feed on a phone in the light theme: numbered casts, the app each came through, reply, recast and like as words, and the bar for a new cast at the bottom](https://castyfun.github.io/casty/assets/web-phone-light.png) ## What the page does The page has the commands of the CLI, under the same names. | On the page | In a terminal | | --- | --- | | The feed, numbered: your casts and those of the accounts you follow, newest first | `casty feed` | | mentions: casts that mention you and replies to your newest 25 casts | `casty mentions` | | The toggles above it: replies on or off, and one per app | `--no-replies`, `--exclude-app` | | Tapping a channel name: only that channel; tapping the filter again: all | `--channel` | | Tapping a cast: the cast it answers, the cast, its replies | `casty thread` | | reply, recast, like, and in more: delete, open on farcaster.xyz, copy link | `reply`, `recast`, `like`, `delete` | | `› cast something` at the bottom: text with @names as mentions, an optional channel, up to two links | `cast --channel --embed` | | Tapping a name: that account's casts, the apps it posts through, follow | `clients --fid`, `follow`, `unfollow` | | you: your fid, custody address, app keys, storage, node and clock | `whoami`, `key list`, `storage`, `doctor` | | Under you, "add a key with your wallet" (folded under "add another key" once this casty has one): a new app key, authorised by the wallet in the browser | `casty init` with the seed phrase | | A profile's verified addresses, Ethereum or Solana, with copy | | | The search box under you: an account by @name or fid | `follow @name` | A draft in the compose box survives a reload, kept in the browser by what it answers, until the cast goes out. The feed loads its next page as its end comes into view, and the "show more" button stays for keyboards. On a phone, pulling the feed down from its top and letting go refreshes it, as an installed page has no browser bar to pull. Whenever the page draws the feed or mentions, it saves their order as `casty feed` does, so `casty like 3` in a terminal acts on the cast numbered 3 on screen. The last list shown wins: another browser, or `casty feed`, `casty mentions` or `casty thread` in a terminal, renumbers. ## An app key from your wallet The custody address authorises an app key with two EIP-712 signatures, the key request and the key add; `casty init` has the seed phrase sign them. The page can have a wallet sign them instead: under you, "add a key with your wallet" asks `casty web` for a fresh key and the two messages, hands them to the wallet in the browser (a wallet extension, or the page opened in a wallet app's own browser, `eth_signTypedData_v4` over EIP-1193), checks that the wallet's account is the custody address, and sends the signatures back; `casty web` checks them, sends the `KEY_ADD` through your node, and stores the key, encrypted, as its newest. The seed phrase never leaves the wallet, and a hardware wallet behind the extension works the same way. To start with no phrase at all, `casty init --fid ` writes the config from the node, the custody address included, and makes no key; `casty web` then asks for a passphrase for the store it is about to make, and the page adds the first key. Removing a key stays in the terminal (`casty key revoke`, with the phrase or with the key itself). The page finds a wallet the way wallets announce themselves (EIP-6963), so several extensions side by side are fine, and `window.ethereum` as a fallback. If the wallet offers another account than the custody, the page asks it to let you pick the right one. The key messages name Ethereum mainnet, so a wallet on Base or OP Mainnet is asked to switch first (MetaMask refuses to sign for another chain than the active one); the switch costs nothing. On a phone with no wallet in the browser, the page offers links that open it inside MetaMask, Coinbase Wallet or Trust Wallet, and a button that copies the pairing link to paste on the page there, so the wallet's own browser signs. The links carry no token: each goes to the wallet maker's server first. An account whose custody is the Farcaster recovery phrase from farcaster.xyz can import that phrase into any of those wallets once, and never type it into a terminal. A profile shows the addresses its account has verified as its own, Ethereum or Solana, from the node: each links to its explorer (opened only when tapped) and copies. Yours are listed under you as well. Nothing is read from a chain. To stay quick, the page reuses what it read from your node in the last 30 seconds when you toggle a filter, show more or come back to a view. Anything you write from the page clears that, and so do refresh, reloading the page, and tapping feed, mentions or you while it is open; a cast made in a terminal shows on the next refresh, or on a load more than 30 seconds later. Reloading the page reads the casts again but takes your follow list from the last ten minutes; refresh reads that too, so an account followed in another app shows at once. While the page is open it asks your node every two minutes, and again whenever you come back to it, whether anyone has mentioned you or replied to your newest 25 casts since you last looked. The answer shows as a number next to mentions, in the tab's title, and on the icon of the page if you installed it as an app and the browser allows that. Opening mentions clears it. So does `casty mentions` in a terminal, or mentions opened in another browser: casty remembers the newest mention you were shown, in its config directory, not in the browser. The first count starts from the moment the page first asks, so mentions from before then never count as new. A week before this casty's app key lapses, the notice under the bar says so; any cast, like or follow renews it. The compose box counts bytes. Past 320 the cast goes out as a long cast, up to 1024. A write waits until the node has the message, as the CLI does; if it fails, the page shows the same message and hint the CLI would print. The dot next to casty is your node: green when synced, amber when behind, red when it does not answer. On a keyboard: `n` new cast, `r` refresh, `j` and `k` move between casts, `l` like, `o` or Enter open, Esc close, Ctrl-Enter or Cmd-Enter send. The word at the top right sets the theme: auto follows the system, light and dark stay as chosen in that browser. On a phone the bar has no room for it, nor for refresh: the theme is under you, and tapping feed, mentions or you while it is open reloads it. ![A cast in the dark theme, with the cast it answers above it and its reply below](https://castyfun.github.io/casty/assets/web-phone-dark.png) The page loads nothing from other sites. It shows no images and no avatars, and a link in a cast opens only when you follow it. ## On a phone ```sh ./bin/casty web --listen 0.0.0.0:3390 ``` This prints a link for each IPv4 address the machine has on a network, and a QR code of the first one. Scan it with a phone on the same network. The page can be added to the home screen and then opens full screen; if it shows the pairing screen there, paste the link into it. The machine has to stay on with `casty web` running. The connection is plain HTTP. Anyone who can watch the traffic on that network can read the token and post as you until you stop `casty web`. Use a network you trust, or join the phone and the machine to a VPN such as WireGuard and listen on the VPN address only. To make every link printed before stop working, stop `casty web` and start it again with `--new-token`. A paired browser keeps the token for that address and port, `http://192.168.1.20:3390` say. On another network the same address can belong to someone else's machine, and a page opened there from the home screen would hand it the token. If that may have happened, start `casty web` with `--new-token`. Opening the link puts it, token included, in the browser's history, and in its synced history if the browser syncs. Clear that entry on a browser you share. `--read-only` skips the passphrase. The page then reads and cannot write. Ctrl-C stops `casty web`. A write (a cast, like, recast, delete or follow) still waiting for its message is let finish first, and the terminal says so; a second Ctrl-C stops at once. To reach the page from any network, not only the one the machine is on, run it on the node's host behind HTTPS instead, where a mention or a reply can also reach the phone as a push notification: [From any network](https://castyfun.github.io/casty/serve/). ## How the page is protected - It listens on 127.0.0.1 unless `--listen` says otherwise. - Every API request carries the token. The link holds it after `#`, which a browser never sends to a server; the page moves it into the browser's storage and drops it from the address bar, also when the link is opened in a tab that already shows the page. - The token lives in `~/.config/casty/web-token`, mode 0600, so a paired phone stays paired across restarts. - Writes are accepted only as JSON, so a form on another site cannot post to the page. - A request whose Host is not an IP address, `localhost` or the `--listen` host is refused, so a site that points its own domain at your machine cannot reach the page. - The page runs only its own script and style, and puts cast text in as text, never as HTML. - While `casty web` runs, the app key is decrypted in memory, as it is for the length of a single `casty cast`. ## Options ```text Usage: casty web [options] the feed, threads and a compose box in a browser, on this machine or a phone Options: --listen where to serve; 0.0.0.0:3390 lets a phone on the same network in (default: "127.0.0.1:3390") --new-token pair again; links printed before stop working --read-only do not unlock the app key; the page reads but cannot write --no-allow-fname-server look @names up with your node only; by default names it does not know are looked up on fnames.farcaster.xyz (public, no key) -h, --help display help for command ``` --- # From any network Source: https://castyfun.github.io/casty/serve/ ```sh ./scripts/1-node.sh serve --domain node.example.org # or: --tailscale ``` `casty web` runs on your machine and reaches a phone on the same network. `1-node.sh serve` runs the same page on the node's host, next to the node, behind a proxy that ends HTTPS, so the phone reaches it from a mobile network as well. It is the same `casty` code, built on the host in a container from this checkout, and it reads and writes through the node as `http://snapchain:3381` on the host's own Docker network. ## What it does 1. Reads casty's config directory here (`~/.config/casty`, or `--config`): `config.json` with your fid, and `keys.enc.json` with the app key, and asks for the key store passphrase. With `--read-only`, neither the key store nor a passphrase leaves this machine, and the page reads but cannot write. 2. Ships the client's source (not `node_modules`, tests or a build) to `~/snapchain/casty/src` on the host with a Dockerfile: `node:22-alpine`, the dependencies from the lockfile, the same build as `2-client.sh --from-source`. The host builds the image; the first time takes a minute or two. 3. Writes `~/snapchain/docker-compose.casty.yml` next to the node's compose file and names it in `COMPOSE_FILE` in `~/snapchain/.env`, so every `docker compose` command there includes it. The `casty` service runs `casty serve --public-host `, restarts with the host, keeps its token and caches in `~/snapchain/casty/config` (bind-mounted), and reads the passphrase from a Docker secret, the file `~/snapchain/casty/passphrase`, mode 0600. 4. Puts a proxy in front: - `--domain `: a `caddy` service publishes 80 and 443 and gets the certificate from Let's Encrypt by itself. The name must point at the host, and 80 and 443 must be reachable from the internet: `./scripts/0-provision.sh --https` opens them on a Hetzner or AWS firewall (a rerun keeps the server); on a host of your own, open them yourself. - `--tailscale`: `casty` is published on the host's loopback only and `tailscale serve --bg 3390` serves it as `https://..ts.net` to your tailnet, with a certificate Tailscale provides. Tailscale must be installed and logged in on the host, and the phone must be on the tailnet. No port is opened to the internet. 5. Waits for the service to write its pairing token, then prints `https:///#token=...`. The QR code of that link is in the service's logs: `./scripts/1-node.sh logs casty`. Run `serve` again after a change to casty: it ships the source again, rebuilds and restarts. `./scripts/1-node.sh down` stops the node and casty together; on the host, `docker compose stop casty` (and `caddy`, with `--domain`) stops only the page. To pair again from scratch, delete `~/snapchain/casty/config/web-token` on the host and `docker compose restart casty`. ## What casty serve adds to casty web `casty serve` is `casty web` over the index described below, with push, a request limit and, with `--public-host`, the proxy's name: - the links it prints carry `https://`, and that name is accepted as the request's `Host`, besides an IP address or `localhost`; - every answer carries `Strict-Transport-Security`, so a browser that has seen the page once never asks for it over plain HTTP; - `/api/*` is limited to 120 requests a minute per client address, counted before the token is checked, with `429` and `Retry-After` past that. Behind the proxy the client's address is the last one in `X-Forwarded-For`, the one the proxy appended; an address the client put there itself does not count; - without `--public-host`, on a laptop, `casty serve` is `casty web` with the index and the limit: the same links, no HSTS, and the limit counts by the socket's address, since without a proxy `X-Forwarded-For` is whatever the client wrote. Built from source, `casty serve` needs Node 22.13 or newer for its SQLite; the one-file binary brings its own; - the key store passphrase comes from `CASTY_PASSPHRASE_FILE`, the Docker secret, so the service starts without a terminal. Everything else is the page described in [In a browser and on a phone](https://castyfun.github.io/casty/web/): the same token pairing, the same numbering shared with the terminal, the same writes through the node. ## The feed from an index `casty serve` keeps the feed in `index.sqlite` in its config directory (`~/snapchain/casty/config/index.sqlite` on the host), with Node's own SQLite. At its first start it reads your follows, the newest 100 casts of you and of each of them, the casts that mention you, the replies to your newest 25 casts, and your likes and recasts. From then on it follows the node's event log (`/v1/events`, every shard, every 1.5 s): a cast by someone you follow, a mention of you or a reply to you goes in; a removed or pruned cast goes out; a follow you make brings the new account's newest casts in and an unfollow takes the account out; your likes and recasts stay current. The page's feed, mentions and like state then come from the index in a few milliseconds instead of hundreds of requests to the node, and a write you make on the page reads the events at once, so it shows in the next list. Once the first pass is done, the names and apps of your follows are read too, so the first feed after a start asks the node for nothing. Until the first pass is done, the page reads the node as `casty web` does. Profiles and threads always read the node. While you read, the page asks the index every half minute, and whenever it comes back into view, whether casts newer than the one on top have come. If so, a pill under the bar says "3 new casts"; the list never moves under you. A tap draws them. A node keeps about three days of events; a service that was off for longer than two does the first pass again when it starts, and goes on from there. Delete `index.sqlite` on the host and restart `casty` to start from nothing. ## A mention reaches the phone Under you, "notifications" turns Web Push on for that browser: the page registers its service worker (`sw.js`), subscribes with the key `casty serve` made at its first start (`vapid.json` in its config directory, mode 0600), and hands the subscription to the service, which keeps it in `cache/push.json`. When the events bring a cast by someone else that mentions you or answers one of yours, the service sends each subscribed browser one message: "new mention from @name" or "new reply from @name", and nothing else, not the cast's text. It is encrypted for the browser's own key (RFC 8291) and signed with the service's (RFC 8292), so the push service in between, Apple's, Google's or Mozilla's, carries ciphertext it cannot read. A tap opens mentions. A subscription the push service reports gone is dropped. On an iPhone, Web Push works only for a page added to the home screen (iOS 16.4 and later); the count next to mentions works everywhere, as before. Turning notifications off removes the subscription from both the browser and the service. ## What this puts on the host With `serve` the host holds, besides the node: | What | Where | What it can do | | --- | --- | --- | | App key, encrypted | `~/snapchain/casty/config/keys.enc.json`, mode 0600 | Nothing without the passphrase | | Key store passphrase | `~/snapchain/casty/passphrase`, mode 0600, read by the container at start | Decrypt the key: together, write as you | | The page's token | `~/snapchain/casty/config/web-token`, and in each paired browser | Read and write as you, from anywhere, while the service runs | | The certificate and its key | Caddy's `caddy_data` volume, or Tailscale's | Serve the page as that name | | The index of your feed | `~/snapchain/casty/config/index.sqlite` | Nothing: it holds public casts, your follows and reactions, which the node holds too | | The push key pair | `~/snapchain/casty/config/vapid.json`, mode 0600 | Send pushes to the subscribed browsers; a new pair voids every subscription | | The push subscriptions | `~/snapchain/casty/config/cache/push.json` | Send a push to those browsers, as the push service's endpoints, with the browsers' public keys | Whoever has root on the host has the app key, decrypted, in the service's memory. That is the trade for a page from anywhere, and the reasons it stays bounded: the key signs only what its scopes allow (casts, reactions, follows by default) and lapses unused after its TTL; `casty key revoke ` with the seed phrase, from any machine, revokes it at once; and `--read-only` keeps the key at home for a page that only reads. The seed phrase never goes anywhere. [Security](https://castyfun.github.io/casty/security/) lists every secret. ## Options ```text Usage: casty serve [options] the page over an index of the feed, with push; on the node's host behind HTTPS, which 1-node.sh serve installs Options: --public-host the domain the page is reached at through a proxy that ends HTTPS; it goes into the pairing link --listen where to serve, for the proxy (default: "0.0.0.0:3390") --new-token pair again; links printed before stop working --read-only do not unlock the app key; the page reads but cannot write --no-allow-fname-server look @names up with your node only; by default names it does not know are looked up on fnames.farcaster.xyz (public, no key) -h, --help display help for command ``` --- # A node at home Source: https://castyfun.github.io/casty/home-node/ Any Linux machine you can reach over SSH can be the node host: a server at home, or one at a provider casty has no script for. Step 0 then creates nothing and only checks the machine. ```sh ./scripts/0-provision.sh --host you@192.168.1.20 ./scripts/1-node.sh ./scripts/2-client.sh ``` ## What the machine needs Snapchain asks for 16 GB RAM, 4 vCPU, 2 TB of free disk and a public IP. Step 0 and step 1 check the machine over SSH and stop with exit code 5 when it falls short: - at least 15 GiB of RAM; - 1.8 TiB free where the database will live. `CASTY_MIN_FREE_GIB=1700` before the command asks for less instead, at your risk: once the database fills the disk, the node stops; - a Docker server the SSH user can talk to, so the user must be root or in the `docker` group; - a clock within 60 seconds of yours. Nodes reject messages timestamped more than 15 minutes in the future, so keep NTP running on the host. A rented server gets Docker, `chrony` and a higher open-file limit from cloud-init. On your own machine those are up to you. ## SSH casty logs in with your key and never with a password: it runs SSH with `BatchMode=yes`. The key is `~/.ssh/id_ed25519` unless you pass `--ssh-pubkey`, and the private half sits next to the public one. The first connection accepts the host's key and records it; later connections check it. ## Where the data goes The node lives in `~/snapchain` of the SSH user, and the free-space check measures the filesystem of that user's home directory. To keep the database on another disk, mount it, make `~/snapchain` a link to a directory on it, and set `NODE_DATA_DIR` in `node.env` to the mount point, so the check measures that disk instead. ## Ports and firewall casty creates no firewall for a machine it did not rent. Other nodes reach yours on 3382/udp, the only gossip port Snapchain's compose file publishes, so forward that from your router. Keep 3381 (HTTP API) and 3383 (gRPC) closed to the internet: casty reaches them through the SSH tunnel. Snapchain's compose file publishes statsd on 8125/udp and 8126/tcp on every interface unless told otherwise; `1-node.sh` sets `STATSD_PUBLISH` and `STATSD_ADMIN_PUBLISH` in its `.env` to the loopback on any host it runs on, this one included, so statsd and Grafana answer on `127.0.0.1` only. ## Deleting `0-provision.sh --destroy` does not apply here, since nothing was created. `./scripts/1-node.sh down` stops the node; the database stays in `~/snapchain` until you delete it. --- # FIDs Source: https://castyfun.github.io/casty/concepts/fids/ An fid is the number that identifies a Farcaster account. Every message carries the fid it speaks for, and every message must be signed by a key registered to that fid. ## Who owns an fid fids are registered in the IdRegistry contract on OP Mainnet. The Ethereum address an fid is registered to is its custody address, and whoever holds that address's private key controls the account: they can add and remove the keys that sign messages, and move the fid to another address. A seed phrase holds such keys; casty derives the custody address from it at `--address-index`, 0 by default. casty takes the contract's address and interface from `@farcaster/core`; none of it is typed into casty itself. ## How casty finds yours Snapchain nodes index the IdRegistry's events. `casty init` asks your node for the latest registration event of your custody address (`/v1/onChainIdRegistryEventByAddress`) and takes the fid from it, so no Ethereum RPC is needed. A node that is still syncing may not have that event yet. `casty init --op-rpc ` then calls `IdRegistry.idOf(address)` over an OP Mainnet RPC of your choice. An fid that was moved to another address is not found under the old one. ## Names A username, or fname, is separate from the fid: the node stores proofs that link a name to an fid. `casty follow @name` asks your node for the proof first and asks the public registry at `fnames.farcaster.xyz` only if the node does not know the name. In the feed, an fid without a known name is shown as `!`. ## Storage An fid can only publish while it has storage. `casty init` refuses an fid with no storage units; see [Storage rent](https://castyfun.github.io/casty/concepts/storage-rent/). ## The fids in these docs Examples come from the mock node in the repository. Its fids start at 2000000001 and belong to no one; the test seed phrase holds 2000000001. --- # Custody address and app keys Source: https://castyfun.github.io/casty/concepts/custody-and-app-keys/ Two kinds of keys are involved. The custody address owns your fid. An app key signs your everyday messages: casts, reactions, follows. casty uses the custody address once, to add an app key, and keeps only the app key. The custody signs either in a wallet, from the page ([Wallets and keys](https://castyfun.github.io/casty/concepts/wallets/)), or in a terminal, from the seed phrase `casty init` asks for. ## The custody address It is an Ethereum account, derived from your seed phrase. It can add and remove app keys and move the fid, so casty never stores it: `casty init` reads the phrase, signs one request, and zeroes the phrase. `casty key revoke` asks for it again, because removing a key with the custody address needs a new signature. ## The app key An Ed25519 key that casty generates on your machine. It signs messages for your fid, and nodes accept them as long as the key is registered and has not lapsed. Since engine version 16, Snapchain registers such keys with a `KEY_ADD` message instead of an onchain transaction: no gas, and the key is usable as soon as the message merges. casty builds it with the `@farcaster/core` helpers `makeKeyAddBody` and `makeKeyAdd`. See [writing messages](https://snapchain.farcaster.xyz/guides/writing-messages) in the Snapchain docs. Older tutorials, the hello-world example in hub-monorepo among them, still add the key with a transaction to the KeyGateway contract on OP Mainnet: that costs gas and needs a funded wallet and an RPC, and casty does not use it. A `KEY_ADD` carries three signatures: - the requesting app's signature over a key request, made by the custody address of the app fid (`--app-fid`, your own fid by default); - the custody address's signature over the key, its scopes, its lifetime, a nonce and a deadline; - the message envelope, signed by the new key itself, which proves casty holds its private half. The nonce is per fid and must grow with every key change: casty reads the current one from the node and adds 1. If another change lands first, the node refuses the message, and `casty init` retries once with a fresh nonce. ## Scopes A gasless key lists the message types it may sign. casty asks for these by default: | `--scopes` name | Message types | | --- | --- | | `cast` | `CAST_ADD`, `CAST_REMOVE` | | `reaction` | `REACTION_ADD`, `REACTION_REMOVE` | | `link` | `LINK_ADD`, `LINK_REMOVE` | `user-data` (`USER_DATA_ADD`, your profile) is added only when you ask for it. `KEY_ADD` and `KEY_REMOVE` can never be among a key's scopes, so an app key cannot add keys or remove other keys; it can only remove itself. ## Lifetime `--ttl` sets how long the key lives without use: 30 days by default, at most 90. Every message the key signs that merges starts the period again, so a key you use does not lapse. `casty init` prints the date it would lapse if left unused. An fid may hold at most 1000 active gasless keys. ## Which app posted a cast Every key records the fid of the app that requested it. A cast carries the public key that signed it, so the feed can say which app it came from: casty looks the signer up in the author's list of keys, takes the requesting fid and shows that fid's username after `via`. `--only-app` and `--exclude-app` filter on it, and `casty clients --fid ` lists any account's keys with their apps. Keys added onchain carry the same request, and casty reads them the same way. ## Removing a key | Command | Signed by | Needs | | --- | --- | --- | | `casty key revoke ` | the custody address | the seed phrase | | `casty key revoke --self` | the app key itself | the key in casty's store | Both send a `KEY_REMOVE` and wait until the node no longer lists the key. Keys added onchain are removed onchain, in the KeyRegistry contract; casty does not do that. --- # Read nodes and validators Source: https://castyfun.github.io/casty/concepts/read-nodes-vs-validators/ Snapchain has two kinds of nodes. Validators agree on the order of messages and produce blocks. Read nodes download those blocks, keep the full state, and serve it over the HTTP and gRPC APIs. casty sets up a read node. The Snapchain docs compare the two in [node types](https://snapchain.farcaster.xyz/node-types). ## What a read node gives you - Every account's casts, reactions, follows, profiles and keys, read from your own disk, through `http://127.0.0.1:3381` once the tunnel is up. The HTTP API is documented in the [reference](https://snapchain.farcaster.xyz/reference/httpapi/httpapi). - A way in for writes: `POST /v1/submitMessage` works on a read node. - No API key: the data comes from your own host. Running one earns no rewards: Snapchain's `.env` has to say `AGREE_NO_REWARDS_FOR_ME=true`, which `1-node.sh` writes for you. ## What it costs to run Snapchain asks for 16 GB RAM, 4 vCPU, 2 TB of free disk and a public IP. The first start downloads a snapshot of several hundred GB, then syncs the blocks since. The database lives in `~/snapchain/.rocks`; while a snapshot is being restored, `.rocks.snapshot` exists next to it and needs room too. Ports, from the Snapchain docs: | Port | What | Open to | | --- | --- | --- | | 3381/tcp | HTTP API | your address only, on hosts casty rents | | 3382/udp | gossip with other nodes (casty also opens TCP) | everyone | | 3383/tcp | gRPC API | your address only, on hosts casty rents | Grafana (3000) and statsd (8125/udp, 8126/tcp) are published on the host's loopback only: `1-node.sh` sets `STATSD_PUBLISH` and `STATSD_ADMIN_PUBLISH` in Snapchain's `.env` to `127.0.0.1`, on any host it runs on. ## Writes go through, but are not kept When you submit a message to a read node, it checks the message, passes it on over gossip and keeps nothing. An HTTP `200` means "accepted for relay", not "in a block". If the message is lost on the way it is gone, and has to be sent again. The Snapchain docs say the same of [read nodes](https://snapchain.farcaster.xyz/node-types): one validates a message, relays it to the validators over gossip and keeps no copy, so a message lost on the way needs resubmitting, which is likelier when the network is under heavy load. So casty confirms every write by reading it back from the node until it shows up as merged. It waits 20 seconds, sends the same bytes once more, waits another 20, and then stops with: ```text casty: was accepted but not merged within 40s ``` The message may still merge after that. Look for it before you send it again, since running the command again builds a new message. ## Pruning A read node keeps every block by default. `1-node.sh --block-retention ` turns on block pruning, which Snapchain offers on read nodes only. Events are kept for 3 days by default. --- # Snapchain messages Source: https://castyfun.github.io/casty/concepts/snapchain-messages/ Everything an account does on Farcaster is a signed message: a cast, a like, a follow, a profile change, a key change. Nodes check each message and store the ones they accept. The protocol is described in [Messages](https://docs.farcaster.xyz/learn/what-is-farcaster/messages) in the Farcaster docs. ## What is in one | Field | What | | --- | --- | | `data` | the content: type, fid, timestamp, network, and a body for the type | | `hash` | BLAKE3 hash of `data`, 20 bytes | | `signature` | Ed25519 signature over the hash | | `signer` | the public key that signed, which must be registered to the fid | casty never assembles these by hand; it calls the `@farcaster/core` builders (`makeCastAdd`, `makeReactionAdd`, `makeLinkAdd` and the others) with your app key. Timestamps count seconds since the Farcaster epoch, 2021-01-01 00:00:00 UTC. Nodes reject a message stamped more than 15 minutes in the future, which is why `casty doctor` and step 1 check your clock. ## Casts A cast holds up to 320 bytes of UTF-8 text, or up to 1024 bytes as a long cast; casty picks the type from the length. Text is counted in bytes, not characters, so a cast in Cyrillic or with emoji fits fewer characters. Mentions are not part of the text. A cast stores the mentioned fids and the byte offsets where they belong, and the feed puts `@name` back in place. A reply points at its parent by fid and hash; a channel post points at the channel's URL instead. Links go in embeds, `--embed ` on the command line. Deleting a cast publishes a `CAST_REMOVE`. Likes and recasts are reactions, follows are links, and each has a remove message too. ## Shards Snapchain splits the network into shards. An account's casts, reactions, follows and profile live on one message shard, chosen from the first 4 bytes of SHA-256 of its fid. Key changes and a few other account-wide messages go to shard 0. That is why `status` and `casty doctor` print one line per shard: the node is synced only when every shard is. ## Sending one through your node 1. casty builds and signs the message and posts its bytes to `POST /v1/submitMessage`. 2. The node checks it and answers `200`, or an error with the reason. 3. A read node keeps nothing: it passes the message on over gossip and forgets it. `200` means accepted for relay, not stored. 4. Once a validator includes the message in a block, every node, yours included, has it. casty therefore reads the message back, for example with `castById` after a cast, until it shows up; [Read nodes and validators](https://castyfun.github.io/casty/concepts/read-nodes-vs-validators/) has the timings. The same bytes have the same hash, so a message sent twice is stored once. ## Storage Every message an account keeps counts against its storage, removes included: a `CAST_REMOVE` stays as a record that the cast is gone. See [Storage rent](https://castyfun.github.io/casty/concepts/storage-rent/). --- # Storage rent Source: https://castyfun.github.io/casty/concepts/storage-rent/ Nodes keep an account's messages only while the account has storage. Storage is rented in units, and each unit allows a number of messages of each kind. The Farcaster docs describe it under [Messages](https://docs.farcaster.xyz/learn/what-is-farcaster/messages). ## What a unit holds The limits per unit depend on the unit's type. `@farcaster/core` 0.20.0 defines three: | Unit type | Casts | Reactions | Links | User data | Verifications | | --- | --- | --- | --- | --- | --- | | `UNIT_TYPE_LEGACY` | 5000 | 2500 | 2500 | 50 | 25 | | `UNIT_TYPE_2024` | 2000 | 1000 | 1000 | 50 | 25 | | `UNIT_TYPE_2025` | 100 | 200 | 200 | 25 | 5 | casty does not compute limits itself; it shows what your node reports for your fid. ## When a store fills up When an account goes over the limit for one kind, the oldest message of that kind is pruned to make room. Removes count too: a deleted cast leaves a `CAST_REMOVE` behind, and that takes a slot like a cast does. When the storage expires, nodes keep the messages for a 30-day grace period, then drop them. ## What casty shows ```sh ./bin/casty storage ``` prints your units and, for each store, how many messages it holds, its limit and how full it is. It warns at 90 percent: ```text warning: is % full; when it fills, the oldest messages of that kind are pruned ``` `casty whoami` shows the same numbers on one line, and `casty storage --fid ` shows any account's. `casty init` refuses an fid with no units at all, because the network would not accept its messages. casty does not rent storage. --- # Wallets and keys Source: https://castyfun.github.io/casty/concepts/wallets/ casty touches two keys. The custody address, an Ethereum account that owns your fid, signs one thing: the request for an app key. The app key, an Ed25519 key casty makes, signs everything else: casts, reactions, follows. This page says how that works. ## The two keys | What | Where it lives | When it is used | | --- | --- | --- | | Seed phrase | Nowhere: typed once into `casty init` or `casty key revoke`, zeroed after use | To derive the custody address and sign the key request, or a revocation | | App key | `~/.config/casty/keys.enc.json`, scrypt and XChaCha20-Poly1305, mode 0600; on the node's host too with `1-node.sh serve` | Every message; lapses after its TTL without use (30 days by default, at most 90), and every message renews it | | Key store passphrase | Typed at a hidden prompt, or a Docker secret on the host | To open the store | `casty init` derives the custody account from the phrase (`--address-index` picks another account of the same phrase), asks the node which fid it holds, makes the app key, has the custody account sign the key request (EIP-712 typed data, as `@farcaster/core` builds it) and sends the `KEY_ADD` message through the node. No transaction, no gas, no API key. `casty key list` shows every key the node lists for your fid; `casty key revoke` removes one, with the phrase or, with `--self`, with the key itself. The page under "you" shows the keys, warns a week before this casty's key lapses, and adds a key with the wallet in the browser (below). ## Add a key with a wallet The custody signature is ordinary EIP-712 typed data, so anything that signs typed data for the custody address can authorise an app key: a wallet app, a hardware wallet behind one, a browser extension. Under you, "add a key with your wallet": casty makes the Ed25519 key and the two typed-data messages, the page hands them to the wallet in the browser (`eth_signTypedData_v4` over EIP-1193: MetaMask, Rabby, Rainbow and the others, a Ledger behind any of them), the wallet signs with the custody address, casty checks both signatures against that address, sends the `KEY_ADD` through the node and stores the key. The seed phrase never leaves the wallet, and casty never sees it. `casty init --fid ` sets up with no phrase and no key, for the page to add the first one. The terminal flow stays for an account whose custody is a bare recovery phrase and nothing else. [In a browser and on a phone](https://castyfun.github.io/casty/web/) has the steps. - On a phone with no wallet in the browser, the page offers to open itself inside MetaMask, Coinbase Wallet or Trust Wallet, pairing included; wallets that announce themselves (EIP-6963) are found side by side; a wallet on the wrong account is asked to let you pick. WalletConnect, a dependency and a relay run by someone else, was left out on purpose. - An account whose custody is the recovery phrase farcaster.xyz gave it imports that phrase into one of those wallets once; from then on the wallet signs and the phrase is typed into nothing of casty's. ## Verified addresses The protocol carries `VerificationAddAddress` messages, an account's proof that it owns an Ethereum or Solana address, and the node answers `verificationsByFid`. A profile on the page lists them, each a link to its explorer and a copy; yours are under you too. Reading them costs nothing and asks no third party; nothing is read from a chain. ## How long a key lives A gasless key lapses after its TTL without use, and every message it signs renews it. casty never posts in your name to keep a key alive; the page warns a week ahead, and `casty init --ttl 90d` is the longest the protocol allows. ## What casty leaves out - Payments, tips and swaps. Each needs a chain RPC, a funded wallet and chain state, which casty keeps off its path on purpose (see [Security](https://castyfun.github.io/casty/security/)). A verified address on a profile can be copied into a wallet you trust. - WalletConnect. It would add a dependency and a relay run by someone else; wallets in the browser and the wallet apps' own browsers cover the same ground. - Passkey pairing and the OS keychain. The page pairs with a 256-bit token that travels in the link's fragment, and `casty web` asks for the passphrase once per start. --- # Security Source: https://castyfun.github.io/casty/security/ casty has not been audited. This page lists what it holds and what each piece can do. ## Secrets | Secret | Where it is | What it can do | How to take it back | | --- | --- | --- | --- | | Seed phrase | In memory while `casty init` or `casty key revoke` runs; never written to disk or sent anywhere. With `casty init --fid` and a key added from the page by a wallet, casty never sees it at all | Everything: it holds the custody of your fid | casty cannot. Keep it offline | | App key (Ed25519) | `~/.config/casty/keys.enc.json`, encrypted with scrypt and XChaCha20-Poly1305, mode 0600 | Sign casts, reactions and follows for your fid until it lapses unused (30 days by default, at most 90) | `casty key revoke ` with the seed phrase, or `--self` with the key itself | | Signed `KEY_ADD` not yet confirmed | In the same encrypted store, until `casty init` sees the key listed or the node refuses the message | Add that one key, once | `casty key revoke` if it ever merges | | Key store passphrase | Typed at a hidden prompt | Decrypt the app keys on this machine | Revoke the keys, delete `keys.enc.json`, run `casty init` | | casty web token | `~/.config/casty/web-token`, mode 0600; in the paired browser's storage | While `casty web` runs: read the feed and write with your app key, from wherever the page is reachable | Stop `casty web`, then start it with `--new-token` | | The same, on the node's host (`1-node.sh serve`) | `~/snapchain/casty/config/keys.enc.json` (0600), the passphrase in `~/snapchain/casty/passphrase` (0600, a Docker secret), the token in `~/snapchain/casty/config/web-token`; the key decrypted in the service's memory while it runs | Read and write as you from any network, while the service runs; root on the host has the key | `casty key revoke ` with the seed phrase; `docker compose stop casty` on the host; `--read-only` keeps the key at home | | Push key pair and subscriptions (`casty serve`) | `~/snapchain/casty/config/vapid.json` (0600) and `cache/push.json` on the host | Send a push, "new mention from @name", to the subscribed browsers; the push service sees only ciphertext | Delete both files and restart `casty`; turn notifications off in the browser | | Cloud token (`HCLOUD_TOKEN`, AWS credentials) | Your environment; the scripts never write it | Create and delete servers in that account | Revoke it in the provider's console | | SSH key | Yours; only the public half is uploaded | Log in to the node host with root rights | Remove it from the host and the provider | A seed phrase controls the whole account. Use a test account for this tutorial, paste a seed phrase only into software from developers you trust, and prefer a key authorised by your wallet: `casty init --fid `, then `casty web` and, under you, "add a key with your wallet". The seed phrase prompt reads from the terminal with echo off. With `--mnemonic-stdin` the phrase comes from a pipe instead, and the passphrase prompt still reads the terminal. casty zeroes the bytes of the phrase after use; the string handed to the key derivation library, and the key it derives, cannot be cleared from memory in JavaScript. `CASTY_PASSPHRASE` replaces the passphrase prompt. It exists for tests and `pnpm demo`; do not use it for a key store that holds a real key. ## What is reachable - On a rented host, the firewall opens SSH (22), the HTTP API (3381) and gRPC (3383) to `--allow-ip` only, your own address by default. Gossip (3382) is open to everyone so other nodes can connect. A host you already have gets no firewall from casty. - statsd (8125/udp, 8126/tcp) and Grafana (3000) are published on the host's loopback only, statsd through the compose file's own `STATSD_PUBLISH` variables in `.env`, Grafana through the override. Grafana's `admin` password is the random one `.env` records; viewing needs no login, as Snapchain's `grafana.ini` has it. - casty talks to your node through the SSH tunnel at `127.0.0.1:3381`. It calls no hosted Farcaster API. The public fname registry at `fnames.farcaster.xyz` is asked only for a name the node does not know: an `@name` you follow or mention in a cast (`--no-allow-fname-server` turns that off), read with `feed --fids` or look up in `casty web`, and the author of a cast you name by its farcaster.xyz link. An OP Mainnet RPC is asked only when you pass `--op-rpc`. - `casty web` listens on 127.0.0.1 unless `--listen` names another address, and every API request to it needs its token. [In a browser and on a phone](https://castyfun.github.io/casty/web/) lists how the page is protected, and what changes when a phone reaches it over the local network. - `1-node.sh serve --domain` opens the page to the internet over HTTPS (80 and 443, opened with `0-provision.sh --https`); `--tailscale` only to your tailnet. The page then needs its token, sends HSTS, and answers one address at most 120 API requests a minute. [From any network](https://castyfun.github.io/casty/serve/) lists what the host holds. - Casts from other people may contain terminal escape sequences. casty removes control characters and bidirectional overrides before printing them, and the web page inserts cast text as text. ## If something leaks - An app key: `casty key list` shows what is registered to your fid. Revoke the key with `casty key revoke --self` from a machine that still has it, or with the seed phrase from anywhere. - The seed phrase: move the fid to a new custody address with a wallet you trust, then revoke every app key. - A cloud token: revoke it, then look for servers in the account that you did not create. - The casty web token: stop `casty web` and start it again with `--new-token`. Every link printed before stops working. On the node's host: delete `~/snapchain/casty/config/web-token` and `docker compose restart casty`. - The node's host: revoke the app key with the seed phrase, delete `~/snapchain/casty`, and treat the host as compromised for the node too. ## Reporting a problem Report vulnerabilities privately through the repository's security advisories, not in a public issue. The repository's `SECURITY.md` carries the same table. --- # Troubleshooting Source: https://castyfun.github.io/casty/troubleshooting/ Every failure prints one line starting with `error:` (the scripts) or `casty:` (the client). casty often adds a `hint:` line with the next step. The messages below are copied from the code; values in angle brackets vary. ## Step 0: provisioning ### No provider ```text error: no provider: set HCLOUD_TOKEN for Hetzner or AWS credentials for AWS, or pass --host user@ip for a host you have (see --help) ``` Export `HCLOUD_TOKEN`, configure the AWS CLI, or pass `--host` for a machine you have. When both Hetzner and AWS credentials are set, the script asks for `--provider hetzner` or `--provider aws` instead. ### No SSH key ```text error: no SSH public key at /home/you/.ssh/id_ed25519.pub. Create one with ssh-keygen -t ed25519, or pass --ssh-pubkey. ``` ### Your IP could not be found ```text error: could not learn your public IP from api.ipify.org; pass --allow-ip ``` The script asks `api.ipify.org` for your address to restrict SSH and the node's APIs to it. Pass the address yourself, for example `--allow-ip 198.51.100.4/32`. ### No server type available ```text warning: could not create a cpx41 server in fsn1; trying the next type error: none of cpx41 ccx23 cpx51 could be created in fsn1 ``` The provider had none of the default types in that location, or your account may not create them. Try another `--region`, or name a type with `--type`. On AWS the message says `instance` and names the zone. ### A different key under the same name ```text error: the project has a different SSH key named fc-node; pass another --name or --ssh-pubkey ``` Hetzner already holds a key called `fc-node` that is not yours. Pick another `--name`, or use the key that is there. ### The run stopped half way ```text node.env records what was created so far: run the same command again to finish, or add --destroy ``` Run the same `0-provision.sh` command again; it reuses what exists. Step 1 refuses such a `node.env`: ```text error: node.env has no NODE_IP: provisioning stopped half way; run ./scripts/0-provision.sh again ``` ## Step 1: the node ### No SSH connection ```text error: no ssh connection to root@: . Check NODE_IP and NODE_SSH_KEY in node.env. ``` A new server takes a while to boot. If it persists, check that `NODE_SSH_KEY` in `node.env` is the private half of the key you gave step 0, and that your IP is still the one the firewall allows: running step 0 again from your current network updates the Hetzner rules. ### The host falls short ```text error: root@ does not meet the requirements above ``` Exit code 5. The table above the message has `no` in the rows that failed: `ram`, `disk`, `docker`, `compose` or `clock`. `docker missing` means Docker is not installed. `docker no server` means the `docker` command is there but cannot reach the Docker daemon: it is stopped (`sudo systemctl start docker`), or the SSH user is neither root nor in the `docker` group. ### The node is not answering yet ```text warning: the node did not return JSON from http://127.0.0.1:3381/v1/info (still starting?); waiting for it ``` Printed once, while the node does not answer yet, for example right after it starts. The wait goes on. If it never ends, `./scripts/1-node.sh logs` shows what the node is doing. ### Not synced in time ```text error: not synced after 43200s ``` The node keeps syncing; only the wait gave up. Watch it with `./scripts/1-node.sh status --wait`, or wait longer with `--timeout `. ### The tunnel does not come up ```text error: the tunnel did not come up; is something else using local port 3381 or 3383? ``` Another process holds one of those ports on your machine, often a tunnel from an earlier run or a local mock node. Stop it, or run `./scripts/1-node.sh tunnel --stop`, then try again. ### A release cannot be pinned ```text error: Snapchain 's docker-compose.yml does not name farcasterxyz/snapchain:latest, so it cannot be pinned ``` The release's compose file names its image some other way, so casty will not rewrite it. Run without `--release`. ## Step 2: the client ### No release for this machine ```text error: no casty binary is built for -; run this script with --from-source, which needs Node 22.12 or newer error: could not download casty-- and SHA256SUMS from : no release for - there yet, or no connection. Run this script with --from-source to build casty here. ``` Releases carry linux-x64, linux-arm64 and darwin-arm64. On another machine, or without a connection to GitHub, `./scripts/2-client.sh --from-source` builds the client here. ### The download does not match its checksum ```text error: the sha256 of the downloaded casty-- is , not the SHA256SUMS lists; nothing was installed ``` The file is not the one the release published: a download cut short, a proxy that changed it, or a mirror named in `CASTY_RELEASE_URL` that holds something else. Run the script again; if it repeats, build from source. ### Node.js too old ```text error: Node is too old; casty needs 22.12 or newer. ``` Only `--from-source` needs Node. The message continues with how to install a newer one with fnm or nvm. On Node 25 or later, corepack is no longer bundled: `npm install -g corepack`. ### casty cannot reach the node ```text casty: cannot reach the node at http://127.0.0.1:3381 hint: start the tunnel with ./scripts/1-node.sh tunnel, or pass --node ``` Exit code 4. The tunnel is down, or the node is somewhere else. ### No fid for the seed phrase ```text casty: no fid is registered to
hint: check --address-index; if your node is still syncing, pass --op-rpc ``` The address at index 0 of the seed phrase holds no fid, at least as far as the node knows. If your wallet uses another account of the same phrase, pass `--address-index`. A node that is still syncing may not have seen the registration yet. ### No storage ```text casty: fid has no storage units, so the network will not accept its messages ``` Rent a storage unit for the fid first. See [Storage rent](https://castyfun.github.io/casty/concepts/storage-rent/). ### The seed phrase is rejected ```text casty: a seed phrase has 12, 15, 18, 21 or 24 words, not casty: word is not in the BIP-39 English word list casty: the seed phrase checksum does not match ``` The messages name word positions, never the words. Check spelling and order. ### The node refuses a message casty prints the node's own message, then a hint chosen by what the message contains: | The message contains | What it means | | --- | --- | | `invalid signature` | the seed phrase does not hold the fid's custody address; check `--address-index` | | `invalid signed key request` | the app fid must be custodied by `--app-address-index` of the same seed phrase | | `nonce is not greater than stored nonce` | another key change landed first; `casty init` retries once by itself | | `active key cap exceeded` | the fid holds 1000 active app keys; revoke some with `casty key revoke` | | `invalid scope` | the app key may not sign this message type; add one with `casty init --scopes` | These strings come from Snapchain's [guide to writing messages](https://snapchain.farcaster.xyz/guides/writing-messages). Exit code 3. ### Wrong passphrase ```text casty: cannot decrypt the key store: wrong passphrase or a damaged file ``` A forgotten passphrase cannot be recovered. Delete `keys.enc.json` and run `casty init` to add a new key. The old keys stay registered until they lapse unused; to remove them sooner, find them with `casty key list` and run `casty key revoke ` with your seed phrase. ### No usable app key ```text casty: no app key for fid yet hint: add one with your wallet in casty web, under you, or run casty init with the seed phrase ``` `casty init --fid` adds no key: approve one with your wallet first. ```text casty: none of the app keys in is registered to fid ``` Your node does not list the key. One approved a moment ago counts once your node has synced past it: `casty doctor` shows how far behind it is. Otherwise the key lapsed after its TTL without use or was revoked; add another in `casty web`, under you, or with `casty init`. ### Accepted but not merged ```text casty: was accepted but not merged within 40s ``` The node took the message but casty did not see it merge ([why](https://castyfun.github.io/casty/concepts/read-nodes-vs-validators/#writes-go-through-but-are-not-kept)). Look for it, for example with `casty feed`, before you send it again: running the command again sends a new message. After `casty init`, the key is kept, and running `casty init` again resends the same message without asking for the seed phrase. ### Numbers that do not match ```text casty: no list numbered yet to take cast 2 from casty: the last list numbered has no cast 9 ``` `casty like 2` means the second cast of the last list shown, by `casty feed`, `casty mentions`, `casty thread` or casty web. Show one first, or name the cast as `:<0x hash>`. ### The node answers 429 or 503 to a feed `casty feed` and casty web ask the node for up to 64 pages at once, so a feed of 300 accounts takes five round trips rather than nineteen. A node that answers some of them with 429 or 503 is being asked for too much at a time: set `CASTY_CONCURRENCY=32` (any whole number from 1 to 256) in the environment of the command, or of `casty web`, and lower it until the errors stop. ### The clock is off ```text warning: this clock and the node's differ by over a minute; nodes reject messages from the future ``` `casty doctor` compares your clock with the node's. Messages stamped more than 15 minutes ahead are refused; turn on time sync on your machine. ## Exit codes | Code | Scripts | casty | | --- | --- | --- | | 0 | done | done | | 1 | not synced, tunnel down | not synced (`node status`), anything else | | 2 | bad arguments | bad arguments | | 3 | a prerequisite is missing | the node refused the message | | 4 | the provider or the host failed | the node cannot be reached | | 5 | the host falls short | config, key store or seed phrase problem | --- # Versions Source: https://castyfun.github.io/casty/versions/ What casty was built and tested with. | Component | Version | Source | | --- | --- | --- | | Node.js | >=22.12, CI runs 22 | package.json, ci.yml | | pnpm | 10.34.7 | package.json packageManager | | @farcaster/core | 0.20.0 | pnpm-lock.yaml | | viem | 2.57.7 | pnpm-lock.yaml | | bun, which packs the one-file client | 1.4.3 | release.yml | | Snapchain | latest release, tested on 0.14.3 | farcasterxyz/snapchain:latest from the @latest compose file; 1-node.sh --release pins one |