How to verify a Solana Pay payment (reference key, findReference, getSignaturesForAddress)

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.

1. Put a unique reference in the payment URL

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.

2. Option A: the @solana/pay helpers

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...)
  }
}

3. Option B: raw JSON-RPC (works in a Cloudflare Worker, no dependencies)

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.

4. Rules that prevent real losses

5. Test it

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

Skip the wiring: the Drop-In Kit (7 USDC) has the checkout and verifier; the Pro Kit (29 USDC) adds a cart, receipts and a dashboard; or have it installed.

Payments are on-chain and final; if delivery fails, email smallbizaicourses@gmail.com with your order reference. Software provided as is. Not financial advice.