Appearance
Environments
There are two: production and your laptop. That is worth stating plainly before anything else.
There is no staging database
bz-backend's local .env uses the production Atlas connection string, and Atlas Network Access includes 223.233.0.0/16 for rotating developer ISP IPs. Local development reads and writes production data.
Set CRON=false before starting a local backend, or your machine becomes a second CSIP and Autopilot executor using the production PRIVATE_KEY_EXECUTOR.
Production
The VM
| Property | Value |
|---|---|
| Host | bz-prod / 34.14.156.239 (static) |
| Project | blazpay-latest |
| Zone | asia-south1-c |
| SSH user | bisht |
| Key | ~/.ssh/google_compute_engine |
| Fallback | gcloud compute ssh bz-prod --zone=asia-south1-c |
| Process manager | PM2 |
Processes
| PM2 app | Port | Directory | Branch | Built on deploy | Local checkout |
|---|---|---|---|---|---|
bz-backend | 5000 | /home/bisht/bz-backend | bishtNew | no | yes |
bz-agent | 4000 | /home/bisht/bz-agent | main | yes | yes |
bz-tokens | 5003 | /home/bisht/bz-tokens | main | yes | no |
qube-backend | 3001 | /home/bisht/qubetics/backend | main | no | no |
bz-tokens and qube-backend exist only on the server
There is no local copy. deploy.gcp.sh refuses to sync them and tells you so. Editing means editing on the VM, or cloning locally and setting LOCAL_DIR in the script.
PM2 configuration for the backend is bz-backend/ecosystem.config.cjs: one instance, fork mode, autorestart, max_memory_restart: 1G, kill_timeout and listen_timeout both 10 s, logs at /home/bisht/.pm2/logs/bz-backend-{error,out}.log.
Single instance, fork mode — do not change this casually
The backend holds in-process cron schedulers. PM2 cluster mode would start N copies of every cron, so dcaCron would submit the same batch N times with the same executor key. Horizontal scaling requires moving the crons out of the API process first.
Frontend
Netlify, two sites from one repository:
| Site | Build command | Output | Config |
|---|---|---|---|
defi.blazpay.com | npm run build | dist/ | netlify.toml |
ai.blazpay.com | npm run build:ai | dist-ai/ | netlify.ai.toml |
Both auto-deploy from the optimized branch.
Data stores
| Store | Deployment | Notes |
|---|---|---|
| MongoDB | Atlas | 130+ collections. Network Access includes 223.233.0.0/16 |
| Redis | Managed | Quote cache, fee cache, Autopilot price series, Socket.io adapter, LangGraph checkpoints |
| PostgreSQL + PGVector | — | bz-agent knowledge base only |
A separate agents database holds the external copy-trading engine's copytrade_* collections. Not part of bz-backend.
Hostnames
| Host | Serves | Built from |
|---|---|---|
blazpay.com | Brand hub | Separate marketing site |
defi.blazpay.com | Blazpay DEX — trading only | defi-dex, VITE_SITE=defi |
ai.blazpay.com | BlazAI — chat only | defi-dex, VITE_SITE=ai |
rewards.blazpay.com | Rewards, staking, GameFi, DAO | Separate rewards frontend |
api.blazpay.com | REST + SSE + Socket.io | bz-backend on the VM |
audit.blazpay.ai | Public share artifacts | Migrating to ai.blazpay.com |
kima.blazpay.com | Kima bridge backend | Server-only (provider disabled) |
qube.blazpay.com | Qubetics surface | Server-only |
uat.blazpay.com | UAT | CORS-allowlisted; a legacy target |
Local
| Service | Port |
|---|---|
bz-backend | 5000 |
bz-agent | 4000 |
defi-dex defi shell | 5173 |
defi-dex AI shell | 5174 |
defi-dex AI preview | 4174 |
| These docs | 5199 |
All of the frontend ports plus 5000, 3000, 3001, 3002 and 3039 are in the backend CORS allowlist, so a local frontend can talk to production API with no CORS change.
Full setup: local development.
CORS allowlist
bz-backend/index.js, with credentials: true:
https://rewards.blazpay.com https://blazpay.com
https://uat.blazpay.com https://qube.blazpay.com
https://galxe.com https://defi.blazpay.com
https://admin.blazpay.com https://nfts.qubetics.com
https://blazpay.ai https://www.blazpay.ai
https://audit.blazpay.ai https://ai.blazpay.com
http://localhost:3000 http://localhost:3001
http://localhost:3002 http://localhost:3039
http://localhost:5000 http://localhost:5173
http://localhost:5174Socket.io CORS is separate
Configured in services/socket.js, not by the REST corsOptions. That was the one real gap the AI domain split had to close.
Deprecated hosts
For context when reading old scripts or logs:
| Host | Was | Superseded by |
|---|---|---|
Azure 20.198.176.70 | Original backend, scripts/deploy.sh | GCP |
Kamatera 194.113.195.167 (bz-backend-us) | Previous backend, scripts/deploy.kamatera.sh | GCP |
Both deploy scripts are still in the repo. Do not run them.
Contract environments
| Product | Mainnet | Testnet |
|---|---|---|
| BlazpayRelayer | 22 chains | — |
| DCA vault | Arbitrum | — |
| Autopilot vault | Arbitrum, Robinhood Chain | Arbitrum Sepolia (0x7cA5A09E…) |
Testnets cannot test the interesting half
There is no aggregator liquidity on testnets, so a route cannot be quoted — which means quote → build → settle cannot be exercised there. A tiny mainnet trade is more informative. That is what was done to prove the Autopilot loop.
Arbitrum has two hardhat network definitions, and they use different keys
arbitrum signs with BACKEND_PRIVATE_KEY (the Autopilot owner, 0x2Ed0…0162). arbitrum_dca_owner signs with DCA_OWNER_KEY (the executor, 0x5935…0459, which is the DCA vault's owner). Same chain id, different signer.
Pick deliberately: running a DCA upgrade against --network arbitrum fails the onlyOwner check, and running an Autopilot script against arbitrum_dca_owner fails the other way.
Feature flags at runtime
Two mechanisms, and the second is usually the right one:
| Mechanism | Change cost | Use for |
|---|---|---|
| Environment variable | SSH + edit .env + restart | Secrets, infrastructure, cron master switch |
SystemSetting in Mongo | A database write | Limits, quotas, feature toggles |
js
await SystemSetting.getValue('audit_free_limit_per_wallet') // default 3
await SystemSetting.setValue('audit_free_limit_per_wallet', 5)
await SystemSetting.getByCategory('limits')Categories: bridge, security, network, fees, limits, general.
Notable env-var switches:
| Variable | Effect |
|---|---|
CRON=false | Disables every cron |
DISABLE_BRIDGE_STATUS_CRON | Just that one |
DISABLE_REDIS | Force the in-memory fallback |
AUTOPILOT_ADDRESS_* | Per-chain vault address; empty means the cron skips that chain |
*_FORCE_PROXY, *_PROXY_GROUPS | Route rewards/threads traffic to separate services |
External dependencies
A partial outage degrades a specific product rather than the platform:
| Service | Product affected |
|---|---|
| 21 aggregator APIs | Individual routes drop out; the rest still quote |
| Orderly Network | Perpetuals entirely — no fallback venue |
| Zerion | Portfolio and wallet card |
| Etherscan v2 | Auditor, token safety, approvals, wallet card |
| Model provider (extraction) | Audit, token safety, all extraction nodes |
| Model provider (conversation) | Conversation node (falls back to the extraction provider, more cheaply) |
| Model provider (drafting) | Autopilot policy drafting |
| CoinMarketCap | Autopilot triggers |
| CoinGecko | Fee summary |
| ZeroDev | dApp builder deployment |
| 5 fiat ramps | Buy/sell — degrades gracefully |
| Firebase, AWS SES, Cloudinary, Moralis | Notifications, email, images, some balances |
Aggregator failures are swallowed by design
AggregatorFactory.getQuotes wraps each provider in a try/catch so one failure cannot kill an SSE stream. The user sees fewer routes, not an error — which also means a permanently-broken provider goes unnoticed. See monitoring.