PayKit is an open-source SDK for registering autonomous AI agents on Solana, enforcing spend limits, and recording payment history immutably onchain. It is the accountability infrastructure layer for the autonomous AI economy.
PayKit is not a payment processor. It is an accountability layer. The SDK wraps the PayKit Anchor program deployed on Solana Devnet and exposes a simple JavaScript/TypeScript API that any developer can integrate in minutes.
Install the PayKit SDK via npm or yarn:
npm install @paykit/sdk
Or with yarn:
yarn add @paykit/sdk
The SDK requires Node.js 18+ and a Solana keypair file to sign transactions.
Get up and running in under 5 minutes:
const { createClient } = require("@paykit/sdk");
// 1. Create a client with your keypair
const client = createClient("/path/to/keypair.json", "devnet");
// 2. Register an AI agent with a 1 SOL spend limit
const { agentPDA, tx } = await client.registerAgent(
"my-agent",
1_000_000_000 // 1 SOL in lamports
);
console.log("Agent registered:", agentPDA.toBase58());
// 3. Record a payment made by the agent
await client.recordPayment(
"my-agent",
1_000_000, // 0.001 SOL
recipientPublicKey,
"API call payment"
);
// 4. Agent pays another agent autonomously
await client.agentToAgentPayment(
"my-agent",
"other-agent",
250_000,
"Data analysis service"
);
// 5. Fetch agent state
const agent = await client.fetchAgent("my-agent");
console.log("Total spent:", agent.totalSpent.toString());
console.log("Days remaining:", (await client.checkAgentExpiry("my-agent")).daysRemaining);Registers a new AI agent onchain. Creates a PDA (Program Derived Address) account that stores the agent's identity, spend limit, payment history, and expiration date. Each agent is uniquely identified by the combination of its owner wallet and name. Agents expire after 365 days by default and can be renewed with renewAgent.
const { tx, agentPDA } = await client.registerAgent(name, spendLimitLamports);{
tx: string, // Transaction signature
agentPDA: PublicKey // Onchain address of the agent account
}const { tx, agentPDA } = await client.registerAgent(
"trading-agent-01",
5_000_000_000 // 5 SOL
);
console.log("Agent PDA:", agentPDA.toBase58());Records a payment made by an agent onchain. Increments the agent's total_spent counter and payment_count. The contract enforces that total_spent never exceeds spend_limit, and that the agent does not exceed 10% of its total budget in any 24-hour period.
const { tx } = await client.recordPayment(agentName, amountLamports, recipient, memo);{ tx: string }await client.recordPayment(
"trading-agent-01",
500_000,
new PublicKey("recipient..."),
"OpenAI API call - GPT-4"
);Records a direct payment from one registered agent to another. Both agents must be registered, active, and not expired. The sender's spend limit and daily limit are enforced by the contract. The receiver's payment_count is incremented atomically.
const { tx } = await client.agentToAgentPayment(senderName, receiverName, amountLamports, service);await client.agentToAgentPayment( "agent-alpha", "agent-beta", 250_000, "Market data analysis - Q4 2026" );
Sends payments from one agent to multiple agents in a single Solana transaction. All payments succeed or all fail atomically — there is no partial execution. This is the key primitive for orchestrator patterns where one agent delegates work and pays multiple specialized agents.
const { tx, count } = await client.batchPayment(senderName, payments);{
tx: string, // Transaction signature
count: number // Number of payments executed
}// Orchestrator pays 3 specialized agents in 1 transaction
const { tx, count } = await client.batchPayment("orchestrator", [
{ receiverName: "agent-researcher", amountLamports: 100_000, service: "Web research" },
{ receiverName: "agent-writer", amountLamports: 150_000, service: "Content generation" },
{ receiverName: "agent-reviewer", amountLamports: 50_000, service: "Quality review" },
]);
console.log(`${count} payments confirmed in TX: ${tx}`);Updates the spend limit of an existing agent. Only the owner wallet can call this. Does not reset total_spent or daily_spent.
await client.updateSpendLimit(agentName, newLimitLamports);
await client.updateSpendLimit("trading-agent-01", 10_000_000_000); // 10 SOLPermanently deactivates an agent. Once deactivated, the agent cannot make or receive payments. This action is irreversible. The agent account remains onchain as an immutable audit record.
await client.deactivateAgent(agentName);
await client.deactivateAgent("trading-agent-01");Extends an agent's expiration date. If the agent is already expired, the extension starts from the current time. If the agent is still active, the extension is added to the current expiration date. Only agents created with the current contract version support renewal — legacy agents (created before the expires_at field was added) cannot be renewed.
const { tx } = await client.renewAgent(agentName, extensionSeconds);// Renew for 1 year
await client.renewAgent("my-agent", 31_536_000);
// Renew for 6 months
await client.renewAgent("my-agent", 15_768_000);Returns the expiration status of an agent. Useful for checking before executing a payment, or for building renewal UIs.
const expiry = await client.checkAgentExpiry(agentName, ownerPubkey?);
{
expired: boolean, // true if agent has expired
expiresAt: Date, // JavaScript Date object of expiration
daysRemaining: number // 0 if expired, otherwise days until expiration
}const expiry = await client.checkAgentExpiry("my-agent");
if (expiry.expired) {
console.log("Agent has expired — renewing...");
await client.renewAgent("my-agent", 31_536_000);
} else {
console.log(`Agent expires in ${expiry.daysRemaining} days`);
}Estimates the Solana network fee for a payment transaction before executing it. Useful for agents that need to account for gas costs in their budget calculations.
const { fee, feeSOL } = await client.estimateFee(agentName, amountLamports, type);{
fee: number, // Fee in lamports
feeSOL: string // Fee in SOL (formatted string)
}const { fee, feeSOL } = await client.estimateFee("my-agent", 1_000_000, "record");
console.log(`Estimated fee: ${feeSOL} SOL`); // ~0.000005000 SOLFetches the current state of a single agent account from Solana. Any public key can be inspected — this call does not require the caller to be the agent's owner.
const agent = await client.fetchAgent(agentName, ownerPubkey?);
{
owner: PublicKey,
name: string,
spendLimit: BN,
totalSpent: BN,
paymentCount: BN,
isActive: boolean,
lastPaymentAt: BN,
dailySpent: BN,
dailyResetAt: BN,
expiresAt: BN
}const agent = await client.fetchAgent("my-agent");
const remaining = agent.spendLimit.toNumber() - agent.totalSpent.toNumber();
console.log("Remaining budget:", remaining / 1e9, "SOL");
console.log("Active:", agent.isActive);Fetches all agent accounts owned by the current wallet using a memcmp filter on the owner field.
const agents = await client.fetchAllAgents();
const agents = await client.fetchAllAgents();
for (const agent of agents) {
console.log(`${agent.name}: ${agent.totalSpent.toString()} lamports spent`);
}Fetches recent transaction history from the PayKit program by reading onchain transaction logs. Categorizes each transaction by instruction type.
const history = await client.getPaymentHistory(limit?);
Array<{
type: "agent_to_agent" | "record_payment" | "register_agent",
time: string, // ISO 8601 timestamp
tx: string // Transaction signature
}>const history = await client.getPaymentHistory(10);
for (const entry of history) {
console.log(`[${entry.type}] ${entry.time} — ${entry.tx}`);
}The PayKit smart contract returns structured error codes when a transaction fails. All errors are thrown as AnchorError.
try {
await client.recordPayment("my-agent", 1_000_000, recipient, "payment");
} catch (e) {
if (e.message.includes("SpendLimitExceeded")) {
console.log("Agent has reached its spend limit");
} else if (e.message.includes("DailyLimitExceeded")) {
console.log("Agent has reached its daily limit — resets in 24h");
} else if (e.message.includes("AgentInactive")) {
console.log("Agent is deactivated");
} else if (e.message.includes("AgentExpired")) {
console.log("Agent has expired — renew with renewAgent()");
}
}expires_at field cannot be deserialized against the new struct. These agents appear as LEGACY in the dashboard and cannot be renewed or used with the current contract version.This happens because Solana account data is fixed at creation time. When the contract struct gains a new field, existing accounts on-chain don't have that field — so the new deserializer fails to read them.
Legacy agents show EXPIRES: LEGACY in the dashboard. In the SDK, their expiresAt field returns a value of 0 or throws a deserialization error.
// Step 1 — Note the legacy agent's name and spend limit
const legacy = await client.fetchAllAgents();
const legacyAgent = legacy.find(a => a.name === "my-old-agent");
const originalLimit = legacyAgent.spendLimit.toNumber();
// Step 2 — Register a new agent with the same name
// (The old PDA still exists on-chain but is effectively replaced)
const { agentPDA } = await client.registerAgent(
"my-old-agent", // same name
originalLimit // same spend limit
);
// Step 3 — Verify the new agent is active and has an expiration
const expiry = await client.checkAgentExpiry("my-old-agent");
console.log("New agent expires in:", expiry.daysRemaining, "days");
// Output: New agent expires in: 365 daysPayKit integrates naturally with LangChain agents as a custom tool. Register PayKit methods as tools that your LangChain agent can call autonomously to record payments and manage budgets.
npm install langchain @langchain/openai @paykit/sdk
const { createClient } = require("@paykit/sdk");
const { DynamicStructuredTool } = require("langchain/tools");
const { z } = require("zod");
const paykit = createClient("/path/to/keypair.json", "devnet");
// Tool: record a payment onchain
const recordPaymentTool = new DynamicStructuredTool({
name: "record_payment",
description: "Record a payment made by the AI agent onchain with an immutable audit trail.",
schema: z.object({
agentName: z.string().describe("Name of the registered PayKit agent"),
amountSOL: z.number().describe("Payment amount in SOL"),
recipientAddress: z.string().describe("Recipient wallet address"),
memo: z.string().describe("Description of what the payment is for"),
}),
func: async ({ agentName, amountSOL, recipientAddress, memo }) => {
const { PublicKey } = require("@solana/web3.js");
const { tx } = await paykit.recordPayment(
agentName,
Math.floor(amountSOL * 1_000_000_000),
new PublicKey(recipientAddress),
memo
);
return `Payment recorded onchain. TX: ${tx}`;
},
});
// Tool: check agent budget
const checkBudgetTool = new DynamicStructuredTool({
name: "check_agent_budget",
description: "Check how much budget the agent has remaining before taking action.",
schema: z.object({
agentName: z.string().describe("Name of the registered PayKit agent"),
}),
func: async ({ agentName }) => {
const agent = await paykit.fetchAgent(agentName);
const remaining = agent.spendLimit.toNumber() - agent.totalSpent.toNumber();
const expiry = await paykit.checkAgentExpiry(agentName);
return JSON.stringify({
remainingSOL: (remaining / 1e9).toFixed(4),
paymentCount: agent.paymentCount.toString(),
daysUntilExpiry: expiry.daysRemaining,
});
},
});
// Tool: pay another agent
const payAgentTool = new DynamicStructuredTool({
name: "pay_agent",
description: "Pay another registered PayKit agent for a completed service.",
schema: z.object({
senderAgent: z.string().describe("Name of the paying agent"),
receiverAgent: z.string().describe("Name of the receiving agent"),
amountSOL: z.number().describe("Payment amount in SOL"),
service: z.string().describe("Description of the service rendered"),
}),
func: async ({ senderAgent, receiverAgent, amountSOL, service }) => {
const { tx } = await paykit.agentToAgentPayment(
senderAgent,
receiverAgent,
Math.floor(amountSOL * 1_000_000_000),
service
);
return `Agent payment confirmed. TX: ${tx}`;
},
});const { ChatOpenAI } = require("@langchain/openai");
const { AgentExecutor, createOpenAIFunctionsAgent } = require("langchain/agents");
const { ChatPromptTemplate } = require("@langchain/core/prompts");
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const tools = [recordPaymentTool, checkBudgetTool, payAgentTool];
const prompt = ChatPromptTemplate.fromMessages([
["system", `You are an autonomous AI agent with a PayKit wallet on Solana.
Your agent name is "langchain-agent-01".
Always check your budget before making payments.
Record all significant actions onchain for accountability.`],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]);
const agent = await createOpenAIFunctionsAgent({ llm, tools, prompt });
const executor = new AgentExecutor({ agent, tools, verbose: true });
// The agent now autonomously manages payments
const result = await executor.invoke({
input: "Check my remaining budget, then pay agent-researcher 0.001 SOL for completing the market analysis task."
});
console.log(result.output);PayKit works with CrewAI to give your crew members autonomous payment capabilities. Each crew member can be assigned a PayKit agent identity and pay other crew members for completed tasks.
pip install crewai npm install @paykit/sdk # PayKit runs as a sidecar Node.js service
// paykit-service.js — Run this alongside your CrewAI crew
const express = require("express");
const { createClient } = require("@paykit/sdk");
const app = express();
app.use(express.json());
const paykit = createClient("/path/to/keypair.json", "devnet");
app.post("/register", async (req, res) => {
const { name, spendLimitSOL } = req.body;
const result = await paykit.registerAgent(name, spendLimitSOL * 1e9);
res.json({ pda: result.agentPDA.toBase58(), tx: result.tx });
});
app.post("/pay", async (req, res) => {
const { sender, receiver, amountSOL, service } = req.body;
const result = await paykit.agentToAgentPayment(
sender, receiver, Math.floor(amountSOL * 1e9), service
);
res.json({ tx: result.tx });
});
app.post("/batch", async (req, res) => {
const { sender, payments } = req.body;
const result = await paykit.batchPayment(sender, payments.map(p => ({
...p, amountLamports: Math.floor(p.amountSOL * 1e9)
})));
res.json({ tx: result.tx, count: result.count });
});
app.get("/agent/:name", async (req, res) => {
const agent = await paykit.fetchAgent(req.params.name);
const expiry = await paykit.checkAgentExpiry(req.params.name);
res.json({
spendLimit: agent.spendLimit.toString(),
totalSpent: agent.totalSpent.toString(),
paymentCount: agent.paymentCount.toString(),
isActive: agent.isActive,
daysRemaining: expiry.daysRemaining,
});
});
app.listen(3333, () => console.log("PayKit sidecar running on port 3333"));# paykit_tools.py
import requests
from crewai.tools import BaseTool
PAYKIT_URL = "http://localhost:3333"
class RecordPaymentTool(BaseTool):
name: str = "record_payment"
description: str = (
"Record a payment onchain via PayKit for accountability. "
"Use this after completing any paid task."
)
def _run(self, agent_name: str, amount_sol: float,
recipient: str, memo: str) -> str:
res = requests.post(f"{PAYKIT_URL}/pay", json={
"sender": agent_name,
"receiver": recipient,
"amountSOL": amount_sol,
"service": memo,
})
data = res.json()
return f"Payment recorded onchain. TX: {data['tx']}"
class CheckBudgetTool(BaseTool):
name: str = "check_budget"
description: str = (
"Check the remaining budget and status of a PayKit agent "
"before executing any payment."
)
def _run(self, agent_name: str) -> str:
res = requests.get(f"{PAYKIT_URL}/agent/{agent_name}")
data = res.json()
spent = int(data["totalSpent"]) / 1e9
limit = int(data["spendLimit"]) / 1e9
remaining = limit - spent
return (
f"Agent: {agent_name} | "
f"Spent: {spent:.4f} SOL | "
f"Remaining: {remaining:.4f} SOL | "
f"Payments: {data['paymentCount']} | "
f"Days until expiry: {data['daysRemaining']}"
)# crew.py
from crewai import Agent, Task, Crew
from paykit_tools import RecordPaymentTool, CheckBudgetTool
record_payment = RecordPaymentTool()
check_budget = CheckBudgetTool()
researcher = Agent(
role="Research Specialist",
goal="Gather and analyze market data",
backstory="Expert in data research with a PayKit agent identity on Solana.",
tools=[check_budget, record_payment],
verbose=True,
)
writer = Agent(
role="Content Writer",
goal="Produce high-quality written content",
backstory="Professional writer compensated via PayKit for each deliverable.",
tools=[check_budget, record_payment],
verbose=True,
)
research_task = Task(
description="Research the current state of AI agent payment protocols.",
agent=researcher,
expected_output="A concise market analysis with key findings.",
)
writing_task = Task(
description="Write a blog post based on the research findings.",
agent=writer,
expected_output="A 500-word blog post ready for publication.",
)
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
verbose=True,
)
result = crew.kickoff()
print(result)The PayKit program is written in Rust using the Anchor framework and deployed on Solana Devnet.
pub struct AgentAccount {
pub owner: Pubkey, // Wallet that controls the agent
pub name: String, // Unique identifier (max 32 chars)
pub spend_limit: u64, // Maximum spend in lamports
pub total_spent: u64, // Cumulative spend in lamports
pub payment_count: u64, // Number of payments recorded
pub is_active: bool, // Active/inactive status
pub bump: u8, // PDA bump seed
pub last_payment_at: i64, // Unix timestamp of last payment
pub daily_spent: u64, // Amount spent today in lamports
pub daily_reset_at: i64, // Unix timestamp of last daily reset
pub expires_at: i64, // Unix timestamp of expiration (0 = legacy)
}PayKit is structured as three independent layers that can be used together or separately:
┌─────────────────────────────────────────────────────┐ │ AI Agent / LangChain / CrewAI / Custom │ ├─────────────────────────────────────────────────────┤ │ PayKit SDK (Node.js) │ │ registerAgent · recordPayment · batchPayment │ │ agentToAgent · renewAgent · estimateFee · expiry │ ├─────────────────────────────────────────────────────┤ │ PayKit Program (Solana / Anchor) │ │ PDAs · Spend Limits · Rate Limiting · Expiration │ ├─────────────────────────────────────────────────────┤ │ Solana Blockchain │ │ ~400ms finality · ~$0.00025/tx │ └─────────────────────────────────────────────────────┘
Each agent account is a Program Derived Address derived from the program ID, the string "agent", the owner's public key, and the agent name:
const [agentPDA] = PublicKey.findProgramAddressSync(
[
Buffer.from("agent"),
ownerPublicKey.toBuffer(),
Buffer.from(agentName),
],
PROGRAM_ID
);