Complete Digital Ocean VPS provisioning and OpenClaw bot orchestration service.
This repo supports:
- Standalone microservice (
claw-spawn-server) - Embedding into a larger Axum server via
claw_spawn::server::router(...)
Just run make and you're ready to go:
# Clone the repository
git clone https://github.com/conorholds/claw-spawn.git
cd claw-spawn
# Run everything (creates .env, sets up DB, runs migrations, builds, and starts server)
makeThat's it! The server will start on http://localhost:8080
Then edit .env and add your DigitalOcean API token:
# Edit the .env file
CLAW_DIGITALOCEAN_TOKEN=your_actual_token_hereThe Makefile provides easy commands for development:
| Command | Description |
|---|---|
make |
Full setup and start (default) |
make dev |
Quick dev mode with hot reload |
make setup |
Initial environment setup only |
make db |
Create database |
make migrate |
Run database migrations |
make build |
Build release binary |
make run |
Start the server |
make test |
Run all tests |
make clean |
Clean build artifacts |
make docker-run |
Run with Docker Compose |
make help |
Show all available commands |
- Rust/Cargo: https://rustup.rs/
- PostgreSQL:
brew install postgresql(macOS) orapt-get install postgresql - sqlx-cli:
cargo install sqlx-cli
export CLAW_DATABASE_URL="postgres://user:password@localhost/claw_spawn"
export CLAW_DIGITALOCEAN_TOKEN="your_digitalocean_api_token"
export CLAW_ENCRYPTION_KEY="$(openssl rand -base64 32)"
export CLAW_API_BEARER_TOKEN="$(openssl rand -base64 32)"# Create database
createdb claw_spawn
# Run migrations
sqlx migrate run# Build release binary
cargo build --release --bin claw-spawn-server
# Start server
./target/release/claw-spawn-server# Required for docker-compose.yml (no insecure default is baked in)
export POSTGRES_PASSWORD="$(openssl rand -base64 24)"
# Start with Docker Compose (includes PostgreSQL)
make docker-run
# Or manually:
docker-compose up --buildThe Docker setup includes:
- PostgreSQL database (auto-created)
- Automatic migrations on startup
- Server exposed on port 8080
- PostgreSQL is not published to the host by default (service-only network access)
If you need host DB access for local tooling, add this to the postgres service in docker-compose.yml:
ports:
- "127.0.0.1:5432:5432"| Variable | Required | Default | Description |
|---|---|---|---|
CLAW_DATABASE_URL |
Yes | - | PostgreSQL connection string |
CLAW_DIGITALOCEAN_TOKEN |
Yes | - | DigitalOcean API token |
CLAW_ENCRYPTION_KEY |
Yes | - | Base64-encoded 32-byte key |
CLAW_API_BEARER_TOKEN |
Yes | - | Bearer token required for privileged /accounts and /bots routes |
CLAW_SERVER_HOST |
No | 0.0.0.0 |
Server bind address |
CLAW_SERVER_PORT |
No | 8080 |
Server port |
CLAW_OPENCLAW_IMAGE |
No | ubuntu-22-04-x64 |
DO droplet image |
CLAW_CONTROL_PLANE_URL |
No | https://api.example.com |
Bot control-plane base URL |
CLAW_CUSTOMIZER_REPO_URL |
No | https://github.com/janebot2026/janebot-cli.git |
Public git repo for workspace customizer |
CLAW_CUSTOMIZER_REF |
No | pinned SHA | Git ref (tag/branch/SHA) to checkout for reproducible bootstrap |
CLAW_CUSTOMIZER_WORKSPACE_DIR |
No | /opt/openclaw/workspace |
Workspace directory on droplet |
CLAW_CUSTOMIZER_AGENT_NAME |
No | Jane |
Agent name passed to customizer |
CLAW_CUSTOMIZER_OWNER_NAME |
No | Cedros |
Owner name passed to customizer |
CLAW_CUSTOMIZER_SKIP_QMD |
No | true |
Skip QMD install at droplet bootstrap |
CLAW_CUSTOMIZER_SKIP_CRON |
No | true |
Skip OpenClaw cron install at droplet bootstrap |
CLAW_CUSTOMIZER_SKIP_GIT |
No | true |
Skip git init at droplet bootstrap |
CLAW_CUSTOMIZER_SKIP_HEARTBEAT |
No | true |
Skip heartbeat install at droplet bootstrap |
CLAW_TOOLCHAIN_NODE_MAJOR |
No | 20 |
Node major version installed via NodeSource on droplet (18, 20, etc.) |
CLAW_TOOLCHAIN_INSTALL_PNPM |
No | true |
Install pnpm via corepack on droplet |
CLAW_TOOLCHAIN_PNPM_VERSION |
No | empty | Optional pinned pnpm version (example: 9.12.0) |
CLAW_TOOLCHAIN_INSTALL_RUST |
No | true |
Install Rust (rustup) for openclaw user on droplet |
CLAW_TOOLCHAIN_RUST_TOOLCHAIN |
No | stable |
Rust toolchain channel/version passed to rustup |
CLAW_TOOLCHAIN_EXTRA_APT_PACKAGES |
No | empty | Space-separated extra apt packages to install during bootstrap |
CLAW_TOOLCHAIN_GLOBAL_NPM_PACKAGES |
No | empty | Space-separated global npm packages to install during bootstrap |
CLAW_TOOLCHAIN_CARGO_CRATES |
No | empty | Space-separated cargo crates to install for openclaw user |
- The droplet must be able to reach
CLAW_CONTROL_PLANE_URLover HTTPS. If you runclaw-spawnlocally, you typically need a tunnel (ngrok/cloudflared) until you have a live URL. - Workspace customization (janebot-cli) runs once and writes:
- Marker:
/opt/openclaw/.customizer_ran - Status:
/opt/openclaw/customizer_status.txt
- Marker:
- Droplet bootstrap installs Node (default 20),
pnpm, and Rust by default; useCLAW_TOOLCHAIN_*env vars to customize per deployment.
claw-spawn supports a dual architecture:
- Standalone microservice via the
claw-spawn-serverbinary - Embedded router via
claw_spawn::server::router(...)
Example (host app nests this service under /spawn):
use axum::Router;
use claw_spawn::infrastructure::AppConfig;
use claw_spawn::server::{build_state_with_pool, router};
use sqlx::PgPool;
let cfg = AppConfig::from_env()?;
let pool = PgPool::connect(&cfg.database_url).await?;
let state = build_state_with_pool(cfg, pool, /* run_migrations */ true).await?;
let app = Router::new().nest("/spawn", router(state));Add to Cargo.toml:
[dependencies]
# Core library only (no Axum server/router)
claw-spawn = { version = "0.1", default-features = false }Or, if you want the embeddable HTTP server/router:
[dependencies]
claw-spawn = { version = "0.1", features = ["server"] }curl -X POST http://localhost:8080/bots \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $CLAW_API_BEARER_TOKEN" \
-d '{
"account_id": "123e4567-e89b-12d3-a456-426614174000",
"name": "My First Bot",
"persona": "beginner",
"asset_focus": "majors",
"algorithm": "trend",
"strictness": "medium",
"paper_mode": true,
"max_position_size_pct": 10.0,
"max_daily_loss_pct": 5.0,
"max_drawdown_pct": 15.0,
"max_trades_per_day": 20,
"llm_provider": "openai",
"llm_api_key": "sk-your-openai-key-here"
}'Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"account_id": "123e4567-e89b-12d3-a456-426614174000",
"name": "My First Bot",
"persona": "beginner",
"status": "provisioning",
"droplet_id": 123456789,
"created_at": "2024-01-15T10:30:00Z"
}curl -H "Authorization: Bearer $CLAW_API_BEARER_TOKEN" http://localhost:8080/bots/{bot_id}# Pause
curl -X POST http://localhost:8080/bots/{bot_id}/actions \
-H "Authorization: Bearer $CLAW_API_BEARER_TOKEN" \
-d '{"action": "pause"}'
# Resume
curl -X POST http://localhost:8080/bots/{bot_id}/actions \
-H "Authorization: Bearer $CLAW_API_BEARER_TOKEN" \
-d '{"action": "resume"}'
# Destroy
curl -X POST http://localhost:8080/bots/{bot_id}/actions \
-H "Authorization: Bearer $CLAW_API_BEARER_TOKEN" \
-d '{"action": "destroy"}'Require header: Authorization: Bearer $CLAW_API_BEARER_TOKEN
POST /bots- Create botGET /bots/:id- Get bot detailsGET /accounts/:id/bots- List account botsPOST /bots/:id/actions- pause/resume/redeploy/destroy
GET /bot/:id/config- Pull configPOST /bot/:id/config_ack- Acknowledge configPOST /bot/:id/heartbeat- Health checkPOST /bot/register- Initial registration
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β claw-spawn β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Axum HTTP Server β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β β β App API β β Bot API β β Health ββ β
β β β (/bots/*) β β (/bot/*) β β (/health) ββ β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Application Layer β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β βProvisioning β β BotLifecycle β β
β β β Service β β Service β β
β β ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Infrastructure Layer β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββ β
β β βDigitalOcean β βPostgreSQL β β Crypto ββ β
β β β Client β βRepositories β β(AES-256) ββ β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β DigitalOcean VPS β
β βββββββββββββββββββ β
β β OpenClaw Bot β β
β β Agent β β
β βββββββββββββββββββ β
βββββββββββββββββββββββββ
- AES-256-GCM encryption for all secrets (LLM API keys)
- Per-bot registration tokens for authentication
- Firewall rules on droplets (default deny inbound)
- No secrets in logs - all sensitive data redacted
- Setup: See Super Quick Start and Manual Setup
- API Reference: See API Usage Examples
- Architecture: See Architecture section
# Quick dev cycle
make dev
# Run tests
make test
# Check code
make check
# Format code
make fmt
# Lint
make lint# Create migration
make migrate-add
# Run migrations
make migrate
# Check status
make migrate-status
# Revert last
make migrate-revertmake install-sqlx# Check PostgreSQL is running
make db# Edit .env and change CLAW_SERVER_PORT
CLAW_SERVER_PORT=8081 make runUse as a library in your Rust project:
use claw_spawn::{
application::{ProvisioningService, BotLifecycleService},
domain::{Account, BotConfig, Persona, SubscriptionTier},
infrastructure::{AppConfig, DigitalOceanClient, PostgresAccountRepository},
};
// See README.md for full exampleMIT