Skip to content

AgentWallet

What you'll learn

  • How the AgentWallet orchestrates the entire transaction lifecycle
  • How to construct a wallet with all required and optional components
  • The 10-step execute pipeline that every transaction passes through
  • How to handle the four possible transaction outcomes (confirmed, denied, pending, failed)
  • How to expose wallet operations to AI agents via the MCP (Model Context Protocol) server

Overview

The AgentWallet is the central hub of the Kova SDK -- think of it as a bank account with built-in spending rules and a full audit trail. Just like a corporate bank account has daily transfer limits, requires manager approval for large payments, and keeps a ledger of every transaction, the AgentWallet enforces policies, requests human approval when needed, and logs every action your AI agent takes.

You give your AI agent an AgentWallet instead of raw access to a blockchain. The wallet makes sure the agent can only do what you have explicitly allowed.

The AgentWallet class is the main entry point for the SDK. It wires together the policy engine, signer, chain adapter, and store, and exposes a high-level API for executing transactions and checking balances.

When would I use this?

  • You are building an AI agent that needs to send payments, swap tokens, or interact with a blockchain on its own -- but you want guardrails so it cannot drain your funds.
  • You want to expose wallet operations to AI agents via the Model Context Protocol (MCP), so any AI model can invoke wallet operations safely.
  • You need a full audit trail of every transaction attempt (approved or denied) for compliance or debugging.

Configuration

typescript
// Import the main AgentWallet class from the kova SDK.
// AgentWallet is the top-level object that orchestrates transaction execution,
// policy enforcement, signing, and chain interaction.
import { AgentWallet } from "@kova-sdk/wallet";

// Import the TypeScript type for the wallet configuration object.
// This type defines the shape of the options you pass when constructing an AgentWallet.
import type { AgentWalletConfig } from "@kova-sdk/wallet";

AgentWalletConfig

FieldTypeRequiredDescription
signerSignerYesThe signer responsible for signing transactions (like the private key that authorizes payments from your bank account)
chainChainAdapterYesThe chain adapter for blockchain interactions (the connection to the blockchain network -- similar to an API client for your bank)
policyPolicy | PolicyEngineYesThe policy or policy engine for evaluating transaction intents (your spending rules and approval workflows)
storeStoreYesThe store for persisting spending counters and tx logs (the database that remembers how much has been spent today)
approvalApprovalChannelNoOptional approval channel for human-in-the-loop (e.g., CallbackApprovalChannel or WebhookApprovalChannel)
loggerAuditLoggerNoOptional audit logger. If not provided, one is created using the store
circuitBreakerPartial<CircuitBreakerConfig> | { dangerouslyDisable: true } | falseNoCircuit breaker config. Use { dangerouslyDisable: true } to disable (passing false is deprecated). Default: { threshold: 5, cooldownMs: 300000 }
onAuditFailureAuditFailureCallbackNoCallback invoked when an audit log write fails
idempotencyTtlnumberNoTTL for idempotency cache entries in seconds. Determines how long a duplicate intent ID returns a cached result instead of re-executing. Default: 86400 (24 hours)
idempotencyHmacKeystring | BufferNoHMAC-SHA256 key for verifying authenticity of cached idempotency entries. Prevents an attacker with store write access from forging cached "confirmed" results. Generate with crypto.randomBytes(32).toString('hex') and store securely -- not in the database
storePrefixstringNoKey prefix for multi-wallet store isolation. When multiple AgentWallet instances share a Store backend, each must use a unique prefix to prevent cross-wallet interference in spending limits, rate counters, and audit logs
mutexTimeoutMsnumberNoTimeout in milliseconds for acquiring the execute mutex. If the mutex cannot be acquired within this period, the call fails instead of blocking indefinitely. Default: 30000 (30 seconds)
enabledToolsReadonlySet<string>NoSet of tool names enabled for handleToolCall(). Defaults to all 10 read-only tools (wallet_get_balance, wallet_get_transaction_history, wallet_get_supported_tokens, wallet_get_address, wallet_estimate_fee, wallet_get_token_price, wallet_get_transaction_status, wallet_get_policy, wallet_get_spending_remaining, wallet_get_all_balances). Write tools require explicit opt-in. See Tool Access Control
dangerouslyAllowAutoHmacKeybooleanNoWhen true, allows auto-generation of the idempotency HMAC key. Auto-generated keys do not survive process restarts, risking duplicate transactions. For production, provide a persistent idempotencyHmacKey instead
authTokenstringNoCapability token for caller authentication. When set, execute() and handleToolCall() require this token; calls without it are rejected with AUTH_FAILED
agentIdstringNoWallet-level agent identifier. Used for circuit breaker isolation and per-agent rate limiting instead of the self-reported agentId in intent metadata, which is untrusted
verboseErrorsbooleanNoWhen true, policy denial messages include full details (amounts, limits, rule names) without sanitization. Useful for dashboards and development. Do not enable for untrusted agent callers -- detailed denials enable policy reconnaissance. Default: false
dangerouslyAllowVerboseErrorsInProductionbooleanNoWhen true, allows verboseErrors to be used in production environments. By default, verboseErrors is blocked in production to prevent policy reconnaissance
dangerouslyDisableAuthbooleanNoWhen true, disables authentication token checks on execute() and handleToolCall(). Only use for development or testing
storeTimeoutMsnumberNoTimeout in milliseconds for individual store operations. Prevents the wallet from hanging on unresponsive store backends
strictAdvisoryLockbooleanNoWhen true, enforces strict advisory locking semantics on store operations
requireProgramAllowlistForCustombooleanNoWhen true, requires a program allowlist to be configured when using custom intent types

