Skip to content

Solana Pay USDC: How to Accept Stablecoin Payments, Tested (2026)

Solana's Commerce Kit snippet asks customers for 0.025 USDC on a 25 USDC order. Here's the SDK that works, a 7-step merchant setup, and the validation traps.

Table of Contents

Copy the USDC example from Solana's own Accept Payments page, and the QR code you show your customer asks for 0.025 USDC, not 25. Stablecoin Insider ran that snippet against the package it names, decoded the URL, and then simulated the matching transaction on mainnet. The transaction failed too.

This guide shows how to accept Solana Pay USDC payments with the SDK that actually works today, step by step: the payment URL, the order reference, the QR code, finding the payment, and validating it before you ship anything. You'll also get the test results for both official SDKs, the five traps that cost merchants money, and what changes for PYUSD.

Key Takeaways

  • Use @solana/pay 1.0.26 with decimal amounts: 25 means 25 USDC.
  • The Commerce Kit snippet on solana.com encodes 25000000n as 0.025 USDC.
  • Commerce Kit 0.1.1 also targets the wrong token program for USDC and fails.
  • validateTransfer accepts overpayments and rejects version 1 transactions.
  • Validate every signature on the reference, not just the oldest one.
🧭
What this guide is: a merchant-side test of Solana Pay transfer requests for USDC and PYUSD using @solana/pay 1.0.26 (npm latest) and @solana-commerce/solana-pay 0.1.1 (npm latest), run on 8 October 2026 against live mainnet mint accounts, a mainnet simulateTransaction call, and a local mock RPC for the validation cases. No real funds moved.

What Solana Pay USDC actually is

Solana Pay is a URL standard, not a payment processor. A transfer request is a solana: link that tells a wallet who to pay, how much, in which token, and which order reference to attach. The wallet builds and signs the transfer itself, so there's no checkout server in the middle and no processor fee.

For USDC, the token is Circle's mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, which has 6 decimals. Stablecoin Insider read the mint account directly on 8 October 2026: 7,690,556,013 USDC outstanding on Solana at slot 454,542,403 (Solscan USDC token page; mint address confirmed on Circle's USDC contract addresses list).

The one rule that matters most sits in the Solana Pay specification: the amount field is in "user" units. For SOL that's SOL, not lamports. For tokens it's the human amount, so 25 USDC is written as 25. Every bug in this guide comes from code that forgets that line.

You'll pay the network fee only if you build the transaction yourself. Solana's base fee is 5,000 lamports per signature plus an optional priority fee (Solana fee docs). In a plain transfer request the customer's wallet pays it.

Two official SDKs, two amount rules

Here's the problem. Solana's docs currently point merchants at two different packages. The transfer requests quickstart uses @solana/pay with decimal amounts like 10.5. The Accept Payments: Solana Pay page uses @solana-commerce/solana-pay from the Commerce Kit, which it says is in beta, with bigint minor units like 25000000n for 25 USDC.

Adoption isn't close. From 5 September to 4 October 2026, @solana/pay logged 32,822 npm downloads (npm downloads API) against 1,416 for @solana-commerce/solana-pay (npm downloads API). Stablecoin Insider installed both at their npm latest tags and fed them the same 25 USDC order.

Input (25 USDC order)@solana/pay 1.0.26@solana-commerce/solana-pay 0.1.1What the customer sees
amount: 25 (decimal, @solana/pay style)amount=25 in URL (correct)Throws: cannot mix BigInt and other types25 USDC with @solana/pay
amount: 25000000n (minor units, solana.com Commerce snippet)Not applicable (number type)amount=0.025 in URL0.025 USDC, a 1,000x underpay
amount: 25000000 (minor units passed to @solana/pay by mistake)amount=25000000 in URLNot applicable25,000,000 USDC; createTransfer refused it with insufficient funds in the 100 USDC test wallet
parseURL of amount=10.5 with spl-token=USDC10.510500000000 (scaled by 9 SOL decimals)If used as USDC base units: 10,500 USDC
parseURL of amount=1.1234567 (7 decimals)1.1234567 (createTransfer later rejects)1123456700 (accepted)Spec says wallets must reject it as malformed

