Verifiable AI Agent Messages: ERC-8128 Signing for Trustless Communication

Verifiable AI Agent Messages: ERC-8128 Signing for Trustless Communication

Giving an AI agent a wallet without guardrails is like giving a toddler a credit card — the agent might be brilliant at executing trades, but nothing stops it from doing exactly what you told it to do in a context you never intended. ERC-8128 signing is one piece of a larger answer to that problem: a way for external services to verify that a message genuinely came from a specific, authenticated agent — not a replay, not a spoof, not an injection. This post walks through how WAIaaS implements ERC-8128 as part of a broader security architecture built around the assumption that AI agents will, eventually, try to do something you didn't expect.

Why Message Authenticity Matters More Than You Think

When an AI agent interacts with an external API — say, a DeFi protocol, a data provider, or another autonomous agent — that external service has no way to know whether the request is legitimate unless you give it one. Traditional API keys solve part of this: they prove the caller has a secret. But they don't prove which agent made the call, when, or what wallet it's acting on behalf of.

This matters for a few concrete reasons:

ERC-8128 addresses this by requiring agents to cryptographically sign HTTP requests, binding the signature to a specific domain, timestamp, and payload. WAIaaS exposes this through two MCP tools — erc8128-sign-request and erc8128-verify-signature — and enforces it through the ERC8128_ALLOWED_DOMAINS policy type.

The Three Layers Between Your Agent and Your Funds

Before diving into ERC-8128 specifically, it helps to understand where it sits in WAIaaS's overall security model. There are three layers:

Layer 1: Session authentication. Every AI agent operates through a session token (wai_sess_...), created by the system administrator using the master password. The agent can't create its own sessions. Sessions have per-session TTL, maxRenewals, and absoluteLifetime constraints. If a session token leaks, it expires.

Layer 2: Policy engine with default-deny. This is the core of the risk model. WAIaaS ships with 21 policy types and 4 security tiers: INSTANT, NOTIFY, DELAY, and APPROVAL. The default-deny principle means your agent cannot transfer a token, call a contract, or interact with a DeFi protocol unless you have explicitly whitelisted it. If ALLOWED_TOKENS is not configured, token transfers are blocked. If CONTRACT_WHITELIST is not configured, contract calls are blocked. The agent doesn't get to ask nicely — the policy engine just says no.

Layer 3: Monitoring and kill switch. Incoming transactions are monitored in real time. Human approval is required for transactions that exceed the DELAY tier threshold. Owners can connect via WalletConnect to approve or reject pending transactions.

ERC-8128 signing lives primarily in layers 1 and 2 — it's a mechanism for establishing that outgoing requests from your agent are authentically signed, and for restricting which external domains your agent is allowed to sign requests for.

What ERC8128_ALLOWED_DOMAINS Actually Does

The ERC8128_ALLOWED_DOMAINS policy is one of the 21 policy types in WAIaaS's policy engine. It restricts which external domains your AI agent is permitted to generate ERC-8128 signed requests for.

Here's what that looks like in practice. You create a policy that says: "This agent is only allowed to sign HTTP requests destined for api.example.com and *.trusted-protocol.io."

curl -X POST http://localhost:3100/v1/policies \
  -H 'Content-Type: application/json' \
  -H 'X-Master-Password: <password>' \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "ERC8128_ALLOWED_DOMAINS",
    "rules": {
      "domains": ["api.example.com", "*.trusted-protocol.io"]
    }
  }'

If your agent tries to sign a request for a domain not on this list, the policy engine blocks it. This is the same default-deny logic that governs token transfers — explicit permission, not implicit trust.

This matters because prompt injection attacks are real. An adversarially crafted input might instruct your agent to sign a request to an attacker-controlled endpoint, leaking the wallet's identity or triggering a payment via x402. ERC8128_ALLOWED_DOMAINS closes that vector at the policy layer, not at the application layer.

The Signing Flow via MCP Tools

WAIaaS exposes 45 MCP tools for AI agent integration. Two of them are directly relevant here: erc8128-sign-request and erc8128-verify-signature.

The flow for an AI agent making an authenticated outbound call looks like this:

  1. The agent constructs the HTTP request it wants to make.
  2. It calls the erc8128-sign-request tool, passing the target domain and request payload.
  3. WAIaaS signs the request using the wallet's private key, producing a signature bound to the domain, timestamp, and payload.
  4. The agent attaches the signature headers to the outgoing request.
  5. The receiving service calls erc8128-verify-signature (or implements its own verification) to confirm the signature is valid and hasn't been replayed.