What is a circuit breaker?

A circuit breaker is a safety mechanism borrowed from electrical engineering. If too many transactions are denied in a row (suggesting a bug or runaway loop), the circuit breaker "trips" and blocks ALL transactions for a cooldown period. This prevents a misbehaving agent from hammering the system with doomed requests.

Basic Construction

typescript
// Import all the core components needed to assemble a minimal AgentWallet.
// AgentWallet: the main class that ties everything together.
// Policy: the fluent builder for declaring policy constraints.
// MemoryStore: an in-memory implementation of the Store interface (good for development/testing; data is lost on restart).
// LocalSigner: signs transactions using a local Solana keypair (NOT recommended for production with real funds).
// SolanaAdapter: the chain adapter that knows how to build, send, and confirm Solana transactions.
import {
  AgentWallet,
  Policy,
  MemoryStore,
  LocalSigner,
  SolanaAdapter,
} from "@kova-sdk/wallet";

// Import the Keypair class from the Solana web3.js library.
// Keypair represents a Solana public/private key pair used for signing transactions.
import { Keypair } from "@solana/web3.js";

// Create an in-memory store instance.
// The store is used by the policy engine to persist spending counters, rate-limit counters,
// and by the audit logger to save transaction history. MemoryStore is ephemeral — all data
// is lost when the process exits.
const store = new MemoryStore();

// Create a local signer from a randomly generated Solana keypair.
// The signer is responsible for cryptographically signing transactions before they are broadcast.
// Keypair.generate() creates a brand-new random keypair (useful for testing on devnet).
const signer = new LocalSigner(Keypair.generate());

// Create a Solana chain adapter pointing at the Solana devnet RPC endpoint.
// The chain adapter handles chain-specific operations: building transactions from intents,
// broadcasting signed transactions, confirming them on-chain, and fetching balances.
const chain = new SolanaAdapter({ rpcUrl: "https://api.devnet.solana.com" });

// Create a Policy using the fluent builder with a per-transaction cap of 1 SOL.
// The Policy builder provides a declarative, chainable API for defining constraints.
const policy = Policy.create("basic-policy")
  .spendingLimit({ perTransaction: { amount: "1", token: "SOL" } })
  .build();

