<?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: Mubarak Yakubu</title>
    <description>The latest articles on DEV Community by Mubarak Yakubu (@mubaraqabba).</description>
    <link>https://dev.to/mubaraqabba</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%2F3922593%2F1c246f7c-3ea4-4a1a-bc90-f3d4c5d24ea7.png</url>
      <title>DEV Community: Mubarak Yakubu</title>
      <link>https://dev.to/mubaraqabba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mubaraqabba"/>
    <language>en</language>
    <item>
      <title>From Token Watcher to Program Builder: 100 Days of Solana</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:36:29 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/from-token-watcher-to-program-builder-100-days-of-solana-4l36</link>
      <guid>https://dev.to/mubaraqabba/from-token-watcher-to-program-builder-100-days-of-solana-4l36</guid>
      <description>&lt;p&gt;I came into this program as a Web2 developer who moderates Web3 communities.&lt;/p&gt;

&lt;p&gt;I knew Solana as "the blockchain people build products and solutions on, and launch tokens on." That's it. I'd watched projects build on it, seen the ecosystem from the outside as a community moderator and ambassador. But I'd never written a line of Solana code.&lt;/p&gt;

&lt;p&gt;Ninety-nine days later, I have a program deployed to devnet with an agent that reads its state and a Token-2022 badge minted to my wallet.&lt;/p&gt;

&lt;p&gt;Here's what actually changed.&lt;/p&gt;

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

&lt;p&gt;I expected blockchain development to feel like backend development with a crypto coating on top. Servers become programs. Databases become accounts. Same shape, different words.&lt;/p&gt;

&lt;p&gt;That was wrong in a specific way.&lt;/p&gt;

&lt;p&gt;There is no server. There's no database you can quietly edit. Everything is a transaction, and every transaction is permanent.&lt;/p&gt;

&lt;p&gt;When I started, I assumed the authority owner could just change things after the fact. Redeploy, fix a bug, update a record. And in one sense, that's true. Programs are upgradeable by default. The upgrade authority can push new code to the same program ID.&lt;/p&gt;

&lt;p&gt;But here's what I got wrong: you can change the code, but you can't change what the code already did.&lt;/p&gt;

&lt;p&gt;If a wallet called your program yesterday and wrote a record, that record is on-chain. You can write a new transaction that changes its current state, but the history stays. You can't reach back and pretend it didn't happen.&lt;/p&gt;

&lt;p&gt;That's the shift Web2 doesn't prepare you for. There's no &lt;code&gt;UPDATE&lt;/code&gt; that erases the old value. There's no rollback. The code you deploy is the code that runs, and what it wrote is written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it clicked
&lt;/h2&gt;

&lt;p&gt;The vault program.&lt;/p&gt;

&lt;p&gt;I built a program that holds SOL, lets you deposit, lets you withdraw, and enforces that only the authority can withdraw. Small. Basic.&lt;/p&gt;

&lt;p&gt;But when it worked, I saw something bigger. The pieces I'd been learning one at a time (accounts, PDAs, constraints, error codes, tests) stopped being separate exercises. They became a system.&lt;/p&gt;

&lt;p&gt;I've worked in Web2 long enough to recognize that feeling. It's the moment you stop seeing a framework and start seeing the shape of something you can build. The vault was small, but it was the first time I could picture building real products this way.&lt;/p&gt;

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

&lt;p&gt;The toolchain is what makes Solana manageable.&lt;/p&gt;

&lt;p&gt;That's a distinction I didn't have words for until recently. The protocol is what Solana &lt;em&gt;is&lt;/em&gt;: accounts, transactions, programs, the runtime. You could write a Solana program in raw Rust with no framework at all. It would just be painful.&lt;/p&gt;

&lt;p&gt;The toolchain is what makes the protocol usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchor&lt;/strong&gt; gives you structure. The &lt;code&gt;#[program]&lt;/code&gt; macro, &lt;code&gt;#[derive(Accounts)]&lt;/code&gt;, the &lt;code&gt;require!&lt;/code&gt; macro. Everything has a place. Without it, you're writing instruction dispatch and account validation by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDAs&lt;/strong&gt; are a protocol feature, but knowing when and how to use them is toolchain-level knowledge. Anchor gives you a clean way to declare them: &lt;code&gt;seeds = [...]&lt;/code&gt;. Once I understood that a PDA isn't a wallet, that it's a deterministic address only your program can sign for, half of Solana's design made sense.&lt;/p&gt;

&lt;p&gt;The capstone's seeds are &lt;code&gt;[b"ship", builder.key()]&lt;/code&gt;. That's it. The address itself enforces "one record per wallet." No runtime check, no guard clause. The seed is the constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDLs&lt;/strong&gt; are the bridge between the on-chain program and everything else. The protocol doesn't know or care about IDLs. But without them, every client that wants to call your program has to hand-encode instruction bytes. With them, you get autocomplete, typed clients, and agents that know what to call.&lt;/p&gt;

&lt;p&gt;You can learn the protocol and still not be able to build anything real. The toolchain is the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built for the capstone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;proof-of-ship&lt;/code&gt;. One instruction: &lt;code&gt;ship(project_name, message)&lt;/code&gt;. Any wallet can call it once to permanently record on-chain that it shipped a capstone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program:&lt;/strong&gt; &lt;code&gt;6xKXthNFn8YdaPVqdrAJWDfaAxeNeBX5MZuUSaVxd7mL&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Explorer:&lt;/strong&gt; &lt;a href="https://explorer.solana.com/address/6xKXthNFn8YdaPVqdrAJWDfaAxeNeBX5MZuUSaVxd7mL?cluster=devnet" rel="noopener noreferrer"&gt;https://explorer.solana.com/address/6xKXthNFn8YdaPVqdrAJWDfaAxeNeBX5MZuUSaVxd7mL?cluster=devnet&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Mubaraqq/mlh_100daysofsolana/tree/main/day-99" rel="noopener noreferrer"&gt;github.com/Mubaraqq/mlh_100daysofsolana/tree/main/day-99&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The capstone pulls from everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Anchor program with a PDA whose seeds encode the rule&lt;/li&gt;
&lt;li&gt;Custom errors and a runtime constraint&lt;/li&gt;
&lt;li&gt;Happy-path and failure-path tests&lt;/li&gt;
&lt;li&gt;A TypeScript script that calls &lt;code&gt;ship&lt;/code&gt; on devnet&lt;/li&gt;
&lt;li&gt;A read-only agent tool that fetches any wallet's ShipRecord&lt;/li&gt;
&lt;li&gt;A 1-of-1 Token-2022 badge with metadata embedded in the mint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small. Complete. Deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another developer
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The toolchain is not the same as the protocol.&lt;/strong&gt; You can learn Solana primitives and still not know how to build on Solana. Anchor, PDAs, and IDLs are what make the primitives usable. Don't skip them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account design is the hard part.&lt;/strong&gt; Anyone can write an instruction. The interesting decisions are in the accounts: what state you store, how you derive the PDAs, what the seeds encode. A good PDA design can replace a dozen runtime checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web2 instincts help, then they get in the way.&lt;/strong&gt; The instinct to check state before writing is fine. The instinct to assume you can fix a mistake after it's made is not. On Solana, the code and the state are the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;I want to build systems, not demos.&lt;/p&gt;

&lt;p&gt;The vault program showed me I could. The capstone showed me the pieces fit together. The agent work showed me there's a whole layer of tooling being built right now that most developers haven't touched yet.&lt;/p&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go deeper on agent tooling, since the harness layer is where the interesting engineering is right now&lt;/li&gt;
&lt;li&gt;Contribute to open source, starting with the tools I used&lt;/li&gt;
&lt;li&gt;Help other developers starting behind me, since I moderated communities before I wrote code and I still want to be useful there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One hundred days ago, Solana was unfamiliar territory. Today I have a program live on devnet that anyone can look up, call, and verify.&lt;/p&gt;

