<?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: Russell Oje</title>
    <description>The latest articles on DEV Community by Russell Oje (@russell_oje).</description>
    <link>https://dev.to/russell_oje</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%2F3846695%2F5f9b12d1-01cf-4a2a-ab89-b52838094707.jpg</url>
      <title>DEV Community: Russell Oje</title>
      <link>https://dev.to/russell_oje</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russell_oje"/>
    <language>en</language>
    <item>
      <title>Solana: The Byte Tax</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:19:32 +0000</pubDate>
      <link>https://dev.to/russell_oje/solana-the-byte-tax-35n9</link>
      <guid>https://dev.to/russell_oje/solana-the-byte-tax-35n9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why does every feature you bolt onto a Solana Token come with a price tag?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In every Web2 stack I've worked in, adding a column to a table is basically free. You run a migration, maybe wait a minute for it to backfill, and move on. Storage is cheap, and nobody itemizes what a single boolean field costs you.&lt;/p&gt;

&lt;p&gt;On Solana, I found out the hard way that &lt;em&gt;every&lt;/em&gt; feature I bolt onto a token has a &lt;strong&gt;literal&lt;/strong&gt;, &lt;strong&gt;permanent&lt;/strong&gt;, &lt;strong&gt;one-time&lt;/strong&gt; price tag, and even more fascinating, you can watch that price change in real SOL as you add extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rent Isn't a Metaphor Here
&lt;/h2&gt;

&lt;p&gt;Proportional to how many bytes of data it stores, every account on Solana, including a token's mint account, has to hold enough SOL to stay "rent-exempt". Token Extensions live inside that same account, packed into a Type-Length-Value buffer bolted onto the base mint. So more extensions means more bytes, which implies a bigger one-time deposit before the account can exist at all.&lt;/p&gt;

&lt;p&gt;I built three different mints in Arc 6 of the &lt;code&gt;100DaysOfSolana&lt;/code&gt; and measured them side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mint&lt;/th&gt;
&lt;th&gt;Extensions Enabled&lt;/th&gt;
&lt;th&gt;Account Size (bytes)&lt;/th&gt;
&lt;th&gt;Rent Cost (SOL)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interest-bearing only&lt;/td&gt;
&lt;td&gt;Interest-bearing&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;~0.002436&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-extension&lt;/td&gt;
&lt;td&gt;Interest-bearing, Transfer fees, Metadata Pointer, Metadata&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;~0.005060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance-gated&lt;/td&gt;
&lt;td&gt;Default Account State (Frozen)&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;td&gt;~0.002081&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly a 2.4x jump in both size and cost just from stacking three extra behaviors onto one mint. Meanwhile, the "default frozen" mint, despite sounding like the heaviest compliance feature of the three, turned out to be one of the cheapest, smallest accounts I created all Epoch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Actually Matters
&lt;/h2&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 single command above (the one that produced my 599-byte mint), reads like a feature checklist: fees, yield, and metadata, all in one line. In Web2, shipping that many "nice to haves" costs you engineering time, not a line item you pay before the feature goes live. On Solana, the cost is upfront, denominated in SOL, and visible the moment you run &lt;code&gt;solana account $MINT --output json&lt;/code&gt; and read the space field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;The correlation is almost too clean: every extension adds a predictable number of bytes, and every byte has a knowable SOL cost. There's no hidden "it depends on load" the way cloud storage pricing gets murky. You can price out a token's entire feature set before writing a single line of code, the same way you'd budget infrastructure before choosing a stack.&lt;/p&gt;

&lt;p&gt;It reframed extensions for me as a real trade-off instead of a free checkbox. There's no such thing as a free feature flag on a mint account. You decide what you actually need, because you're the one paying the rent-exempt deposit for whatever you don't cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;Say you design a schema, choose any product that you've done in the past. But the caveat is that every field you added to the schema, will cost you real money, forever, and upfront; which of your product's "nice to have" fields would you cut first? I want to hear about the most bloated schema you've ever inherited. Let's see how it stacks up against a 599-byte mint account.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Solana: Can You Own Something You're Not Allowed to Sell?</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:12:24 +0000</pubDate>
      <link>https://dev.to/russell_oje/solana-can-you-own-something-youre-not-allowed-to-sell-1p39</link>
      <guid>https://dev.to/russell_oje/solana-can-you-own-something-youre-not-allowed-to-sell-1p39</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Solana's Soulbound Credentials, explained for Web2 Devs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ownership on a blockchain is supposed to be the whole pitch: whoever holds the private key holds the asset, full stop, no admin panel required. So what happens when I build a token that you can hold... but can never sell, trade, or give away — &lt;em&gt;and that I can delete from your wallet without asking you?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's exactly what I built in Arc 6 of the &lt;code&gt;#100DaysOfSolana&lt;/code&gt; using two Token Extensions stacked on the same mint (three, actually), and it forced me to rethink what "ownership" even means once code, not policy, is the one enforcing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Extensions That Make This Possible
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Non-Transferable&lt;/strong&gt; locks a token to the account it was minted into. Forever. Not "until the marketplace updates its terms" — the transfer instruction itself is rejected by the protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent Delegate&lt;/strong&gt; grants one authority (in this case, me, the issuer) the power to burn or move the token out of any holder's account without their signature. No multisig, no support ticket, no "please confirm this action."&lt;/p&gt;

&lt;p&gt;Combined with a &lt;strong&gt;Metadata&lt;/strong&gt; extension, this is enough to build a revocable credential entirely from CLI flags; think about a certification, a compliance badge, or even a membership pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-permanent-delegate&lt;/span&gt; &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;&lt;code&gt;--decimals 0&lt;/code&gt; because a credential is a whole thing. You either hold it, or you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Rules, Not Just Trusting Them
&lt;/h2&gt;