// Construct the AgentWallet by wiring together all the components.
// - signer: signs transactions
// - chain: builds and broadcasts transactions on Solana
// - policy: the Policy that enforces spending/rate/allowlist rules
// - store: shared persistence layer for counters and audit logs
const wallet = new AgentWallet({
  signer,
  chain,
  policy,
  store,
  dangerouslyDisableAuth: true,
});

Key terms in the code above

  • Signer: The component that holds your private key and "signs" (cryptographically authorizes) transactions -- like entering your PIN to approve a bank transfer.
  • RPC endpoint: A URL that lets your code talk to the blockchain network. "RPC" stands for Remote Procedure Call -- think of it as the API base URL for the blockchain.
  • Devnet: A test version of the Solana blockchain where tokens have no real value. Use it for development and testing.
  • SOL: The native currency of the Solana blockchain, like ETH on Ethereum or USD in traditional finance.
  • Lamports: The smallest unit of SOL. 1 SOL = 1,000,000,000 lamports. Similar to how 1 dollar = 100 cents, but with more decimal places.

Construction with All Options

typescript
// Import all components needed for a fully-featured production wallet setup.
// SqliteStore: a persistent store backed by SQLite (survives process restarts).
// RateLimitRule: limits how many transactions can execute per minute/hour.
// ApprovalGateRule: requires human approval for transactions above a dollar threshold.
// CallbackApprovalChannel: sends approval requests via callbacks and waits for human response.
// AuditLogger: records every transaction attempt (allowed or denied) in a tamper-evident hash chain.
import {
  AgentWallet,
  PolicyEngine,
  SqliteStore,
  LocalSigner,
  SolanaAdapter,
  SpendingLimitRule,
  RateLimitRule,
  ApprovalGateRule,
  CallbackApprovalChannel,
  AuditLogger,
} from "@kova-sdk/wallet";
import { Keypair } from "@solana/web3.js";

// Create a persistent SQLite-backed store.
// Unlike MemoryStore, SqliteStore writes data to disk so spending counters, rate-limit counters,
// and audit logs survive process restarts. The path specifies where the database file is created.
const store = new SqliteStore({ path: "./wallet.db" });

// Create a signer from an existing secret key (e.g., loaded from a secure vault or env variable).
// In production, you should NEVER hard-code secret keys. Use HSMs, MPC, or secure enclaves instead.
const signer = new LocalSigner(Keypair.fromSecretKey(mySecretKey));

// Create a Solana chain adapter pointing at mainnet-beta with "finalized" commitment.
// "finalized" means the SDK waits until the transaction is confirmed by a supermajority of validators,
// providing the strongest guarantee that the transaction will not be rolled back.
const chain = new SolanaAdapter({
  rpcUrl: "https://api.mainnet-beta.solana.com",
  commitment: "finalized",
});

// Set up a callback-based approval channel for human-in-the-loop approval.
// You provide two callbacks: one to notify a human, one to wait for their decision.
// This works with any notification mechanism (Telegram, Slack, email, SMS, etc.).
const approval = new CallbackApprovalChannel({
  name: "my-approval",
  onApprovalRequest: async (request) => {
    await notifyApprover(request); // Send notification via your preferred channel
  },
  waitForDecision: async (request) => {
    return pollForResponse(request.id); // Wait for human's response
  },
});

// Build the PolicyEngine with multiple rules, ordered from cheapest to most expensive.
// Rule evaluation stops at the first DENY, so placing cheap rules first avoids unnecessary work.
const engine = new PolicyEngine(
  [
    // Rule 1: Rate limit — at most 20 transactions per hour.
    // This is the cheapest rule (simple counter lookup), so it runs first.
    new RateLimitRule({ maxTransactionsPerHour: 20 }),

    // Rule 2: Spending limit — at most 50 SOL per rolling 24-hour period.
    // This reads and updates spending counters in the store.
    new SpendingLimitRule({ daily: { amount: "50", token: "SOL" } }),

    // Rule 3: Approval gate — transactions above 10 SOL require human approval.
    // The timeout of 600,000ms (10 minutes) means the approval request expires after 10 minutes
    // if the human does not respond, resulting in an automatic DENY.
    new ApprovalGateRule({
      above: { amount: "10", token: "SOL" },
      timeout: 600_000,
    }),
  ],
  store,
  approval, // Pass the approval channel so the ApprovalGateRule can send requests
);

