Agent wallet security is the practice of protecting autonomous AI agents that hold and transact real money in prediction markets from key extraction, unauthorized spending, prompt injection-driven fund drainage, and operational failures. This guide covers the five major wallet security architectures (MPC, multisig, session keys, TEE, and HSM), provides complete Python implementations for spending controls, and includes a production security checklist for teams deploying agent wallets on Polymarket, Kalshi, and other prediction market platforms.

If your agent holds money and makes decisions autonomously, every vulnerability is a direct path to financial loss. There is no “low-risk” configuration for an agent wallet that lacks protocol-level spending controls. This guide exists because application-layer guardrails alone are not enough — a bug in your guardrail code, a prompt injection that bypasses your validation, or a compromised dependency can all defeat code-level protections. You need defense in depth.

For the wallet selection decision itself, see Best Agent Wallet for Prediction Markets. For how wallets fit into the full stack, see The Agent Betting Stack. For general agent security beyond wallets, see Security Best Practices.


Threat Model for Prediction Market Agents

Agent wallets face a unique combination of risks that traditional wallets do not. Three properties create this elevated threat surface: agents spend money autonomously (no human in the loop for each transaction), agents process adversarial inputs as part of normal operation (market data, social feeds, API responses), and agents transact with real money on irreversible on-chain networks.

A compromised traditional wallet requires the attacker to steal the private key. A compromised agent wallet can be exploited without ever extracting the key — the attacker manipulates the agent into signing transactions willingly, using its own key, through its normal execution flow.

Attack Surface Map

The core attack vectors for prediction market agents flow from input manipulation to unauthorized transactions:

  1. Prompt injection via Moltbook posts, market descriptions, or API responses that override agent reasoning
  2. Key extraction from environment variables, logs, memory dumps, or unencrypted storage
  3. Dependency compromise through malicious packages in the supply chain (typosquatting, maintainer takeover)
  4. Oracle manipulation where attackers influence the data feeds an agent relies on for trading decisions
  5. Transaction replay where signed but unsubmitted transactions are captured and rebroadcast
  6. API credential theft from exposed config files, Git history, or shared environments

Threat/Impact/Mitigation Matrix

ThreatImpactLikelihoodMitigation
Prompt injection via market dataAgent places attacker-directed trades, draining bankrollHighSeparate LLM reasoning from execution layer; structured output validation; input sanitization
Private key extractionTotal loss of all wallet fundsMediumMPC key splitting, TEE enclave isolation, HSM storage; never store raw keys in environment variables
Unlimited token approvalAttacker drains wallet via approved contractHighSet exact approval amounts per transaction; never use approve(MAX_UINT256); revoke stale approvals
Dependency supply chain attackMalicious code executes in agent runtime, exfiltrates keys or signs transactionsMediumPin dependency versions; use lockfiles; audit new dependencies; run in sandboxed containers
Oracle/data feed manipulationAgent makes trades based on false data, losing money to the manipulatorMediumUse multiple independent data sources; set outlier detection thresholds; reject data that deviates >3 standard deviations from median
Session key over-permissioningCompromised session key allows larger transactions or more contracts than intendedMediumScope session keys to minimum required permissions; set short expiry windows; allowlist only necessary contract addresses
Kill switch bypassAgent continues trading after an incident because kill switch is unreachableLowMultiple kill switch channels (Redis + DB + remote config); dead-man’s switch pattern; heartbeat monitoring
Transaction replay attackSigned transaction is captured and resubmitted on same or different chainLowUse chain-specific nonces; include deadline parameters in transactions; EIP-155 chain ID protection

For a broader treatment of prompt injection defense and agent safety, see Security Best Practices for Agent Betting.


Key Security Architectures

Each architecture provides a different security model for protecting your agent’s private key and controlling what the agent can do with it. The right choice depends on your threat model, latency requirements, fund size, and operational complexity tolerance.

MPC (Multi-Party Computation)

How it works: The private key is never generated or stored as a single entity. Instead, it is mathematically split into multiple shares (typically 2-of-3 or 3-of-5) distributed across independent parties. To sign a transaction, a threshold of parties must cooperate using a cryptographic protocol that produces a valid signature without ever reconstructing the complete key.

Security model: Even if one share is compromised, the attacker cannot sign transactions. They would need to compromise multiple independent parties simultaneously. Key shares can be refreshed (re-split into new shares) without changing the wallet address, limiting the window of exposure.

Providers: Turnkey (2-of-2 with secure enclave), Privy (embedded MPC with social recovery), Fireblocks (institutional-grade 3-of-3 with policy engine).

Tradeoffs:

  • Pro: No single point of failure for key compromise
  • Pro: Key share refresh without address change
  • Pro: Policy engines (Turnkey, Fireblocks) enforce spending rules at the signing layer
  • Con: Vendor dependency — your key shares are held by the provider
  • Con: Signing latency is higher than raw EOA (network round-trips between share holders)
  • Con: Recovery depends on the provider’s process

When to use: Production agents managing $1K-$100K, teams that want managed key security without smart contract complexity, enterprise deployments requiring SOC 2 compliance.

Code example — MPC signing with Turnkey:

from turnkey_client import TurnkeyClient
from eth_account.messages import encode_defunct

