Sub Rosa does not require a committed .env file. Secrets stay out of git; you inject them where each layer runs.
| Layer | Needs env? | When vars are read |
|---|---|---|
Jury UI (apps/web) |
Optional | Build time (VITE_* baked into static JS) |
| Keeper / appraisal API | Yes (secrets) | Runtime (shell, systemd, Fly/Railway secrets) |
| One-off scripts (deploy, e2e) | Yes | Runtime (inline VAR=… command or CI secrets) |
The demo works from embedded DEMO_TRACE (demo-trace.generated.ts from pnpm agents:e2e). No .env needed.
pnpm install
pnpm web:build
# static output → apps/web/distHost dist/ on Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, etc.
Build settings (generic):
| Setting | Value |
|---|---|
| Install | pnpm install |
| Build | pnpm web:build |
| Output directory | apps/web/dist |
| Node | 22+ |
Only if you want “Poll live contract” on the deployed site, set these before pnpm web:build in the hosting UI (Vercel → Settings → Environment Variables, etc.).
Copy from apps/web/.env.example:
VITE_RPC_URL=https://soroban-testnet.stellar.org
VITE_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
VITE_CONTRACT_ID=CAPTODBCDEVIK23ALBJBS2TXRTIK47ZA5MBTHYF4XLHG2BK7JPYUCU2Y
VITE_ROUND_ID=1Mainnet example (live poll against mainnet smoke):
VITE_RPC_URL=https://rpc.ankr.com/stellar_soroban
VITE_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"
VITE_CONTRACT_ID=CA7KSDEYJEPGZEB2ZROTLUWKQQ6GIRIQNGG6Z745MZ34QHP4UJPWODEX
VITE_ROUND_ID=1Important: Vite only exposes vars prefixed with VITE_. They are public in the browser bundle — never put secret keys here.
cp apps/web/.env.example apps/web/.env.local
# edit VITE_* then:
pnpm web:dev.env.local is gitignored; not required for production.
Runs on a server/VM, not in the static site. No .env file required — export vars in the process manager:
export KEEPER_SECRET="S…"
export ROUND_CONTRACT_ID="C…"
export RPC_URL="https://soroban-testnet.stellar.org"
export NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
export WATCH_POLL_MS=15000
export WATCH_ROUND_IDS=1
pnpm keeper:watchOr one line:
KEEPER_SECRET=S… ROUND_CONTRACT_ID=C… pnpm keeper:watchFor a read-only preflight of one round, no signing secret is required:
KEEPER_DRY_RUN=true ROUND_CONTRACT_ID=C… ROUND_ID=1 pnpm --filter @sub-rosa/keeper startThe command prints a structured JSON summary with the round status, Drand round, bidder/revealed counts, current phase, and next action. It exits without submitting open, reveal, clear, settle, or void transactions.
On Fly.io / Railway / GitHub Actions: put the same names in Secrets, not in the web build.
See root .env.example for the full keeper variable list.
Scripts read env at invocation — no .env file on disk:
OPERATOR_SECRET=S… pnpm mainnet:deploy
OPERATOR_SECRET=S… \
ROUND_CONTRACT_ID=CA7KSDEY… \
RPC_URL=https://rpc.ankr.com/stellar_soroban \
pnpm mainnet:settleE2E scripts (lifecycle:e2e, agents:e2e) generate ephemeral keys via Stellar CLI — they do not need a root .env either.
Runtime env for pnpm appraisal:start:
| Var | Purpose |
|---|---|
FACILITATOR_SECRET |
Signs/submits x402 settle txs |
PAY_TO / server key |
Receives USDC |
X402_NETWORK |
stellar:testnet (default) or stellar:pubnet |
NETWORK_PASSPHRASE |
Optional; when set, must match X402_NETWORK |
RPC_URL |
Soroban RPC; required for pubnet |
PAYMENT_ASSET |
SEP-41 contract; defaults to USDC for the selected network |
PRICE |
Appraisal price (default 0.10) |
PORT |
HTTP port (default 4021) |
Minimal local/testnet configuration:
export FACILITATOR_SECRET="S…"
export PAY_TO="G…"
export X402_NETWORK="stellar:testnet"
export NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
pnpm appraisal:startTestnet defaults to the canonical testnet USDC contract, Soroban testnet RPC,
price 0.10, and port 4021. For pubnet, set
X402_NETWORK=stellar:pubnet, the public-network passphrase, and an explicit
RPC_URL; the default payment asset then changes to pubnet USDC.
Configuration errors fail before the HTTP server starts and name the affected variable, for example:
FACILITATOR_SECRET: missing required env var
PRICE: must be a finite number greater than 0
X402_NETWORK: unsupported network "stellar:mainnet"
NETWORK_PASSPHRASE: does not match X402_NETWORK=stellar:testnet
RPC_URL: is required when X402_NETWORK=stellar:pubnet
Agents point at the public URL via X402_APPRAISAL_URL — not baked into the web UI.
Shipping jury demo only?
→ pnpm web:build, upload dist/, no env
Want live on-chain overlay on the site?
→ set VITE_* in hosting build env, then build
Running keeper 24/7?
→ KEEPER_SECRET + ROUND_CONTRACT_ID on the server (runtime)
Deploying new contract round?
→ OPERATOR_SECRET inline for mainnet:deploy / mainnet:settle
pnpm mainnet:ready -- --strict # consolidated read-only readiness
pnpm mainnet:verify # read-only — no secrets
pnpm mainnet:micro # dry-run checklist
MAINNET_CONFIRM=SUB_ROSA_MAINNET OPERATOR_SECRET=S… BIDDER_SECRET=S… \
pnpm mainnet:micro -- --execute # optional micro commit (≤1 XLM escrow)
MAINNET_CONFIRM=SUB_ROSA_MAINNET KEEPER_SECRET=S… ROUND_CONTRACT_ID=C… \
pnpm mainnet:settle # keeper settle (requires readiness + confirm)- Run
pnpm mainnet:ready -- --strict(no secrets required for baseline checks). - Run
pnpm mainnet:verifyto confirm settled round 1 matches frozen artifacts. - Optional balance review:
pnpm mainnet:ready -- --with-balanceswith funded operator/keeper secrets in env. - For value-moving commands, set
MAINNET_CONFIRM=SUB_ROSA_MAINNETand re-run readiness implicitly via deploy/micro/settle guards. - After settlement, confirm contract native XLM SAC balance is 0 (settle script enforces this).
- Never commit
.envwith secrets (already in.gitignore). - Never put
S…secret keys inVITE_*— they end up in public JS. - Rotate any key that was pasted in chat or logs.