This creates a verifiable chain: the external service can prove the request came from a specific wallet, at a specific time, for a specific payload. If any of those change, the signature is invalid.

For Claude Desktop users, the MCP setup is straightforward:

{
  "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"
      }
    }
  }
}

Once connected, Claude can call erc8128-sign-request and erc8128-verify-signature directly, within the bounds of whatever ERC8128_ALLOWED_DOMAINS policy you've configured.

Combining ERC-8128 with the Spending Policy for Defense in Depth

ERC-8128 is a message authentication mechanism. It doesn't, by itself, limit what an agent spends. For that, you pair it with the SPENDING_LIMIT policy, which implements the 4-tier security model:

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

The tier logic is deterministic: a transaction at or below instant_max_usd executes immediately. Between instant_max_usd and notify_max_usd, it executes and you get a notification. Between notify_max_usd and delay_max_usd, it queues for delay_seconds and can be cancelled. Above delay_max_usd, it requires explicit human approval via WalletConnect, Telegram, or push notification.

There are 3 signing channels: push-relay-signing-channel, telegram-signing-channel, and wallet-notification-channel. You configure whichever fits your setup, and any transaction that hits the APPROVAL tier sits in the queue until you act on it.

This means an ERC-8128-authenticated request that also triggers a large payment doesn't automatically go through. The payment path runs through the full 7-stage transaction pipeline — validate, authenticate, policy check, wait, execute, confirm — regardless of how the originating request was signed.

Authentication Layers: Who Signs What

WAIaaS uses three distinct authentication methods, each for a different actor:

The separation is deliberate. If your agent's session token is compromised, the attacker can only do what the policy engine allows — they can't create new wallets, change policies, or approve their own transactions. The master password and owner key are separate credentials held by separate parties.

Quick Start: Deploying with Security from Day One

Here's the minimal path to running WAIaaS with sensible security defaults:

Step 1: Deploy the daemon

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

The Docker image is ghcr.io/minhoyoo-iotrust/waiaas:latest, binding to 127.0.0.1:3100 by default. For production, use the secrets overlay:

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

Step 2: Initialize and create a wallet

npm install -g @waiaas/cli
waiaas init
waiaas start
waiaas quickset --mode mainnet

Or manually via the API:

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": "agent-wallet", "chain": "solana", "environment": "mainnet"}'

Step 3: Set your policies before creating the session

Create your SPENDING_LIMIT, ALLOWED_TOKENS, and ERC8128_ALLOWED_DOMAINS policies (examples above) before you hand a session token to any agent. The policy engine is default-deny — configure it first, not after an incident.

Step 4: Create the agent session and connect MCP

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>"}'

Paste the returned session token into your MCP configuration or TypeScript SDK client:

import { WAIaaSClient } from '@waiaas/sdk';

const client = new WAIaaSClient({
  baseUrl: 'http://127.0.0.1:3100',
  sessionToken: process.env.WAIAAS_SESSION_TOKEN,
});

const balance = await client.getBalance();
console.log(`${balance.balance} ${balance.symbol}`);

Step 5: Test before going live

WAIaaS includes a dry-run API. Before your agent executes anything real, 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 simulation runs through the full pipeline including policy checks, without touching the chain. If the policy engine would block it, you'll see the POLICY_DENIED error here rather than in production.

What You're Actually Preventing

Let's be concrete about the threat model. With this stack in place:

None of this is magic. It's default-deny policy enforcement, cryptographic authentication, and mandatory human checkpoints at configurable thresholds. The security properties are visible, auditable, and configurable by the operator — not assumed.

What's Next

The interactive API reference at http://127.0.0.1:3100/reference documents all 39 route modules including the policy and transaction endpoints, with a live interface for testing. The full policy reference covers all 21 policy types and their rule schemas in detail.

If you're building agents that need to interact with external services, the MCP tool list — including erc8128-sign-request, erc8128-verify-signature, and the full set of 45 tools — is the right starting point. Pair it with a SPENDING_LIMIT and domain whitelist from day one, and you have a defensible security posture before your agent touches mainnet.

Explore the codebase on GitHub at https://github.com/minhoyoo-iotrust/WAIaaS and read more about the project at https://waiaas.ai.