Table of Contents
Solana Agent Kit sends USDC in one call, but hand it PYUSD and it fails before anything gets signed. PayPal USD on Solana is a Token-2022 mint, and the kit's built-in transfer only knows the classic SPL Token program.
This guide shows how to send PYUSD with Solana Agent Kit anyway, using a custom action built on the Token-2022 program, transferChecked, and transfer-hook account resolution. It picks up where Stablecoin Insider's Solana Agent Kit USDC guide left off. Every step below was run against Solana mainnet on 15 October 2026 and checked against Solana's PYUSD developer page and the transfer hook integration guide.
Key Takeaways
- The kit's built-in transfer fails on PYUSD with an empty error message.
- PYUSD needs Token-2022 token accounts and a transferChecked instruction.
- A plain Transfer to PYUSD is rejected because the mint has a fee config.
- The hook is off today, so build with a hook-aware helper and re-read the mint.
- Wrap the send in a guarded action: allowlist, cap, integer amounts, simulate first.
Why the built-in TRANSFER action can't send PYUSD
The Token plugin's transfer tool derives token accounts, checks the recipient account, and reads the mint, all with the default SPL Token program. You can see it in the v2 transfer.ts source: getAssociatedTokenAddress, getAccount, and getMint are called with no program ID, then createTransferInstruction builds a plain Transfer.
PYUSD's mint is owned by the Token-2022 program (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb), not the classic program, according to a getAccountInfo read of mint 2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo. So the first call that inspects the mint throws TokenInvalidAccountOwnerError.
Stablecoin Insider ran the kit's own method, agent.methods.transfer(agent, recipient, 2.01, PYUSD), on 15 October 2026. It threw "Transfer failed: " with nothing after the colon, because that spl-token error class carries no message. An agent that reads its tool errors gets no clue what went wrong, which invites blind retries.
Fixing the program ID alone isn't enough, either. The team simulated three variants against a real PYUSD account, and only one instruction shape works:
| Instruction tried | Program | Simulation result (15 Oct 2026) |
|---|---|---|
| Plain Transfer (what the kit builds) | Classic SPL Token | InvalidAccountData: the account isn't a classic token account |
| Plain Transfer | Token-2022 | Custom error 0x1f: "Mint required for this account to transfer tokens, use transfer_checked" |
| TransferChecked, decimals 9 | Token-2022 | Custom error 0x12: "decimals different from the Mint decimals" |
| TransferChecked, decimals 6 | Token-2022 | Success, 3,849 compute units |
Source: Stablecoin Insider simulation of a 2.01 PYUSD transfer between two existing mainnet PYUSD accounts via simulateTransaction (sigVerify off); error strings from the Token-2022 error definitions.
The 0x1f error is the one most guides skip. Token-2022 refuses a plain Transfer whenever the source account carries the transfer fee extension, even when the fee is zero. PYUSD accounts carry it, so the instruction has to include the mint and its decimals. The Solana transfer fee docs explain why: the program needs the mint to compute any withheld fee.
What the PYUSD mint looks like on Solana
Paxos Trust Company issues PYUSD and PayPal distributes it, according to solana.com/pyusd, which lists $702M of PYUSD on Solana on 8 October 2026, $2.89B of reserves on 31 August 2026, and a 25.8% share of redeemable PYUSD issued on Solana. A getTokenSupply call on 15 October 2026 returned 687,996,769.32 PYUSD, or about 688.00 million.
For a market view across issuers, Stablecoin Insider's Q3 2026 stablecoin report and the top stablecoins on Solana list put PYUSD's Solana footprint in context. For the agent, though, the extensions matter more than the supply.
| Extension on the PYUSD mint | State read on 15 Oct 2026 | What your agent must do |
|---|---|---|
| Permanent delegate | Set to the Paxos authority 2apBG...YJjk | Accept that the issuer can move or burn tokens in any account |
| Transfer fee config | 0 basis points, maximum fee 0 | Use transferChecked; re-read the fee before each send |
| Transfer hook | Initialized, programId null | Build with a hook-aware helper so a future hook doesn't break sends |
| Confidential transfer (+ fee config) | Initialized, auto-approve off | Nothing for public transfers |
| Metadata pointer + token metadata | Points to the mint itself: PayPal USD, PYUSD | Read name and symbol from the mint |
| Mint close authority | Set to the Paxos authority | Nothing at send time |
| Freeze authority | Set to the Paxos authority | Expect a frozen account to fail the send |
Source: getAccountInfo (jsonParsed) on mint 2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo; extension behavior per solana.com/pyusd and the Token Extensions docs.
Decimals are 6, the same as USDC, and the devnet mint is CXk2AMBfi3TwaEL2468s6zP8xq9NxTXjp9gjMgzeUynM. Stablecoin Insider read that devnet mint too: it is Token-2022 with the same eight extension types and a null hook program. Devnet PYUSD for testing is available from the Paxos faucet.
Step 1: Install the core kit and the Token-2022 clients
You don't need the Token plugin for this action. The custom action only needs the core kit for its wallet and signOrSendTX, plus a token client that understands Token-2022. Latest npm releases on 15 October 2026 were solana-agent-kit 2.0.10 (published 24 September 2025, per the npm registry) and @solana-program/token-2022 0.19.0 (21 September 2026, per its registry record).
npm install solana-agent-kit @solana/web3.js @solana/spl-token zod bs58
# optional: the Kit-based client recommended for new Token-2022 integrations
npm install @solana/kit @solana-program/token-2022Skipping the plugin also dodges an install problem the team hit. On a fresh install with Node 20.19.2, importing @solana-agent-kit/plugin-token 2.0.9 as an ES module failed with "Named export 'BN' not found" from a transitive @pump-fun dependency, while require() worked and the core package imported fine. The plugin's 2.0.9 manifest pins @pump-fun/pump-sdk only to a caret range, so what you get depends on install day.
Adoption gives a sense of who maintains what. The core kit logged 6,062 npm downloads from 13 September to 12 October 2026 (npm downloads API), while @solana-program/token-2022 logged 4,263,559 in the same window (npm downloads API). The kit's last commit on the v2 branch is dated 14 May 2026, per the v2 commit history, so don't wait for a built-in Token-2022 path.
Step 2: Derive Token-2022 token accounts
An associated token account address depends on the token program, so the classic derivation gives you the wrong address for PYUSD. In the team's test, the classic derivation for one holder pointed at an address with no account on chain, while the Token-2022 derivation matched the account that actually holds that wallet's PYUSD.
import { PublicKey } from "@solana/web3.js";
import { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
const PYUSD = new PublicKey("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo");
// Pass the Token-2022 program ID, or you derive a classic address that never holds PYUSD.
const ata = getAssociatedTokenAddressSync(PYUSD, ownerPublicKey, false, TOKEN_2022_PROGRAM_ID);PYUSD token accounts are also bigger than classic ones. A live PYUSD associated account is 187 bytes, carrying the immutable owner, transfer fee amount, and transfer hook account extensions, per getAccountInfo. Rent-exempt minimum for 187 bytes was 1,600,200 lamports on 15 October 2026, per getMinimumBalanceForRentExemption. The immutable owner docs explain why the associated token program always adds that extension.
One more trap: @solana/spl-token 0.4.15 can't size a PYUSD account. getAccountLenForMint threw "Unknown extension type: undefined" in the team's test, because the library doesn't map one of the mint's extensions. Read the size of a real account, or let the associated token program allocate it.
Step 3: Build transferChecked with transfer-hook resolution
PYUSD's hook program is null today, but Solana's PYUSD page says Paxos can switch initialized extensions on later. The transfer hook integration guide is blunt about what happens then: a client that doesn't resolve the hook's extra accounts can't send the token at all.
So build the instruction with the hook-aware helper from day one. With no hook set, it returns a plain four-account transferChecked; with a hook set, it reads the ExtraAccountMetaList and appends the accounts the hook needs.
import {
TOKEN_2022_PROGRAM_ID, getMint, getTransferFeeConfig, getTransferHook,
getAssociatedTokenAddressSync, createTransferCheckedWithTransferHookInstruction,
} from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
export async function buildPyusdTransfer(connection, owner, to, raw) {
// Re-read the mint on every send: fee and hook settings can change.
const mint = await getMint(connection, PYUSD, "confirmed", TOKEN_2022_PROGRAM_ID);
if (mint.decimals !== 6) throw new Error("unexpected decimals");
const fee = getTransferFeeConfig(mint);
if (fee && (fee.newerTransferFee.transferFeeBasisPoints !== 0 ||
fee.olderTransferFee.transferFeeBasisPoints !== 0)) {
throw new Error("PYUSD transfer fee is on; re-quote before paying");
}
const hook = getTransferHook(mint);
const hookOn = hook && !hook.programId.equals(PublicKey.default); // log this
const from = getAssociatedTokenAddressSync(PYUSD, owner, false, TOKEN_2022_PROGRAM_ID);
const dest = getAssociatedTokenAddressSync(PYUSD, to, false, TOKEN_2022_PROGRAM_ID);
const ix = await createTransferCheckedWithTransferHookInstruction(
connection, from, PYUSD, dest, owner, raw, 6, [], "confirmed", TOKEN_2022_PROGRAM_ID,
);
return { ix, hookOn };
}Note the hookOn line. getTransferHook returns an object for PYUSD even though no hook is set, with the system program address (32 zero bytes) standing in for null. A naive "if (getTransferHook(mint))" check reports a hook that isn't there. The helper copes with it, and in the team's test it returned four accounts and simulated cleanly.
If your app already uses @solana/kit, the @solana-program/token-2022 client offers getTransferCheckedWithTransferHookInstructionAsync, the path the integration guide recommends for new code. Stablecoin Insider built the same 2.01 PYUSD transfer with it: Token-2022 program, four accounts, data 0c90ab1e000000000006 (TransferChecked, amount 2010000, decimals 6). Converted to a web3.js instruction, it simulated with the same result.
Step 4: Wrap it in a guarded PAY_PYUSD action
Now give the model one tool that can only do what the business allows. This mirrors the PAY_USDC action in the USDC guide: a decimal string in, BigInt base units out, an allowlist and a cap in code, and a simulation before anything is signed.
import { z } from "zod";
import { PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
import { signOrSendTX } from "solana-agent-kit";
const ALLOW = new Set(["VENDOR_A_ADDRESS", "VENDOR_B_ADDRESS"]);
const MAX_PER_TX = 250_000_000n; // 250 PYUSD in base units
const toBaseUnits = (s) => {
const [whole, frac = ""] = s.split(".");
if (frac.length > 6) throw new Error("max 6 decimals");
return BigInt(whole) * 1_000_000n + BigInt(frac.padEnd(6, "0"));
};
async function simulate(connection, payer, ixs) {
const { blockhash } = await connection.getLatestBlockhash();
const msg = new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: ixs })
.compileToV0Message();
const sim = await connection.simulateTransaction(new VersionedTransaction(msg),
{ sigVerify: false, replaceRecentBlockhash: true });
return sim.value;
}
export const payPyusd = {
name: "PAY_PYUSD",
similes: ["pay vendor in pyusd", "send pyusd payment"],
description: "Pay an allowlisted vendor in PYUSD on Solana. Amount is a decimal string.",
examples: [],
schema: z.object({ to: z.string(), amount: z.string().regex(/^\d+(\.\d{1,6})?$/) }),
handler: async (agent, { to, amount }) => {
if (!ALLOW.has(to)) throw new Error("recipient not on allowlist");
const raw = toBaseUnits(amount);
if (raw > MAX_PER_TX) throw new Error("limit exceeded");
const { ix } = await buildPyusdTransfer(agent.connection, agent.wallet.publicKey, new PublicKey(to), raw);
const sim = await simulate(agent.connection, agent.wallet.publicKey, [ix]);
if (sim.err) throw new Error("simulation failed: " + JSON.stringify(sim.err));
const signature = await signOrSendTX(agent, [ix]);
return { status: "success", amount, to, signature };
},
};Register it with the agent and expose only the tools the job needs. signOrSendTX adds compute budget and priority fee instructions before it signs, per the kit's send_tx.ts, and it returns a signed transaction instead of sending when the agent is created with signOnly: true.
Stablecoin Insider ran the handler on 15 October 2026 inside a real SolanaAgentKit 2.0.10 instance with signOnly on and an empty wallet. A request to an unknown address stopped at the allowlist. A 300 PYUSD request stopped at the cap. A 2.01 PYUSD request to an allowlisted address built the right instruction, then stopped at simulation with AccountNotFound because the wallet held nothing, so nothing was signed. The schema also rejected a float amount and a seventh decimal place.
Step 5: Simulate, then sign
The integration guide's rule is to simulate before signing, because a hook can reject a transfer for its own reasons and stale hook accounts fail with an account-validation error. Here is what the same builder produced against live mainnet accounts:
| Scenario (2.01 PYUSD) | Result | Compute units |
|---|---|---|
| buildPyusdTransfer between two existing PYUSD accounts | Success | 3,849 |
| Create recipient account (idempotent) + transferChecked | Success | 23,450 |
| PAY_PYUSD handler, empty agent wallet | Stopped before signing: AccountNotFound | n/a |
Source: Stablecoin Insider simulateTransaction runs on Solana mainnet, 15 October 2026, using real PYUSD holder accounts with signature checks off.
Those numbers sit far below the kit's floor. send_tx.ts sets the compute limit to the larger of the estimate plus 100,000 units or 1.2 times the estimate, per the kit source, so a hook-free PYUSD send has plenty of headroom. If Paxos ever turns the hook on, re-measure: the integration guide warns that hook CPIs add compute and can need one-time setup accounts.
Permanent delegate, fee config, and freeze: what an agent should watch
Three PYUSD settings belong in your agent's monitoring, not just its send path.
- Permanent delegate. The issuer can transfer or burn PYUSD from any account, for example to comply with a legal order, per solana.com/pyusd. Reconcile balances from chain, not from your own ledger of sends, and see the permanent delegate docs for how it works.
- Transfer fee. It's 0 basis points today. If it ever changes, a 250 PYUSD send would deliver less than 250, so the action above refuses to pay until a person re-quotes. The transfer fee docs cover transferCheckedWithFee if you decide to support it.
- Freeze authority. A frozen source or destination account fails the transfer. Treat that as a hard stop and alert a person, never a retry.
- Confidential transfers. Initialized with auto-approve off, per the mainnet read. Standard public sends are unaffected, per the confidential transfer docs.
Paxos describes PYUSD's issuer and reserves on its PYUSD page and publishes reserve reports on its transparency page. If your business mints or redeems directly rather than buying on the market, Stablecoin Insider's PYUSD mint and redeem guide covers Paxos Orchestrations, and the Charles Cascarilla profile covers the issuer behind it.
Built-in TRANSFER vs PAY_PYUSD
| Attribute | Built-in TRANSFER (plugin-token 2.0.9) | PAY_PYUSD custom action |
|---|---|---|
| Token program | Classic SPL Token only | Token-2022 |
| Instruction | Plain Transfer | TransferChecked, hook-aware |
| Amount | JavaScript float times 10^decimals | Decimal string to BigInt |
| Recipient | Any address | Allowlist in code |
| Limits | None | Per-transfer cap (add a daily cap in your database) |
| Pre-sign check | Compute estimate only | Mint re-read, fee check, full simulation |
| Result on PYUSD | Throws "Transfer failed: " before signing | Simulated success, 3,849 compute units |
Source: kit transfer.ts and Stablecoin Insider's 15 October 2026 mainnet simulations.
The same action works for USDG
Global Dollar (USDG), also issued by Paxos, uses the same Token-2022 extension set on Solana. A getAccountInfo read of mint 2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH on 15 October 2026 showed the same eight extension types, a 0 basis point fee, a null hook, and a supply of about 601.73 million. Swap the mint constant and keep everything else.
The issuer side is in Stablecoin Insider's USDG mint and redeem guide. Don't share one allowlist or cap across both tokens: give each its own action, so logs and limits stay per asset.
Where PYUSD agent payments break
- Classic program calls. Any getMint, getAccount, or address derivation without TOKEN_2022_PROGRAM_ID fails or points at an empty address.
- Plain Transfer. Token-2022 rejects it on PYUSD with error 0x1f, even with the right program.
- Empty error strings. The built-in tool surfaces "Transfer failed: " with no reason, so map errors to plain text before the model sees them.
- Cached mint state. Fee and hook settings can change; re-read the mint for every send.
- Retries without a key. Store a payment ID and check it before resending, or a retried tool call pays twice.
- Raw keys on mainnet. Pair the action with a managed signer and signOnly for anything above a threshold.
Most of the controls carry over from the USDC side unchanged. See spend limits for an agent wallet, testing an agent payment before going live, revoking agent access, and reconciling agent spend. For signers, compare Privy agent wallets, Turnkey agentic payments, and the custodial vs non-custodial agent wallet breakdown. The wider landscape is in stablecoin payments for AI agents.
Stablecoin Insider's take: write one Token-2022 action and own it
Stablecoin Insider's take: Solana Agent Kit is still a fine harness for a payment agent, but its built-in TRANSFER action is classic-SPL sample code, and the v2 branch hasn't moved since May. PYUSD needs a few dozen lines of your own code: the Token-2022 program, a hook-aware transferChecked, a fee check, and a simulation gate. Write that once, keep it narrow, and treat any change to PYUSD's fee, hook, or delegate as a reason to pause the agent, not a reason to patch on the fly.
Run the 2.01 PYUSD case on devnet first, then on mainnet with signOnly, and confirm the signature lands before the model gets a live wallet. Then link this action back to the USDC Agent Kit guide so both assets share one review process.
Already running USDC through Solana Agent Kit? Stablecoin Insider's USDC guide covers the float-amount bug and the guarded PAY_USDC action this PYUSD version is built on.
FAQ
Can Solana Agent Kit send PYUSD?
Not with the built-in TRANSFER action, which uses the classic SPL Token program and fails before signing. A custom action using the Token-2022 program and transferChecked works.
Why does my PYUSD transfer say "Transfer failed: " with no reason?
The kit's getMint call throws TokenInvalidAccountOwnerError, which has an empty message. PYUSD's mint is owned by Token-2022, so read it with TOKEN_2022_PROGRAM_ID.
Why does a Token-2022 Transfer fail with error 0x1f?
PYUSD accounts carry the transfer fee extension, so Token-2022 requires transferChecked, which includes the mint and decimals. Plain Transfer is rejected even when the fee is zero.
Does PYUSD on Solana have a transfer hook?
The extension is initialized, but no hook program was set on 15 October 2026. Use a hook-aware builder anyway, because Paxos can turn it on later.
What is PYUSD's permanent delegate?
An issuer authority that can transfer or burn PYUSD from any account, for example under a legal order. Agents should reconcile balances from chain rather than assume them.
Can the same action send USDG?
Yes. USDG uses the same Token-2022 extension set on Solana, so swap the mint address and give it its own allowlist and caps.
This content is provided for informational and educational purposes only and does not constitute financial, investment, legal, or tax advice; it is not financial advice, and no material herein should be interpreted as a recommendation, endorsement, or solicitation to buy or sell any asset or token. Package versions, mint extensions, and issuer settings change; check the live docs and the mint account on chain before you send real funds.