<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prasiddh Naik</title>
    <description>The latest articles on DEV Community by Prasiddh Naik (@prasiddhnaik).</description>
    <link>https://dev.to/prasiddhnaik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896171%2F3f38eb74-b076-4e13-808a-ac3a7d7d080a.jpeg</url>
      <title>DEV Community: Prasiddh Naik</title>
      <link>https://dev.to/prasiddhnaik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prasiddhnaik"/>
    <language>en</language>
    <item>
      <title>CPI on Solana is a function call with a guest list</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Wed, 08 Jul 2026 11:24:38 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/cpi-on-solana-is-a-function-call-with-a-guest-list-3ko9</link>
      <guid>https://dev.to/prasiddhnaik/cpi-on-solana-is-a-function-call-with-a-guest-list-3ko9</guid>
      <description>&lt;p&gt;Five days ago CPI was three letters in the docs. Now I have called the System Program to move SOL, Token-2022 to mint tokens, a PDA vault to release lamports, and my own counter program to bump a tally — all through the same mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thesis:&lt;/strong&gt; A cross-program invocation is a function call with a guest list. The runtime does not care whether the callee is built into Solana or forty lines you wrote this morning. Your job is to bring the right program ID, the right accounts, and the right signer authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;Strip away the syntax. Every CPI answers three questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which program are you calling?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A program ID — a public key. In Anchor 1.0, &lt;code&gt;CpiContext::new&lt;/code&gt; takes that &lt;code&gt;Pubkey&lt;/code&gt; as its first argument: &lt;code&gt;ctx.accounts.counter_program.key()&lt;/code&gt;, not &lt;code&gt;to_account_info()&lt;/code&gt;. Point at the wrong program and a real one still runs — just not the one you meant. I once passed the System Program ID (&lt;code&gt;11111111…&lt;/code&gt;) while sending counter instruction bytes. The log said &lt;code&gt;invalid instruction data&lt;/code&gt;. Fair enough. The System Program was not broken; my target was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which accounts does it need?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The callee owns an accounts struct: &lt;code&gt;Transfer&lt;/code&gt; for SOL, &lt;code&gt;MintTo&lt;/code&gt; for tokens, &lt;code&gt;Increment&lt;/code&gt; for my counter. You pass every account it expects, wired to &lt;code&gt;AccountInfo&lt;/code&gt;. Skip one and Anchor names it: &lt;code&gt;AccountNotEnoughKeys on admin_config&lt;/code&gt; means the callee added an account your CPI did not include. Pass the wrong account and you get &lt;code&gt;ConstraintSeeds&lt;/code&gt; or &lt;code&gt;has_one&lt;/code&gt; instead — same idea, different constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who authorizes the call?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Two paths, and mixing them up causes most CPI pain.&lt;/p&gt;

&lt;p&gt;When a wallet signs the outer transaction, the runtime carries that signature into the inner call. No extra signing code. That is how my Day 71 SOL transfer and Day 72 token mint worked.&lt;/p&gt;

&lt;p&gt;When a PDA must authorize, there is no private key. Your program calls &lt;code&gt;invoke_signed&lt;/code&gt; with the exact seed bytes plus canonical bump. In Anchor that is &lt;code&gt;.with_signer(signer_seeds)&lt;/code&gt; on the &lt;code&gt;CpiContext&lt;/code&gt;. Those seeds must match &lt;code&gt;#[account(seeds = ..., bump)]&lt;/code&gt; on the PDA account — byte for byte, bump included.&lt;/p&gt;

&lt;p&gt;The helper function — &lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;mint_to&lt;/code&gt;, &lt;code&gt;cpi::increment&lt;/code&gt; — is just the envelope.&lt;/p&gt;
&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Here is &lt;code&gt;compose_lab&lt;/code&gt; calling my own &lt;code&gt;counter&lt;/code&gt;. Same shape as calling the System Program on Day 71:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bump&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter_program&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;Increment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.tally&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;cpi&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Accounts)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Bump&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;tally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tally&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;declare_program!(counter)&lt;/code&gt; read my counter's IDL and generated &lt;code&gt;cpi::increment&lt;/code&gt; — the same role &lt;code&gt;system_program::transfer&lt;/code&gt; plays for built-in programs. The caller never compiled counter's source. Change counter's internals tomorrow; if the IDL holds, the caller stays the same.&lt;/p&gt;

&lt;p&gt;The PDA-signed variant adds one thing — &lt;code&gt;.with_signer&lt;/code&gt; — on top of the same pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signer_seeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"vault"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_key&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;]]];&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.system_program&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Transfer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;.with_signer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signer_seeds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User deposit: plain &lt;code&gt;CpiContext::new&lt;/code&gt; — the wallet signed. Vault withdraw: &lt;code&gt;.with_signer&lt;/code&gt; — the vault is a PDA with no keypair.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tripped me up
&lt;/h2&gt;

&lt;p&gt;On Day 75 I broke vault withdraw on purpose by using the wrong bump. The terminal was specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AvSWLkjNFjJ5wrw4jL6342nBUygcwn2By745qHci5gtw's signer privilege escalated
Program failed: Cross-program invocation with unauthorized signer or writable account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime re-derived an address from my seeds, it did not match the vault in the instruction, and it refused to authorize the transfer. Fix: &lt;code&gt;ctx.bumps.vault&lt;/code&gt; in &lt;code&gt;signer_seeds&lt;/code&gt; — the same bump as the account constraint.&lt;/p&gt;

&lt;p&gt;That line is not noise. It is a diagnosis. Read program logs top to bottom; &lt;code&gt;[1]&lt;/code&gt; and &lt;code&gt;[2]&lt;/code&gt; show which program was running when things broke.&lt;/p&gt;

&lt;p&gt;None of my CPI failures were logic bugs inside handlers. They were mismatches at the boundary: accounts the caller passed versus constraints the callee enforced. When a CPI breaks, one of those two sides moved.&lt;/p&gt;




&lt;p&gt;This draws from &lt;strong&gt;#100DaysOfSolana&lt;/strong&gt; Days 71–75. Code: &lt;a href="https://github.com/prasiddhnaik/solana-100-days" rel="noopener noreferrer"&gt;solana-100-days&lt;/a&gt; — &lt;code&gt;sol-mover&lt;/code&gt;, &lt;code&gt;token_cpi&lt;/code&gt;, &lt;code&gt;vault&lt;/code&gt;, &lt;code&gt;compose-lab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://solana.com/docs/core/cpi" rel="noopener noreferrer"&gt;Solana CPI docs&lt;/a&gt; · &lt;a href="https://www.anchor-lang.com/docs/cross-program-invocations" rel="noopener noreferrer"&gt;Anchor CPI guide&lt;/a&gt; · &lt;a href="https://docs.rs/anchor-lang/latest/anchor_lang/system_program/index.html" rel="noopener noreferrer"&gt;system_program helpers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>rust</category>
      <category>anchor</category>
    </item>
    <item>
      <title>What I learned about PDAs in a week of building on Solana</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:22:11 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/what-i-learned-about-pdas-in-a-week-of-building-on-solana-1bk5</link>
      <guid>https://dev.to/prasiddhnaik/what-i-learned-about-pdas-in-a-week-of-building-on-solana-1bk5</guid>
      <description>&lt;p&gt;On Solana, programs are stateless. If your program needs to remember something per user, per game, or per config, it needs a deterministic address it can find again later without storing that address anywhere. &lt;strong&gt;Program Derived Addresses (PDAs)&lt;/strong&gt; are the addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;The Web2 framing that finally clicked for me: &lt;strong&gt;PDAs are like a database primary key you can compute from a row's logical identity&lt;/strong&gt; — except the "database" is Solana's entire account model, the key derivation is a hash, and your program ID is baked in so only your program can sign for accounts at that address.&lt;/p&gt;

&lt;p&gt;In a normal backend, you insert a row, the database assigns an ID, and you store that ID somewhere so you can look the row up later. On Solana, there is no central database handing out IDs. You pick a logical identity — &lt;code&gt;"counter"&lt;/code&gt; plus a user's public key, for example — hash it together with your program ID, and you get an address. Anyone who knows the seeds and the program ID can recompute the same address. No lookup table required.&lt;/p&gt;

