<?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: Siddhant Chavan</title>
    <description>The latest articles on DEV Community by Siddhant Chavan (@babydriver).</description>
    <link>https://dev.to/babydriver</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%2F3889440%2F08f445db-c2ec-4241-8e16-0e17a57e9b4f.png</url>
      <title>DEV Community: Siddhant Chavan</title>
      <link>https://dev.to/babydriver</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/babydriver"/>
    <language>en</language>
    <item>
      <title>The Bug I Never Wrote: What Testing Failure Taught Me About Solana</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:35:41 +0000</pubDate>
      <link>https://dev.to/babydriver/the-bug-i-never-wrote-what-testing-failure-taught-me-about-solana-28jo</link>
      <guid>https://dev.to/babydriver/the-bug-i-never-wrote-what-testing-failure-taught-me-about-solana-28jo</guid>
      <description>&lt;p&gt;&lt;em&gt;100 Days of Solana, Day 100&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I started
&lt;/h2&gt;

&lt;p&gt;I'd built REST APIs for years but had never touched a blockchain, or written a line of Rust. The curiosity how blockchain works, started my curiosity.&lt;/p&gt;

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

&lt;p&gt;I came in with a Web2 instinct: tests exist to prove your code does what it's supposed to do. Write the function, write a test that calls it, watch it pass, move on. A "failing test" was something you fixed, not something you shipped on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed my understanding
&lt;/h2&gt;

&lt;p&gt;The moment this cracked open was building the capstone: a small Anchor program called &lt;code&gt;proof-of-ship&lt;/code&gt; that lets a wallet permanently record, on chain, that it shipped something. The rule is simple — one ship record per wallet, forever. The rule lives entirely in the account's seeds:&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="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"ship"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="n"&gt;bump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each wallet's record lives at one deterministic address. Try to create a second one, and &lt;code&gt;init&lt;/code&gt; refuses, because an account already exists there.&lt;/p&gt;

&lt;p&gt;I wrote two tests. The first proved the happy path: call &lt;code&gt;ship()&lt;/code&gt;, fetch the record, confirm the name and builder match.&lt;/p&gt;

&lt;p&gt;The second test is the one that changed how I think about testing:&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;only lets each wallet ship once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&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;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Second try&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This should never land&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second ship should have been rejected&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;p&gt;This test isn't checking for a bug. It's checking that a &lt;em&gt;rule&lt;/em&gt; holds. There's no function in my program called &lt;code&gt;preventDuplicateShip()&lt;/code&gt;. There's no &lt;code&gt;if&lt;/code&gt; statement rejecting the second attempt. The rule "one ship per wallet" isn't enforced by logic I wrote — it's enforced by the Solana runtime itself, because the PDA's address already has data in it. My job wasn't to write the rejection. My job was to prove the rejection actually happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I understand now
&lt;/h2&gt;

&lt;p&gt;On Web2 systems I controlled the whole stack, so "does it work" mostly meant "does the happy path return the right JSON." On Solana, the program shares an environment with every other program and every other wallet, and the account model itself becomes part of your security boundary. A constraint you wrote, a &lt;code&gt;require!&lt;/code&gt; check, an &lt;code&gt;init&lt;/code&gt; on a PDA with specific seeds — none of these mean anything until you've watched them reject bad input in a test, on purpose, and watched that rejection come from the runtime rather than from your own code's &lt;code&gt;if&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;The happy-path test proves your program &lt;em&gt;can&lt;/em&gt; do the right thing. The failure-path test proves it &lt;em&gt;can't&lt;/em&gt; do the wrong thing — and on a public, permissionless network, that second guarantee is the one that actually matters when a stranger's transaction lands on your program at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it appeared in my capstone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;proof-of-ship&lt;/code&gt; is intentionally small: one instruction, one PDA account, one constraint on input length, one happy-path test, one failure-path test, deployed to devnet. [Add your program ID / Explorer link here.] [Add your GitHub repo link here.]&lt;/p&gt;

&lt;p&gt;I connected it to [the Arc 13 frontend / your wallet-adapter React app], so any wallet can connect, ship once, and see its on-chain message rendered back — which meant testing the "only once" rule wasn't just an assertion in a test file, it was something I could watch fail in the browser too, with a real wallet, a real rejected transaction, and a real error toast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell another developer
&lt;/h2&gt;

&lt;p&gt;Write the test for what your program refuses to do before you're confident you need it. If you can't write a failing-path test for a rule, you probably haven't actually built the rule — you've just described it in a comment. [Add a specific "I wish I'd known this on Day X" note here.]&lt;/p&gt;

&lt;p&gt;*Tags: #solana, #webdev, #100daysofsolana *&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>rust</category>
      <category>react</category>
    </item>
    <item>
      <title>The Missing Manual for My On-Chain Agent</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:55:47 +0000</pubDate>
      <link>https://dev.to/babydriver/the-missing-manual-for-my-on-chain-agent-4mc5</link>
      <guid>https://dev.to/babydriver/the-missing-manual-for-my-on-chain-agent-4mc5</guid>
      <description>&lt;p&gt;Could a teammate rebuild my agent stack using nothing but what's written down here? A few days ago the honest answer was no. Over the course of this build I put together something genuinely sophisticated: an agent loop that reads balances, a guarded transfer tool, an MCP server that exposes those tools to any client, a deny-by-default policy engine, and — most recently — a fully autonomous run where the whole system pursued a goal on its own, without me typing a recipient, an amount, or a single command.&lt;/p&gt;

&lt;p&gt;Every design decision that makes it work has, until now, lived in one place: my head. This post gets it onto the page.&lt;/p&gt;

&lt;p&gt;A REST API behaves the same way every time you call it, so its code is nearly self-documenting. My agent doesn't — the same goal can produce a different tool-call path on every run, because a language model is doing the reasoning instead of an &lt;code&gt;if&lt;/code&gt; statement. That means the most important facts about this system, the invariants that hold no matter what the model decides, are exactly the facts a single run log will never reveal on its own. They have to be written down on purpose. So here's the runbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  System inventory
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Lives in&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-workflow.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calls the model in a loop, hands it tool results, keeps going until it stops asking for tools or hits the turn limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;get_balance&lt;/code&gt; tool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-workflow.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads the live SOL balance of any devnet address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer_sol&lt;/code&gt; tool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-workflow.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Moves lamports from the operating wallet to a recipient, but only after the policy layer signs off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wraps read/write tools in the Model Context Protocol so any MCP-compatible client (Claude Code, Claude Desktop, a support bot) can reuse them without copying code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy engine&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;checkPolicy()&lt;/code&gt; / &lt;code&gt;policy.mjs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Deny-by-default gate that every proposed transfer must clear before anything gets signed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the skeleton. Everything below fills it in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;p&gt;Here's how a plain-English goal turns into a confirmed devnet transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your goal, in plain English
        |
        v
+---------------------+
|  agent loop (LLM)   |  decides which tool to call next
+---------------------+
        |
        v  tool call, e.g. transfer_sol
+---------------------+
|  MCP server         |  exposes your tools to any client
+---------------------+
        |
        v
+---------------------+
|  policy engine      |  deny by default; every spend checked
+---------------------+
        |  allowed
        v
+---------------------+
|  Solana devnet      |  transaction submitted and confirmed
+---------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model never touches the network, the keypair, or the RPC connection directly. It only ever sends a structured tool call. Everything below the "MCP server" box is code the model cannot talk its way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_balance&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; account address (string)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; address and balance in lamports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects:&lt;/strong&gt; none&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded by policy:&lt;/strong&gt; no — read-only, so there's nothing to guard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;transfer_sol&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; recipient address (string), amount in lamports (number)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; a confirmed transaction signature, or a policy denial with a reason&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects:&lt;/strong&gt; spends SOL from the agent's operating wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded by policy:&lt;/strong&gt; yes — every call passes through &lt;code&gt;checkPolicy()&lt;/code&gt; before a transaction is ever built or signed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool's description tells the model the rule exists ("transfers above the cap are rejected"), but that description is a courtesy, not a security boundary. It could be deleted entirely and the enforcement would be unchanged, because the enforcement lives in code the model never sees or edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy layer
&lt;/h2&gt;

&lt;p&gt;This is the part of the system that actually makes it safe to let a model hold a signing key, so it's worth writing down rule by rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deny by default.&lt;/strong&gt; &lt;code&gt;checkPolicy(to, lamports)&lt;/code&gt; starts from "no" and only returns &lt;code&gt;allowed: true&lt;/code&gt; once every rule has failed to object. There is no code path where the absence of a rule accidentally means "yes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1 — recipient allowlist.&lt;/strong&gt; A transfer is only considered if the recipient is in &lt;code&gt;POLICY.allowedRecipients&lt;/code&gt;. Anything else is denied before amount is even checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2 — per-transfer cap.&lt;/strong&gt; &lt;code&gt;maxLamportsPerTransfer&lt;/code&gt; bounds any single transfer, currently 0.05 SOL (50,000,000 lamports) in the run below. This slows an agent down; it does not, by itself, bound total spend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3 — per-run cap.&lt;/strong&gt; &lt;code&gt;maxLamportsPerRun&lt;/code&gt; bounds cumulative spend across every transfer in one session, currently 0.5 SOL (500,000,000 lamports). This is the rule that actually stops an agent from draining a wallet by making many small, individually-compliant transfers. A per-transfer cap without a per-run cap is a speed bump, not a wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The invariant.&lt;/strong&gt; The prompt can change. The model can change. The tool-call sequence and how many times it retries can change. But no transaction moves funds without passing this layer, and the layer runs in my code, not in the conversation.&lt;/p&gt;