# Initialize Turnkey client with API credentials
client = TurnkeyClient(
    api_public_key="YOUR_API_PUBLIC_KEY",
    api_private_key="YOUR_API_PRIVATE_KEY",
    organization_id="YOUR_ORG_ID",
    base_url="https://api.turnkey.com"
)

# Sign a transaction using MPC — key never leaves Turnkey infrastructure
def sign_with_mpc(wallet_id: str, transaction: dict) -> str:
    """
    Sign a transaction using Turnkey MPC.
    The private key is split across Turnkey's infrastructure.
    No single party ever holds the complete key.
    """
    # Turnkey validates the transaction against your policy engine
    # before producing the MPC signature
    result = client.sign_transaction(
        wallet_id=wallet_id,
        transaction=transaction,
        # Policy engine checks happen server-side:
        # - Per-transaction limit
        # - Daily spending cap
        # - Contract allowlist
    )
    return result.signed_transaction

# Create a policy that limits agent spending
policy = client.create_policy(
    name="prediction-market-agent",
    rules=[
        {
            "type": "max_transaction_amount",
            "amount": "100",       # $100 max per trade
            "currency": "USDC"
        },
        {
            "type": "daily_spending_limit",
            "amount": "1000",      # $1,000 daily cap
            "currency": "USDC"
        },
        {
            "type": "contract_allowlist",
            "addresses": [
                "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",  # Polymarket CTF Exchange
                "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",  # USDC on Polygon
            ]
        }
    ]
)

Multisig (Safe)

How it works: A Safe smart contract wallet requires M-of-N owner signatures to execute any transaction. For agent wallets, the typical configuration is 2-of-3: the agent holds one key, a backend service holds a second, and a human admin holds the third. The agent proposes transactions, and the backend co-signs after policy validation. The human key is reserved for emergencies and recovery.

Security model: No single key compromise can move funds. The smart contract enforces the threshold on-chain — there is no way to bypass it without M valid signatures. Transaction guards (Zodiac modules) add programmable constraints: spending limits, contract allowlists, time-locks, and cooldown periods.

Tradeoffs:

  • Pro: Strongest on-chain enforcement — constraints live in the smart contract, not in application code
  • Pro: Battle-tested — over $100B secured by Safe contracts
  • Pro: Human-in-the-loop for high-value or unusual transactions
  • Pro: Modular — add transaction guards, delay modules, recovery modules
  • Con: Higher gas costs per transaction (smart contract execution overhead)
  • Con: Latency — multi-sig approval adds seconds to minutes
  • Con: Complex setup and management
  • Con: Not suitable for high-frequency strategies

When to use: Treasuries holding $50K+, agents requiring human oversight for large trades, multi-agent governance, DAO-controlled agent operations.

Config example — Safe with transaction guard:

from safe_eth.safe import Safe
from safe_eth.safe.safe_tx import SafeTx
from web3 import Web3

w3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com"))

# Load existing Safe
safe = Safe(
    address="0xYOUR_SAFE_ADDRESS",
    ethereum_client=w3
)

# Transaction guard contract enforces constraints on-chain
# Deploy this guard to restrict what the agent can do
TRANSACTION_GUARD_ABI = [
    # checkTransaction is called before every Safe transaction
    # Revert here = transaction is blocked at the contract level
    {
        "name": "checkTransaction",
        "inputs": [
            {"name": "to", "type": "address"},
            {"name": "value", "type": "uint256"},
            {"name": "data", "type": "bytes"},
            # ... additional parameters
        ]
    }
]

# Guard configuration (set via Safe's setGuard function)
GUARD_CONFIG = {
    "max_value_per_tx": Web3.to_wei(100, "ether"),   # 100 USDC max
    "daily_limit": Web3.to_wei(1000, "ether"),        # 1,000 USDC daily
    "allowed_targets": [
        "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",  # Polymarket CTF
    ],
    "cooldown_seconds": 60,  # Minimum 60s between transactions
}

# Agent proposes a trade — needs co-signer approval
def propose_trade(safe: Safe, to: str, value: int, data: bytes):
    """
    Agent proposes a transaction to the Safe.
    The transaction guard validates constraints on-chain.
    Co-signer must approve before execution.
    """
    safe_tx = safe.build_multisig_tx(
        to=to,
        value=value,
        data=data,
        safe_nonce=safe.retrieve_nonce()
    )

    # Agent signs with its key
    safe_tx.sign(agent_private_key)

    # Submit to Safe Transaction Service for co-signer approval
    # Co-signer (backend or human) reviews and co-signs
    safe.post_transaction(safe_tx)

    return safe_tx.safe_tx_hash

Session Keys (ERC-4337)

How it works: ERC-4337 account abstraction enables smart contract wallets to issue temporary, scoped signing keys called session keys. Each session key has hard-coded constraints: which contracts it can call, which functions it can invoke, maximum spend amounts, and an expiry timestamp. The agent uses the session key for trading; the master key stays in cold storage.

Security model: Even if the session key is fully compromised, the attacker can only do what the session permits — interact with allowlisted contracts, spend up to the session cap, and only until the key expires. The master key is never exposed during normal operation.

Tradeoffs:

  • Pro: Minimum privilege — agent gets exactly the permissions it needs, nothing more
  • Pro: Auto-expiry — compromised keys become worthless after the time window
  • Pro: No key rotation needed — just issue a new session key
  • Pro: On-chain enforcement of all constraints
  • Con: ERC-4337 infrastructure is still maturing
  • Con: Gas overhead for UserOperation validation
  • Con: Bundler/paymaster dependency for gas abstraction
  • Con: Not all chains have mature ERC-4337 support

