If you're building a Solana frontend that lets users connect wallets and submit transactions, you're going to spend more time on wallet integration than the docs suggest. The Solana wallet ecosystem is reasonable but not perfect; the SDKs handle most of the work but the rough edges become production issues if you don't address them.
This piece covers what you actually need to know to integrate Solana wallets correctly: the SDK choices, the signing patterns, transaction submission, and the production concerns that distinguish working integrations from production-grade ones.
What "Solana Wallet API" Actually Means
The term covers a few different things:
- Wallet adapter SDKs — libraries that let your frontend connect to user wallets (Phantom, Solflare, Backpack, etc.) and request signatures
- Wallet provider APIs — the standardised interface wallets expose (
window.solanastyle) that adapters wrap - RPC API for wallet operations — reading balances, fetching token holdings, getting transaction history
We'll cover the first two here. RPC details for wallets are mostly covered in Solana RPC for Developers.
The SDK Landscape
For most frontend integrations, the choice is @solana/wallet-adapter. It handles:
- Multiple wallet detection (Phantom, Solflare, Backpack, Glow, etc.)
- Connection state management
- Transaction signing requests
- Network switching
For React frontends specifically, the related packages provide hooks:
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";
const wallets = [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
];
function App() {
return (
<ConnectionProvider endpoint={RPC_URL}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<YourApp />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}
For non-React apps, use the lower-level @solana/wallet-adapter-base directly.
Basic Connection Flow
Standard pattern in a React component:
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
function MyComponent() {
const { connection } = useConnection();
const { publicKey, signTransaction, sendTransaction } = useWallet();
if (!publicKey) {
return <WalletMultiButton />;
}
return <YourConnectedUI publicKey={publicKey} />;
}
Key things from the wallet adapter:
publicKey— the user's public key once connected, null otherwisesignTransaction— request a signature without submittingsendTransaction— sign and submit in one callsignAllTransactions— sign a batch (some wallets support this; some don't)signMessage— sign arbitrary message data (for auth flows)
Sending Transactions Through Wallets
Two patterns:
Pattern 1: wallet adapter handles submission.
const tx = new Transaction().add(yourInstruction);
const signature = await sendTransaction(tx, connection, {
skipPreflight: true,
maxRetries: 0,
});
The wallet adapter signs and submits via the connection you provide. Simple; works fine for most cases.
Pattern 2: sign separately, submit yourself.
const tx = new Transaction().add(yourInstruction);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = publicKey;
const signed = await signTransaction(tx);
const signature = await connection.sendRawTransaction(signed.serialize(), {
skipPreflight: true,
maxRetries: 0,
});
This pattern gives you more control over submission. Useful when you want to use a different RPC for sends than the one passed to the wallet provider, or when you need to inspect/log the signed transaction before submission.
Why You Often Want Two Different RPCs
A pattern many production apps use: one RPC for reads (passed to ConnectionProvider), another for writes (used directly for sendRawTransaction).
Reasoning:
- Read traffic profile — high frequency, parallel queries, often cached. Optimised for read-heavy throughput.
- Write traffic profile — sub-second confirmation needed, Anti-MEV protection critical, latency-sensitive. Optimised for transaction submission.
Using one RPC for both is a compromise. Using the right one for each gives better user experience (snappier UI from the read RPC) and better economics (lower sandwich tax from the write RPC).
const READ_RPC = "https://your-read-rpc.example/?api-key=...";
const WRITE_RPC = "https://bolttx.io/?api-key=...";
const readConnection = new Connection(READ_RPC, "confirmed");
const writeConnection = new Connection(WRITE_RPC, "processed");
// Pass readConnection to your provider
<ConnectionProvider endpoint={READ_RPC}>...</ConnectionProvider>
// Use writeConnection directly for sends
const signature = await writeConnection.sendRawTransaction(signed.serialize(), {
skipPreflight: true,
maxRetries: 0,
});
Common Wallet Integration Mistakes
Hardcoding wallet adapters. Lock the user into one wallet. Always include multiple options.
Ignoring connection failures. wallet-adapter will throw on rejection or wallet not installed. Wrap in try/catch and show meaningful errors.
Not handling network mismatch. User connected on devnet but your app expects mainnet. Detect and prompt to switch.
Polling the wallet for state. The hooks already give you reactive state. Don't poll wallet.publicKey in intervals.
Submitting transactions without confirmation. The wallet returns a signature but the transaction may not have landed. Always confirm.
Forgetting blockhash freshness. Same issue as anywhere: stale blockhashes silently fail. Refresh before signing.
Not handling the user-cancellation case. Some wallets return a specific error code; handle it gracefully (don't show "transaction failed" if the user cancelled).
Mobile Wallet Integration
Mobile is its own thing. Mobile wallets use deep links for transaction signing — your dApp opens the wallet via a deep link, the wallet signs, then redirects back. The flow is more complex than desktop:
- Build the transaction
- Encode it for the wallet's deep link format
- Open the deep link
- Handle the redirect-back with the signed transaction
- Submit the signed transaction yourself
Most teams use Mobile Wallet Adapter (MWA) which abstracts this. If you're building a mobile-first dApp, plan time for this — it's not the desktop flow.
Common Patterns by App Type
DEX UIs / swap interfaces. Quote on read RPC, sign via wallet adapter, submit through Anti-MEV write RPC. Show fill prices vs expected fills as user feedback.
NFT marketplaces. Heavy read traffic for listings; writes for purchases/listings. Both need to be reliable; reads dominate by volume.
Lending / borrowing dApps. Mixed read/write. Reads must be accurate (don't show stale collateral health). Writes must confirm reliably.
Token launchpads / IDOs. Burst write traffic during launches. The write RPC's behaviour during congestion is critical — exactly when you need it most is when most providers degrade.
Wallet apps themselves. Heavy read for balance/history, occasional write for transactions. Read-RPC quality dominates user perception.
What to Do This Week
If you're building a wallet-integrated dApp:
- Set up wallet-adapter with multiple wallet options. Include at least Phantom, Solflare, Backpack.
- Use separate read and write RPCs. Read for the provider, write for
sendRawTransaction. - Sign separately, submit yourself. Gives you control over the submission path.
- Confirm transactions after submission. Don't trust that signature returned means landed.
- Handle the cancellation case. Don't show errors for user-rejected transactions.
- Test on multiple wallets. Phantom and Solflare have subtle behavior differences.
- Add per-signature telemetry on the write side. Track what users actually experience.
Try BoltTx for Wallet App Submission
BoltTx is purpose-built for the write side of wallet-integrated dApps:
- Sub-second confirmation for snappy user experience
- Native Anti-MEV routing so user swaps aren't sandwiched
- SWQoS-aware delivery for inclusion under congestion (when high-traffic events happen)
- Per-signature telemetry for debugging what users experienced
- Tip-based pricing that aligns cost with successful user transactions
Drop-in usage:
import { Connection } from "@solana/web3.js";
const writeConnection = new Connection(
"https://bolttx.io/?api-key=YOUR_API_KEY",
"processed"
);
const signed = await wallet.signTransaction(tx);
const signature = await writeConnection.sendRawTransaction(signed.serialize(), {
skipPreflight: true,
maxRetries: 0,
});
Free tier signup. Pair with your read RPC of choice. Test against your real user transaction patterns for a week.
FAQ
Should I use wallet-adapter or roll my own? Use wallet-adapter. Rolling your own wallet detection is a tar pit you don't want.
Which wallets should I support? At minimum: Phantom, Solflare. Adding Backpack and Glow covers most users. Mobile wallet adapter for mobile.
How do I handle the user being on devnet when my app needs mainnet? Detect via the connection's genesis hash and prompt them to switch in their wallet. Don't try to switch programmatically.
What's the right way to sign messages for auth?
signMessage from the wallet adapter. Generate a server-side nonce, have the user sign it, verify the signature on your backend.
Do all wallets support signAllTransactions?
Most major ones do; some smaller wallets don't. Test on your target wallets.
Why do my transactions sometimes show as "successful" but the user didn't get the result?
Signature returned doesn't mean transaction landed. Always confirm with the connection. Use confirmTransaction after sending.
Phantom vs Solflare — which should I prioritise supporting? Both. Phantom vs Solflare is the question users ask when choosing a wallet, but for an integrator, you support both via wallet-adapter and the user picks. Phantom has the larger user base; Solflare has stronger desktop / hardware features. For a web3 wallet development solution — i.e., you're the dApp side integrating wallets — wallet-adapter abstracts the difference so you don't really code against either one specifically.