DEV Community

Cover image for ALLOWED_TOKENS + CONTRACT_WHITELIST: The Foundation of AI Agent Security
Wallet Guy
Wallet Guy

Posted on

ALLOWED_TOKENS + CONTRACT_WHITELIST: The Foundation of AI Agent Security

ALLOWED_TOKENS + CONTRACT_WHITELIST: The Foundation of AI Agent Security

Giving an AI agent a wallet without guardrails is like giving a toddler a credit card — the agent might be perfectly capable of executing trades, but one misconfigured prompt or unexpected edge case can drain your funds before you can blink. If you're building anything that lets an AI agent sign transactions on your behalf, the question isn't whether you need security controls — it's which controls actually prevent the scenarios that matter.

Why This Is a Hard Problem

Most wallet security is designed around human users who behave predictably. You log in, you review a transaction, you click confirm. AI agents don't work that way. They receive instructions in natural language, interpret them, and fire off API calls — sometimes in rapid succession, sometimes for amounts and contracts they've never touched before. A prompt injection in a data source, an overly broad system prompt, or a simple misunderstanding can translate directly into an on-chain transaction.

The security model has to shift. You can't rely on the agent to know what it shouldn't do. You need a layer that enforces those limits at the infrastructure level, before any transaction reaches the network — regardless of what the agent believes it was asked to do.

WAIaaS approaches this with three layers between your agent and your funds: session authentication, a policy engine with default-deny enforcement, and human approval channels for high-stakes operations. This post focuses on the two policies that form the foundation of that middle layer: ALLOWED_TOKENS and CONTRACT_WHITELIST.

The Default-Deny Principle

Before getting into specifics, it's worth understanding the default posture. In WAIaaS, the policy engine operates on default-deny: if ALLOWED_TOKENS or CONTRACT_WHITELIST is not configured for a wallet, those transaction types are blocked. The agent doesn't get to try and fail — the request never reaches the network.

This is the opposite of how most systems work. Most systems are default-allow with optional restrictions layered on top. Default-allow is convenient for development and catastrophic for production. Default-deny means you make an explicit decision about every token and every contract your agent is permitted to interact with. Anything outside that explicit list returns a POLICY_DENIED error immediately.

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

The error is not retryable. There is no fallback. The transaction is dead on arrival.

ALLOWED_TOKENS: Controlling What the Agent Can Send

ALLOWED_TOKENS is a whitelist of token contracts your agent is permitted to transfer. If a token address isn't on the list, the transaction is blocked — full stop.

Here's what that policy configuration looks like:

curl -X POST http://localhost:3100/v1/policies \
  -H 'Content-Type: application/json' \
  -H 'X-Master-Password: <password>' \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "ALLOWED_TOKENS",
    "rules": {
      "tokens": [
        {
          "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
          "symbol": "USDC",
          "chain": "solana"
        }
      ]
    }
  }'
Enter fullscreen mode Exit fullscreen mode

This policy is set via masterAuth — the system administrator credential authenticated with Argon2id. The AI agent itself, which only holds a sessionAuth JWT, cannot create or modify policies. That separation is intentional and critical. The agent operates within whatever envelope the operator defines; it has no mechanism to expand that envelope.

The practical implication: if your agent is a USDC-only treasury bot, you configure exactly one token in ALLOWED_TOKENS. Any attempt to transfer SOL, any other SPL token, any ERC-20 — denied. Even if the agent is convinced by a user or a data source that it should be doing something else.

CONTRACT_WHITELIST: Controlling What the Agent Can Call

Token transfers are only half the picture. DeFi interactions — swaps, lending, staking — go through smart contracts. CONTRACT_WHITELIST gives you the same default-deny control over contract interactions.

curl -X POST http://localhost:3100/v1/policies \
  -H 'Content-Type: application/json' \
  -H 'X-Master-Password: <password>' \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "CONTRACT_WHITELIST",
    "rules": {
      "contracts": [
        {
          "address": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
          "name": "Jupiter",
          "chain": "solana"
        }
      ]
    }
  }'
Enter fullscreen mode Exit fullscreen mode

If your agent is authorized to swap via Jupiter, that's the one contract address in the list. An attempt to call any other program — a new DEX, a yield protocol your agent found in some documentation, a contract it was hallucinated into believing exists — hits the whitelist and stops.

This matters particularly for prompt injection scenarios. A malicious document or API response that tells your agent to "approve spending to 0x[attacker-contract]" can't actually succeed if that address isn't in CONTRACT_WHITELIST. The policy layer doesn't read natural language; it reads transaction parameters.

Layering Policies: Defense in Depth

ALLOWED_TOKENS and CONTRACT_WHITELIST are the access control layer — they define what the agent can interact with. But WAIaaS gives you additional policies to layer on top of that foundation.

The policy engine supports 21 policy types in total. For a security-focused deployment, you'd typically combine several:

SPENDING_LIMIT adds amount-based tier escalation on top of the token whitelist. Even if USDC is an allowed token, you can require human approval for transfers above a certain threshold:

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

The four security tiers work like this:

  • INSTANT — execute immediately, no notification
  • NOTIFY — execute immediately, send you a notification
  • DELAY — queue the transaction for delay_seconds, then execute (you can cancel it during that window)
  • APPROVAL — full stop, requires your explicit approval via WalletConnect, Telegram, or push notification