When to use: Agents that need scoped, time-bounded permissions. Ideal for agents trading on a schedule (e.g., 4-hour trading sessions) where permissions should automatically expire.

Code example — session key with scoped permissions:

from eth_account import Account
from web3 import Web3
import time
import json

# Session key configuration for a Polymarket trading agent
SESSION_KEY_CONFIG = {
    # Scoped permissions
    "allowed_contracts": [
        "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",  # Polymarket CTF Exchange
        "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",  # USDC on Polygon
    ],
    "allowed_functions": [
        "0x12345678",  # placeTrade function selector
        "0xabcdef01",  # cancelOrder function selector
        "0x095ea7b3",  # approve (USDC approval for CTF Exchange only)
    ],
    # Spending limits
    "max_spend_per_tx": Web3.to_wei(100, "mwei"),   # 100 USDC (6 decimals)
    "max_spend_session": Web3.to_wei(500, "mwei"),   # 500 USDC total
    # Time bounds
    "valid_after": int(time.time()),                  # Starts now
    "valid_until": int(time.time()) + (4 * 3600),     # Expires in 4 hours
}

def create_session_key(master_wallet, config: dict) -> dict:
    """
    Create a scoped session key for the agent.
    The smart contract wallet validates every UserOperation
    against these constraints before executing.
    """
    # Generate a fresh ephemeral key pair
    session_account = Account.create()

    # Register the session key with the smart contract wallet
    # This is a simplified representation — actual implementation
    # depends on your ERC-4337 wallet provider
    session_data = {
        "session_key": session_account.address,
        "permissions": {
            "allowed_contracts": config["allowed_contracts"],
            "allowed_functions": config["allowed_functions"],
            "max_spend_per_tx": config["max_spend_per_tx"],
            "max_spend_session": config["max_spend_session"],
            "valid_after": config["valid_after"],
            "valid_until": config["valid_until"],
        }
    }

    # The master key signs the session key registration
    # After this, the master key goes back to cold storage
    registration_tx = master_wallet.register_session_key(session_data)

    return {
        "session_private_key": session_account.key.hex(),
        "session_address": session_account.address,
        "permissions": session_data["permissions"],
        "expires_at": config["valid_until"],
    }

def validate_against_session(tx: dict, session: dict) -> bool:
    """
    Check if a transaction is within session key permissions.
    The smart contract does this on-chain, but we pre-validate
    in the agent to fail fast and avoid wasting gas.
    """
    perms = session["permissions"]

    if tx["to"] not in perms["allowed_contracts"]:
        raise ValueError(f"Contract {tx['to']} not in session allowlist")

    if tx["data"][:10] not in [f"0x{s}" for s in perms["allowed_functions"]]:
        raise ValueError(f"Function not permitted in this session")

    if tx.get("value", 0) > perms["max_spend_per_tx"]:
        raise ValueError(f"Transaction exceeds per-tx limit")

    if time.time() > perms["valid_until"]:
        raise ValueError("Session key expired")

    return True

TEE/Enclave (Coinbase)

How it works: Trusted Execution Environments (TEEs) are hardware-isolated regions of a processor where code and data are protected from the operating system, hypervisor, and even physical access to the machine. Coinbase Agentic Wallets generate and store private keys inside TEE enclaves (specifically, AWS Nitro Enclaves). The key never leaves the enclave — signing happens inside the secure boundary, and only the signed transaction output exits.

Security model: The strongest single-party key isolation available. Even if an attacker gains root access to the server running the agent, they cannot extract the private key from the enclave. The enclave provides cryptographic attestation — a proof that the signing code running inside is exactly the expected code, not a modified version.

Key properties:

  • Hardware isolation — Keys are protected by CPU-level security boundaries, not software
  • Attestation — Third parties can verify the enclave is running expected code
  • Sealed storage — Data encrypted to the enclave can only be decrypted by that specific enclave
  • No key export — The private key is generated inside and never leaves the enclave boundary

Tradeoffs:

  • Pro: Strongest key isolation for single-party custody
  • Pro: Attestation provides verifiable security guarantees
  • Pro: Low latency — signing happens locally, no network round-trips
  • Con: Vendor dependency on Coinbase infrastructure
  • Con: TEE vulnerabilities do exist (side-channel attacks, though rare and complex to exploit)
  • Con: Less transparent than on-chain solutions — you trust Coinbase’s enclave implementation
  • Con: Recovery depends on Coinbase’s backup and disaster recovery processes

When to use: Default choice for new prediction market agents. Best combination of security, speed, and developer experience for agents managing $100-$50K.

For the full Coinbase Agentic Wallets setup guide, see Coinbase Agentic Wallets Guide.

HSM (Hardware Security Module)

How it works: Hardware Security Modules are dedicated cryptographic processors that generate, store, and use private keys in tamper-resistant hardware. Unlike TEEs (which are isolated regions of a general-purpose CPU), HSMs are purpose-built devices certified to federal standards (FIPS 140-2 Level 3 or higher). Cloud providers offer managed HSM services: AWS CloudHSM, Azure Dedicated HSM, and Google Cloud HSM.

