DEV Community

Cover image for Transfer Fees, Metadata, and Soulbound Tokens: A Tour of Solana Token Extensions
Vinay
Vinay

Posted on

Transfer Fees, Metadata, and Soulbound Tokens: A Tour of Solana Token Extensions

If you are a Web2 developer and the phrase "on-chain token" sounds like something you need a PhD to build, this post is for you. I went from zero to creating tokens with metadata, transfer fees, and non-transferable locks using nothing more than CLI commands. Here is exactly how I did it and what surprised me along the way.

Before starting this challenge, I had never created a token on any blockchain. I understood the basics of Solana — sending SOL, reading balances, using the CLI — but the idea of creating my own digital asset felt like advanced territory. The goal was simple: learn what Solana tokens can do by building them, one step at a time.


Step 1: Create a Basic Mint

spl-token create-token
Enter fullscreen mode Exit fullscreen mode

This creates a Mint account — the on-chain definition of your token. It stores supply, decimals, and authority. The mint holds no tokens by itself. To hold a balance you need a token account:

spl-token create-account YOUR_MINT_ADDRESS
spl-token mint YOUR_MINT_ADDRESS 100
Enter fullscreen mode Exit fullscreen mode

The mint is the global token definition. Each token account is a wallet's balance for that specific token. In Web2 terms, the mint is your currency definition and each token account is a user's balance row. The SPL Token Program handles all the hard parts for you.


Step 2: Add On-Chain Metadata

A mint address with no name is just a string. The original SPL Token Program does not support on-chain metadata. The Token Extensions Program (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) stores metadata — name, symbol, URI — directly on the mint account, so everything is in one place instead of requiring a separate lookup:

spl-token create-token \
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  --enable-metadata \
  --decimals 6

spl-token initialize-metadata YOUR_MINT_ADDRESS \
  "100DaysCoin" "HUNDO" \
  "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json"
Enter fullscreen mode Exit fullscreen mode

The --enable-metadata flag activates the metadata extension at mint creation. The initialize-metadata command writes the name, symbol, and URI. The URI points to a JSON file with additional details like description and image.


Step 3: Attach a Transfer Fee

Instead of building middleware to collect fees on every transfer, the transfer fee extension enforces collection at the program level:

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

100 basis points = 1%. Every transfer of this token automatically withholds 1% in the recipient's token account. The recipient cannot spend withheld tokens. Only the withdraw withheld authority (the wallet that created the mint) can collect them:

spl-token transfer YOUR_MINT_ADDRESS 100 RECIPIENT --expected-fee 1
spl-token withdraw-withheld-tokens YOUR_ACCOUNT RECIPIENT_ACCOUNT
Enter fullscreen mode Exit fullscreen mode

The --expected-fee flag is a safety check — the transfer only succeeds if the calculated fee matches what you specified. The withheld tokens accumulate in the recipient's account until the authority sweeps them out.


Step 4: Full Lifecycle in One Session

Combining multiple extensions on a single mint works in one flow:

spl-token create-token \
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  --transfer-fee-basis-points 200 \
  --transfer-fee-maximum-fee 5000 \
  --enable-metadata \
  --decimals 9

spl-token initialize-metadata YOUR_MINT_ADDRESS \
  "ReinforceCoin" "RFC" \
  "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/CompressedCoil/metadata.json"

spl-token create-account YOUR_MINT_ADDRESS
spl-token mint YOUR_MINT_ADDRESS 1000
spl-token transfer --fund-recipient YOUR_MINT_ADDRESS 100 RECIPIENT --expected-fee 2 --allow-unfunded-recipient
spl-token withdraw-withheld-tokens YOUR_ACCOUNT RECIPIENT_ACCOUNT
Enter fullscreen mode Exit fullscreen mode

Two extensions (metadata + transfer fee) configured on the same mint. No smart contract needed.


Step 5: Create a Non-Transferable (Soulbound) Token

Some tokens should never be transferred — verified badges, course certificates, KYC credentials. The non-transferable extension blocks all transfers at the protocol level:

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

After minting, attempting a transfer returns an error:

spl-token transfer YOUR_MINT_ADDRESS 5 RECIPIENT \
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  --allow-unfunded-recipient
Enter fullscreen mode Exit fullscreen mode
Program log: Transfer is disabled for this mint
Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb failed: custom program error: 0x25
Enter fullscreen mode Exit fullscreen mode

The token program itself rejects the instruction. No client or program can override it. Burning still works — the holder can destroy tokens they own:

spl-token burn YOUR_TOKEN_ACCOUNT 3 --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Enter fullscreen mode Exit fullscreen mode

Non-transferable does not mean non-destructible. The holder controls their own account but cannot move tokens to another wallet.


What Surprised Me

Two things did not work the way I expected.

First, extensions must be configured when the mint is first created. You cannot add a transfer fee or metadata extension later. You have to decide how your token behaves before it exists, which is the opposite of how I would approach it in Web2.

Second, the non-transferable failure was more satisfying than I expected. I ran the transfer command fully expecting tokens to move. Instead, the blockchain returned Transfer is disabled for this mint. The error was clear and no flag or workaround could bypass it. That is when it clicked: this is a protocol-level lock, not a convention.


What's Next

I plan to build a token with real utility — combining metadata and transfer fees into something that could serve as a community reward token. If you are following the #100DaysOfSolana challenge or exploring Solana tokens on your own, start with the basic mint, then add one extension at a time. The CLI makes it easy to experiment without writing any smart contract code.


Resources

Top comments (1)

Collapse
 
sankri_enterprises_644275 profile image
sankri enterprises

🔥🙌🏻