I spent the last five days building tokens on Solana — and it completely
changed how I think about what a "token" actually is.
Coming from Web2 and EVM development, I assumed a token was a number in a
database with some transfer logic wrapped around it. On Solana, it's much
closer to a configurable protocol object. Here's everything I learned across
Days 29–33 of my #100DaysOfSolana challenge.
The Starting Point: Two Token Programs
Solana has two token programs:
- SPL Token (original) — simple, battle-tested, does the basics
- Token Extensions Program (Token-2022) — everything the original does, plus built-in extensions for fees, metadata, soulbound tokens, and more
I used Token-2022 for almost everything this week because it lets you attach
behavior directly to the mint — no separate smart contract needed.
Day 29 — Creating My First SPL Token
The first thing that surprised me: a token on Solana is two separate accounts.
- Mint account — holds the token's configuration (supply, decimals, mint authority)
- Token account — holds a specific wallet's balance of that token
spl-token create-token --decimals 9
spl-token create-account <MINT_ADDRESS>
spl-token mint <MINT_ADDRESS> 1000
In Web2, this would be one database table with a balance column. On Solana,
the separation means the mint config is completely independent of who holds
what — any wallet can hold any token as long as they have a token account for it.
Day 30 — Token-2022 with On-Chain Metadata
The original SPL token has no name or symbol stored on-chain. You need an
external metadata program (Metaplex) to give it an identity. Token-2022
changes this with the metadata extension — name, symbol, and URI live
directly inside the mint account.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--enable-metadata \
--decimals 9
spl-token initialize-metadata <MINT_ADDRESS> \
"GopichandToken" "GOPI" \
"https://your-metadata-uri.json"
Run spl-token display <MINT_ADDRESS> and you'll see the name and symbol
sitting right there in the mint account output. No external call. No
separate metadata program. The token is the metadata.
Day 31 — Transfer Fees at the Protocol Level
This is where it got interesting. I attached a 1% transfer fee to a
token mint — meaning every transfer automatically withholds 1% in the
recipient's token account, uncollectable by the recipient, waiting for the
mint authority to withdraw.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--transfer-fee-basis-points 100 \
--transfer-fee-maximum-fee 5000 \
--decimals 9
After transferring 100 tokens, the recipient got 99. The 1 withheld token
sat locked in their account until I ran:
spl-token withdraw-withheld-tokens \
<MY_TOKEN_ACCOUNT> \
<RECIPIENT_TOKEN_ACCOUNT>
In Web2, building a platform fee on transfers requires intercepting every
transaction in your backend. On Solana, it's a flag on the mint. The
program enforces it — not your server.
Day 32 — The Full Lifecycle in One Sitting
Day 32 was a consolidation day. No new concepts — just proving I could
build the complete token lifecycle from scratch without checking notes:
- Create Token-2022 mint with metadata + 2% transfer fee
- Initialize metadata (name: ReinforceCoin, symbol: RFC)
- Create token account and mint 1000 RFC
- Transfer 100 to a second wallet → recipient gets 98, 2 withheld
- Withdraw withheld fees → balance becomes 902 Mint: 6YnDTE8cETtvKyesVM9frSeWCfpQUx757qcCQyHaBe9T Final balance: 902 RFC ✅
The "aha" moment: I realized the pattern is always the same regardless of
which extensions you use. Create → configure → mint → transfer →
collect. The specifics change. The lifecycle doesn't.
Day 33 — Soulbound Tokens
The most conceptually interesting day. I created a non-transferable
token — a token that is permanently locked to the wallet it's minted into.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--enable-non-transferable
I minted 10 tokens, then tried to transfer 5 to a second wallet. The
program rejected it instantly:
Program log: Transfer is disabled for this mint
custom program error: 0x25
That error code 0x25 (decimal 37) is NonTransferable — a first-class
error in the Token Extensions Program. This isn't application logic that
a clever script could bypass. The program itself rejects the instruction.
What's interesting is that burning still works. The owner can destroy
tokens they hold. They just can't send them. After burning 3, my balance
dropped from 10 to 7 — exactly as expected.
Real-world uses for non-transferable tokens:
- Course completion certificates
- KYC / identity verification
- Hackathon participation badges
- Employee credentials
- On-chain reputation scores
In Web2, preventing credential trading means writing application rules that
can be worked around. On Solana, the restriction is part of the asset itself.
What Surprised Me
The separation of mint and token accounts took longer to internalize than
I expected. I kept confusing "the mint address" with "my token account."
They're different accounts with different purposes. The mint is the factory.
The token account is the warehouse.
The error messages are actually readable. Transfer is disabled for is exactly what it sounds like. Coming from debugging opaque
this mint
EVM reverts, this felt refreshingly honest.
Extensions compose. You can combine metadata + transfer fees in a
single mint creation command. Day 32 showed me that Token-2022 isn't a
collection of separate features — it's a composable system.
What's Next
I'm on Day 33 of 100. Next up: deeper into Token-2022 extensions
(interest-bearing tokens, confidential transfers), then moving into
Anchor and on-chain programs.
I'm building toward AI agents that can own wallets and execute Solana
transactions autonomously — and understanding the token layer deeply is
a prerequisite for that.
If you're following along, my full build log is at:
👉 github.com/gopichandchalla16/100-days-of-solana
Day 34 of #100DaysOfSolana — writing in public, building in public.
Top comments (0)