// Construct the wallet with all options configured.
const wallet = new AgentWallet({
  signer,
  chain,
  policy: engine,
  store,
  approval, // Also pass approval to the wallet for policy introspection
  dangerouslyDisableAuth: true,

  // Circuit breaker: after 3 consecutive policy denials, block ALL transactions for 10 minutes.
  // This protects against runaway agents that keep retrying denied transactions in a tight loop.
  circuitBreaker: { threshold: 3, cooldownMs: 600_000 },

  // Callback invoked whenever an audit log write fails.
  // Use this to alert your operations team. After too many consecutive failures,
  // the audit circuit breaker opens and ALL transactions are blocked.
  onAuditFailure: (error, count) => {
    console.error(`Audit write failed (${count} consecutive):`, error);
  },
});

Production checklist

Before deploying with real funds, make sure you:

  1. Use SqliteStore (or a custom persistent store) instead of MemoryStore -- otherwise spending limits reset every time your process restarts.
  2. Never hard-code private keys. Load them from environment variables, a secrets manager, or an HSM.
  3. Use "finalized" commitment on mainnet to avoid acting on transactions that get rolled back.
  4. Set up the onAuditFailure callback to alert your team if the audit trail breaks.

The Execute Pipeline

When you call wallet.execute(intent), the following 10-step pipeline runs. Think of it like an HTTP request passing through a chain of middleware -- each step can approve the request, reject it, or transform it before passing it to the next step.

The entire pipeline is serialized via a mutex (a lock that ensures only one transaction runs at a time) to prevent TOCTOU race conditions. TOCTOU (Time-Of-Check-to-Time-Of-Use) is a class of bug where two transactions check the same limit simultaneously and both slip through -- for example, two concurrent $8 transfers could each see a $10 limit as not exceeded, resulting in $16 total spending. The mutex prevents this by processing transactions one at a time.

StepNameDescription
1ValidateVerify intent structure: type, chain, params, ID format
2NormalizeAssign UUID and timestamp if not provided
3Idempotency CheckLook up intent ID in store (24h TTL). Return cached result if found
4Audit Circuit CheckIf audit logger is broken (circuit open), refuse all transactions
5Transaction Circuit BreakerIf too many consecutive denials, refuse with cooldown
6Policy EvaluationRun all rules sequentially. DENY stops immediately. PENDING waits for approval
7Build TransactionChain adapter builds the unsigned transaction
8SignSigner signs the transaction
9BroadcastChain adapter sends to network and waits for confirmation
10Audit Log + CacheRecord audit entry with hash chain, cache result for idempotency

What is idempotency?

Idempotency means "doing the same thing twice produces the same result." If your agent retries a transaction with the same intent ID (for example, after a network timeout), the SDK returns the cached result from the first attempt instead of sending a duplicate payment. This is the same concept used in payment APIs like Stripe.

WARNING

Denied and pending results are not cached for idempotency. Only confirmed and failed results are cached. This means retrying a denied intent after a rate limit expires will re-evaluate the policy rather than returning the stale denial.

Methods

execute(intent)

Execute a transaction intent through the full pipeline. This is the primary method you will use.

A transaction intent is a high-level description of what you want to do (e.g., "send 0.5 SOL to this address") -- like a purchase order that describes what you want to buy, not how the payment is processed. See the Transaction Intents guide for details.