&lt;p&gt;More importantly, I can explain why it works.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Day 100 of #100DaysOfSolana.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>anchor</category>
      <category>agents</category>
    </item>
    <item>
      <title>From Prompt to Policy: How I Made a Solana Agent Safe Enough to Run Unattended</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:04:31 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/from-prompt-to-policy-how-i-made-a-solana-agent-safe-enough-to-run-unattended-1kd2</link>
      <guid>https://dev.to/mubaraqabba/from-prompt-to-policy-how-i-made-a-solana-agent-safe-enough-to-run-unattended-1kd2</guid>
      <description>&lt;p&gt;Over five days I built an autonomous agent that manages Solana devnet wallets. It reads balances, moves SOL, and chases a plain-English goal without me telling it the steps.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that it works. It's that I trust it enough to let it run.&lt;/p&gt;

&lt;p&gt;And the reason I trust it has almost nothing to do with the model.&lt;/p&gt;

&lt;p&gt;This is the manual for that system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You give it a goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure the savings wallet holds at least 0.2 SOL. Check balances before moving anything, move only what is needed from the operating wallet, and verify the final balances before you finish.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No recipient. No amount. No steps.&lt;/p&gt;

&lt;p&gt;The agent reads both balances, does the math, decides if a transfer is needed, runs it through a policy layer, verifies, and reports back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inventory
&lt;/h2&gt;

&lt;p&gt;Five moving parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Agent loop&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File: &lt;code&gt;agent-workflow.mjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Day: 96 (evolved from Days 93 and 95)&lt;/li&gt;
&lt;li&gt;Job: Calls the model, reads its tool requests, runs them, feeds results back, repeats until the model stops or hits the turn limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Balance tool — &lt;code&gt;get_balance&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File: inside &lt;code&gt;agent-workflow.mjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Day: 92 (originally), folded into Day 96&lt;/li&gt;
&lt;li&gt;Job: Reads the lamport balance of any devnet account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Transfer tool — &lt;code&gt;transfer_sol&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File: inside &lt;code&gt;agent-workflow.mjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Day: 93 (as &lt;code&gt;send_sol&lt;/code&gt;), renamed for Day 96&lt;/li&gt;
&lt;li&gt;Job: Moves SOL from the operating wallet to a recipient, but only after the policy approves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. MCP server&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File: &lt;code&gt;day-94/solana-mcp-server/server.ts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Day: 94&lt;/li&gt;
&lt;li&gt;Job: Exposes a vault program's tools to any MCP client over stdio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Policy engine&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File: &lt;code&gt;day-95/solana-policy-agent/policy.mjs&lt;/code&gt; and inline in &lt;code&gt;day-96/agent-workflow.mjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Day: 95 (as a module), 96 (inlined)&lt;/li&gt;
&lt;li&gt;Job: Deny-by-default check on every transfer. Allowlist, per-transfer cap, per-run cap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&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
|  agent-workflow.mjs       |  runs until the model stops or MAX_TURNS
+---------------------------+
        |
        v  tool call, e.g. transfer_sol
+---------------------------+
|  tool dispatcher          |  routes the call to the right handler
|  runTool()                |  get_balance or transfer_sol
+---------------------------+
        |
        v
+---------------------------+
|  policy engine            |  deny by default; every spend checked
|  checkPolicy()            |  allowlist, per-transfer cap, per-run cap
+---------------------------+
        |  allowed
        v