&lt;p&gt;A denial is returned to the model as an ordinary tool result — &lt;code&gt;{ status: "denied", reason: "..." }&lt;/code&gt; — not as an error or a crash. That matters: it means a rejected transfer is something the agent can reason about and explain honestly, rather than something that takes the whole run down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotated run: the impossible-goal trial
&lt;/h2&gt;

&lt;p&gt;This is a real log from an autonomous run. The goal handed to the agent was to bring a savings wallet up to 5 SOL — a target that, combined with the policy caps, cannot be reached in one run. I saved this one specifically because a denial log is the most convincing evidence a policy actually works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 1 — reconnaissance.&lt;/strong&gt; The agent calls &lt;code&gt;get_balance&lt;/code&gt; on both the savings wallet (400,000,000 lamports) and the operating wallet (~5.55 SOL), without being told to check either one first. &lt;em&gt;Comment: it derived the shortfall itself — nobody told it the current balance or the gap to close.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 2 — the ambitious attempt.&lt;/strong&gt; It calls &lt;code&gt;transfer_sol&lt;/code&gt; for 4,600,000,000 lamports, the full amount needed to close the gap in one shot. Policy denies it: &lt;code&gt;4600000000 lamports exceeds the per-transfer cap of 50000000&lt;/code&gt;. &lt;em&gt;Comment: this is the per-transfer cap doing its job — the model's math was correct, the code's answer was still no.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 3 — the pivot.&lt;/strong&gt; The agent retries at exactly 50,000,000 lamports, the maximum a single transfer allows. Policy allows it; it confirms on-chain. &lt;em&gt;Comment: instead of giving up, it inferred the cap from the denial reason and adjusted its plan — this is the "split it into small transfers" behavior the policy makes possible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turns 4–5 — ten transfers, then the wall.&lt;/strong&gt; The agent repeats 50,000,000-lamport transfers. The first ten confirm, bringing cumulative spend to exactly 500,000,000 lamports. The eleventh attempt, identical in shape to the ones before it, is denied: &lt;code&gt;this transfer would push total spend past the per-run cap of 500000000 lamports&lt;/code&gt;. It tries the same call several more times before giving up. &lt;em&gt;Comment: the per-transfer cap never stopped it — only the per-run cap did, and only once the running total actually crossed the line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 6 — verification.&lt;/strong&gt; Before saying anything, the agent re-reads both balances: savings now at 900,000,000 lamports, operating down to ~5.05 SOL. &lt;em&gt;Comment: it checked the world instead of trusting its own memory of what it had done.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 7 — final report.&lt;/strong&gt; The agent's own summary: it explains the per-transfer denial, the pivot to smaller transfers, the ten confirmations, the per-run denial, and states plainly that the 5 SOL target was not reached because of policy limits — not because of an error.&lt;/p&gt;

&lt;p&gt;Nothing bad happened to the funds at any point. Not because the model behaved cautiously (it tried the maximum possible transfer first, then kept retrying denied calls several times) but because the ceiling was enforced somewhere the model's persistence couldn't reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A per-transfer cap alone is not a spending limit — it's a rate limit.&lt;/strong&gt; Watching the agent methodically split 4.6 SOL into 50,000,000-lamport pieces made it obvious that the real backstop was the per-run cap. This is the same distinction a Web2 API has between a request-size limit and a quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-determinism showed up as strategy, not randomness.&lt;/strong&gt; The agent didn't fail chaotically when the big transfer was denied — it read the denial reason and changed approach. That's more capability than I expected from a single-sentence goal, and it's exactly why the enforcement can't live in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrying against a known-denied policy wastes turns.&lt;/strong&gt; The agent kept re-attempting the same 50,000,000-lamport transfer several times after the per-run cap was hit, instead of recognizing the pattern and stopping sooner. Before I'd trust this near anything beyond devnet, I'd want it to reason about &lt;em&gt;why&lt;/em&gt; a denial repeated, not just retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification-before-reporting was not something I explicitly asked for.&lt;/strong&gt; The agent re-checked both balances before writing its summary. That's good behavior, and I'd want to understand whether it's reliable or incidental before depending on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open question:&lt;/strong&gt; what should happen when an agent hits a hard, un-liftable policy wall mid-goal — should it stop immediately, or is there value in letting it exhaust a bounded number of retries the way this run did? I don't have a strong answer yet, and that's the kind of edge this documentation exists to surface.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Follow along with the rest of the build under #100DaysOfSolana.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>javascriptlibraries</category>
      <category>web3</category>
      <category>agents</category>
    </item>
    <item>
      <title>The checklist I wish I'd had before my first mainnet deploy</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:24:41 +0000</pubDate>
      <link>https://dev.to/babydriver/the-checklist-i-wish-id-had-before-my-first-mainnet-deploy-2cfo</link>
      <guid>https://dev.to/babydriver/the-checklist-i-wish-id-had-before-my-first-mainnet-deploy-2cfo</guid>
      <description>&lt;p&gt;Pilots run a checklist before every takeoff. Surgeons run one before every incision. Not because they forgot how to fly or operate — because the cost of skipping a single step under pressure is too high to trust to memory. Shipping a Solana program to mainnet-beta belongs in that same category: irreversible actions, real value on the line, and a dozen small things that each seem obvious right up until the moment you forget one.&lt;/p&gt;

&lt;p&gt;I spent the last several days building every piece of a real launch — promoting a program from devnet to mainnet-beta, taking deliberate control of its upgrade authority, publishing an IDL and generating a typed client, wiring a React frontend through the Wallet Standard, and teaching that frontend to fail politely instead of loudly. All of that knowledge is fresh in my head right now, which is exactly the problem: in three weeks, when I'm about to ship the next program, it won't be. "I'll remember" is not a launch strategy.&lt;/p&gt;

&lt;p&gt;So here's the checklist, written in the order the steps actually happen, with the gotchas I personally hit called out along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Pre-flight, on devnet
&lt;/h2&gt;

&lt;p&gt;These are the steps that catch problems while they're still cheap to fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Every test passes against your final program build.&lt;/strong&gt; Not an earlier build, not "close enough" — the exact bytecode you're about to ship.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Run the program end-to-end on devnet exactly as it will run on mainnet.&lt;/strong&gt; If there's a step you're planning to skip "just this once" on the real deploy, do it on devnet first and see what breaks.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Produce a verifiable build with &lt;code&gt;anchor build --verifiable&lt;/code&gt;.&lt;/strong&gt; This is what lets anyone later match the bytecode sitting on-chain back to your actual source code. Skip it and your program is just a trusted black box forever.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Confirm your deploy wallet holds enough SOL&lt;/strong&gt; to cover the rent-exempt minimum for the program account plus fees. You can preview the rent number with &lt;code&gt;solana rent $(wc -c &amp;lt; target/deploy/vault.so)&lt;/code&gt; before you commit to anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; once you have a verifiable build, do not overwrite it with a plain &lt;code&gt;anchor build&lt;/code&gt; or &lt;code&gt;cargo build-sbf&lt;/code&gt; afterward. Either of those can produce a different hash and quietly break verification later, and you won't find out until someone tries to verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: The deploy itself
&lt;/h2&gt;

