In my previous article on Dev.to, I shared how I built a small app - Solana Quiz, where users answer daily questions and earn rewards.
Here’s the link to that article (to skip architecture and basic setup):
Since then, I’ve significantly expanded the functionality: I added on-chain rewards, 7-day streaks, and the most exciting part - NFTs for a series of perfect answers.
At first glance, it seems simple: trigger an event → reward tokens → sometimes issue an NFT.
But that "sometimes" hides a mountain of nuances: from Solana PDAs to the order of metadata calls and weird errors not explained anywhere officially.
In this article, I’ll share:
- How I implemented on-chain/off-chain rewards
- Why streaks behave differently in these modes
- How NFT rewards work
- Common pitfalls I encountered
- What you need to know about the correct command sequence when creating NFTs (if you change the order, everything breaks)
🟡 How Solana Quiz Works Now
When a user completes a quiz (currently always 5 questions, hardcoded; soon configurable in .env), the app:
- Sends the result to Kafka
- Rust service receives the event and grants the reward
- Depending on the mode (
SOLANA_ON_CHAIN=true/false), the reward is applied:- On-chain → Solana transaction
- Off-chain → Local API
- Rust confirms the reward via Kafka
- Node.js marks the reward as issued
- If a 7-day streak of perfect answers is achieved → NFT is minted
🧰 Working with Solana test-validator + Transaction Simulation
🚀 Why use solana-test-validator
When I started writing the on-chain part of the quiz, I quickly realized: testing everything directly on devnet is painful.
- Transactions confirm slower
- It’s hard to tell if an error is mine or the network’s
- Some errors (especially PDA / metadata) are hard to catch in production
Solana has an amazing tool - the local validator:
solana-test-validator
This is a full local Solana chain, launched with a single command.
Benefits:
- Fast, infinite tokens
- Deploy programs locally without restrictions
- RPC available by default:
http://127.0.0.1:8899
Now you can point your app to it:
SOLANA_NETWORK=local
SOLANA_RPC_ENDPOINT=http://127.0.0.1:8899 # http://host.docker.internal:8899
When useful:
- Writing your Solana programs (Anchor or pure Rust)
- Testing mint NFT / PDA / metadata
- Simulating transactions in bulk
- Reproducing errors quickly and consistently
🧪 Devnet, Localnet, Mainnet - What’s the Difference?
| Environment | Purpose | Pros | Cons |
|---|---|---|---|
Localnet (solana-test-validator) |
Instant dev | Instant blocks, full control | Not reflective of real network |
| Devnet | Pre-prod / public tests | Real load, public RPC | Sometimes slow, has limits |
| Mainnet | Production | Stable, real tokenomics | Expensive transactions |
💡 Recommended workflow:
- 80% of the time → work on test-validator
- 20% → final checks on devnet
🔍 Simulating Transactions & Getting Readable Errors
Without simulations, you lose half the debugging info. Simulation allows you to:
- Catch errors before sending
- See program logs
- Read SPL token, Metaplex errors, etc.
- Check which PDAs don’t match
- Verify if you have enough signers
- See on-chain stack traces
Example:
let simulation = self.rpc_client.simulate_transaction(&transaction).await?;
println!("Simulation logs: {:#?}", simulation.value.logs);
These lines saved me a lot of time.
🔄 On-chain vs Off-chain: What SOLANA_ON_CHAIN Really Controls
A common mistake is thinking this variable enables or disables rewards.
No. Rewards are always calculated. It only affects how the reward is applied:
| SOLANA_ON_CHAIN | Token Calculation | Streak Calculation |
|---|---|---|
| true | On-chain (Solana transaction) | Based on successful on-chain tx |
| false | Off-chain (via Rust API) | Locally in Rust, without interacting with Solana |
Why? Streak logic must be consistent. On-chain → real transactions. Off-chain → faster and cheaper to calculate locally.
🧩 NFT Rewards for Streaks
NFTs are issued for 7-day streaks of perfect answers (SOLANA_STREAK_DAYS=7).
Future plans: rarities, levels, different types of NFTs. For now, only one type is implemented.
Technically, NFT creation process:
- Create a new mint
- Create ATA for the user
- Mint 1 token to the ATA
- Create metadata (with master edition)
Here begins the magic - or rather, the pain.
🔥 Main Pain: Command Order Matters
If you look at Metaplex or Solana SDK examples, you won’t see warnings about call order. I didn’t either.
In my real project, here’s what happened:
❌ Doing it incorrectly:
create_metadata();
mint_token();
Result: NFT often didn’t create; errors like:
-
"mint must have exactly 1 token minted before metadata creation" -
"MasterEdition account does not match mint supply" - Or transaction failed silently
✔ Working order (experimentally verified):
mint_token();
create_metadata(); // metadata is created (along with the master edition)
Why this order?
- Mint must have supply = 1 before metadata creation
- ATA must exist beforehand; otherwise
mint_to_checkedhas no destination - MasterEdition can only be created after successful
mint_to_checked - All encapsulated in
create_metadata()
I tried many variations; only this order works reliably, regardless of RPC or environment.
⚠️ Signers Matter
Errors often happen if authority is incorrect or keypair not signed. Any transaction without the correct signer will fail or return weird errors.
💡 Tip: Use transaction simulation to see all errors and warnings in readable form, without wasting SOL on devnet or mainnet.
🧠 Why NFTs at All?
- Motivates users - 7-day streak is a small achievement, but tangible rewards feel nice
- Great way to learn Metaplex & Solana SDK deeper
- GitHub showcase - Rust + Solana + Node.js + Kafka interaction
Repository: Solana Quiz GitHub
Solana program: lib.rs
Example logs: Solscan tx
NFT API: nft_api.rs
Example logs: Solscan tx
🎯 Other Technical Notes
- PDA for Metadata & MasterEdition must be calculated manually. No convenient module exists (
mpl_token_metadata::pda) - Mint authority should exist only until token issuance
- ATA for NFT is always unique (NFT = 1 token, 0 decimals). Errors occur if decimals != 0
🎯 Future Expansion
- Different NFT types for different streak lengths
- On-chain streak counting fully in Anchor program
- Reward levels
- NFT collections
- mainnet-beta support
Currently, one NFT type & 7-day streak, but architecture allows painless future expansion.
⚡ Conclusion
I went through:
- On-chain/off-chain reward logic
- Full NFT creation on Solana
- Manual PDA calculation
- Metaplex errors not documented anywhere
- Experimentally determined correct NFT creation sequence
Now Solana Quiz is not just a game - it’s a service where:
- Tokens are awarded honestly and transparently
- Streaks are tracked correctly
- Users can earn a small but real NFT for persistence
Check the working code or fork the project: 👉 GitHub

Top comments (0)