DEV Community

Cover image for Two-Container AI Wallet Setup: WAIaaS + Push-Relay Production Architecture
Wallet Guy
Wallet Guy

Posted on

Two-Container AI Wallet Setup: WAIaaS + Push-Relay Production Architecture

Two-Container AI Wallet Setup: WAIaaS + Push-Relay Production Architecture

Would you trust a third party with your AI agent's private keys? If you're building autonomous agents that move real money — swapping tokens, paying for API calls, managing DeFi positions — that question deserves a serious answer. WAIaaS is an open-source, self-hosted Wallet-as-a-Service that puts the answer firmly in your hands: your keys, your server, your rules.

Why Self-Hosting Your Agent's Wallet Actually Matters

There's a philosophical divide in crypto infrastructure that mirrors the one in email: you can use Gmail, or you can run your own mail server. Most people choose convenience. A smaller group chooses control — and for good reason.

When an AI agent has signing authority over a wallet, the custody question becomes acutely important. A hosted wallet service means a third party holds the keys, enforces their own rate limits, can inspect your transaction patterns, and can cut off your agent's access at any time. For hobbyists building toy projects, that's probably fine. For anyone running agents with meaningful funds, or anyone who simply values sovereignty over their infrastructure, self-hosting is the natural conclusion.

The good news is that WAIaaS makes this genuinely practical. Unlike running your own email server — which is famously painful — a production WAIaaS deployment is two Docker containers and a docker compose up -d.

The Two-Container Architecture

WAIaaS ships two Docker images (fact DOCKER-05): the main daemon and a push-relay. Understanding why both exist is the key to understanding the production architecture.

The daemon (waiaas/daemon:latest) is the core of the system. It exposes the REST API on port 3100, manages wallets and sessions, runs the 7-stage transaction pipeline, enforces policies, and handles all signing operations. This is the container your AI agents talk to.

The push-relay is a lightweight relay service that handles real-time notifications — specifically, it's what allows your phone to receive push notifications when an AI agent submits a transaction that requires your approval. Without it, you'd have to poll for pending transactions manually. With it, your phone buzzes the moment your agent tries to do something that needs a human decision.

Together they give you:

  • Full local custody of private keys (the daemon never phones home)
  • Real-time approval workflows via push notifications
  • A clean separation of concerns between signing logic and notification delivery

What's Actually Running in Each Container

The daemon is the heavy lifter. It's a 15-package monorepo (PKG-01) condensed into a single container. It provides:

  • 39 REST API route modules (API-01) covering wallets, sessions, transactions, DeFi actions, NFTs, and more
  • 45 MCP tools (MCP-01) for direct Claude Desktop or other AI framework integration
  • 15 DeFi protocol integrations (DEFI-01) including Jupiter, Aave v3, Hyperliquid, Lido, Jito, and others (DEFI-02)
  • A policy engine with 21 policy types (POLICY-01) and 4 security tiers (POLICY-02)
  • 2 chain types across 18 networks (NET-01)
  • 3 signing channels (SIGN-01): push-relay, Telegram, and wallet notification

The default port binding is 127.0.0.1:3100:3100 (DOCKER-02), which means by default the daemon only listens on localhost. That's intentional — you don't want your signing API accidentally exposed to the internet.

The push-relay handles the notification side of things. When the daemon needs to send you a push notification (say, an AI agent just tried to move $2,000 and your SPENDING_LIMIT policy requires your approval), the push-relay is the bridge that gets that alert to your device without requiring the daemon itself to be reachable from the public internet.

Setting Up the Production Stack

Let's walk through a real production deployment. The full docker-compose.yml looks like 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
Enter fullscreen mode Exit fullscreen mode

Notice a few things about this config that matter for production:

  1. Port binding to 127.0.0.1 — The daemon isn't reachable from outside the host. If you need external access, put a reverse proxy (nginx, Caddy) in front of it.
  2. Named volume waiaas-data — Your wallet data and keys persist in a named volume. Running docker compose down won't delete them; you'd need docker compose down -v to wipe the data.
  3. Healthcheck — Docker will restart the container if the health endpoint stops responding.

Handling Production Secrets Properly

The default setup is fine for local development, but production deserves better than environment variables in a .env file. WAIaaS ships a Docker Secrets overlay (DOCKER-04) specifically for 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
Enter fullscreen mode Exit fullscreen mode

The docker-compose.secrets.yml overlay wires Docker Secrets into the container without the secret values ever appearing in environment variables or docker inspect output. For a self-hoster who's serious about security, this is the right way to do it.

Auto-Provision: First Boot Without a Password Prompt

If you're deploying to a headless server and don't want to manually set a master password on first boot, the WAIAAS_AUTO_PROVISION flag handles it:

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
Enter fullscreen mode Exit fullscreen mode

The daemon generates a random master password on first start, writes it to /data/recovery.key, and proceeds without waiting for human input (DOCKER-03). This is useful for automated deployments. After you've retrieved the recovery key and stored it somewhere safe, you can harden the password later with the CLI's set-master command.

The Three Authentication Layers

