Skip to content

How to Send PYUSD with Solana Agent Kit (Token-2022) (2026)

The built-in Solana Agent Kit transfer can't move PYUSD. Here's a mainnet-simulated PAY_PYUSD action: Token-2022 accounts, transferChecked, transfer-hook resolution, integer amounts, and the permanent delegate and fee config to watch.

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.
🧭
What this guide is: a worked custom action for sending PayPal USD (PYUSD) from a Solana Agent Kit v2 agent, tested on 15 October 2026 with solana-agent-kit 2.0.10, @solana/spl-token 0.4.15, @solana/kit 8.5.0, and @solana-program/token-2022 0.19.0. Stablecoin Insider read the PYUSD mint from mainnet and simulated every transfer against real token accounts without signing or sending. No funds moved, and nothing here recommends a token.
How to Send USDC with Solana Agent Kit (2026)
Stablecoin Insider's tested USDC setup for Solana Agent Kit: the float-amount bug, the Token-2022 gap, and a guarded PAY_USDC action.

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 triedProgramSimulation result (15 Oct 2026)
Plain Transfer (what the kit builds)Classic SPL TokenInvalidAccountData: the account isn't a classic token account
Plain TransferToken-2022Custom error 0x1f: "Mint required for this account to transfer tokens, use transfer_checked"
TransferChecked, decimals 9Token-2022Custom error 0x12: "decimals different from the Mint decimals"
TransferChecked, decimals 6Token-2022Success, 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 mintState read on 15 Oct 2026What your agent must do
Permanent delegateSet to the Paxos authority 2apBG...YJjkAccept that the issuer can move or burn tokens in any account
Transfer fee config0 basis points, maximum fee 0Use transferChecked; re-read the fee before each send
Transfer hookInitialized, programId nullBuild with a hook-aware helper so a future hook doesn't break sends
Confidential transfer (+ fee config)Initialized, auto-approve offNothing for public transfers
Metadata pointer + token metadataPoints to the mint itself: PayPal USD, PYUSDRead name and symbol from the mint
Mint close authoritySet to the Paxos authorityNothing at send time
Freeze authoritySet to the Paxos authorityExpect 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-2022

Skipping 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.

🔒
Design choice: PAY_PYUSD does not create the recipient's token account. Onboard each payee once, confirm their Token-2022 PYUSD account exists, then add them to the allowlist. The agent never pays rent for strangers, and a typo can't open an account on someone else's behalf.

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)ResultCompute units
buildPyusdTransfer between two existing PYUSD accountsSuccess3,849
Create recipient account (idempotent) + transferCheckedSuccess23,450
PAY_PYUSD handler, empty agent walletStopped before signing: AccountNotFoundn/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

AttributeBuilt-in TRANSFER (plugin-token 2.0.9)PAY_PYUSD custom action
Token programClassic SPL Token onlyToken-2022
InstructionPlain TransferTransferChecked, hook-aware
AmountJavaScript float times 10^decimalsDecimal string to BigInt
RecipientAny addressAllowlist in code
LimitsNonePer-transfer cap (add a daily cap in your database)
Pre-sign checkCompute estimate onlyMint re-read, fee check, full simulation
Result on PYUSDThrows "Transfer failed: " before signingSimulated 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.

Read the USDC Agent Kit guide
Build with PYUSD, a regulated dollar on Solana
Solana's developer page for PayPal USD: mint and devnet addresses, Token-2022 extensions, and how to derive a PYUSD token account.

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.

Latest