&lt;p&gt;I minted one credential to a second wallet, then tried to move it to a third wallet using the second wallet's own keypair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token transfer &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1 &lt;span class="nv"&gt;$THIRD_PARTY&lt;/span&gt; &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/recipient-wallet.json &lt;span class="nt"&gt;--fund-recipient&lt;/span&gt; &lt;span class="nt"&gt;--allow-unfunded-recipient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejected. Not "denied by my backend" — rejected by the Token-2022 program itself, because the non-transferable rule is written into the mint's on-chain data.&lt;/p&gt;

&lt;p&gt;Then I did the uncomfortable part. Using my permanent delegate authority, I burned that same credential straight out of the recipient's account, without requesting a signature from them:&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 burn &lt;span class="nv"&gt;$RECIPIENT_TOKEN_ACCOUNT_ADDRESS&lt;/span&gt; 1 &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/.config/solana/id.json &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. Instantly. No dispute process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside the Theory
&lt;/h2&gt;

&lt;p&gt;This pattern already exists in production. A good example is Paxos, which uses the Permanent Delegate extension on its USDP stablecoin specifically to claw back funds tied to illegal activity, satisfying a regulatory requirement it operates under. That's a real, audited, non-hypothetical use of &lt;em&gt;"I can take this back from you"&lt;/em&gt; living directly inside a currency that moves real money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;I expected "soulbound" logic to require deploying a custom contract or program that intercepts every transfer instruction and checks a condition. It doesn't. The protocol understands non-transferability and delegated revocation natively — no custom smart contract, no audit surface beyond the extensions I opted into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable question I can't fully answer myself: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if I can revoke your credential without your signature, is it actually &lt;em&gt;yours&lt;/em&gt;, or is it just a permission slip with extra ceremony? - Is this meaningfully different from a company revoking your enterprise SSO seat the day you're offboarded, or is it even worse, since it's permanent and public?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want the Web2 argument here. Tell me where I'm wrong in the comments.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>What Solana's Transfer Fee Extension Taught Me About Trustless Payments</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:04:34 +0000</pubDate>
      <link>https://dev.to/russell_oje/what-solanas-transfer-fee-extension-taught-me-about-trustless-payments-403h</link>
      <guid>https://dev.to/russell_oje/what-solanas-transfer-fee-extension-taught-me-about-trustless-payments-403h</guid>
      <description>&lt;p&gt;Every web2 payment flow I've ever shipped has the same shape: a request hits my server, my server calculates the amount and fees, and then my server updates the database. The fee only exists because my code enforces it. If someone finds a way around my API, say, using a direct database write, or a race condition, or even a webhook that never fires, then the fee just doesn't happen.&lt;/p&gt;

&lt;p&gt;In Epoch 2 of &lt;code&gt;#100DaysOfSolana&lt;/code&gt;, I built a token where this entire problem disappears. It disappears, not because I wrote better backend code, but because there was no backend to bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Web2 Way:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fees Live in Application Code
&lt;/h3&gt;

&lt;p&gt;Think about how a typical marketplace fee works. A buyer pays $100, your Stripe webhook fires, your server calculates a 5% platform fee, and your server moves $95 to the seller's payout and the $5 fee to your treasury. That logic lives entirely in your application layer. It is correct as long as nobody modifies to your database directly, nobody manipulates your webhook, and your cron job never double-fires.&lt;/p&gt;

&lt;p&gt;That's a lot of "as long as."&lt;/p&gt;

&lt;h2&gt;
  
  
  How Solana Does It:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Baking the Fee into the Currency Itself
&lt;/h3&gt;

&lt;p&gt;Solana's Token Extensions Program (Token-2022) lets you attach a &lt;code&gt;TransferFeeConfig&lt;/code&gt; directly to a token's mint account. Once it's there, the fee isn't a suggestion your application makes, but a rule the network itself enforces on every single transfer, for every wallet, FOREVER.&lt;/p&gt;

&lt;p&gt;Here's the exact command I used to create a mint with a 1% fee and a fee cap:&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; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 1000000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No webhook. No cron job. No server at all. The 100 basis points (1%) is stored in the mint's own account data, and the Token-2022 program checks it on every transfer instruction before the transaction is even allowed to land.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching It Actually Refuse to Be Bypassed
&lt;/h3&gt;

&lt;p&gt;The part that changed how I think about this wasn't creating the mint, but it was when I tried to move tokens around it. When I sent 1,000 tokens to a second wallet, I had to tell the CLI what fee I expected:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If that number doesn't match what the protocol calculates, the transfer fails outright. There's no code path where the fee "doesn't happen." The 10 tokens land in a withheld balance sitting inside the recipient's own token account, visible, auditable, and untouched until the mint's withdraw authority explicitly harvests it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token withdraw-withheld-tokens &lt;span class="nv"&gt;$MY_TOKEN_ACCOUNT&lt;/span&gt; &lt;span class="nv"&gt;$RECIPIENT_TOKEN_ACCOUNT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could see the withheld amount sitting there before I swept it. In Web2 terms, it's like the database enforcing your business rule for you at the storage engine level, instead of trusting your application code to get it right every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside the Theory
&lt;/h2&gt;

&lt;p&gt;This isn't just a devnet toy "in theory". There's a meme-adjacent token called BERN, which uses the Transfer Fee extension so that every transfer automatically skims a percentage split between burning BONK, burning BERN, and rewarding holders. &lt;/p&gt;

&lt;p&gt;Regulated stablecoin issuers also use the same Token-2022 toolbox for very different reasons. Another interesting use case is Paxos, where they specifically enabled the Permanent Delegate extension on their USDP stablecoin so it can claw back funds used for illegal purposes, meeting strict regulatory requirements it operates under.&lt;/p&gt;