&lt;p&gt;Where the analogy breaks (and you should be honest about this):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDAs are not stored in a table.&lt;/strong&gt; They are derived on demand. The address might exist as an on-chain account, or it might not — derivation and initialization are separate steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no row until you pay rent.&lt;/strong&gt; Creating an account at a PDA costs lamports (Solana's smallest currency unit; 1 SOL = 1 billion lamports). That payment is called &lt;strong&gt;rent&lt;/strong&gt;, and it reserves space on the chain for your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Deleting" an account is not a SQL DELETE.&lt;/strong&gt; Closing an account zeros its data, sends the lamports back to a wallet, and the runtime garbage-collects the account at the end of the transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hold onto one sentence: &lt;strong&gt;a PDA is an address your program can predict, and optionally own, because it was derived from seeds only your program knows how to use.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a derivation
&lt;/h2&gt;

&lt;p&gt;Here is the canonical pattern from my counter program, copied verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&lt;/span&gt;
    &lt;span class="nd"&gt;init,&lt;/span&gt;
    &lt;span class="nd"&gt;payer&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;user,&lt;/span&gt;
    &lt;span class="nd"&gt;space&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nd"&gt;Counter::INIT_SPACE,&lt;/span&gt;
    &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bump&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk through each piece:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;seeds = [b"counter", user.key().as_ref()]&lt;/code&gt;&lt;/strong&gt; — This is the logical identity. The first element is a static prefix: a string tag that namespaces this account type inside your program. The second is dynamic: the user's public key, so every wallet gets its own counter. These bytes are concatenated and hashed together with your program ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The program ID is not optional.&lt;/strong&gt; &lt;code&gt;find_program_address&lt;/code&gt; (and Anchor's &lt;code&gt;seeds&lt;/code&gt; constraint) always includes the deploying program's public key in the hash. The same seed bytes in a different program produce a completely different address. That is a feature, not a quirk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;bump&lt;/code&gt;&lt;/strong&gt; — PDAs must live &lt;em&gt;off&lt;/em&gt; the ed25519 curve so they have no private key. The runtime tries bump values 255 down to 0, appending each candidate as an extra seed byte, until the hash result is not a valid curve point. The winning byte is the &lt;strong&gt;canonical bump&lt;/strong&gt;. It is part of the derivation input, not a separate magic number floating beside the seeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;/strong&gt; — Derivation gives you an address. &lt;code&gt;init&lt;/code&gt; actually creates an account there and writes your struct's initial data. Until &lt;code&gt;init&lt;/code&gt; runs, the PDA address is just math — no account exists yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;space = 8 + Counter::INIT_SPACE&lt;/code&gt;&lt;/strong&gt; — Every Anchor account starts with an 8-byte discriminator (so the runtime knows which struct type this is), plus whatever fields you defined. You pay rent proportional to this size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;payer = user&lt;/code&gt;&lt;/strong&gt; — The wallet signing the transaction pays the rent to create the account.&lt;/p&gt;

&lt;p&gt;On subsequent instructions, I do not re-derive the bump from scratch. I store it in the account and pass it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&lt;/span&gt;
    &lt;span class="nd"&gt;mut,&lt;/span&gt;
    &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anchor verifies that &lt;code&gt;counter.bump&lt;/code&gt; is the canonical bump for these seeds. If you pass the wrong bump, the transaction fails before your handler runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the seeds matter
&lt;/h2&gt;

&lt;p&gt;Yesterday I ran a collision exploration script against my deployed program. The output is worth keeping because it makes the seed design concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user counter&lt;/strong&gt; — seeds: &lt;code&gt;["counter", wallet_pubkey]&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Per-user counter PDAs
  Wallet A PDA: 8Fr8LiP6khZsTcDAeh7Gjvot5TFCbPEDBxuzv7Ju5JMj
  Wallet B PDA: 6ba3KK9vCMYPogDpXrHoS8fYsVxy893vCy7kudwkM4fa
  Same address? false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice and Bob each get their own counter. This is what you want when state belongs to one user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global counter&lt;/strong&gt; — seeds: &lt;code&gt;["counter"]&lt;/code&gt; (no wallet)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Global counter PDA (no wallet in seeds)
  Derived from A's perspective: 3VKtGLreXepsZFDnyhFKsCUz9cjy96b1cHXLaYj3XA6z
  Derived from B's perspective: 3VKtGLreXepsZFDnyhFKsCUz9cjy96b1cHXLaYj3XA6z
  Same address? true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every wallet derives the same address. That is exactly what you want for a &lt;strong&gt;global config singleton&lt;/strong&gt; — my program uses &lt;code&gt;[b"config"]&lt;/code&gt; for admin settings and a pause flag. It would be a disaster for a per-user counter, because every user would be mutating the same account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Near-miss seed variants&lt;/strong&gt; — change one character and you get a different address entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Near-miss seed variants
  ["counter", walletA]         -&amp;gt; 8Fr8LiP6khZsTcDAeh7Gjvot5TFCbPEDBxuzv7Ju5JMj
  ["counters", walletA]        -&amp;gt; Cre7aNxq6dHKvp68ucUgLfCRzrSNjPNDr2tzyeqMbNcn
  ["counter\0", walletA]       -&amp;gt; FFWPDBuhjXGqALszpVrp9DWJcPLTd4TCoKrk2jpMDAwr
  ["Counter", walletA]         -&amp;gt; 494XwXexJXLpiKk2APhairz1E7KLGp7hiQ4ogQBAQKqf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeds are not fuzzy. &lt;code&gt;"counter"&lt;/code&gt; and &lt;code&gt;"counters"&lt;/code&gt; are different namespaces. &lt;code&gt;"Counter"&lt;/code&gt; and &lt;code&gt;"counter"&lt;/code&gt; are different. A null byte matters. Treat seed strings like schema migrations: changing them creates a new address, not an updated row.&lt;/p&gt;

&lt;p&gt;I also tried to spoof a PDA — pass Wallet B's counter address while signing as Wallet A:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempting to spoof a PDA...
  Spoof rejected: AnchorError caused by account: counter. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime did not care that I knew an address. It recomputed the expected PDA from the seeds and the signer, saw they did not match the account I passed in, and rejected the transaction. That is the whole point of putting &lt;code&gt;seeds&lt;/code&gt; in the account constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bump buys you
&lt;/h2&gt;

&lt;p&gt;There are 256 possible bump values (255 down to 0). Only one is canonical — the first value that produces a valid off-curve address when tried in that order.&lt;/p&gt;

&lt;p&gt;You should always use the canonical bump. Non-canonical bumps might derive valid-looking addresses in some edge cases, but Anchor's &lt;code&gt;seeds&lt;/code&gt; constraint expects the canonical one. Passing a wrong bump fails verification.&lt;/p&gt;

&lt;p&gt;When you write &lt;code&gt;bump&lt;/code&gt; in an &lt;code&gt;init&lt;/code&gt; constraint, Anchor finds the canonical bump for you and exposes it as &lt;code&gt;ctx.bumps.counter&lt;/code&gt;. Store it in your account struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every later instruction, pass &lt;code&gt;bump = counter.bump&lt;/code&gt; instead of re-running the 255→0 search. Re-derivation is cheap in a script and expensive inside a tight on-chain loop. Storing one byte in your account is free by comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full lifecycle
&lt;/h2&gt;

&lt;p&gt;Here is the sequence my counter program went through over five days, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Derive the address (off-chain or in a client)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counterPda&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findProgramAddressSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;programId&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is pure math. No transaction, no rent, no account yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Initialize the account at that address&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;init_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InitCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.bumps.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; constraint creates the account, pays rent from the user, writes the discriminator and initial fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Mutate on subsequent instructions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt;&lt;span class="nf"&gt;.checked_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CounterError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Overflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The account already exists. Anchor verifies seeds, bump, and &lt;code&gt;has_one = user&lt;/code&gt; before your code runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Close to reclaim rent&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&lt;/span&gt;
    &lt;span class="nd"&gt;mut,&lt;/span&gt;
    &lt;span class="nd"&gt;close&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;user,&lt;/span&gt;
    &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;close = user&lt;/code&gt; does three things: it drains the account's lamports to the user, zeroes the data, and marks the account for deallocation at the end of the transaction. My test logged roughly 1.23M lamports refunded — real money back in the wallet, not a metaphorical row deletion.&lt;/p&gt;

