Setting Up @solana/web3.js and @solana/spl-token for Agent Tools
Scaffold a small TypeScript module that agents call for Solana reads and writes, with cluster pinning, typed connections, and SPL Token helpers ready for tool wrappers.
Search across all documentation pages
Scaffold a small TypeScript module that agents call for Solana reads and writes, with cluster pinning, typed connections, and SPL Token helpers ready for tool wrappers.
Install @solana/web3.js and @solana/spl-token, centralize Connection + payer loading, and expose narrow functions your agent framework can register as tools - without leaking secrets into model context.
devnet first) and construct one shared Connection.npm init -y
npm install @solana/web3.js @solana/spl-token bs58 zod
npm install -D typescript tsx @types/node
npx tsc --init// src/solana/client.ts
import {
Connection,
Keypair,
PublicKey,
clusterApiUrl,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import bs58 from "bs58";
export type ClusterName = "devnet" | "testnet" | "mainnet-beta";
export function requiredCluster(): ClusterName {
const c = (process.env.SOLANA_CLUSTER ?? "devnet") as ClusterName;
if (!["devnet", "testnet", "mainnet-beta"].includes(c)) {
throw new Error(`Unknown SOLANA_CLUSTER=${c}`);
}
return c;
}
export function makeConnection(commitment: "confirmed" | "finalized" = "confirmed") {
const cluster = requiredCluster();
const url =
process.env.SOLANA_CLUSTER_URL ??
clusterApiUrl(cluster === "mainnet-beta" ? "mainnet-beta" : cluster);
return { cluster, connection: new Connection(url, commitment) };
}
export function loadKeypairFromEnv(name = "AGENT_PAYER_SECRET_BASE58"): Keypair {
const raw = process.env[name];
if (!raw) throw new Error(`${name} is required for signing tools`);
return Keypair.fromSecretKey(bs58.decode(raw));
}
export async function solBalance(connection: Connection, owner: PublicKey) {
const lamports = await connection.getBalance(owner, "confirmed");
return { lamports, sol: lamports / LAMPORTS_PER_SOL };
}
export async function splTokenBalance(
connection: Connection,
owner: PublicKey,
mint: PublicKey,
) {
const ata = getAssociatedTokenAddressSync(mint, owner);
try {
const acct = await getAccount(connection, ata);
return {
ata: ata.toBase58(),
amount: acct.amount.toString(),
mint: mint.toBase58(),
programId: TOKEN_PROGRAM_ID.toBase58(),
};
} catch {
return { ata: ata.toBase58(), amount: "0", mint: mint.toBase58(), missing: true };
}
}// src/solana/tools.ts - framework-agnostic tool handlers
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
import { makeConnection, loadKeypairFromEnv, solBalance, splTokenBalance } from "./client";
const GetBalanceIn = z.object({
address: z.string().min(32).max(64),
});
export async function toolGetSolBalance(raw: unknown) {
const { address } = GetBalanceIn.parse(raw);
const { cluster, connection } = makeConnection();
const bal = await solBalance(connection, new PublicKey(address));
return { cluster, address, ...bal };
}
const GetTokenBalIn = z.object({
owner: z.string(),
mint: z.string(),
});
export async function toolGetSplBalance(raw: unknown) {
const args = GetTokenBalIn.parse(raw);
const { cluster, connection } = makeConnection();
const result = await splTokenBalance(
connection,
new PublicKey(args.owner),
new PublicKey(args.mint),
);
return { cluster, owner: args.owner, ...result };
}
// Signing tools should import loadKeypairFromEnv only when the tool is enabled.
export function signingEnabled() {
return process.env.AGENT_CHAIN_WRITES === "1";
}Wire toolGetSolBalance / toolGetSplBalance into LangGraph, OpenAI Agents SDK, CrewAI, or your custom registry as pure functions with JSON-schema mirrors of the Zod objects.
| Package | Role in agent tools |
|---|---|
@solana/web3.js | RPC Connection, keys, transactions, system transfers, confirms |
@solana/spl-token | ATAs, mint/account layout helpers, token transfer instructions |
Agents that only move SOL still benefit from web3.js alone.
Agents that price, escrow, or pay in SPL tokens need both.
Package majors evolve (including the web3.js 2.x / Kit line). Pin versions in lockfiles and verify at build against current Solana docs when upgrading.
agent loop (model)
-> tool schemas (no secrets)
-> tool handlers (validate args)
-> solana/client (connection, optional signer)
-> RPC / custody APIDo not pass a global Keypair into every tool.
Read tools should not import signing material at all.
Public devnet/mainnet URLs are fine for learning and light tests.
Production agents usually need a dedicated RPC provider, retries with jitter, and explicit commitment for confirms.
Standardize on one commitment for "business success" (confirmed or finalized) so every tool agrees when the agent may continue.
getSolBalance, getSplTokenBalance, transferSOL.intentId.cluster, ids, and signatures so traces stay searchable.getAccount throws when missing; handle and optionally expose createAta as a separate gated tool.@solana/web3.js and @solana/spl-token must stay compatible; upgrade together in CI.| Approach | Pros | Cons |
|---|---|---|
@solana/web3.js + @solana/spl-token (this recipe) | Mature examples, wide docs | API surface is large; versions move |
@solana/kit / web3.js 2.x style stack | Modern composable APIs | Different patterns; rewrite cost (verify at build) |
| Wallet-adapter only (user signs) | Agent never holds keys | Needs interactive user; not server autonomy |
| Custody API (Fireblocks, etc.) | Policy + HSM/MPC | Vendor integration; not free |
Python (solana-py / solders) | Fits Python agent codebases | Ecosystem examples skew TS; keep parity tests |
One connection factory per process is enough for most bots. Create fresh clients if you multi-tenant different RPC keys or clusters in one process.
No. Add it when you touch mints, ATAs, or token transfers. Many agents start SOL-only and add SPL later.
Have the tool accept human units only after you fix decimals in code, or accept raw integer amounts only. Mixing both in one field invites 10^6 errors.
Yes, after devnet proof, by changing env - not by forking code. Keep write enable flags and custody stricter on mainnet.
Add a programs/ module for IDL-driven builders (Anchor clients, etc.). Still validate args and keep signing behind the same policy gate.
It is convenient for demos. Production agents should use provider URLs with auth, monitoring, and SLOs.
Return structured { status: "error", code, message } for expected failures (bad pubkey, insufficient funds). Throw only for programmer errors. That keeps the loop recoverable.
They will bite agent loops that poll aggressively. Back off, cache read results briefly, and use webhooks or subscription APIs where appropriate.
Related: Blockchain & On-Chain Agents Basics
Related: Building transferSOL and createTransaction Agent Tools
Related: Key Management for Agent-Controlled Wallets: Fireblocks and Alternatives
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026