&lt;p&gt;This is the irreversible phase, so this is where the checklist earns its keep.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Switch your CLI to mainnet-beta and confirm it&lt;/strong&gt; with &lt;code&gt;solana config get&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; running anything else. A stray command pointed at the wrong cluster is the easiest mistake to make and one of the more annoying to notice.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Deploy through a dedicated RPC endpoint, not the public one.&lt;/strong&gt; A deploy is dozens of write transactions in a row, and the free public RPC is rate-limited enough that writes can expire before they land.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Attach a priority fee if the network is busy.&lt;/strong&gt; A priority fee is a small extra payment per unit of compute that nudges the current block leader to include your transaction ahead of others competing for the same block. Add one with &lt;code&gt;--with-compute-unit-price&lt;/code&gt;, and route through your endpoint with &lt;code&gt;--use-rpc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Know the recovery path before you need it.&lt;/strong&gt; A deploy is not atomic. If it's interrupted, you can be left with a buffer account holding your SOL. &lt;code&gt;solana program show --buffers&lt;/code&gt; lists anything stranded, and you can resume into it or close it to recover the rent rather than starting over and leaking SOL on every failed attempt. The official deployment docs walk through this recovery flow — read it once before you deploy, it's cheaper than reading it for the first time mid-failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 3: Authority and verification, right after deploy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Confirm the program is live&lt;/strong&gt; and inspect it with &lt;code&gt;solana program show &amp;lt;PROGRAM_ID&amp;gt;&lt;/code&gt; to read back its upgrade authority and confirm it's what you intended.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Decide, deliberately, who holds upgrade authority.&lt;/strong&gt; Whoever signed the deploy holds it by default, and that's a lot of quiet power to leave unexamined. The real options are: a single keypair you control, a Squads multisig where upgrades require multiple approvals, or &lt;code&gt;--final&lt;/code&gt; to make the program permanently immutable. Each is legitimate; write down which one you chose and why, because "I meant to move it later" is how authority ends up parked on a laptop indefinitely.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Publish your IDL&lt;/strong&gt; so the program's interface travels with the program itself, retrievable by anyone with just the program ID.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Regenerate your typed client&lt;/strong&gt; from the published IDL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; skipping the IDL publish step will cause build verification to fail later, even if the deploy itself was perfectly fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: Frontend and going live
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Point your React frontend at the mainnet program ID.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Confirm the Wallet Standard connection surfaces real wallets against mainnet&lt;/strong&gt;, not a leftover devnet configuration. A mismatch between where your app reads and where your wallet is set is the single most common reason a balance shows up as zero.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Re-confirm every failure mode you classified&lt;/strong&gt; — rejected approval, insufficient funds, expired blockhash — still produces a calm, legible message now that the stakes are real and not devnet-free.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Announce the launch.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Write down where users should report problems&lt;/strong&gt;, before they need to find you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prove it
&lt;/h2&gt;

&lt;p&gt;Before publishing this checklist anywhere, I ran the one command that confirms the single most important fact in the whole thing — that the program is live and its upgrade authority is exactly what I claim it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana program show &amp;lt;YOUR_PROGRAM_ID&amp;gt; &lt;span class="nt"&gt;--url&lt;/span&gt; mainnet-beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output is the receipt. If you're writing your own version of this checklist, paste yours in too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that surprised me most
&lt;/h2&gt;

&lt;p&gt;Going in, I expected the deploy command itself to be the scary part. It wasn't. The deploy is just &lt;code&gt;anchor build&lt;/code&gt; and a command you've run a dozen times on devnet, aimed somewhere more permanent. What actually required care was everything sitting &lt;em&gt;around&lt;/em&gt; the deploy — confirming the cluster before touching anything irreversible, treating upgrade authority as a decision instead of a default, and remembering that a stalled deploy isn't a failure to restart from scratch but SOL sitting in a buffer waiting to be recovered. None of that is complicated. All of it is exactly the kind of thing that's obvious in the moment and gone from memory three weeks later, which is the whole reason this checklist exists.&lt;/p&gt;

&lt;p&gt;The best engineering teams don't treat launches as feats of memory or heroics — they treat them as procedures. A multisig on the upgrade authority, a verifiable build, a confirmed cluster before an irreversible command: none of that is paranoia. It's the small discipline that lets a team ship to production calmly and repeatedly, instead of white-knuckling it every time. This is my version of that discipline, written down so I don't have to relearn it, and so the next person walking this path for the first time has something more useful than the reference docs: a checklist written by someone who still remembers exactly what was confusing.&lt;/p&gt;

</description>
      <category>react</category>
      <category>web3</category>
      <category>100daysofsolana</category>
      <category>wallet</category>
    </item>
    <item>
      <title>My Solana Program Security Checklist</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Sat, 25 Jul 2026 19:55:08 +0000</pubDate>
      <link>https://dev.to/babydriver/my-solana-program-security-checklist-3kgf</link>
      <guid>https://dev.to/babydriver/my-solana-program-security-checklist-3kgf</guid>
      <description>&lt;p&gt;My Solana Program Security Checklist&lt;/p&gt;

&lt;p&gt;This is for anyone shipping an Anchor program to mainnet — especially if you're coming from a web2 background and looking for the Solana equivalent of a PR template. Run it top to bottom before every deploy. Every item here came out of a bug I reproduced myself: I wrote adversarial tests that tried to rob my own vault, fuzzed the arithmetic until proptest handed me a counterexample, and rebuilt the exact missing-owner-check bug that drained Wormhole and Cashio. Nothing below is theoretical.&lt;/p&gt;

&lt;p&gt;A quick note on how to read this: each item should be answerable with a plain yes or no by someone looking at the code, not a vague reminder to "be careful." If you can't verify an item just by reading the account struct and handler, rewrite it until you can.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Account validation
Every deserialized account has its owner verified. A typed Account&amp;lt;'info, T&amp;gt; does this for you automatically; a raw AccountInfo or UncheckedAccount does not, and manual deserialization off one of those is exactly how Wormhole lost roughly $326M — the program trusted the instructions sysvar without confirming the account's real address, and a forged account deserialized cleanly because nobody checked who owned it.
Every #[account] type is distinguished by its 8-byte discriminator, so one account type can never be passed where another is expected.
Any remaining_accounts are individually validated before use — they arrive with zero automatic checks.
Every /// CHECK: comment in the codebase is treated as a flag, not a formality. Run grep -rn "UncheckedAccount|AccountInfo|/// CHECK" programs/*/src and account for every hit.&lt;/li&gt;
&lt;li&gt;Authority and signer checks
Every privileged instruction confirms the expected signer with Signer&amp;lt;'info&amp;gt;, not just a Pubkey comparison. Comparing a stored public key to a passed-in account proves someone knew a public key — public keys are public — it does not prove the holder of the matching private key approved the transaction.
Every account named authority, owner, admin, or creator is either typed Signer&amp;lt;'info&amp;gt; directly, or bound to one via has_one against an account that is itself a Signer.
PDAs that gate access are re-derived and checked with seeds = [...] , bump = state.bump, not just accepted as whatever address the caller supplied. A wrong signer or a look-alike decoy account should fail here with ConstraintSeeds.&lt;/li&gt;
&lt;li&gt;Arithmetic safety
Every balance or supply change uses checked_add / checked_sub / checked_mul, never raw +, -, *, on any value influenced by a caller.
No silent truncating cast (e.g. u64 to u32) sits anywhere in a balance or supply path.
The property actually holds under generated input, not just the examples you thought of by hand. I pulled the arithmetic into a pure function and ran a proptest property against it — "a deposit either grows the balance or refuses honestly, never a silent wraparound" — over hundreds of generated u64 pairs, not just the numbers I happened to type. Rounding and precision bugs are real and expensive even without a headline heist: Neodyme's 2021 disclosure on the SPL token-lending program put roughly $2.6B of deposited value at risk from a rounding-direction bug, and Certora's audit of Kamino caught a fixed-point precision issue that could have let a user redeem more collateral than they deposited. Both were caught by exactly this kind of testing before anyone lost money.
overflow-checks = true is set under [profile.release] in the workspace-root Cargo.toml (Anchor sets this by default on anchor init, but verify it if you assembled the workspace by hand).&lt;/li&gt;
&lt;li&gt;Cross-program invocation (CPI) safety
Every CPI verifies the target program ID is the one you actually intend, not whatever account the caller happened to supply.
Any account whose data is read again after a CPI is reloaded first, since a CPI can mutate account state your local copy no longer reflects.
Token accounts passed into an instruction are constrained with token::mint = ... and token::authority = ... rather than trusted at face value — this is the exact substitution class the Sealevel attacks catalog documents, and it's the same shape of bug that let Cashio mint roughly $52M in fake tokens against unvalidated collateral.&lt;/li&gt;
&lt;li&gt;Account lifecycle
Closed accounts are emptied and marked so they can't be revived or reused within the same transaction.
No instruction allows reinitializing an already-initialized account.
init_if_needed is used deliberately, not defensively — know exactly which instructions are allowed to create state and which should fail if the account doesn't already exist.&lt;/li&gt;
&lt;li&gt;Testing and pre-deploy hygiene
The test suite includes adversarial tests, not just happy-path tests. For every constraint you rely on, there's a test that builds the exact malicious transaction that constraint is supposed to reject, and asserts the specific error code — not just "the transaction failed." A transaction can fail for boring reasons unrelated to your security logic, and a test that only checks for failure can hide a broken defense behind an unrelated error.
At least one property-based test or fuzz run has been pointed at the program's core invariants (balances never go negative, a close always returns exactly its rent, two operations in sequence equal the combined operation done once). Tools like Trident drive full instruction sequences against live account state and will hand you the exact failing input if an invariant breaks.
Dependencies are current and free of known advisories.
The exploit you're most worried about has actually been reproduced against your own program, end to end, and the fix has been confirmed to turn a passing (exploited) test into a failing (rejected) one. Don't just read about Wormhole and Cashio — rebuild the smallest honest version of the bug, watch it succeed, apply the owner-check fix, and watch the same transaction get rejected. It's a different kind of confidence than reading a post-mortem.
What Anchor gives you for free — and what it doesn't&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worth stating plainly, because it's the single most common source of false confidence: typing Account&amp;lt;'info, T&amp;gt; and Signer&amp;lt;'info&amp;gt; automatically gets you an owner check, a discriminator check, and a signer check. That's real and it's most of the mechanical work. What Anchor cannot do is reason about your business logic — it doesn't know if the right authority is attached to the right vault unless you write the has_one, it doesn't know if a CPI target is the program you meant, and it has no opinion on whether your arithmetic is safe. The moment a program reaches for UncheckedAccount to make something compile, all of that goes away and the developer has personally taken on the promise Anchor would otherwise have kept.&lt;/p&gt;