&lt;p&gt;One thing that tripped me up: &lt;strong&gt;closing is not instant visibility.&lt;/strong&gt; Within the same transaction the account still exists until the runtime finishes. After confirmation, &lt;code&gt;getAccountInfo&lt;/code&gt; returns null. Plan your client code accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The program ID is part of the derivation.&lt;/strong&gt; Same seeds, different program, different address. Copy-pasting seed logic between programs without updating the program ID will silently point at the wrong account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PDAs cannot sign transactions on their own.&lt;/strong&gt; They have no private key. When a PDA needs to transfer tokens or invoke another program, your program signs on its behalf using &lt;strong&gt;signer seeds&lt;/strong&gt; — the same seed bytes plus the bump, passed to &lt;code&gt;invoke_signed&lt;/code&gt;. Client wallets sign with a keypair; programs sign for PDAs with seeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;init_if_needed&lt;/code&gt; is convenient and also a footgun.&lt;/strong&gt; Reach for it deliberately when you genuinely want "create if missing, otherwise use existing." Default to explicit &lt;code&gt;init&lt;/code&gt; so you never accidentally overwrite or underpay for an account you thought was new.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start the local validator before you deploy.&lt;/strong&gt; Every &lt;code&gt;fetch failed&lt;/code&gt; and &lt;code&gt;failed to get recent blockhash&lt;/code&gt; error I hit was because nothing was listening on &lt;code&gt;localhost:8899&lt;/code&gt;. Terminal 1: &lt;code&gt;solana-test-validator&lt;/code&gt;. Terminal 2: everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The seeds constraint and &lt;code&gt;has_one&lt;/code&gt; do different jobs.&lt;/strong&gt; Seeds prove you passed the right account address. &lt;code&gt;has_one&lt;/code&gt; proves the data inside that account belongs to the signer. You need both when users own their own PDAs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;If you are exactly where I was a week ago — program compiles, tests pass, mental model still fuzzy — write your own explainer. Every sentence you cannot finish cleanly is a gap worth filling.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>rust</category>
      <category>anchor</category>
    </item>
    <item>
      <title>How I built a counter program in Anchor and learned to trust my tests</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:06:38 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/how-i-built-a-counter-program-in-anchor-and-learned-to-trust-my-tests-37g8</link>
      <guid>https://dev.to/prasiddhnaik/how-i-built-a-counter-program-in-anchor-and-learned-to-trust-my-tests-37g8</guid>
      <description>&lt;p&gt;The counter program is small, but the useful part was not the counter. The useful part was learning where state lives, how Anchor turns account validation into Rust types, and how a test suite can prove that a permission check is actually doing work.&lt;/p&gt;

&lt;p&gt;Here is the first piece that made Anchor feel different from a normal Web2 backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Accounts)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Initialize&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(init,&lt;/span&gt; &lt;span class="nd"&gt;payer&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;authority,&lt;/span&gt; &lt;span class="nd"&gt;space&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nd"&gt;Counter::INIT_SPACE)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a Web2 handler, I am used to receiving a request body and then deciding what database rows to read or write. In Anchor, the accounts are part of the request. This struct says the &lt;code&gt;counter&lt;/code&gt; account should be created, the &lt;code&gt;authority&lt;/code&gt; signer pays for that account, and the System Program is available because account creation needs it. The &lt;code&gt;space&lt;/code&gt; value reserves enough bytes for Anchor's discriminator plus the fields in my &lt;code&gt;Counter&lt;/code&gt; account.&lt;/p&gt;

&lt;p&gt;The account itself is tiny: it stores the wallet allowed to update the counter and the current number.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; handler is short because the account plumbing already happened before the body runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.authority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.authority&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ctx.accounts&lt;/code&gt; gives me typed access to the accounts from the &lt;code&gt;Initialize&lt;/code&gt; struct. At this point, Anchor has already checked that &lt;code&gt;authority&lt;/code&gt; signed, that the counter account can be initialized, and that the right programs are present. My code only has to write the starting state.&lt;/p&gt;

&lt;p&gt;The second instruction increments the counter. This is the important part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut,&lt;/span&gt; &lt;span class="nd"&gt;has_one&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;authority)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line is &lt;code&gt;has_one = authority&lt;/code&gt;. It tells Anchor to compare the &lt;code&gt;authority&lt;/code&gt; field stored inside the &lt;code&gt;Counter&lt;/code&gt; account with the signer passed into this instruction. If they do not match, the handler body never gets to run.&lt;/p&gt;

&lt;p&gt;That was the permission check. The next question was whether my tests could prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Happy Path
&lt;/h2&gt;

&lt;p&gt;The first test creates a fresh LiteSVM instance, initializes a counter, increments it, then reads the account back. The whole point is this final check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="py"&gt;.authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test would fail if &lt;code&gt;initialize&lt;/code&gt; stored the wrong authority, if &lt;code&gt;increment&lt;/code&gt; changed the wrong amount, or if the account could not be deserialized back into &lt;code&gt;Counter&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;But happy paths are not enough.&lt;/p&gt;

&lt;p&gt;If I only test that the right wallet can increment, I have not proved that the wrong wallet cannot. That is the Solana version of testing that an admin endpoint works but never testing that a non-admin gets rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Path
&lt;/h2&gt;

&lt;p&gt;So I added a test where &lt;code&gt;authority_a&lt;/code&gt; creates the counter and &lt;code&gt;authority_b&lt;/code&gt; tries to increment it. The important assertion is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"increment should fail when signed by the wrong authority"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test would fail if I accidentally removed the &lt;code&gt;has_one = authority&lt;/code&gt; constraint or otherwise allowed any signer to increment someone else's counter.&lt;/p&gt;

&lt;p&gt;That sentence is the whole reason the test exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking It On Purpose
&lt;/h2&gt;

&lt;p&gt;The most useful thing I did was break the program intentionally.&lt;/p&gt;

&lt;p&gt;For one experiment, I changed the increment logic from "add one" to "add two":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.checked_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction still succeeded, but the test caught the wrong state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thread 'initialize_then_increment' panicked at programs/counter/tests/counter.rs:86:5:
assertion `left == right` failed
  left: 2
 right: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of failure I want. It is clear, specific, and tied directly to the behavior I care about.&lt;/p&gt;

&lt;p&gt;I also removed &lt;code&gt;has_one = authority&lt;/code&gt; once. Before the failure test existed, the suite stayed green. After the failure test existed, the suite failed exactly where it should:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;increment should fail when signed by the wrong authority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the moment the permission check stopped being a line I trusted and became a line I could audit by running tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Anchor programs are small on the surface, but a lot happens before a handler body runs. The account structs are not just TypeScript-style types or documentation. They are validation rules. They decide which accounts are writable, which accounts must sign, which accounts get initialized, and which relationships must already be true.&lt;/p&gt;

&lt;p&gt;I also learned that a green test suite only means something if it tries to prove the program wrong. The happy path proved the counter could work. The failure path proved the authority gate was real. The mutation experiments proved both assertions were load-bearing.&lt;/p&gt;

&lt;p&gt;If I had another week, I would add one more failure test for double initialization, tighten the failure assertions to check the exact Anchor error, and then move the counter account to a PDA so the client does not have to create a random keypair for it.&lt;/p&gt;

&lt;p&gt;Small program. Real lesson.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>rust</category>
      <category>anchor</category>
    </item>
    <item>
      <title>Three Token-2022 mints in one week: fees, yield, and soul-bound</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:13:34 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/three-token-2022-mints-in-one-week-fees-yield-and-soul-bound-41hn</link>
      <guid>https://dev.to/prasiddhnaik/three-token-2022-mints-in-one-week-fees-yield-and-soul-bound-41hn</guid>
      <description>&lt;p&gt;If you know SPL tokens but not &lt;strong&gt;Token Extensions&lt;/strong&gt;, here is the short version: &lt;strong&gt;Token-2022&lt;/strong&gt; is the upgraded Solana token program. At mint creation, you can opt into extensions: fees, interest, transfer rules, and other behaviors that live directly on the mint account. Think of it like middleware baked into the asset. Every wallet and program that touches the token sees the same rules, not a wrapper I wrote on the side.&lt;/p&gt;

&lt;p&gt;I shipped three mints on &lt;strong&gt;devnet&lt;/strong&gt; over this arc. Same CLI, three different behaviors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mint 1: Transfer Fee
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Days:&lt;/strong&gt; 50 and 51&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Mint:&lt;/strong&gt; &lt;code&gt;8gSexQr1JF4NeCKRR72nckSUdexmN5ZCuBTVuTfsJdPp&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Explorer:&lt;/strong&gt; &lt;a href="https://explorer.solana.com/address/8gSexQr1JF4NeCKRR72nckSUdexmN5ZCuBTVuTfsJdPp?cluster=devnet" rel="noopener noreferrer"&gt;View on Solana Explorer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Extension:&lt;/strong&gt; Transfer fee, configured at 100 basis points, with a max fee of 1,000,000 tokens.&lt;/p&gt;

&lt;p&gt;This was the first mint where the token itself started enforcing behavior. I configured a 1% transfer fee and then sent 1000 tokens to a fresh recipient wallet. The recipient received 990 spendable tokens, while 10 tokens sat in the account's withheld fee field until I withdrew them with the fee authority.&lt;/p&gt;