Security model: FIPS 140-2 Level 3 certification means the device has physical tamper-evidence and tamper-response mechanisms — attempts to physically access the key material trigger key destruction. HSMs provide the highest assurance level for key storage, which is why banks and certificate authorities use them.

Key properties:

  • FIPS 140-2 certified — Government-standard cryptographic security
  • Tamper-resistant hardware — Physical attacks trigger key destruction
  • Audit logging — Every key usage is logged with tamper-evident audit trails
  • High availability — Cloud HSM services provide clustered, redundant deployments

Tradeoffs:

  • Pro: Highest certification level for key security
  • Pro: Regulatory compliance (required for some financial applications)
  • Pro: Tamper-resistant physical hardware
  • Con: Expensive — AWS CloudHSM costs $1.50/hour ($1,100/month)
  • Con: Complex integration — HSMs use PKCS#11 interfaces, not standard web3 libraries
  • Con: Overkill for most prediction market agent use cases
  • Con: Latency for high-frequency operations

When to use: Enterprise deployments with regulatory requirements, agents managing $500K+, financial institutions building agent infrastructure, situations where FIPS 140-2 compliance is mandated.


Spending Controls Implementation

Spending controls are the last line of defense. Even if your key security is perfect, a logic bug or prompt injection can cause the agent to sign legitimate-looking transactions that drain its bankroll. Every production agent needs hard spending limits that the agent cannot override.

The following implementations are application-layer controls. For maximum security, combine these with protocol-level limits (MPC policy engine, Safe transaction guards, or session key constraints) so that even a compromised application layer cannot bypass the controls.

Per-Transaction Limits

Every single trade must be validated against a maximum size before signing.

from dataclasses import dataclass
from decimal import Decimal

@dataclass
class TransactionLimits:
    max_per_trade: Decimal       # Maximum USDC per single trade
    min_per_trade: Decimal       # Minimum to prevent dust attacks

def validate_trade_size(amount: Decimal, limits: TransactionLimits) -> bool:
    """
    Reject any trade above the per-transaction maximum.
    Called before every trade execution.
    """
    if amount > limits.max_per_trade:
        raise ValueError(
            f"Trade size ${amount} exceeds per-transaction limit "
            f"of ${limits.max_per_trade}"
        )
    if amount < limits.min_per_trade:
        raise ValueError(
            f"Trade size ${amount} below minimum of ${limits.min_per_trade}"
        )
    return True

# Usage
limits = TransactionLimits(
    max_per_trade=Decimal("100.00"),   # $100 max per trade
    min_per_trade=Decimal("1.00"),     # $1 minimum
)
validate_trade_size(Decimal("50.00"), limits)  # OK
validate_trade_size(Decimal("200.00"), limits) # Raises ValueError

Session Spending Caps

Track cumulative spending across a session and halt when the cap is reached.

from decimal import Decimal
from datetime import datetime, timezone

class SessionTracker:
    def __init__(self, session_cap: Decimal):
        self.session_cap = session_cap
        self.total_spent = Decimal("0.00")
        self.trade_count = 0
        self.started_at = datetime.now(timezone.utc)

    def record_and_validate(self, amount: Decimal) -> bool:
        """
        Check if this trade would exceed the session cap.
        Must be called before signing.
        """
        projected = self.total_spent + amount
        if projected > self.session_cap:
            raise ValueError(
                f"Session cap exceeded: ${self.total_spent} spent + "
                f"${amount} proposed = ${projected} > "
                f"cap of ${self.session_cap}"
            )
        self.total_spent = projected
        self.trade_count += 1
        return True

    @property
    def remaining(self) -> Decimal:
        return self.session_cap - self.total_spent

# Usage
session = SessionTracker(session_cap=Decimal("500.00"))
session.record_and_validate(Decimal("100.00"))  # OK, $400 remaining
session.record_and_validate(Decimal("350.00"))  # OK, $50 remaining
session.record_and_validate(Decimal("100.00"))  # Raises ValueError

Daily/Weekly Rolling Limits

Time-window based caps that automatically roll forward.

import time
from decimal import Decimal
from collections import deque
from dataclasses import dataclass

@dataclass
class TradeRecord:
    amount: Decimal
    timestamp: float

class RollingLimitTracker:
    def __init__(self, daily_limit: Decimal, weekly_limit: Decimal):
        self.daily_limit = daily_limit
        self.weekly_limit = weekly_limit
        self.trades: deque[TradeRecord] = deque()

    def _prune_old_trades(self):
        """Remove trades older than 7 days."""
        cutoff = time.time() - (7 * 24 * 3600)
        while self.trades and self.trades[0].timestamp < cutoff:
            self.trades.popleft()

    def _sum_window(self, window_seconds: int) -> Decimal:
        """Sum trade amounts within a time window."""
        cutoff = time.time() - window_seconds
        return sum(
            (t.amount for t in self.trades if t.timestamp >= cutoff),
            Decimal("0.00")
        )

    def validate(self, amount: Decimal) -> bool:
        """Validate against daily and weekly rolling limits."""
        self._prune_old_trades()

        daily_spent = self._sum_window(24 * 3600)
        if daily_spent + amount > self.daily_limit:
            raise ValueError(
                f"Daily limit: ${daily_spent} spent in last 24h + "
                f"${amount} = ${daily_spent + amount} > "
                f"limit of ${self.daily_limit}"
            )

        weekly_spent = self._sum_window(7 * 24 * 3600)
        if weekly_spent + amount > self.weekly_limit:
            raise ValueError(
                f"Weekly limit: ${weekly_spent} spent in last 7d + "
                f"${amount} = ${weekly_spent + amount} > "
                f"limit of ${self.weekly_limit}"
            )

        self.trades.append(TradeRecord(amount=amount, timestamp=time.time()))
        return True

