Skip to main content
A paper windmill on a walnut post
Windrose is three long-running processes plus a reverse proxy. Everything defaults to Robinhood testnet (46630); the same steps work for any chain in packages/abis/chains.json once the contracts are deployed there and packages/abis/deployments/<chainId>.json exists (see Deploy contracts and Add a chain). Requirements on the box: node 22, pnpm 9 (via corepack), pm2, git. About 1.5 GB of RAM headroom is enough for a testnet; each indexer sits around 600 to 700 MB during backfill, so a box running three chains wants a swapfile (the production box has 3.8 GB of RAM and a 2 GB swapfile).

Choosing the chain

packages/abis/chains.json is the chain registry: one entry per chain with its RPCs and websocket, explorer and Blockscout verifier URL, gas parameters, the dollar launches settle in, EURC, Permit2, Multicall3, Pyth, the official Uniswap v4 addresses, a faucet and the indexer’s eth_getLogs range. @launchpad/abis exports it as CHAINS and getChain(id), scripts/chain-env.sh prints it for shell scripts, and contracts/script/Deploy.s.sol mirrors the addresses in its _defaults table. Every process selects an entry by chain id: An id that is not in the registry fails at startup with the list of registered chains. Registered today: Arc testnet 5042002 (arc-testnet), Arc 5042 (arc, not deployed), Robinhood testnet 46630 (robinhood-testnet), Robinhood Chain 4663 (robinhood).

One keeper and one indexer per chain

The web app serves every chain at once, so each chain the switcher offers needs its own keeper (rates) and its own indexer (history). Both run from the same packages with a different CHAIN_ID; deploy/ecosystem.config.cjs starts all of them: The indexers share packages/indexer/.env.local (the RPC and start-block variables carry the chain id) and, without DATABASE_URL, each keeps its own PGlite database in .ponder/pglite-<chainId>. The keepers share packages/keeper/.env plus one .env.<chainId> per chain for the key. See Indexer and Keeper.
docs/HOSTING.md in the repository still describes windrose-keeper and windrose-indexer as the Robinhood testnet pair on port 42069. deploy/ecosystem.config.cjs is the current state: that pair is Robinhood Chain mainnet, and the testnet pair carries the -rh-testnet suffix on port 42071.

First run

The script copies the three env files from their examples when they are missing, so check them first:
KEEPER_PRIVATE_KEY, CHAIN_ID, optionally RPC_URL (default: the registry rpc of CHAIN_ID). The keeper must be allow-listed by the oracle owner (setKeeper). Rates are fresh for 2 hours and the keeper re-posts hourly, so this process must stay up or minting and backing stop. Gas parameters come from the registry.
CHAIN_ID, PONDER_RPC_URL_<chainId> (an endpoint with archive state that tolerates topic-filtered eth_getLogs; see the per-chain table in Indexer), START_BLOCK_<chainId> (the deployment block), DATABASE_SCHEMA. ETH_GETLOGS_BLOCK_RANGE defaults to the registry value. For durability add DATABASE_URL=postgresql://...; without it Ponder uses an embedded PGlite database under packages/indexer/.ponder.
NEXT_PUBLIC_CHAIN_ID (the default chain), an RPC per chain (NEXT_PUBLIC_RPC_URL_<chainId>) and an indexer URL per chain (NEXT_PUBLIC_INDEXER_URL_<chainId>=https://api.<your-domain>). The browser calls the indexer URLs, so they must be public; rebuild the web app after changing any of them.
pm2 runs the packages’ JS entry files (node_modules/next/dist/bin/next, node_modules/tsx/dist/cli.mjs, node_modules/ponder/dist/esm/bin/ponder.js), not the node_modules/.bin/* shims: pnpm writes those as shell scripts, which pm2 would try to run with node.

Domain and reverse proxy

One hostname for the app plus one per indexer. Caddy issues TLS certificates automatically:
The production box uses nginx and certbot instead: one site file with server blocks on port 80 that proxy the app hostname (and www) to 127.0.0.1:3000, api. to 127.0.0.1:42069 (with a location /testnet/ that proxies to 127.0.0.1:42071 for the Robinhood testnet indexer) and api-arc. to 127.0.0.1:42070, each serving /.well-known/acme-challenge/ from /var/www/html for certbot’s HTTP challenge and passing the usual X-Forwarded-* and Upgrade headers. nginx -t && systemctl reload nginx after edits. The indexers already answer CORS, so the browser can query api. from the app’s origin without extra rules. DNS: an A record per hostname (@, www, api, api-arc) pointing at the box, optional AAAA records, and behind Cloudflare keep them “DNS only” until the certificate exists. deploy/certbot-when-dns.sh runs from root’s crontab every 5 minutes, waits until every hostname resolves to the box’s public IPv4, then runs certbot --nginx --redirect for all of them (which also rewrites the port-80 blocks to redirect to HTTPS) and removes its own cron line. Renewal is certbot’s systemd timer.
This documentation site is not on the box. It is built by Mintlify from apps/docs and served at docs.windrose.market through a CNAME record, with the TXT verification records the Mintlify dashboard shows. The app links to it through NEXT_PUBLIC_DOCS_URL.
Then set the public indexer URLs in apps/web/.env.local, rebuild (pnpm --filter @launchpad/web build) and pm2 restart windrose-web. Regenerate the token list with the real origin so wallets can fetch logos: TOKEN_LOGO_BASE=https://launch.example.com/tokens/ CHAIN_ID=<id> pnpm abis:tokenlist.

Day to day

Updating: git pull, pnpm install --frozen-lockfile, pnpm --filter @launchpad/web build, pm2 restart all. If the web build gets killed for lack of memory, pm2 stop windrose-indexer-arc first, build, then start it again; if the box swaps hard, stop the Arc pair (windrose-indexer-arc, windrose-keeper-arc) while Arc is listed as “soon” on the public site.

Contract operations from the box

Install Foundry (WITH_FOUNDRY=1 bash scripts/vps-setup.sh), put a testnet deployer key in contracts/.env, then follow Deploy contracts. The testnet deployer and keeper keys hold only faucet funds; mainnet is signed by an encrypted keystore on the workstation, and the box’s mainnet keeper key only ever holds gas. Never commit any .env file.