I've been building on Solana for the past few weeks as part of the
100DaysOfSolana challenge by MLH. Days 15–19 were all about transactions —
sending them, reading them, tracking them through confirmation stages, and
deliberately breaking them. Here's what I learned, explained for backend
developers who think in HTTP requests and database calls.
A Solana Transaction Is Not an API Call
When you hit a REST endpoint, you send a request and get a response.
If it fails, you retry. No cost, no permanent record, no cryptographic proof
it ever happened.
A Solana transaction is different in every one of those ways:
- It requires a cryptographic signature from the fee payer's private key
- It is permanently recorded on-chain whether it succeeds or fails
- It expires after ~60–90 seconds based on a recent blockhash
- It costs a fee even if it fails
That last point surprised me the most. Let me show you with real output.
The Anatomy of a Transaction
Every Solana transaction has these parts:
Signature 0: 2xQrSQU...
Account 0: srw- AWKYsCGB... (fee payer)
Account 1: -rw- 8nwngJPM...
Account 2: -r-x 11111111... (System Program)
Instruction 0
Program: 11111111... (System Program)
Transfer { lamports: 9999000000000 }
Recent Blockhash: FVSnvFfG...
- Signature — cryptographic proof the fee payer authorized this tx
- Accounts — every account the tx will read or write must be declared upfront
- Instructions — the actual operations (transfer SOL, call a program, etc.)
- Recent Blockhash — acts like a timestamp + nonce; tx expires if too old
The account permission flags (srw-, -rw-, -r-x) tell you exactly what
each account can do: signer, readable, writable, e*x*ecutable.
Solana's 3 Confirmation Levels
Unlike a database commit that's either done or not, Solana confirmation
happens in stages:
| Level | What it means | Web2 analogy |
|---|---|---|
| Processed | Validator included tx in a block | POST reached the server |
| Confirmed | 66%+ validators voted on the block | 200 OK from load-balanced API |
| Finalized | 31+ blocks built on top | DB commit flushed + replicated |
I built a live tracker that shows each stage in real time:
Tracking confirmation stages...
[Processed → Confirmed] ✅ reached in 0.3s
[Confirmed → Finalized] ✅ reached in 0.2s
Transaction successful! 🎉
Signature: 3hYmkD3m...
On devnet, Processed→Confirmed takes ~400ms. Confirmed→Finalized takes
~6–12 seconds. In production those numbers matter for UX decisions.
What Happens When Transactions Fail
This is where things get interesting. I deliberately triggered a failed
transaction by skipping preflight simulation (skipPreflight: true) and
attempting to send 9,999 SOL when my wallet only had ~6.14 SOL.
Here's the real on-chain output from solana confirm -v:
Status: Error processing Instruction 0: custom program error: 0x1
Fee: ◎0.000005
Account 0 balance: ◎6.13593 -> ◎6.135925
Account 1 balance: ◎0
Compute Units Consumed: 150
Log Messages:
Program 11111111111111111111111111111111 invoke
Transfer: insufficient lamports 6135925000, need 9999000000000
Program 11111111111111111111111111111111 failed: custom program error: 0x1
Three things stand out:
1. The fee was charged anyway.
Account 0 balance dropped by 0.000005 SOL — just the fee, not the transfer.
The network did work (verified signature, attempted execution), so it got paid.
2. The error is structured.
custom program error: 0x1 from the System Program always means insufficient
lamports. As you write your own programs, these error codes become your
primary debugging tool — like HTTP status codes but for on-chain logic.
3. The Log Messages are your stack trace.
Transfer: insufficient lamports 6135925000, need 9999000000000 tells you
exactly what was available vs what was needed. This is how you debug failed
instructions in production.
Two Types of Failure: Local vs On-Chain
Not all failures reach the chain:
| Failure type | Where it stops | Fee charged? |
|---|---|---|
| CLI preflight (insufficient funds check) | Never leaves your machine | ❌ No |
| On-chain failure (skipPreflight) | Executed by validators | ✅ Yes |
This is why production apps use simulateTransaction before submitting.
Simulation catches errors locally for free. Every on-chain failure is real
money, even if it's tiny amounts.
The Mental Model Shift
The biggest shift from Web2 to Solana transactions:
Web2: You send a request. The server decides what happens. You get a
response. If something breaks server-side, you don't pay for it.
Solana: You sign and broadcast a state change. Validators execute it
atomically. The result — success or failure — is permanent and public.
You pay regardless.
The blockhash expiry (~60–90s) also changes how you think about retries.
You can't just retry the same signed transaction forever — the blockhash
goes stale. You need to rebuild and re-sign with a fresh blockhash.
What I Built This Week
- Day 15 — Inspected transaction anatomy: signatures, accounts, instructions, compute units
- Day 16 — Sent first SOL transfer on devnet (<1s settlement)
- Day 17 — Built a reusable Node.js CLI transfer tool with balance checks and Explorer links
- Day 18 — Added live confirmation tracking (Processed→Confirmed→Finalized)
-
Day 19 — Deliberately broke transactions, read on-chain errors with
solana confirm -v
All code is on my GitHub:
github.com/gopichandchalla16/100-days-of-solana
If you're coming from a Web2 background and starting with Solana, the
transaction model will feel strange at first. The fee-on-failure behavior,
the blockhash expiry, and the upfront account declaration all exist for
good reasons — they're what makes Solana fast, parallel, and predictable.
Once it clicks, it's actually elegant.
Day 20 of #100DaysOfSolana — building in public every day 🚀
Follow my progress: @GopichandAI
Top comments (0)