typescript
// Method signature: takes a TransactionIntent and returns a Promise that resolves
// to a TransactionResult. The entire 10-step pipeline runs inside this call.
async execute(intent: TransactionIntent, authToken?: string): Promise<TransactionResult>
typescript
// Execute a transfer intent: send 0.5 SOL to a specific Solana address.
// The intent is a declarative description — the SDK handles building, signing,
// and broadcasting the actual Solana transaction.
const result = await wallet.execute({
  type: "transfer",       // The operation type — "transfer" means sending tokens
  chain: "solana",        // Target blockchain — tells the SDK which chain adapter to use
  params: {
    to: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", // Recipient's Solana wallet address
    amount: "0.5",        // Human-readable amount (the SDK converts to lamports internally)
    token: "SOL",         // Token symbol — "SOL" is the native Solana token
  },
  metadata: {
    reason: "Paying for completed task",  // Human-readable justification (shown in audit logs and approval requests)
    agentId: "agent-01",                  // Identifies which AI agent initiated this transaction
  },
});

// Check the result status and handle each possible outcome.
// The four possible statuses represent the full lifecycle of a transaction attempt.
if (result.status === "confirmed") {
  // Transaction was successfully broadcast and confirmed on the Solana blockchain.
  // result.txId contains the on-chain transaction signature.
  console.log("Transaction confirmed:", result.txId);
} else if (result.status === "denied") {
  // The policy engine rejected this transaction (e.g., spending limit exceeded, rate limit hit).
  // result.error.message explains which rule denied it and why.
  console.log("Denied:", result.error?.message);
} else if (result.status === "pending") {
  // The transaction requires human approval (e.g., ApprovalGateRule triggered).
  // An approval request has been sent via the configured ApprovalChannel and is awaiting a human decision.
  console.log("Awaiting approval:", result.summary);
} else {
  // The transaction was attempted but failed during build, sign, or broadcast.
  // This means the policy allowed it, but something went wrong on-chain.
  console.log("Failed:", result.error?.message);
}

getBalance(token)

Get the wallet's balance for a specific token. A token is any digital currency on the blockchain -- SOL is the native token of Solana, and SPL tokens (like USDC) are additional currencies built on top of Solana (similar to how ERC-20 tokens work on Ethereum).

typescript
// Method signature: takes a token symbol (e.g., "SOL", "USDC") and returns
// a Promise resolving to a TokenBalance object with the amount and metadata.
async getBalance(token: string): Promise<TokenBalance>
typescript
// Query the wallet's SOL balance via the chain adapter.
// The chain adapter fetches the balance from the Solana RPC endpoint.
const balance = await wallet.getBalance("SOL");

// Display the balance amount and token symbol (e.g., "Balance: 5.23 SOL").
console.log(`Balance: ${balance.amount} ${balance.token}`);

// Display the number of decimal places for this token.
// SOL has 9 decimals (1 SOL = 1,000,000,000 lamports).
console.log(`Decimals: ${balance.decimals}`);

// If a USD value is available (provided by a price feed), display it.
// This field is optional and depends on the chain adapter's price feed configuration.
if (balance.usdValue !== undefined) {
  console.log(`USD value: $${balance.usdValue.toFixed(2)}`);
}

getAddress()

Get the wallet's public address. A public address is like a bank account number -- it is safe to share and is what other people use to send you funds.

typescript
// Method signature: returns a Promise resolving to the wallet's public address string.
// This is derived from the signer's public key.
async getAddress(): Promise<string>
typescript
// Retrieve the wallet's public address (the Solana base58-encoded public key).
// This is the address others use to send tokens to this wallet.
const address = await wallet.getAddress();
console.log("Wallet address:", address);

getPolicy()

Get a read-only summary of the current policy constraints. Agents can use this to plan within their limits -- for example, an agent can check its remaining daily budget before deciding whether to attempt a transaction.

typescript
// Method signature: returns a Promise resolving to a PolicySummary object.
// The summary contains a human-readable overview of all configured policy constraints.
async getPolicy(): Promise<PolicySummary>
typescript
// Fetch the current policy summary so the agent can understand its operational limits.
// This is useful for AI agents that want to check their remaining budget or rate limits
// before attempting a transaction.
const summary = await wallet.getPolicy();

