A Solana Pay QR code only requests a payment. Before you ship a download or mark an order paid, you have to confirm on-chain that the right amount reached the right wallet. The reference key is how you find that transaction without asking the buyer for a signature or running a database of wallet addresses.
For each order, generate a fresh random 32-byte public key and add it as reference. The wallet attaches it to the transfer instruction as a read-only, non-signer account, so the transaction becomes searchable by that address.
import { Keypair } from '@solana/web3.js';
const reference = Keypair.generate().publicKey; // new for every order, store it with the order
// USDC (mainnet mint) — omit spl-token to charge SOL instead
const url = `solana:${RECIPIENT}?amount=12`
+ `&spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
+ `&reference=${reference.toBase58()}`
+ `&label=${encodeURIComponent('My Shop')}`
+ `&message=${encodeURIComponent('Order #1042')}`;
In a browser or worker without web3.js, 32 bytes from crypto.getRandomValues encoded as base58 gives the same result. amount is in whole units (USDC or SOL), not lamports or base units.
findReference waits for a transaction that includes the reference, and validateTransfer checks recipient, amount, token and reference.
import { Connection, PublicKey } from '@solana/web3.js';
import { findReference, validateTransfer, FindReferenceError } from '@solana/pay';
import BigNumber from 'bignumber.js';
const connection = new Connection(RPC_URL, 'confirmed');
const USDC = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
async function check(reference) {
try {
const { signature } = await findReference(connection, reference, { finality: 'confirmed' });
await validateTransfer(connection, signature, {
recipient: new PublicKey(RECIPIENT),
amount: new BigNumber(12), // from YOUR order record, never from the client
splToken: USDC, // remove for SOL payments
reference,
}, { commitment: 'confirmed' });
return { paid: true, signature }; // validateTransfer throws if anything is off
} catch (e) {
if (e instanceof FindReferenceError) return { paid: false }; // not paid yet, keep polling
throw e; // found but invalid (wrong amount, recipient...)
}
}
Two RPC calls do the same job: getSignaturesForAddress on the reference, then getTransaction with jsonParsed to read balance changes.
const rpc = async (method, params) => (await (await fetch(RPC_URL, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) })).json()).result;
async function verify(ref, recipient, amount, isUsdc) {
const sigs = await rpc('getSignaturesForAddress', [ref, { limit: 10, commitment: 'confirmed' }]);
for (const s of sigs || []) {
if (s.err) continue;
const tx = await rpc('getTransaction', [s.signature,
{ encoding: 'jsonParsed', commitment: 'confirmed', maxSupportedTransactionVersion: 0 }]);
if (!tx || tx.meta.err) continue;
let received;
if (isUsdc) {
const mint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const bal = list => BigInt((list.find(b => b.owner === recipient && b.mint === mint)
|| { uiTokenAmount: { amount: '0' } }).uiTokenAmount.amount);
received = bal(tx.meta.postTokenBalances) - bal(tx.meta.preTokenBalances); // 6 decimals
if (received >= BigInt(Math.round(amount * 1e6))) return { paid: true, signature: s.signature };
} else {
const i = tx.transaction.message.accountKeys.findIndex(k => k.pubkey === recipient);
if (i < 0) continue;
received = tx.meta.postBalances[i] - tx.meta.preBalances[i]; // lamports
if (received >= Math.round(amount * 1e9)) return { paid: true, signature: s.signature };
}
}
return { paid: false };
}
A missing preTokenBalances entry means the recipient's USDC token account was created in that same transaction, so the code treats it as 0. Matching on owner (not the token account address) handles that case.
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Otherwise anyone can pay with a worthless token that has the same decimals.confirmed for a fast checkout, or finalized for high-value orders.Create an order for 0.01 USDC, pay it from Phantom or Solflare, and confirm your verifier returns paid: true with the signature. Then open the signature on Solscan and check that the reference appears in the account list.
Related: generate a Solana Pay QR code · accept USDC on your website · WooCommerce
Payments are on-chain and final; if delivery fails, email smallbizaicourses@gmail.com with your order reference. Software provided as is. Not financial advice.