+---------------------------+
|  Solana devnet            |  transaction submitted and confirmed
+---------------------------+
        |
        v
   every event logged to run-log.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server from Day 94 is a sibling stack. It exposes the vault program to external clients. It does not sit on this flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; &lt;code&gt;address&lt;/code&gt; (string) — Base58 account address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; &lt;code&gt;{ address, lamports }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects:&lt;/strong&gt; none (read-only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded by policy:&lt;/strong&gt; no&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; &lt;code&gt;to&lt;/code&gt; (string), &lt;code&gt;lamports&lt;/code&gt; (number)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; one of three statuses:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{ status: "confirmed", signature }&lt;/code&gt; — it landed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{ status: "denied", reason }&lt;/code&gt; — policy refused it, nothing signed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{ status: "failed", reason }&lt;/code&gt; — policy allowed it, but the send failed on chain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects:&lt;/strong&gt; spends lamports from the operating wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded by policy:&lt;/strong&gt; yes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That three-way return is on purpose. A denial is not an error. It's a normal result the agent should reason about. The tool description says it outright:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A denial is not an error: adjust your plan or stop."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line is the contract.&lt;/p&gt;

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

&lt;p&gt;This is the heart of it. The part that makes the agent safe. The one piece that never varies between runs.&lt;/p&gt;

&lt;p&gt;Deny by default. &lt;code&gt;checkPolicy()&lt;/code&gt; returns &lt;code&gt;{ allowed, reason }&lt;/code&gt;. Every rule is a reason to say no. The function only returns &lt;code&gt;allowed: true&lt;/code&gt; when it runs out of objections.&lt;/p&gt;

&lt;p&gt;Three rules, checked in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Allowlist.&lt;/strong&gt; Only addresses in &lt;code&gt;POLICY.allowedRecipients&lt;/code&gt; can receive funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Per-transfer cap.&lt;/strong&gt; No single transfer over &lt;code&gt;POLICY.maxLamportsPerTransfer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Per-run cap.&lt;/strong&gt; Total spend across the run cannot exceed &lt;code&gt;POLICY.maxLamportsPerRun&lt;/code&gt;. Tracked in &lt;code&gt;spentThisRun&lt;/code&gt;, which adds up after each confirmed send.&lt;/p&gt;

&lt;p&gt;The default when no rule matches: deny. There's no allow branch unless all three pass.&lt;/p&gt;

&lt;p&gt;A denial looks like this to the model:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200000000 lamports exceeds the per-transfer cap of 50000000"&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;The model sees a normal tool result. Nothing crashes. Nothing signs.&lt;/p&gt;

&lt;p&gt;The core invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The prompt can change. The model can change. The tool-call sequence can change.&lt;br&gt;
No transaction moves funds without passing this layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The signing function is only called after &lt;code&gt;checkPolicy()&lt;/code&gt; returns &lt;code&gt;allowed: true&lt;/code&gt;. There is no code path where a denied transfer touches the keypair.&lt;/p&gt;

&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;p&gt;The per-transfer cap is a rate limit, not a spend limit. I proved that in Trial 2 below.&lt;/p&gt;

&lt;p&gt;The policy module is portable. Day 95 built it as a standalone &lt;code&gt;policy.mjs&lt;/code&gt;. Day 96 inlined it. Same logic. It knows nothing about AI, prompts, or models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotated run — Trial 1, goal met
&lt;/h2&gt;

&lt;p&gt;Goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure the savings wallet holds at least 0.2 SOL.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_call&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"get_balance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ddfz...c6cN"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;savings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wallet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;first.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Doesn't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;know&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;balance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;yet.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_result&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Empty.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Shortfall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;full&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SOL.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_call&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"get_balance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"9Sxx...dTf1"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;operating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wallet,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;confirm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;transfer.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_result&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4441970000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.44&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SOL.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Enough.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;valid.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_call&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"transfer_sol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ddfz...c6cN"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Computes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shortfall:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SOL.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;policy_check&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"allowed"&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;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"within policy"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;three&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rules&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pass.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Allowlist:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;yes.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Per-transfer:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;≤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Per-run:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;≤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_result&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2mGQ...wisv"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Signed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;confirmed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;devnet.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;signature.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_call&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"get_balance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ddfz...c6cN"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Verifies&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;savings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wallet,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;goal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;instructed.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_result&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200000000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Exactly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SOL.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Goal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;met.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_call&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"get_balance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"9Sxx...dTf1"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Verifies&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;operating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wallet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;too.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_result&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"lamports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4241965000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Dropped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exactly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Nothing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;extra&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;moved.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;final_report&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"The savings wallet now holds 200,000,000 lamports (0.2 SOL)..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Honest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;report.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Matches&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;actually&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;happened&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;chain.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6 turns. 5 tool calls. 1 confirmed transfer. 0 denials. Goal met exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotated run — Trial 2, policy denial
&lt;/h2&gt;

&lt;p&gt;Goal changed to 0.4 SOL. Per-transfer cap tightened to 0.05 SOL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[turn 3] tool_call      {"tool":"transfer_sol","input":{"lamports":200000000,"to":"Ddfz...c6cN"}}
# Tries to move the full 0.2 SOL in one transfer.

[turn 3] policy_check   {"allowed":false,"reason":"200000000 lamports exceeds the per-transfer cap of 50000000"}
# Refused. 0.2 &amp;gt; 0.05. Nothing signed.

[turn 3] tool_result    {"status":"denied","reason":"..."}
# Model gets the denial as a normal tool result.

[turn 4] tool_call      {"tool":"transfer_sol","input":{"lamports":50000000,"to":"Ddfz...c6cN"}}
# Adjusts. Drops to the cap: 0.05 SOL.

[turn 4] tool_result    {"status":"confirmed","signature":"..."}
# Allowed. Signed.

[turn 5, 6, 7] ...three more 0.05 transfers, all confirmed
# Split the work into pieces.

[turn 12] ...hit MAX_TURNS before writing a final report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12 turns. Hit the limit. Overshot to 0.45 SOL instead of 0.4.&lt;/p&gt;

&lt;p&gt;The finding: the per-transfer cap did not stop the agent. It slowed it down. The agent found a path around it — four small transfers instead of one big one. Only the per-run cap bounds total spend.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. The policy layer is the heart of the system, not the model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything else is replaceable. The model can change. I went from a local 3B to a 120B on Groq. The prompt can change. The tool-call sequence can change, and it does, run to run. The policy module doesn't care about any of that. It takes a recipient and an amount and returns a decision. Same rules every time. That's the piece you actually trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MAX_TURNS doesn't care about your goal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Trial 2, the agent hit the turn limit mid-plan. It had no way to know it was about to be cut off. It just stopped. No final report, no summary, no "here's where I got to." A turn limit is a hard stop, not a graceful one. In production, you'd want the last turn reserved for a forced report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A per-transfer cap is a rate limit, not a spend limit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I thought capping transfers at 0.05 SOL would stop the agent from moving 0.2. It didn't. It split the work into four transfers and moved the same amount. The cap changed how the agent spent, not whether it spent. Only the per-run cap bounds total spend. Without it, the per-transfer cap just slows the drain down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Confirmation lag is real.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model read the savings balance while one transfer was still confirming. Saw less than expected. Sent another. Overshot by 0.05. The model has no built-in sense that a recent write might not have settled yet. Unless you tell it, it treats every read as final.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Non-determinism isn't a bug, it's the environment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I ran the same goal twice in Trial 1. First run: 6 turns, 1 transfer. Second run: 3 turns, 0 transfers — because the goal was already met. Same code, same model, different behavior, both correct. That's not something you fix. It's something you design around. The policy holds regardless of which path the model takes. That's the whole argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open questions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Should the policy layer persist &lt;code&gt;spentThisRun&lt;/code&gt; across runs, instead of resetting each time?&lt;/li&gt;
&lt;li&gt;Should &lt;code&gt;transfer_sol&lt;/code&gt; wait for finalization rather than confirmation before returning?&lt;/li&gt;
&lt;li&gt;What happens when the agent needs to move funds to a recipient that isn't the savings wallet? The allowlist is one address today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the edges. They're where the next version starts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of #100DaysOfSolana. Days 92–96 built this stack. Day 97 documented it.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>100daysofsolana</category>
      <category>anchor</category>
      <category>webdev</category>
      <category>web3</category>
    </item>
    <item>
      <title>My Solana Program Launch Checklist</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:03:10 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/my-solana-program-launch-checklist-55ci</link>
      <guid>https://dev.to/mubaraqabba/my-solana-program-launch-checklist-55ci</guid>
      <description>&lt;p&gt;Just shipped my first Solana program to mainnet. Took notes the whole way.&lt;/p&gt;

&lt;p&gt;But I know how this goes. Three weeks from now, I'll stare at a terminal and forget half of it.&lt;/p&gt;

&lt;p&gt;Here's the checklist I wish I had.&lt;/p&gt;

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

&lt;p&gt;Catch problems while they're cheap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every test passes against your final build.&lt;/li&gt;
&lt;li&gt;You've run the program end-to-end on devnet exactly as it will run on mainnet.&lt;/li&gt;
&lt;li&gt;You produced a verifiable build with &lt;code&gt;anchor build --verifiable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your deploy wallet holds enough SOL for the rent-exempt minimum plus fees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; Once you have a verifiable build, don't overwrite it with a plain &lt;code&gt;anchor build&lt;/code&gt; or &lt;code&gt;cargo build-sbf&lt;/code&gt;. Those produce a different hash and break verification later.&lt;/p&gt;

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

&lt;p&gt;This is the irreversible phase. Slow down here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch your CLI to mainnet-beta with &lt;code&gt;solana config set --url mainnet-beta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm with &lt;code&gt;solana config get&lt;/code&gt; that you're pointing at the right cluster.&lt;/li&gt;
&lt;li&gt;Run your deploy command.&lt;/li&gt;
&lt;li&gt;If the last attempt struggled to land, add a priority fee with &lt;code&gt;--with-compute-unit-price&lt;/code&gt; and route through a reliable RPC with &lt;code&gt;--use-rpc&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; A deploy is not atomic. If it's interrupted, you may be left with a buffer account holding your SOL. Check for stranded buffers and recover:&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 &lt;span class="nt"&gt;--buffers&lt;/span&gt;     &lt;span class="c"&gt;# list stranded buffers&lt;/span&gt;
solana program deploy &amp;lt;SO_PATH&amp;gt; &lt;span class="nt"&gt;--buffer&lt;/span&gt; &amp;lt;BUFFER_KEYPAIR&amp;gt; &lt;span class="c"&gt;#resume a stalled deploy&lt;/span&gt;
solana program close &amp;lt;BUFFER_ADDRESS  &lt;span class="c"&gt;#close and reclaim the rent&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 3: Authority and verification
&lt;/h2&gt;

&lt;p&gt;Right after deploy. Confirm what landed and decide who controls it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;solana program show &amp;lt;PROGRAM_ID&amp;gt;&lt;/code&gt; and read back the upgrade authority. Is it what you intended?&lt;/li&gt;
&lt;li&gt;Decide who holds the upgrade authority: a single keypair you control, a Squads multisig, or &lt;code&gt;--final&lt;/code&gt; for permanent immutability. Write down which one and why.&lt;/li&gt;
&lt;li&gt;Publish your IDL on-chain with &lt;code&gt;anchor idl init&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Regenerate your Codama client from the published IDL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; Skipping the IDL publish causes build verification to fail. Do it in the same session as the deploy.&lt;/p&gt;

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

&lt;p&gt;The last mile. Make sure a real human can use it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Point your React frontend at the mainnet program ID.&lt;/li&gt;
&lt;li&gt;Confirm the Wallet Standard connection surfaces real wallets against mainnet.&lt;/li&gt;
&lt;li&gt;Confirm every error your frontend can throw still produces a calm, legible message now that the stakes are real: rejected approval, insufficient funds, expired blockhash.&lt;/li&gt;
&lt;li&gt;Announce the launch.&lt;/li&gt;
&lt;li&gt;Write down where users report problems.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;One thing surprised me most. The deploy itself was the easy part. The hard part was deciding who holds the upgrade authority and what to do with it.&lt;/p&gt;

&lt;p&gt;A single key on your laptop can replace your program. That's the biggest risk in the whole launch.&lt;/p&gt;

&lt;p&gt;A multisig distributes the key. But the real protection is a timelock plus a verifiable build. The timelock gives users time to see an upgrade coming and exit if they don't like it. The verifiable build lets anyone confirm the on-chain bytecode matches your source.&lt;/p&gt;

&lt;p&gt;Signer count is secondary. Transparency and delay are what actually protect users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://solana.com/docs/programs/deploying" rel="noopener noreferrer"&gt;Deploying Programs&lt;/a&gt; — the official guide to deployment, buffer accounts, and recovery&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anchor-lang.com/docs/verifiable-builds" rel="noopener noreferrer"&gt;Anchor Verifiable Builds&lt;/a&gt; — how to produce a build whose on-chain bytecode matches your source&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://squads.so/blog/managing-program-upgrades" rel="noopener noreferrer"&gt;Managing Program Upgrades with Multisig&lt;/a&gt; — Squads on why and how to secure upgrade authority&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://solana.com/developers/evm-to-svm/production" rel="noopener noreferrer"&gt;Solana Production Readiness&lt;/a&gt; — RPC reliability and production considerations&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>webdev</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>My Solana Program Security Checklist</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:09:15 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/my-solana-program-security-checklist-3jle</link>
      <guid>https://dev.to/mubaraqabba/my-solana-program-security-checklist-3jle</guid>
      <description>&lt;p&gt;A pre-deploy checklist I run top to bottom before any Anchor program goes to mainnet. Built from the bugs I reproduced myself.&lt;/p&gt;

&lt;p&gt;This checklist is for Anchor developers about to deploy to mainnet. Run it top to bottom before every deploy. It's built from the bugs I reproduced myself over the last few days: forged accounts, arithmetic overflows, and CPI trust failures. Each item is a yes-or-no check. If you can't answer yes, stop and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Account Validation&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every deserialized account has its owner verified. Typed &lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; does this automatically. Raw &lt;code&gt;AccountInfo&lt;/code&gt; or &lt;code&gt;UncheckedAccount&lt;/code&gt; does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Account types are distinguished by their 8-byte discriminator, so one account type cannot be passed where another is expected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any &lt;code&gt;remaining_accounts&lt;/code&gt; passed in are validated before use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any PDA is verified with &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump,&lt;/code&gt; not just accepted by address.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; In February 2022, $326M left the Wormhole bridge because the program trusted a forged account without verifying it was the real one. The fix was to check the owner before reading the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Authority and Signer Checks&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every privileged instruction confirms the expected signer with &lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; or &lt;code&gt;has_one / constraint&lt;/code&gt; checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No instruction trusts a pubkey without also checking its signer flag. Comparing a public key only proves someone knew a public key. Public keys are public.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;UncheckedAccount&lt;/code&gt; for an authority is a red flag. If you see it, you have no signer check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Admin-only instructions check the admin against stored state AND require the signer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The vault's owner is set on first deposit and never silently overwritten.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Comparing a public key without checking the signer flag lets an attacker pass any public key. The instruction must require a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Arithmetic Safety&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every balance or supply change uses &lt;code&gt;checked_add&lt;/code&gt;, &lt;code&gt;checked_sub&lt;/code&gt;, or &lt;code&gt;checked_mul&lt;/code&gt;, never raw &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;*&lt;/code&gt; on untrusted values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No silent cast that could truncate (e.g., &lt;code&gt;u64&lt;/code&gt; to &lt;code&gt;u32&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overflow and underflow return a named error, not a panic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Property tests prove that a deposit never shrinks a balance and a withdrawal never increases it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; A &lt;code&gt;u64&lt;/code&gt; that overflows wraps silently to a tiny number. Without checked math, a deposit of 2 lamports into a balance one lamport below &lt;code&gt;u64::MAX&lt;/code&gt; wraps the balance back to 1. The attacker destroys the vault with a tiny deposit.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. CPI Safety&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every CPI verifies the target program ID is the one expected, not whatever account the caller supplied.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Program&amp;lt;'info, T&amp;gt;&lt;/code&gt; is used where possible, so Anchor checks the program ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Accounts are reloaded after a CPI if their data is read again. The callee may have changed the account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signer seeds used for PDA-signed CPIs match the seeds in the &lt;code&gt;#[account(seeds = ...)]&lt;/code&gt; constraint exactly. One byte off and the runtime rejects the PDA.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No CPI trusts the callee to validate its own accounts. The caller is responsible for passing the right ones.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; A CPI that accepts whatever program ID the caller supplies can be redirected to an attacker's program. The attacker's program returns success and the caller believes the work was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Account Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Closed accounts are emptied and marked so they cannot be revived or reused within the same transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No instruction allows reinitializing an already-initialized account. &lt;code&gt;init&lt;/code&gt; fails on existing accounts. &lt;code&gt;init_if_needed&lt;/code&gt; succeeds silently. Use it deliberately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Closing an account returns the rent to the correct wallet. Never to an arbitrary signer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;close = authority&lt;/code&gt; is used, not manual lamport draining.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; An account that is closed but not marked can be reinitialized and reused. An account that is reinitialized loses its old state and can be repurposed by an attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. Pre-Deploy Hygiene&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cargo audit&lt;/code&gt; has been run and reports no known advisories in dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adversarial tests pass, not just the happy path. Every defense has a test that proves it works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Property tests pass. Rules like "a deposit never shrinks a balance" hold for random inputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fuzz tests pass. Trident has run the program through thousands of random instruction sequences without a panic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;anchor keys sync&lt;/code&gt; has been run. The program ID in &lt;code&gt;lib.rs&lt;/code&gt; matches &lt;code&gt;Anchor.toml&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;overflow-checks = true&lt;/code&gt; flag is set under &lt;code&gt;[profile.release]&lt;/code&gt; in the workspace root &lt;code&gt;Cargo.toml&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;grep -rn "UncheckedAccount\|AccountInfo\|/// CHECK" programs/*/src&lt;/code&gt; returns only accounts you have manually verified.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The best defense is a test that proves it works. A constraint you cannot see fail is a constraint you are only hoping is there. Run the checklist before every deploy, not just the first one.&lt;/p&gt;

&lt;p&gt;This checklist is a living document. Run it before every mainnet deploy, top to bottom. If you find something it misses, open an issue or reach out. Security is a community effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This post draws from Days 75-82 of #100DaysOfSolana.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rust</category>
      <category>webdev</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>CPIs on Solana: From Confusion to Confidence</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:29:14 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/cpis-on-solana-from-confusion-to-confidence-3p2g</link>
      <guid>https://dev.to/mubaraqabba/cpis-on-solana-from-confusion-to-confidence-3p2g</guid>
      <description>&lt;p&gt;On Day 71, I thought CPI was a magic black box. I knew it stood for Cross-Program Invocation, but I didn't understand why the runtime needed a program ID, accounts, and sometimes signer seeds. After working through this arc, I realized the pattern is always the same. The program ID says which program to call, the accounts say what data to read or write, and the signer seeds say who's authorized. Whether you're moving SOL through the System Program, minting tokens through Token-2022, or calling your own counter program, the CpiContext shape never changes.&lt;/p&gt;

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

&lt;p&gt;A CPI is how one program calls another. Think of it like a function call, but with three explicit pieces.&lt;/p&gt;

&lt;p&gt;The program ID tells the runtime which program you're calling. This is like a function name in a library.&lt;/p&gt;

&lt;p&gt;The accounts are the data the callee needs. This is like passing arguments, but the arguments are accounts on-chain.&lt;/p&gt;

&lt;p&gt;The signer authority is who is authorizing this call. This is either a real wallet (the signature flows through) or a PDA (the program signs with seeds).&lt;/p&gt;

&lt;p&gt;The same CpiContext struct bundles all three. You build it once and pass it to the helper function. The runtime handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A real code snippet&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the smallest CPI I wrote this week. The caller program calls the counter program's &lt;em&gt;increment&lt;/em&gt; instruction.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument is the program ID. &lt;code&gt;counter_program.key()&lt;/code&gt; tells the runtime which program to call.&lt;/p&gt;

&lt;p&gt;The second argument is the accounts struct. &lt;code&gt;Increment { tally: ... }&lt;/code&gt; provides the account the counter program needs.&lt;/p&gt;

&lt;p&gt;The signer authority is implied here. The user signed the outer transaction, and that signature flows through to the CPI automatically.&lt;/p&gt;

&lt;p&gt;If the signer was a PDA, I would need &lt;code&gt;.with_signer(signer_seeds)&lt;/code&gt; to prove the program owns the PDA. But for a regular wallet, the signature just works.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cpi::increment&lt;/code&gt; helper does the actual invocation. If the counter program's increment fails, this function returns an error and the entire transaction rolls back.&lt;/p&gt;

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

&lt;p&gt;The error that tripped me up most was &lt;code&gt;ConstraintSeeds&lt;/code&gt;. I changed one byte in the signer seeds — from &lt;code&gt;b"vault"&lt;/code&gt; to &lt;code&gt;b"voult"&lt;/code&gt; — and the transaction failed with:&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: AnchorError caused by account: vault. Error Code: ConstraintSeeds.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error meant the runtime re-derived the PDA address from my seeds and got a different address than the account I passed. The seeds must match exactly. One byte off and the runtime refuses to sign.&lt;/p&gt;

&lt;p&gt;The fix was simple: match the seeds in the CPI to the seeds in the &lt;code&gt;#[account(seeds = ...)]&lt;/code&gt; constraint. If they match, the runtime treats the PDA as a signer. If they don't, you get &lt;code&gt;ConstraintSeeds&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CPIs are how Solana programs compose. The pattern is always the same: program ID, accounts, signer. Whether you're calling the System Program, Token-2022, or your own code, the &lt;code&gt;CpiContext&lt;/code&gt; shape doesn't change.&lt;/p&gt;

&lt;p&gt;If you're stuck, check the three pieces. The program ID tells the runtime where to go. The accounts tell it what data to touch. The signer tells it who authorized the call. When all three line up, the CPI works.&lt;/p&gt;

&lt;p&gt;This post draws from Days 71-75 of #100DaysOfSolana.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Understanding Solana PDAs: A Web2 Developer's Guide</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 09 Aug 2026 17:00:10 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/understanding-solana-pdas-a-web2-developers-guide-12b8</link>
      <guid>https://dev.to/mubaraqabba/understanding-solana-pdas-a-web2-developers-guide-12b8</guid>
      <description>&lt;p&gt;On Solana, programs are stateless. If your program needs to remember something per user, per game, or per configuration, it needs a deterministic address it can find again later without storing it anywhere. PDAs are that address.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a PDA actually is&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In Web2, a database primary key is computed from a row's logical identity — user_id, order_id, something the system already knows. You never look up a row by a random UUID you can't derive. PDAs work the same way, except the database is the entire Solana account model, the key derivation is a hash, and the program ID is baked in so only your program can sign for it.&lt;/p&gt;

&lt;p&gt;Unlike a primary key, a PDA is not stored in a table. It's derived on demand and may or may not have an account at that address yet. The program doesn't know if the account exists until it tries to read it. That's why init and init_if_needed exist.&lt;/p&gt;

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

&lt;p&gt;Here's the canonical pattern from my counter program:&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;The &lt;code&gt;seeds&lt;/code&gt; array is the core of PDA derivation. It contains two parts: a static seed prefix &lt;code&gt;b"counter"&lt;/code&gt; and a dynamic seed &lt;code&gt;user.key().as_ref()&lt;/code&gt;. The static seed creates a namespace — all counters share this prefix. The dynamic seed separates one user's counter from another's.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bump&lt;/code&gt; is the byte that makes the address off-curve. Anchor computes it during derivation and stores it on the account. The &lt;code&gt;init&lt;/code&gt; constraint tells Anchor to create the account at the derived address. &lt;code&gt;payer = user&lt;/code&gt; says the user pays the rent. &lt;code&gt;space&lt;/code&gt; reserves enough bytes for the account data.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;seeds&lt;/code&gt; you choose determine who can access what. In my counter program, I used &lt;code&gt;[b"counter", user.key().as_ref()]&lt;/code&gt;. This gives every wallet its own PDA. If I had used only &lt;code&gt;[b"counter"]&lt;/code&gt;, every wallet would get the same PDA — which is useful for a global config but disastrous for a per-user counter.&lt;/p&gt;

&lt;p&gt;I tested this on Day 68. The per-user derivation gave different addresses for different wallets. The global derivation gave the same address for every wallet. Same program ID, same derivation function, different seeds.&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: 3ZQj5KhoHaoDb9g1LoFpDZ5tAxMwA4ZumnUzrxmMPFfg
  Wallet B PDA: DgfkXgoFoHDRUGKiHvduDHrjuofRJKoFyJ13SqWqMgFs
  Same address? false

Global counter PDA (no wallet in seeds):
  Derived from A: 2ESHd2ZG3Yjh3kUZdSLGhweaVPfLaqs1qqqF4Hgh4Ndn
  Derived from B: 2ESHd2ZG3Yjh3kUZdSLGhweaVPfLaqs1qqqF4Hgh4Ndn
  Same address? true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The canonical bump is the first bump value that produces an off-curve address. &lt;code&gt;find_program_address&lt;/code&gt; returns it along with the PDA. Anchor stores it for you when you write &lt;code&gt;bump&lt;/code&gt; in the constraint.&lt;/p&gt;

&lt;p&gt;You should re-pass the stored bump on subsequent instructions rather than re-deriving every time. Re-derivation is expensive (it hashes the seeds with 256 possible bump values). Storing the bump is free — it's just one byte on the account.&lt;/p&gt;

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

&lt;p&gt;Over the course of this arc, I walked through the entire lifecycle of a PDA account:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address — Use &lt;code&gt;find_program_address&lt;/code&gt; with seeds and program ID. No account exists yet, just the address.&lt;/li&gt;
&lt;li&gt;Initialize the account — Use &lt;code&gt;init&lt;/code&gt; in the accounts struct. This creates the account at the PDA, pays rent from the user, and stores the bump.&lt;/li&gt;
&lt;li&gt;Mutate the data — Call instructions like &lt;code&gt;increment&lt;/code&gt;. The seeds constraint re-derives the address and verifies it matches the passed account.&lt;/li&gt;
&lt;li&gt;Close the account — Use &lt;code&gt;close = user&lt;/code&gt; to drain lamports back to the user and mark the account as closed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Closing is not "delete from a table." It's zero the data, transfer the lamports, and mark it for garbage collection at the end of the transaction. The runtime removes it from the next slot's account state.&lt;/p&gt;

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

&lt;p&gt;A few things I wish I had known on Day 64:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The program ID is part of the derivation. The same seeds in a different program produce a different address. This is why PDAs are program-specific.&lt;/li&gt;
&lt;li&gt;PDAs cannot sign transactions on their own. Only programs can sign on their behalf using the same seeds. The program signs for the PDA, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;init_if_needed&lt;/code&gt; is convenient and also a footgun. Use it deliberately when you want idempotent creation, not by default. If you use it everywhere, you lose the ability to know whether an account was just created or already existed.&lt;/li&gt;
&lt;li&gt;The bump is not optional. Without it, you can't re-derive the address. Store it on the account and pass it back on every instruction that uses the PDA.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;PDAs are not complicated. They are deterministic addresses derived from seeds, a program ID, and a bump. They replace keypair tracking and let your program own state without a private key. The pattern is always the same: derive, initialize, mutate, close.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://solana.com/docs/core/pda" rel="noopener noreferrer"&gt;Program Derived Addresses &lt;/a&gt; — the official Solana docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anchor-lang.com/docs/pdas" rel="noopener noreferrer"&gt;PDAs in Anchor&lt;/a&gt; — Anchor's guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.rs/anchor-lang/latest/anchor_lang/" rel="noopener noreferrer"&gt;Anchor crate docs&lt;/a&gt; — the source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My code is on &lt;a href="https://github.com/Mubaraqq/mlh_100daysofsolana/tree/main" rel="noopener noreferrer"&gt;GitHub.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My First Anchor Program: A Counter with Tests That Actually Catch Bugs</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:33:46 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/my-first-anchor-program-a-counter-with-tests-that-actually-catch-bugs-10jl</link>
      <guid>https://dev.to/mubaraqabba/my-first-anchor-program-a-counter-with-tests-that-actually-catch-bugs-10jl</guid>
      <description>&lt;p&gt;I spent 3 days building a counter program on Solana. It does one thing — count. But the real value wasn't the program. It was the tests that proved it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The accounts struct:&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;#[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(&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;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="nd"&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;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;This struct defines what accounts the initialize instruction needs. The &lt;code&gt;counter&lt;/code&gt; account is being created (&lt;code&gt;init&lt;/code&gt;), the &lt;code&gt;authority&lt;/code&gt; pays for it (&lt;code&gt;payer&lt;/code&gt;) and becomes its owner, and the &lt;code&gt;system_program&lt;/code&gt; is required because creating accounts is a System Program operation. The &lt;code&gt;8 + Counter::INIT_SPACE&lt;/code&gt; is 8 bytes for Anchor's discriminator plus the size of the Counter data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The initialize handler:&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;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;The handler is short. &lt;code&gt;ctx.accounts&lt;/code&gt; gives you typed access to the accounts passed in. You set the authority to the wallet that signed the transaction, set count to 0 and return success. Anchor handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The increment handler:&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;ProgramError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArithmeticOverflow&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 increment handler adds 1 to the count. The &lt;code&gt;checked_add&lt;/code&gt; prevents overflow — if the count hits u64 max, the transaction fails cleanly instead of panicking. The constraint in the accounts struct guarantees only the authority can call it.&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;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 &lt;code&gt;has_one = authority&lt;/code&gt; constraint is where the security lives. Before the handler runs, Anchor checks that &lt;code&gt;counter.authority&lt;/code&gt; matches the &lt;code&gt;authority&lt;/code&gt; signing the transaction. If it doesn't, the transaction fails before your code executes. Declarative authorization — checked at the protocol level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The happy-path test:&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;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize_then_increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LiteSVM&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;program_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&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;so_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;concat!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_MANIFEST_DIR"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"/../../target/deploy/counter.so"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.add_program_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;so_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;authority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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;svm&lt;/span&gt;&lt;span class="nf"&gt;.airdrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&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;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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_kp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;init_ix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instruction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt; &lt;span class="p"&gt;{&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;counter_kp&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&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;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;.to_account_metas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="nf"&gt;.data&lt;/span&gt;&lt;span class="p"&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_signed_with_payer&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;init_ix&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&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;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="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;counter_kp&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.latest_blockhash&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;inc_ix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instruction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;accounts&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;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter_kp&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&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;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;.to_account_metas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;instruction&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="nf"&gt;.data&lt;/span&gt;&lt;span class="p"&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_signed_with_payer&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;inc_ix&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&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;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="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.latest_blockhash&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.get_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;counter_kp&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_deserialize&lt;/span&gt;&lt;span class="p"&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;account&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="nf"&gt;.as_slice&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;.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;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test proves the program works: initialize creates a counter with count 0, increment changes it to 1, and the final state is exactly what we expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure test:&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;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;increment_fails_when_wrong_authority_signs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setup_svm_with_program&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;authority_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;authority_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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;svm&lt;/span&gt;&lt;span class="nf"&gt;.airdrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;authority_a&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.airdrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;authority_b&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;init_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_initialize_tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;authority_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"initialize should succeed"&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;bad_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_increment_tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;authority_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad_tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test proves the authorization works: authority_a creates the counter, authority_b tries to increment it, and the transaction fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking it on purpose:&lt;/strong&gt;&lt;br&gt;
I planted a bug on purpose. I changed &lt;code&gt;checked_add(1)&lt;/code&gt; to &lt;code&gt;checked_add(2)&lt;/code&gt; in the increment handler. The happy-path test failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assertion `left == right` failed
  left: 2
 right: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the test catching a real regression. Without that assertion, the program would silently store the wrong number and the suite would still be green. That's why the tests matter.&lt;/p&gt;

&lt;p&gt;Next, I'd add more instructions — decrement, reset, maybe a way to transfer ownership. The pattern is the same: define the instruction, define the accounts, write the test, break it, fix it. The counter is just the first step.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Minting NFTs on Solana with Token Extensions</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:04:17 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/what-i-learned-minting-nfts-on-solana-with-token-extensions-3b4k</link>
      <guid>https://dev.to/mubaraqabba/what-i-learned-minting-nfts-on-solana-with-token-extensions-3b4k</guid>
      <description>&lt;p&gt;An NFT on Solana is just a token with supply 1, decimals 0 and metadata. Everything else is optional.&lt;/p&gt;

&lt;p&gt;Before this week, I thought Solana NFTs required Metaplex. Turns out you can mint one with just the Token Extensions program, a GitHub Gist and a few CLI commands.&lt;/p&gt;

&lt;p&gt;On Solana, an NFT is not a special type. It's a token mint with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decimals: 0 (can't be split)&lt;/li&gt;
&lt;li&gt;Supply: 1 (only one exists)&lt;/li&gt;
&lt;li&gt;Metadata extension (name, symbol, image URI)&lt;/li&gt;
&lt;li&gt;Mint authority disabled (can't mint more)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The rest is metadata, collections, and off-chain JSON.&lt;/p&gt;

&lt;p&gt;Everything else you see in wallets and marketplaces—images, attributes, collection pages—is built on top of this foundation.&lt;/p&gt;

&lt;p&gt;Over three days, I minted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A standalone NFT with metadata (name, symbol, image, attributes)&lt;/li&gt;
&lt;li&gt;A collection mint with the Group extension (max size 3)&lt;/li&gt;
&lt;li&gt;Two member NFTs linked to the collection via the Member extension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All on devnet. All with the Token-2022 program. No separate metadata program needed.&lt;/p&gt;

&lt;p&gt;Here's how the collection + member flow works:&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;# Create collection mint with group extension&lt;/span&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-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-group&lt;/span&gt;

&lt;span class="c"&gt;# Initialize group with max size 3&lt;/span&gt;
spl-token initialize-group &amp;lt;COLLECTION_MINT&amp;gt; 3

&lt;span class="c"&gt;# Create member mint with member extension&lt;/span&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-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-member&lt;/span&gt;

&lt;span class="c"&gt;# Link member to collection&lt;/span&gt;
spl-token initialize-member &amp;lt;MEMBER_MINT&amp;gt; &amp;lt;COLLECTION_MINT&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Metadata is mutable by default. The update authority can change name, symbol, URI and custom fields at any time.&lt;/p&gt;

&lt;p&gt;This is useful for fixing errors. But it also means you trust the update authority not to rug you.&lt;/p&gt;

&lt;p&gt;I mutated my NFT live on devnet:&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;# Update name&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; name &lt;span class="s2"&gt;"Field Notes"&lt;/span&gt;

&lt;span class="c"&gt;# Add custom field&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; rarity legendary

&lt;span class="c"&gt;# Remove custom field&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; rarity &lt;span class="nt"&gt;--remove&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On-chain updates are instant. Off-chain image caching is not.&lt;/p&gt;

&lt;p&gt;The name updated immediately in Solana Explorer. The new image may took longer to appear in Phantom. That asymmetry is something you have to design for.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;What I'd build next&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A complete NFT drop with a fixed collection size, automated minting, and on-chain royalties.&lt;/p&gt;

&lt;p&gt;Token Extensions support transfer fees. Royalties can be enforced at the protocol level without a separate marketplace integration. No middleware. No trust. Just math.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;The takeaway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Solana NFTs are not complicated. They're just tokens with a few extensions enabled.&lt;/p&gt;

&lt;p&gt;Metaplex is one way to build them. Token Extensions is another. Both work. One is lighter, more composable, and lives entirely inside the mint account.&lt;/p&gt;

&lt;p&gt;If you're coming from Web2, this might feel unfamiliar. But the mental shift is worth it.&lt;/p&gt;

&lt;p&gt;Try it yourself. Mint one on devnet. Change its name. Add a custom field. Send it to a friend. The tools are all there.&lt;/p&gt;

&lt;p&gt;This post is part of #100DaysOfSolana. Follow along or jump in any day.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&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://solana.com/solutions/token-extensions" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
          

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

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Official Solana documentation on token extensions and their use cases&lt;br&gt;&lt;/p&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://solana.com/docs/tokens/extensions/metadata" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
      &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
      

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


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Technical reference for storing NFT metadata directly on mint accounts &lt;br&gt;&lt;/p&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://solana.com/vi/docs/tokens/extensions/dynamic-metadata-nft" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
      &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
      

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


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Official guide for building dynamic NFTs with on-chain metadata &lt;br&gt;&lt;/p&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__cover"&gt;
      &lt;a href="https://www.metaplex.com/docs/solana/spl-tokens-and-token-programs" class="c-link align-middle" rel="noopener noreferrer"&gt;
        &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.metaplex.com%2Fdocs%2Fapi%2Fog%3Ftitle%3DSPL%2BTokens%2Band%2BToken%2BPrograms%26description%3DLearn%2Bhow%2BSPL%2Btokens%2Bwork%2Bon%2BSolana%252C%2Bincluding%2Bthe%2BToken%2BProgram%252C%2Bmint%2Baccounts%252C%2Btoken%2Baccounts%252C%2Band%2Bhow%2BMetaplex%2Bbuilds%2Bon%2Btop%2Bof%2Bthe%2Btoken%2Bstandard.%26product%3Dsolana" height="630" class="m-0" width="1200"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;div class="c-embed__body"&gt;
    &lt;h2 class="fs-xl lh-tight"&gt;
      &lt;a href="https://www.metaplex.com/docs/solana/spl-tokens-and-token-programs" rel="noopener noreferrer" class="c-link"&gt;
        SPL Tokens and Token Programs | Understanding Solana Tokens
      &lt;/a&gt;
    &lt;/h2&gt;
      &lt;p class="truncate-at-3"&gt;
        Learn how SPL tokens work on Solana, including the Token Program, mint accounts, token accounts, and how Metaplex builds on top of the token standard.
      &lt;/p&gt;
    &lt;div class="color-secondary fs-s flex items-center"&gt;
        &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.metaplex.com%2Fdocs%2Ffavicon.png" width="256" height="256"&gt;
      metaplex.com
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comparison of original SPL Token, Token-2022, and Metaplex standards &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>solana</category>
      <category>nft</category>
      <category>web3dev</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>You Already Understand Token Extensions.</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:29:04 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/you-already-understand-token-extensions-1h6m</link>
      <guid>https://dev.to/mubaraqabba/you-already-understand-token-extensions-1h6m</guid>
      <description>&lt;p&gt;Because you've built payment flows, loyalty systems and credentials in Web2.&lt;/p&gt;

&lt;p&gt;In Web2, charging a 1% transaction fee means building middleware. Verifying a user's identity before they can transact means writing KYC logic. Issuing a credential that can't be traded means designing a database with access controls.&lt;/p&gt;

&lt;p&gt;On Solana, all of this is handled at the protocol level by Token Extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What are Token Extensions?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Token Extensions are optional features you enable when creating a token mint. They're not separate smart contracts. They live in the same account as your token. Once enabled, the runtime enforces them.&lt;/p&gt;

&lt;p&gt;There are two Token Programs on Solana:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original Token Program (Tokenkeg...): Basic mint, transfer, burn&lt;/li&gt;
&lt;li&gt;Token-2022 Program (TokenzQdBN...): Same basics, plus extensions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Extensions I used:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transfer fee&lt;/strong&gt;: Withholds a percentage on every transfer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt;: Stores name, symbol, URI on-chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default frozen&lt;/strong&gt;: Every account starts locked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-transferable&lt;/strong&gt;: Tokens cannot be moved (soulbound)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent delegate&lt;/strong&gt;: Issuer can burn tokens from any wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interest-bearing&lt;/strong&gt;: Display balance grows over time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A real example: Multi-extension token&lt;/strong&gt;&lt;br&gt;
One command, three extensions:&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; 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;This creates a token with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1% transfer fee (capped at 500 units)&lt;/li&gt;
&lt;li&gt;Interest-bearing display balance&lt;/li&gt;
&lt;li&gt;On-chain metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then add metadata so wallets can display your token properly:&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-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"ArcCoin"&lt;/span&gt; &lt;span class="s2"&gt;"ARC"&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/metadata.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extensions cannot be added after mint creation. You have to declare everything upfront. In Web2, you'd add a feature later. On Solana, the account size is fixed at creation. No space for new extensions. Plan before you mint.&lt;/p&gt;

&lt;p&gt;The word "permanent" in permanent delegate. I assumed it meant the address could never change. Tried to change it. It worked. "Permanent" means the authority to burn from any wallet, not an immutable address. Took me a failed assumption to learn that.&lt;/p&gt;

&lt;p&gt;Token Extensions aren't separate smart contracts. They live in the mint account. When you create the mint, you're allocating space for extensions. The runtime handles the rest. No middleware, no backend, no bypass.&lt;/p&gt;

&lt;p&gt;Token Extensions are how Solana handles real-world financial logic at the protocol level. Transfer fees, compliance gates, credentials, yield-bearing assets — all built in. You don't write the logic. You just enable the extension.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, start here:&lt;br&gt;
Token Extensions Official Docs&lt;/p&gt;

&lt;p&gt;Or just create a token on devnet and try one. That's what I did.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>webdev</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Transfer Fees, Metadata, and Soulbound Tokens: A Tour of Solana Token Extensions</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Wed, 17 Jun 2026 17:05:47 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/transfer-fees-metadata-and-soulbound-tokens-a-tour-of-solana-token-extensions-29b6</link>
      <guid>https://dev.to/mubaraqabba/transfer-fees-metadata-and-soulbound-tokens-a-tour-of-solana-token-extensions-29b6</guid>
      <description>&lt;p&gt;In Web2, adding a transfer fee means building middleware. On Solana, it's a flag. I built four tokens in six days. Here's what I learned.&lt;/p&gt;

&lt;p&gt;A few weeks ago, I knew nothing about Solana tokens. I'd used Solana for transfers and read account data, but tokens felt like a black box. I wanted to understand how they actually work, not just copy-paste commands but know what each flag does and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Basic Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My first token was barebones. No name. No symbol. Just an address.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
That's it. One command. A mint account appeared on devnet with a 9-decimal default.&lt;/p&gt;

&lt;p&gt;I created a token account to hold it, then minted 100 tokens to myself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-account &amp;lt;MINT&amp;gt;&lt;br&gt;
spl-token mint &amp;lt;MINT&amp;gt; 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What surprised me: You can't receive tokens directly into your wallet. You need a token account for each token type. One per token, per wallet.&lt;/p&gt;

&lt;p&gt;The mint is the factory. The token account is your bucket.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Giving It an Identity&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A token without metadata is just an address. Phantom would show a random string. No one would know what it is.&lt;/p&gt;

&lt;p&gt;I needed a name, symbol and URI.&lt;/p&gt;

&lt;p&gt;But the original Token Program (Tokenkeg...) doesn't support metadata on the mint itself. You'd need a separate account.&lt;/p&gt;

&lt;p&gt;So I used Token Extensions Program (Token-2022) at TokenzQdBN.... It stores metadata directly on 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;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; 6

spl-token initialize-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"100daysofsol"&lt;/span&gt; &lt;span class="s2"&gt;"100DSOL"&lt;/span&gt; &amp;lt;URI&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One mint. Multiple extensions possible. No separate accounts.&lt;/p&gt;

&lt;p&gt;The URI points to a JSON file with description and image. Wallet UIs read it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Adding Transfer Fees&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In Web2, charging a fee on every transaction means building middleware. Handling edge cases. Making sure no one bypasses it.&lt;/p&gt;

&lt;p&gt;On Solana, it's a flag.&lt;/p&gt;

&lt;p&gt;I used Token-2022 again, this time with --transfer-fee-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;100 basis points = 1%. I set 200 (2%) with a cap of 5000 displayed tokens.&lt;/p&gt;

&lt;p&gt;Then I minted 1000, transferred 100 to a second wallet, and watched the fee get withheld.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token transfer &amp;lt;MINT&amp;gt; 100 &amp;lt;RECIPIENT&amp;gt; --expected-fee 2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Recipient got 98. 2 tokens withheld in their token account. They couldn't touch it.&lt;/p&gt;

&lt;p&gt;Only the withdrawal authority (me) could collect it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token withdraw-withheld-tokens &amp;lt;MY_ACCOUNT&amp;gt; &amp;lt;RECIPIENT_ACCOUNT&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I swept the fee back. Balance went from 900 to 902.&lt;/p&gt;

&lt;p&gt;The fee logic lives in the mint. Every transfer respects it. No application code required.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Soulbound Tokens&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not all tokens should be transferable.&lt;/p&gt;

&lt;p&gt;A course completion certificate. A KYC verification. An employee badge. They're credentials, not currency.&lt;/p&gt;

&lt;p&gt;Token-2022 has an extension for this: --enable-non-transferable.&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;I minted 10 tokens to myself. Then tried to transfer 5 to a second wallet.&lt;/p&gt;

&lt;p&gt;It failed.&lt;br&gt;
&lt;em&gt;Program log: Transfer is disabled for this mint&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The blockchain itself rejected the transaction. No client-side check. No middleware. Protocol-level enforcement.&lt;/p&gt;

&lt;p&gt;But I could still burn them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token burn &amp;lt;ACCOUNT&amp;gt; 3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Balance went from 10 to 7. The holder can destroy tokens but cannot send them away.&lt;/p&gt;

&lt;p&gt;This is how credentials should work on-chain. They prove something about the wallet that holds them. Trading them defeats the purpose.&lt;/p&gt;

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

&lt;p&gt;What surprised me most: the protocol doesn't care about intent. It just enforces rules. When I tried to transfer a non-transferable token, the program rejected it without explanation beyond "Transfer is disabled." No appeal. No admin override. The rules are the rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What's Next&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I've built tokens. Now I'm moving to programs.&lt;/p&gt;

&lt;p&gt;I want to build something that does more than transfer value—something that enforces custom logic on-chain. A game. A marketplace. A DAO.&lt;/p&gt;

&lt;p&gt;Follow along. I'm documenting everything.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>ai</category>
    </item>
    <item>
      <title>Solana Accounts for Web2 Developers (You Already Understand Files)</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:41:13 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/solana-accounts-for-web2-developers-you-already-understand-files-k90</link>
      <guid>https://dev.to/mubaraqabba/solana-accounts-for-web2-developers-you-already-understand-files-k90</guid>
      <description>&lt;p&gt;I spent the past month building on Solana. Sent transfers. Decoded raw bytes. Inspected accounts until my terminal turned into a wall of hex.&lt;/p&gt;

&lt;p&gt;Along the way, I had to unlearn some Web2 habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;One model. No exceptions.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Your wallet is an account. The program that moves your SOL is an account. The token you just bought? Also an account.&lt;/p&gt;

&lt;p&gt;Solana doesn't have special types. Every account has the same five fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lamports (SOL balance, 1 SOL = 1 billion lamports)&lt;/li&gt;
&lt;li&gt;Data (raw bytes. empty for wallets)&lt;/li&gt;
&lt;li&gt;Owner (the program that can modify this account)&lt;/li&gt;
&lt;li&gt;Executable (true if this account holds code)&lt;/li&gt;
&lt;li&gt;Rent epoch (ignore this. it's deprecated)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run solana account on any address. Same structure every time.&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="nv"&gt;$ &lt;/span&gt;solana account 8CtdyqtzBd597eDz9PTZHuuT62vvLc6YXdXjVkHnboqj

Public Key: 8CtdyqtzBd597eDz9PTZHuuT62vvLc6YXdXjVkHnboqj
Balance: 3.93896 SOL
Owner: 11111111111111111111111111111111
Executable: &lt;span class="nb"&gt;false
&lt;/span&gt;Rent Epoch: 18446744073709551615

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Who owns what matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The owner field determines control.&lt;/p&gt;

&lt;p&gt;My wallet is owned by the System Program &lt;em&gt;(111...111)&lt;/em&gt;. Only the System Program can deduct SOL from it. I sign. The program executes.&lt;/p&gt;

&lt;p&gt;A token account is owned by the Token Program. Only that program can move tokens.&lt;/p&gt;

&lt;p&gt;In Web2, your app authorizes changes. On Solana, the program that owns the account authorizes changes. Your signature proves you approved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Programs have no memory&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This was the hardest shift.&lt;/p&gt;

&lt;p&gt;In Node.js, my app holds variables in memory. App and state live together.&lt;/p&gt;

&lt;p&gt;On Solana, a program account holds code. Nothing else. No variables. No state.&lt;/p&gt;

&lt;p&gt;State lives in separate data accounts.&lt;/p&gt;

&lt;p&gt;A program reads from its data accounts, does math, writes back. It never stores anything itself.&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;// In Web2, you store state in variables&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// On Solana, you read from a data account, modify, write back&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterAddress&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataAccount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? Update the program without losing data. One program, many data accounts. Programs stay pure.&lt;/p&gt;

&lt;p&gt;The Token Program is 36 bytes of code. The mint accounts it controls? Separate accounts. Owned by the Token Program. Code in one place. State in another.&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="nv"&gt;$ &lt;/span&gt;solana account TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Public Key: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Owner: BPFLoaderUpgradeab1e11111111111111111111111
Executable: &lt;span class="nb"&gt;true
&lt;/span&gt;Length: 36 bytes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Rent isn't rent&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every account needs a minimum SOL balance based on data size. For a basic wallet with zero data: &lt;em&gt;~0.00089 SOL&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Pay once. Store forever. Close the account, get your SOL back.&lt;/p&gt;

&lt;p&gt;It's a deposit. Not monthly rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;You already know this&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A file has a name, contents, and permissions.&lt;/p&gt;

&lt;p&gt;A Solana account has an address, data, and an owner.&lt;/p&gt;

&lt;p&gt;Same pattern. Different environment.&lt;/p&gt;

&lt;p&gt;Took me a few weeks to stop overcomplicating it.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Hit the 1,232-Byte Wall So You Don't Have To</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 25 May 2026 04:56:21 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/i-hit-the-1232-byte-wall-so-you-dont-have-to-10</link>
      <guid>https://dev.to/mubaraqabba/i-hit-the-1232-byte-wall-so-you-dont-have-to-10</guid>
      <description>&lt;p&gt;I was building a batch transfer tool. Send SOL to 20 recipients in one transaction. Simple, right?&lt;/p&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;p&gt;The RPC rejected my transaction with: Transaction too large: 1400 &amp;gt; 1232&lt;/p&gt;

&lt;p&gt;I had no idea what 1232 meant. Now I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Limit&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every Solana transaction — transfer, swap, mint, everything — has a hard cap: 1,232 bytes.&lt;/p&gt;

&lt;p&gt;Not 1,232 lines of code. 1,232 bytes&lt;/p&gt;

&lt;p&gt;Where does 1,232 come from? IPv6 packet size. The internet requires every packet to be at least 1,280 bytes. Subtract 48 bytes for network headers. Leftover: 1,232 bytes.&lt;/p&gt;

&lt;p&gt;Solana didn't choose this number. The internet did.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why I Hit the Wall&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My batch transfer had 22 accounts (sender + 20 recipients + system program). Each account key takes 32 bytes. 22 × 32 = 704 bytes just for the addresses.&lt;/p&gt;

&lt;p&gt;Add signature (64 bytes), header (3 bytes), blockhash (32 bytes), instructions (variable). Total: ~1,148 bytes.&lt;/p&gt;

&lt;p&gt;Dangerously close to 1,232. Add one more recipient? Fail. Add a memo? Fail. Add any complexity? Fail.&lt;/p&gt;

&lt;p&gt;I was 84 bytes from the edge. That's one short sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Escape&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I learned about Address Lookup Tables (ALTs).&lt;/p&gt;

&lt;p&gt;An ALT is an on-chain account that stores addresses. Once stored, a transaction references them by a 1-byte index instead of the full 32-byte address.&lt;/p&gt;

&lt;p&gt;Think of it like a URL shortener. You store the long URL once. Everyone else uses the short code.&lt;/p&gt;

&lt;p&gt;Same concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Changed&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My batch transfer went from 22 full addresses to 1 ALT address + 21 one-byte indexes.&lt;/p&gt;

&lt;p&gt;Before: 704 bytes for addresses&lt;br&gt;
After: 32 bytes (ALT) + 21 bytes (indexes) = 53 bytes&lt;/p&gt;

&lt;p&gt;My transaction size dropped from ~1,148 bytes to ~562 bytes.&lt;/p&gt;

&lt;p&gt;Suddenly I had room. Add more recipients. Add a memo. Add error handling. All fit.&lt;/p&gt;

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

&lt;p&gt;The 1,232-byte limit isn't arbitrary. It's Solana prioritizing network speed over developer convenience.&lt;/p&gt;

&lt;p&gt;Fragmented packets are slower. Solana said no fragmentation. One packet. One transaction. Done.&lt;/p&gt;

&lt;p&gt;This forces constraints:&lt;/p&gt;

&lt;p&gt;List every account upfront&lt;/p&gt;

&lt;p&gt;Batch operations need ALTs&lt;/p&gt;

&lt;p&gt;Complex DeFi requires careful design&lt;/p&gt;

&lt;p&gt;But the network stays fast. 4,000+ transactions per second fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxladeqg3c40dvh10p2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxladeqg3c40dvh10p2w.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>webdev</category>
      <category>solanadev</category>
      <category>mlh</category>
    </item>
  </channel>
</rss>
