SpawnClaw is a platform that lets users spawn isolated AI agent instances (running OpenClaw) inside Docker containers. Users authenticate via MetaMask wallet, deposit $SPAWNCLAW tokens to get USD-based balance, then create instances connected to Telegram or WhatsApp.
Architecture:
Frontend (Next.js) → Control Plane API → Spawn Agent → Docker Containers (OpenClaw)
Key components:
apps/web- Landing page (wallet connect)apps/app- Dashboard (instance management, deposits)apps/api- Control Plane API (auth, billing, orchestration)spawn-agent- Docker container lifecycle manager (runs on VPS)docker-runtime- Dockerfile + scripts for OpenClaw containers
- Node.js 22+
- Docker (or Colima on macOS)
- PostgreSQL database (local, Supabase, or Neon)
- OpenRouter API key(s) from https://openrouter.ai
- MetaMask wallet (for frontend auth)
git clone <repo-url> && cd spawnclaw
npm install # root workspaceThis image pre-installs OpenClaw to avoid 4+ minute startup delay per container.
cd docker-runtime
docker build -t spawnclaw-runtime .Important notes:
- First build takes ~7 minutes (OpenClaw npm install)
- Subsequent builds are fast if only
entrypoint.shchanges (layer caching) - The Dockerfile installs OpenClaw via
setup.sh, then copiesentrypoint.shlast for optimal caching
SPAWNCLAW_SECRET=your-shared-secret-between-api-and-agent
PORT=4000
GATEWAY_PORT_START=18790
GATEWAY_PORT_END=18850PORT=6702
# Must match spawn-agent secret
SPAWN_AGENT_URL=http://localhost:4000
SPAWN_AGENT_SECRET=your-shared-secret-between-api-and-agent
# Database
DATABASE_URL=postgres://user:password@localhost:5432/spawnclaw
# OpenRouter (passed to every container)
OPENROUTER_API_KEY=sk-or-v1-xxx
# Or multiple keys for rotation:
# OPENROUTER_API_KEYS=sk-or-v1-key1,sk-or-v1-key2,sk-or-v1-key3
OPENROUTER_DEFAULT_MODEL=openrouter/auto
# Instance resources
INSTANCE_MEMORY=1g
INSTANCE_CPUS=1
# $SPAWNCLAW Token (for deposit verification)
TREASURY_BASE=0xYourTreasuryAddress
TOKEN_ADDRESS_BASE=0xYourTokenContract
TOKEN_DECIMALS_BASE=18
INSTANCE_COST_USD=15
# Admin
ADMIN_SECRET=your-admin-secretNEXT_PUBLIC_API_URL=http://localhost:6702
NEXT_PUBLIC_LANDING_URL=http://localhost:6700Create the PostgreSQL database and run migrations:
cd apps/api
npx drizzle-kit pushDatabase tables:
| Table | Purpose |
|---|---|
users |
Wallet addresses, USD balance, blocked status |
instances |
Running instances, status, gateway endpoints |
instances_channels |
Telegram/WhatsApp config per instance |
deposits |
Verified $SPAWNCLAW token deposits |
payments |
Payment records for instance creation |
Start in this order:
cd spawn-agent
node index.js
# Output: Spawn Agent API running on port 4000cd apps/api
node index.js
# Output: Control Plane API running on port 6702cd apps/app
npm run dev
# Output: Ready on http://localhost:3000- User clicks "Connect Wallet" on the landing page (
apps/web) - MetaMask prompts for wallet connection
- Wallet address (e.g.,
0xABC...123) is stored inlocalStorageasauthToken - Every API request includes
Authorization: Bearer <wallet-address> - API looks up or creates user by wallet address
- User sends $SPAWNCLAW tokens to treasury wallet on Base or Solana chain
- User submits transaction hash on the deposit page
- API verifies the transaction on-chain:
- Fetches live token price from DexScreener
- Verifies token transfer to treasury address
- Calculates USD value of deposited tokens
- USD value is credited to user's
balancefield - Each instance costs
$15(configurable viaINSTANCE_COST_USD)
User Dashboard → POST /api/instances/create → Control Plane API → Spawn Agent → Docker
- User fills form: channel (Telegram/WhatsApp), bot token, model selection
- API checks balance >= $15, deducts balance
- API builds spawn config with OpenRouter keys from env vars
- Compute Pool selects best provider (Docker by default)
- Docker Provider sends POST to Spawn Agent
/spawn - Spawn Agent allocates a unique port (18790-18850), runs
docker run:docker run -d --name <instanceId> \ --memory=1g --cpus=1 \ -p <port>:<port> \ -e OPENROUTER_API_KEY=... \ -e OPENROUTER_MODEL=... \ -e GATEWAY_PORT=<port> \ -e TG_BOT_TOKEN=... \ spawnclaw-runtime - Container starts →
entrypoint.shruns:- Picks random API key from
OPENROUTER_API_KEYS(if multiple) - Runs
openclaw onboardwith OpenRouter auth - Configures Telegram credentials
- Starts OpenClaw gateway on assigned port with
--bind lan
- Picks random API key from
- API saves instance to database, returns gateway endpoint to frontend
- Dashboard polls instance status every few seconds
User Dashboard → POST /api/instances/:id/stop → API → Spawn Agent → docker stop + rm
- API verifies user owns the instance
- Spawn Agent runs
docker stop+docker rm - Port is released back to the pool
- Instance status updated to "stopped" in database
SpawnClaw uses OpenRouter as the only AI provider. This gives access to all major models (Claude, GPT, Llama, Gemini, etc.) through a single API.
If you provide multiple API keys via OPENROUTER_API_KEYS (comma-separated), each container randomly selects one key at startup. This distributes rate limits across keys.
# In apps/api/.env
OPENROUTER_API_KEYS=sk-or-v1-key1,sk-or-v1-key2,sk-or-v1-key3Users can choose a model when creating an instance. Default is openrouter/auto (cheapest available). The model is passed through the full chain:
Frontend → API (model param) → Docker Provider → Spawn Agent → Container env → OpenClaw config
Each OpenClaw instance needs its own gateway port. The Spawn Agent manages this automatically:
- Range: 18790-18850 (configurable via
GATEWAY_PORT_START/GATEWAY_PORT_END) - Assignment: Sequential, first available port
- Mapping: 1:1 host-to-container (
-p 18790:18790) - Release: Port freed when instance is stopped
- Capacity: 60 ports per agent, but RAM is usually the bottleneck
For a VPS with 12GB RAM / 6 CPU cores:
| Metric | Value |
|---|---|
| Peak RAM per instance (onboarding) | ~800MB |
| Steady-state RAM per instance | ~300MB |
| Recommended instances | 6-8 |
| Max instances (theoretical) | ~10 |
| Port range capacity | 60 |
Key constraint: RAM, not ports. Each instance needs ~1GB allocated (--memory=1g), with actual usage around 300MB steady-state but 800MB during OpenClaw onboarding.
You can scale beyond a single VPS by adding multiple servers. Each VPS runs its own spawn-agent, and the Control Plane API distributes instances across them.
Frontend → Control Plane API (1 server)
↓
ComputePool (scheduler)
↙ ↓ ↘
VPS-1 VPS-2 VPS-3
(spawn-agent) (spawn-agent) (spawn-agent)
↓ ↓ ↓
Docker Docker Docker
- Run
setup-vps.shon each VPS (installs Docker, Node.js, etc.) - On each VPS, only start the spawn-agent (not the API or frontend):
cd /opt/spawnclaw/spawn-agent npm install pm2 start index.js --name spawn-agent - Open firewall port 4000 for the API server's IP only:
ufw allow from <API_SERVER_IP> to any port 4000
On the API server, set DOCKER_PROVIDERS in apps/api/.env with all VPS servers:
DOCKER_PROVIDERS='[
{
"id": "vps-us-1",
"name": "US East VPS",
"url": "http://167.71.0.1:4000",
"secret": "shared-secret-vps1",
"region": "us-east",
"maxInstances": 8,
"priority": 10
},
{
"id": "vps-eu-1",
"name": "EU Frankfurt VPS",
"url": "http://138.68.0.1:4000",
"secret": "shared-secret-vps2",
"region": "eu-west",
"maxInstances": 8,
"priority": 5
}
]'Note: When DOCKER_PROVIDERS is set, the single SPAWN_AGENT_URL/SPAWN_AGENT_SECRET vars are ignored.
| Field | Required | Default | Description |
|---|---|---|---|
id |
Yes | - | Unique provider ID (e.g., vps-us-1) |
url |
Yes | - | Spawn agent URL on this VPS |
secret |
Yes | - | Shared secret (must match VPS's SPAWNCLAW_SECRET) |
name |
No | same as id |
Human-readable name |
region |
No | default |
Region label for region-based scheduling |
maxInstances |
No | 50 |
Max instances on this VPS |
priority |
No | 5 |
Higher = preferred (used by priority_first strategy) |
costPerHour |
No | 0.01 |
Cost tracking (used by cost_optimized strategy) |
enabled |
No | true |
Set false to disable without removing |
Control which VPS gets new instances via SCHEDULER_STRATEGY:
| Strategy | Behavior |
|---|---|
priority_first |
Use highest-priority VPS first (default) |
capacity_balanced |
Use VPS with most available slots |
cost_optimized |
Use cheapest VPS first |
round_robin |
Rotate across all VPS servers |
random |
Random selection |
If spawn fails on the selected VPS, the ComputePool automatically retries on the next available provider. No manual intervention needed.
# List all providers and their status
curl http://your-api:6702/api/compute/providers
# Check combined capacity across all VPS
curl http://your-api:6702/api/compute/capacity
# Health check per provider
curl http://your-api:6702/api/compute/healthThe frontend needs zero changes for multi-VPS. The compute pool is transparent - the API picks the best VPS and returns the gateway endpoint. Instances are tracked with their provider_id in the database.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
System health check |
| GET | /api/treasury |
Treasury wallet addresses |
| GET | /api/pricing |
Token price & instance cost |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/balance |
User's USD balance & instance count |
| POST | /api/deposits/verify |
Verify token deposit (chain, txHash) |
| POST | /api/instances/create |
Create new instance |
| GET | /api/instances |
List user's instances |
| GET | /api/instances/:id |
Get instance details |
| POST | /api/instances/:id/stop |
Stop an instance |
| POST | /api/instances/:id/status |
Check instance status |
| Method | Endpoint | Description |
|---|---|---|
| PATCH | /api/admin/users/:id/block |
Block/unblock user |
| PATCH | /api/admin/providers/:id/enable |
Enable/disable provider |
| GET | /api/compute/providers |
List compute providers |
| GET | /api/compute/capacity |
Pool capacity info |
| GET | /api/compute/health |
Provider health status |
The dashboard (apps/app) connects to the API using the APIClient class in apps/app/lib/api.ts.
// apps/web/components/ConnectWallet.tsx
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
const walletAddress = accounts[0];
apiClient.setToken(walletAddress); // Stored in localStorage as authToken// apps/app/lib/api.ts
const result = await apiClient.createInstance({
activator: "general",
channel: "telegram",
channelConfig: { telegramBotToken: "123456:ABC..." },
selectedModels: ["openrouter/auto"],
});
// result: { instanceId, status, gatewayEndpoint, ... }const instances = await apiClient.getInstances();
// Polls every few seconds for live status updatesconst result = await apiClient.verifyDeposit("base", "0xTxHash...");
// result: { balanceUsd, instances, creditedUsd, ... }Run the setup script on a fresh Ubuntu 22.04/24.04 VPS:
sudo bash setup-vps.shThis installs: Docker, Node.js 22, pnpm, PM2, PostgreSQL, and configures the firewall.
See setup-vps.sh in the repo root for the full script.
sudo bash setup-vps.shcd /opt/spawnclaw
git clone <your-repo-url> .
pnpm installcd /opt/spawnclaw/docker-runtime
docker build -t spawnclaw-runtime .
# Takes ~7 min first time (OpenClaw install), fast after thatThe setup script creates template files. Update them with real values:
# Edit the secrets
nano /opt/spawnclaw/.env.spawn-agent
nano /opt/spawnclaw/.env.api
# Copy to service directories
cp /opt/spawnclaw/.env.spawn-agent /opt/spawnclaw/spawn-agent/.env
cp /opt/spawnclaw/.env.api /opt/spawnclaw/apps/api/.envCRITICAL: Make sure SPAWNCLAW_SECRET matches in both .env.spawn-agent and .env.api (SPAWN_AGENT_SECRET).
cd /opt/spawnclaw/apps/api
npx drizzle-kit pushcd /opt/spawnclaw/spawn-agent
npm installcd /opt/spawnclaw/spawn-agent && pm2 start index.js --name spawn-agent
cd /opt/spawnclaw/apps/api && pm2 start index.js --name spawnclaw-api
pm2 save
pm2 startup # auto-start on rebootcurl http://localhost:4000/health
# {"status":"ok","timestamp":"..."}
curl http://localhost:6702/health
# {"status":"ok","timestamp":"...","database":{"count":0},"computePool":{...}}The frontend has two apps: Landing Page (apps/web) and Dashboard (apps/app).
Both are Next.js apps, so Vercel is the easiest option.
Landing Page (apps/web):
cd apps/webVercel env variables:
NEXT_PUBLIC_DASHBOARD_URL=https://app.yourdomain.com
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-dynamic-id # if using Dynamic.xyz for walletDashboard (apps/app):
cd apps/appVercel env variables:
NEXT_PUBLIC_API_URL=https://api.yourdomain.com # Your VPS API URL
NEXT_PUBLIC_LANDING_URL=https://yourdomain.com # Landing page URL
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-dynamic-id # if using Dynamic.xyzDeploy each app separately on Vercel:
# From repo root
cd apps/web && vercel --prod
cd apps/app && vercel --prodOr connect the repo to Vercel and set the root directory to apps/web or apps/app.
Build and serve with PM2:
# Landing page
cd /opt/spawnclaw/apps/web
cp .env.example .env.local
# Edit .env.local with production URLs
npm run build
pm2 start npm --name spawnclaw-web -- start # Serves on port 6700
# Dashboard
cd /opt/spawnclaw/apps/app
cp .env.example .env.local
# Edit .env.local with production URLs
npm run build
pm2 start npm --name spawnclaw-app -- start # Serves on port 6701If self-hosting, put Nginx in front for SSL:
server {
listen 443 ssl;
server_name yourdomain.com;
# SSL certs (use certbot)
location / {
proxy_pass http://127.0.0.1:6700;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 443 ssl;
server_name app.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:6701;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 443 ssl;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:6702;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Then install SSL:
apt install nginx certbot python3-certbot-nginx -y
certbot --nginx -d yourdomain.com -d app.yourdomain.com -d api.yourdomain.com| Variable | Required | Default | Description |
|---|---|---|---|
SPAWNCLAW_SECRET |
Yes | - | Shared secret with API (must match SPAWN_AGENT_SECRET) |
PORT |
No | 4000 |
HTTP port for spawn agent API |
GATEWAY_PORT_START |
No | 18790 |
First port in gateway range |
GATEWAY_PORT_END |
No | 18850 |
Last port in gateway range |
| Variable | Required | Default | Description |
|---|---|---|---|
| Core | |||
PORT |
No | 6702 |
HTTP port for API server |
NODE_ENV |
No | development |
development or production |
SPAWN_AGENT_URL |
No | http://localhost:4000 |
URL to spawn agent |
SPAWN_AGENT_SECRET |
Yes | - | Shared secret (must match SPAWNCLAW_SECRET) |
DATABASE_URL |
Yes | - | PostgreSQL connection string |
ADMIN_SECRET |
No | - | Secret for admin endpoints |
| OpenRouter | |||
OPENROUTER_API_KEY |
Yes* | - | Single OpenRouter API key |
OPENROUTER_API_KEYS |
Yes* | - | Comma-separated keys for rotation (overrides single key) |
OPENROUTER_DEFAULT_MODEL |
No | openrouter/auto |
Default AI model |
| Instance Resources | |||
INSTANCE_MEMORY |
No | 1g |
RAM per container (min 1g for OpenClaw) |
INSTANCE_CPUS |
No | 1 |
CPU cores per container |
| Token & Billing | |||
INSTANCE_COST_USD |
No | 15 |
USD cost per instance |
TREASURY_BASE |
No | - | Base chain treasury wallet(s), comma-separated |
TREASURY_SOLANA |
No | - | Solana treasury wallet(s), comma-separated |
TOKEN_ADDRESS_BASE |
No | - | $SPAWNCLAW contract on Base |
TOKEN_ADDRESS_SOLANA |
No | - | $SPAWNCLAW mint on Solana |
TOKEN_DECIMALS_BASE |
No | 18 |
Token decimals (Base) |
TOKEN_DECIMALS_SOLANA |
No | 6 |
Token decimals (Solana) |
BASE_RPC_URL |
No | https://mainnet.base.org |
Base chain RPC |
SOLANA_RPC_URL |
No | https://api.mainnet-beta.solana.com |
Solana RPC |
| Docker Provider | |||
DOCKER_PROVIDER_ENABLED |
No | true |
Enable Docker provider |
DOCKER_PROVIDER_PRIORITY |
No | 5 |
Scheduling priority |
DOCKER_PROVIDER_MAX_INSTANCES |
No | 50 |
Max instances this provider |
DOCKER_PROVIDER_REGION |
No | default |
Region label |
DOCKER_PROVIDER_COST |
No | 0.01 |
Cost per hour (internal tracking) |
SCHEDULER_STRATEGY |
No | priority_first |
priority_first, capacity_balanced, cost_optimized, round_robin, random |
*One of OPENROUTER_API_KEY or OPENROUTER_API_KEYS is required.
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_DASHBOARD_URL |
No | http://localhost:6701 |
Dashboard URL (redirect after wallet connect) |
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID |
No | - | Dynamic.xyz environment ID (if using Dynamic wallet) |
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_API_URL |
Yes | http://localhost:6702 |
Control Plane API URL |
NEXT_PUBLIC_LANDING_URL |
No | http://localhost:6700 |
Landing page URL |
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID |
No | - | Dynamic.xyz environment ID |
| Service | Port | Notes |
|---|---|---|
Landing Page (apps/web) |
6700 | Next.js |
Dashboard (apps/app) |
6701 | Next.js |
Control Plane API (apps/api) |
6702 | Express |
Spawn Agent (spawn-agent) |
4000 | Express (internal only) |
| Gateway range | 18790-18850 | One per OpenClaw instance |
# 1. Health check
curl http://your-vps:6702/health
# 2. Create instance via curl (use any wallet address for auth)
curl -X POST http://your-vps:6702/api/instances/create \
-H "Authorization: Bearer 0xYourWalletAddress" \
-H "Content-Type: application/json" \
-d '{
"activator": "general",
"channel": "telegram",
"channelConfig": { "telegramBotToken": "YOUR_BOT_TOKEN" },
"model": "openrouter/auto"
}'
# 3. List instances
curl http://your-vps:6702/api/instances \
-H "Authorization: Bearer 0xYourWalletAddress"
# 4. Check container is running
docker ps
# 5. Check container logs
docker logs <instanceId>| Issue | Fix |
|---|---|
| Container OOM during onboard | Increase INSTANCE_MEMORY to 2g |
--model flag not found |
Don't pass model to openclaw onboard, only to gateway config |
| Gateway bind error | Use --bind lan (not 0.0.0.0) |
| OpenClaw not found in container | Rebuild image: docker build -t spawnclaw-runtime . |
| Port already in use | Check docker ps for orphaned containers, stop them |
| 4+ min container startup | Rebuild Docker image (OpenClaw should be pre-installed) |
set -e exits on onboard |
Already fixed with ` |
| API returns "User not found" | User must deposit first, or use ensureUserByWallet endpoint |
| "Insufficient balance" on create | Deposit $SPAWNCLAW tokens worth at least $15 USD |
| Spawn Agent unreachable | Check firewall (port 4000), verify SPAWN_AGENT_URL matches |
| Database connection error | Verify DATABASE_URL, check PostgreSQL is running |
| Frontend can't reach API | Check NEXT_PUBLIC_API_URL, add CORS if needed |