// Display each aspect of the policy configuration.
console.log("Policy name:", summary.name);                       // The human-readable policy name
console.log("Spending limits:", summary.spendingLimits);         // Per-tx, daily, weekly, monthly caps
console.log("Allowlisted addresses:", summary.allowlistedAddresses); // Count of allowlisted addresses (number)
console.log("Allowlisted programs:", summary.allowlistedPrograms); // Count of allowlisted programs (number)
console.log("Rate limits:", summary.rateLimits);                 // Max transactions per minute/hour
console.log("Active hours:", summary.activeHours);               // Time windows when transactions are allowed
console.log("Approval required:", summary.approvalRequired);     // Threshold above which human approval is needed
console.log("Circuit breaker:", summary.circuitBreaker);         // Circuit breaker status and configuration

getTransactionHistory(limit?)

Get recent transaction history from the audit log. Useful for dashboards, debugging, or letting your agent review what it has already done.

typescript
// Method signature: takes an optional limit (default 10, max 1000) and optional options,
// and returns a Promise resolving to an array of past TransactionResult objects from the audit log.
async getTransactionHistory(limit?: number, options?: { redactAddresses?: boolean }): Promise<TransactionResult[]>

The limit parameter defaults to 10 and is clamped to the range [1, 1000].

typescript
// Fetch the 20 most recent transactions from the audit log.
// This includes both successful and denied transactions.
const history = await wallet.getTransactionHistory(20);

// Iterate over the transaction history and display a summary line for each.
// Each entry includes the status (confirmed/denied/failed), a human-readable summary,
// and the intent ID for cross-referencing.
for (const tx of history) {
  console.log(`[${tx.status}] ${tx.summary} (${tx.intentId})`);
}

handleToolCall(name, input)

Handle a tool call from an AI agent. This is the bridge between AI model tool calling and the wallet. When an AI model (like Claude or GPT-4) decides it wants to perform a wallet operation, it outputs a tool name and parameters. You pass those directly to handleToolCall(), and it dispatches to the appropriate wallet method. Errors are sanitized to prevent leaking internal details to the AI model.

typescript
// Method signature: takes a tool name string and an input object (key-value pairs from the AI model),
// and returns a ToolCallResult indicating success or failure.
// This is the bridge between AI agent tool calls and the wallet's internal methods.
async handleToolCall(
  name: string,
  input: Record<string, unknown>,
  authToken?: string
): Promise<ToolCallResult>

Supported tool names:

Tool NameMaps To
wallet_transferexecute({ type: "transfer", ... })
wallet_swapexecute({ type: "swap", ... })
wallet_mintexecute({ type: "mint", ... })
wallet_stakeexecute({ type: "stake", ... })
wallet_execute_customexecute({ type: "custom", ... })
wallet_get_balancegetBalance(token)
wallet_get_all_balancesgetBalance() for all tokens
wallet_get_policygetPolicy()
wallet_get_transaction_historygetTransactionHistory(limit)
wallet_get_supported_tokensList supported tokens
wallet_get_addressgetAddress()
wallet_estimate_feeEstimate transaction fees
wallet_get_token_priceGet token price
wallet_get_transaction_statusCheck transaction status
wallet_get_spending_remainingGet remaining spending budget
typescript
// Handle a tool call from an AI agent that wants to transfer SOL.
// The AI model outputs a tool name ("wallet_transfer") and parameters as a flat object.
// handleToolCall maps this to wallet.execute() with the correct intent structure.
const result = await wallet.handleToolCall("wallet_transfer", {
  chain: "solana",                                          // Target chain
  to: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",    // Recipient address
  amount: "0.5",                                            // Amount to send
  token: "SOL",                                             // Token symbol
  reason: "Payment for task",                               // Metadata reason for audit trail
});

