An agent should never receive a generic sendTransaction tool.
That interface gives a probabilistic system the same authority as your application backend. A valid tool call can still target the wrong cluster, invoke an unexpected program, transfer too much SOL, or retry a transaction you already paid for.
The safer pattern is smaller: let the model propose an intent, then let deterministic code decide whether that intent may become a transaction.
Checked against the Solana and AI SDK documentation on July 22, 2026.
Kill the default transaction tool
The default implementation usually looks like this:
execute: async ({ serializedTransaction }) => {
return rpc.sendTransaction(serializedTransaction);
}It is compact, but the model now controls the payload. Prompt rules are not an authorization boundary. A tool description is not a spending limit.
Keep transaction construction behind code you own. Give the model a narrow intent such as “transfer this many lamports to this address,” then validate the intent against policy before building, simulating, approving, signing, and broadcasting.
By the end you will have
- A typed intent instead of an arbitrary transaction payload
- A deterministic policy gate for cluster, programs, value, and retries
- A simulation receipt that can be inspected before approval
- A clear separation between proposal, signing, and broadcast
- A failure checklist for production agent tools
The control path
Treat the model as a proposer, not a signer:
model proposes intent
↓
schema validates shape
↓
policy authorizes effect
↓
application builds transaction
↓
RPC simulates transaction
↓
human or policy approves receipt
↓
isolated signer signs
↓
broadcaster sends and confirmsEvery boundary has one job. Schema validation answers “is this shaped correctly?” Policy answers “is this allowed?” Simulation answers “what would happen now?” Approval answers “do we accept that effect?”
Magnet: Solana Intent Policy
Start with an intent small enough to audit. This example permits one native SOL transfer on devnet and rejects everything else.
import { z } from "zod";
export const transferIntentSchema = z.object({
intentId: z.string().uuid(),
cluster: z.literal("devnet"),
recipient: z.string().min(32).max(44),
lamports: z.number().int().positive().max(10_000_000),
reason: z.string().min(8).max(160),
});
type TransferIntent = z.infer<typeof transferIntentSchema>;
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
export function authorizeTransfer(
input: unknown,
seenIntentIds: ReadonlySet<string>,
) {
const intent = transferIntentSchema.parse(input);
if (seenIntentIds.has(intent.intentId)) {
throw new Error("Duplicate intent");
}
return {
intent,
allowedProgramIds: [SYSTEM_PROGRAM],
requiresApproval: true,
} as const;
}You should see: malformed calls rejected before transaction construction, duplicate intent IDs refused, and every accepted proposal pinned to devnet, one program, and a 0.01 SOL ceiling.
This is the minimum boundary, not the whole security model. In production, store consumed intent IDs durably and bind the policy to the authenticated user, wallet, environment, and current risk limits.
Wire it into an AI tool
The current AI SDK tool() helper accepts an inputSchema, and needsApproval can force approval before execution. Use both, but keep authorizeTransfer() independent so you can test it without a model or SDK.
import { tool } from "ai";
export const proposeSolTransfer = tool({
description: "Propose a small devnet SOL transfer",
inputSchema: transferIntentSchema,
strict: true,
needsApproval: true,
execute: async (input) => {
const decision = authorizeTransfer(input, await loadUsedIntentIds());
return buildAndSimulateTransfer(decision);
},
});Strict tool calling improves input reliability when the provider supports it. It does not replace your policy. The SDK documentation also notes that tools with an execute function otherwise run automatically, which is exactly why approval belongs in the definition for a write-capable tool.
Return a receipt, not a signature
The first execution should stop after simulation and return a compact receipt:
type SimulationReceipt = {
intentId: string;
cluster: "devnet" | "mainnet-beta";
feePayer: string;
programIds: string[];
balanceChanges: Array<{ account: string; lamports: number }>;
estimatedFeeLamports: number;
unitsConsumed: number | null;
recentBlockhash: string;
lastValidBlockHeight: number;
logs: string[];
error: string | null;
};Approval should be tied to the receipt hash and intent ID. If the transaction is rebuilt with a new recipient, amount, program, or instruction set, approval expires. If the recent blockhash expires, simulate again and create a new receipt instead of silently rebuilding under the old approval.
Solana transactions are atomic and currently capped at 1,232 bytes. Fees are charged even when execution fails. The current compute-budget documentation recommends simulating to measure compute use, then adding a 10% safety margin rather than requesting the maximum. That makes simulation useful for both safety and fee control.
Keep the signer boring
The agent process should not hold a seed phrase or an unrestricted mainnet key.
Put the signer behind a boundary that accepts only an approved, immutable transaction message. It should verify the same cluster, fee payer, program allowlist, spend ceiling, and receipt hash immediately before signing. Log public metadata and signatures, never private key material.
For the first production version, prefer:
- one purpose-specific wallet with a low balance
- one allowed action per tool
- server-side signing outside model context
- durable intent and signature records
- explicit daily and per-transaction limits
- an emergency switch that disables broadcast
Make retries idempotent
An agent will retry when a response is slow. Your chain may already have accepted the first request.
Use intentId as an idempotency key. Before a new send:
- Check whether the intent already has a confirmed signature.
- If it has a pending signature, query its status instead of rebuilding.
- Rebuild only after the blockhash is no longer valid and policy still permits it.
- Never let a generic “try again” create a second economic action.
Confirmation is part of the tool result. “Submitted” is not “completed.” If this path becomes latency-sensitive or you need stronger request observability, measure your RPC path and compare production infrastructure such as RPC Edge against your actual workload. Infrastructure cannot compensate for a missing intent boundary, but it can make status and broadcast behavior easier to operate.
Failure modes
| Failure | Why it survives basic validation | Gate |
|---|---|---|
| Wrong cluster | The address is valid on every cluster | Literal cluster plus environment assertion |
| Unexpected program | Transaction bytes can encode any instruction | Build from intent and verify program IDs |
| Duplicate payment | The retry is individually valid | Durable intent ID and signature lookup |
| Approval drift | A rebuilt transaction differs from the preview | Bind approval to receipt and message hash |
| Fee waste | Failed transactions still pay fees | Simulate, cap retries, right-size compute |
| Secret leakage | Logs and traces accept strings | Isolate signer and redact at the boundary |
| Model bypass | Prompt says the action is urgent | Authorization lives outside the prompt |
When not to automate the send
- The action can move more value than you can immediately lose.
- The allowed program behavior is upgradeable and you do not monitor upgrades.
- You cannot reconstruct who approved the effect and what they saw.
- The operation is rare enough that a manual wallet flow is cheaper and safer.
- You are responding to an incident and have not tested the agent path under failure.
Read-only tools can stay automatic. Simulation can usually stay automatic. Signing and broadcast earn automation only after the receipt and retry paths are boring.
Source notes and related paths
- Solana core concepts for transaction structure and limits
- Solana fee structure for fees on failed transactions
- Solana compute budget for simulation-based compute sizing
- AI SDK tool reference for schema and approval controls
- Solana RPC Failover Without Duplicate Transactions for safe rebroadcast and replacement
- Solana + AI Agents: A Beginner Stack for the broader learning path
- Monitor Solana Transactions Without Missing Events for confirmation and recovery architecture
Bottom line
The model may decide what to propose. It should not decide what your wallet is allowed to sign.
Your next action: copy the Solana Intent Policy, replace the recipient and value rules with your real limits, and make your current write tool return a simulation receipt before it can reach a signer.