DEV Community

Cover image for How I Added On-Chain Rewards and NFTs to Solana Quiz: Practical Insights, Pitfalls, and Tips
Dima Zaichenko
Dima Zaichenko

Posted on

How I Added On-Chain Rewards and NFTs to Solana Quiz: Practical Insights, Pitfalls, and Tips

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):

👉 Previous Article

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:

  1. Sends the result to Kafka
  2. Rust service receives the event and grants the reward
  3. Depending on the mode (SOLANA_ON_CHAIN=true/false), the reward is applied:
    • On-chain → Solana transaction
    • Off-chain → Local API
  4. Rust confirms the reward via Kafka
  5. Node.js marks the reward as issued
  6. If a 7-day streak of perfect answers is achieved → NFT is minted

Solana Quiz Streaker: 7 Days


🧰 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create a new mint
  2. Create ATA for the user
  3. Mint 1 token to the ATA
  4. 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();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Why this order?

  • Mint must have supply = 1 before metadata creation
  • ATA must exist beforehand; otherwise mint_to_checked has 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?

  1. Motivates users - 7-day streak is a small achievement, but tangible rewards feel nice
  2. Great way to learn Metaplex & Solana SDK deeper
  3. 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)