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 |
|---|---|---|
Hosted pilot 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_STELLAR_NETWORK=testnet
VITE_RPC_URL=https://soroban-testnet.stellar.org
VITE_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
VITE_CONTRACT_ID=CCOVGOQQZJKZ2R55GRWBLTJTGBAMSHXZVN3ICPG3WRVMLMM6RHISC5OVCore v2 mainnet example:
VITE_STELLAR_NETWORK=mainnet
VITE_RPC_URL=https://mainnet.sorobanrpc.com
VITE_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"
VITE_CONTRACT_ID=CDQOFNCJE5Z4ZZL76DU5652FOUKJVEIZWHFGCZVWH63UYBGPSZIPC325
VITE_ROUND_ID=1Do not use the legacy v1 mainnet proof as the Core v2 UI contract. Named network configuration validates this boundary at build time.
Important: 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 STELLAR_NETWORK=testnet
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:
pnpm mainnet:v2:prepare # local/read-only; sends no transaction
MAINNET_CONFIRM=SUB_ROSA_MAINNET OPERATOR_IDENTITY=sub-rosa-mainnet-v2-operator \
pnpm mainnet:v2:deploy
MAINNET_CONFIRM=SUB_ROSA_MAINNET \
OPERATOR_IDENTITY=sub-rosa-mainnet-v2-operator \
BIDDER_IDENTITY=sub-rosa-mainnet-v2-bidder \
ROUND_CONTRACT_ID=C… pnpm mainnet:v2:smoke -- --executeThe named identities can be generated with stellar keys generate NAME --secure-store. Secure-store identities intentionally do not reveal an S...
secret; these scripts ask Stellar CLI to sign with the Keychain identity
directly. Headless CI may use OPERATOR_SECRET and BIDDER_SECRET instead,
but each role must set exactly one identity or secret source.
# Headless CI alternative; inject these from the CI secret store.
OPERATOR_SECRET=S…
BIDDER_SECRET=S…The deployer pays the WASM upload and deployment fees. During the capped smoke, the seller/operator pays create and lifecycle calls while the bidder pays its commit. The SDK never charges these fees to a Sub Rosa-owned account. Escrow and the auction lot are separate from network fees.
E2E 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 the hosted pilot UI 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?
→ mainnet:v2:prepare first; funded signers only for deploy/smoke execution
pnpm mainnet:v2:prepare # build/hash/bindings; 0 transactions
ROUND_CONTRACT_ID=C… pnpm mainnet:v2:smoke -- --dry-run
MAINNET_CONFIRM=SUB_ROSA_MAINNET OPERATOR_IDENTITY=sub-rosa-mainnet-v2-operator \
pnpm mainnet:v2:deploy # funded deployment
MAINNET_CONFIRM=SUB_ROSA_MAINNET \
OPERATOR_IDENTITY=sub-rosa-mainnet-v2-operator \
BIDDER_IDENTITY=sub-rosa-mainnet-v2-bidder \
ROUND_CONTRACT_ID=C… pnpm mainnet:v2:smoke -- --executeAfter deployment, verify the exact on-chain WASM before executing the smoke:
ROUND_CONTRACT_ID=CDQOFNCJE5Z4ZZL76DU5652FOUKJVEIZWHFGCZVWH63UYBGPSZIPC325 pnpm mainnet:v2:verifyThe historical mainnet:ready, mainnet:verify, mainnet:micro, and
mainnet:settle commands target the frozen legacy v1 proof. They are retained
for backward compatibility. Prefer the explicit mainnet:legacy:ready,
mainnet:legacy:verify, mainnet:legacy:micro, and mainnet:legacy:settle
names for historical evidence; they are not the Core v2 launch path.
- Run
pnpm mainnet:v2:prepare; require the built hash to match the testnet-reviewed Core v2 WASM. - Fund separate mainnet operator and bidder accounts. Keep values capped.
- Deploy only with
MAINNET_CONFIRM=SUB_ROSA_MAINNETand preserve the local deployment artifact. - Set the resulting Core v2
ROUND_CONTRACT_ID, run the secrets-freepnpm mainnet:v2:verify, then run the smoke dry-run. - Execute the capped lifecycle and publish the six transaction links plus verified receipt.
- Promote the address into
SUB_ROSA_DEPLOYMENTS.mainnetonly after independent review of the artifact and evidence.
- 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.