Production Docker for 15-Package Monorepo: Microservices Architecture for Self-Hosted AI Wallets

Self-Hosted AI Wallets with Docker: Your Keys, Your Server, Your Rules

Would you trust a third party with your AI agent's private keys? If you're building autonomous agents that move real money across DeFi protocols, that's not a hypothetical question — it's the central architectural decision you need to make before you write a single line of agent code. WAIaaS (Wallet-as-a-Service for AI agents) is an open-source, self-hosted wallet daemon you can run on your own infrastructure, giving your AI agents full onchain capability while keeping custody entirely in your hands.

Why Self-Hosting an AI Wallet Actually Matters

The conversation around self-hosting usually starts and ends with privacy. That's valid — your wallet's signing keys, transaction history, and session tokens never leave your infrastructure. But for AI agent developers specifically, there are two more reasons that matter just as much.

Control over your own rate limits. Hosted wallet APIs impose quotas. When your agent needs to execute a batch of DeFi operations or monitor dozens of incoming transactions in real time, you don't want an external service throttling you at a critical moment.

Auditability. When an AI agent makes a financial decision, you need to be able to audit every step of that decision — from the policy check that allowed it, through the signing, to the confirmation on-chain. With a self-hosted daemon, every log line, every pipeline stage, every policy evaluation happens on hardware you control.

WAIaaS is a 15-package monorepo — actions, adapters, admin, cli, core, daemon, desktop-spike, e2e-tests, mcp, openclaw-plugin, push-relay, sdk, shared, skills, and wallet-sdk — and all of it runs behind a single Docker image. Let's dig into how that's structured and why it's built the way it is.

The Architecture: One Monorepo, Two Docker Images

The WAIaaS codebase ships two Docker images: the main waiaas daemon and a push-relay service. For most self-hosters, you'll start with the daemon, which handles everything from key management to DeFi execution to the REST API layer.

The daemon exposes 39 REST API route modules and runs on port 127.0.0.1:3100 by default — bound to localhost, not the public interface, which is the right default for a service that controls signing keys. The Docker Compose file reflects this:

services:
  daemon:
    image: ghcr.io/waiaas/waiaas:latest
    container_name: waiaas-daemon
    ports:
      - "127.0.0.1:3100:3100"
    volumes:
      - waiaas-data:/data
    environment:
      - WAIAAS_DATA_DIR=/data
      - WAIAAS_DAEMON_HOSTNAME=0.0.0.0
    env_file:
      - path: .env
        required: false
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3

volumes:
  waiaas-data:
    driver: local

A few things worth noting here. Data lives in a named volume (waiaas-data), which means docker compose down preserves your wallets and sessions — you need to explicitly pass -v to destroy the volume. The healthcheck pings /health every 30 seconds, so orchestrators like Watchtower (for auto-updates) or your own monitoring stack get a reliable liveness signal. The daemon runs as a non-root user (UID 1001), which is the right call for a service that's holding cryptographic material.

Getting Running in One Command

If you want the fastest possible path to a running self-hosted wallet daemon:

git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
docker compose up -d

That's genuinely it. For homelab setups or first-time evaluation, the auto-provision mode is even more convenient — it generates a master password on first start and writes it to a recovery file inside the container:

docker run -d \
  --name waiaas \
  -p 127.0.0.1:3100:3100 \
  -v waiaas-data:/data \
  -e WAIAAS_AUTO_PROVISION=true \
  ghcr.io/waiaas/waiaas:latest

# Retrieve auto-generated master password
docker exec waiaas cat /data/recovery.key

The auto-provision flag tells the entrypoint script to handle first-run initialization without requiring you to be present at the terminal to set a password. You retrieve the generated key from the container, save it somewhere safe, and then later harden it with the set-master CLI command.

Production Secrets: The Right Way

For homelab use, environment variables are fine. For anything that's actually in production — a VPS, a server, a dedicated machine running agents with real funds — you want Docker Secrets instead of plaintext environment variables.

The secrets overlay pattern WAIaaS ships with looks like this:

# Create secret files
mkdir -p secrets
echo "your-secure-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt

# Deploy with secrets overlay
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d

The docker-compose.secrets.yml overlay wires those secret files into the container without them appearing in environment variables, process lists, or docker inspect output. This is the difference between a homelab setup and something you'd actually trust with meaningful funds.

The Three-Layer Authentication Model

Once the daemon is running, it exposes three distinct authentication mechanisms, each scoped to a different principal:

masterAuth (Argon2id hashed password) — used for administrative operations: creating wallets, managing sessions, setting policies. This is you as the system operator.

sessionAuth (JWT HS256) — used by AI agents at runtime. Each agent gets a session token scoped to a specific wallet, with configurable TTL, max renewals, and absolute lifetime.

ownerAuth (SIWS/SIWE signature) — used by the fund owner for approval-tier transactions. This is the cryptographic proof that the human who controls the keys has approved a specific action.

Here's what that looks like in practice. You create a wallet as the operator:

curl -X POST http://127.0.0.1:3100/v1/wallets \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{"name": "trading-wallet", "chain": "solana", "environment": "mainnet"}'

Then you create a session token for the agent:

curl -X POST http://127.0.0.1:3100/v1/sessions \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{"walletId": "<wallet-uuid>"}'

The agent uses that session token for everything it does at runtime — balance checks, transfers, DeFi actions — without ever touching the master password.

