3/11/2024
Solana Pay — crypto payment integration in PHP without libraries
TL;DR: Solana Pay is a URL specification for SOL/USDC payments. In PHP you verify transactions via the Solana JSON RPC API — one endpoint, no libraries. Transaction confirmation time: 400ms–2s instead of 10 minutes like Bitcoin.
Cryptocurrency payments are often associated with complex integrations, specialized libraries, and the need to understand dozens of blockchain concepts. Solana Pay flips this thinking: you generate a URL according to the spec, the customer’s wallet does all the work, and you just verify whether the transaction arrived. In PHP you need a single HTTP endpoint.
How Solana Pay works
The Solana Pay specification defines a URL format that crypto wallets (Phantom, Solflare, Backpack) know how to interpret:
solana:{recipient_address}?amount={amount}&spl-token={token_address}&reference={reference_key}&label={label}&message={message}
When a customer scans a QR code containing this URL (or taps a link on mobile), the wallet automatically pre-fills the payment form: recipient address, amount, token. The customer just approves the transaction with biometrics or a PIN.
As the merchant, you generate a unique reference for each payment — a random Solana public key (32 bytes in base58). After the customer submits the payment, the wallet attaches this key to the transaction as an “account key.” You then verify whether a transaction with that reference has appeared on the blockchain.
Generating the URL in PHP
function generateSolanaPayUrl(
string $recipientAddress,
float $amountUsdc,
string $reference,
string $label = 'My Shop',
string $message = 'Payment for order'
): string {
$usdcMintAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC mainnet
$params = http_build_query([
'amount' => number_format($amountUsdc, 6, '.', ''),
'spl-token' => $usdcMintAddress,
'reference' => $reference,
'label' => $label,
'message' => $message,
]);
return 'solana:' . $recipientAddress . '?' . $params;
}
// Generating a reference (random public key as base58)
function generateReference(): string {
$bytes = random_bytes(32);
return base58_encode($bytes); // custom base58 implementation
}
Note that amount in the URL for USDC is the value in whole USDC with a decimal point — not in “lamports” or “micro-USDC.” Wallets interpret this value and convert to internal units (USDC has 6 decimal places).
Verifying a transaction via RPC
Verification happens in two steps. First, you look up the transaction by the reference key:
function findTransactionByReference(string $reference, string $rpcUrl): ?string {
$response = solanaRpc($rpcUrl, 'getSignaturesForAddress', [
$reference,
['limit' => 5, 'commitment' => 'confirmed']
]);
if (empty($response['result'])) {
return null;
}
return $response['result'][0]['signature'];
}
function verifyPayment(string $signature, float $expectedUsdc, string $rpcUrl): bool {
$tx = solanaRpc($rpcUrl, 'getTransaction', [
$signature,
['encoding' => 'jsonParsed', 'commitment' => 'confirmed']
]);
if (!$tx['result'] || $tx['result']['meta']['err'] !== null) {
return false; // transaction doesn't exist or has an error
}
// Check token transfers in postInstructions
foreach ($tx['result']['transaction']['message']['instructions'] as $ix) {
if (isset($ix['parsed']['type']) && $ix['parsed']['type'] === 'transferChecked') {
$info = $ix['parsed']['info'];
$actualAmount = (float)$info['tokenAmount']['uiAmount'];
if (abs($actualAmount - $expectedUsdc) < 0.001) {
return true;
}
}
}
return false;
}
function solanaRpc(string $url, string $method, array $params): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'jsonrpc' => '2.0', 'id' => 1, 'method' => $method, 'params' => $params
]),
CURLOPT_TIMEOUT => 5,
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
Handling timeout and retry
The public Solana RPC (https://api.mainnet-beta.solana.com) has aggressive rate limiting — after a few requests in quick succession you’ll get HTTP 429. For production applications, use a private RPC:
- Helius — 100k requests/day free, then $0.000025/request
- QuickNode — similar model
- Triton — dedicated for larger projects
An exponential backoff retry implementation for payment verification (polling every few seconds for up to 5 minutes) should account for these limits by adding sleep between attempts and handling 429 responses.
Pitfalls
USDC on Solana has 6 decimal places (not 9 like native SOL). If you store the amount in the database as an integer “micro-USDC,” remember that 1 USDC = 1,000,000 base units. An error in this conversion is the classic cause of verification failures.
Second issue: don’t use your wallet’s public address as the reference. The reference must be a new, random public key for each transaction. Reusing the same reference for different payments means getSignaturesForAddress will return multiple transactions and you won’t know which one is current.
Summary
Solana Pay in PHP is about 100 lines of code with no external dependencies. You generate a URL with the payment specification, the customer pays with their mobile wallet, and you verify via JSON RPC. Transaction fees for the customer are a fraction of a cent, confirmation time is under 2 seconds. For USDC subscriptions in TTRPG or SaaS projects — an ideal solution with no intermediaries.