&lt;p&gt;A closing note&lt;/p&gt;

&lt;p&gt;This list is not exhaustive — it's a representative core built from the bugs in front of me, not every bug that exists. If you run it against your own program and find a gap it doesn't cover, that's the system working as intended: a security checklist is a living document, and the next person who reads it should leave it better than they found it. Corrections and additions welcome.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>security</category>
      <category>rust</category>
      <category>web3</category>
    </item>
    <item>
      <title>A CPI Is a Function Call With a Guest List</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Fri, 24 Jul 2026 13:59:42 +0000</pubDate>
      <link>https://dev.to/babydriver/a-cpi-is-a-function-call-with-a-guest-list-9fg</link>
      <guid>https://dev.to/babydriver/a-cpi-is-a-function-call-with-a-guest-list-9fg</guid>
      <description>&lt;p&gt;The thing I wish someone had told me on Day 71&lt;/p&gt;

&lt;p&gt;Five days ago, CPI was three letters I kept skimming past in the Anchor docs. I nodded along, assumed it meant "one program calls another," and moved on before the details could embarrass me. That worked right up until I had to write one myself, stared at CpiContext::new, and realized I couldn't explain which argument was which or why the compiler kept telling me it wanted a Pubkey where I'd handed it an AccountInfo.&lt;/p&gt;

&lt;p&gt;Here's the sentence I wish past-me had pinned to the top of a notebook: a CPI is a function call with a guest list. Your program doesn't just say "run this instruction on my behalf" — it has to name the exact program it's calling, hand over every account that instruction touches, and prove that whoever is authorizing the change is actually allowed to. Miss any one of those and the runtime stops you cold, every time, no exceptions.&lt;/p&gt;

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

&lt;p&gt;Every CPI you'll ever write, no matter which program is on the other end, comes down to the same three pieces:&lt;/p&gt;

&lt;p&gt;The program being called — identified by its program ID. This is who you're calling, not what you're asking for.&lt;br&gt;
The accounts that program needs — your program doesn't own these accounts, it just passes them through. The callee decides what it needs; you supply exactly that, no more, no less.&lt;br&gt;
The signer authority — either a real wallet that already signed your outer transaction (its signature flows down into the CPI automatically, no extra code required), or a PDA that has no private key at all. For a PDA, your program "signs" by handing the runtime the exact seeds used to derive that address. The runtime re-derives the address from those seeds; if it matches, the PDA is treated as a signer for the length of that one call, and never again after.&lt;/p&gt;

&lt;p&gt;Once those three pieces are separate in your head, every CPI reads the same way: bundle a program ID and an accounts struct into a CpiContext, then hand that context to a helper function that fires the actual invocation.&lt;/p&gt;

&lt;p&gt;One CPI, trimmed down&lt;/p&gt;

&lt;p&gt;The cleanest example I've got is a vault that signs for itself. A user deposits SOL into a PDA, and later the program — not the user — authorizes the withdrawal, because the vault has no keypair of its own:&lt;/p&gt;

&lt;p&gt;rust&lt;br&gt;
pub fn withdraw(ctx: Context, amount: u64) -&amp;gt; Result&amp;lt;()&amp;gt; {&lt;br&gt;
    let user_key = ctx.accounts.user.key();&lt;br&gt;
    let bump = ctx.bumps.vault;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The recipe for the vault: literal seed, owner key, canonical bump.
let signer_seeds: &amp;amp;[&amp;amp;[&amp;amp;[u8]]] = &amp;amp;[&amp;amp;[b"vault", user_key.as_ref(), &amp;amp;[bump]]];

// The vault has no private key, so the program signs for it.
let cpi_ctx = CpiContext::new(
    ctx.accounts.system_program.key(),
    Transfer {
        from: ctx.accounts.vault.to_account_info(),
        to: ctx.accounts.user.to_account_info(),
    },
)
.with_signer(signer_seeds);

transfer(cpi_ctx, amount)?;
Ok(())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;h1&gt;
  
  
  [derive(Accounts)]
&lt;/h1&gt;

&lt;p&gt;pub struct Withdraw&amp;lt;'info&amp;gt; {&lt;br&gt;
    #[account(mut)]&lt;br&gt;
    pub user: Signer&amp;lt;'info&amp;gt;,&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[account(
    mut,
    seeds = [b"vault", user.key().as_ref()],
    bump,
)]
pub vault: SystemAccount&amp;lt;'info&amp;gt;,

pub system_program: Program&amp;lt;'info, System&amp;gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Map that back to the three pieces: the System Program is who's being called, Transfer { from, to } is the guest list it expects, and signer_seeds is the proof of authority — a recipe instead of a signature. The seeds on the #[account(...)] constraint and the signer_seeds slice have to describe the exact same PDA, because the runtime re-derives the address from both and checks they land on the same account.&lt;/p&gt;

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

&lt;p&gt;Partway through deliberately breaking this same withdraw function, I fed it the wrong bump and got this back:&lt;/p&gt;

&lt;p&gt;AnchorError caused by account: tally. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.&lt;br&gt;
Program log: Left: 4Ea3EzynSnP5B7G5ad3tQwz2Go5u9LGy6ywMjyujy7Vn&lt;br&gt;
Program log: Right: 2ALUabpuiAzAdbm8D5h3kBVMiuTTzC1iN8NyXmLucx5T&lt;/p&gt;

&lt;p&gt;ConstraintSeeds means Anchor re-derived the PDA from the seeds you declared on the account struct and got a pubkey that doesn't match what the client actually passed in — the Left:/Right: pair in the logs is literally "expected vs. got." The fix was boring once I understood it: my seeds recipe on the Rust side and the derivation on the client side have to be byte-for-byte identical, because the runtime doesn't trust intent, only the math.&lt;/p&gt;

&lt;p&gt;That error is easy to misread as "something is broken in my program logic." It isn't. It's Anchor telling you, with total precision, that the account you handed it isn't the account its own constraints say it should be — and it catches that before your instruction body ever runs.&lt;/p&gt;

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

&lt;p&gt;If you want the full picture, the Anchor CPI documentation covers CpiContext and invoke_signed in more depth than a 900-word post can, and the Token-2022 docs are worth having open the first time you CPI into a token program instead of the System Program.&lt;/p&gt;

&lt;p&gt;This post draws from Day 73 through Day 75 of #100DaysOfSolana — moving SOL with a CPI, minting Token-2022 tokens from inside a program, signing for a PDA-owned vault, composing two of my own programs, and deliberately breaking all of it to see what the failures actually look like.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>What I learned about PDAs in a week of building on Solana #solana #rust #anchor #webdev</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:01:48 +0000</pubDate>
      <link>https://dev.to/babydriver/what-i-learned-about-pdas-in-a-week-of-building-on-solana-solana-rust-anchor-webdev-3cmn</link>
      <guid>https://dev.to/babydriver/what-i-learned-about-pdas-in-a-week-of-building-on-solana-solana-rust-anchor-webdev-3cmn</guid>
      <description>&lt;p&gt;On Solana, programs are stateless. If your program needs to remember something per user, per game, per config, it needs a deterministic address it can find again later without storing that address anywhere. PDAs are that address.&lt;/p&gt;

&lt;p&gt;I spent this week building a small Anchor counter program — nothing exotic, just enough surface area to run into the real questions: why does find_program_address need a program ID, why is the bump part of the seeds and not a separate thing bolted on afterward, and what actually happens when an account gets "closed." This post is me writing down the answers before they fade, aimed at whoever is where I was a week ago.&lt;/p&gt;

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

&lt;p&gt;The Web2 shorthand I kept reaching for was: a PDA is like a database primary key you can compute from the row's logical identity. If you know the user ID, you know the row. You never store the key anywhere separate from the thing that produces it.&lt;/p&gt;

&lt;p&gt;That analogy gets you most of the way, with the database being the entire Solana account model, the key derivation being a hash, and the program ID baked into the hash so that only your program can ever sign for the result.&lt;/p&gt;

&lt;p&gt;Where it breaks: a primary key always points at a row that exists. A PDA is derived on demand and may or may not have an account sitting at that address yet. Deriving the address and creating the account are two separate steps, and the derivation works identically whether or not anything has been initialized there. A PDA is closer to "the address this row would live at, if it exists" than to a key you look up in a table.&lt;/p&gt;

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

&lt;p&gt;Here's the canonical pattern from my own program, from the InitCounter accounts struct:&lt;/p&gt;

&lt;p&gt;rust&lt;/p&gt;

&lt;h1&gt;
  
  
  [account(
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init,
payer = user,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", user.key().as_ref()],
bump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;)]&lt;br&gt;
pub counter: Account&amp;lt;'info, Counter&amp;gt;,&lt;/p&gt;

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