&lt;p&gt;Same program, wildly different use cases, both enforced at the protocol layer instead of the application layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;I stacked the Transfer Fee extension with an Interest-Bearing rate on the same mint later in the week, and both behaviors lived independently in the same account's data. The fee deducts on transfer, and the interest accrues on display; neither one needs to know the other exists. Composability at the protocol level means that, I get a "middleware" for my currency, without writing a single line of middleware. Cool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;If a fee genuinely could not be bypassed, no matter who touched the raw ledger, what would you build with that guarantee? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A treasury that funds itself? &lt;/li&gt;
&lt;li&gt;Or a royalty stream that survives even if the marketplace's website goes down? &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tell me the fee-based idea you'd never trust a Web2 backend to enforce correctly.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>A Mental Dive into Solana PDAs</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:49:02 +0000</pubDate>
      <link>https://dev.to/russell_oje/a-mental-dive-into-solana-pdas-5fi4</link>
      <guid>https://dev.to/russell_oje/a-mental-dive-into-solana-pdas-5fi4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;On Solana, programs are stateless. So if your program needs to remember something per user, per config, per anything, it needs a deterministic address it can find again later without storing it anywhere. PDAs are that address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Background Picture
&lt;/h2&gt;

&lt;p&gt;From the web2 world, a PDA (or Program Derived Address) is like a database primary key you compute from the row's logical identity. But instead of &lt;code&gt;INSERT INTO counters (user_id) RETURNING id&lt;/code&gt;, you run a hash over &lt;code&gt;["counter", user_pubkey, program_id]&lt;/code&gt; and that hash &lt;em&gt;is&lt;/em&gt; the address or PDA. It requires no lookup table and no coordination. The client and the program independently arrive at the same address every time.&lt;/p&gt;

&lt;p&gt;But there's a twist here, that must be noted: PDAs are not necessarily similar to rows in a table. Meaning that the address may exist, i.e. there is a live account with data at that location, or it may not. So deriving an address does not create an account. That is a separate step, and it costs rent (a small SOL deposit, held as collateral for the bytes you occupy on every validator). The derivation is just arithmetic.&lt;/p&gt;

&lt;p&gt;Another observation to note is that the program ID is baked into the hash. The same seeds in a different program produce a completely different address. This implies that a PDA belongs to exactly one program, and only that program can authorize writes to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Derivation
&lt;/h2&gt;

&lt;p&gt;On Day 64, I ran a standalone script to make the determinism tangible. The core of it was this:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Running the same call twice gave the identical address. Running it with &lt;code&gt;"alice"&lt;/code&gt; as a second seed gave something completely different, which is the whole point of PDAs.&lt;/p&gt;

&lt;p&gt;When the same seeds moved into the counter program itself, they looked like this:&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;Key pieces to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;b"counter"&lt;/code&gt;&lt;/strong&gt; — a static byte string that namespaces this address. Without it, different account types with the same user key would collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;user.key().as_ref()&lt;/code&gt;&lt;/strong&gt; — the signer's public key as a byte slice. This is what gives every wallet its own address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bump&lt;/code&gt;&lt;/strong&gt; — this is a range from 0 - 255, more on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;space = 8 + Counter::INIT_SPACE&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;8&lt;/code&gt; is Anchor's discriminator, a prefix that identifies which account type this is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the Seeds Matter
&lt;/h2&gt;

&lt;p&gt;Day 68 especially, was deliberately breaking things, and it was my most instructive day this week.&lt;/p&gt;

&lt;p&gt;Compare these two seed arrays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;  &lt;span class="c1"&gt;// per-user PDA&lt;/span&gt;
&lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="c1"&gt;// global PDA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first gives every wallet its own isolated counter, while the second gives every wallet the &lt;em&gt;same&lt;/em&gt; address, with a first-write-wins collision where whoever calls &lt;code&gt;init_counter&lt;/code&gt; first owns the account for everyone. This is not a bug in either case; it is a design choice, just like the config singleton is intentionally global:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"config"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="n"&gt;bump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's usually only one config per program, derived from a fixed seed, and initialized once. Calling &lt;code&gt;init&lt;/code&gt; on an existing account will return a runtime error, so the singleton guarantee is enforced for free.&lt;/p&gt;

&lt;p&gt;The near-miss variants from the collision experiment on Day 68 drove the point home. &lt;code&gt;"counter"&lt;/code&gt;, &lt;code&gt;"counters"&lt;/code&gt;, &lt;code&gt;"counter\0"&lt;/code&gt;, and &lt;code&gt;"Counter"&lt;/code&gt; each produce a completely unrelated address. There is no fuzzy matching, one byte difference, entirely different account namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 'Bump' Key Piece
&lt;/h2&gt;

&lt;p&gt;The bump, is the byte (0-255) that makes the whole thing work, even though it seems easy to hand-wave past it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findProgramAddressSync&lt;/code&gt; hashes the seeds together with the program ID and a trailing byte, the bump, which starts at 255 and counting down to 0. Most public keys happen to fall on the ed25519 elliptic curve, which means they could theoretically have a private key. The runtime needs your PDA to be off that curve, so no one can generate a private key for it. The first bump value that produces an off-curve address is the &lt;strong&gt;canonical bump&lt;/strong&gt;, and it is always the same for a given seed + program ID combination.&lt;/p&gt;

&lt;p&gt;The canonical bump is the only safe one to use. Anchor finds it for you during &lt;code&gt;init&lt;/code&gt; and Anchor also stores it for you (i.e. &lt;code&gt;bump&lt;/code&gt; in the constraint and &lt;code&gt;counter.bump&lt;/code&gt; in the struct). So that on subsequent instructions, you pass the stored bump back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&lt;/span&gt;
    &lt;span class="nd"&gt;mut,&lt;/span&gt;
    &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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;Storing and re-passing is important here also, because re-deriving the canonical bump every time costs compute. Storing it once at init and re-using it is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Picture