Contract Allowlists

Restrict the agent to interacting with known, verified contracts only.

from web3 import Web3

class ContractAllowlist:
    """
    Only permit transactions to known, verified contracts.
    Prevents the agent from interacting with malicious contracts
    even if prompt injection directs it to do so.
    """

    POLYMARKET_CONTRACTS = {
        "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E": "CTF Exchange",
        "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174": "USDC (Polygon)",
        "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045": "Neg Risk CTF Exchange",
        "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296": "Neg Risk Adapter",
    }

    def __init__(self, additional_contracts: dict[str, str] | None = None):
        self.allowed = dict(self.POLYMARKET_CONTRACTS)
        if additional_contracts:
            self.allowed.update(additional_contracts)

    def validate(self, target_address: str) -> bool:
        """
        Reject transactions to any address not in the allowlist.
        This is a hard block — no override, no exceptions.
        """
        checksummed = Web3.to_checksum_address(target_address)
        if checksummed not in self.allowed:
            raise ValueError(
                f"Contract {checksummed} is not in the allowlist. "
                f"Allowed contracts: {list(self.allowed.values())}"
            )
        return True

Emergency Kill Switch

An immediate halt mechanism that stops all agent operations.

import time
import json
import logging
from pathlib import Path

logger = logging.getLogger(__name__)

class KillSwitch:
    """
    Emergency halt mechanism for agent wallets.
    Supports multiple check sources for redundancy.
    """

    def __init__(
        self,
        local_flag_path: str = "/tmp/agent_kill_switch.json",
        redis_client=None,
        redis_key: str = "agent:kill_switch",
        heartbeat_timeout: int = 300,  # 5 minutes
    ):
        self.local_flag_path = Path(local_flag_path)
        self.redis_client = redis_client
        self.redis_key = redis_key
        self.heartbeat_timeout = heartbeat_timeout
        self.last_heartbeat = time.time()

    def is_killed(self) -> bool:
        """
        Check if the kill switch is active.
        ANY active signal means HALT.
        """
        # Check 1: Local file flag
        if self.local_flag_path.exists():
            try:
                data = json.loads(self.local_flag_path.read_text())
                if data.get("killed", False):
                    logger.critical(
                        f"KILL SWITCH ACTIVE (local): {data.get('reason', 'unknown')}"
                    )
                    return True
            except (json.JSONDecodeError, IOError):
                # If we can't read the flag file, assume killed (fail safe)
                logger.critical("KILL SWITCH: Cannot read flag file — halting")
                return True

        # Check 2: Redis flag (if configured)
        if self.redis_client:
            try:
                value = self.redis_client.get(self.redis_key)
                if value and json.loads(value).get("killed", False):
                    logger.critical("KILL SWITCH ACTIVE (Redis)")
                    return True
            except Exception as e:
                # Redis unreachable — fail safe
                logger.critical(f"KILL SWITCH: Redis unreachable ({e}) — halting")
                return True

        # Check 3: Dead-man's switch (heartbeat timeout)
        if time.time() - self.last_heartbeat > self.heartbeat_timeout:
            logger.critical(
                f"KILL SWITCH: No heartbeat for {self.heartbeat_timeout}s — halting"
            )
            return True

        return False

    def heartbeat(self):
        """Agent must call this regularly to prove it's operating normally."""
        self.last_heartbeat = time.time()

    def trigger(self, reason: str):
        """Manually trigger the kill switch."""
        data = {"killed": True, "reason": reason, "triggered_at": time.time()}
        self.local_flag_path.write_text(json.dumps(data))
        if self.redis_client:
            self.redis_client.set(self.redis_key, json.dumps(data))
        logger.critical(f"KILL SWITCH TRIGGERED: {reason}")

Combined SpendingGuard Class

A unified class that implements all spending controls in a single validation pipeline.

import time
import logging
from decimal import Decimal
from dataclasses import dataclass, field
from collections import deque

logger = logging.getLogger(__name__)

@dataclass
class TradeRecord:
    amount: Decimal
    timestamp: float
    market_id: str
    tx_hash: str = ""

@dataclass
class SpendingGuardConfig:
    max_per_trade: Decimal = Decimal("100.00")
    min_per_trade: Decimal = Decimal("1.00")
    session_cap: Decimal = Decimal("500.00")
    daily_limit: Decimal = Decimal("1000.00")
    weekly_limit: Decimal = Decimal("5000.00")
    allowed_contracts: dict = field(default_factory=lambda: {
        "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E": "CTF Exchange",
        "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174": "USDC (Polygon)",
    })
    max_open_positions: int = 10
    cooldown_seconds: int = 5

