684 Tests in a 15-Package Monorepo: Docker Testing Strategy for AI Wallets

Docker makes it practical to self-host your own AI wallet infrastructure — but how do you know the thing you're running is actually trustworthy? That's the question behind WAIaaS's testing strategy: a 15-package monorepo with 684+ test files, all validated before a single Docker image ships. If you're the kind of person who'd rather run your own server than hand your private keys to a third party, this post is for you.

The Problem With "Just Trust Us"

Would you trust a third party with your AI agent's private keys?

That question sounds rhetorical, but it's genuinely the fork in the road when you're building autonomous agents that move real money. Hosted wallet services are convenient — until they go down, rate-limit your agent at the worst possible moment, change their API, or simply have access to keys you'd rather keep on hardware you control.

The crypto-native answer has always been self-custody. The practical answer, when you're talking about automated agents executing DeFi trades at 3am, is that self-custody needs to come with serious infrastructure — proper authentication, policy guardrails, and enough test coverage that you can actually trust what you're deploying.

WAIaaS is an open-source, self-hosted Wallet-as-a-Service built for exactly this. And the 684+ test files spread across its 15-package monorepo are what make the self-hosting story believable rather than aspirational.

Why Monorepo Testing Is Hard (And Why It Matters Here)

A monorepo sounds like a convenience — everything in one repo, one place to look. But testing a monorepo is a different beast than testing a single application. WAIaaS ships as 15 packages:

PKG-01: actions, adapters, admin, cli, core, daemon, desktop-spike, e2e-tests, mcp, openclaw-plugin, push-relay, sdk, shared, skills, wallet-sdk

Each of those packages has its own concerns. The core package owns transaction schemas and policy enums. The daemon package owns the HTTP API and the pipeline that executes transactions. The mcp package owns the 45 tools that Claude and other LLMs call directly. The cli package owns the 20 commands you use to manage everything from the terminal.

When your transaction pipeline has 7 stages — validate, auth, policy, wait, execute, confirm — a bug in stage 3 (policy enforcement) that only surfaces after stage 5 (execution) is the kind of thing that costs real money in production. The test suite exists to catch that before the Docker image is built.

For self-hosters, this matters in a specific way: you're not filing a support ticket when something goes wrong. You're reading logs at midnight. A codebase with 684+ test files is a codebase where the authors have thought hard about failure modes before you encounter them.

The Architecture You're Actually Running

Before getting into Docker specifics, it helps to understand what's inside the image you're pulling.

WAIaaS's daemon is a REST API service with 39 route modules. When an AI agent wants to send a token, the request travels through a 7-stage pipeline: validation, authentication, policy enforcement, optional waiting (for time-delayed transactions), execution, and confirmation. Every stage is a discrete unit of work — which means every stage can be tested in isolation.

The policy engine alone has 21 policy types and 4 security tiers (INSTANT, NOTIFY, DELAY, APPROVAL). Default-deny enforcement means that if you haven't explicitly whitelisted a token or contract, the transaction doesn't go through. That's not a configuration option — it's the default. Testing that behavior correctly across 21 policy types is non-trivial, which is why it represents a significant slice of the test suite.

Authentication runs three separate paths: masterAuth (Argon2id, for system administration), ownerAuth (SIWE/SIWS signatures, for the fund owner), and sessionAuth (JWT HS256, for the AI agent itself). Each path has different trust levels and different attack surfaces. All three are tested independently.

Docker Deployment: What You're Actually Running

The self-hosting story starts here:

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

That's it for a development setup. The Docker image is waiaas/daemon:latest, binding by default to 127.0.0.1:3100 — localhost only, not exposed to the network unless you explicitly change it. That's a deliberate choice. Your wallet infrastructure shouldn't be internet-accessible by default.

For production, the auto-provision flow handles first-run secrets without you having to set anything manually:

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 WAIAAS_AUTO_PROVISION=true flag tells the entrypoint to generate a master password on first start and write it to /data/recovery.key. You retrieve it once, store it somewhere safe, then optionally harden it later with waiaas set-master. The data lives in a named Docker volume — it persists across container restarts and upgrades.

Two Docker images ship with WAIaaS: the main daemon and a push-relay service for notifications. The push-relay is what delivers approval requests to your phone when a transaction hits the APPROVAL tier.

Production Secrets Without the Footguns

The Compose file includes a secrets overlay specifically for production:

mkdir -p secrets echo "your-secure-password" > secrets/master_password.txt chmod 600 secrets/master_password.txt docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d 

This uses Docker Secrets rather than environment variables — secrets don't appear in docker inspect output or get logged accidentally. The entrypoint.sh reads them from the filesystem path that Docker Secrets mounts at /run/secrets/.

The full Compose configuration includes a healthcheck out of the box:

healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3100/health"] interval: 30s timeout: 5s start_period: 10s retries: 3 

