HeadlessPayment Flows
Headless commerce primitives for any framework
Payment Flows
The @solana-commerce/headless package provides framework-agnostic functions for building commerce payment flows. These tools help create payment request objects, generate Solana Pay URLs, and verify on-chain payments. The package is designed to work in any JavaScript environment: React, Vue, Svelte, vanilla JS, Node.js, or serverless functions.
Installation
pnpm add @solana-commerce/headlessPayment Request Functions
These functions create standardized payment request objects for different commerce patterns. They don't perform any blockchain operations—they simply structure data for use in wallets, Solana Pay URLs, or custom payment UIs.
createBuyNowRequest()
Creates a standardized payment request for a single product purchase.
function createBuyNowRequest(
recipient: string,
product: any,
options?: {
memo?: string;
label?: string;
message?: string;
}
): PaymentRequestParameters
-
recipient(string, required) - Merchant wallet address (base58-encoded Solana public key) that will receive the payment. -
product(any, required) - Product object containing:price(number, required) - Product price. The unit depends on context (e.g., lamports for SOL, minor units for tokens).currency(string, required) - Payment currency. Can be'SOL'or a token mint address.name(string, required) - Product name, used for default memo/label if not provided.- Additional fields (id, description, image, etc.) are passed through in the
productsarray.
-
options(object, optional) - Customization options:memo(string) - On-chain memo for the transaction. Default:"Purchase: {product.name}".label(string) - Display label for the payment request (used in Solana Pay). Default:product.name.message(string) - Success message shown after payment. Default:"Thank you for purchasing {product.name}!".
Returns
A PaymentRequest object containing:
recipient- The merchant wallet addressamount- The product price (copied fromproduct.price)currency- Payment currency (copied fromproduct.currency)products- Array containing the single productmemo- Transaction memo (option or default: "Purchase: (product.name)")label- Payment label (option or default: "product.name")message- Success message (option or default: "Thank you for purchasing (product.name)!")
Example:
const payment = createBuyNowRequest(
'merchant-wallet-address',
{
id: 'prod_123',
name: 'Premium Subscription',
price: 50000000, // 0.05 SOL in lamports
currency: 'SOL'
},
{
label: 'Premium Subscription',
message: 'Thank you for subscribing!'
}
);createCartRequest()
Creates a payment request for multiple products in a shopping cart.
function createCartRequest(
recipient: string,
products: any[],
options?: {
memo?: string;
label?: string;
message?: string;
currency?: string;
}
): PaymentRequestParameters
-
recipient(string, required) - Merchant wallet address that will receive the payment. -
products(any[], required) - Array of product objects. -
options(object, optional) - Customization options:currency(string) - Payment currency for the entire cart.memo(string) - On-chain memo. Default:"Cart purchase (products.length items)".label(string) - Payment label. Default:"Cart Checkout".message(string) - Success message. Default:"Thank you for your purchase!".
Returns
A PaymentRequest object with:
recipient- The merchant wallet addressamount- Sum of all product prices (products.reduce((sum, p) => sum + p.price, 0))currency- Payment currency (from options, or undefined)products- The array of productsmemo,label,message- Option values or defaults
Example:
const cart = createCartRequest(
'merchant-wallet-address',
[
{ id: '1', name: 'Product A', price: 25 },
{ id: '2', name: 'Product B', price: 15 },
{ id: '3', name: 'Product C', price: 10 }
],
{
currency: 'USDC',
label: 'My Store Checkout',
message: 'Thank you for your order!'
}
);
// cart.amount === 50 (sum of prices)createTipRequest()
Creates a payment request for tips or donations with a user-defined amount.
function createTipRequest(
recipient: string,
amount: number,
options?: {
currency?: string;
memo?: string;
label?: string;
message?: string;
}
): PaymentRequestParameters
-
recipient(string, required) - Wallet address of the tip recipient (creator, streamer, charity, etc.). -
amount(number, required) - Tip amount. Unit depends on currency (lamports for SOL, minor units for tokens). -
options(object, optional) - Customization options:currency(string) - Payment currency. Default: undefined (typically treated as SOL by consumers).memo(string) - On-chain memo. Default:"Thank you for your support!".label(string) - Payment label. Default:"Tip".message(string) - Success message. Default:"Thanks for the tip!".
Returns
A PaymentRequest object with:
recipient- The tip recipient's wallet addressamount- The tip amountcurrency- Payment currency (from options, or undefined)memo,label,message- Option values or defaults
Example:
const tip = createTipRequest(
'creator-wallet-address',
5_000_000, // 0.005 SOL in lamports
{
currency: 'SOL',
label: 'Tip for Content Creator',
message: 'Thanks for the support!'
}
);Payment Verification Functions
These functions interact with Solana to verify transactions and wait for confirmations. They require a Solana RPC client from the gill library.
verifyPayment()
Verifies that a transaction exists on-chain and optionally validates the payment amount, recipient, and token.
async function verifyPayment(
rpc: SolanaClient['rpc'],
signatureString: string,
expectedAmount?: number,
expectedRecipient?: string,
expectedMint?: string
): Promise<PaymentVerificationResult>Parameters
-
rpc(SolanaClient['rpc'], required) - RPC client fromgill. Create withcreateSolanaClient(rpcUrl).rpc. -
signatureString(string, required) - Transaction signature (base58-encoded) to verify. -
expectedAmount(number, optional) - Expected payment amount to validate. Unit should match the currency:- For SOL: lamports (1 SOL = 1,000,000,000 lamports)
- For SPL tokens: minor units based on token decimals (e.g., USDC uses 6 decimals)
If not provided, amount validation is skipped.
-
expectedRecipient(string, optional) - Expected recipient wallet address. If provided (along withexpectedAmount), the function validates that the recipient received at least this amount. If not provided, validation is skipped. -
expectedMint(string, optional) - SPL token mint address. Only required for SPL token transfers. If not provided, the function assumes SOL transfer.
Returns
A Promise<PaymentVerificationResult> object:
interface PaymentVerificationResult {
verified: boolean; // True if payment is valid
signature?: string; // Transaction signature (echoed back)
amount?: number; // Expected amount (echoed back)
recipient?: string; // Expected recipient (echoed back)
error?: string; // Error message if verification failed
}Verification Logic
The function performs these checks:
-
Signature Validity: Checks that the signature is valid.
-
Transaction Existence: Fetches the transaction using
rpc.getTransaction(). If not found, returnsverified: false. -
Confirmation Status: Checks that the transaction has landed on-chain.
-
SOL Transfer Validation (if
expectedRecipientandexpectedAmountprovided, and noexpectedMint):- Finds the recipient's account index in the transaction
- Compares
preBalancesandpostBalancesto calculate balance delta - Verifies delta is at least
expectedAmount
-
SPL Token Transfer Validation (if
expectedMintprovided):- Derives the recipient's Associated Token Account (ATA) for both Token Program and Token-2022 Program
- Checks
postTokenBalancesfor a matching ATA with the expected mint - Verifies the token amount is at least
expectedAmount
Security Considerations
- Client-Side Verification: This function fetches transaction data from RPC. Do not expose your RPC URL to the client.
- Finalization: The function checks for confirmed transactions, but for high-value payments, consider waiting for
finalizedstatus.
Example:
import { verifyPayment } from '@solana-commerce/headless';
import { createSolanaClient } from 'gill';
const client = createSolanaClient({
urlOrMoniker: "mainnet",
});
const result = await verifyPayment(
client.rpc,
'transaction-signature-here',
50_000_000, // 0.05 SOL in lamports
'merchant-wallet-address'
// No mint = SOL transfer
);
if (result.verified) {
console.log('Payment confirmed!');
} else {
console.error('Verification failed:', result.error);
}waitForConfirmation()
Polls the blockchain until a transaction reaches confirmed or finalized status, or times out.
async function waitForConfirmation(
rpc: SolanaClient['rpc'],
signatureStr: string,
timeoutMs?: number
): Promise<boolean>Parameters
-
rpc(SolanaClient['rpc'], required) - RPC client fromgill. -
signatureStr(string, required) - Transaction signature to wait for. -
timeoutMs(number, optional) - Maximum time to wait in milliseconds. Default:30000(30 seconds).
Returns
Promise<boolean>- Returnstrueif transaction reachesconfirmedorfinalizedstatus within the timeout,falseotherwise.
Example:
import { waitForConfirmation } from '@solana-commerce/headless';
import { createSolanaClient } from 'gill';
const client = createSolanaClient({
urlOrMoniker: "mainnet",
});
// After sending transaction
const signature = await wallet.sendTransaction(transaction);
// Wait for confirmation (30 second timeout)
const confirmed = await waitForConfirmation(client.rpc, signature, 30000);
if (confirmed) {
console.log('Transaction confirmed!');
} else {
console.log('Timeout - transaction not confirmed within 30 seconds');
}Solana Pay Functions
These functions generate Solana Pay URLs and styled QR codes for mobile wallet scanning.
createSolanaPayRequest()
Creates a Solana Pay URL and a styled QR code.
async function createSolanaPayRequest(
request: TransferRequestURLFields,
options: SolanaPayRequestOptions
): Promise<{ url: URL; qr: string }>Parameters
-
request(TransferRequestURLFields, required) - Solana Pay transfer request fields:recipient- Recipient public key (usecreateRecipient(address)from@solana-commerce/solana-pay)amount- (optional) Payment amount in minor units (lamports for SOL)splToken(optional) - SPL token mint public key (usecreateSPLToken(address))reference(optional) - Reference public key for trackinglabel(optional) - Merchant namemessage(optional) - Success messagememo(optional) - On-chain memo
-
options(SolanaPayRequestOptions, required) - QR code styling options:size(number) - QR code width/height in pixels. Default:256.background(string) - Background color (hex/rgb). Default:'white'.color(string) - QR code color (hex/rgb). Default:'black'.margin(number) - Margin around QR code in modules.errorCorrectionLevel('L' | 'M' | 'Q' | 'H') - Error correction level. Higher levels allow more damage but create denser codes.logo(string) - Logo image URL to embed in center of QR code.logoSize(number) - Logo size as percentage of QR code size.logoBackgroundColor(string) - Background color behind logo.logoMargin(number) - Margin around logo.dotStyle('dots' | 'rounded' | 'square') - Shape of QR code modules.cornerStyle('square' | 'rounded' | 'extra-rounded' | 'full-rounded' | 'maximum-rounded') - Shape of corner markers.
Returns
A Promise resolving to an object:
url(URL) - Solana Pay URL (e.g.,solana:recipient?amount=10&spl-token=...)qr(string) - Base64-encoded data URL of the QR code image (use as<img src={qr} />)
Example:
import { createSolanaPayRequest, createRecipient, createSPLToken } from '@solana-commerce/solana-pay';
const payment = await createSolanaPayRequest(
{
recipient: createRecipient('merchant-wallet-address'),
amount: 10_000_000, // 0.01 SOL in lamports
splToken: createSPLToken('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), // USDC
label: 'My Store',
message: 'Thank you for your purchase!'
},
{
size: 400,
background: '#FFFFFF',
color: '#000000',
logo: '/logo.png',
logoSize: 20,
errorCorrectionLevel: 'H'
}
);
// Display QR code
document.getElementById('qr').src = payment.qr;
console.log(payment.url.toString()); // solana:merchant...?amount=10000000&spl-token=...