DEV Community

atharv shukla
atharv shukla

Posted on

Three Token-2022 Mints in One Week: Fees, Yield, and Soulbound

Token-2022 Is Middleware for Your Currency

If you come from Web2, think of Token-2022 as the upgraded SPL token program on Solana — the same way you might think of Express middleware layered on top of a Node server. Instead of forking the token program to add custom behaviors, you flip extension flags at mint creation time. The runtime enforces them from that point forward. No custom smart contracts. No Rust. Just CLI commands and configuration that lives on chain forever.

Every extension is stored inside the mint account using Solana's Type-Length-Value (TLV) layout. When you enable an extension during mint creation, the account allocates extra bytes for that extension's data, which is why more features require more rent. The architecture looks like this:

              Token-2022 Mint Account
             ┌─────────────────────┐
             │   Base Mint Data    │  ← supply, decimals, authorities
             ├─────────────────────┤
             │   Transfer Fee      │  ← TLV: basis points, max fee, authorities
             ├─────────────────────┤
             │   Interest-Bearing  │  ← TLV: rate, timestamp, authority
             ├─────────────────────┤
             │   Metadata          │  ← TLV: name, symbol, uri
             ├─────────────────────┤
             │   Non-Transferable  │  ← TLV: flag (no extra data)
             └─────────────────────┘
              Extensions stack as
              additional TLV entries
Enter fullscreen mode Exit fullscreen mode

Over the past week, I shipped three mints on devnet using three different extensions. Here's exactly what I built and why each one matters.


Mint 1: The Fee-Bearing Token (Transfer Fee Extension)

Mint Address: WjbwpciSWwF8XWjHKtrACU9ypK1LwpFjSgGMb4gV9Dp
Extension: TransferFeeConfig

The Command

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  create-token \
  --transfer-fee-basis-points 100 \
  --transfer-fee-maximum-fee 1000000 \
  --decimals 6
Enter fullscreen mode Exit fullscreen mode

This creates a token that automatically skims 1% (100 basis points) off every transfer, capped at 1,000,000 tokens. The fee is withheld in the recipient's token account, and the withdrawal authority (me) can sweep it back at any time.

On-Chain Proof

Extensions
  Transfer fees:
    Current fee: 100bps
    Current maximum: 1000000000000
    Config authority: BJpejz8HQwF1TciYZEBD8VGu12wdVQxq3KkcECcT1AiK
    Withdrawal authority: BJpejz8HQwF1TciYZEBD8VGu12wdVQxq3KkcECcT1AiK
    Withheld fees: 0
Enter fullscreen mode Exit fullscreen mode

I transferred 1,000 tokens to a second wallet and watched the protocol automatically withhold 10 tokens as the fee. Then I swept those withheld fees back with a single command:

spl-token withdraw-withheld-tokens <MY_ACCOUNT> <RECIPIENT_ACCOUNT>
Enter fullscreen mode Exit fullscreen mode

When Would You Use This?

Anytime you want the protocol itself to enforce a take rate — royalties on a creator token, a protocol fee on a stablecoin, or a treasury skim on a community currency. The fee is baked into the mint. No one can transfer without paying it.


Mint 2: The Interest-Bearing Token (Transfer Fee + Interest Stacked)

Mint Address: GS7JaVubA3AxVbubg7tU8WsxLP2FLgMuMS82Yj8n6maL
Extensions: TransferFeeConfig + InterestBearingConfig

The Command

spl-token create-token \
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  --decimals 6 \
  --transfer-fee-basis-points 100 \
  --transfer-fee-maximum-fee 1000000 \
  --interest-rate 5000
Enter fullscreen mode Exit fullscreen mode

Two extensions on one mint. The transfer fee works exactly like Mint 1. The interest-bearing extension adds a 50% APR (5000 basis points) that makes the displayed balance drift upward over time.

The Subtle Thing That Matters

Here's what caught me off guard: the interest extension does not mint new supply. It updates the UI-displayed amount using a continuous compounding formula based on elapsed time. The raw token amount in the account stays the same. The "interest" is a presentation-layer calculation that wallets and explorers render.

The extension changes how wallets compute and display balances. The raw token amount stored on-chain never increases. If you're building a product on top of this, you need to understand that distinction — the displayed balance and the actual balance are two different numbers.

On-Chain Proof

Running spl-token display on the same account 30 seconds apart:

First check:
  1,000,000.332734

30 seconds later:
  1,000,000.730303
Enter fullscreen mode Exit fullscreen mode

The displayed balance drifts upward without any transaction. That's the interest extension at work — pure math, zero minting.


Mint 3: The Soulbound Badge (Non-Transferable Extension)

Mint Address: 295DTMWYjrVwFqRDtmCh386gfHzj3EtVtFVUQtAdRoed
Extension: Non-transferable

The Command

spl-token create-token \
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  --enable-non-transferable \
  --decimals 0
Enter fullscreen mode Exit fullscreen mode

Decimals 0, supply 1. A certificate of completion that can never leave the wallet it's minted to.

The Proof: Breaking It on Purpose

I pre-created a recipient's token account (so the transfer would actually reach the program), then tried to send my badge:

$ spl-token transfer 295DTMWYjrVwFqRDtmCh386gfHzj3EtVtFVUQtAdRoed 1 \
  9YCJgq52Msb8tehyyokP5YNhkv2cGK2Bd83aAtwjFUdy --allow-unfunded-recipient

Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]
Program log: Instruction: TransferChecked
Program log: Transfer is disabled for this mint
Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb failed: custom program error: 0x25
Enter fullscreen mode Exit fullscreen mode

Transfer is disabled for this mint — error 0x25 (decimal 37). The Token-2022 program itself refused the instruction. Not my code. Not an API layer. The validator rejected it at the protocol level.

When Would You Use This?

Proof-of-attendance tokens, certification badges, reputation scores, KYC stamps — any asset that should be permanently bound to its holder and never tradeable.


The Extension Map

Here's the full audit across all three mints:

Mint Extensions Behavior
WjbwpciS...9Dp Transfer Fees Charges 1% on every move
GS7JaVub...maL Transfer Fees + Interest-Bearing Charges 1% + accrues 50% APR display interest
295DTMWY...oed Non-Transferable Cannot be sent, sold, or moved. Ever.

What Surprised Me

The biggest thing I didn't expect was how composable extensions are. Stacking transfer fees and interest on one mint felt like adding two Express middlewares to a route — they don't interfere with each other, and the runtime handles the ordering.

The non-transferable extension was the most visceral. Seeing the validator print Transfer is disabled for this mint and reject your instruction at the protocol level hits differently than a 403 from an API. There's no backdoor. There's no admin panel. The rule lives in the bytes of the mint account, readable by anyone, enforceable by the network.

After building these three mints, the biggest takeaway for me is that Token-2022 turns token behavior into configuration rather than application logic. Instead of writing separate smart contracts for fees, rewards, or credentials, you compose extensions directly into the mint and let the Solana runtime enforce those rules everywhere the token is used.


This post is part of my #100DaysOfSolana journey. Follow along for daily hands-on Solana experiments.

Top comments (0)