class SpendingGuard:
    """
    Unified spending controls for prediction market agents.
    Every trade must pass through validate() before signing.

    Implements five layers of protection:
    1. Per-transaction size limits
    2. Session spending cap
    3. Daily/weekly rolling limits
    4. Contract allowlist
    5. Emergency kill switch

    Usage:
        guard = SpendingGuard(config)
        try:
            guard.validate(
                amount=Decimal("50.00"),
                target_contract="0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
                market_id="polymarket-election-2026"
            )
            # Trade is approved — proceed with signing
        except SpendingViolation as e:
            # Trade is blocked — log and skip
            logger.warning(f"Trade blocked: {e}")
    """

    def __init__(self, config: SpendingGuardConfig, kill_switch=None):
        self.config = config
        self.kill_switch = kill_switch
        self.session_spent = Decimal("0.00")
        self.session_trade_count = 0
        self.trades: deque[TradeRecord] = deque()
        self.last_trade_time = 0.0
        self.session_start = time.time()

    def validate(
        self,
        amount: Decimal,
        target_contract: str,
        market_id: str = "",
    ) -> bool:
        """
        Run all spending validations. Raises SpendingViolation
        if any check fails. Returns True if all checks pass.
        """
        # 0. Kill switch — checked first, overrides everything
        if self.kill_switch and self.kill_switch.is_killed():
            raise SpendingViolation("Kill switch is active — all trading halted")

        # 1. Contract allowlist
        from web3 import Web3
        checksummed = Web3.to_checksum_address(target_contract)
        if checksummed not in self.config.allowed_contracts:
            raise SpendingViolation(
                f"Contract {checksummed} not in allowlist"
            )

        # 2. Per-transaction limits
        if amount > self.config.max_per_trade:
            raise SpendingViolation(
                f"Trade ${amount} exceeds per-tx limit ${self.config.max_per_trade}"
            )
        if amount < self.config.min_per_trade:
            raise SpendingViolation(
                f"Trade ${amount} below minimum ${self.config.min_per_trade}"
            )

        # 3. Session cap
        if self.session_spent + amount > self.config.session_cap:
            raise SpendingViolation(
                f"Session cap: ${self.session_spent} + ${amount} "
                f"> ${self.config.session_cap}"
            )

        # 4. Daily rolling limit
        daily_spent = self._sum_window(24 * 3600)
        if daily_spent + amount > self.config.daily_limit:
            raise SpendingViolation(
                f"Daily limit: ${daily_spent} + ${amount} "
                f"> ${self.config.daily_limit}"
            )

        # 5. Weekly rolling limit
        weekly_spent = self._sum_window(7 * 24 * 3600)
        if weekly_spent + amount > self.config.weekly_limit:
            raise SpendingViolation(
                f"Weekly limit: ${weekly_spent} + ${amount} "
                f"> ${self.config.weekly_limit}"
            )

        # 6. Cooldown between trades
        elapsed = time.time() - self.last_trade_time
        if elapsed < self.config.cooldown_seconds and self.last_trade_time > 0:
            raise SpendingViolation(
                f"Cooldown: {self.config.cooldown_seconds - elapsed:.1f}s remaining"
            )

        # All checks passed — record the trade
        self.session_spent += amount
        self.session_trade_count += 1
        self.last_trade_time = time.time()
        self.trades.append(TradeRecord(
            amount=amount,
            timestamp=time.time(),
            market_id=market_id,
        ))
        self._prune_old_trades()

        logger.info(
            f"Trade approved: ${amount} on {market_id} | "
            f"Session: ${self.session_spent}/${self.config.session_cap} | "
            f"Daily: ${daily_spent + amount}/${self.config.daily_limit}"
        )
        return True

    def _sum_window(self, window_seconds: int) -> Decimal:
        cutoff = time.time() - window_seconds
        return sum(
            (t.amount for t in self.trades if t.timestamp >= cutoff),
            Decimal("0.00")
        )

    def _prune_old_trades(self):
        cutoff = time.time() - (7 * 24 * 3600)
        while self.trades and self.trades[0].timestamp < cutoff:
            self.trades.popleft()

    def get_status(self) -> dict:
        """Return current spending status for monitoring dashboards."""
        return {
            "session_spent": str(self.session_spent),
            "session_remaining": str(self.config.session_cap - self.session_spent),
            "session_trade_count": self.session_trade_count,
            "daily_spent": str(self._sum_window(24 * 3600)),
            "weekly_spent": str(self._sum_window(7 * 24 * 3600)),
            "kill_switch_active": (
                self.kill_switch.is_killed() if self.kill_switch else False
            ),
            "session_uptime_seconds": time.time() - self.session_start,
        }


class SpendingViolation(Exception):
    """Raised when a trade violates spending controls."""
    pass

Integration example — connecting SpendingGuard to a Polymarket trading loop:

from decimal import Decimal

# Initialize spending guard with conservative limits
config = SpendingGuardConfig(
    max_per_trade=Decimal("100.00"),     # 2% of $5K bankroll
    session_cap=Decimal("500.00"),       # 10% per session
    daily_limit=Decimal("1000.00"),      # 20% per day
    weekly_limit=Decimal("2500.00"),     # 50% per week
    cooldown_seconds=10,                 # 10s between trades
)

kill_switch = KillSwitch(redis_client=redis_conn)
guard = SpendingGuard(config=config, kill_switch=kill_switch)