Source: Stablecoin Insider test, 8 October 2026, packages from @solana/pay on npm and @solana-commerce/solana-pay on npm. The Commerce Kit encoder converts every bigint with 9 decimals, SOL's precision, even when an SPL token is set (published 0.1.1 bundle).

⚠️
Warning: the solana.com Commerce Kit example for USDC (amount: 25000000n, splToken: USDC_MINT) produces a URL asking for 0.025 USDC. A customer who pays exactly what the wallet shows has paid one thousandth of the order. Don't ship that snippet for any 6-decimal stablecoin.

It gets worse on the transaction side. Commerce Kit's createSplTransfer keeps 25000000n as raw base units, which is the right number, but it builds the transferChecked instruction against the Token-2022 program for every mint, because the token helpers it imports from gill default to Token-2022. USDC lives on the classic Token program.

Stablecoin Insider simulated both SDKs' 1.25 USDC transfers between two real mainnet USDC holders with signature checks off (simulateTransaction docs). @solana/pay's instruction succeeded on the classic Token program using 180 compute units. Commerce Kit's failed with IncorrectProgramId at slot 454,542,900.

How to accept Solana Pay USDC, step by step

The steps below use @solana/pay 1.0.26, the package the quickstart uses and the one that passed every test. It's built on @solana/kit, so install it next to kit and the token programs it lists as peers.

Step 1: Create the merchant wallet and its USDC account

Share your wallet address, never the token account, as Solana's Accept Payments page advises. But make sure the wallet's USDC associated token account exists before you go live.

Two things break without it. createTransfer throws "Account not found" when the recipient has no USDC account, so any server-built payment fails. And if a customer's wallet creates the account inside the payment transaction, validateTransfer fails with "missing balance data" because there's no pre-balance to compare. The money arrives; your checkout says it didn't.

Step 2: Price in cents, convert once

Cart math in JavaScript floats will bite you. 0.1 + 0.2 is 0.30000000000000004, and createTransfer rejects it with "amount decimals invalid" because that has more than 6 decimal places. The URL encoder hides the problem by rounding to 10 places, so a QR code looks fine while server-side transaction requests fail (MDN on toFixed rounding).

Keep prices as integer cents and convert at the edge. In Stablecoin Insider's test, the same total rounded to two decimals built a correct 300,000 base-unit transfer (createTransfer source).

Step 3: Generate a unique reference and the payment URL

The reference is a random public key added as a read-only account on the transfer instruction. It's how you find this order's payment on-chain without asking the customer for anything. Make one per order and store it with the order id, amount, and mint.

import { encodeURL, createQR } from '@solana/pay';
import { address, generateKeyPairSigner } from '@solana/kit';

const USDC = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const MERCHANT = address(process.env.MERCHANT_WALLET);

function centsToUsdc(cents) {
  if (!Number.isInteger(cents) || cents <= 0) throw new Error('bad price');
  return Number((cents / 100).toFixed(2)); // 2599 cents becomes 25.99
}

export async function createOrderPayment(order) {
  const reference = (await generateKeyPairSigner()).address; // unique per order
  const amount = centsToUsdc(order.totalCents);              // decimal USDC, not base units
  const url = encodeURL({
    recipient: MERCHANT,
    amount,
    splToken: USDC,
    reference,
    label: 'Your Store',
    message: `Order ${order.id}`,
    memo: `order-${order.id}`,
  });
  await db.orders.update(order.id, { reference, amount, mint: USDC, status: 'awaiting_payment' });
  return { url: url.toString(), qr: createQR(url, 360) };
}

Stablecoin Insider's run produced a URL of the form solana:<merchant>?amount=25&spl-token=EPjF...Dt1v&reference=<key>&memo=order-1042. Decoding the instruction createTransfer builds from it showed transferChecked, 25,000,000 base units, decimals 6, and the reference as the fifth account (@solana/pay 1.0.26 source).

createQR returns a styled QR object you append to the page. On mobile, render the same URL as a tappable link, since a phone can't scan its own screen. Keep the label short; wallets show it as the merchant name.

Step 5: Find the payment

findReference polls getSignaturesForAddress on the reference, and watchReference subscribes over WebSocket. watchReference defaults to confirmed commitment, and findReference uses whatever commitment you pass, otherwise your RPC's default. The legacy Solana Pay merchant guide warns that confirmed can, rarely, return a transaction that isn't final, so use finalized before you release goods of real value.

