Master Solana web3.js: Complete Beginner Tutorial.

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.

First things first: get your environment sorted

Don't skip this. I always mess up here.

  1. Make a new folder: mkdir solana fun && cd solana fun
  2. Init npm: npm init -y
  3. Grab Node.js 18+ if you don't have it. Seriously, check with node -v.
  4. For TypeScript fans (me included): 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.

Generate a wallet - don't use mainnet yet

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?

Install the libs - gill vs classic web3.js

MethodCommandWhy bother?
Gill (new hotness)npm i gillCleaner RPC calls. Auto handles blockhash, signing. My go to now.
Classic @solana/web3.jsnpm i @solana/web3.jsEverywhere already. Stable. But more verbose.
Bonus: SPL tokensnpm i @solana/spl tokenFor tokens later. You'll need it.

Pick gill for this guide. It's future proof. Okay, create index.ts.

Your first connection - talk to devnet

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.

Load your wallet - keypairs from file

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.

Build and send your first transaction - airdrop simulator

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).

Common screw ups and fixes

  • Blockhash expired: Fetch fresh one. Or use gill.
  • Insufficient funds: Check balance first. Faucet more SOL.
  • Signature verification failed: Wrong keypair. Double check path.
  • Rate limited: Switch RPC. Public devnet sucks under load.

Why does this matter? Every dApp wallet connect does this under the hood.

Level up: SPL tokens - mint your own meme coin

Your airdrop worked? Now mint a token. Say, "FRIENDCOIN".

Need spl token lib. Import it.

Steps - classic style cuz it's battle tested:

  1. Connection, keypair - you got this.
  2. Mint keypair: const mint = Keypair.generate();
  3. Get rent exemption: const rent = await connection.getMinimumBalanceForRentExemption(82); // Mint space = 82 bytes
  4. Tx1: Create mint account.
const 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.

Priority fees - make your tx land faster

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.

Debugging fails - explorer is your friend

Tx fails? Don't guess.

  • Get sig before send: const sig = getSignatureFromTransaction(signedTx);
  • Explorer link: https://explorer.solana.com/tx/${sig}?cluster=devnet
  • Inspector: Add /inspector for sim errors. Shows "Program Error: 0x1" etc.
  • Gill debug: Set logLevel: "debug" in client. Base64 dumps full tx.

Last fail I had: Wrong fee payer. Explorer screamed "Invalid account owner". Fixed in 30 secs.

Stake some SOL - passive income vibes

Got 0.02 SOL extra? Delegate stake.

  1. Stake account: const stakeKp = Keypair.generate();
  2. Create: SystemProgram.createAccount for stake, then StakeProgram.initialize.
  3. Delegate: 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.

Wrap SOL - WSOL for DeFi

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.

Real talk: RPC providers compared

ProviderFree TierSpeedMy take
Public (api.devnet.solana.com)Unlimited?Slow when busyFine for learning. Crashes under load.
Helius1M credits/moFastBest docs. Webhooks killer.
QuickNode50M reqs/moGoodEasy dashboard. Reliable.
ChainstackFree trialDecentSelf host vibes.

I rotate Helius + public. Never pay till production.

Subscriptions - watch the chain live

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.

Production tips - don't get rekt

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.