# Trading loop
for signal in trading_signals:
    try:
        # Validate before signing
        guard.validate(
            amount=Decimal(str(signal.size)),
            target_contract=signal.contract_address,
            market_id=signal.market_id,
        )
        # All checks passed — execute trade
        execute_trade(signal)
        # Send heartbeat to keep dead-man's switch happy
        kill_switch.heartbeat()

    except SpendingViolation as e:
        logger.warning(f"Trade blocked by SpendingGuard: {e}")
        # Do NOT execute — move to next signal

    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        # Trigger kill switch on unexpected errors
        kill_switch.trigger(reason=f"Unexpected error: {e}")
        break

Security Comparison Matrix

How do the major wallet solutions compare on security-specific features?

FeatureCoinbase AgenticSafeLit ProtocolTurnkey/PrivyRaw EOA
Key isolationTEE enclave (hardware)Smart contract (on-chain)Distributed PKP (network)MPC shares (off-chain)None (single key in memory)
Spending capsSession + per-tx (built-in)Transaction guards (modular)Lit Actions (programmable)Policy engine (dashboard)Code-level only (fragile)
Kill switchAPI-level haltOwner revocation on-chainRevoke PKP permissionsDashboard toggleCode-level only
Audit trailCoinbase logs + on-chainFull on-chain historyOn-chain + Lit node logsEnterprise dashboard + API logsOn-chain only
Key recoveryCoinbase backup processM-of-N social recoveryDistributed recovery via Lit networkOrg-level recovery via TurnkeyNo recovery (key loss = fund loss)
Key rotationEnclave re-keyingAdd/remove Safe ownersGenerate new PKPMPC share refreshTransfer to new EOA
ComplianceSOC 2 (Coinbase), KYT screeningOn-chain transparencyEmergingSOC 2 (Turnkey), full audit logsNone
Setup complexityLow (CLI, 2 minutes)High (contract deploy + guards)Medium (PKP + Lit Actions)Medium (API integration)Trivial
Best for security tier$100-$50K$50K+ (with human oversight)$1K-$50K (conditional logic)$1K-$100K (enterprise)Dev/test only

For the full non-security comparison (latency, chain support, gas, DX), see Best Agent Wallet for Prediction Markets.


Production Security Checklist

Use this checklist before deploying any agent wallet to production. Organized by phase: pre-deployment, runtime monitoring, and incident response.

Pre-Deployment (Before First Trade)

  1. Key storage validated — Private key is in MPC shares, TEE enclave, HSM, or multisig. Never in environment variables, config files, or source code.
  2. Per-transaction spending limit configured — Hard cap on single trade size (recommended: 2% of bankroll maximum).
  3. Session spending cap set — Cumulative session limit that halts trading when reached (recommended: 10% of bankroll).
  4. Daily and weekly rolling limits active — Time-window caps that automatically reset (recommended: 20% daily, 50% weekly).
  5. Contract allowlist deployed — Agent can only interact with known, verified prediction market contracts. No wildcard approvals.
  6. Token approvals scoped — Approve exact amounts per transaction. Never approve(MAX_UINT256). Revoke approvals when not in use.
  7. Kill switch tested — Verified that kill switch halts all operations within 5 seconds. Tested all trigger mechanisms (manual, automatic, dead-man’s switch).
  8. Key rotation schedule documented — MPC share refresh every 7 days. Session keys expire in 4-24 hours. Full rotation every 30 days or after any incident.
  9. Dependencies pinned and audited — All Python/Node packages pinned to exact versions in lockfile. No floating version ranges. New dependencies reviewed before adding.
  10. Secrets scanning enabled — Pre-commit hooks that block commits containing private keys, API keys, or mnemonics. Tools: trufflehog, gitleaks, detect-secrets.
  11. Agent runs in isolated container — Docker container with minimal permissions, no host network access, read-only filesystem where possible.
  12. Backup and recovery tested — Verified that wallet can be recovered from backup. Tested the full recovery flow at least once.

Runtime Monitoring (During Operation)

  1. Transaction logging active — Every trade attempt (approved and rejected) logged with timestamp, amount, contract, market ID, and outcome.
  2. Anomaly detection running — Alert on: trades outside normal size range, unusual market targets, burst of rapid trades, session cap approaching.
  3. Heartbeat monitoring live — External service verifies agent is alive and healthy every 60 seconds. Dead-man’s switch triggers if heartbeat stops.
  4. Spending dashboard accessible — Real-time view of session spending, daily totals, weekly totals, and remaining limits. Accessible to on-call team.
  5. Wallet balance alerts configured — Alert when wallet balance drops below 20% of starting balance. Alert on any balance change not correlated with a logged trade.

Incident Response (When Things Go Wrong)

  1. Kill switch accessible by on-call — At least two team members can trigger the kill switch at any time. Kill switch does not require agent cooperation (external mechanism).
  2. Incident runbook documented — Step-by-step process: trigger kill switch, cancel pending orders, revoke token approvals, rotate keys, assess damage, notify stakeholders.
  3. Post-incident key rotation mandatory — After any security incident, rotate all keys (MPC share refresh, new session keys, new API credentials) before resuming operations.

Key Rotation Best Practices

Key rotation limits the blast radius of a compromise. Even if a key is silently extracted, rotation ensures it becomes useless after a fixed window.

Wallet TypeRotation MethodRecommended FrequencyAddress Changes?
MPC (Turnkey/Fireblocks)Key share refreshEvery 7 daysNo
Session keys (ERC-4337)Issue new session keyEvery 4-24 hoursNo (same master wallet)
Safe multisigAdd new owner, remove oldEvery 30 daysNo
TEE/Enclave (Coinbase)Enclave re-keying via APIEvery 30 daysDepends on implementation
Raw EOATransfer funds to new addressEvery 7-14 daysYes (new address each time)

