Okay, here's the deal. Most "Solana API" guides out there? They drag you through endless setup hell-install this CLI, configure that wallet, spin up a full node just to say hello. Hours wasted. You just wanna build something fast with Express, right? Like, slap together an API that handles Solana payments or queries in minutes. Not days.
That's what Express Solana API stuff gets wrong everywhere. They pretend you're building a spaceship when you need a skateboard. In my experience, you can have a working Express server hitting Solana RPC, verifying payments, all in under 10 minutes if you cut the crap. Why does this matter? 'Cause Solana's fast as hell-your API should be too.
solana airdrop 2 --url devnet.Make a folder. mkdir solana express fast. cd in. npm init -y. Boom. Project born.
So, install the essentials. Run this:
npm i express @solana/web3.js @solana/spl token
Now, your app.js. Start simple. Connect to devnet RPC. Public one's fine for starters-https://api.devnet.solana.com. Fees? Like ~0.000005 SOL per tx. Dirt cheap.
const express = require('express');
const { Connection, clusterApiUrl } = require('@solana/web3.js'); const app = express();
app.use(express.json()); const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); app.get('/balance/:address', async (req, res) => { const pubkey = new PublicKey(req.params.address); try { const balance = await connection.getBalance(pubkey); res.json({ balance: balance / 1e9, inSOL: (balance / 1e9).toFixed(4) }); } catch (err) { res.status(500).json({ error: err.message }); }
}); app.listen(3000, () => console.log('Server on 3000'));
Test it. node app.js. Hit http://localhost:3000/balance/YourWalletHere. Gets your SOL balance. Took 2 minutes. Sound familiar? This is what fast feels like.
Public RPCs throttle you hard. 100 calls/sec max, then 429 errors. Fix? Use a paid one later like Helius or Alchemy. Free tier's 25M credits/month. But for now? Devnet's chill.
Now the fun part. Ever want your API to charge USDC before spilling secrets? That's x402. HTTP 402 Payment Required, but Solana style. Client pays, server verifies, done. No middleman.
I usually start with this 'cause it's killer for premium endpoints. Price it tiny: 0.0001 USDC. That's like a coffee crumb.
Create your recipient wallet. Use Phantom or solana keygen new. Note the pubkey. Say it's seFkxFkXEY9JGEpCyPfCWTuPZG9WK6ucf95zvKCfsRX.
Derive token accounts. USDC mint on devnet: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.
Add to your Express app. Middleware handles 402 quotes and verifies.
Here's the code. Paste it in.
const { PublicKey, Transaction, TOKENPROGRAMID, getAssociatedTokenAddress } = require('@solana/web3.js');
const { createTransferInstruction, getOrCreateAssociatedTokenAccount } = require('@solana/spl token'); const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const RECIPIENT_WALLET = new PublicKey('seFkxFkXEY9JGEpCyPfCWTuPZG9WK6ucf95zvKCfsRX');
const RECIPIENTTOKENACCOUNT = await getAssociatedTokenAddress(USDCMINT, RECIPIENTWALLET);
const PRICE_USDC = 100n; // 0.0001 USDC (6 decimals)
Wait, async derive? Do it outside. Now the endpoint.
app.get('/premium', async (req, res) => { const xPaymentHeader = req.header('X Payment'); if (xPaymentHeader) { // Verify payment try { const paymentData = JSON.parse(Buffer.from(xPaymentHeader, 'base64').toString()); const txBuffer = Buffer.from(paymentData.payload.serializedTransaction, 'base64'); const tx = Transaction.from(txBuffer); // Check for SPL transfer let valid = false; for (const ix of tx.instructions) { if (ix.programId.equals(TOKENPROGRAMID) && ix.data === 3) { const amount = Number(ix.data.readBigUInt64LE(1)); const dest = ix.keys?.pubkey; if (dest.equals(RECIPIENTTOKENACCOUNT) && amount >= Number(PRICEUSDC)) { valid = true; break; } } } if (!valid) return res.status(402).json({ error: 'Invalid payment' }); // Submit tx const sig = await connection.sendRawTransaction(txBuffer); await connection.confirmTransaction(sig, 'confirmed'); res.json({ message: '🎉 Premium access!', secret: 'Top secret data', sig }); } catch (err) { res.status(402).json({ error: err.message }); } } else { // Quote payment res.status(402).json({ payment: { recipient: RECIPIENTWALLET.toBase58(), amount: PRICEUSDC.toString(), mint: USDCMINT.toBase58(), amountUSDC: 0.0001 } }); }
});
Restart. Hit GET /premium. Get 402 with payment deets. Now build client to pay.
Don't skip this. Clients gotta build the tx, sign, retry with header. Node example.
const fetch = require('node fetch');
const { Keypair, Transaction } = require('@solana/web3.js');
const { createTransferInstruction, getOrCreateAssociatedTokenAccount } = require('@solana/spl token'); // Load your keypair from json file
const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(require('fs').readFileSync('client.json')))); async function payAndAccess() { // 1. Get quote const quote = await fetch('http://localhost:3000/premium'); const q = await quote.json(); const mint = new PublicKey(q.payment.mint); const amount = BigInt(q.payment.amount); // 2. Payer token account const payerATA = await getOrCreateAssociatedTokenAccount(connection, payer, mint, payer.publicKey); // Check balance - bail if low const bal = await connection.getTokenAccountBalance(payerATA.address); if (Number(bal.value.amount) < Number(amount)) throw new Error('Low funds bro'); // 3. Build tx const tx = new Transaction(); const transferIx = createTransferInstruction(payerATA.address, RECIPIENTTOKENACCOUNT, payer.publicKey, amount); tx.add(transferIx); tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; tx.sign(payer); // 4. Serialize and send with header const serializedTx = bs58.encode(tx.serialize()); const xPayment = Buffer.from(JSON.stringify({ payload: { serializedTransaction: serializedTx } })).toString('base64'); const paid = await fetch('http://localhost:3000/premium', { headers: { 'X Payment': xPayment } }); console.log(await paid.json());
}
Run it. Pays 0.0001 USDC. Gets premium data + sig. Verify on explorer. Boom.
The thing is, token accounts mess people up. Recipient doesn't have one? Client adds create instruction. Like this:
// Before transfer
const recipientATA = await getAssociatedTokenAddress(USDCMINT, RECIPIENTWALLET);
try { await connection.getAccount(recipientATA);
} catch { tx.add(createAssociatedTokenAccountInstruction(payer.publicKey, recipientATA, RECIPIENTWALLET, USDCMINT));
}
Also, decimals. USDC is 6. So 1 USDC = 1000000. Your PRICE_USDC = 100n for 0.0001.
Network? Stick to devnet first. Mainnet fees same, but use real USDC. Bridge or buy.
| Network | RPC URL | USDC Mint | Gas per Tx |
|---|---|---|---|
| Devnet | https://api.devnet.solana.com | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | ~0.000005 SOL |
| Mainnet | https://api.mainnet.solana.com | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | ~0.000005 SOL |
Want largest accounts? Like getLargestAccounts RPC.
app.get('/top holders', async (req, res) => { const data = await connection.getLargestAccounts(); res.json(data.value.slice(0, 10).map(acc => ({ address: acc.address.toBase58(), lamports: acc.lamports / 1e9 })));
});
Or send SOL. But careful, feePayer signs.
Honestly, mix RPC calls with payments. Charge for top holders list. 0.001 USDC. Scale it.
In my experience, 90% crashes from bad pubkeys. Validate with new PublicKey(str) in try catch.
One endpoint? Lame. Middleware all routes.
const paymentMiddleware = (recipient, prices) => { return (req, res, next) => { const endpoint = req.path; const price = prices[endpoint]; if (!price) return next(); // Quote or verify logic here };
};
app.use(paymentMiddleware(RECIPIENT_WALLET, { '/premium': { price: '0.0001', network: 'devnet' }, '/top': { price: '0.001', network: 'devnet' }
}));
Applies to all. Clients auto pay. Magic.
Frontend? Wrap fetch with payment handler libs if lazy. But manual's lighter.
Potential issue: Blockhash expires. Fetch fresh every time. lastValidBlockHeight saves you.
Why bother? 'Cause users hate "tx expired" errors. Refresh blockhash = happy users.
SOL transfers? Swap SPL for system transfer.
const transferSOLIx = SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: recipient, lamports: 1000000 // 0.001 SOL
});
PDAs? For program owned accounts.
['my pda', Buffer.from('data')]const [pda] = await PublicKey.findProgramAddress(seeds, programId);Solana Actions? Like REST but tx powered. GET quotes, POST builds tx. Use @solana/actions sdk. Express works perfect.
That's your base. 5 minutes to balance checker. 15 to paid API. Tweak, deploy to Render or Fly. You're live.
Hit snags? Pubkey format. Always base58. Check with solana explorer.
| Action | Cost (SOL) | USDC equiv (6dec) |
|---|---|---|
| RPC Call (balance) | 0 | 0 |
| Simple Tx Submit | 0.000005 | ~0.00003 |
| Token Transfer | 0.000005 | 100 (your price) |
| ATA Create | 0.00203928 | N/A |
ATA create hurts first time. Client pays it. Budget 0.01 SOL start.
Now build. Tweak. Share what you make. Questions? Imagine I'm texting back.