I spent the past week building tokens on Solana. Not wrapping existing ones, not swapping — actually creating token mints from scratch, attaching economic rules to them at the protocol level, and watching a blockchain reject a transaction I told it to make. This post walks through what I built, what surprised me, and why the Token Extensions Program changes how I think about on-chain rules.
Where I Started
My background is Web2. I know how platforms build internal currencies — a database table for balances, API endpoints for transfers, middleware to collect fees, application logic to enforce rules like "this badge can't be sold." It works, but the rules live in code that can be changed, bypassed, or taken offline.
My starting question going into this was simple: what does Solana actually give you that a well-designed backend doesn't? By the end of the week, I had a concrete answer.
1: Your First Mint (It's Just an Account)
The first thing that reframes everything is understanding that a token on Solana is not a smart contract. It's an account — specifically a Mint account — that stores three pieces of state: total supply, decimal precision, and the address that's allowed to create more tokens (the mint authority).
spl-token create-token
spl-token create-account YOUR_MINT_ADDRESS
spl-token mint YOUR_MINT_ADDRESS 100
What surprised me: you can't receive tokens directly into your wallet. Every wallet needs a dedicated token account for each token type it holds. Think of your wallet as a filing cabinet — each token account is a labeled folder inside it. It felt awkward at first, but it's how Solana keeps account state fast and cheap to look up.
The Mint account is the source of truth. The token account is where your specific balance lives. One program, the SPL Token Program, manages both.
2: Metadata — Giving Your Token an Identity
A freshly minted token has no name, no symbol, nothing. In a block explorer it shows up as "Unknown Token." That's where the Token Extensions Program (Token-2022) comes in.
Instead of storing metadata in a separate account (the old Metaplex approach), Token-2022 lets you attach metadata directly to the mint account itself using the metadata extension.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--enable-metadata \
--decimals 6
spl-token initialize-metadata YOUR_MINT "100DaysCoin" "HUNDO" \
"https://your-metadata-uri.json"
The result: a single mint account that holds the token's supply, decimal config, mint authority, and its name and symbol. Fewer accounts, fewer transactions, lower cost. After running this I could see 100DaysCoin (HUNDO) show up properly in the Solana Explorer on devnet.
Key insight: The URI in the metadata points to a JSON file with extended details (description, image, attributes). It's the same pattern as NFT metadata — one on-chain pointer to off-chain details.
3: Transfer Fees — Economics Without Middleware
This is where things got genuinely interesting. In Web2, collecting a percentage of every transaction means building middleware: intercept the transfer, calculate the fee, split the payment, handle edge cases. And it can be bypassed if someone finds a way around your API.
Token-2022 has a transfer fee extension that enforces collection at the program level. When you configure it on a mint, every transfer of that token automatically withholds a percentage in the recipient's account — locked there until the withdraw authority (you) sweeps it out.
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--transfer-fee-basis-points 200 \
--transfer-fee-maximum-fee 5000000000000 \
--decimals 9
I transferred 100 tokens at a 2% fee. The recipient received 98. Two tokens were withheld in their account, untouchable by them. Then I ran the withdraw:
spl-token withdraw-withheld-tokens YOUR_TOKEN_ACCOUNT RECIPIENT_TOKEN_ACCOUNT
Final balance: 902. The 2 tokens came home.
What tripped me up: The --transfer-fee-maximum-fee parameter is in base units, not whole tokens. With 9 decimals, 5000 base units = 0.000005 tokens — basically nothing. My first two runs had the fee capped at a fraction of a token because I forgot to scale it. The fix: multiply by 10 ** decimals.
const MAX_FEE = BigInt(5000 * 10 ** DECIMALS); // 5000 whole tokens as cap
This is the kind of thing that only clicks after you see the wrong number in your output.
4: The Full Lifecycle in One Run
Day 4 was a consolidation challenge: reproduce the entire workflow — metadata + transfer fees — from a blank terminal without notes. I built a single Node.js script that:
- Created a Token-2022 mint with both metadata and transfer fee extensions
- Minted 1000 tokens
- Transferred 100 to a second wallet (98 received, 2 withheld)
- Harvested and withdrew the fees
- Printed the full token config as a summary
```Final output:
Primary wallet: 902 tokens
Second wallet: 98 tokens
Fees collected: 2 tokens
Running it clean, without errors, in one go, felt like the proof that the concepts had actually landed.
---
## Day 5: Non-Transferable Tokens — Soulbound on the Protocol
The last experiment was the most conceptually interesting. Solana's Token-2022 has a **non-transferable extension** that permanently prevents any token from that mint from moving between wallets. Not "our API won't let you" — the transaction is rejected by the token program before it even hits the chain.
```bash
spl-token create-token \
--program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
--enable-non-transferable
I minted 10 tokens, tried to transfer 5 to a second wallet, and got this:
✗ TRANSFER REJECTED (This is correct!)
Error: Transaction simulation failed: Error processing instruction
The Non-Transferable extension blocks ALL transfers at the protocol level.
Then I burned 3 tokens — and that worked fine. Balance dropped from 10 to 7.
That distinction matters: non-transferable doesn't mean non-destructible. The holder can burn their own tokens. They just can't send them to anyone else. This is exactly the behavior you'd want for things like:
- Course completion certificates
- KYC verification tokens
- DAO membership credentials
- Event participation proofs
In Web2, preventing credential trading means application-layer rules. Here, the restriction is part of the asset itself. No backend change, no API update, no workaround possible.
What I'd Tell Myself at the Start
1. Read the error messages carefully. Solana program errors are specific. "NonTransferable" in an error isn't noise — it's the program telling you exactly which extension blocked you.
2. Base units will get you. Every numeric parameter involving token amounts is in base units. Always multiply by 10 ** decimals before passing values into instructions.
3. Extensions are set at creation time. You can't add a transfer fee to an existing mint. Design your token before you deploy it.
4. Token-2022 is the right default now. The original SPL Token Program is simpler, but Token-2022 is a strict superset. Unless you have a specific reason to use the old program, start with Token-2022.
What's Next
I'm continuing through the 100 Days of Solana challenge. Next up: diving into compressed NFTs and Solana's state compression model. If you're following along or building something similar, drop a comment — I'd like to see what you're working on.
Built on Solana devnet. All code written in Node.js using @solana/web3.js and @solana/spl-token. Windows environment — no CLI available, so every challenge was solved programmatically.
Top comments (0)