One hundred days ago, I had no idea this challenge would become one of the most rewarding technical journeys I've taken. There were days when everything clicked, and there were days when nothing compiled.

I celebrated successful deployments, stared at cryptic errors for
hours, rewrote programs that weren't good enough, and learned that understanding Solana has far less to do with memorizing APIs than changing how you think about state, ownership, and security.

Looking back now, I realize I didn't just complete 100 challenges; I built a completely different mental model of software.


Where I started

I came in with a biochemistry degree I never used, a self-taught
engineering career I built in public, and a GitHub full of Rust and Python work. I co-maintain statix, a Nix linter that ended up in the canonical NixOS/nixpkgs upstream. I had shipped real software before.

What I did not have was any intuition for how Solana actually works. I knew the buzzwords. I had the mental model of "it's fast and cheap." I did not know what an account was. I did not know what a program was. I did not know why those two things were different.

That gap, between knowing the words and understanding the model,
is what these 100 days actually closed.


What I expected

I expected Solana to feel like a database with extra steps.

In Web2, you have a server that holds state. You call an API. The
server reads from the database, does something, writes back. I
expected a blockchain to be the same thing, just slower and
decentralized.

The first thing that broke that model: accounts.

On Solana, an account is not a row in your database. It is the
database. Every piece of state, your wallet balance, a token you
hold, the program you deployed, is an account. Programs are accounts. Data is accounts. Everything is an account.

That sounds obvious written down. It took me until around Day 15 to actually feel it, when I was staring at a getAccountInfo call wondering why the program I deployed was also an account, and why that account had a field called executable.

The model clicked when I stopped asking "where is the database?" and started asking "which account holds this state?"


The thing that changed everything: PDAs

I spent a week not understanding Program Derived Addresses.

I understood the definition. I could recite it: a PDA is an address derived from seeds and a program ID, guaranteed to be off the ed25519 curve, meaning no private key exists for it.

I could not feel why that mattered.

The moment it clicked was when I was building the counter program.
I had been thinking of PDAs as "special addresses the program uses."
What I finally understood is that they are the program's only way to prove it owns something.

When you pass a PDA to an instruction, Anchor re-derives the expected address from the seeds and compares it to what you passed in. If they don't match, the transaction is rejected before your handler runs.

The seeds are not just naming, they are the authorization check.

#[account(
    mut,
    seeds = [b"counter", user.key().as_ref()],
    bump = counter.bump,
    has_one = user,
)]
pub counter: Account<'info, Counter>,

Enter fullscreen mode Exit fullscreen mode

This one constraint block replaced an entire ownership check I would have written manually in handler code. The derivation is the policy.

I use that mental model constantly now.


The week I built exploits on purpose

Somewhere around Day 82, something shifted.

I had been building programs. Writing tests. Deploying to devnet.
Learning the mechanics. Then the challenge asked me to reproduce
the Wormhole exploit, the $326M hack that came down to a single
missing owner check.

I built the vulnerable program. I wrote a test that forged an account owned by the System Program, stuffed it with a fake Config, and watched my own authorization get bypassed. The test passed. That meant the exploit worked.

Then I changed one line:

// BEFORE (vulnerable)
pub config: UncheckedAccount<'info>,

// AFTER (fixed)
pub config: Account<'info, Config>,

Enter fullscreen mode Exit fullscreen mode

The same attack, against the fixed program, was rejected before my handler ever ran. Account<'info, T> verifies the program owner and the 8-byte discriminator automatically. The exploit was stopped at the door.

That week taught me more about security than any checklist could have.
Not because I read about the attack, because I ran it, watched it succeed, and then watched one type change stop it cold.
EXPLOIT SUCCEEDED on the left, EXPLOIT BLOCKED on the right

The lesson I carry:

never read an account's contents until you've
confirmed its identity. Every item on my security checklist traces
back to that principle.


What I built on Windows with no CLI

Here is the thing nobody warned me about: spl-token isn't available on Windows.

Token-2022 transfer fee workflow

A successful end-to-end Token-2022 transfer fee workflow implemented entirely in Node.js on Windows. Without the spl-token CLI, I had to initialize the mint extensions, perform the transfer, harvest the withheld fees, and withdraw them programmatically.

I did the entire token extensions arc: transfer fees, metadata,
interest-bearing tokens, soulbound credentials, NFT collections,
in Node.js, programmatically, using @solana/spl-token directly.

No CLI shortcuts. Every extension initialized by hand, in the correct order, with the correct account space calculated manually.

This turned out to be a better education than the CLI would have been.
When you can't run spl-token create-token --enable-metadata, you have to understand what that command actually does:

  1. Allocate the mint account with space for extensions
  2. Initialize the metadata pointer extension
  3. Initialize the mint itself
  4. Write the metadata fields

The order matters. The space calculation matters. The program ID
matters. I know these things because I got them wrong and read the errors, not because a command abstracted them away.

If you have the CLI, use it. But spend at least one day doing the
same thing programmatically. The gap between "it worked" and "I
understand why it worked" is worth finding.


The base units trap (the mistake I made three times)

I will not pretend everything was smooth.

I spent three separate challenge days debugging a transfer fee that wasn't withheld correctly. The recipient kept getting 99.999995 tokens instead of 99. The transaction succeeded. The number was silently wrong.

The problem: --transfer-fee-maximum-fee 5000 with 9 decimals sets the cap at 0.000005 tokens, not 5000. Everything is in base units.

I fixed it. Then I forgot. Then I fixed it again.

// Wrong
const MAX_FEE = BigInt(5000);

// Right — 5000 whole tokens as the cap
const MAX_FEE = BigInt(5000 * 10 ** DECIMALS);

Enter fullscreen mode Exit fullscreen mode