&lt;p&gt;The create command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 1000000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I created my token account and minted supply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-account 8gSexQr1JF4NeCKRR72nckSUdexmN5ZCuBTVuTfsJdPp
spl-token mint 8gSexQr1JF4NeCKRR72nckSUdexmN5ZCuBTVuTfsJdPp 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fee lifecycle looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token transfer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expected-fee&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1000 &lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unfunded-recipient&lt;/span&gt;

spl-token display &lt;span class="nv"&gt;$RECIPIENT_TA&lt;/span&gt;
spl-token withdraw-withheld-tokens &lt;span class="nv"&gt;$MY_TA&lt;/span&gt; &lt;span class="nv"&gt;$RECIPIENT_TA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where I would use it: a creator token with royalties on secondary movement, a protocol treasury skim, or a community currency where every transfer contributes a small amount back to the project. The important part is that the fee is not enforced by a web server. The Token-2022 program enforces it during transfer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mint 2: Transfer Fee Plus Interest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day:&lt;/strong&gt; 52&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Mint:&lt;/strong&gt; &lt;code&gt;6KHkyuc1v8BqZySkEp6wGdAiG26TJnH6h5JGSU91FD7s&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Explorer:&lt;/strong&gt; &lt;a href="https://explorer.solana.com/address/6KHkyuc1v8BqZySkEp6wGdAiG26TJnH6h5JGSU91FD7s?cluster=devnet" rel="noopener noreferrer"&gt;View on Solana Explorer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Extensions:&lt;/strong&gt; Transfer fee and interest-bearing.&lt;/p&gt;

&lt;p&gt;This mint stacked two behaviors onto the same asset: the same 1% transfer fee, plus an interest-bearing configuration at 5000 basis points. In CLI terms, that is a 50% interest rate.&lt;/p&gt;

&lt;p&gt;The create command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 1000000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interest-rate&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After creation, &lt;code&gt;spl-token display&lt;/code&gt; showed both extensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extensions
  Interest-bearing:
    Current rate: 5000bps
    Average rate: 5000bps
  Transfer fees:
    Current fee: 100bps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtle thing matters here: the interest-bearing extension changes the &lt;strong&gt;displayed UI amount&lt;/strong&gt; over time. It does not mint new supply into existence. After minting 1,000,000 tokens, I watched the account's displayed balance creep upward over about 30 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1000004.991023
1000005.482204
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction is easy to miss. It feels like yield, but it is a Token-2022 display behavior based on the configured rate, not a custom staking program minting rewards.&lt;/p&gt;

&lt;p&gt;Where I would use it: savings-style balances, loyalty points that visibly grow, or any token where a wallet-facing balance should accrue over time without a custom program.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mint 3: Non-Transferable
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day:&lt;/strong&gt; 54&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Mint:&lt;/strong&gt; &lt;code&gt;DRVQaZxsixMWiUrJxUmwXq6eY6vj1ZjEUz5epgoc25fk&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Explorer:&lt;/strong&gt; &lt;a href="https://explorer.solana.com/address/DRVQaZxsixMWiUrJxUmwXq6eY6vj1ZjEUz5epgoc25fk?cluster=devnet" rel="noopener noreferrer"&gt;View on Solana Explorer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Extension:&lt;/strong&gt; Non-transferable.&lt;/p&gt;

&lt;p&gt;This one flipped the mental model. Instead of making transfers more complex, it disabled transfers entirely.&lt;/p&gt;

&lt;p&gt;The create command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="nt"&gt;--program-2022&lt;/span&gt; &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt;
spl-token create-account &lt;span class="nv"&gt;$MINT&lt;/span&gt;
spl-token mint &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I created a recipient account and tried to transfer the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-account &lt;span class="nv"&gt;$MINT&lt;/span&gt; &lt;span class="nt"&gt;--owner&lt;/span&gt; &lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt; &lt;span class="nt"&gt;--fee-payer&lt;/span&gt; ~/.config/solana/id.json
spl-token transfer &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1 &lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt; &lt;span class="nt"&gt;--allow-unfunded-recipient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The program rejected it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Program log: Instruction: TransferChecked
Program log: Transfer is disabled for this mint
Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb failed: custom program error: 0x25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;spl-token display&lt;/code&gt; confirmed the mint-level rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extensions
  Non-transferable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where I would use it: certificates, proof-of-attendance badges, reputation credentials, or any token that should stay attached to the original holder instead of becoming a tradable asset.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Audit Habit
&lt;/h2&gt;

&lt;p&gt;Before writing this, I ran &lt;code&gt;spl-token display&lt;/code&gt; on each mint and checked the &lt;code&gt;Extensions&lt;/code&gt; block. That felt like doing &lt;code&gt;DESCRIBE table&lt;/code&gt; after a database migration. I was not trusting the commands I remembered typing. I was reading the public account state that wallets, explorers, and other programs read too.&lt;/p&gt;

&lt;p&gt;One practical note: these mints live under the Token-2022 program. If &lt;code&gt;spl-token display &amp;lt;MINT&amp;gt;&lt;/code&gt; says an account does not exist, try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token display &amp;lt;MINT&amp;gt; &lt;span class="nt"&gt;--program-2022&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit that exact confusion while testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Surprised Me
&lt;/h2&gt;

&lt;p&gt;The surprising part is how much product behavior can live on the mint itself. Transfer fees felt like something I would normally write in backend code. Interest felt like something I would expect from a staking program. Non-transferability felt like an app-level rule. In Token-2022, all three are mint configuration.&lt;/p&gt;

&lt;p&gt;In a real product, I would reach for &lt;strong&gt;transfer fees&lt;/strong&gt; when the asset needs a treasury or royalty mechanism, &lt;strong&gt;interest-bearing tokens&lt;/strong&gt; when the displayed balance should grow over time, and &lt;strong&gt;non-transferable tokens&lt;/strong&gt; when the point is reputation or identity rather than liquidity.&lt;/p&gt;

&lt;p&gt;That is the arc I wanted to understand this week: not just how to create a token, but how much behavior can be attached to the token before writing a custom program.&lt;/p&gt;

&lt;p&gt;All mints and transactions above are on Solana devnet.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>solana</category>
    </item>
    <item>
      <title>What I learned minting NFTs on Solana with Token Extensions published: true</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Wed, 10 Jun 2026 11:48:12 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/what-i-learned-minting-nfts-on-solana-with-token-extensionspublished-true-50i8</link>
      <guid>https://dev.to/prasiddhnaik/what-i-learned-minting-nfts-on-solana-with-token-extensionspublished-true-50i8</guid>
      <description>&lt;p&gt;Before this week I thought a Solana NFT was a Metaplex thing. It turns out you can mint a full NFT — with metadata, a collection, and live updates — using just the &lt;a href="https://solana.com/docs/tokens/extensions" rel="noopener noreferrer"&gt;Token Extensions program&lt;/a&gt; and the &lt;code&gt;spl-token&lt;/code&gt; CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;If you already understand SPL tokens, an NFT is the same program with tighter rules: &lt;strong&gt;0 decimals&lt;/strong&gt; (nothing to split), &lt;strong&gt;supply capped at 1&lt;/strong&gt;, and &lt;strong&gt;mint authority disabled&lt;/strong&gt; when you are done so no one can ever inflate it. Uniqueness is enforced by token math, not by a &lt;code&gt;is_nft&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/tokens/extensions/metadata" rel="noopener noreferrer"&gt;Metadata extension&lt;/a&gt; stores name, symbol, and URI directly on the mint account — no separate Metaplex metadata account. The URI points at a JSON file hosted somewhere public; that JSON holds the image URL and attributes. Wallets and &lt;a href="https://explorer.solana.com/?cluster=devnet" rel="noopener noreferrer"&gt;Solana Explorer&lt;/a&gt; read the on-chain fields, fetch the JSON, render the art.&lt;/p&gt;

&lt;p&gt;Collections use the &lt;a href="https://solana.com/docs/tokens/extensions/group-member" rel="noopener noreferrer"&gt;Group and Member extensions&lt;/a&gt;: one mint is the collection (group), each NFT mint points back to it (member). Web2 analogy: a &lt;code&gt;collection_id&lt;/code&gt; foreign key stamped into the row — except there is no central database to join against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Over Days 44–47 on devnet I went from a named NFT to a collection I could inspect and patch live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. NFT with on-chain metadata.&lt;/strong&gt; I minted &lt;strong&gt;Black Box Oracle&lt;/strong&gt; (&lt;code&gt;ORCL&lt;/code&gt;) on Token-2022 with &lt;code&gt;--enable-metadata&lt;/code&gt;, pointed the URI at a gist JSON file, minted one token, and disabled mint authority.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TOKENZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb

spl-token create-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; &lt;span class="nv"&gt;$TOKENZ&lt;/span&gt; &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 ./nft....json
spl-token initialize-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"Black Box Oracle"&lt;/span&gt; &lt;span class="s2"&gt;"ORCL"&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;GIST_RAW_URL&amp;gt;"&lt;/span&gt;
spl-token mint &amp;lt;MINT&amp;gt; 1
spl-token authorize &amp;lt;MINT&amp;gt; mint &lt;span class="nt"&gt;--disable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mint: &lt;a href="https://explorer.solana.com/address/Nft5CnhQbXUjvce9RGux9BVJwdH8QNGZgqoQtQ2KJeJ?cluster=devnet" rel="noopener noreferrer"&gt;&lt;code&gt;Nft5CnhQbXUjvce9RGux9BVJwdH8QNGZgqoQtQ2KJeJ&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A collection with two members.&lt;/strong&gt; &lt;strong&gt;Solana Sketchbook&lt;/strong&gt; (&lt;code&gt;SKTCH&lt;/code&gt;) uses &lt;code&gt;--enable-group&lt;/code&gt;; each sketch uses &lt;code&gt;--enable-member&lt;/code&gt;. Both flags must be set at mint creation — you cannot bolt them on later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token initialize-group &amp;lt;COLLECTION&amp;gt; 3
spl-token initialize-member &amp;lt;MEMBER&amp;gt; &amp;lt;COLLECTION&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collection: &lt;a href="https://explorer.solana.com/address/6CBzxZ96JX87hhVbrr7NdyiXSneeTbVW2mTHCExGFVbg?cluster=devnet" rel="noopener noreferrer"&gt;&lt;code&gt;6CBzxZ96JX87hhVbrr7NdyiXSneeTbVW2mTHCExGFVbg&lt;/code&gt;&lt;/a&gt; · Members: &lt;a href="https://explorer.solana.com/address/CXr5TwDWQz72W77F4YRZV2HKUQDP33tVnhbhqTL9GjuY?cluster=devnet" rel="noopener noreferrer"&gt;Sketch #1&lt;/a&gt; · &lt;a href="https://explorer.solana.com/address/8R5BfGpvCDc5n9o4YdsrCFqaMdu5z45vw93gKQBTJfv8?cluster=devnet" rel="noopener noreferrer"&gt;Sketch #2&lt;/a&gt;. Group shows &lt;code&gt;size: 2&lt;/code&gt;, &lt;code&gt;max_size: 3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An on-chain audit.&lt;/strong&gt; I ran &lt;code&gt;spl-token display&lt;/code&gt; on the member and collection mints and verified extension blocks, supply, and that &lt;code&gt;TokenGroupMember → Group&lt;/code&gt; matched the collection address byte-for-byte. Same habit as querying a row after a migration — trust the CLI output, not just the Explorer UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Live metadata edits.&lt;/strong&gt; While I still held update authority, I renamed Sketch #1 to &lt;strong&gt;Field Notes&lt;/strong&gt;, added and removed a custom &lt;code&gt;rarity&lt;/code&gt; field, and pointed the URI at new JSON — each change one transaction, visible in Explorer within a slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprising part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extensions are schema decisions.&lt;/strong&gt; Group and member must be declared when you create the mint. I could not retro-fit my metadata NFT into the Sketchbook collection afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three layers, three speeds.&lt;/strong&gt; Name and URI update on chain immediately. The image lives off chain and wallets cache it aggressively — Explorer showed "Field Notes" while Phantom still showed the old art.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No separate NFT program.&lt;/strong&gt; Collectibles are SPL Token plus a small set of extensions. &lt;a href="https://developers.metaplex.com/core" rel="noopener noreferrer"&gt;Metaplex Core&lt;/a&gt; is still the common marketplace path; Token Extensions is the native SPL stack when you want metadata and grouping inside the token program itself.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
    </item>
    <item>
      <title>What Token Extensions Finally Made Me Understand</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:09:02 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/what-token-extensions-finally-made-me-understand-36p7</link>
      <guid>https://dev.to/prasiddhnaik/what-token-extensions-finally-made-me-understand-36p7</guid>
      <description>&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; Token-2022 showed me that tokens can have rules, not just balances.&lt;/p&gt;

&lt;p&gt;This week I moved past basic SPL tokens and started using Token Extensions.&lt;/p&gt;

&lt;p&gt;At first, I thought a token was mostly just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a mint&lt;/li&gt;
&lt;li&gt;token accounts&lt;/li&gt;
&lt;li&gt;balances&lt;/li&gt;
&lt;li&gt;transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But Token-2022 adds something more interesting: behavior.&lt;/p&gt;

&lt;p&gt;A token can charge transfer fees.&lt;br&gt;
A token can display interest over time.&lt;br&gt;
A token can start every account frozen.&lt;br&gt;
A token can be non-transferable.&lt;br&gt;
A token can have a permanent delegate for revocation.&lt;/p&gt;

&lt;p&gt;That made tokens feel less like simple coins and more like product systems.&lt;/p&gt;
&lt;h2&gt;
  
  
  Interest-bearing tokens
&lt;/h2&gt;

&lt;p&gt;The first extension that clicked was interest-bearing tokens.&lt;/p&gt;

&lt;p&gt;The weird part is that the raw balance does not actually grow. If I mint 1000 tokens, the account still holds 1000 tokens. The interest-bearing extension changes the UI amount by applying an interest formula over time.&lt;/p&gt;

&lt;p&gt;So it feels like a savings account, but without a backend cron job constantly minting more tokens.&lt;/p&gt;

&lt;p&gt;That was the first “ohhh” moment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Multi-extension tokens
&lt;/h2&gt;

&lt;p&gt;Then I tried combining extensions.&lt;/p&gt;

&lt;p&gt;One mint could have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfer fees&lt;/li&gt;
&lt;li&gt;interest&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means the token can charge a fee when it moves, display an interest-adjusted balance, and carry its name, symbol, and URI.&lt;/p&gt;

&lt;p&gt;The important lesson: extensions must be planned before the mint is created. You cannot just bolt them on later like a random npm package. Annoying, but fair.&lt;/p&gt;

&lt;p&gt;Here is the kind of command I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 500 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interest-rate&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates a Token-2022 mint with multiple extensions enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default frozen accounts
&lt;/h2&gt;

&lt;p&gt;The default frozen account extension felt like a compliance system.&lt;/p&gt;

&lt;p&gt;Every new token account starts frozen. The authority has to thaw an account before it can receive, send, or burn tokens.&lt;/p&gt;

&lt;p&gt;In Web2 terms, it is like saying:&lt;/p&gt;

&lt;p&gt;“You can have an account, but you cannot use it until you are approved.”&lt;/p&gt;

&lt;p&gt;That makes sense for regulated assets, gated communities, or tokens where both sender and receiver need permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspecting mints
&lt;/h2&gt;

&lt;p&gt;One of the best lessons was not building, but reading.&lt;/p&gt;

&lt;p&gt;Using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token display &lt;span class="o"&gt;[&lt;/span&gt;MINT_ADDRESS] &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could inspect which extensions were active on a mint.&lt;/p&gt;

&lt;p&gt;That made me realize extension configuration is like reading a project’s config file. Before trusting a token, you should check its rules.&lt;/p&gt;

&lt;p&gt;Does it have fees?&lt;br&gt;
Who controls the fee authority?&lt;br&gt;
Is there a freeze authority?&lt;br&gt;
Is there a permanent delegate?&lt;/p&gt;

&lt;p&gt;The token’s behavior is part of the trust model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revocable credentials
&lt;/h2&gt;

&lt;p&gt;My favorite idea was combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;non-transferable tokens&lt;/li&gt;
&lt;li&gt;permanent delegate&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That creates something like a certificate.&lt;/p&gt;

&lt;p&gt;The token cannot be transferred, so the holder cannot sell it or send it to someone else. But the issuer can still revoke it by burning it through the permanent delegate.&lt;/p&gt;

&lt;p&gt;That makes sense for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;course certificates&lt;/li&gt;
&lt;li&gt;badges&lt;/li&gt;
&lt;li&gt;memberships&lt;/li&gt;
&lt;li&gt;licenses&lt;/li&gt;
&lt;li&gt;reputation tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was the moment Token Extensions felt less like “crypto stuff” and more like actual product design.&lt;/p&gt;

&lt;p&gt;Token Extensions are not random features.&lt;/p&gt;

&lt;p&gt;They are rules.&lt;/p&gt;

&lt;p&gt;Transfer fees decide what happens when value moves.&lt;br&gt;
Interest-bearing extensions decide how balances are displayed.&lt;br&gt;
Default frozen accounts decide who is allowed in.&lt;br&gt;
Non-transferable tokens decide whether something can be sold.&lt;br&gt;
Permanent delegates decide who can revoke or control tokens.&lt;/p&gt;