Policy Engine: Default-Deny by Design

This is the part that matters most for self-hosters who are actually running agents with real funds. WAIaaS has a policy engine with 21 policy types and a default-deny posture: if you haven't explicitly whitelisted a token or a contract, transactions involving it are blocked.

The four security tiers map cleanly to your risk tolerance:

A realistic policy setup for an agent you actually trust but want guardrails on:

curl -X POST http://127.0.0.1:3100/v1/policies \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 100,
      "notify_max_usd": 500,
      "delay_max_usd": 2000,
      "delay_seconds": 900,
      "daily_limit_usd": 5000
    }
  }'

Under $100 USD equivalent: the agent acts immediately. $100-500: acts immediately but you get a notification. $500-2000: queued for 15 minutes, giving you a window to cancel. Over $2000: requires your explicit approval. No single policy does this alone — you can layer SPENDING_LIMIT with ALLOWED_TOKENS (whitelist which tokens can move at all), CONTRACT_WHITELIST (whitelist which contracts can be called), and RATE_LIMIT (cap transactions per hour).

The policy evaluation happens in stage 3 of a 7-stage transaction pipeline: validate → auth → policy → wait → execute → confirm. If policy denies a transaction, you get a structured error response:

{
  "error": {
    "code": "POLICY_DENIED",
    "message": "Transaction denied by SPENDING_LIMIT policy",
    "domain": "POLICY",
    "retryable": false
  }
}

MCP Integration: Connecting Claude to Your Self-Hosted Daemon

The Model Context Protocol integration is where self-hosting gets genuinely interesting for AI agent developers. WAIaaS ships an MCP server (@waiaas/mcp) with 45 tools covering wallet operations, transactions, DeFi, NFTs, and x402 payments. The daemon itself is what does the work — the MCP server is just the interface that exposes it to Claude Desktop or any other MCP-compatible agent framework.

Because you're self-hosting, the MCP server points at your own daemon, not some external service:

{
  "mcpServers": {
    "waiaas": {
      "command": "npx",
      "args": ["-y", "@waiaas/mcp"],
      "env": {
        "WAIAAS_BASE_URL": "http://127.0.0.1:3100",
        "WAIAAS_SESSION_TOKEN": "wai_sess_<your-token>",
        "WAIAAS_DATA_DIR": "~/.waiaas"
      }
    }
  }
}

The CLI can automate this setup entirely:

waiaas mcp setup --all    # Auto-register all wallets with Claude Desktop

You can also run multiple MCP server entries pointing at the same daemon but scoped to different wallets — one session token per agent, each with its own policy set.

Key Environment Variables for Self-Hosters

A few configuration knobs worth knowing about before you go to production:

WAIAAS_AUTO_PROVISION=true              # Auto-generate master password on first start
WAIAAS_DAEMON_PORT=3100                 # Listening port
WAIAAS_DAEMON_HOSTNAME=0.0.0.0         # Bind address
WAIAAS_DAEMON_LOG_LEVEL=info            # Log level (trace/debug/info/warn/error)
WAIAAS_DATA_DIR=/data                   # Data directory
WAIAAS_RPC_SOLANA_MAINNET=<url>         # Solana mainnet RPC endpoint
WAIAAS_RPC_EVM_ETHEREUM_MAINNET=<url>   # Ethereum mainnet RPC endpoint

The RPC endpoint variables are worth calling out specifically. By default the daemon uses public RPC endpoints. For production use, you'll want to point these at your own nodes or a private RPC service — both for reliability and to avoid leaking your wallet's query patterns to public infrastructure.

Quick Start: Five Steps to a Running Self-Hosted AI Wallet

Here's the minimal path from zero to a working setup:

Step 1: Clone and start

git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
docker compose up -d

Step 2: Initialize and configure

npm install -g @waiaas/cli
waiaas init
waiaas start

Step 3: Create wallets and sessions

waiaas quickset --mode mainnet

Step 4: Wire up Claude Desktop

waiaas mcp setup --all

Step 5: Add agent skills

npx @waiaas/skills add all

At this point you have a self-hosted wallet daemon running in Docker, wallets created, session tokens scoped per-agent, policies not yet configured (which means default-deny is protecting you), and Claude Desktop connected via MCP.

The Philosophy: Running Your Own Infrastructure

There's a reason r/selfhosted and r/homelab communities are skeptical of "just use our hosted API" solutions for anything security-sensitive. The crypto equivalent of running your own email server used to mean weeks of configuration and ongoing maintenance. With a containerized daemon that ships with sensible defaults, auto-provisioning, healthchecks, and a 20-command CLI, the operational burden is genuinely low.

Your AI agent's private keys never leave your machine. Your transaction history isn't being analyzed by a third party. Your RPC calls go where you tell them to go. The daemon has 684+ test files, runs as a non-root user, binds to localhost by default, and ships with a default-deny policy engine. The self-hosting story here is actually practical, not just ideological.

What's Next

The best place to start is the GitHub repository at https://github.com/waiaas/WAIaaS, where the Docker Compose files, entrypoint scripts, and full documentation live. The official site at https://waiaas.ai has guides covering the policy engine in more depth, MCP integration patterns, and the SDK reference for building agents programmatically. Once you have the daemon running, the interactive API reference at http://127.0.0.1:3100/reference (auto-generated from the OpenAPI 3.0 spec) is the fastest way to explore everything the daemon can do without reading source code.