All articles
solanasolana-paypaymentsstablecoins

Solana Pay: the underrated spec every wallet already implements

Every major Solana wallet supports Solana Pay. Almost nobody writes about it. Here's the spec, the QR flow, and why it's the easiest checkout to ship.

Solana Pay has the strange status of being both ubiquitous and ignored. Every wallet on the network — Phantom, Backpack, Solflare, Glow, Exodus, every mobile wallet — supports it. The spec was finalised in 2022. There are hundreds of POS systems shipping with Solana Pay support today. And almost nobody writes about it.

It's the simplest possible thing: a URL scheme for payment requests. Anyone with a Solana wallet can scan a QR code, click a link, or follow a deeplink to a Solana Pay URL, and their wallet will surface a pre-filled transaction prompt. Approve, sign, done.

The transfer request URL

The simplest form. A static request to send SOL (or any SPL token) to a specific address:

text
solana:RECIPIENT_PUBKEY?amount=10&label=Coffee&message=Thanks&memo=order-1234

# With an SPL token (USDC):
solana:RECIPIENT_PUBKEY?amount=10&spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&label=Coffee

That URL, encoded as a QR code, is a complete payment request. Every Solana wallet recognises it. The user scans, sees "Send 10 USDC to Coffee", confirms, signs.

Generating the QR code

typescript
import { encodeURL, createQR } from "@solana/pay"
import { PublicKey } from "@solana/web3.js"
import BigNumber from "bignumber.js"

const url = encodeURL({
  recipient: new PublicKey("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"),
  amount: new BigNumber(10),
  splToken: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
  reference: new PublicKey("..." /* unique ref pubkey for this order */),
  label: "Coffee",
  message: "Thanks for your purchase",
  memo: "order-1234",
})

// QR code as an SVG
const qr = createQR(url, 320, "transparent")
const svg = await qr.getRawData("svg")

The transaction request URL

The static form is fine for fixed-price items. For dynamic flows (variable amounts, server-built transactions, time-limited offers) there's the transaction request form: the URL points to your HTTPS endpoint, and the wallet POSTs back to fetch a server-built transaction.

text
solana:https://api.your-shop.com/pay?orderId=1234

The wallet makes two HTTP calls to that endpoint:

  1. GET — your server returns a label and icon to show in the wallet UI.
  2. POST (with the user's pubkey in the body) — your server returns a base64-encoded transaction for the user to sign.
typescript
// Next.js route handler
export async function GET() {
  return Response.json({ label: "Your Shop", icon: "https://your-shop.com/icon.png" })
}

export async function POST(req: Request) {
  const { account } = await req.json() // the user's pubkey

  const tx = new Transaction()
  tx.add(
    /* your instructions — e.g. token transfer + a reference key + a memo */
  )
  tx.feePayer = new PublicKey(account)
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash

  return Response.json({
    transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
    message: "Thanks for your order",
  })
}

Watching for payment

The reference field is the key for confirmation. Each order generates a unique reference pubkey (just a randomly generated keypair — only the address matters). Include it as a read-only account in the transaction. After payment, you poll Solana for any transaction containing that reference:

typescript
import { findReference, FindReferenceError } from "@solana/pay"

const reference = new PublicKey("...") // the unique ref for this order

try {
  const sig = await findReference(connection, reference, { finality: "confirmed" })
  console.log("Paid! signature:", sig.signature)
} catch (err) {
  if (err instanceof FindReferenceError) {
    // Not yet paid — poll again or show "waiting" UI
  }
}

Your server now has the signature. You verify the amount, recipient, and token mint match the expected order, then fulfill.

Why nobody talks about it

Solana Pay is small, finished, and not currently the focus of any major announcement push. There's no token, no SDK arms race, no aggressive marketing. It just works, has worked for three years, and shows up quietly in checkout flows for Shopify integrations, physical-world POS systems, in-app purchases, and tipping flows.

Which is exactly why it's the right primitive when you need to ship a payment. Wallets already understand it. Users already know how to use it. The spec hasn't changed.

References

If you're building anything that needs to accept Solana payments — a checkout, a paywall, a donation flow, a coffee shop register — start here. The simplest path that already works everywhere.

Solana Pay: the underrated spec every wallet already implements | devrels.xyz