&lt;p&gt;seeds = [b"counter", user.key().as_ref()] — a static label, "counter", plus a dynamic component, the signer's public key. Both get hashed together with the program ID.&lt;br&gt;
The program ID isn't in the seeds array, but it's an input to the derivation anyway — find_program_address takes it as a separate argument. Same seeds, different program, different address. This is what makes the address belong to your program specifically.&lt;br&gt;
bump — Solana public keys are points on the ed25519 curve. A PDA is deliberately not one of those points, because that's what guarantees no private key exists for it. The derivation function starts at bump value 255 and counts down, hashing seeds + program ID + bump each time, until it lands on a result that is off the curve. The bump is not a separate mechanism from the seeds — it's the extra byte you have to smuggle into the hash input to force the output off-curve. Whatever value first produces an off-curve address is the canonical bump, and that's the one find_program_address (and the bump keyword in Anchor) hands back.&lt;br&gt;
init / payer / space — this is the "create the row if it doesn't exist yet" step. payer says who funds the account, space says how many bytes to reserve. 8 + Counter::INIT_SPACE is the byte layout: InitSpace computes the size of your struct's fields, and the extra 8 bytes are Anchor's discriminator — a prefix it stamps on every account so it can tell what type is stored there later. Forgetting that 8 is a fast way to corrupt an account.&lt;br&gt;
Why the seeds matter&lt;/p&gt;

&lt;p&gt;This is the part that actually determines who can write where, and it's easy to get backwards.&lt;/p&gt;

&lt;p&gt;I derived the same PDA two ways: [b"counter", user.key().as_ref()] and [b"counter"] — with no wallet mixed in.&lt;/p&gt;

&lt;p&gt;The first gives every wallet its own address. Alice's counter and Bob's counter land at two different places on the ledger, because their public keys are different inputs to the hash. That's what you want for per-user state.&lt;/p&gt;

&lt;p&gt;The second gives every caller the same address, regardless of who's asking. That's not automatically wrong — it's exactly what you want for a global singleton, like a config account with a paused flag and an admin key. But if you meant to write a per-user counter and left the user out of the seeds by accident, every wallet after the first one to call init hits an already in use error, because the system program refuses to create the same address twice. Same mechanism, opposite outcome, depending entirely on what you decided to hash in.&lt;/p&gt;

&lt;p&gt;PDAs don't really "collide" in the cryptographic sense — engineering two different seed inputs to land on the same address is infeasible. The collisions that actually bite you are the ones you write on purpose, by leaving identity out of the seeds and not noticing until a second user shows up.&lt;/p&gt;

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

&lt;p&gt;Only the canonical bump — the one find_program_address returns — is safe to use. Non-canonical bumps can also produce off-curve addresses, but there's nothing stopping two different bumps from both being valid off-curve results for slightly different reasons, and accepting an arbitrary one opens the door to an attacker deriving a second, unintended valid address for the same seeds.&lt;/p&gt;

&lt;p&gt;Anchor stores the canonical bump for you the moment you write bump (no value) in an init constraint — it shows up in ctx.bumps.counter inside your handler, and I saved it onto the account itself:&lt;/p&gt;

&lt;p&gt;rust&lt;br&gt;
counter.bump = ctx.bumps.counter;&lt;/p&gt;

&lt;p&gt;On every instruction after that, I stopped asking Anchor to re-derive the bump from scratch and instead passed the stored one back in:&lt;/p&gt;

&lt;p&gt;rust&lt;/p&gt;

&lt;h1&gt;
  
  
  [account(
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mut,
seeds = [b"counter", user.key().as_ref()],
bump = counter.bump,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;)]&lt;br&gt;
pub counter: Account&amp;lt;'info, Counter&amp;gt;,&lt;/p&gt;

&lt;p&gt;Re-deriving is a loop that walks down from 255 doing a hash at each step; reading a stored u8 is free. Once you know the canonical bump, there's no reason to pay for finding it again.&lt;/p&gt;

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

&lt;p&gt;Putting the week together as one sequence, using the same counter account the whole way through:&lt;/p&gt;

&lt;p&gt;Derive. find_program_address([b"counter", user], programId) gives you an address and a bump. Nothing exists there yet — this step is pure computation, no network call.&lt;br&gt;
Initialize. The init constraint creates the account at that address, funded by the payer, sized to hold your struct plus the 8-byte discriminator.&lt;br&gt;
Mutate. Later instructions load the same PDA by re-supplying the same seeds and the stored bump, so Anchor re-derives the address and confirms it matches what you handed it before running your handler logic.&lt;br&gt;
Close. This is the step that doesn't map cleanly onto a Web2 mental model. close = user doesn't run a DELETE FROM — it drains every lamport in the account back to the wallet named, zeroes out the account's data, and marks it for garbage collection at the end of the transaction:&lt;br&gt;
rust&lt;/p&gt;

&lt;h1&gt;
  
  
  [account(
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mut,
close = user,
seeds = [b"counter", user.key().as_ref()],
bump = counter.bump,
has_one = user,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;)]&lt;br&gt;
pub counter: Account&amp;lt;'info, Counter&amp;gt;,&lt;/p&gt;

&lt;p&gt;The handler for close_counter is empty — Ok(()) and nothing else — because all the actual work happens declaratively in that accounts struct, not in the function body. The rent you paid at init time was never spent; it was a refundable deposit compensating the network for the storage bytes, and it comes back in full the moment the account stops existing.&lt;/p&gt;

&lt;p&gt;What I would tell past me&lt;br&gt;
The program ID is part of the derivation. The exact same seeds passed to a different program produce a completely different address — the seeds alone don't own anything.&lt;br&gt;
PDAs cannot sign for themselves. Only the program that owns them can sign on their behalf, and only by supplying the same seeds at signing time. There is no private key anywhere for a PDA, ever.&lt;br&gt;
The seeds/bump constraint isn't just for finding the account — it's also the access control. When I tried passing Wallet B's counter into an increment call signed by Wallet A, Anchor re-derived the expected address from Wallet A's key, found it didn't match the account I supplied, and rejected the transaction with a ConstraintSeeds error before any of my handler code ran. I didn't write that check; the seeds constraint was the check.&lt;br&gt;
has_one is a different, complementary guard — it compares a field stored on the account (like counter.user or config.admin) against a signer you passed in, rather than re-deriving an address. I use both, for different reasons, and it's worth keeping straight which one is doing which job.&lt;br&gt;
init_if_needed is convenient and also a footgun. Reach for it deliberately, not as a default, because it quietly changes an "only run once" instruction into one that can be called again against existing state.&lt;br&gt;
Further reading&lt;/p&gt;

&lt;p&gt;If you want the primary sources instead of my paraphrase of them: the Solana PDA documentation and the Anchor PDA guide cover the same ground with more precision than a week-one blog post can. My counter program is on GitHub if you want to see the whole thing in context.&lt;/p&gt;

&lt;h1&gt;
  
  
  100DaysOfSolana
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>rust</category>
      <category>web3</category>
    </item>
    <item>
      <title>How I built a Counter program in Anchor and learned to trust my tests</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:57:55 +0000</pubDate>
      <link>https://dev.to/babydriver/how-i-built-a-counter-program-in-anchor-and-learned-to-trust-my-tests-20c</link>
      <guid>https://dev.to/babydriver/how-i-built-a-counter-program-in-anchor-and-learned-to-trust-my-tests-20c</guid>
      <description>&lt;p&gt;My first Anchor program finally started feeling like a real on-chain application when I stopped thinking of accounts as “database rows” and started understanding how ownership, constraints, and tests work together.&lt;/p&gt;

&lt;p&gt;The first thing Anchor made clear was the account context.&lt;/p&gt;

&lt;h1&gt;
  
  
  [derive(Accounts)]
&lt;/h1&gt;

&lt;p&gt;pub struct Initialize&amp;lt;'info&amp;gt; {&lt;br&gt;
    pub counter: Account&amp;lt;'info, Counter&amp;gt;,&lt;br&gt;
    pub authority: Signer&amp;lt;'info&amp;gt;,&lt;br&gt;
    pub system_program: Program&amp;lt;'info, System&amp;gt;,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The Accounts struct is where Anchor changes the way you think compared to a Web2 backend.&lt;/p&gt;

&lt;p&gt;Instead of manually validating requests, allocating storage, and checking permissions, you describe the rules. counter is the on-chain state account, authority is the wallet signing the transaction, and system_program handles account creation. Anchor uses this information to generate the validation logic before your instruction runs.&lt;/p&gt;

&lt;p&gt;The initialize handler is surprisingly small:&lt;/p&gt;

