Okay, so your friend hits you up: "Hey, can you make a script that sends me 0.1 SOL on devnet real quick?" That's the exact spot I was in last week. Ended up firing up my terminal, grabbing some keys, and boom - transaction lands in seconds. Solana's fast as hell, right? But the web3.js part? It tripped me up at first. Fees, blockhashes, signers.. confusing mess. Not anymore. I'm walking you through it like we're pair programming over Discord.
Why Solana web3.js? It's the main JS bridge to the blockchain. You talk to RPC nodes, build transactions, sign 'em, send 'em. And heads up - there's this new kid called gill that's basically web3.js v2 on steroids. Cleaner code, less boilerplate. In my experience, stick with gill for new projects. But I'll show both so you're covered.
Don't skip this. I always mess up here.
mkdir solana fun && cd solana funnpm init -ynode -v.npm i -D typescript ts node @types/node. Then npx tsc --init and tweak tsconfig.json to include ES2022.Pro tip: Use esrun for running TS files without compiling. npm i -g esrun. Saves headaches.
Run this in terminal: solana keygen new --outfile ~/.config/solana/id.json. Boom, you got a keypair. Fund it on devnet via faucet. Costs nothing. Expect ~2 SOL free daily.
What's a keypair? Private + public. Public is your address, like "9WzDX.." Private signs stuff. Lose it? Funds gone forever. Sound familiar?
| Method | Command | Why bother? |
|---|---|---|
| Gill (new hotness) | npm i gill | Cleaner RPC calls. Auto handles blockhash, signing. My go to now. |
| Classic @solana/web3.js | npm i @solana/web3.js | Everywhere already. Stable. But more verbose. |
| Bonus: SPL tokens | npm i @solana/spl token | For tokens later. You'll need it. |
Pick gill for this guide. It's future proof. Okay, create index.ts.
Connections are your RPC lifeline. Public ones rate limit, so for real apps grab Helius or QuickNode free tier.
Here's gill style. Dead simple.
import { createSolanaClient } from "gill"; const { rpc } = createSolanaClient({ urlOrMoniker: "devnet" }); async function checkIt() { const slot = await rpc.getSlot().send(); console.log("Current slot:", slot);
} checkIt();
Run esrun index.ts. See a number? You're connected. That slot is like Ethereum block number, but Solana cranks ~thousands per second.
Classic web3.js? More steps.
import { Connection } from "@solana/web3.js"; const connection = new Connection("https://api.devnet.solana.com", "confirmed"); const slot = await connection.getSlot();
console.log(slot);
Notice "confirmed" commitment? Means wait for 31/32 validators to agree. Faster than "finalized". Use "processed" for speed, but riskier.
In my experience, devnet lags sometimes. Switch to testnet if faucet's dry.
Never hardcode privkeys. Disaster waiting.
Gill way - super clean:
import { createSolanaClient, readKeypairFile } from "gill"; const { rpc } = createSolanaClient({ urlOrMoniker: "devnet" });
const keypair = readKeypairFile("~/.config/solana/id.json"); console.log("Your address:", keypair.publicKey.toString());
Check balance next. await rpc.getBalance({ address: keypair.address }).send(). Returns lamports - 1 SOL = 1e9 lamports. So 0.001 SOL? 1000000 lamports.
Issue? "File not found"? Path wrong. Use absolute: /Users/yourname/.config/solana/id.json. Fixed it for me twice.
Now the fun. Send 0.001 SOL to a random address. Watch it fly.
Generate receiver:
const receiver = Keypair.generate();
console.log("Receiver:", receiver.publicKey.toString());
Gill makes tx building a breeze. No manual blockhash hunting.
import { createTransferSolInstruction, Address } from "gill"; const amount = 1000000n; // 0.001 SOL const instruction = createTransferSolInstruction({ source: keypair.address, destination: Address(receiver.publicKey), amount,
}); const signature = await sendAndConfirmTransaction({ from: keypair, transaction: { instructions: [instruction] }, rpc, commitment: "confirmed",
}); console.log("Tx sig:", signature);
console.log(getExplorerLink({ transaction: signature, cluster: "devnet" }));
Paste that sig in Solana Explorer. See your tx? Green means success. Fees? ~0.000005 SOL. Pennies.
Classic web3.js version - bit more work:
import { Transaction, SystemProgram, LAMPORTSPERSOL } from "@solana/web3.js"; const tx = new Transaction().add( SystemProgram.transfer({ fromPubkey: keypair.publicKey, toPubkey: receiver.publicKey, lamports: LAMPORTSPERSOL * 0.001, })
); const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = keypair.publicKey; const signed = await sendAndConfirmTransaction(connection, tx, [keypair]);
See the difference? Gill hides blockhash, recentBlockhash crap. But understand it - tx expires after slots (~1 min).
Why does this matter? Every dApp wallet connect does this under the hood.
Your airdrop worked? Now mint a token. Say, "FRIENDCOIN".
Need spl token lib. Import it.
Steps - classic style cuz it's battle tested:
const mint = Keypair.generate();const rent = await connection.getMinimumBalanceForRentExemption(82); // Mint space = 82 bytesconst createMintTx = new Transaction().add( SystemProgram.createAccount({ fromPubkey: keypair.publicKey, newAccountPubkey: mint.publicKey, space: 82, lamports: rent, programId: TOKENPROGRAMID, }), createInitializeMintInstruction( mint.publicKey, 9, // decimals keypair.publicKey, // mint authority keypair.publicKey // freeze authority )
); await sendAndConfirmTransaction(connection, createMintTx, [keypair, mint]);
Token account next. For your wallet:
const tokenAccount = await getOrCreateAssociatedTokenAccount( connection, keypair, mint.publicKey, keypair.publicKey
);
Mint 1000 tokens to yourself:
await mintTo( connection, keypair, mint.publicKey, tokenAccount.address, keypair, 1000 * 109 // with decimals
);
Costs? Create mint ~0.002 SOL rent (locked, refundable). Tx fees negligible.
Gill does this too, but spl token wrappers lag a tad. Stick classic for tokens.
Trouble? "Invalid account data"? Wrong program ID. TOKENPROGRAMID is TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.
Solana's congested? Your tx drops. Fix: add compute budget.
Gill auto optimizes sometimes, but manual:
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from "@solana program/compute budget"; const computeIx = getSetComputeUnitLimitInstruction({ units: 300_000 });
const priceIx = getSetComputeUnitPriceInstruction({ microLamports: 10_000n }); // ~0.00001 SOL extra tx.add(priceIx, computeIx / your ix /);
Units: max compute your tx uses. Price: tip validators. 10k microLamports = tiny fee, huge speed boost. Test on devnet.
Tx fails? Don't guess.
const sig = getSignatureFromTransaction(signedTx);https://explorer.solana.com/tx/${sig}?cluster=devnet/inspector for sim errors. Shows "Program Error: 0x1" etc.logLevel: "debug" in client. Base64 dumps full tx.Last fail I had: Wrong fee payer. Explorer screamed "Invalid account owner". Fixed in 30 secs.
Got 0.02 SOL extra? Delegate stake.
const stakeKp = Keypair.generate();StakeProgram.delegate({ stakePubkey: stakeKp.publicKey, authorizedPubkey: keypair.publicKey, votePubkey: someValidator })Full code in those chainstack examples. Rewards accrue fast on testnet. Unstake? Another tx.
Pick validator via connection.getVoteAccounts(). Stake ~4-8% APY usually.
Tokens need WSOL sometimes. Wrap 0.1 SOL:
const wrappedKey = await createAssociatedTokenAccount( connection, payer, new PublicKey("So11111111111111111111111111111111111111112"), // WSOL mint payer.publicKey
); const wrapIx = createTransferInstruction( payer.publicKey, // from your SOL account wrappedKey, payer.publicKey, payer.publicKey, LAMPORTSPERSOL * 0.1
); // wait, actually use syncNative for wrap
Better: Use spl's syncNative after transfer to self token account. Unwrap reverses.
| Provider | Free Tier | Speed | My take |
|---|---|---|---|
| Public (api.devnet.solana.com) | Unlimited? | Slow when busy | Fine for learning. Crashes under load. |
| Helius | 1M credits/mo | Fast | Best docs. Webhooks killer. |
| QuickNode | 50M reqs/mo | Good | Easy dashboard. Reliable. |
| Chainstack | Free trial | Decent | Self host vibes. |
I rotate Helius + public. Never pay till production.
Want real time? Websockets.
Gill: const subs = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
const unsub = await subs.accountSubscribe({ account: keypair.address, commitment: "confirmed"
}).subscribe(result => { console.log("Balance changed:", result.value.lamports);
});
Call unsub() later. Drains battery in browsers.
Browser dApp? Use @solana/wallet adapter. Phantom signs for users.
Errors? Wrap in try/catch. Log sigs always.
Optimize: Preflight sim with simulateTransaction. Catches issues pre send.
Versioning: Pin deps. "gill": "^1.0.0". Breaking changes bite.