CLI Session Hardening: TTL, Renewals, and Emergency Disconnection for Self-Hosted Wallets
CLI Session Hardening: TTL, Renewals, and Emergency Disconnection for Self-Hosted Wallets
Would you trust a third party with your AI agent's private keys? If you're running autonomous agents that move real money, the answer shapes everything about how you architect your setup. Self-hosted wallet infrastructure gives you something no hosted service can: the ability to define, inspect, and enforce exactly how long an AI agent's access lasts — and to kill that access instantly when something goes wrong.
Why Session Hygiene Matters When Agents Hold Value
There's a meaningful difference between an AI agent that can access a wallet and one that should — and that difference is time.
Hosted wallet services abstract this away, which is convenient until it isn't. If an agent session token is long-lived and you don't control the server, a compromised token is someone else's problem to revoke. When you self-host, the session lifecycle is entirely yours. You decide when a token expires, whether it can be renewed, and how hard the ceiling is. And when you need to pull the plug — because the agent behaved unexpectedly, or you just want to sleep soundly — you can do it from your own terminal, against your own server, with no support ticket required.
WAIaaS (Wallet-as-a-Service for AI agents) is an open-source, self-hosted daemon built around this philosophy. Your keys live on your server. Your policies run on your server. Your session rules are enforced on your server.
The Three Auth Layers You're Actually Working With
Before diving into session hardening specifically, it helps to understand the three authentication roles in WAIaaS, because they serve different principals with different levels of trust.
masterAuth is the system administrator credential, used with X-Master-Password. It can create wallets, manage sessions, and set policies. This is the credential you use at setup time and keep locked away.
sessionAuth is what your AI agent carries around — a JWT token prefixed wai_sess_. This is the credential that actually moves money. It's scoped to a single wallet and constrained by whatever policies and TTL you configure.
ownerAuth is for the human fund owner — the person who approves high-value transactions or issues a kill switch. It uses cryptographic signatures (Ed25519 or secp256k1) rather than a shared password.
The session layer is where most of your day-to-day hardening happens.
Creating a Session with Explicit Lifetime
Creating a session is a single API call with masterAuth:
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>"}'
WAIaaS supports per-session TTL, maxRenewals, and absoluteLifetime configuration. This means you can create a session that:
- Expires after a short window (TTL)
- Can be renewed by the agent up to N times (
maxRenewals) - Has a hard ceiling it can never cross (
absoluteLifetime), regardless of renewals
The practical effect: an agent can keep itself alive during a long trading run by renewing its own session, but it cannot extend that access indefinitely. When absoluteLifetime hits, the session is dead and the agent must come back to a human (or an automated provisioning system) for a new one.
This is the crypto equivalent of issuing a contractor a keycard that works for 8 hours, auto-renews if they badge in before expiry, but physically deactivates at midnight no matter what.
The CLI Owns Your Session Workflow
WAIaaS ships a CLI with 20 commands. The session-relevant ones are what you'll reach for most:
npm install -g @waiaas/cli
waiaas init # Initialize config directory
waiaas start # Start the daemon
waiaas quickset # Create wallets + sessions in one step
For day-to-day operations:
waiaas session prompt # Interactive session management
waiaas status # Check daemon status
waiaas owner status # Check owner connection state
waiaas owner disconnect # Disconnect the owner approval channel
The waiaas quickset command is worth calling out specifically. If you want wallets created and MCP session tokens generated in a single step — without manually stitching together API calls — quickset handles that. For self-hosters who want to go from "fresh Docker volume" to "agent has a working token" in under two minutes, this is the path.
Emergency Disconnection: The Kill Switch
Here's the scenario: your agent starts behaving oddly — maybe it's submitting transactions you didn't expect, or your logs show something you don't understand. You want to stop it now, not after you've found the right API endpoint in documentation at 2am.
WAIaaS's security architecture includes three layers. The first is session auth — time-limited tokens that constrain normal operation. The second is time delay plus approval — transactions above your configured threshold get queued and require human sign-off before execution. The third is monitoring and kill switch.
The kill switch is operated via ownerAuth. If you have WalletConnect connected (via waiaas owner connect), you can approve or reject pending transactions from your phone. More relevantly here: you can disconnect and revoke access through the owner channel entirely.
waiaas owner disconnect # Disconnect owner approval channel
For pending transactions that already hit the APPROVAL tier, the ownerAuth endpoint lets you reject them:
curl -X POST http://127.0.0.1:3100/v1/transactions/<tx-id>/approve \
-H "X-Owner-Signature: <ed25519-or-secp256k1-signature>" \
-H "X-Owner-Message: <signed-message>"
Rejection works the same way — the ownerAuth endpoint handles both approval and denial of queued transactions.
Policy as a Second Line of Defense
Session TTL handles when access expires. Policies handle what the agent can do while access is active. These two systems work together.
The policy engine has 21 policy types and 4 security tiers: INSTANT, NOTIFY, DELAY, and APPROVAL. The default is deny — if you haven't explicitly allowed a token or contract, the transaction is blocked.
A minimal spending policy looks like this:
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
}
}'
With this configuration:
- Under $100: executes immediately
- $100–$500: executes and sends a notification
- $500–$2,000: queued for 15 minutes, cancellable during that window
- Over $2,000: requires explicit owner approval
Combined with a short session TTL and maxRenewals, this means even a fully autonomous agent is operating inside a box with hard walls on both time and value. If the agent misbehaves, the DELAY tier gives you a window to cancel before anything executes. The APPROVAL tier means certain actions simply cannot happen without a human.
Dry-Run Before You Commit
One underrated tool for self-hosters: the dry-run API. Before your agent goes live, you can simulate any transaction to see how the policy engine would respond — without broadcasting anything to a blockchain.
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
}'
This is how you validate your policy configuration before exposing a session token to an agent. Run a dry-run at $50, $150, $600, $2,100 — confirm each hits the tier you intended. If a POLICY_DENIED comes back when you expected INSTANT, you know your ALLOWED_TOKENS policy isn't configured yet.
The error format is machine-readable, so agents can inspect why a transaction was rejected:
{
"error": {
"code": "POLICY_DENIED",
"message": "Transaction denied by SPENDING_LIMIT policy",
"domain": "POLICY",
"retryable": false
}
}
Getting a Self-Hosted Instance Running
If you want to try this on your own hardware, the Docker path is the fastest:
Step 1: Clone and start
git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
docker compose up -d
The daemon binds to 127.0.0.1:3100 by default — it's not publicly accessible unless you explicitly expose it.
Step 2: Auto-provision for a passwordless first start
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
docker exec waiaas cat /data/recovery.key
Step 3: Install the CLI and initialize
npm install -g @waiaas/cli
waiaas init
waiaas quickset --mode mainnet
Step 4: Harden your master password when ready
waiaas set-master # Replace auto-provisioned password with your own
After that, delete recovery.key from your data volume. It's only needed for the initial bootstrap.
Step 5: Configure a spending policy and set a session TTL
Use the policy API call from earlier in this post. Then create a session for your agent with the scoped TTL you want. The agent gets a wai_sess_ token; your policies enforce the rest.
For Production: Secrets Overlay
If you're running this on a server rather than a local machine, use the Docker secrets overlay for your master password:
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 keeps your master password out of environment variables and out of your compose file's commit history.
The Self-Hosted Mindset
Running your own wallet infrastructure for AI agents is closer to "actually practical" than it used to be. The operational surface is a single Docker container, a CLI, and a handful of API calls. The tradeoff you're making — accepting operational responsibility — is the same one you make when you run your own email server, your own VPN, or your own Nextcloud instance.
What you get in exchange is meaningful: no third-party custody, no API rate limits from hosted services, no dependency on another company's uptime, and complete auditability of every session token ever issued. When an agent's session expires, it's because you said it should. When a transaction gets blocked, it's because your policy said so. When the kill switch fires, it fires on your server.
That's not a philosophical position — it's an architecture decision with concrete security properties.
What's Next
The WAIaaS documentation covers the full policy type reference, MCP integration for connecting agents to Claude and other LLM frameworks, and the TypeScript and Python SDKs for building agents that interact with the REST API directly.
Start at the GitHub repository — https://github.com/waiaas/WAIaaS — where you'll find the Docker Compose files, the CLI source, and the full monorepo. The project site at https://waiaas.ai has additional guides for specific deployment patterns and agent integrations.