Here's the catch most tutorials skip. findReference returns the oldest signature that mentions your reference (findReference source). Your reference is printed in a public QR code, so anyone can send a dust or failed transaction that mentions it first. Validate every signature on the reference and accept the first one that passes.

import { validateTransfer } from '@solana/pay';
import { createSolanaRpc } from '@solana/kit';

const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL);

export async function confirmOrder(order) {
  const sigs = await rpc
    .getSignaturesForAddress(order.reference, { commitment: 'finalized', limit: 50 })
    .send();
  for (const { signature, err } of sigs.reverse()) {   // oldest first
    if (err) continue;                                  // failed or spam transaction
    try {
      await validateTransfer(rpc, signature, {
        recipient: MERCHANT, amount: order.amount, splToken: USDC,
        reference: order.reference, memo: `order-${order.id}`,
      }, { commitment: 'finalized' });
      return { paid: true, signature };
    } catch (e) {
      log.warn('payment candidate rejected', { order: order.id, signature, reason: e.message });
    }
  }
  return { paid: false };
}

Step 6: Validate before you fulfill

validateTransfer checks that the last instruction is a token transfer to your USDC account, the mint matches, the references match in order, the memo matches if you pass one, and the balance rose by at least the amount. Stablecoin Insider signed a real 25 USDC transfer built by createTransfer and ran nine cases through it with a mocked getTransaction response (validateTransfer source).

CaseResultWhat it means for you
Exact 25 USDC, right reference and memoPASSHappy path works.
Expected 25.000001, received 25Rejected: amount not transferredUnderpay by one base unit is caught.
Expected 20, received 25PASSOverpayments pass silently. Compare amounts yourself and refund the difference.
Wrong referenceRejected: invalid reference 0Reference binding works.
No reference passed to validateTransferPASSWithout a reference, any matching transfer to you validates. Always pass it.
Wrong memoRejected: invalid memoUse the memo as a second order check.
Recipient USDC account created in the same transactionRejected: missing balance dataPre-create the account (Step 1).
Transaction failed on-chainRejected with the program errorFailed attempts never count as paid.
Expected 0.1 + 0.2 as a float, received 300,000 base unitsPASSValidation rounds to 6 decimals; only createTransfer is strict.

Source: Stablecoin Insider mock-RPC test of @solana/pay 1.0.26, 8 October 2026. The overpayment pass is by design: the code checks post minus pre is at least the expected amount, never equal to it.

Step 7: Record, reconcile, and move the USDC

Store the signature, slot, amount received, and any overpayment against the order. Then decide where the USDC goes. If you settle to a bank, compare routes in how to off-ramp USDC to ACH or wire, or see how to run a SpherePay off-ramp for PIX, ACH, and SEPA payouts. If your treasury sits on another chain, bridging USDC between Ethereum and Solana covers the reverse route too.

The version 1 transaction trap

validateTransfer in 1.0.26 calls getTransaction with maxSupportedTransactionVersion set to 0. Mainnet RPC now returns version 1 transactions, and when a client asks for 0, the node refuses with error -32015 (getTransaction docs).

Stablecoin Insider sampled 15 recent transactions touching the USDC mint on 8 October 2026: 6 were version 1, 6 were version 0, and 3 were legacy. Passing one of the version 1 signatures into validateTransfer threw SolanaError -32015 before any payment check ran (validateTransfer source).

Stablecoin Insider didn't test which wallets send version 1 transactions for Solana Pay transfers, so treat this as a risk to monitor, not a confirmed outage. The practical fix: catch -32015 in your confirm loop, mark the order "needs review" instead of "unpaid", and watch the solana-foundation/pay repository for a release that raises the version.

PYUSD and other Token-2022 stablecoins

PYUSD on Solana is a Token-2022 mint, and @solana/pay 1.0.26 handles it. createTransfer reads the mint's owner and built the PYUSD transfer against the Token-2022 program with 6 decimals and the reference attached, the same shape as USDC.

The mint carries more machinery than USDC, though. On 8 October 2026 it showed 705,897,373 PYUSD on Solana at slot 454,542,413, a transfer fee config set to 0 basis points, a transfer hook with no program set, and a permanent delegate set to the same authority key that controls the fee and hook settings (Solscan PYUSD token page; extension behavior in the Token-2022 program repository).