// Check whether the tool call succeeded.
// Errors are sanitized — the AI agent only sees a generic message, not internal details.
if (result.success) {
  console.log("Transfer succeeded:", result.data);  // Contains the TransactionResult
} else {
  console.log("Transfer failed:", result.error);    // Sanitized error message
}

Tool Access Control

By default, only read-only tools are enabled for handleToolCall(). This prevents an AI agent from executing transactions unless your code explicitly opts in.

Default Enabled Tools (read-only)

Tool NameDescription
wallet_get_balanceQuery the wallet's token balance
wallet_get_transaction_historyRetrieve recent audit log entries
wallet_get_supported_tokensList supported tokens
wallet_get_addressGet the wallet's public address
wallet_estimate_feeEstimate transaction fees
wallet_get_token_priceGet token price information
wallet_get_transaction_statusCheck a transaction's status
wallet_get_policyView policy summary
wallet_get_spending_remainingGet remaining spending budget
wallet_get_all_balancesGet balances for all tokens

Opt-in Write Tools

To allow the AI agent to execute transactions, list the tools explicitly in enabledTools:

typescript
// Import the main AgentWallet class.
import { AgentWallet } from "@kova-sdk/wallet";

const wallet = new AgentWallet({
  signer,
  chain,
  policy: engine,
  store,
  dangerouslyDisableAuth: true,
  // Explicitly enable transfer and swap tools alongside the defaults.
  // All 10 read-only tools are included by default; only write tools need opt-in.
  enabledTools: new Set([
    // Default read-only tools
    "wallet_get_balance",
    "wallet_get_transaction_history",
    "wallet_get_supported_tokens",
    "wallet_get_address",
    "wallet_estimate_fee",
    "wallet_get_token_price",
    "wallet_get_transaction_status",
    "wallet_get_policy",
    "wallet_get_spending_remaining",
    "wallet_get_all_balances",
    // Opt-in write tools
    "wallet_transfer",
    "wallet_swap",
  ]),
});
Tool NameCategoryDescription
wallet_transferWriteExecute a token transfer
wallet_swapWriteExecute a token swap via Jupiter
wallet_mintWriteMint an NFT
wallet_stakeWriteStake tokens to a validator
wallet_execute_customWrite (dangerous)Execute an arbitrary custom transaction

WARNING

wallet_execute_custom is the most dangerous tool because it allows arbitrary program interactions. Only enable it when your policy rules are strict enough to prevent abuse.

Built-in Rate Limits

The SDK enforces hardcoded rate limits on tool calls regardless of your policy configuration:

CategoryLimitPurpose
Write tools30 per minute per walletPrevents a runaway agent from draining funds even with misconfigured policy rules
Read tools60 per minute per walletPrevents reconnaissance abuse and resource exhaustion
Per-agent writes15 per minute per agentEnsures fair sharing when multiple agents share a wallet

These limits are floors -- they cannot be disabled via configuration. Your policy rules provide additional, more granular limits on top of these.

TransactionResult

Every execute() call returns a TransactionResult. This object tells you exactly what happened -- whether the transaction succeeded, was blocked by policy, is waiting for approval, or failed for a technical reason.

TransactionResult is a discriminated union on the status field. The available fields depend on the status:

typescript
// When status is "confirmed": has txId, NO error
interface ConfirmedResult {
  status: "confirmed";
  txId: string;              // On-chain transaction ID / signature
  summary: string;
  intentId: string;
  timestamp: number;
  chainData?: Record<string, unknown>;
  warnings?: string[];
}

// When status is "denied" or "failed": may have error, NO txId
interface DeniedOrFailedResult {
  status: "denied" | "failed";
  error?: TransactionError;  // Structured error details
  summary: string;
  intentId: string;
  timestamp: number;
  chainData?: Record<string, unknown>;
  warnings?: string[];
}

// When status is "pending": NO txId, NO error
interface PendingResult {
  status: "pending";
  summary: string;
  intentId: string;
  timestamp: number;
  chainData?: Record<string, unknown>;
  warnings?: string[];
}

