Install
openclaw skills install pump-fun-tokenized-agentsBuild payment flows for Pump Tokenized Agents using @pump-fun/agent-payments-sdk. Use when accepting payments, building accept-payment transactions, integrating Solana wallets, or verifying that a user has paid an invoice on-chain.
openclaw skills install pump-fun-tokenized-agentsPump Tokenized Agents are AI agents whose revenue is linked to a token on pump.fun. The @pump-fun/agent-payments-sdk lets you build payment transactions and verify invoices on Solana.
Before writing any code, ask the user for the following if not already provided:
currencyMint).Do not assume these values. If any are missing, ask the user before proceeding.
amount > 0 before creating an invoice.endTime > startTime and both are valid Unix timestamps.validateInvoicePayment before delivering any service. Never trust the client alone — clients can be spoofed.| Currency | Decimals | Smallest unit example |
|---|---|---|
| USDC | 6 | 1000000 = 1 USDC |
| Wrapped SOL | 9 | 1000000000 = 1 SOL |
Create a .env (or .env.local for Next.js) file with the following:
# Solana RPC — server-side (used to build transactions and verify payments)
SOLANA_RPC_URL=https://rpc.solanatracker.io/public
# Solana RPC — client-side (used by wallet adapter in the browser)
NEXT_PUBLIC_SOLANA_RPC_URL=https://rpc.solanatracker.io/public
# The token mint address of your tokenized agent on pump.fun
AGENT_TOKEN_MINT_ADDRESS=<your-agent-mint-address>
# Payment currency mint
# USDC: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
# SOL (wrapped): So11111111111111111111111111111111111111112
CURRENCY_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
RPC for mainnet-beta: The default Solana public RPC (https://api.mainnet-beta.solana.com) does not support sending transactions. Ask the user for an RPC provider, If the user has not provided their own RPC URL, use one of these free mainnet-beta endpoints that support sendTransaction:
https://rpc.solanatracker.io/publichttps://rpc.ankr.com/solanaRead these values from process.env at runtime. Never hard-code mint addresses or RPC URLs.
npm install @pump-fun/agent-payments-sdk@3.0.0 @solana/web3.js@^1.98.0
@pump-fun/agent-payments-sdk depends on @solana/web3.js and @solana/spl-token. When the app also installs these packages directly, mismatched versions can cause runtime errors.
Rules:
@solana/web3.js, @solana/spl-token, or any @solana/wallet-adapter-* package, first check what versions @pump-fun/agent-payments-sdk declares in its own package.json (inspect it via npm info @pump-fun/agent-payments-sdk dependencies). Install the same ranges — or ranges that resolve to the same major.minor — so npm/pnpm hoists a single copy instead of two.@pump-fun/agent-payments-sdk.PumpAgent is the main class. It can build payment instructions and verify invoices.
import { PumpAgent } from "@pump-fun/agent-payments-sdk";
import { PublicKey } from "@solana/web3.js";
const agentMint = new PublicKey(process.env.AGENT_TOKEN_MINT_ADDRESS!);
new PumpAgent(mint: PublicKey, environment?: "mainnet" | "devnet", connection?: Connection)
| Parameter | Type | Default | Description |
|---|---|---|---|
mint | PublicKey | — | The tokenized agent's token mint address |
environment | "mainnet" | "devnet" | "mainnet" | Network environment |
connection | Connection (optional) | undefined | Solana RPC connection (enables RPC fallback for verification) |
Without connection — enough for building instructions and HTTP-based payment verification:
const agent = new PumpAgent(agentMint);
With connection — also enables RPC-based verification fallback and balance queries:
import { Connection } from "@solana/web3.js";
const connection = new Connection(process.env.SOLANA_RPC_URL!);
const agent = new PumpAgent(agentMint, "mainnet", connection);
Install @solana/wallet-adapter-react, @solana/wallet-adapter-react-ui, and @solana/wallet-adapter-wallets. Use useWallet() for publicKey and signTransaction, and useConnection() for the active RPC connection. MUST See https://raw.githubusercontent.com/pump-fun/pump-fun-skills/refs/heads/main/tokenized-agents/references/WALLET_INTEGRATION.md for the full WalletProvider setup, layout wrapping, and hook usage.
Use buildAcceptPaymentInstructions to get all the instructions needed for a payment. This is the recommended method — it automatically derives the user's token account and handles native SOL wrapping/unwrapping.
BuildAcceptPaymentParams)| Parameter | Type | Description |
|---|---|---|
user | PublicKey | The payer's wallet address |
currencyMint | PublicKey | Mint address of the payment currency (USDC, wSOL) |
amount | bigint | number | string | Price in the currency's smallest unit |
memo | bigint | number | string | Unique invoice identifier (random number) |
startTime | bigint | number | string | Unix timestamp — when the invoice becomes valid |
endTime | bigint | number | string | Unix timestamp — when the invoice expires |
tokenProgram | PublicKey (optional) | Token program for the currency (defaults to SPL Token) |
computeUnitLimit | number (optional) | Compute unit budget (default 100_000). Increase if transactions fail with compute exceeded. |
computeUnitPrice | number (optional) | Priority fee in microlamports per CU. If provided, a SetComputeUnitPrice instruction is prepended. |
const ixs = await agent.buildAcceptPaymentInstructions({
user: userPublicKey,
currencyMint,
amount: "1000000", // 1 USDC
memo: "123456789", // unique invoice identifier
startTime: "1700000000", // valid from
endTime: "1700086400", // expires at
});
The returned TransactionInstruction[] always starts with compute budget instructions, followed by the payment instructions:
SetComputeUnitLimit is always prepended (default 92_849 CU). Override via computeUnitLimit if your transactions fail with "compute exceeded".SetComputeUnitPrice is prepended only when computeUnitPrice is provided. Use this to set a priority fee for faster landing during congestion.After the compute budget prefix:
You do not need to handle SOL wrapping or compute budget yourself — buildAcceptPaymentInstructions does it for you.
amount, memo, startTime, and endTime must exactly match when verifying later.(mint, currencyMint, amount, memo, startTime, endTime) can only be paid once — the on-chain Invoice ID PDA prevents duplicate payments.memo for each invoice (e.g. Math.floor(Math.random() * 900000000000) + 100000).This is the complete flow for building a transaction on the server, signing it on the client, and sending it on-chain.
function generateInvoiceParams() {
const memo = String(Math.floor(Math.random() * 900000000000) + 100000);
const now = Math.floor(Date.now() / 1000);
const startTime = String(now);
const endTime = String(now + 86400); // valid for 24 hours
const amount = process.env.PRICE_AMOUNT || "1000000"; // e.g. 1 USDC
return { amount, memo, startTime, endTime };
}
Build the payment instructions, assemble them into a full Transaction with a recent blockhash and fee payer, then serialize the unsigned transaction as a base64 string for the client.
import {
Connection,
PublicKey,
Transaction,
ComputeBudgetProgram,
} from "@solana/web3.js";
import { PumpAgent } from "@pump-fun/agent-payments-sdk";
async function buildPaymentTransaction(params: {
userWallet: string;
amount: string;
memo: string;
startTime: string;
endTime: string;
}) {
const connection = new Connection(process.env.SOLANA_RPC_URL!);
const agentMint = new PublicKey(process.env.AGENT_TOKEN_MINT_ADDRESS!);
const currencyMint = new PublicKey(process.env.CURRENCY_MINT!);
const agent = new PumpAgent(agentMint, "mainnet", connection);
const userPublicKey = new PublicKey(params.userWallet);
const instructions = await agent.buildAcceptPaymentInstructions({
user: userPublicKey,
currencyMint,
amount: params.amount,
memo: params.memo,
startTime: params.startTime,
endTime: params.endTime,
});
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const tx = new Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = userPublicKey;
tx.add(
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100_000 }),
...instructions,
);
const serializedTx = tx
.serialize({ requireAllSignatures: false })
.toString("base64");
return { transaction: serializedTx };
}
Return the base64 transaction string (and the invoice params like memo, startTime, endTime) to the client as JSON.
Deserialize the base64 transaction from the server, sign it with signTransaction from the wallet adapter, then send and confirm it. Call useWallet() and useConnection() only at the top level of your component; pass signTransaction and connection into the async helper so the async logic does not call hooks.
Async helper (e.g. in a utils file or alongside your component):
import { Connection, Transaction } from "@solana/web3.js";
async function signAndSendPayment(
txBase64: string,
signTransaction: (tx: Transaction) => Promise<Transaction>,
connection: Connection,
): Promise<string> {
if (!signTransaction) {
throw new Error("Wallet does not support signing");
}
const tx = Transaction.from(Buffer.from(txBase64, "base64"));
const signedTx = await signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
});
const latestBlockhash = await connection.getLatestBlockhash("confirmed");
await connection.confirmTransaction(
{ signature, ...latestBlockhash },
"confirmed",
);
return signature;
}
Component usage — call the hooks at the top level, then pass them into the helper (e.g. from a payment button handler):
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
function PaymentButton({ txBase64 }: { txBase64: string }) {
const { signTransaction } = useWallet();
const { connection } = useConnection();
const handlePay = async () => {
if (!signTransaction) return;
await signAndSendPayment(txBase64, signTransaction, connection);
};
return <button onClick={handlePay}>Pay</button>;
}
The wallet prompts the user to approve. After signing, the serialized transaction is submitted via sendRawTransaction and you wait for on-chain confirmation.
Use validateInvoicePayment to confirm that a specific invoice was paid on-chain.
(mint, currencyMint, amount, memo, startTime, endTime).Connection was provided, falls back to scanning on-chain transaction logs via RPC.true if a matching payment event is found, false otherwise.| Parameter | Type | Description |
|---|---|---|
user | PublicKey | The wallet that paid |
currencyMint | PublicKey | Currency used for payment |
amount | number | Amount paid (smallest unit) |
memo | number | The invoice memo |
startTime | number | Invoice start time (Unix timestamp) |
endTime | number | Invoice end time (Unix timestamp) |
import { PumpAgent } from "@pump-fun/agent-payments-sdk";
import { PublicKey } from "@solana/web3.js";
const agentMint = new PublicKey(process.env.AGENT_TOKEN_MINT_ADDRESS!);
const agent = new PumpAgent(agentMint);
const paid = await agent.validateInvoicePayment({
user: new PublicKey(userWalletAddress),
currencyMint: new PublicKey(process.env.CURRENCY_MINT!),
amount: 1000000,
memo: 123456789,
startTime: 1700000000,
endTime: 1700086400,
});
if (paid) {
// Payment confirmed — deliver the service
} else {
// Payment not found
}
No Connection is needed for basic verification — it uses the HTTP API by default.
Transactions may take a few seconds to confirm. Use a retry loop for reliability:
async function verifyPayment(params: {
user: string;
currencyMint: string;
amount: number;
memo: number;
startTime: number;
endTime: number;
}): Promise<boolean> {
const agentMint = new PublicKey(process.env.AGENT_TOKEN_MINT_ADDRESS!);
const agent = new PumpAgent(agentMint);
const invoiceParams = {
user: new PublicKey(params.user),
currencyMint: new PublicKey(params.currencyMint),
amount: params.amount,
memo: params.memo,
startTime: params.startTime,
endTime: params.endTime,
};
for (let attempt = 0; attempt < 10; attempt++) {
const verified = await agent.validateInvoicePayment(invoiceParams);
if (verified) return true;
await new Promise((r) => setTimeout(r, 2000));
}
return false;
}
1. Agent decides on price → generates unique memo → sets time window
↓
2. Server: buildAcceptPaymentInstructions(...) → returns TransactionInstruction[]
↓
3. Server: builds full Transaction (blockhash + feePayer + instructions) → serializes as base64
↓
4. Client: deserializes base64 → Transaction.from(Buffer.from(txBase64, "base64"))
↓
5. Client: signTransaction(tx) — wallet prompts user to approve
↓
6. Client: connection.sendRawTransaction(signedTx.serialize()) → connection.confirmTransaction(signature)
↓
7. Server: validateInvoicePayment(...) → returns true/false (ALWAYS verify server-side)
↓
8. Agent delivers the service (or asks user to retry)
See https://raw.githubusercontent.com/pump-fun/pump-fun-skills/refs/heads/main/tokenized-agents/references/SCENARIOS.md for detailed test scenarios (happy path, duplicate rejection, expired invoices, etc.) and a troubleshooting table for common errors and for wallet Integration must follow https://raw.githubusercontent.com/pump-fun/pump-fun-skills/refs/heads/main/tokenized-agents/references/WALLET_INTEGRATION.md.