perkun.eu Services Portfolio Blog About Contact PL
← Blog

2/11/2026

Solana Pay — transaction verification and avoiding timeouts with public RPC

TL;DR: Public Solana RPC (api.mainnet-beta.solana.com) has rate limiting and timeouts. Private RPC (Helius free tier) = stable, < 1s responses. Change one .env variable.

Deploying Solana Pay in a Laravel application seems simple — the documentation is good, the SDK exists, examples work. The problem starts in production when the first real transactions hit the public Solana RPC endpoint and timeouts begin. The user pays, waits, the page spins, the subscription doesn’t activate. This isn’t a bug in your code — it’s the architecture of the public RPC, which isn’t designed for production use.

The problem with public RPC

The public endpoint https://api.mainnet-beta.solana.com is a node shared by all developers who don’t want to pay for their own RPC. The effects are predictable: 429 Too Many Requests errors with just two requests per second, timeouts exceeding 30 seconds during network congestion, random 503 errors during heavy load across the Solana network.

In practice it looks like this: the user scans a QR code, makes a transfer from a mobile wallet. Your application starts polling — every 2 seconds it checks whether the transaction has arrived at the reference address. After 3 requests you get a 429. Laravel logs an error. The user sees “Checking payment…” for 5 minutes, then closes the page and writes to support.

Helius RPC — free tier

Helius is one of several private RPC node providers for Solana. The free tier offers 25 req/s, 100,000 requests per day without a credit card — sufficient for a small subscription platform handling a few dozen transactions per day.

Registration via email at helius.dev, no card verification required. After registration you get a URL in the format:

https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY

That’s the only change you need. One URL instead of another.

Configuration in Laravel

Create a config/solana.php file:

<?php

return [
    'rpc_url' => env('SOLANA_RPC_URL', 'https://api.mainnet-beta.solana.com'),
    'usdc_mint' => env('SOLANA_USDC_MINT', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
    'destination_address' => env('SOLANA_DESTINATION_ADDRESS'),
];

In your .env file:

SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY
SOLANA_DESTINATION_ADDRESS=YourUSDCWalletAddress

No changes to the verification code — just swap the URL in configuration.

Retry logic

Even a private RPC can return a timeout during momentary Solana network congestion. Retry logic with exponential backoff is a mandatory element of any integration:

public function callRpc(string $method, array $params = []): array
{
    $attempts = 3;
    $delay = 1;

    for ($i = 1; $i <= $attempts; $i++) {
        try {
            $response = Http::timeout(10)
                ->post(config('solana.rpc_url'), [
                    'jsonrpc' => '2.0',
                    'id' => 1,
                    'method' => $method,
                    'params' => $params,
                ]);

            return $response->json();
        } catch (\Exception $e) {
            Log::warning('Solana RPC timeout', [
                'attempt' => $i,
                'method' => $method,
                'url' => config('solana.rpc_url'),
                'error' => $e->getMessage(),
            ]);

            if ($i < $attempts) {
                sleep($delay);
                $delay *= 2; // 1s, 2s, 4s
            }
        }
    }

    throw new \RuntimeException("Solana RPC failed after {$attempts} attempts");
}

Every timeout is logged in Laravel with the attempt number and RPC method. This lets you monitor whether Helius is working correctly and whether the free tier limit is approaching exhaustion.

Transaction verification — complete PHP code

Full flow for USDC transaction verification in Laravel:

public function verifyPayment(string $referenceAddress, float $expectedAmount): bool
{
    // Step 1: find transaction signatures for reference address
    $signaturesResponse = $this->callRpc('getSignaturesForAddress', [
        $referenceAddress,
        ['limit' => 10],
    ]);

    $signatures = $signaturesResponse['result'] ?? [];
    if (empty($signatures)) {
        return false; // No transactions
    }

    // Step 2: check each signature
    foreach ($signatures as $sigInfo) {
        $txResponse = $this->callRpc('getTransaction', [
            $sigInfo['signature'],
            ['encoding' => 'jsonParsed', 'maxSupportedTransactionVersion' => 0],
        ]);

        $tx = $txResponse['result'] ?? null;
        if (!$tx) continue;

        // Check confirmations
        if (($tx['meta']['confirmationStatus'] ?? '') !== 'finalized') continue;
        if (($tx['meta']['err'] ?? 'error') !== null) continue;

        // Check USDC token transfer
        $tokenBalances = $tx['meta']['postTokenBalances'] ?? [];
        foreach ($tokenBalances as $balance) {
            if ($balance['mint'] !== config('solana.usdc_mint')) continue;
            if ($balance['owner'] !== config('solana.destination_address')) continue;

            $amount = (float) $balance['uiTokenAmount']['uiAmount'];
            // Tolerance ±1 USDC for fee differences
            if (abs($amount - $expectedAmount) <= 1.0) {
                return true;
            }
        }
    }

    return false;
}

Three conditions must be met: the transaction has finalized status (not just confirmed), there is no error in meta.err, and the USDC token amount at your recipient address matches the expected amount with ±1 USDC tolerance.

Summary

Switching from the public RPC to Helius solves 95% of Solana Pay timeout issues in production. The Helius free tier is sufficient for platforms with a few dozen transactions per day. Add retry with exponential backoff and logging — you have a solid foundation for crypto payments in Laravel without external packages.