&lt;p&gt;The big lesson: with Token-2022, the token program can enforce behavior that an app would normally need to manage itself.&lt;/p&gt;

&lt;p&gt;That is powerful.&lt;/p&gt;

&lt;p&gt;Also slightly terrifying.&lt;/p&gt;

&lt;p&gt;Because once these rules exist, users need to know what they are holding.&lt;/p&gt;

&lt;p&gt;Tags: #solana #web3 #learning #100daysofsolana&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>Solana Token Extensions: Fees and Metadata</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Tue, 26 May 2026 14:48:24 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/solana-token-extensions-fees-and-metadata-2500</link>
      <guid>https://dev.to/prasiddhnaik/solana-token-extensions-fees-and-metadata-2500</guid>
      <description>&lt;p&gt;Over five days on devnet I went from a basic mint to on-chain metadata, transfer fees enforced by the Token-2022 program, a full lifecycle run without notes, and a non-transferable credential token. The throughline: &lt;strong&gt;token behavior is configured at the mint&lt;/strong&gt;, not patched in later.&lt;/p&gt;

&lt;p&gt;This post is what I wish I had read before I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting point
&lt;/h2&gt;

&lt;p&gt;I already had Solana accounts, wallets, and SOL transfers down. Tokens were the next layer.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/tokens/extensions" rel="noopener noreferrer"&gt;Token Extensions Program&lt;/a&gt; (Token-2022) attaches capabilities to the mint account at creation time. Transfer fees, metadata pointers, non-transferability — you opt in with CLI flags when you run &lt;code&gt;create-token&lt;/code&gt;. You cannot add those extensions after the fact.&lt;/p&gt;

&lt;p&gt;Plan the token's rules before you mint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Program vs Token Extensions
&lt;/h2&gt;

&lt;p&gt;Solana has two token programs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Program&lt;/th&gt;
&lt;th&gt;Address (short)&lt;/th&gt;
&lt;th&gt;What I used it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original SPL Token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TokenkegQfeZyiNw...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classic fungible tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token Extensions (Token-2022)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metadata, transfer fees, non-transferable mints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want extensions, opt in at mint creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson I learned the hard way:&lt;/strong&gt; after creating a Token-2022 mint, pass &lt;code&gt;--program-2022&lt;/code&gt; (or &lt;code&gt;--program-id TokenzQd...&lt;/code&gt;) on every follow-up &lt;code&gt;spl-token&lt;/code&gt; command. I hit &lt;code&gt;AccountInvalidOwner&lt;/code&gt; on &lt;code&gt;initialize-metadata&lt;/code&gt; until I added that flag. The CLI defaults to the original Token Program unless you tell it otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 29–30: Mint and metadata
&lt;/h2&gt;

&lt;p&gt;The first step is boring on purpose — create a mint, create an associated token account, mint supply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana config &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; devnet
solana balance

spl-token create-token &lt;span class="nt"&gt;--decimals&lt;/span&gt; 9
spl-token create-account &amp;lt;MINT&amp;gt;
spl-token mint &amp;lt;MINT&amp;gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a fungible token. Wallets and explorers can show a balance, but without metadata it is just an address with decimals.&lt;/p&gt;

&lt;p&gt;Token-2022 lets you attach identity on-chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 9

spl-token initialize-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"MyToken"&lt;/span&gt; &lt;span class="s2"&gt;"MTK"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://example.com/metadata.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-2022&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Name, symbol, and URI live in the mint's extension data. Explorers and wallets read them directly from chain state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order matters:&lt;/strong&gt; initialize metadata before you mint large supply if your workflow depends on a fully configured mint from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 31: Transfer fees at the protocol level
&lt;/h2&gt;

&lt;p&gt;This is where Token Extensions stopped feeling like "SPL Token plus extras" and started feeling like a different design surface.&lt;/p&gt;

&lt;p&gt;I created &lt;strong&gt;ReinforceCoin (RFC)&lt;/strong&gt; with a 2% transfer fee (200 basis points):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 200 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mint: &lt;code&gt;8rVUkh7tbh1uR9GMbAZYZeFQ3EZ8Tkqu5nh3kToSAZ7a&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After metadata, I minted 1000 tokens to my wallet, created a token account for a second devnet wallet (&lt;code&gt;second-wallet.json&lt;/code&gt;), and transferred 100 tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token transfer &lt;span class="nt"&gt;--fund-recipient&lt;/span&gt; &amp;lt;MINT&amp;gt; 100 &amp;lt;RECIPIENT_PUBKEY&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expected-fee&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unfunded-recipient&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-2022&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recipient balance was &lt;strong&gt;98&lt;/strong&gt;, not 100. Two tokens were withheld as fees — enforced by the Token-2022 program during &lt;code&gt;TransferChecked&lt;/code&gt;, not by anything I wrote in a script.&lt;/p&gt;

&lt;p&gt;Every transfer through this mint pays the same fee. Wallets, DEXs, and CLI tools all hit the same on-chain rule.&lt;/p&gt;

&lt;p&gt;To collect withheld fees, the mint authority withdraws from the recipient's token account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token withdraw-withheld-tokens &amp;lt;MY_TOKEN_ACCOUNT&amp;gt; &amp;lt;RECIPIENT_TOKEN_ACCOUNT&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-2022&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fees accrue on transfer and get collected in a separate step. That two-phase flow was not obvious from the docs alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 32: Reinforcing the full lifecycle
&lt;/h2&gt;

&lt;p&gt;Day 32 was deliberately repetitive: run the entire flow in one sitting without notes. Create mint → metadata → mint supply → fund recipient → transfer with fee → withdraw withheld → verify with &lt;code&gt;spl-token display&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Muscle memory, not trivia.&lt;/p&gt;

&lt;p&gt;The commands that stuck:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;spl-token display &amp;lt;MINT&amp;gt; --program-2022&lt;/code&gt; — extensions, fee config, authority&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--expected-fee&lt;/code&gt; on transfer — CLI sanity-checks your math against on-chain rules&lt;/li&gt;
&lt;li&gt;Token &lt;strong&gt;account&lt;/strong&gt; addresses vs &lt;strong&gt;mint&lt;/strong&gt; addresses — see below&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Day 33: Non-transferable tokens (soulbound credentials)
&lt;/h2&gt;

&lt;p&gt;For credentials, diplomas, or achievement badges, you want the chain to reject transfers — not a UI warning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mint: &lt;code&gt;Gvztbeuf4Szs9KDvXxHkzRGPEsnf895FozQPoTnUPipu&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I minted 10 tokens to my ATA (&lt;code&gt;5abnDEFHy5Btm2kyazagTaiaQaXUv4bStZusw69jndS3&lt;/code&gt;) and tried to send 5 to an experiment wallet. The transaction failed in simulation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Program log: Transfer is disabled for this mint
custom program error: 0x25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That failure was success. The Token-2022 program enforces non-transferability — no workaround at the client level.&lt;/p&gt;

&lt;p&gt;Burning still worked. I reduced my balance from 10 to 7 by burning from the &lt;strong&gt;token account&lt;/strong&gt;, not the mint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wrong — mint address is not a token account&lt;/span&gt;
spl-token burn Gvztbeuf4Szs9KDvXxHkzRGPEsnf895FozQPoTnUPipu 3 ...

&lt;span class="c"&gt;# Right — pass the ATA that holds your balance&lt;/span&gt;
spl-token burn 5abnDEFHy5Btm2kyazagTaiaQaXUv4bStZusw69jndS3 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Gvztbeuf...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mint — token definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5abnDEFH...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;My associated token account — holds balance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5K6a2t28...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Experiment wallet's token account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Confusing mint with a token account is an easy mistake to make. The CLI error messages nudge you toward the right object, but knowing the account model saves time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rules are chosen at birth.&lt;/strong&gt; Extensions are set when the mint is created. Plan the economics before you mint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fees are withheld, not instantly routed.&lt;/strong&gt; Transfer fees sit in recipient token accounts until someone with authority runs &lt;code&gt;withdraw-withheld-tokens&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-transferable does not mean frozen.&lt;/strong&gt; I can still mint and burn. I just cannot move balances between owners — which is exactly what you want for credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;--program-2022&lt;/code&gt; flag is load-bearing.&lt;/strong&gt; Same CLI, different program owner. Forgetting the flag produces errors that look like corruption but are just wrong program IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write down addresses as you go.&lt;/strong&gt; Mint, your ATA, recipient ATA — each step prints new ones. I lost time re-deriving them from transaction logs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful references while you work: &lt;a href="https://solana.com/docs/tokens" rel="noopener noreferrer"&gt;Tokens on Solana&lt;/a&gt;, &lt;a href="https://solana.com/docs/tokens/extensions" rel="noopener noreferrer"&gt;Token Extensions&lt;/a&gt;, and the &lt;a href="https://spl.solana.com/token" rel="noopener noreferrer"&gt;SPL Token CLI&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>blockchain</category>
      <category>100daysofsolana</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Solana Mental Model for Web2 Developers</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Sun, 17 May 2026 12:02:14 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/solana-mental-model-for-web2-developers-2c02</link>
      <guid>https://dev.to/prasiddhnaik/solana-mental-model-for-web2-developers-2c02</guid>
      <description>&lt;p&gt;If you come from Web2 development, the easiest way to understand Solana is to stop thinking about contracts first and start thinking about accounts.&lt;/p&gt;