APPROVED_SPENDERS closes a common attack vector: token approvals. An AI agent managing DeFi positions will sometimes need to call approve() to allow a protocol to spend tokens on its behalf. APPROVED_SPENDERS applies default-deny to that operation — only whitelisted addresses can be approved, and you can cap the maximum approval amount:

{
  "spenders": [
    {
      "address": "0xDEF1...",
      "name": "Uniswap Router",
      "maxAmount": "1000000000"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

APPROVE_AMOUNT_LIMIT and APPROVE_TIER_OVERRIDE give you further control over approval transactions specifically — blocking unlimited approvals and forcing them through the APPROVAL tier regardless of dollar amount.

The Three-Layer Architecture

Zooming out, the full security model has three layers:

  1. Session authentication — AI agents authenticate with JWT session tokens (sessionAuth). Sessions have configurable TTL, maximum renewals, and absolute lifetime. The agent can't extend its own session or escalate its own permissions. Session creation is masterAuth only.

  2. Policy engine — The 7-stage transaction pipeline runs every transaction through validation, authentication, and policy enforcement before execution. Policies are evaluated in the stage3-policy stage. A POLICY_DENIED result terminates the pipeline immediately.

  3. Human approval channels — Transactions that reach the APPROVAL tier don't execute until you sign off. WAIaaS supports WalletConnect integration for owner approval, with the owner authenticating via ownerAuth (SIWE for EVM, SIWS for Solana). There are 3 signing channels available: push-relay, Telegram, and wallet notification.

These layers are independent. Compromising the session token doesn't let an attacker bypass policy. Bypassing policy would require masterAuth credentials, which aren't exposed to the agent at all.

Simulating Before You Deploy

Before putting any of this in production, you should verify that your policies behave the way you expect them to. WAIaaS has a dry-run API for exactly this purpose:

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

With dryRun: true, the transaction runs through the full pipeline — validation, policy evaluation, simulation — but never broadcasts to the network. You get back the same response structure you'd get from a real transaction, including any policy denials. This lets you confirm that your ALLOWED_TOKENS and CONTRACT_WHITELIST configurations are actually blocking what you think they're blocking before real funds are involved.

Quick Start: Securing Your First Agent Wallet

Here's the minimal path to a policy-hardened agent wallet:

Step 1: Start WAIaaS

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

Step 2: Create a wallet and session

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

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

Step 3: Apply ALLOWED_TOKENS — whitelist only the tokens your agent needs

curl -X POST http://localhost:3100/v1/policies \
  -H 'Content-Type: application/json' \
  -H 'X-Master-Password: <password>' \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "ALLOWED_TOKENS",
    "rules": {
      "tokens": [{"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "symbol": "USDC", "chain": "solana"}]
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Step 4: Apply CONTRACT_WHITELIST — whitelist only the contracts your agent needs to call

Step 5: Test with dry-run — confirm policy denials work before going live, using the dryRun: true flag shown above.

The session token goes to your agent. The master password stays with you. Your agent now operates in an explicitly defined envelope, and anything outside that envelope fails fast with a clear error code.

What This Looks Like at Runtime

Once your agent is running with the TypeScript SDK, the security layer is invisible when everything is authorized — and loud when it isn't:

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

const client = new WAIaaSClient({
  baseUrl: process.env['WAIAAS_BASE_URL'] ?? 'http://localhost:3100',
  sessionToken: process.env['WAIAAS_SESSION_TOKEN'],
});

try {
  const tx = await client.sendToken({ to: '...', amount: '1.0' });
} catch (error) {
  if (error instanceof WAIaaSError) {
    console.error(`API Error: [${error.code}] ${error.message}`);
    // error.code examples: INSUFFICIENT_BALANCE, POLICY_DENIED, TOKEN_EXPIRED
  }
}
Enter fullscreen mode Exit fullscreen mode

A POLICY_DENIED error with retryable: false means the policy engine caught it. The transaction never left your machine. You have a complete audit trail in the daemon logs, and your funds are untouched.

The Honest Assessment

No security system is perfect. WAIaaS's policy engine operates at the transaction level — it enforces rules about what gets signed and sent, not about why your agent decided to send it. If your agent is given legitimate authorization to transfer USDC to a whitelisted address, and it does so because it was manipulated, that transaction will execute.

The value of ALLOWED_TOKENS and CONTRACT_WHITELIST is that they constrain the blast radius. A compromised agent with a narrow token whitelist and a small spending limit can do far less damage than one with no policy controls at all. The DELAY tier gives you a cancellation window. The APPROVAL tier requires you to be in the loop entirely.

Layer these with sensible system prompts, conservative spending limits, and regular review of your agent's transaction history. The policy engine is one part of a broader security practice, not a substitute for it.

What's Next

The full OpenAPI 3.0 spec for WAIaaS is available at http://127.0.0.1:3100/doc, with an interactive reference UI at /reference — useful for exploring the complete policy configuration surface. The codebase is open source, so you can review exactly how the 7-stage transaction pipeline evaluates policies before trusting it with real funds.

If you're ready to go further, the GitHub repository is at https://github.com/minhoyoo-iotrust/WAIaaS and the official site is at https://waiaas.ai. Both have the current documentation and deployment guides for getting a hardened instance running in production.

Top comments (0)