Blockchain & On-Chain Agents Basics
8 examples to get you started with on-chain agent tools - 5 basic and 3 intermediate.
You will pin Solana devnet, wrap a tiny SOL transfer as a tool, confirm the signature, and keep the agent from treating chain I/O like a harmless HTTP call.
Prerequisites
- Node.js 20+ recommended
- A disposable devnet keypair (never reuse mainnet keys in tutorials)
- Devnet SOL from a public faucet for that keypair
- Familiarity with agent tool schemas (JSON parameters in, structured result out)
mkdir onchain-agent-basics && cd onchain-agent-basics
npm init -y
npm install @solana/web3.js bs58 zod
export SOLANA_CLUSTER_URL="https://api.devnet.solana.com"
# DEVNET only. Do not put mainnet keys here.
export AGENT_PAYER_SECRET_BASE58="..."Generate a throwaway keypair when needed:
node -e "const {Keypair}=require('@solana/web3.js'); const k=Keypair.generate(); console.log(require('bs58').encode(k.secretKey)); console.log(k.publicKey.toBase58());"Basic Examples
1. Pin the Cluster Explicitly
Never rely on a silent default RPC.
import { Connection, clusterApiUrl } from "@solana/web3.js";
const cluster = process.env.SOLANA_CLUSTER ?? "devnet";
if (cluster !== "devnet") {
throw new Error(`Refusing to run basics against cluster=${cluster}`);
}
const connection = new Connection(
process.env.SOLANA_CLUSTER_URL ?? clusterApiUrl("devnet"),
"confirmed",
);
const version = await connection.getVersion();
console.log("rpc ok", version);- Basics and CI should hard-fail on non-devnet.
- Pass commitment (
confirmedhere) so later confirms match your policy. - Log the RPC URL host in traces (without secrets).
Related: Testing On-Chain Agent Flows on Devnet Before Mainnet
2. Load a Payer Keypair for Tool Runtime Only
The agent model proposes args. The runtime holds the key.
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";
function loadPayer(): Keypair {
const secret = process.env.AGENT_PAYER_SECRET_BASE58;
if (!secret) throw new Error("AGENT_PAYER_SECRET_BASE58 missing");
return Keypair.fromSecretKey(bs58.decode(secret));
}
const payer = loadPayer();
console.log("payer", payer.publicKey.toBase58());- Keep secrets out of prompts, tool results, and logs.
- Use a low-balance devnet wallet dedicated to the agent.
- Prefer custody products before mainnet (Fireblocks guide).
3. Define a Strict transferSOL Tool Schema
Validate before you build a transaction.
import { z } from "zod";
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
const TransferSolArgs = z.object({
to: z.string().refine((s) => {
try {
new PublicKey(s);
return true;
} catch {
return false;
}
}, "invalid pubkey"),
sol: z.number().positive().max(0.1), // hard tutorial cap
intentId: z.string().min(8).max(128),
});
type TransferSolArgs = z.infer<typeof TransferSolArgs>;
// Example agent-facing description (framework-specific wiring omitted):
// name: transferSOL
// description: Send a small amount of SOL on the pinned cluster. Always pass a unique intentId.- Caps and pubkey checks belong in code, not only in the prompt.
intentIdis your idempotency handle for retries.- Max
0.1SOL is pedagogical; production caps come from risk policy.
Related: Why On-Chain Agents Need Different Guardrails Than Other Tools
4. Implement the Tool: Build, Sign, Confirm
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
const seenIntents = new Set<string>(); // demo only; use durable storage in real agents
export async function transferSOLTool(input: unknown, connection: Connection, payer: Keypair) {
const args = TransferSolArgs.parse(input);
if (seenIntents.has(args.intentId)) {
return { status: "duplicate", intentId: args.intentId };
}
const tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: new PublicKey(args.to),
lamports: Math.round(args.sol * LAMPORTS_PER_SOL),
}),
);
const signature = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
});
seenIntents.add(args.intentId);
return {
status: "confirmed",
intentId: args.intentId,
signature,
cluster: "devnet",
to: args.to,
sol: args.sol,
};
}- Return the signature and intentId so the agent can cite them.
- Mark the intent only after confirm (or use a pending→confirmed state machine).
- In-memory
Setis for learning; see idempotency.
5. Drive the Tool from a Minimal Agent Loop Stub
// Pseudocode: replace with your framework's tool registry (LangGraph, OpenAI Agents SDK, etc.)
async function handleUserTurn(userText: string) {
// 1) Model chooses tool + args (not shown)
const modelArgs = {
to: process.env.DEMO_RECIPIENT!, // known devnet address
sol: 0.001,
intentId: `demo-${Date.now()}`,
};
// 2) Runtime executes; model never sees the secret key
const result = await transferSOLTool(modelArgs, connection, payer);
return `On-chain result: ${JSON.stringify(result)}`;
}
console.log(await handleUserTurn("Tip the demo wallet 0.001 SOL on devnet"));- The model should receive tool results, not raw key material.
- Prefer structured results over free-text "success" strings.
- Persist
intentId↔signaturefor support and audits.
Intermediate Examples
6. Check Balance Before Spending
export async function getBalanceSol(connection: Connection, who: PublicKey) {
const lamports = await connection.getBalance(who, "confirmed");
return lamports / LAMPORTS_PER_SOL;
}
const bal = await getBalanceSol(connection, payer.publicKey);
if (bal < 0.01) {
throw new Error(`Fund devnet payer first; balance=${bal}`);
}- Fail fast with a clear tool error the agent can surface.
- Separate
getBalanceas its own read tool when useful. - Do not scrape faucet pages through the agent on a schedule without rate limits.
7. Wait for a Specific Signature (Observe Path)
export async function observeSignature(connection: Connection, signature: string) {
const latest = await connection.getLatestBlockhash("confirmed");
const conf = await connection.confirmTransaction(
{ signature, ...latest },
"confirmed",
);
if (conf.value.err) {
return { status: "failed", signature, err: conf.value.err };
}
return { status: "confirmed", signature };
}- Useful when broadcast and confirm are split across tools.
- Agent loops should not continue payment-dependent steps until observe succeeds.
- Pair with Solana Pay verification for commerce flows (Solana Pay).
8. Refuse Mainnet and Over-Cap Arguments
function assertSafeSpendEnv() {
if ((process.env.SOLANA_CLUSTER ?? "devnet") !== "devnet") {
throw new Error("basics runner is devnet-only");
}
if (process.env.ALLOW_MAINNET_AGENT === "1") {
throw new Error("ALLOW_MAINNET_AGENT is not valid in basics examples");
}
}
function assertArgs(args: TransferSolArgs) {
if (args.sol > 0.1) throw new Error("amount exceeds basics cap");
}- Defense in depth: env checks + schema caps + policy service later.
- Make "unsafe mode" explicit and loud when you eventually need it.
- Log denials so prompt-injection attempts are visible.
Related: Building transferSOL and createTransaction Agent Tools
Related
- Why On-Chain Agents Need Different Guardrails Than Other Tools - why this is not a normal tool
- Setting Up @solana/web3.js and @solana/spl-token for Agent Tools - library scaffolding
- Building transferSOL and createTransaction Agent Tools - fuller tool recipe
- Idempotency and Double-Spend Protection for On-Chain Agent Actions - durable retry safety
- Blockchain & On-Chain Agents Best Practices - production habits
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.