&lt;p&gt;On Solana, almost everything is an account. A wallet is an account. A token balance is an account. A program which is Solana's version of a smart contract is also an account. Instead of storing application state in a database table or inside a contract object, Solana stores state in accounts that live on-chain.&lt;/p&gt;

&lt;p&gt;A Solana account is like a file in a filesystem. It has an address, some metadata, and some contents. The address is a 32-byte public key. The metadata tells you who owns it, whether it is executable, and how much SOL it holds. The contents are just raw bytes.&lt;/p&gt;

&lt;p&gt;Every Solana account has five core fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11111111111111111111111111111111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rentEpoch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18446744073709551615&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lamports&lt;/code&gt; are the account's SOL balance. One SOL equals one billion lamports.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data&lt;/code&gt; is a byte array. This is where programs store custom state. A normal wallet account usually has zero bytes of data. A token account or NFT metadata account has structured data inside this field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;owner&lt;/code&gt; is the program that controls the account. This is one of the most important ideas in Solana. Only the owner program can modify an account's data or debit its lamports.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;executable&lt;/code&gt; tells you whether the account is a program. If it is &lt;code&gt;true&lt;/code&gt;, the account contains executable program code. If it is &lt;code&gt;false&lt;/code&gt;, it is a regular data account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rentEpoch&lt;/code&gt; is a legacy field. Today, accounts are generally rent-exempt, and this field is usually set to the maximum value.&lt;/p&gt;

&lt;p&gt;The ownership rule is Solana's security model in one sentence: only the owning program can change an account's data. For example, your wallet is owned by the System Program, so the System Program handles SOL transfers. Token accounts are owned by the Token Program, so only the Token Program can move tokens or update token balances.&lt;/p&gt;

&lt;p&gt;The part that surprises many Web2 developers is that Solana programs do not store their own state. Programs are stateless. A program account stores executable code, while separate data accounts store the actual application state.&lt;/p&gt;

&lt;p&gt;A useful analogy is a web server and a database. The program is like the backend server: it contains the logic. The accounts are like database records: they contain the data. When a transaction runs it tells the program which accounts it wants to read or write.&lt;/p&gt;

&lt;p&gt;This is different from Ethereum, where a smart contract usually owns both the code and the state together. On Solana code and state are separated. That separation is one reason Solana can process many transactions in parallel. If two transactions touch different accounts, they can often run at the same time.&lt;/p&gt;

&lt;p&gt;There is also the idea of rent exemption. Because account data takes space on-chain accounts must hold a minimum SOL balance based on their size. This keeps the account alive. For a basic account with no extra data, the rent-exempt amount is small roughly around 0.00089 SOL. You can calculate exact values with the Solana CLI or the &lt;code&gt;getMinimumBalanceForRentExemption&lt;/code&gt; RPC method.&lt;/p&gt;

&lt;p&gt;So the mental model is simple:&lt;br&gt;
Solana is a giant public key-value store. The key is an account address. The value is an account object with balance, owner, executable status, and raw data. Programs are accounts that contain code. Data accounts are accounts that contain state. The owner field decides who is allowed to modify what.&lt;/p&gt;

&lt;p&gt;Once this clicks, the rest of Solana starts to make more sense. Tokens, NFTs, DeFi protocols, staking, governance, and games all use the same basic building block: accounts.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Solana Transactions Cost Money Even When They Fail</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Sat, 09 May 2026 11:50:55 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/solana-transactions-cost-money-even-when-they-fail-2md6</link>
      <guid>https://dev.to/prasiddhnaik/solana-transactions-cost-money-even-when-they-fail-2md6</guid>
      <description>&lt;p&gt;Here's what I learned in 3 weeks: &lt;strong&gt;failed transactions on Solana cost real SOL&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Surprise
&lt;/h2&gt;

&lt;p&gt;As a backend dev, I'm used to failed requests being free. A 500 error? Retry it. It's no big deal.&lt;/p&gt;

&lt;p&gt;Solana is different. When you send a transaction, three things can happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CLI catches it&lt;/strong&gt; → Free (local balance check)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulation catches it&lt;/strong&gt; → Free (RPC preflight check)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain fails&lt;/strong&gt; → &lt;strong&gt;Costs 0.000005 SOL&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Watching It Happen
&lt;/h2&gt;

&lt;p&gt;I created a broke wallet (0 SOL) and tried to transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana transfer &lt;span class="nt"&gt;--from&lt;/span&gt; /tmp/broke-wallet.json &lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt; 1
&lt;span class="c"&gt;# Error: insufficient funds (caught locally, free)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I used a funded wallet but tried to send more than my balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// With skipPreflight = true (dangerous)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skipPreflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Signature: 5NWqdwSCmc3vz2PAwMmBQppL6uogPB7qkRWzUnCFtA9...
Status: Error processing Instruction 0: custom program error: 0x1
Fee: ◎0.000005
&lt;/span&gt;&lt;span class="gp"&gt;Balance: 8.39795 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;8.397945 SOL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction failed. The fee was still charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;In Web2, validation happens &lt;em&gt;before&lt;/em&gt; the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Solana, validation happens &lt;em&gt;inside&lt;/em&gt; the transaction. The blockchain doesn't check first—it tries to execute and fails if conditions aren't met. The attempt itself is a processed transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Always simulate before sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Default: skipPreflight = false (simulates first)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skipPreflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check balances client-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Insufficient funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;On Solana, there's no "try it and see." Every attempt that reaches the network costs money. Design your error handling accordingly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Day 19 of 100 Days of Solana. Full code: [&lt;a href="https://github.com/prasiddhnaik/solana-100-days" rel="noopener noreferrer"&gt;https://github.com/prasiddhnaik/solana-100-days&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #solana #web3 #blockchain #backend&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>programming</category>
      <category>web3</category>
    </item>
    <item>
      <title>BharatZero</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Fri, 08 May 2026 12:34:30 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/bharatzero-9ck</link>
      <guid>https://dev.to/prasiddhnaik/bharatzero-9ck</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;BharatZero is an India-focused legislative explorer that aggregates parliamentary data spanning 1947 to the present into a unified, searchable platform. The app solves a critical transparency problem: India's legislative information is scattered across dozens of government portals (Sansad, PRS Legislative Research, Parliament Digital Library, Data.gov.in), hidden in PDFs, and nearly impossible to navigate comprehensively.&lt;/p&gt;

&lt;p&gt;BharatZero brings it all together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4,708 bill records from three different official sources&lt;/li&gt;
&lt;li&gt;7,253 timeline events tracking parliamentary activity&lt;/li&gt;
&lt;li&gt;2,560 sitting days with chronological browsing&lt;/li&gt;
&lt;li&gt;Prime Minister term filtering—see what legislation passed during any PM's tenure&lt;/li&gt;
&lt;li&gt;Complete source attribution for data provenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform features eight sections: Overview, Houses (with Lok Sabha power charts), Timeline, Bills, Committees, Debates, Acts, and Sources. Every record shows exactly where it came from—critical for democratic transparency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://bharatzero.vercel.app/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;bharatzero.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/prasiddhnaik" rel="noopener noreferrer"&gt;
        prasiddhnaik
      &lt;/a&gt; / &lt;a href="https://github.com/prasiddhnaik/Bharat0" rel="noopener noreferrer"&gt;
        Bharat0
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;BharatZero&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;BharatZero is an India-focused legislative explorer for bills, Acts, Parliament timelines, House power, Prime Minister terms, and official source coverage. It is built as a Vite/React frontend with a small Node API server, Prisma, and PostgreSQL/Neon.&lt;/p&gt;
&lt;p&gt;The current app is no longer just a static prototype. It uses generated legislative datasets plus a Prisma-backed repository path for runtime data.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Current Coverage&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Generated local datasets currently include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;4,708&lt;/code&gt; Bill records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;7,268&lt;/code&gt; Bill action records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;7,253&lt;/code&gt; timeline events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2,560&lt;/code&gt; sitting days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;217&lt;/code&gt; Act records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;15&lt;/code&gt; Prime Minister profile records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;16&lt;/code&gt; Lok Sabha power snapshots&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Main source families:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sansad legislation data, as of &lt;code&gt;2026-04-25&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PRS historical bill data, &lt;code&gt;1992-2019&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Parliament Digital Library bill/proceeding data, &lt;code&gt;1947-2003&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Curated Prime Minister profile data from PM India&lt;/li&gt;
&lt;li&gt;Curated Lok Sabha power snapshots from ECI/IPU-style election summaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The PDL sync now covers older Prime Ministers, including Nehru, Shastri, Nanda, and Indira Gandhi-era windows where source records exist.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/prasiddhnaik/Bharat0" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I chose Gemma 4 31B Dense (gemma-4-31b-it) via the Gemini/OpenAI-compatible API for bill analysis generation. Legislative bills are dense, technical documents—often 50+ pages of legal language. Citizens, journalists, and researchers need accessible summaries, but manual summarization doesn't scale across 4,700+ bills.&lt;/p&gt;