That means three things for you. If the fee ever rises above 0, the amount you receive will be less than the amount sent, and validateTransfer will reject a correct payment. If a hook program is set, wallets will need extra accounts. And the permanent delegate can move tokens out of any PYUSD account, including yours. PayPal's own PYUSD merchant flows are covered in how to enable PayPal PYUSD settlement.

When Solana Pay is the right way to take USDC

OptionBest forWhat you runWhere it breaks
Solana Pay transfer requestQR at a counter, payment links, invoicesURL, reference store, confirm loopOverpay handling, version 1 transactions, no refunds built in
Solana Pay transaction requestCustom flows: discounts, loyalty, fee sponsorshipAn HTTPS endpoint that builds the transactionYou own transaction building and security
USDC on Shopify PaymentsExisting Shopify storesNothing on-chainPlatform rules, chain choice not yours
Stablecoin payments on StripeCard-first businesses adding USDCStripe integrationProcessor fees and payout timing
Hosted stablecoin checkoutsMulti-chain acceptanceProvider integrationProvider fees and custody

If you're still deciding whether stablecoin acceptance fits at all, stablecoin vs crypto payments for merchants and stablecoin payment rails in 2026 lay out the tradeoffs. Platforms onboarding many sellers should read how to onboard merchants for stablecoin payments before they hand out QR codes.

💡
Stablecoin Insider's take: Solana Pay is still the cleanest way to take USDC face to face, because the customer's wallet does the work and you pay nothing per transaction. The SDK situation isn't clean. Use @solana/pay, ignore the Commerce Kit snippet until it fixes token decimals and the program ID, and treat validateTransfer as one check inside your own confirm loop, not the whole loop. A merchant who checks every signature on the reference, refunds overpayments, and pre-creates the USDC account will catch every failure this test found.

Taking Solana Pay USDC this quarter? Run one 0.30 USDC order through your confirm loop on mainnet, check the decoded instruction shows 6 decimals and the base-unit amount you expect, and only then print the QR code.

Selling on Shopify instead of a custom checkout? See how USDC acceptance works on Shopify Payments.

Accept USDC on Shopify Payments
How to Send USDC with Solana Agent Kit (2026)
Stablecoin Insider's tested Solana Agent Kit USDC setup, the float-math bug, Token-2022 limits, and a guarded PAY_USDC action.

Moving USDC across chains after it lands? Compare routes in the best cross-chain bridges for 2026 or route through LI.FI's cross-chain aggregator.

FAQ

How do you accept USDC with Solana Pay?

Create a transfer request URL with your wallet as recipient, the USDC mint as spl-token, a decimal amount, and a unique reference, then show it as a QR code. Confirm by validating the transaction that mentions the reference before you fulfill the order.

Is the Solana Pay amount in USDC or in base units?

It's in USDC, the human amount. The Solana Pay spec says user units, so 25 USDC is amount=25, and 25000000 would ask for 25 million USDC.

Which Solana Pay SDK should you use in 2026?

Use @solana/pay 1.0.26, the package in Solana's transfer request quickstart. In Stablecoin Insider's test, @solana-commerce/solana-pay 0.1.1 encoded 25000000n as 0.025 USDC and built a USDC transfer that failed simulation.

Does Solana Pay charge merchant fees?

No. Solana Pay is an open standard with no processor; the customer's wallet pays the network fee, 5,000 lamports per signature plus any priority fee, as listed in Solana's fee docs.

Can Solana Pay accept PYUSD?

Yes. @solana/pay detects that PYUSD is a Token-2022 mint and builds the transfer against the right program. Watch the mint's transfer fee and hook settings, since a nonzero fee would make exact-amount validation fail.

What happens if a customer overpays with Solana Pay?

The payment still validates. validateTransfer only checks that at least the expected amount arrived, so compare the received amount yourself and refund the difference.

Why does validateTransfer fail on a real payment?

The usual causes are a USDC account created inside the payment transaction, a wrong or missing memo, or a version 1 transaction that the 1.0.26 RPC call refuses. Log the error message and send the order to review rather than marking it unpaid.


This content is provided for informational and educational purposes only and does not constitute financial, investment, legal, or tax advice; no material herein should be interpreted as a recommendation, endorsement, or solicitation to buy or sell.

Latest