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:
- Prompt injection via Moltbook posts, market descriptions, or API responses that override agent reasoning
- Key extraction from environment variables, logs, memory dumps, or unencrypted storage
- Dependency compromise through malicious packages in the supply chain (typosquatting, maintainer takeover)
- Oracle manipulation where attackers influence the data feeds an agent relies on for trading decisions
- Transaction replay where signed but unsubmitted transactions are captured and rebroadcast
- API credential theft from exposed config files, Git history, or shared environments
Threat/Impact/Mitigation Matrix
| Threat | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Prompt injection via market data | Agent places attacker-directed trades, draining bankroll | High | Separate LLM reasoning from execution layer; structured output validation; input sanitization |
| Private key extraction | Total loss of all wallet funds | Medium | MPC key splitting, TEE enclave isolation, HSM storage; never store raw keys in environment variables |
| Unlimited token approval | Attacker drains wallet via approved contract | High | Set exact approval amounts per transaction; never use approve(MAX_UINT256); revoke stale approvals |
| Dependency supply chain attack | Malicious code executes in agent runtime, exfiltrates keys or signs transactions | Medium | Pin dependency versions; use lockfiles; audit new dependencies; run in sandboxed containers |
| Oracle/data feed manipulation | Agent makes trades based on false data, losing money to the manipulator | Medium | Use multiple independent data sources; set outlier detection thresholds; reject data that deviates >3 standard deviations from median |
| Session key over-permissioning | Compromised session key allows larger transactions or more contracts than intended | Medium | Scope session keys to minimum required permissions; set short expiry windows; allowlist only necessary contract addresses |
| Kill switch bypass | Agent continues trading after an incident because kill switch is unreachable | Low | Multiple kill switch channels (Redis + DB + remote config); dead-man’s switch pattern; heartbeat monitoring |
| Transaction replay attack | Signed transaction is captured and resubmitted on same or different chain | Low | Use 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?
| Feature | Coinbase Agentic | Safe | Lit Protocol | Turnkey/Privy | Raw EOA |
|---|---|---|---|---|---|
| Key isolation | TEE enclave (hardware) | Smart contract (on-chain) | Distributed PKP (network) | MPC shares (off-chain) | None (single key in memory) |
| Spending caps | Session + per-tx (built-in) | Transaction guards (modular) | Lit Actions (programmable) | Policy engine (dashboard) | Code-level only (fragile) |
| Kill switch | API-level halt | Owner revocation on-chain | Revoke PKP permissions | Dashboard toggle | Code-level only |
| Audit trail | Coinbase logs + on-chain | Full on-chain history | On-chain + Lit node logs | Enterprise dashboard + API logs | On-chain only |
| Key recovery | Coinbase backup process | M-of-N social recovery | Distributed recovery via Lit network | Org-level recovery via Turnkey | No recovery (key loss = fund loss) |
| Key rotation | Enclave re-keying | Add/remove Safe owners | Generate new PKP | MPC share refresh | Transfer to new EOA |
| Compliance | SOC 2 (Coinbase), KYT screening | On-chain transparency | Emerging | SOC 2 (Turnkey), full audit logs | None |
| Setup complexity | Low (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)
- Key storage validated — Private key is in MPC shares, TEE enclave, HSM, or multisig. Never in environment variables, config files, or source code.
- Per-transaction spending limit configured — Hard cap on single trade size (recommended: 2% of bankroll maximum).
- Session spending cap set — Cumulative session limit that halts trading when reached (recommended: 10% of bankroll).
- Daily and weekly rolling limits active — Time-window caps that automatically reset (recommended: 20% daily, 50% weekly).
- Contract allowlist deployed — Agent can only interact with known, verified prediction market contracts. No wildcard approvals.
- Token approvals scoped — Approve exact amounts per transaction. Never
approve(MAX_UINT256). Revoke approvals when not in use. - Kill switch tested — Verified that kill switch halts all operations within 5 seconds. Tested all trigger mechanisms (manual, automatic, dead-man’s switch).
- 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.
- Dependencies pinned and audited — All Python/Node packages pinned to exact versions in lockfile. No floating version ranges. New dependencies reviewed before adding.
- Secrets scanning enabled — Pre-commit hooks that block commits containing private keys, API keys, or mnemonics. Tools: trufflehog, gitleaks, detect-secrets.
- Agent runs in isolated container — Docker container with minimal permissions, no host network access, read-only filesystem where possible.
- Backup and recovery tested — Verified that wallet can be recovered from backup. Tested the full recovery flow at least once.
Runtime Monitoring (During Operation)
- Transaction logging active — Every trade attempt (approved and rejected) logged with timestamp, amount, contract, market ID, and outcome.
- Anomaly detection running — Alert on: trades outside normal size range, unusual market targets, burst of rapid trades, session cap approaching.
- Heartbeat monitoring live — External service verifies agent is alive and healthy every 60 seconds. Dead-man’s switch triggers if heartbeat stops.
- Spending dashboard accessible — Real-time view of session spending, daily totals, weekly totals, and remaining limits. Accessible to on-call team.
- 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)
- 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).
- Incident runbook documented — Step-by-step process: trigger kill switch, cancel pending orders, revoke token approvals, rotate keys, assess damage, notify stakeholders.
- 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 Type | Rotation Method | Recommended Frequency | Address Changes? |
|---|---|---|---|
| MPC (Turnkey/Fireblocks) | Key share refresh | Every 7 days | No |
| Session keys (ERC-4337) | Issue new session key | Every 4-24 hours | No (same master wallet) |
| Safe multisig | Add new owner, remove old | Every 30 days | No |
| TEE/Enclave (Coinbase) | Enclave re-keying via API | Every 30 days | Depends on implementation |
| Raw EOA | Transfer funds to new address | Every 7-14 days | Yes (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
- Best Agent Wallet for Prediction Markets — Head-to-head wallet comparison with scoring
- Legal & Liability Guide for Agent Wallets — Who is liable when an agent loses money
- Agentic Payments Protocols — x402, AP2, and machine-to-machine payments
- Agent Wallet Comparison — General wallet architecture overview
- Security Best Practices for Agent Betting — Prompt injection, API keys, operational security
- The Agent Betting Stack — Full stack architecture and how wallets fit in
- Agent Betting Glossary — 130+ terms defined
- Coinbase Agentic Wallets Guide — Complete Coinbase developer guide
Guide updated March 2026. Not financial or legal advice. Built for builders.