Two deployable units: the API (Fastify, Node 22) and the site (Next.js). The API needs Postgres; Redis is optional (QUEUE_DRIVER=bullmq moves verification to a worker process, inline runs jobs inside the API).
Environment
| Variable | Default | Meaning |
|---|---|---|
PORT, HOST | 4100, 127.0.0.1 | API listener (0.0.0.0 behind a proxy) |
DATABASE_URL | local eworld | Postgres |
SESSION_SECRET | — | set a random 32-byte string |
CORS_ORIGIN | localhost:3000,3001 | comma-separated site origins |
ADMIN_WALLETS | — | comma-separated wallets that review problems and results |
QUEUE_DRIVER, REDIS_URL | inline | bullmq needs Redis |
STORAGE_DRIVER, STORAGE_DIR | local, ./data/storage | or r2 with R2_* |
VERIFIER_RUNNER | local | docker runs nodes in isolated containers |
SOLANA_MODE | mock | mock, rpc, program |
SOLANA_RPC_URL, SOLANA_COMMITMENT | devnet, confirmed | |
SOLANA_KEYPAIR_PATH / SOLANA_KEYPAIR_JSON | ./data/platform-keypair.json | the platform key (JSON wins) |
SOLANA_PAYOUTS | false | rpc mode: let the platform pay winners |
PRIZE_TOKEN_MINT, PRIZE_TOKEN_DECIMALS | — , 6 | the prize token |
DEVNET_FAUCET | false | expose /api/faucet (platform must be mint authority) |
ANTHROPIC_API_KEY | — | enables the Claude provider for server swarms |
SWARM_CLAUDE_ACCESS | admins | admins or all: who may spend the server's model key |
SWARM_MAX_BUDGET_USD, SWARM_DAILY_BUDGET_USD | 25, 100 | caps on server-paid model spend |
SOLANA_PUBLIC_RPC_URL | — | an RPC URL safe to show browsers; the server's own URL is never exposed |
VERIFIER_UID, VERIFIER_GID | 10001 in the image | unprivileged user verifier code runs as |
PUBLIC_API_URL | bound port | how server swarms reach the API |
NEXT_PUBLIC_API_URL | localhost:4100 | the site's API origin (build-time) |
Railway
The repository ships Dockerfile.api and Dockerfile.web; each Railway service picks its file through the RAILWAY_DOCKERFILE_PATH variable. With the Railway CLI:
railway init -n eworld
railway add -d postgres
railway add -s api -v RAILWAY_DOCKERFILE_PATH=Dockerfile.api -v HOST=0.0.0.0 -v PORT=4100 -v SESSION_SECRET=<32 random bytes> -v STORAGE_DIR=/data/storage -v SOLANA_MODE=rpc -v SOLANA_RPC_URL=https://api.mainnet-beta.solana.com -v SOLANA_PAYOUTS=true -v SOLANA_KEYPAIR_JSON='[…]' -v PRIZE_TOKEN_MINT=<mint> -v ADMIN_WALLETS=<your wallet>
railway variables -s api --set 'DATABASE_URL=${{Postgres.DATABASE_URL}}'
railway service api && railway volume add -m /data
railway domain -s api # → https://api-…up.railway.app
railway add -s web -v RAILWAY_DOCKERFILE_PATH=Dockerfile.web -v PORT=3000 -v NEXT_PUBLIC_API_URL=https://api-…up.railway.app
railway domain -s web
railway variables -s api --set CORS_ORIGIN=https://web-…up.railway.app
railway up -s api -d && railway up -s web -dThe API container runs migrations and the idempotent seed on every start. On a real cluster the platform wallet (shown on /api/health) must hold SOL before vaults can be created: send it a little SOL (each vault costs about 0.002 SOL in rent, anchors and payouts a fee each), then redeploy the api service and the problems appear. Add a .railwayignore mirroring .dockerignore so node_modules, .next and build artefacts are not uploaded. Set NEXT_PUBLIC_REPO_URL on the web service so the landing page's clone command is real.
A real prize token
On devnet, create a mint with the platform key as authority so the faucet works: spl-token create-token --decimals 6 with the platform keypair, then set PRIZE_TOKEN_MINT. On mainnet, point PRIZE_TOKEN_MINT at the real token, set DEVNET_FAUCET=false, and fund the platform key with SOL for fees. The custodial rpc vault is a hot wallet: for real money move to program mode and deploy programs/eworld-prize.
Operations
- Backups: Postgres is the source of truth for everything except submitted files and verifier
packages, which live in STORAGE_DIR or R2. Back up both.
- Verifier isolation: in the API image, verifier code runs as an unprivileged user in its own
process group: it cannot read the API's environment (where the platform key lives), the data volume, or other processes, and everything it spawns is killed with it. It still has network access, so reviewers read every verifier before opening a problem. VERIFIER_RUNNER=docker (no network, read-only root) is stronger where Docker is available.
- Restarts: with the inline queue, jobs interrupted by a restart are rebuilt on start: interrupted
verifications re-run, pending contributions are re-checked, unanchored certificates are anchored.
- Audit:
GET /api/bank/auditshould always returnok: true; alert on anything else. - Tests:
npm run api:test(Postgreseworld_test), including the on-chain suite when
solana-test-validator is installed.