Once the stack is running, understanding the three auth layers helps you understand who can do what:

# masterAuth — system administrator (wallet creation, session management, policies)
-H "X-Master-Password: my-secret-password"

# sessionAuth — AI agent (transactions, balance queries, DeFi actions)
-H "Authorization: Bearer wai_sess_eyJhbGciOiJIUzI1NiJ9..."

# ownerAuth — fund owner (transaction approval, kill switch recovery)
-H "X-Owner-Signature: <ed25519-or-secp256k1-signature>"
-H "X-Owner-Message: <signed-message>"
Enter fullscreen mode Exit fullscreen mode

This three-layer model (SEC-02) maps cleanly to a production self-hosted setup:

  • You hold the master password (Argon2id hashed). It lives only on your server.
  • Your AI agents get session tokens (JWT HS256) with scoped permissions and TTLs.
  • Your phone or hardware wallet provides owner signatures for high-value approvals.

No third party is in this chain at any point.

Putting Policies Between Your Agent and Your Funds

The policy engine is what makes autonomous agents safe to run. With 21 policy types (POLICY-01) enforcing a default-deny model (SEC-03), you can be very precise about what your agent is allowed to do.

Here's a realistic policy for a trading agent — a spending limit with tiered security:

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
    }
  }'
Enter fullscreen mode Exit fullscreen mode

With this policy, transactions under $100 execute immediately. $100-$500 execute immediately but you get a push notification. $500-$2,000 are queued for 15 minutes — cancellable if you catch them. Over $2,000 requires your explicit approval (POLICY-02). The push-relay container is what makes that approval flow work in real-time.

If your agent tries to interact with a token you haven't whitelisted, it gets blocked entirely — that's the default-deny enforcement of ALLOWED_TOKENS at work (SEC-03).

Quick Start: Five Steps to a Running Stack

Step 1: Clone the repo

git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
Enter fullscreen mode Exit fullscreen mode

Step 2: Start the stack

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Step 3: Initialize via CLI

npm install -g @waiaas/cli
waiaas init
waiaas start
Enter fullscreen mode Exit fullscreen mode

Step 4: Create wallets and sessions in one command

waiaas quickset --mode mainnet
Enter fullscreen mode Exit fullscreen mode

Step 5: Connect Claude Desktop (or your agent framework)

waiaas mcp setup --all
Enter fullscreen mode Exit fullscreen mode

After step 5, paste the printed MCP config JSON into your Claude Desktop configuration. Claude immediately gains access to 45 MCP tools (MCP-01) — balance checks, token transfers, DeFi actions, NFT operations, and more — all routed through your local daemon, all subject to your local policies.

Verifying the Stack is Healthy

A few commands to confirm everything is running correctly:

docker compose logs -f          # Follow logs from both containers
docker compose ps               # Check container status
Enter fullscreen mode Exit fullscreen mode

And from outside Docker, hit the health endpoint directly:

curl http://127.0.0.1:3100/health
Enter fullscreen mode Exit fullscreen mode

The daemon also exposes an OpenAPI 3.0 spec and interactive docs (FEAT-OPENAPI):

# Download the full spec
curl http://127.0.0.1:3100/doc -o openapi.json

# Open interactive API reference in your browser
open http://127.0.0.1:3100/reference
Enter fullscreen mode Exit fullscreen mode

The interactive reference at /reference is useful for exploring all 39 API route modules (API-01) without reading raw JSON.

The Philosophy: Practical Sovereignty

Running your own email server is notoriously painful — SPF records, DKIM, deliverability issues, spam filtering. The comparison to self-hosting crypto infrastructure gets made often, and it's usually meant to scare people off. WAIaaS is designed to make that comparison unfair.

The Docker deployment handles auto-provisioning, Docker Secrets, health checks, runs as a non-root user (UID 1001), and supports watchtower auto-updates (FEAT-DOCKER). The CLI covers backup and restore (CLI-01), so your wallet data isn't one bad hard drive away from disappearing. The 684+ test files (TEST-01) across the codebase mean you're not beta-testing someone's weekend project.

The two-container architecture means you can update either container independently. The push-relay is lightweight and rarely changes. The daemon gets the DeFi protocol updates and security patches. You keep full control of the upgrade schedule.

For homelab enthusiasts and self-hosters who already run Nextcloud, Vaultwarden, or Immich — this fits naturally into that stack. It's one more service you control, one fewer third party with access to something important.

What's Next

To go deeper on the policy engine — particularly DeFi-specific policies like LENDING_LTV_LIMIT, PERP_MAX_LEVERAGE, and VENUE_WHITELIST — the policy configuration documentation in the repo covers every rule format for all 21 types. For connecting your AI agent framework of choice, the MCP package (@waiaas/mcp) and the TypeScript and Python SDKs (FEAT-SDK) give you clean programmatic access without writing raw HTTP calls.

The full source, Dockerfiles, and compose files are at https://github.com/waiaas/WAIaaS. If you want to understand the project before diving into the code, start at https://waiaas.ai. Star the repo if this is useful — it helps others find it.

Top comments (0)