Critical rotation triggers (rotate immediately, regardless of schedule):

  • Any security incident, confirmed or suspected
  • Team member with key access leaves the organization
  • Dependency vulnerability disclosed that could affect key storage
  • Agent exhibits unexplained behavior or unauthorized transaction attempts
  • Third-party provider (Turnkey, Coinbase, etc.) discloses a breach

Common Mistakes

These are the errors we see most often in production agent wallet deployments.

1. Storing private keys in environment variables. Environment variables are readable by any process in the same container, appear in crash dumps, and are often logged by orchestration tools. Use MPC, TEE, or HSM instead.

2. Using approve(MAX_UINT256) for token spending. This gives the approved contract unlimited permission to spend your tokens forever. If that contract is compromised, or if the agent approves the wrong contract, all tokens are at risk. Approve exact amounts per transaction.

3. Relying solely on application-layer spending limits. If your spending limits are implemented only in Python code and the code has a bug, the limits are useless. Layer your controls: protocol-level (MPC policy, Safe guard, session key constraints) plus application-level (SpendingGuard class).

4. No kill switch or untested kill switch. Building a kill switch is the easy part. Testing it under realistic conditions — confirming it actually halts all operations within seconds — is where teams fail. Test your kill switch monthly.

5. Skipping key rotation. “We’ll rotate later” turns into never. Automate key rotation on a schedule. MPC share refresh is painless and should happen weekly.

6. Giving the LLM direct access to signing. The LLM should output a structured decision. A separate, non-LLM execution layer should validate that decision against spending controls and only then sign and submit the transaction. See Security Best Practices for the architectural pattern.

7. No monitoring or alerting. An agent can drain its wallet in minutes. If you don’t have real-time balance monitoring and anomaly alerting, you won’t know until it’s too late.


Frequently Asked Questions

How do I prevent my agent from draining its wallet?

Implement layered spending controls: per-transaction limits (reject any single trade above a threshold), session spending caps (halt all trading when cumulative spend hits a ceiling), daily rolling limits (time-window based caps that reset automatically), and contract allowlists (only interact with known Polymarket or Kalshi contracts). Combine these in the SpendingGuard class shown above. Add an emergency kill switch that can freeze all operations immediately. For maximum security, enforce limits at the protocol level (MPC policy engine, Safe transaction guards, or session key constraints) — not just in application code.

What is the safest wallet architecture for an AI agent?

For most prediction market agents, MPC or TEE wallets provide the strongest security. MPC splits the private key across multiple parties so no single compromise exposes the full key. TEE wallets like Coinbase Agentic Wallets isolate keys inside hardware enclaves. For high-value treasuries above $50K, Safe multisig with transaction guards adds human-in-the-loop approval. The right choice depends on your threat model, latency requirements, and operational complexity tolerance.

How do session keys work for spending limits?

Session keys are temporary, scoped cryptographic keys issued by an ERC-4337 smart contract wallet. You define constraints when creating the session key: maximum spend per transaction, allowed contract addresses, permitted function selectors, and an expiry timestamp. The agent signs transactions with the session key, and the smart contract wallet validates each transaction against the session’s constraints before executing. When the session expires, the key becomes useless automatically.

Can prompt injection drain an agent wallet?

Yes, if the wallet lacks protocol-level spending controls. A prompt injection attack manipulates an LLM-powered agent into executing unauthorized transactions — transferring funds to an attacker address, placing maximum-size trades on manipulated markets, or approving unlimited token spending. The defense is architectural separation: the LLM outputs structured decisions, and a non-LLM execution layer with hard-coded spending limits validates and executes those decisions. See Security Best Practices for detailed prompt injection defenses.

What spending limits should I set for a prediction market bot?

Start conservative and widen as you build confidence. Recommended starting limits: per-transaction maximum of 2% of total bankroll, session cap of 10% of bankroll, daily rolling limit of 20% of bankroll, weekly rolling limit of 50% of bankroll. For a $5,000 bankroll: $100 max per trade, $500 per session, $1,000 per day, $2,500 per week. Arbitrage bots need tighter per-trade limits with higher daily caps. Sentiment bots can have larger per-trade limits with lower daily caps.

How do I implement a kill switch?

A kill switch is a boolean flag checked before every transaction attempt. When active, the agent refuses to sign any new transactions and cancels pending orders. Implement multiple trigger mechanisms for redundancy: a local file flag, a Redis key (for low-latency distributed checks), and a dead-man’s switch (the agent must send a heartbeat every N minutes or it auto-halts). The kill switch must be triggerable externally — it cannot depend on the agent itself being functional. See the KillSwitch class implementation above for complete code.

What is key rotation and how often should agent wallets rotate?

Key rotation replaces the active signing key with a new one, limiting the window of exposure if a key is compromised. For MPC wallets, rotation means generating new key shares without changing the wallet address (Turnkey and Fireblocks support this natively). For session keys, rotation means issuing a new session key with a fresh expiry. Recommended schedule: MPC share refresh every 7 days, session keys every 4-24 hours, full key rotation every 30 days, and immediately after any security incident or team change.


See Also


Guide updated March 2026. Not financial or legal advice. Built for builders.