type TransactionResult = ConfirmedResult | DeniedOrFailedResult | PendingResult;

Status Values

StatusMeaningWhat to do
confirmedTransaction was broadcast and confirmed on-chainSuccess -- the funds have moved. Store txId for your records.
deniedPolicy engine rejected the transactionCheck error.message to understand which rule blocked it and why.
pendingTransaction requires human approval (approval request sent)Wait for the human to approve or reject via the approval channel.
failedTransaction was attempted but failed (build, sign, or broadcast error)Check error.message. The policy allowed it, but something went wrong technically.

Error Codes

The error.code field in TransactionResult uses one of the following TransactionErrorCode values:

CodeDescription
VALIDATION_FAILEDIntent structure is invalid (missing fields, bad types, invalid amount)
POLICY_DENIEDA policy rule denied the transaction
SPENDING_LIMIT_EXCEEDEDTransaction exceeds a spending limit
ADDRESS_NOT_ALLOWEDTarget address is not in the allowlist or is denylisted
PROGRAM_NOT_ALLOWEDProgram ID is not in the allowlist or is denylisted
RATE_LIMIT_EXCEEDEDToo many transactions in the time window
OUTSIDE_TIME_WINDOWTransaction attempted outside active hours
APPROVAL_REJECTEDHuman approver rejected the transaction
APPROVAL_TIMEOUTApproval request timed out
INSUFFICIENT_BALANCEWallet does not have enough funds
SIMULATION_FAILEDTransaction simulation failed before broadcast
TRANSACTION_FAILEDOn-chain transaction failed
SIGNER_ERRORSigner failed to sign the transaction
CHAIN_ERRORChain adapter encountered an error
STORE_ERRORStore operation failed (e.g., audit logging is broken)
CIRCUIT_BREAKER_OPENCircuit breaker is blocking transactions after consecutive denials
WALLET_DRAININGWallet is shutting down via drain(); no new transactions accepted
AUTH_FAILEDInvalid or missing authentication token
UNKNOWN_ERRORUnexpected error

DANGER

When STORE_ERROR is returned with "audit logging circuit breaker is open", all transactions are blocked until audit logging is restored. This is a safety feature -- the SDK refuses to process transactions without a functioning audit trail.

Common Mistakes

1. Using MemoryStore in production.MemoryStore loses all data when your process restarts. This means spending limits reset to zero, rate-limit counters disappear, and your audit trail is gone. Always use SqliteStore or a custom persistent Store implementation in production.

2. Not handling all four result statuses. Many developers only check for confirmed and ignore the rest. Always handle denied, pending, and failed -- your agent needs to know when a transaction was blocked so it can adjust its behavior, not just retry blindly.

3. Retrying denied transactions in a tight loop. If a transaction is denied (e.g., spending limit exceeded), retrying immediately will hit the same limit and trigger the circuit breaker. Check the denial reason and wait for the limit to reset, or reduce the amount.

4. Sharing a wallet across multiple agents without PrefixedStore. If multiple agents share the same AgentWallet instance and store, their spending counters and rate limits will be combined. Use PrefixedStore to isolate each agent's state, or create separate wallet instances for each agent.

Quick Reference

MethodReturnsDescription
execute(intent, authToken?)Promise<TransactionResult>Run an intent through the full 10-step pipeline (validate, policy check, build, sign, broadcast)
getBalance(token)Promise<TokenBalance>Check the wallet's balance for a specific token (e.g., "SOL", "USDC")
getAddress()Promise<string>Get the wallet's public address (the address others send funds to)
getPolicy()Promise<PolicySummary>Get a read-only summary of all policy constraints (limits, allowlists, rate limits)
getTransactionHistory(limit?, options?)Promise<TransactionResult[]>Fetch recent transactions from the audit log (default: 10, max: 1000)
handleToolCall(name, input, authToken?)Promise<ToolCallResult>Dispatch an AI model's tool call to the appropriate wallet method

Released under the MIT License.