Building payments on Solana: a developer's checklist
Most Solana payment bugs are not smart contract bugs. They come from confirming at the wrong commitment level, trusting a client-reported signature, mishandling blockhash expiry, or depending on a single RPC endpoint.
Solana is a good fit for payments: sub-second block times, low and predictable fees, and a mature stablecoin ecosystem. But the properties that make it fast also make a naive integration fragile. The failure modes are specific and, fortunately, all avoidable. Here is the checklist worth walking before any Solana payment flow goes near real value.
1. Confirm at the right commitment level
Solana exposes three commitment levels, and choosing between them is a payment decision, not a performance tweak.
- processed — a node has seen the transaction. It can still be dropped. Never treat this as received value.
- confirmed — a supermajority of validators has voted on the block. Fast, and appropriate for optimistic user feedback.
- finalized — the block is rooted and will not be reverted. This is the level to use before crediting, converting or paying out against a deposit.
The practical pattern: show progress at confirmed so the interface feels responsive, and gate any irreversible downstream action on finalized. Conflating the two is how a payment gets credited against a transaction that never rooted.
2. Never trust a client-reported transaction
A browser telling your backend "here is the signature, I paid" is an assertion, not evidence. Verify server-side, against an RPC node, and check every field that matters:
- 1The transaction exists at finalized commitment and has no error.
- 2The mint is the exact expected token mint, not a lookalike with a similar symbol.
- 3The amount matches the expected amount in the token's smallest unit, accounting for decimals.
- 4The destination is the address your system controls or expects for that specific payment.
- 5The signature has not already been consumed by another payment record — idempotency at the signature level.
3. Understand blockhash expiry
Every Solana transaction references a recent blockhash and is only valid for a limited window — roughly a minute in practice. This has direct product consequences for any flow where a human reviews something before signing.
- A transaction built, then left on screen while the user reads a confirmation dialog, can expire before it is signed.
- An expired transaction fails cleanly rather than executing late, which is the desirable behaviour — but only if your interface explains it and offers a rebuild.
- Capture the last valid block height alongside the transaction, so you can distinguish "expired" from "failed" and give an accurate message.
Design for rebuild-and-retry rather than long-lived transactions. A flow that constructs the transaction immediately before signing, and refreshes it if the user pauses, is both safer and easier to explain.
4. Handle associated token accounts explicitly
SPL tokens live in associated token accounts, not directly at a wallet address. A recipient who has never held a given token has no account for it, and one must be created — which costs rent and requires an instruction in the transaction. Payment integrations trip over this in two ways: assuming the account exists, and forgetting that the account's owner is what determines who controls the balance. Check for existence, create when needed, and always resolve the owner rather than assuming it from the address you were given.
5. Treat RPC as infrastructure, not a URL
Your entire view of the chain comes through an RPC endpoint. Public endpoints rate-limit aggressively and are unsuitable for anything user-facing at volume. Practical requirements:
- A dedicated provider endpoint for primary traffic, with the credential held server-side only.
- A documented fallback path, so a provider outage degrades into read-only or reduced functionality instead of a blank error.
- Cluster awareness — mainnet, devnet and testnet configured separately and never mixed in one code path.
- A health check your own system can poll, so you can tell users "network connectivity is degraded" rather than showing a generic failure.
- Timeouts and retry limits on every call. An unbounded RPC call in a request handler is an outage waiting to happen.
Priority fees and congestion
Base fees on Solana are low, but during congestion inclusion depends on priority fees and compute unit limits. A payment flow that never sets them will sometimes simply not land. Set a compute unit limit and a priority fee, and make both adjustable — hard-coded values from a calm week will not hold during a busy one.
6. Keep signing where it belongs
Private keys belong in the user's wallet. A payment product should build unsigned transactions, hand them to a wallet such as Phantom or Solflare for review and signature, and submit the signed result. No part of a well-designed flow needs a seed phrase or private key, and any interface that asks for one should be treated as an attack.
Two guards are worth building in explicitly. First, destination verification: validate server-side that the output of a swap or transfer goes only to the address the user connected, and reject any request carrying extra destination fields. Second, amount limits: enforce minimum and maximum amounts on the server as well as in the interface, so a modified client cannot exceed them.
7. Make state machines and idempotency non-negotiable
Payments are long-lived objects with a defined set of transitions, not a boolean. Model them that way: an explicit status, a recorded reason for every transition, and an append-only event log. Then make every externally triggered operation idempotent, keyed on something stable — a client-supplied request identifier or the transaction signature itself. Networks retry. Users double-click. Webhooks arrive twice. Without idempotency, each of those becomes a duplicate payment.
8. Give users verifiable receipts
One of the genuine advantages of settling on a public chain is that the user can check your work. Surface the transaction signature, link to a block explorer, and show the exact amounts and rate used. A receipt that can be independently verified is worth more than a confident status message.
Where LamportPay sits on this checklist
The Solana legs of LamportPay are built against these rules: mints are pinned as constants, transaction verification happens server-side at finalized commitment, RPC access is cluster-aware with a documented fallback, the swap output is guarded to the connected wallet only, and amounts are capped on both client and server. Signing always happens in the user's own wallet. The fiat payout leg beyond USDC settlement is a labelled simulation, pending a regulated payout infrastructure partner.
See it in the product
Related reading
- Why stablecoins are the settlement asset for cross-border paymentsStablecoins solve a specific problem in cross-border payments: pre-funding. Here is what they replace, what they do not replace, and the risks that remain.
- Web3 payment rails: the reference architectureA component-by-component architecture for a Web3 payment rail — wallet layer, routing, verification, state machine, payout integration, and the observability that holds it together.