This is the mistake I would most want to hand to past me on Day 1: every numeric parameter involving token amounts is in base units. Multiply by 10 ** decimals. Every time. Without exception.


The moment I understood what "trustless" means

Day 73. I built a vault that lets users deposit SOL and withdraw it.

The deposit was trivial; the user signs, SOL moves. But the
withdrawal was different. The vault is a PDA. No private key exists for it. My program needed to move SOL out of an account that nobody can sign for.

The answer: .with_signer(signer_seeds).

let signer_seeds: &[&[&[u8]]] = &[&[
    b"vault",
    user_key.as_ref(),
    &[bump]
]];

let cpi_ctx = CpiContext::new(system_program, Transfer {
    from: vault.to_account_info(),
    to: user.to_account_info(),
}).with_signer(signer_seeds);

Enter fullscreen mode Exit fullscreen mode

The runtime re-derives the PDA from those seeds and my program ID. If it matches, the transfer is authorized. No human signed this. No backend approved it. The math is the authorization.

That is what trustless actually means. Not "you trust the code" in the sense of hoping it's correct. "You trust the math" in the sense that nobody can override it, not the team that deployed the program, not the infrastructure it runs on, not even me.


An unexpected part of the journey: writing

One unexpected part of this journey had nothing to do with code.

I wrote about nearly every milestone on DEV. Breaking concepts down for other developers forced me to slow down and understand them myself.
I published posts on Token-2022 mistakes, on PDAs, on CPI patterns, on security checklists; all written while the ideas were still fresh from failing at them.

Somewhere along the way, one of those articles became the most-read post of the epoch. I won the MLH Epoch 1 writing challenge.

Epoch 1 writing challenge congratulaory email

Solana's System Program: Understanding the Kernel

That reminded me that learning publicly isn't just about building an audience; it's another way of learning. Every time I tried to explain a concept clearly enough for someone else to use, I discovered gaps in my own understanding I hadn't noticed while building.

Write about what you're learning. You will understand it better.


The capstone: what I built and what it cost me

For Day 99, I built Agent Registry: a Solana program that lets any wallet permanently register an autonomous AI agent on-chain.

#[account(
    init,
    payer = owner,
    space = 8 + AgentRecord::INIT_SPACE,
    seeds = [b"agent", owner.key().as_ref()],
    bump
)]
pub agent_record: Account<'info, AgentRecord>,

Enter fullscreen mode Exit fullscreen mode

One agent per wallet. Permanent. The seeds enforce it.

I built this because of Day 92, when I connected an AI model to
Solana devnet through two read-only tools and watched it decide on its own which RPC calls to make to answer a question in plain English.
I wanted a way to put that agent's identity on-chain, permanently tied to the wallet that owns it.

What I didn't tell you is what the build actually cost me.

I fought Anchor version mismatches for hours. anchor-cli 0.32.1
scaffolded a project that pulled in zeroize 1.9.0, which requires
Rust edition2024, which my Rust version didn't support. I tried
patching Cargo.toml. I tried upgrading Rust. I tried three different project names. I spent the better part of a day watching builds fail at the dependency resolution stage before anything compiled.

Eventually I deployed through Solana Playground.

That is a completely valid solution. It is also a reminder that on
Day 99 of a 100-day challenge, with a deadline and a real project
and a local toolchain fighting back, sometimes the right move is to
ship the thing you came here to ship.

The project started as a single register_agent instruction. After getting it working, I expanded it to support updates, account closure, validation, events, and better error handling. It became a good exercise in taking something that merely worked and making it feel like software I'd actually want someone else to build on.

Day 99 Solana playground

After weeks of building wallets, Token-2022 mints, vaults, CPIs, security exercises, and PDAs, Day 99 brought everything together in a small but complete Anchor program: an on-chain Agent Registry deployed to Solana Devnet.

Program ID:
7GfBSFBR9bgYqfhhLkRm1nph6V59TYBqkf9YBUup5ejL

View on Solana Explorer (devnet)


What I would tell myself on Day 1

The account model is the whole thing. Learn it first and learn it deeply. Every concept in Solana, PDAs, CPIs, program ownership, rent is just the account model applied to a specific problem.

Errors are information, not failures. ConstraintSeeds. Error 2006. A seeds constraint was violated. That message tells you exactly which constraint fired and why. Read it. The runtime is specific when it rejects something. Use that specificity.

Build the failure path first. Every test I wrote that tried to break my own program taught me more than the test that proved it worked. The has_one constraint doesn't mean anything until you write a test that sends the wrong signer and watches it get rejected.

Write about it. Even if nobody reads it yet. Explaining something is the fastest way to find out whether you actually understand it.


What comes next

I'm applying for the MLH Fellowship — Web3 track, final cohort.
The Agent Registry roadmap has things I want to build: a frontend
dashboard, MCP integration so AI agents can discover each other
on-chain, and a Token-2022 identity badge for every registered agent.

I want to contribute to open source Solana tooling the way I
contribute to the Nix ecosystem, not just as a user but as someone who fixes things and ships improvements.

And I want to be the resource I didn't have on Day 1.


One hundred days ago, I looked at Solana and saw unfamiliar
terminology: accounts, PDAs, CPIs, rent, token extensions. Today
those aren't just concepts I can explain; they're tools I've used to build, debug, secure, test, and deploy a real program.

Looking back, that's the biggest lesson of these 100 days. I didn't just learn Solana's syntax. I learned how to think in Solana.


Resources

GitHub Repository

Agent Registry Source

Program on Solana Explorer (Devnet)

Program ID: 7GfBSFBR9bgYqfhhLkRm1nph6V59TYBqkf9YBUup5ejL

100 Days of Solana Articles

100DaysOfSolana Series' Articles

Day 100 of #100DaysOfSolana. I did it.