&lt;/h2&gt;

&lt;p&gt;Across the five days, this is how the counter account went through a complete lifecycle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derive&lt;/strong&gt; — &lt;code&gt;findProgramAddressSync&lt;/code&gt; on the client, same arithmetic in the program. Both sides agree on the address before any transaction is sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize&lt;/strong&gt; — &lt;code&gt;init&lt;/code&gt; allocates the account, sets aside the rent-exempt deposit (lamports — Solana's smallest unit of currency), and populates the fields. This is the moment the address goes from "a possible location" to "a live account."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutate&lt;/strong&gt; — subsequent &lt;code&gt;increment&lt;/code&gt; calls re-derive the address from the signer's key and reject the transaction at the constraint layer if anything does not match. No defensive code needed inside the handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Close&lt;/strong&gt; — Close the PDA account and reclaim rent. Day 67 added this, and it took one attribute:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;close = user&lt;/code&gt; drains the lamports to the user's wallet and zeros the account data. When a Solana account's balance hits zero, the runtime removes it at the end of the transaction, without any separate delete syscall. A subsequent &lt;code&gt;getAccountInfo&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, which means that the rent deposit comes back in full, minus the transaction fee.&lt;/p&gt;

&lt;p&gt;NB: This is not "delete from a table." The account disappears from the account model entirely. There is no tombstone, no soft-delete flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Little Observations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The program ID is part of the derivation.&lt;/strong&gt; Same seeds with different program —&amp;gt; different address. A PDA is not portable across programs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDAs cannot sign transactions on their own.&lt;/strong&gt; Only the program can authorize writes to a PDA, by presenting the same seeds internally. This is called signing with "signer seeds" and it happens inside the program, not in the client transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed design is access control.&lt;/strong&gt; Including the user's pubkey in the seeds gives each wallet its own isolated namespace. Omitting it and every wallet shares one account. Both are valid choices, but you have to be deliberate. I saw this clearly when the spoof attempt where Wallet A trying to increment Wallet B's counter, and was rejected before the handler even ran, purely because the re-derived address did not match the supplied account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;init_if_needed&lt;/code&gt; is a footgun.&lt;/strong&gt; It initializes the account if it does not exist, silently does nothing if it does. That silent-do-nothing behavior is exactly the kind of thing that hides bugs in test suites. Reach for it deliberately and always test the already-initialized path explicitly.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Anchor Constraints as an Entire Auth Layer</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 22 Jun 2026 16:46:13 +0000</pubDate>
      <link>https://dev.to/russell_oje/anchor-constraints-as-an-entire-auth-layer-154h</link>
      <guid>https://dev.to/russell_oje/anchor-constraints-as-an-entire-auth-layer-154h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This week I made the jump from being a &lt;em&gt;consumer&lt;/em&gt; of Solana programs to being a &lt;em&gt;builder&lt;/em&gt; of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me walk you through what I built, what I tested, and the one experiment that made everything click.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Program: A Counter with an Owner
&lt;/h2&gt;

&lt;p&gt;The running example for the week was a counter program. Simple on the surface — one &lt;code&gt;Pubkey&lt;/code&gt; (the owner) and one &lt;code&gt;u64&lt;/code&gt; (the count) — but surprisingly rich in what it teaches.&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;#[derive(InitSpace)]&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;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;#[account]&lt;/code&gt; stamps an 8-byte discriminator onto the front of every serialised &lt;code&gt;Counter&lt;/code&gt;. That discriminator is how the program can later verify "yes, this account was created by me and nothing else." &lt;code&gt;#[derive(InitSpace)]&lt;/code&gt; auto-calculates the byte size so you don't have to count fields by hand. One macro, zero arithmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints Are Like Middleware
&lt;/h2&gt;

&lt;p&gt;In a Web2 backend you might write a route guard like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authority_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Anchor, you write this instead:&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;&lt;code&gt;has_one = authority&lt;/code&gt; tells Anchor: before the &lt;code&gt;increment&lt;/code&gt; handler runs, verify that the &lt;code&gt;authority&lt;/code&gt; field stored inside the &lt;code&gt;counter&lt;/code&gt; account on-chain matches the &lt;code&gt;authority&lt;/code&gt; key passed in this transaction. If they don't match, the transaction fails before your Rust code ever executes. You didn't write an &lt;code&gt;if&lt;/code&gt; statement. You declared a rule.&lt;/p&gt;

&lt;p&gt;Similarly, the &lt;code&gt;init&lt;/code&gt; constraint on &lt;code&gt;Initialize&lt;/code&gt; does the heavy lifting of creating the account:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One attribute makes a CPI to the System Program, allocates the right number of bytes, funds the rent from &lt;code&gt;authority&lt;/code&gt;'s wallet, assigns the account to your program, and refuses to run if the account already exists. You write one line. Anchor writes the boilerplate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing with LiteSVM
&lt;/h2&gt;

&lt;p&gt;Once the program writes state, you need a way to prove it works. In the earlier epochs, I observed that waiting for devnet confirmations was slow and flaky, which brings &lt;strong&gt;LiteSVM&lt;/strong&gt; into the picture, as it is an in-process Solana virtual machine that can run your compiled &lt;code&gt;.so&lt;/code&gt; binary against a fresh ledger in the same Rust test process.&lt;/p&gt;

&lt;p&gt;The happy-path test looks like a normal Rust unit test, but it executes a &lt;em&gt;real&lt;/em&gt; Solana transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State even persists across transactions within one &lt;code&gt;LiteSVM&lt;/code&gt; instance — just like a real cluster — so a test that calls &lt;code&gt;initialize&lt;/code&gt; and then &lt;code&gt;increment&lt;/code&gt; is a genuine end-to-end simulation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Including Failure Tests
&lt;/h2&gt;

&lt;p&gt;A happy-path test proves the program &lt;em&gt;works&lt;/em&gt;. A failure test proves the program &lt;em&gt;refuses&lt;/em&gt;. Both are necessary.&lt;/p&gt;

&lt;p&gt;I wrote two:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Wrong authority is rejected&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;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 for a different signer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Double-initialization is rejected&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="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;first_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;"first initialize should succeed"&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;.expire_blockhash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// make the second tx genuinely new&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;second_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;"initializing the same counter twice should fail"&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;expire_blockhash()&lt;/code&gt; call is subtle but important. Without it, both transactions are byte-for-byte identical and LiteSVM rejects the second as a duplicate signature — the &lt;em&gt;wrong&lt;/em&gt; error. Expiring the blockhash ensures the failure comes from the &lt;code&gt;init&lt;/code&gt; constraint, not the duplicate check.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mutation Testing Experiment
&lt;/h2&gt;

&lt;p&gt;On friday I deliberately broke the program in three ways, one at a time, and watched the tests catch each regression:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug introduced&lt;/th&gt;
&lt;th&gt;Test that failed&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Removed &lt;code&gt;has_one = authority&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;increment_fails_when_wrong_authority_signs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constraint gone, unauthorized call now succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed &lt;code&gt;checked_add(1)&lt;/code&gt; to &lt;code&gt;checked_add(2)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initialize_then_increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Count was 2, expected 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commented out &lt;code&gt;counter.authority = ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initialize_then_increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On-chain authority was all-zeros; &lt;code&gt;has_one&lt;/code&gt; failed at increment time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third bug was the most instructive. The &lt;code&gt;initialize&lt;/code&gt; transaction itself succeeded, i.e. the account was created and rent was paid. The bug only surfaced at &lt;code&gt;increment&lt;/code&gt;, when the runtime compared the stored (all-zero) authority to the real signer and found a mismatch. The error pointed &lt;em&gt;downstream&lt;/em&gt; of the cause. The tests caught it; the logs explained it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Take Home
&lt;/h2&gt;

&lt;p&gt;Anchor constraints are not just convenience sugar. They are a &lt;strong&gt;declarative authorization layer&lt;/strong&gt; that runs before your handler, cannot be bypassed by a careless refactor, and produces clear error messages when violated. Combined with LiteSVM's in-process test runner and a suite that includes both happy-path and failure tests, you get a feedback loop that's fast enough to run on every save and trustworthy enough to catch real bugs.&lt;/p&gt;

&lt;p&gt;The mutation experiments this week weren't busywork. They were the proof. Every assertion that lit up red when I broke something is an assertion I now trust when it stays green.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Trilogy in Solana: Transfer Fee, Interest-Bearing, and Nontransferable</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:18:09 +0000</pubDate>
      <link>https://dev.to/russell_oje/trilogy-in-solana-transfer-fee-interest-bearing-and-nontransferable-5ddk</link>
      <guid>https://dev.to/russell_oje/trilogy-in-solana-transfer-fee-interest-bearing-and-nontransferable-5ddk</guid>
      <description>&lt;p&gt;Token-2022 is the upgraded SPL token program on Solana, bringing native extensions that allow a single mint to opt into advanced behaviors without custom programs. This week, I explored three powerful extensions: Transfer Fees, Interest-Bearing tokens, and Non-Transferable tokens. Think of these extensions as middleware for your tokens—enforced at the protocol level.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Transfer Fee Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;FeoMWHjNJgJM6iEQUnUjsJsdPtP3Cu9m99iiuz9RhALG&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I configured this token to skim a fee on every transfer. This is perfect for protocol treasuries or community currencies where a small percentage of every transaction supports the ecosystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkThT93YfkdY59uL35t4Z5Xl &lt;span class="nt"&gt;--transfer-fee&lt;/span&gt; 100 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command sets a 1% fee (100 basis points) with a maximum cap of 5000 lamports.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Interest-Bearing Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;6vZ9eXTTtGCpwRVLGg6uuyLXxk2yYEykroBTNybf6wT3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This extension allows a token to accrue interest over time. Crucially, it doesn't mint new supply; instead, the UI-displayed amount updates based on the on-chain rate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkThT93YfkdY59uL35t4Z5Xl &lt;span class="nt"&gt;--interest-rate&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: Remember that the rate is stored on-chain, but the principal balance remains the same until updated. It's a clever way to handle yield display natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Non-Transferable Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;3PWCwuiPauNhYxwArDGaRizKLpJHETLS7RNRqHL1AQvm&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally, I built a "Soul-bound" token. Once minted to an account, it cannot be transferred or moved. I tested this by attempting a transfer and received a satisfyingly firm error: &lt;code&gt;Program log: Transfer is disabled for this mint: custom program error: 0x25&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Take Home&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;What caught my attention this week is how simple it is to compose these behaviors. Native extensions remove the need for "wrapped" versions of tokens to get simple features like fees or interest. I’m excited to see how these primitives will be used in real products for loyalty programs.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>NFTs in Solana: Another Week with Token Extensions</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 08 Jun 2026 18:11:03 +0000</pubDate>
      <link>https://dev.to/russell_oje/nfts-in-solana-another-week-with-token-extensions-2h5f</link>
      <guid>https://dev.to/russell_oje/nfts-in-solana-another-week-with-token-extensions-2h5f</guid>
      <description>&lt;p&gt;This week, I stepped away from the usual Extension system from last week, to creating NFTs, using only Solana's native Token Extensions (Token-2022). It turns out, you can build a fully functional, grouped, and mutable NFT collection entirely at the protocol level.&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%2Fjmdgirioo0h0z5zs7orh.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%2Fjmdgirioo0h0z5zs7orh.png" alt="Day 44 NFT" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Mental Model: What is a Solana NFT?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Coming from a Web2 background, I used to think of an NFT as a complex smart contract. On Solana, using Token Extensions, an NFT is simpler and more elegant: it's a token mint with &lt;strong&gt;supply 1&lt;/strong&gt;, &lt;strong&gt;decimals 0&lt;/strong&gt;, and a few specific extensions that handle metadata and grouping.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I Built This Week&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I spent the last few days moving from a single 1-of-1 token to a full-blown collection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The 1-of-1 Mint&lt;/strong&gt;: I started by creating a mint with the &lt;code&gt;Metadata&lt;/code&gt; extension, ensuring it had zero decimals and a supply of exactly one.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Metadata Extension&lt;/strong&gt;: Instead of an external metadata account, I stamped the name, symbol, and URI (pointing to a JSON on a GitHub Gist) directly onto the mint.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Group and Member Extensions&lt;/strong&gt;: I created a collection mint and then linked my individual NFTs to it using the &lt;code&gt;Group&lt;/code&gt; and &lt;code&gt;Member&lt;/code&gt; extensions. This creates a "foreign key" relationship natively on the blockchain.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Surprising Part: Live Mutation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The most eye-opening moment was realizing how "alive" this data is. As long as you hold the update authority, you can mutate the metadata in real-time with a single CLI command.&lt;/p&gt;

&lt;p&gt;On Friday, I renamed my NFT, updated the metadata's JSON uri (to update the image icon) and added custom fields like &lt;code&gt;rarity: legendary&lt;/code&gt; directly from my terminal:&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;# Adding a custom metadata field live on devnet&lt;/span&gt;
spl-token update-metadata &lt;span class="nv"&gt;$MINT_ADDRESS&lt;/span&gt; rarity &lt;span class="s2"&gt;"legendary"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watching Solana Explorer update the "legendary" tag seconds after my transaction confirmed felt like running an &lt;code&gt;UPDATE&lt;/code&gt; statement on a production database, but the "database" is a global public network.&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%2Fe8yuii8hnmktn8v5vp3w.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%2Fe8yuii8hnmktn8v5vp3w.png" alt="Update NFT" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Week Digest&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Using Token Extensions for NFTs feels like the "bare metal" way to build. It's fast, cost-effective, and removes the dependency on third-party programs for basic collection management. I’m excited to see how these primitives will be used to build more complex "programmable" NFTs that react to on-chain events.&lt;/p&gt;

&lt;p&gt;Follow along on my &lt;code&gt;#100DaysOfSolana&lt;/code&gt; journey!&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>nft</category>
      <category>blockchain</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Soulbound Credentials on Solana</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:25:53 +0000</pubDate>
      <link>https://dev.to/russell_oje/soulbound-credentials-on-solana-b5c</link>
      <guid>https://dev.to/russell_oje/soulbound-credentials-on-solana-b5c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Combining Non-Transferable and Permanent Delegate Extensions to Build a Soulbound Token System on Solana&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you're issuing a digital certificate or a compliance-gated membership. In traditional web2 systems, you have absolute control over a database record and can decide who holds it and when it should be revoked or expired. In web3, assets are typically owned by the wallet holding them, so once they are transferred, the issuer loses control.&lt;/p&gt;

&lt;p&gt;But what if you need an on-chain credential that the holder can't sell or transfer, and the only issuer can revoke if necessary?&lt;/p&gt;

&lt;p&gt;This week I explored Solana's Token Extensions (Token-2022) to build exactly this: &lt;em&gt;A Soulbound Token with a Permanent Delegate&lt;/em&gt;.&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%2Fvquxwne7k8g1ekbekwqb.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%2Fvquxwne7k8g1ekbekwqb.png" alt="Soulbound Token" width="799" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Token Extensions Involved&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To achieve a revocable credential, we combine three Token Extensions at the protocol level:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Non-transferable&lt;/strong&gt;: Enforces that the token cannot be moved from the recipient's wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent Delegate&lt;/strong&gt;: Allows an authority (us, the issuer) to burn or transfer the token regardless of who holds it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt;: Binds the credential's details directly to the mint.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Creating and Revoking the Credential&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the exact code snippet I used to generate the token mint with these extensions enabled:&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 the mint with non-transferable and permanent-delegate extensions&lt;/span&gt;
spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt; &lt;span class="nt"&gt;--enable-permanent-delegate&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;Using &lt;code&gt;--decimals 0&lt;/code&gt; makes sense here because credentials are whole units; so you either hold the certification or you don't.&lt;/p&gt;

&lt;p&gt;If the credential owner attempts to transfer the token, the non-transferable extension outright blocks it. The transaction fails on-chain.&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%2F6ypvtdiju1z7hterd243.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%2F6ypvtdiju1z7hterd243.png" alt="Blocked Transfer" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If a credential expires or the user violates compliance, the issuer can revoke it using their permanent delegate authority. We do this by burning the token directly from the recipient’s token account—no signature from the recipient required:&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;# Burning the credential from the recipient's account&lt;/span&gt;
spl-token burn &lt;span class="nv"&gt;$RECIPIENT_TOKEN_ACCOUNT_ADDRESS&lt;/span&gt; 1 &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/.config/solana/id.json &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;What surprised me the most was how deeply these extensions remove the need for custom smart contracts. Previously, I imagined that the "Soulbound" logic would require deploying a complex custom program to intercept transfers, but, the protocol inherently understands these rules natively, making the execution safer and cheaper. &lt;/p&gt;

&lt;p&gt;It was incredibly satisfying to watch a transfer attempt legally fail on-chain because the token was minted with the non-transferable flag!&lt;/p&gt;

&lt;p&gt;If you want to dive deeper and experiment with these configurations, check out the official Token-2022 extensions guide in the Solana documentation.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>Week5 Recap of #100DaysOfSolana</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Tue, 26 May 2026 21:49:54 +0000</pubDate>
      <link>https://dev.to/russell_oje/week5-recap-of-100daysofsolana-2mf3</link>
      <guid>https://dev.to/russell_oje/week5-recap-of-100daysofsolana-2mf3</guid>
      <description>&lt;p&gt;This week I created a custom SPL token on Solana with metadata, transfer fees, and non-transferable extensions!&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%2Fl1swakjkz56gc77uoi6y.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%2Fl1swakjkz56gc77uoi6y.png" alt="Rule Enforcement" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't expect the Token Extensions Program to let you enforce economic rules directly at the protocol level, no off-chain logic needed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#100DaysOfSolana&lt;/code&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title># Week5 of #100DaysOfSolana: A Tour of Solana Token Extensions</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Tue, 26 May 2026 21:47:31 +0000</pubDate>
      <link>https://dev.to/russell_oje/-week5-of-100daysofsolana-a-tour-of-solana-token-extensions-1mk6</link>
      <guid>https://dev.to/russell_oje/-week5-of-100daysofsolana-a-tour-of-solana-token-extensions-1mk6</guid>
      <description>&lt;p&gt;If you come from Web2, the idea of a token might seem like a simple integer variable stored in a database. But on Solana, tokens are deeply integrated into the programming model. This week, I went from compiling my first dependencies to creating fully featured tokens using Solana's Token Extensions Program (Token 2022).&lt;/p&gt;

&lt;p&gt;In this post, I want to walk you through what I learned, how token extensions change the game, and why enforcing economic rules on-chain instead of off-chain is so powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Starting Point: Why The Token Extensions Program?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When I first started looking into Solana, I learned about the standard Token Program. It's the original program that handles SPL tokens. But then I discovered the &lt;strong&gt;Token Extensions Program&lt;/strong&gt; (often referred to as Token-2022).&lt;/p&gt;

&lt;p&gt;Why do we need a new program? Because the original program was simple by design. If you wanted to do something complex-like enforcing transfer fees, tying metadata directly to the token mint, or making a token non-transferable, you had to build an entire custom smart contract to act as an intermediary. &lt;/p&gt;

&lt;p&gt;The Token Extensions Program brings these powerful features directly to the protocol level. You can just configure the mint to utilize specific extensions, and the protocol handles the rest. This drastically reduces the surface area for bugs and the barrier to entry for developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Adding Metadata Directly to the Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Previously, token metadata (like the name, symbol, and URI for an image) was typically handled by Metaplex programs off to the side. With the &lt;code&gt;MetadataPointer&lt;/code&gt; and &lt;code&gt;TokenMetadata&lt;/code&gt; extensions, you can embed this directly.&lt;/p&gt;

&lt;p&gt;Here is what the flow looks like when initializing a mint with token extensions via the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana-keygen new &lt;span class="nt"&gt;--outfile&lt;/span&gt; token-keypair.json
spl-token create-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 200 &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 5000 &lt;span class="nt"&gt;--enable-metadata&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;By linking metadata directly to the mint, building indexers, wallets, or explorers becomes so much simpler. Everything you need is right there in the token's core data structure. No hunting across different accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Enforcing Transfer Fees at the Protocol Level&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most fascinating extensions I experimented with is the &lt;code&gt;TransferFeeConfig&lt;/code&gt;. Let's say you want to build a token that burns 1% of every transaction, or sends a 1% royalty to the creator's treasury.&lt;/p&gt;

&lt;p&gt;In traditional Web2 logic, you'd write a server route that deducts this amount before updating the database. But blockchains are public; users can bypass your server and transact directly. With the transfer fee extension, the rule is baked into the token's DNA.&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%2Ft27lemvg996tigcmmimy.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%2Ft27lemvg996tigcmmimy.png" alt="Token Instruction" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you attach this extension, the blockchain &lt;em&gt;will not allow&lt;/em&gt; a transfer to succeed unless the fee is properly calculated and withheld. This is what it means to enforce economic rules in code rather than policy documents. It requires zero custom smart contracts—just standard configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Soulbound: Non-Transferable Tokens&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The final extension that blew my mind was creating a non-transferable token. In the Web3 world, these are sometimes called "Soulbound" tokens. They are permanent credentials glued to your wallet.&lt;/p&gt;

&lt;p&gt;Think about a university degree or an event ticket. You want the person to own it, but you definitely don't want them selling it on an exchange. &lt;/p&gt;

&lt;p&gt;By applying the &lt;code&gt;NonTransferable&lt;/code&gt; extension during mint creation, the token becomes forever locked to the account it is minted to. If someone tries to &lt;code&gt;solana transfer &amp;lt;mint&amp;gt; &amp;lt;amount&amp;gt; &amp;lt;recipient&amp;gt;&lt;/code&gt;, the transaction will simply fail. The blockchain itself rejects it. &lt;/p&gt;

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

&lt;p&gt;The biggest moment for me was realizing just how much heavy lifting the Solana protocol can do for developers out of the box. Coming from an environment where I normally have to write an entire backend service to manage custom tokenomics, the fact that I could enable transfer fees and non-transferability just by passing a few flags during initialization was stunning. &lt;/p&gt;

&lt;p&gt;It feels less like writing complex financial software and more like configuring a highly secure, decentralized operating system. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What’s Next?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Building these tokens locally and via the CLI on devnet was an incredible foundation. Next week, I plan to dive into how these tokens interact with actual custom on-chain programs (smart contracts). Now that I understand the assets, I want to write the logic that trades, manages, and interacts with them.&lt;/p&gt;

&lt;p&gt;If you are a Web2 developer sitting on the fence, jump in. The water is fine, and the tooling has never been better. Follow along on my #100DaysOfSolana journey!&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>#100DaysOfSolana Week4 Recap</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 17 May 2026 21:57:29 +0000</pubDate>
      <link>https://dev.to/russell_oje/100daysofsolana-week4-recap-4ack</link>
      <guid>https://dev.to/russell_oje/100daysofsolana-week4-recap-4ack</guid>
      <description>&lt;p&gt;I opened my devnet account in Solana Explorer today and it clicked how much the explorer reveals: balance, owner, executable flag, and full transaction history in one view. I like Solana Explorer for this clarity.&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%2Fy0y57att1yuoxrcc87mx.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%2Fy0y57att1yuoxrcc87mx.png" alt="Solana Explorer" width="800" height="912"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#100DaysOfSolana&lt;/code&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>Week4 of #100DaysOfSolana: Solana's Account Model</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 17 May 2026 21:47:11 +0000</pubDate>
      <link>https://dev.to/russell_oje/week4-of-100daysofsolana-solanas-account-model-4254</link>
      <guid>https://dev.to/russell_oje/week4-of-100daysofsolana-solanas-account-model-4254</guid>
      <description>&lt;p&gt;If you come from Web2, Solana's account model can feel strange at first. The first time I inspected an account with the CLI, I saw fields like &lt;code&gt;lamports&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, and &lt;code&gt;data&lt;/code&gt; all sitting together and thought, "Wait, where is the smart contract state stored?"&lt;/p&gt;

&lt;p&gt;That question is the whole lesson. On Solana, everything is an account. Wallets are accounts. Program code lives in accounts. Application state lives in accounts. There is no separate world of contract accounts versus externally owned accounts like you may have seen elsewhere. Solana uses one flat key-value store where the key is a 32-byte address and the value is the account itself.&lt;/p&gt;

&lt;p&gt;That design clicked for me when I started thinking about accounts like files in a filesystem. Each file has metadata, contents, and permissions. Solana accounts work the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the address is the filename&lt;/li&gt;
&lt;li&gt;the owner is the program allowed to manage it&lt;/li&gt;
&lt;li&gt;the data is the file contents&lt;/li&gt;
&lt;li&gt;the balance is the lamports stored alongside it&lt;/li&gt;
&lt;li&gt;the executable flag tells you whether the file is runnable code or just data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That analogy is not perfect, but it gets you close fast. The System Program is like the operating system kernel. It creates accounts, assigns ownership, and handles basic bookkeeping. Other programs then read and write the accounts they own.&lt;/p&gt;

&lt;p&gt;Every Solana account has the same five fields:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;lamports&lt;/code&gt; - the balance stored in the account. One SOL equals 1 billion lamports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data&lt;/code&gt; - a byte array that can hold arbitrary state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;owner&lt;/code&gt; - the program that controls the account.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;executable&lt;/code&gt; - whether the account contains runnable program code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rent_epoch&lt;/code&gt; - a legacy field that is now effectively retired and set to the maximum value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most important rule is ownership. Only the owner program can modify an account's data or debit its lamports. That means a token program can update token accounts it owns, but nobody else can casually rewrite those bytes. At the same time, anyone can credit lamports to a writable account. That combination is simple, but it creates a very clear security model.&lt;/p&gt;

&lt;p&gt;What surprised me most was the statelessness of programs. In Web2 terms, a program is not a server that keeps its own memory forever. It is more like a web server binary that reads from and writes to a database. The executable account stores the code, and separate data accounts store the application state. That separation is why the account model matters so much: if you understand who owns the account and what data it contains, you understand how the program behaves.&lt;/p&gt;

&lt;p&gt;Here is the kind of output I kept seeing while exploring accounts:&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%2F7efzo1a28y91axwcgqik.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%2F7efzo1a28y91axwcgqik.png" alt="Account" width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each field tells a story. A zero-length &lt;code&gt;data&lt;/code&gt; field means the account is just holding value. The owner &lt;code&gt;11111111111111111111111111111111&lt;/code&gt; is the System Program. &lt;code&gt;executable: false&lt;/code&gt; means this is not a program account. And the huge &lt;code&gt;rentEpoch&lt;/code&gt; value is a clue that rent exemption has taken over the old rent schedule.&lt;/p&gt;

&lt;p&gt;Rent exemption is another detail that makes Solana feel different from a traditional backend. Every account must hold a minimum lamport balance proportional to its data size if it wants to stay on-chain. If the account drops below that threshold, it risks being purged over time. For a tiny basic account, the minimum is around 0.00089 SOL, though the exact value depends on the account size. You can check it with &lt;code&gt;solana rent&lt;/code&gt; or the &lt;code&gt;getMinimumBalanceForRentExemption&lt;/code&gt; RPC method.&lt;/p&gt;

&lt;p&gt;That idea matters because it forces you to think about storage costs up front. If you create a larger state account, you need to fund it accordingly. In Web2, disk space is usually someone else's problem. On Solana, the cost of storage is part of the design.&lt;/p&gt;

&lt;p&gt;The account model also explains why Solana feels so composable. Programs do not hide their state inside private memory. They operate on explicit accounts that can be inspected, passed around, and reasoned about independently. Once I stopped thinking in terms of "a contract with internal state" and started thinking in terms of "a program that manages files," the architecture made much more sense.&lt;/p&gt;

&lt;p&gt;If you are a Web2 developer, that is the mental model I would keep: Solana is a filesystem for programmable state. Accounts are the files. Programs are the executables. The System Program is the kernel. And ownership is the permission system that keeps everything safe.&lt;/p&gt;

&lt;p&gt;That framing turned Solana from something mysterious into something structured. The account model is not just an implementation detail. It is the core abstraction that makes the rest of Solana understandable.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