&lt;p&gt;BharatZero uses Gemma 4 to generate plain-language summaries, economic impact analysis, key provisions extraction, and social implications analysis. The model handles the bilingual nature of Indian legislation seamlessly—bill titles and summaries work in both English and Hindi contexts without separate model tuning.&lt;/p&gt;

&lt;p&gt;Analysis is expensive, so BharatZero implements a persistent cache layer. Once a bill is analyzed, results are stored in PostgreSQL. Subsequent requests serve cached results instantly.&lt;/p&gt;

&lt;p&gt;Why Gemma 4 31B Dense was the right fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long context: Bills are lengthy documents; Gemma 4 handles full text input&lt;/li&gt;
&lt;li&gt;Structured output: Returns consistent JSON for parsing into the UI&lt;/li&gt;
&lt;li&gt;Factual accuracy: Critical for legislative analysis—hallucinations are dangerous&lt;/li&gt;
&lt;li&gt;Cost efficiency: More economical than alternatives for high-volume batch analysis
&amp;lt;!-- Don't forget to add a cover image if you want! --&amp;gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>solana notes - week 2</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Wed, 06 May 2026 14:26:30 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/solana-notes-week-2-40bi</link>
      <guid>https://dev.to/prasiddhnaik/solana-notes-week-2-40bi</guid>
      <description>&lt;h1&gt;
  
  
  100DaysOfSolana
&lt;/h1&gt;

&lt;p&gt;i've used normal databases for years, so i kept trying to force solana into that shape. tables, rows, queries. some of that helps, a lot of it breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  what surprised me
&lt;/h2&gt;

&lt;p&gt;reading a balance is stupidly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balanceInLamports&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the code is easy. the mental model is what's hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  the click moment
&lt;/h2&gt;

&lt;p&gt;ran &lt;code&gt;solana account $(solana address)&lt;/code&gt; and saw my wallet data. then realized: anyone can do this. no api keys, no auth. just the public key and you're reading someone's balance.&lt;/p&gt;

&lt;p&gt;that's uncomfortable in a good way. in web2 data is hidden unless i expose it. here it's visible unless i design around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  the sdk
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@solana/kit&lt;/code&gt; feels normal. not crusty like i expected. async/await, clean imports. the &lt;code&gt;devnet()&lt;/code&gt; and &lt;code&gt;mainnet()&lt;/code&gt; wrappers aren't just for show—they actually tag the network for type safety. someone thought about DX.&lt;/p&gt;

&lt;h2&gt;
  
  
  where the database comparison breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;normal thinking&lt;/th&gt;
&lt;th&gt;solana reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sql joins&lt;/td&gt;
&lt;td&gt;lol no. fetch signatures, then fetch details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private by default&lt;/td&gt;
&lt;td&gt;public by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i control who reads&lt;/td&gt;
&lt;td&gt;everyone reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alter table, migrations&lt;/td&gt;
&lt;td&gt;nope. design upfront, serialize to bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;i keep wanting &lt;code&gt;SELECT * FROM transactions WHERE user_id = 123&lt;/code&gt;. instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSignaturesForAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// then fetch each one i guess&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;more manual. that's the tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  devnet vs mainnet hit different
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- Devnet ---
Balance : 0.001159846 SOL

--- Mainnet ---
Balance : 0.069875097 SOL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;same address, same code, different world. staging vs production except anyone can view production.&lt;/p&gt;

&lt;h2&gt;
  
  
  still fuzzy on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDAs&lt;/strong&gt; - when do i actually use them vs regular accounts?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rent&lt;/strong&gt; - deposit not fee, but the exact economics? need to build something that creates/closes accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cross-program calls&lt;/strong&gt; - just words to me right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;writing transactions&lt;/strong&gt; - reading is safe. writing feels serious. nervous about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;send a simple transaction&lt;/li&gt;
&lt;li&gt;one PDA example end to end&lt;/li&gt;
&lt;li&gt;build something that actually writes, not just reads&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  where i'm at
&lt;/h2&gt;

&lt;p&gt;day 12. not an expert. but less intimidated.&lt;/p&gt;

&lt;p&gt;the code looks familiar while the assumptions underneath are totally different. public data by default. accounts instead of tables. pay for storage not queries. still rewiring my brain.&lt;/p&gt;

&lt;p&gt;if you're also confused by accounts, PDAs, or why everything is public: same. i think that's just part of it.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>devjournal</category>
      <category>javascript</category>
      <category>web3</category>
    </item>
    <item>
      <title>What Does "Identity" Actually Mean on a Blockchain?</title>
      <dc:creator>Prasiddh Naik</dc:creator>
      <pubDate>Sat, 25 Apr 2026 11:17:13 +0000</pubDate>
      <link>https://dev.to/prasiddhnaik/what-does-identity-actually-mean-on-a-blockchain-3l69</link>
      <guid>https://dev.to/prasiddhnaik/what-does-identity-actually-mean-on-a-blockchain-3l69</guid>
      <description>&lt;p&gt;If you've been writing code for a while, you've probably used SSH keys. You generate a key pair, drop the public key on a server, and from that point on, you prove who you are by holding the private key. No username, no password reset. You either have the key, or you don't&lt;/p&gt;

&lt;p&gt;Solana identity works on the same principle: you have the key, or you don't — except that, instead of a single server, it's the entire network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with Web2 identity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Web2, you have no power; you're just renting space. You don't control your GitHub or Google accounts; the company does. They give you access, but they can also take it away.&lt;/p&gt;

&lt;p&gt;Even when it feels like yours, it isn't. Your email address exists because Google or Microsoft allows it to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Solana does it differently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On Solana, your identity starts with a single cryptographic keypair — a public key and a private key. The public key is your address, something like &lt;code&gt;14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5&lt;/code&gt;. That's what the world sees. The private key never leaves your wallet, and it's the only thing that can authorize actions on your behalf.&lt;/p&gt;

&lt;p&gt;No company issues this key pair. No server stores it. You generate it yourself, you hold it, and it works across every application on the Solana network without asking anyone's permission. There's no sign-up form, no email verification, no "log in with Google." Your keypair is your identity, and it's yours in a way that a Web2 account never is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership that actually means something&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the part that takes a moment to sink in: on Solana, only the holder of the private key can sign transactions for an account. Not a company. Not an admin. Not a support team. There's no password reset flow, no "verify your identity" email, no override. If you hold the private key, you control the account. If you lose it, it can't be recovered.&lt;/p&gt;

&lt;p&gt;That sounds scary, and it is if you're careless. But it also means no one can lock you out of your own account. No platform ban, no policy change, no company going bankrupt takes your identity with it. The tradeoff is real responsibility — but so is real ownership.&lt;br&gt;
What this identity actually unlocks&lt;/p&gt;

&lt;p&gt;On-chain identity isn't just a replacement for a username. Because it's cryptographic, it becomes the foundation for everything else on the network. Token ownership is tied to your keypair. When you vote in a DAO governance proposal, your keypair is the vote. When you interact with a program, your keypair authorizes it. When you build a reputation across DeFi protocols or NFT platforms, it all traces back to the same address.&lt;/p&gt;

&lt;p&gt;In Web2, your reputation on GitHub doesn't carry over to LinkedIn, which doesn't carry over to your bank. Each platform starts from zero. On Solana, your keypair is consistent across the entire ecosystem. Any application can read your on-chain history without you having to re-verify anything.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>beginners</category>
      <category>web3</category>
      <category>solana</category>
    </item>
  </channel>
</rss>