Your container orchestrator — whether that's Portainer, a plain systemd unit watching Docker, or a full Kubernetes setup — gets a real health signal rather than just "container is running."

From Docker to Working Wallet in Five Minutes

Once the daemon is running, the CLI handles the rest:

npm install -g @waiaas/cli waiaas init # Create data directory + config.toml waiaas start # Start daemon waiaas quickset --mode mainnet # Create wallets + MCP sessions in one step 

quickset is the shortcut that creates wallets, generates session tokens for your AI agent, and prints out the MCP configuration JSON you paste into Claude Desktop's config file. The waiaas mcp setup --all command handles the Claude Desktop registration automatically if you'd rather not edit JSON by hand.

The result is a session token — a wai_sess_ JWT — that your agent uses for all subsequent operations. The agent never sees the master password. It never has permission to create new wallets or change policies. It can only do what the session allows, constrained by whatever policies you've configured.

What the Agent Can Actually Do

With a session token, the agent gets access to 45 MCP tools covering wallet operations, transactions, DeFi positions, NFTs, and x402 payments. Here's what that looks like from the SDK side if you're building a custom integration rather than using Claude:

import { WAIaaSClient } from '@waiaas/sdk'; const client = new WAIaaSClient({ baseUrl: 'http://127.0.0.1:3100', sessionToken: process.env.WAIAAS_SESSION_TOKEN, }); // Check balance const balance = await client.getBalance(); console.log(`${balance.balance} ${balance.symbol}`); // Send native token const tx = await client.sendToken({ to: 'recipient-address...', amount: '0.1', }); console.log(`Transaction: ${tx.id}`); 

The Python SDK covers the same operations with async/await:

from waiaas import WAIaaSClient async with WAIaaSClient("http://localhost:3100", "wai_sess_xxx") as client: balance = await client.get_balance() print(balance.balance, balance.symbol) 

Before your agent sends anything, you can simulate it:

curl -X POST http://127.0.0.1:3100/v1/transactions/send \ -H "Content-Type: application/json" \ -H "Authorization: Bearer wai_sess_<token>" \ -d '{ "type": "TRANSFER", "to": "recipient-address", "amount": "0.1", "dryRun": true }' 

The dryRun: true flag runs the full 7-stage pipeline — including policy checks — without broadcasting to the network. If a policy would block the transaction, you find out before it matters.

Policy: The Part That Actually Protects You

The default-deny policy engine is what separates "AI agent with a wallet" from "AI agent that can drain your wallet." A spending limit policy, for example, creates a 4-tier response based on transaction amount:

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: executes immediately. $100-500: executes with a notification to you. $500-2000: queued for 15 minutes, cancellable. Over $2000: requires your explicit approval via WalletConnect or Telegram before it moves.

All 21 policy types — including DeFi-specific ones like PERP_MAX_LEVERAGE, LENDING_LTV_LIMIT, and X402_ALLOWED_DOMAINS — are tested in the suite before they ship. When you're running this on your own hardware, that coverage is your safety net.

When a policy blocks something, the error is explicit:

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

No ambiguity about why a transaction didn't go through.

The Self-Hosting Philosophy

Running WAIaaS is the crypto equivalent of running your own email server — except the tooling has gotten good enough that it's actually practical. Your keys never leave your infrastructure. No third-party service sees your transaction flow. No rate limits imposed by someone else's API plan. No custody risk from a company that might get acquired, go down, or change their terms.

The 15-package monorepo with 684+ test files is the engineering answer to the question: "how do I know this is safe to run?" The Docker image with auto-provision, Docker Secrets support, healthchecks, and a non-root process (UID 1001) is the operational answer to: "how do I actually run this in production without it being a full-time job?"

The CLI's 20 commands — including backup create, backup list, and restore — mean your wallet data survives hardware failures. The status, stop, and update commands mean you can manage the daemon lifecycle without touching Docker directly.

Quick Start Checklist

If you want to go from zero to running agent in one sitting:

  1. Install the CLI: npm install -g @waiaas/cli
  2. Initialize and start: waiaas init && waiaas start
  3. Create wallets and sessions: waiaas quickset --mode mainnet
  4. Configure Claude Desktop: waiaas mcp setup --all
  5. Set spending limits: Create a SPENDING_LIMIT policy via the REST API or Admin UI at /admin

The OpenAPI spec at /doc and the interactive reference UI at /reference are available as soon as the daemon is running, so you don't need to read source code to explore what the API can do.

What's Next

The codebase, Dockerfiles, and full documentation are at https://github.com/waiaas/WAIaaS. The official site with additional guides is at https://waiaas.ai. If you're already running a homelab or self-hosted stack, WAIaaS fits into that ecosystem without requiring any external services — bring your own RPC endpoints via WAIAAS_RPC_SOLANA_MAINNET and WAIAAS_RPC_EVM_ETHEREUM_MAINNET, and the whole stack runs air-gapped from third-party infrastructure.