&lt;p&gt;pub fn initialize(ctx: Context) -&amp;gt; Result&amp;lt;()&amp;gt; {&lt;br&gt;
    let counter = &amp;amp;mut ctx.accounts.counter;&lt;br&gt;
    counter.authority = ctx.accounts.authority.key();&lt;br&gt;
    counter.count = 0;&lt;br&gt;
    Ok(())&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;ctx.accounts gives direct access to the accounts passed into the transaction. Because Anchor already verified the accounts, the handler only focuses on business logic: storing the owner and setting the initial value.&lt;/p&gt;

&lt;p&gt;Then came the increment instruction:&lt;/p&gt;

&lt;p&gt;pub fn increment(ctx: Context) -&amp;gt; Result&amp;lt;()&amp;gt; {&lt;br&gt;
    let counter = &amp;amp;mut ctx.accounts.counter;&lt;br&gt;
    counter.count = counter.count&lt;br&gt;
        .checked_add(1)&lt;br&gt;
        .ok_or(ProgramError::ArithmeticOverflow)?;&lt;br&gt;
    Ok(())&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The important part was not the increment itself, but the constraint:&lt;/p&gt;

&lt;h1&gt;
  
  
  [account(mut, has_one = authority)]
&lt;/h1&gt;

&lt;p&gt;pub counter: Account&amp;lt;'info, Counter&amp;gt;,&lt;/p&gt;

&lt;p&gt;has_one = authority guarantees that the signer calling increment is the same wallet stored as the counter owner. The check happens before my code executes.&lt;/p&gt;

&lt;p&gt;My tests became the proof that these guarantees actually worked.&lt;/p&gt;

&lt;p&gt;The happy path test checks that initialization creates the correct state:&lt;/p&gt;

&lt;p&gt;assert_eq!(parsed.count, 0);&lt;br&gt;
assert_eq!(parsed.authority, authority.pubkey());&lt;/p&gt;

&lt;p&gt;If this fails, something is wrong with account creation or state initialization.&lt;/p&gt;

&lt;p&gt;The failure test checks that unauthorized users cannot modify someone else’s counter:&lt;/p&gt;

&lt;p&gt;let result = svm.send_transaction(bad_tx);&lt;/p&gt;

&lt;p&gt;assert!(&lt;br&gt;
    result.is_err(),&lt;br&gt;
    "increment should fail when signed by the wrong authority"&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;If this fails, the program is allowing a wallet that does not own the counter to update it.&lt;/p&gt;

&lt;p&gt;The most interesting experiment was breaking the program on purpose.&lt;/p&gt;

&lt;p&gt;I changed:&lt;/p&gt;

&lt;p&gt;checked_add(1)&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;checked_add(2)&lt;/p&gt;

&lt;p&gt;The program still ran, but the test caught the bug:&lt;/p&gt;

&lt;p&gt;assertion &lt;code&gt;left == right&lt;/code&gt; failed&lt;br&gt;
left: 2&lt;br&gt;
right: 1&lt;/p&gt;

&lt;p&gt;That was the moment I understood why tests matter on-chain. A green test is not just a checkmark — it is evidence that a specific rule is still protected.&lt;/p&gt;

&lt;p&gt;Next week, I would build on this by adding more realistic account relationships, better error handling, and connecting the Anchor program with a frontend client.&lt;/p&gt;

&lt;h1&gt;
  
  
  100DaysOfSolana #solana #rust #anchor #testing
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>rust</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Three Token-2022 mints in one week: fees, yield, and soul-bound #100DaysOfSolana</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Wed, 17 Jun 2026 16:46:01 +0000</pubDate>
      <link>https://dev.to/babydriver/three-token-2022-mints-in-one-week-fees-yield-and-soul-bound-100daysofsolana-1oan</link>
      <guid>https://dev.to/babydriver/three-token-2022-mints-in-one-week-fees-yield-and-soul-bound-100daysofsolana-1oan</guid>
      <description>&lt;p&gt;If you have never touched Solana before, here is the one thing you need to know going in: Token-2022 is the upgraded SPL token program, and extensions are how it works. Instead of writing custom smart contract logic to enforce a royalty fee or an interest rate, you flip a flag when you create the mint. The rule travels with the asset, enforced by the protocol itself, visible to every wallet and program that touches it. Think of it as middleware baked directly into the currency, not bolted on next to it.&lt;br&gt;
Over the last week I shipped three distinct mints to devnet. Each one adds a different extension. Here is what I built, what I ran, and what happened.&lt;/p&gt;

&lt;p&gt;Mint 1 — Transfer fee (Days 50–51)&lt;br&gt;
Mint address: G37ZvuZ9wQRnagSDGHb985qqaTsWWxPeuZPPE951LNZ9&lt;/p&gt;

&lt;p&gt;View on Solana Explorer&lt;br&gt;
Extension: TransferFeeConfig — 100 basis points (1%), maximum fee 1,000,000 tokens&lt;br&gt;
bashspl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \&lt;br&gt;
  create-token \&lt;br&gt;
  --transfer-fee-basis-points 100 \&lt;br&gt;
  --transfer-fee-maximum-fee 10000000 \&lt;br&gt;
  --decimals 6&lt;br&gt;
When would you actually reach for this? Think protocol treasury skims on a stablecoin, royalties on a creator token, or a community currency that automatically funds a DAO wallet on every trade. The fee is not enforced by your API or your database — it is enforced by the validator. There is no way to route around it.&lt;br&gt;
The lifecycle I tested: mint supply → transfer 1,000 tokens to a fresh wallet → check the recipient's account for the Transfer fees withheld field → withdraw those withheld fees back to my own account. The whole loop, no middleware, no webhook.&lt;/p&gt;

&lt;p&gt;Mint 2 — Interest-bearing stacked on transfer fee (Day 52)&lt;br&gt;
Mint address: F15ZkWji8PsQM7VrgnRNLUh1n8W8CQJp71TbDkSUsi2a&lt;/p&gt;

&lt;p&gt;View on Solana Explorer&lt;br&gt;
Extensions: InterestBearingConfig (5,000 bps / 50% APR) + TransferFeeConfig (100 bps, same as above)&lt;br&gt;
bashspl-token create-token \&lt;br&gt;
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \&lt;br&gt;
  --decimals 6 \&lt;br&gt;
  --transfer-fee-basis-points 100 \&lt;br&gt;
  --transfer-fee-maximum-fee 1000000 \&lt;br&gt;
  --interest-rate 5000&lt;br&gt;
One thing I want to be honest about here, because it confused me at first: the interest-bearing extension does not mint new supply. It adjusts the UI amount — the number your wallet shows you — based on elapsed time and the configured rate. The raw on-chain balance stays the same. I waited 30 seconds between two spl-token accounts calls and watched the displayed number tick up from 1000004.816733 to 1000007.985649 with no transactions in between. That is the extension doing the compounding math. If you are building a lending protocol or a synthetic yield token, you need to understand this distinction before you go anywhere near production.&lt;br&gt;
The spl-token display output on this mint showed both extensions side by side in the same TLV blob — Interest-bearing at 5,000 bps and Transfer fees at 100 bps. Two behaviors, one create-token invocation.&lt;/p&gt;

&lt;p&gt;Mint 3 — Non-transferable / soul-bound (Day 54)&lt;br&gt;
Mint address: 7u4f9idiq865z2pYndPsPjpiQN95p4uh7fHdTgKkvWgE&lt;/p&gt;

&lt;p&gt;View on Solana Explorer&lt;br&gt;
Extension: NonTransferable&lt;br&gt;
bashspl-token create-token --program-2022 --enable-non-transferable&lt;br&gt;
This one is different in kind. The first two extensions are quantitative — they layer financial behaviors onto a currency primitive. Non-transferable is qualitative. It turns the same mint, the same accounts, the same CLI into an identity object. The closest Web2 analogy is a certificate of completion stapled to a profile page that no one can detach or resell.&lt;br&gt;
I minted one token to myself, generated a throwaway keypair as a recipient, pre-funded their token account so the transfer could actually reach the program, then ran the transfer. Here is what came back:&lt;br&gt;
Error: Client(Error { kind: RpcError(RpcResponseError {&lt;br&gt;
  message: "Transaction simulation failed: Error processing Instruction 0:&lt;br&gt;
  custom program error: 0x25" ...&lt;br&gt;
  logs: ["Program log: Instruction: TransferChecked",&lt;br&gt;
         "Program log: Transfer is disabled for this mint", ...&lt;br&gt;
Transfer is disabled for this mint. The rejection came from the Token-2022 program itself, not from my application layer. In Web2 you might enforce this with a database constraint or an API check — but anyone who talks to the database around your application can break that rule. Here the rule lives on the asset, inside the program, inside the validator. There is no around.&lt;br&gt;
Running spl-token display on the mint confirmed it: the Extensions block shows Non-transferable with nothing else needed.&lt;/p&gt;

&lt;p&gt;What surprised me&lt;br&gt;
I expected extensions to feel like configuration knobs bolted onto a normal token. They do not. They feel like the token is the configuration. The non-transferable experiment especially landed differently than I expected — seeing the validator reject a transfer at the protocol level, not at the app layer, is the moment the mental model clicks. The error message is the feature.&lt;br&gt;
If I were building something real today, I would reach for transfer fees for any protocol where the treasury needs to be funded trustless, and for non-transferable for anything credential-shaped: completion badges, access passes, reputation tokens that should not be liquid. The interest-bearing extension is the most nuanced of the three — powerful for display purposes, but you need to understand what it is and is not doing to use it safely.&lt;/p&gt;

&lt;h1&gt;
  
  
  100DaysOfSolana #solana #web3 #tutorial
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>buildinpublic</category>
      <category>showdev</category>
      <category>web3</category>
    </item>
    <item>
      <title>What I learned minting NFTs on Solana with Token Extensions #100DaysOfSolana</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Sat, 13 Jun 2026 11:48:43 +0000</pubDate>
      <link>https://dev.to/babydriver/what-i-learned-minting-nfts-on-solana-with-token-extensions-100daysofsolana-1f6n</link>
      <guid>https://dev.to/babydriver/what-i-learned-minting-nfts-on-solana-with-token-extensions-100daysofsolana-1f6n</guid>
      <description>&lt;h1&gt;
  
  
  What I learned minting NFTs on Solana with Token Extensions
&lt;/h1&gt;

&lt;p&gt;Before this week, I thought creating a Solana NFT meant using a marketplace stack or relying on a separate metadata program. I wanted to understand what actually happens underneath.&lt;/p&gt;

&lt;p&gt;It turns out an NFT on Solana is still built from the same token primitives I had already been working with: a mint, a supply of one, zero decimals, and extensions that add meaning directly on-chain.&lt;/p&gt;

&lt;p&gt;This week I went from creating a basic 1-of-1 token to building a complete NFT with metadata, collections, and on-chain verification using Token Extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: what an NFT actually is on Solana
&lt;/h2&gt;

&lt;p&gt;The biggest shift was realizing that an NFT is not a completely different asset type.&lt;/p&gt;

&lt;p&gt;At the core, it is still a token mint.&lt;/p&gt;

&lt;p&gt;The difference is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supply is exactly &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Decimals are &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mint authority is disabled so no more copies can be created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Web2 terms, imagine creating a database row where the ID is unique, the quantity can never increase, and ownership history is tracked publicly.&lt;/p&gt;

&lt;p&gt;The first NFT I created started with a simple mint:&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; https://api.devnet.solana.com

spl-token create-token &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0

spl-token create-account &lt;span class="o"&gt;[&lt;/span&gt;YOUR_MINT_ADDRESS]

spl-token mint &lt;span class="o"&gt;[&lt;/span&gt;YOUR_MINT_ADDRESS] 1

spl-token authorize &lt;span class="o"&gt;[&lt;/span&gt;YOUR_MINT_ADDRESS] mint &lt;span class="nt"&gt;--disable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, the token was technically an NFT, but it had no identity.&lt;/p&gt;

&lt;p&gt;No name.&lt;br&gt;
No image.&lt;br&gt;
No metadata.&lt;/p&gt;

&lt;p&gt;It was just an on-chain asset waiting for more information.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I built: metadata, collections, and extensions
&lt;/h2&gt;

&lt;p&gt;The next step was adding identity.&lt;/p&gt;

&lt;p&gt;With Token Extensions, metadata can live directly with the mint account instead of depending on a separate metadata account.&lt;/p&gt;

&lt;p&gt;The metadata extension allowed me to attach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name&lt;/li&gt;
&lt;li&gt;Symbol&lt;/li&gt;
&lt;li&gt;Image URI&lt;/li&gt;
&lt;li&gt;Attributes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow 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 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; 0

spl-token initialize-metadata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;YOUR_MINT_ADDRESS] &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"First Light"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"LIGHT"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;YOUR_METADATA_URI]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing the NFT appear in Solana Explorer with its own metadata was the moment it stopped feeling like a token experiment and started feeling like an actual NFT.&lt;/p&gt;

&lt;p&gt;The next challenge was collections.&lt;/p&gt;

&lt;p&gt;A real NFT usually belongs to something bigger. A collection is basically another mint that acts as the parent.&lt;/p&gt;

&lt;p&gt;The Group extension creates the collection:&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 create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then member NFTs reference that group using the Member extension:&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-member MEMBER_ONE_MINT COLLECTION_MINT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the relationship exists fully on-chain:&lt;/p&gt;

&lt;p&gt;Collection → NFT members&lt;/p&gt;

&lt;p&gt;Similar to a database foreign key connecting two tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprising part: what was different from Web2
&lt;/h2&gt;

&lt;p&gt;Coming from Web2 development, I expected NFTs to be mostly application logic.&lt;/p&gt;

&lt;p&gt;A database stores metadata.&lt;br&gt;
A backend verifies ownership.&lt;br&gt;
A frontend renders the result.&lt;/p&gt;

&lt;p&gt;Solana flips a lot of that thinking.&lt;/p&gt;

&lt;p&gt;The blockchain itself becomes the source of truth.&lt;/p&gt;

&lt;p&gt;The mint account contains the structure.&lt;br&gt;
Extensions define additional capabilities.&lt;br&gt;
Explorer tools can directly read and display the data.&lt;/p&gt;

&lt;p&gt;I also found it interesting that metadata is not just a static image attached forever.&lt;/p&gt;

&lt;p&gt;The update authority can modify fields:&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 update-metadata &lt;span class="o"&gt;[&lt;/span&gt;MINT_ADDRESS] name &lt;span class="s2"&gt;"Field Notes"&lt;/span&gt;

spl-token update-metadata &lt;span class="o"&gt;[&lt;/span&gt;MINT_ADDRESS] rarity legendary

spl-token update-metadata &lt;span class="o"&gt;[&lt;/span&gt;MINT_ADDRESS] rarity &lt;span class="nt"&gt;--remove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This felt very similar to updating a database record, except the change happens through a blockchain transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;After understanding the building blocks, the next step would be building an actual NFT application around them.&lt;/p&gt;

&lt;p&gt;Some ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Flutter mobile app that creates and manages NFTs&lt;/li&gt;
&lt;li&gt;An NFT dashboard that reads extensions directly from Solana&lt;/li&gt;
&lt;li&gt;A creator platform where users mint collections without writing CLI commands&lt;/li&gt;
&lt;li&gt;AI-generated NFTs with dynamic metadata updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson from this week:&lt;/p&gt;

&lt;p&gt;NFTs are not magic objects. They are carefully designed token structures with rules, ownership, metadata, and relationships.&lt;/p&gt;

&lt;p&gt;Understanding the primitives makes the whole ecosystem easier to build on.&lt;/p&gt;

&lt;p&gt;This post is part of #100DaysOfSolana. Follow along or jump in any day.&lt;/p&gt;

&lt;h1&gt;
  
  
  Beginner #DEV
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>learning</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>From Interest-Bearing Assets to Soul-bound Credentials: My Deep Dive into Solana Token Extensions</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Fri, 05 Jun 2026 14:58:02 +0000</pubDate>
      <link>https://dev.to/babydriver/from-interest-bearing-assets-to-soul-bound-credentials-my-deep-dive-into-solana-token-extensions-3i0o</link>
      <guid>https://dev.to/babydriver/from-interest-bearing-assets-to-soul-bound-credentials-my-deep-dive-into-solana-token-extensions-3i0o</guid>
      <description>&lt;p&gt;One of the things that surprised me most while learning Solana is how much functionality can exist directly inside a token.&lt;/p&gt;

&lt;p&gt;In traditional Web2 applications, features like interest accrual, transaction fees, user verification, account restrictions, and credentials are usually implemented through backend services, databases, and application logic. Developers spend significant time wiring these systems together and ensuring they work correctly.&lt;/p&gt;

&lt;p&gt;Over the last five days of my #100DaysOfSolana journey, I explored how Solana's Token-2022 program approaches the same problems differently. Instead of building these features around a token, many of them can be built directly into the token itself through extensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 36 — Creating an Interest-Bearing Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first extension I explored was the Interest-Bearing Token extension.&lt;/p&gt;

&lt;p&gt;At first, I expected the token balance itself to increase over time. What I learned was much more interesting.&lt;/p&gt;

&lt;p&gt;The actual on-chain balance never changes. Instead, Solana stores an interest rate and uses time-based calculations to display an adjusted balance to users and applications.&lt;/p&gt;

&lt;p&gt;This creates an experience similar to a savings account where your balance appears to grow continuously while the underlying ledger remains unchanged.&lt;/p&gt;

&lt;p&gt;Some key takeaways:&lt;/p&gt;

&lt;p&gt;Interest rates are configured in basis points.&lt;br&gt;
Wallets calculate adjusted balances using elapsed time.&lt;br&gt;
No additional tokens are minted automatically.&lt;br&gt;
The extension acts as a display layer rather than modifying token supply.&lt;/p&gt;

&lt;p&gt;It was my first look at how financial primitives can be implemented directly at the protocol level.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Day 37 — Combining Multiple Extensions&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
After experimenting with individual extensions, I wanted to see what would happen if multiple extensions were combined into a single token.&lt;/p&gt;

&lt;p&gt;I created a token that included:&lt;/p&gt;

&lt;p&gt;Transfer fees&lt;br&gt;
Interest-bearing balances&lt;br&gt;
On-chain metadata&lt;/p&gt;

&lt;p&gt;This was where the Token-2022 design really started to click for me.&lt;/p&gt;

&lt;p&gt;Each extension operates independently while sharing the same mint account. The token could charge fees during transfers, display interest-adjusted balances, and carry its own metadata without any of the features interfering with one another.&lt;/p&gt;

&lt;p&gt;The biggest lesson from this day was understanding that extension choices must be made when the mint is created. Solana allocates account space upfront, so planning matters.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Day 38 — Building a Compliance-Gated Token&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Financial systems often require permissions before users can transact.&lt;/p&gt;

&lt;p&gt;Brokerage accounts require verification.&lt;br&gt;
Payment apps may freeze suspicious accounts.&lt;br&gt;
Many regulated assets require approval before transfers can occur.&lt;/p&gt;

&lt;p&gt;To replicate this pattern, I experimented with the Default Account State extension.&lt;/p&gt;

&lt;p&gt;Every new token account created for the mint started in a frozen state.&lt;/p&gt;

&lt;p&gt;Nothing could be transferred, received, or burned until the freeze authority explicitly approved the account.&lt;/p&gt;

&lt;p&gt;What made this powerful was that enforcement happened at the protocol level rather than inside application code.&lt;/p&gt;

&lt;p&gt;Even if a frontend contained a bug, users still couldn't bypass the restriction because the blockchain itself enforced the rules.&lt;/p&gt;

&lt;p&gt;This was my first exposure to how regulated asset systems can be implemented directly on Solana.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 39 — Reading Before Building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After creating several different token configurations, I spent a day inspecting and comparing them.&lt;/p&gt;

&lt;p&gt;Instead of deploying something new, I focused on understanding what I had already built.&lt;/p&gt;

&lt;p&gt;I examined:&lt;/p&gt;

&lt;p&gt;Interest-bearing mints&lt;br&gt;
Multi-extension mints&lt;br&gt;
Default-frozen mints&lt;/p&gt;

&lt;p&gt;Using the CLI, I reviewed extension configurations, authorities, metadata, fees, and account sizes.&lt;/p&gt;

&lt;p&gt;One observation stood out immediately:&lt;/p&gt;

&lt;p&gt;More extensions mean larger account sizes.&lt;/p&gt;

&lt;p&gt;Larger accounts require larger rent-exempt deposits, which means every design decision has a cost attached to it.&lt;/p&gt;

&lt;p&gt;This reinforced an important engineering lesson:&lt;/p&gt;

&lt;p&gt;Before building new systems, understand the systems that already exist.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Day 40 — Creating Revocable Digital Credentials&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The most interesting experiment of the week involved combining two extensions I had never used before:&lt;/p&gt;

&lt;p&gt;Non-Transferable&lt;br&gt;
Permanent Delegate&lt;/p&gt;

&lt;p&gt;Together, these allowed me to create a token that behaved like a digital credential.&lt;/p&gt;

&lt;p&gt;The token could:&lt;/p&gt;

&lt;p&gt;Be issued to a specific wallet&lt;br&gt;
Never be transferred to another wallet&lt;br&gt;
Be revoked by the issuing authority&lt;/p&gt;

&lt;p&gt;This closely resembles real-world certificates, licenses, employee badges, or verified status systems.&lt;/p&gt;

&lt;p&gt;A credential belongs to the recipient, but the issuing organization still retains the ability to revoke it when necessary.&lt;/p&gt;

&lt;p&gt;Seeing this implemented entirely through token extensions was fascinating.&lt;/p&gt;

&lt;p&gt;The token itself enforced ownership and transfer restrictions without requiring additional application logic.&lt;/p&gt;

&lt;p&gt;What I Learned This Week&lt;/p&gt;

&lt;p&gt;This week completely changed how I think about tokens.&lt;/p&gt;

&lt;p&gt;I started by viewing tokens as simple units of value.&lt;/p&gt;

&lt;p&gt;Now I see them as programmable assets capable of enforcing business rules directly at the protocol level.&lt;/p&gt;

&lt;p&gt;Across these five days I learned how to build:&lt;/p&gt;

&lt;p&gt;Interest-bearing financial assets&lt;br&gt;
Fee-generating tokens&lt;br&gt;
Compliance-gated systems&lt;br&gt;
Multi-extension token architectures&lt;br&gt;
Soulbound credentials with revocation mechanisms&lt;/p&gt;

&lt;p&gt;The most valuable lesson wasn't a specific command or extension.&lt;/p&gt;

&lt;p&gt;It was understanding that Token-2022 turns tokens into building blocks for entire applications.&lt;/p&gt;

&lt;p&gt;Instead of asking, "What can my application do with a token?"&lt;/p&gt;

&lt;p&gt;I'm starting to ask,&lt;/p&gt;

&lt;p&gt;"What functionality can I move directly into the token itself?"&lt;/p&gt;

&lt;p&gt;And that shift in perspective might be the biggest lesson I've learned so far in my Solana journey.&lt;/p&gt;

&lt;h1&gt;
  
  
  100DaysOfSolana #Solana #Web3 #Blockchain #Token2022 #BuildInPublic #DeveloperJourney
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>devjournal</category>
      <category>programming</category>
      <category>web3</category>
    </item>
    <item>
      <title>Week 5 of my #100DaysOfSolana challenge is complete 🚀</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Wed, 03 Jun 2026 13:38:33 +0000</pubDate>
      <link>https://dev.to/babydriver/week-5-of-my-100daysofsolana-challenge-is-complete-50go</link>
      <guid>https://dev.to/babydriver/week-5-of-my-100daysofsolana-challenge-is-complete-50go</guid>
      <description>&lt;p&gt;This week I built custom SPL tokens on Solana using the Token Extensions Program. I created tokens with on-chain metadata, configured protocol-level transfer fees, and even experimented with non-transferable (soulbound) tokens.&lt;/p&gt;

&lt;p&gt;The most interesting moment was testing a non-transferable token and watching the blockchain reject the transfer attempt automatically. Seeing the protocol enforce the rule itself — without any custom backend or off-chain validation — completely changed how I think about token design.&lt;/p&gt;

&lt;p&gt;I also explored transfer fee extensions and learned how economic rules can be embedded directly into the token standard instead of being handled by application logic.&lt;/p&gt;

&lt;p&gt;Next week I'm diving deeper into how these token extensions interact with on-chain programs and real-world applications built on Solana.&lt;/p&gt;

&lt;p&gt;All code is open source and documented as part of my learning journey:&lt;br&gt;
GitHub: &lt;a href="https://github.com/Siddhant0122/100-days-of-solana" rel="noopener noreferrer"&gt;https://github.com/Siddhant0122/100-days-of-solana&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Solana #Web3 #Blockchain #BuildInPublic #LearnInPublic #100DaysOfSolana
&lt;/h1&gt;

</description>
      <category>blockchain</category>
      <category>buildinpublic</category>
      <category>learning</category>
      <category>web3</category>
    </item>
    <item>
      <title>I Built My First Token on Solana — Here's What Actually Surprised Me #100DaysOfSolana.</title>
      <dc:creator>Siddhant Chavan</dc:creator>
      <pubDate>Tue, 02 Jun 2026 18:42:37 +0000</pubDate>
      <link>https://dev.to/babydriver/i-built-my-first-token-on-solana-heres-what-actually-surprised-me-100daysofsolana-ng6</link>
      <guid>https://dev.to/babydriver/i-built-my-first-token-on-solana-heres-what-actually-surprised-me-100daysofsolana-ng6</guid>
      <description>&lt;p&gt;I Built My First Token on Solana — Here's What Actually Surprised Me&lt;br&gt;
This week I went from zero tokens to minting, transferring, charging fees, and locking tokens so they can never move. Here's what stuck with me.&lt;/p&gt;

&lt;p&gt;Tokens don't live in your wallet&lt;br&gt;
Coming from Web2, I assumed tokens would just... show up in your account. Nope. On Solana, every wallet needs a separate token account for each token it holds. One mint, one folder. It felt weird at first. Now it makes sense.&lt;br&gt;
You can charge fees without writing a single backend&lt;br&gt;
The Token Extensions Program has a built-in transfer fee. One flag at mint creation time:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token \&lt;br&gt;
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \&lt;br&gt;
  --transfer-fee-basis-points 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's a 1% fee on every transfer, enforced by the blockchain. No middleware. No payment processor. No way to bypass it.&lt;br&gt;
You can make a token that literally cannot be transferred&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token \&lt;br&gt;
  --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \&lt;br&gt;
  --enable-non-transferable&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I minted 10, tried to send 5, and watched the transaction get rejected. Not by my code — by the program itself. Perfect for credentials, badges, or certificates that should belong to one person forever.&lt;/p&gt;

&lt;p&gt;The biggest shift from Web2: these rules are set at creation and are permanent. You can't add a transfer fee to an existing token. You can't make a transferable token non-transferable later. It forces you to think about token design upfront, which is honestly a good constraint.&lt;/p&gt;

&lt;h1&gt;
  
  
  solana #blockchain #webdev #beginners #100DaysOfSolana
&lt;/h1&gt;

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