<?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: 100 Days of Solana</title>
    <description>The latest articles on DEV Community by 100 Days of Solana (100daysofsolana).</description>
    <link>https://dev.to/100daysofsolana</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%2Forganization%2Fprofile_image%2F12963%2F62b6ce6d-db04-444b-88d1-4bea32484faa.png</url>
      <title>DEV Community: 100 Days of Solana</title>
      <link>https://dev.to/100daysofsolana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/100daysofsolana"/>
    <language>en</language>
    <item>
      <title>Arc 11 Catch-Up: Composing Solana Programs with CPIs</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Thu, 16 Jul 2026 12:46:25 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-11-catch-up-composing-solana-programs-with-cpis-406</link>
      <guid>https://dev.to/100daysofsolana/arc-11-catch-up-composing-solana-programs-with-cpis-406</guid>
      <description>&lt;p&gt;Arc 11 covered Days 71–77 of Epoch 3, and it was all about Cross-Program Invocations.&lt;/p&gt;

&lt;p&gt;In Arc 9, we wrote our first Solana program.&lt;/p&gt;

&lt;p&gt;In Arc 10, we gave that program a more useful state model with Program Derived Addresses.&lt;/p&gt;

&lt;p&gt;But our programs were still mostly working alone.&lt;/p&gt;

&lt;p&gt;They could read and update their own accounts, enforce their own constraints, and respond to instructions sent by a client. They could not directly change state owned by another program or bypass the rules that program enforced.&lt;/p&gt;

&lt;p&gt;That is an important part of Solana’s security model.&lt;/p&gt;

&lt;p&gt;The System Program owns the rules for creating accounts and transferring lamports.&lt;/p&gt;

&lt;p&gt;Token-2022 owns the rules for mints, token accounts, supply, and mint authorities.&lt;/p&gt;

&lt;p&gt;If our program needs one of those capabilities, it calls the program that owns the operation.&lt;/p&gt;

&lt;p&gt;That call is a Cross-Program Invocation, or CPI.&lt;/p&gt;

&lt;p&gt;The Web2 comparison is a service-to-service API call. One service sends a request through another service’s public interface, and the receiving service applies its own rules.&lt;/p&gt;

&lt;p&gt;A CPI works in a similar way, with one important difference: the outer and inner instructions execute as part of the same Solana transaction. If the inner call fails, the state changes made by the outer instruction are rolled back too.&lt;/p&gt;

&lt;p&gt;That combination of clear program boundaries and atomic execution is what makes Solana programs composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our first CPI called the System Program
&lt;/h2&gt;

&lt;p&gt;The arc began with the smallest useful CPI we could build.&lt;/p&gt;

&lt;p&gt;Our Anchor program accepted a sender, a recipient, and an amount of SOL to transfer.&lt;/p&gt;

&lt;p&gt;But the program did not edit the sender’s balance directly.&lt;/p&gt;

&lt;p&gt;Instead, it called the System Program’s transfer instruction.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Accounts on Solana are owned by programs, and the owner program controls how their data may be changed. Our program could not simply reproduce the effect of a System Program transfer by adjusting balances itself.&lt;/p&gt;

&lt;p&gt;It had to ask the System Program to perform the operation.&lt;/p&gt;

&lt;p&gt;Every CPI needs three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program being called&lt;/li&gt;
&lt;li&gt;the accounts that program expects&lt;/li&gt;
&lt;li&gt;the authority required to approve the action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the SOL transfer, the called program was the System Program.&lt;/p&gt;

&lt;p&gt;The required accounts were the sender and recipient.&lt;/p&gt;

&lt;p&gt;The authority came from the sender, who had signed the original transaction.&lt;/p&gt;

&lt;p&gt;Our program assembled those pieces and made the inner call. The System Program then applied its own rules and either accepted or rejected the transfer.&lt;/p&gt;

&lt;p&gt;That gave us the basic CPI model we used throughout the arc.&lt;/p&gt;

&lt;p&gt;The caller requests an operation.&lt;/p&gt;

&lt;p&gt;The callee validates the request.&lt;/p&gt;

&lt;p&gt;The callee remains responsible for the state it owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signer authority can flow into an inner call
&lt;/h2&gt;

&lt;p&gt;The sender in that first exercise was a normal wallet signer.&lt;/p&gt;

&lt;p&gt;The wallet signed the outer transaction sent by the client. When our program called the System Program, that signer privilege was also available to the inner instruction.&lt;/p&gt;

&lt;p&gt;The CPI did not invent new authority.&lt;/p&gt;

&lt;p&gt;It passed along authority that already existed in the transaction.&lt;/p&gt;

&lt;p&gt;The same applies to writable accounts. A program cannot make an account writable during a CPI if the original transaction supplied it as read-only.&lt;/p&gt;

&lt;p&gt;That gives us an important boundary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A CPI cannot arbitrarily escalate account privileges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writable access must already have been granted by the transaction. Signer authority must come either from an existing signer or from a PDA controlled by the calling program.&lt;/p&gt;

&lt;p&gt;That second form of authority became the major step forward later in the arc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token-2022 followed the same pattern
&lt;/h2&gt;

&lt;p&gt;The next exercise moved from transferring SOL to minting tokens.&lt;/p&gt;

&lt;p&gt;Our program called Token-2022’s &lt;code&gt;mint_to&lt;/code&gt; instruction to increase a mint’s supply and deposit the new tokens into a destination token account.&lt;/p&gt;

&lt;p&gt;The accounts were different, but the CPI model stayed the same.&lt;/p&gt;

&lt;p&gt;The call needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Token-2022 program&lt;/li&gt;
&lt;li&gt;the mint&lt;/li&gt;
&lt;li&gt;the destination token account&lt;/li&gt;
&lt;li&gt;the mint authority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mint authority was still a regular wallet signer, so its signature flowed from the outer transaction into the Token-2022 instruction.&lt;/p&gt;

&lt;p&gt;Token-2022 then applied its own rules.&lt;/p&gt;

&lt;p&gt;It checked that the mint and destination accounts were valid, that the supplied authority matched the mint’s recorded authority, and that the authority had signed.&lt;/p&gt;

&lt;p&gt;Our program did not need to recreate any token logic.&lt;/p&gt;

&lt;p&gt;It only needed to assemble a valid request.&lt;/p&gt;

&lt;p&gt;That is the larger value of learning the CPI pattern.&lt;/p&gt;

&lt;p&gt;Once we understand the combination of program, accounts, and authority, existing Solana programs become building blocks we can call from our own code.&lt;/p&gt;

&lt;p&gt;The System Program already knows how to transfer SOL.&lt;/p&gt;

&lt;p&gt;Token-2022 already knows how to manage token supply.&lt;/p&gt;

&lt;p&gt;Our program can use those capabilities while each called program keeps control of its own rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CPI still crosses a real program boundary
&lt;/h2&gt;

&lt;p&gt;Anchor’s CPI helpers can make the interaction look similar to calling a local function.&lt;/p&gt;

&lt;p&gt;But the called program does not simply trust the caller.&lt;/p&gt;

&lt;p&gt;It receives an instruction, examines the supplied accounts, checks their privileges and relationships, and runs its own logic.&lt;/p&gt;

&lt;p&gt;The caller decides what it wants to request.&lt;/p&gt;

&lt;p&gt;The callee decides whether that request is valid.&lt;/p&gt;

&lt;p&gt;For the System Program, that means enforcing the rules for lamport transfers.&lt;/p&gt;

&lt;p&gt;For Token-2022, it means enforcing the rules for mints and token accounts.&lt;/p&gt;

&lt;p&gt;For another Anchor program, it means applying that program’s account constraints and instruction logic.&lt;/p&gt;

&lt;p&gt;The inner program is therefore more than a shared code library.&lt;/p&gt;

&lt;p&gt;It is a separate on-chain authority with its own state and security boundary.&lt;/p&gt;

&lt;p&gt;That is what makes composition useful. Programs can depend on one another without surrendering control of the accounts they own.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDA signing gave the program its own authority
&lt;/h2&gt;

&lt;p&gt;The first two CPIs used wallet signers.&lt;/p&gt;

&lt;p&gt;That gave us a useful baseline: the user signed the outer transaction, and the inner program recognised the same authority.&lt;/p&gt;

&lt;p&gt;The next challenge introduced a different situation.&lt;/p&gt;

&lt;p&gt;We built a per-user SOL vault at a PDA.&lt;/p&gt;

&lt;p&gt;Each user had a predictable vault address derived from the program’s seeds.&lt;/p&gt;

&lt;p&gt;Depositing into the vault was straightforward. The user controlled the source account and signed the transaction, so that authority could flow into the inner transfer.&lt;/p&gt;

&lt;p&gt;Withdrawing from the vault raised a harder question.&lt;/p&gt;

&lt;p&gt;The vault was a PDA.&lt;/p&gt;

&lt;p&gt;It had no private key and could not sign a transaction in the way a wallet could.&lt;/p&gt;

&lt;p&gt;The answer was a PDA-signed CPI.&lt;/p&gt;

&lt;p&gt;When the program made the inner call, it supplied the original seeds and canonical bump used to derive the vault.&lt;/p&gt;

&lt;p&gt;The Solana runtime combined those values with the calling program’s ID and derived the address again.&lt;/p&gt;

&lt;p&gt;If the result matched the vault account, the runtime treated that PDA as a signer for the inner invocation.&lt;/p&gt;

&lt;p&gt;The program never obtained or stored a private key for the PDA.&lt;/p&gt;

&lt;p&gt;It proved authority by reproducing the derivation that created the address.&lt;/p&gt;

&lt;p&gt;That is the core of PDA signing.&lt;/p&gt;

&lt;p&gt;A program can control an on-chain account or asset through a deterministic address, then authorise actions for it only when its own instruction logic allows those actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signer seeds are not secrets
&lt;/h2&gt;

&lt;p&gt;The phrase “signer seeds” can make the seeds sound like passwords.&lt;/p&gt;

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

&lt;p&gt;Anyone can inspect the program and see the seed scheme. The bump may also be visible in account data or derived again by a client.&lt;/p&gt;

&lt;p&gt;That does not let another user or program impersonate the PDA.&lt;/p&gt;

&lt;p&gt;The program ID is part of the derivation.&lt;/p&gt;

&lt;p&gt;Another program could use the same seed values, but its own program ID would produce a different address.&lt;/p&gt;

&lt;p&gt;The runtime only grants signer privilege when the seeds, bump, calling program ID, and account address all match.&lt;/p&gt;

&lt;p&gt;The security comes from that relationship, along with the program logic that controls when the signer seeds are used.&lt;/p&gt;

&lt;p&gt;This gives a program its own deterministic on-chain identity without introducing another private key that someone has to create, protect, and rotate.&lt;/p&gt;

&lt;p&gt;A PDA can hold SOL, control a token mint, own a token account, or act as an authority for another program.&lt;/p&gt;

&lt;p&gt;The program decides when that authority may be exercised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programs can call other custom programs
&lt;/h2&gt;

&lt;p&gt;The first exercises called established Solana programs.&lt;/p&gt;

&lt;p&gt;Day 74 took the next step: one custom Anchor program called another.&lt;/p&gt;

&lt;p&gt;The first program owned a tally account and exposed an instruction that incremented it.&lt;/p&gt;

&lt;p&gt;The second program invoked that instruction through a CPI.&lt;/p&gt;

&lt;p&gt;The client sent a transaction to the caller program. The caller then invoked the counter program, which updated the account it owned.&lt;/p&gt;

&lt;p&gt;The client never called the counter instruction directly, but the tally still increased.&lt;/p&gt;

&lt;p&gt;That demonstrated an important boundary.&lt;/p&gt;

&lt;p&gt;The caller program could not edit the counter program’s account itself.&lt;/p&gt;

&lt;p&gt;It had to request the update through the counter program’s public instruction.&lt;/p&gt;

&lt;p&gt;The counter program remained responsible for validating the accounts and deciding whether the update was allowed.&lt;/p&gt;

&lt;p&gt;That is how larger on-chain systems can be assembled from smaller programs.&lt;/p&gt;

&lt;p&gt;One program can provide a focused capability.&lt;/p&gt;

&lt;p&gt;Another can use that capability as part of a wider workflow.&lt;/p&gt;

&lt;p&gt;The callee remains in control of its state, while the caller gains access to its public behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IDL becomes a contract between programs
&lt;/h2&gt;

&lt;p&gt;To call another Anchor program, the caller needs to understand its public interface.&lt;/p&gt;

&lt;p&gt;It needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program ID&lt;/li&gt;
&lt;li&gt;the available instructions&lt;/li&gt;
&lt;li&gt;the arguments those instructions accept&lt;/li&gt;
&lt;li&gt;the accounts they require&lt;/li&gt;
&lt;li&gt;the account and data types they expose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anchor describes that interface in an IDL.&lt;/p&gt;

&lt;p&gt;The caller can use the callee’s IDL to generate typed CPI bindings. It does not need to copy or compile the callee’s source code into its own program.&lt;/p&gt;

&lt;p&gt;That is similar to using an API specification to call another service.&lt;/p&gt;

&lt;p&gt;The caller depends on the published contract rather than the internal implementation.&lt;/p&gt;

&lt;p&gt;The callee can refactor its code while keeping callers compatible, provided that its external instruction and account interface remains stable.&lt;/p&gt;

&lt;p&gt;But changes to that interface can break integrations.&lt;/p&gt;

&lt;p&gt;Renaming an instruction, changing its arguments, adding required accounts, or altering an account layout may affect programs and clients that already depend on it.&lt;/p&gt;

&lt;p&gt;Once other programs compose with ours, the IDL is no longer only a development convenience.&lt;/p&gt;

&lt;p&gt;It becomes part of the interface other software expects us to preserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole call stack remains atomic
&lt;/h2&gt;

&lt;p&gt;A CPI does not create a separate transaction.&lt;/p&gt;

&lt;p&gt;The inner instruction executes as part of the transaction that called the outer program.&lt;/p&gt;

&lt;p&gt;Consider an instruction that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates one of its own accounts.&lt;/li&gt;
&lt;li&gt;Calls Token-2022 to mint tokens.&lt;/li&gt;
&lt;li&gt;Updates another account after the mint succeeds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Token-2022 rejects the mint, the entire transaction fails.&lt;/p&gt;

&lt;p&gt;The state update performed before the CPI does not remain on-chain.&lt;/p&gt;

&lt;p&gt;There is no partially completed result where the caller records that the operation succeeded but the token mint did not change.&lt;/p&gt;

&lt;p&gt;Everything succeeds together or everything is rolled back.&lt;/p&gt;

&lt;p&gt;Anyone who has written compensation logic or reconciliation jobs for distributed workflows will recognise why that matters.&lt;/p&gt;

&lt;p&gt;It allows a Solana program to combine operations across several programs without having to repair state when one step in the middle fails.&lt;/p&gt;

&lt;p&gt;It also means that an error deep in a chain of CPIs can cause every preceding change in the transaction to be reverted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking CPIs showed where integrations fail
&lt;/h2&gt;

&lt;p&gt;Once the working paths were in place, we deliberately broke them.&lt;/p&gt;

&lt;p&gt;We tested three common categories of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;incorrect PDA signer seeds or bump&lt;/li&gt;
&lt;li&gt;missing or incorrect accounts&lt;/li&gt;
&lt;li&gt;the wrong program ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each failure exposed a different part of the contract between caller and callee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong signer seeds
&lt;/h3&gt;

&lt;p&gt;For a PDA-signed CPI, the runtime derives the PDA again using the supplied seeds, bump, and calling program ID.&lt;/p&gt;

&lt;p&gt;If any of those inputs are wrong, the result does not match the account expected to sign.&lt;/p&gt;

&lt;p&gt;The inner instruction then sees that its required authority is missing.&lt;/p&gt;

&lt;p&gt;A one-byte difference is enough to break the derivation.&lt;/p&gt;

&lt;p&gt;When debugging, the key question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do these exact seeds and this bump derive the exact PDA being supplied as the authority?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If they do not, the runtime cannot grant signer privilege.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong accounts
&lt;/h3&gt;

&lt;p&gt;Every instruction expects a particular set of accounts.&lt;/p&gt;

&lt;p&gt;Those accounts may need to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writable&lt;/li&gt;
&lt;li&gt;signers&lt;/li&gt;
&lt;li&gt;owned by a particular program&lt;/li&gt;
&lt;li&gt;derived from particular seeds&lt;/li&gt;
&lt;li&gt;linked through a stored relationship&lt;/li&gt;
&lt;li&gt;associated with a particular mint or authority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passing the wrong account often produces an Anchor constraint error.&lt;/p&gt;

&lt;p&gt;Those errors can identify both the account and the validation that failed.&lt;/p&gt;

&lt;p&gt;The callee’s accounts struct is therefore more than setup code.&lt;/p&gt;

&lt;p&gt;It is the contract the caller must satisfy.&lt;/p&gt;

&lt;p&gt;When a CPI fails, comparing the supplied accounts against that contract is often the fastest route to the cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong program ID
&lt;/h3&gt;

&lt;p&gt;The caller must also invoke the correct program.&lt;/p&gt;

&lt;p&gt;Typed program accounts help protect this boundary. Anchor can validate the supplied program account before attempting the CPI and reject an unexpected ID during account validation.&lt;/p&gt;

&lt;p&gt;With a loosely typed program account, that protection may not exist.&lt;/p&gt;

&lt;p&gt;The runtime can invoke the program it was given, even when the instruction data was intended for someone else.&lt;/p&gt;

&lt;p&gt;The error then comes from the unintended program trying to interpret an instruction it does not recognise.&lt;/p&gt;

&lt;p&gt;This is one reason typed accounts are valuable.&lt;/p&gt;

&lt;p&gt;They turn a confusing downstream failure into a clearer validation failure at the program boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transaction logs reveal the call stack
&lt;/h2&gt;

&lt;p&gt;CPI debugging becomes easier when we stop looking only at the final error code.&lt;/p&gt;

&lt;p&gt;The transaction logs show the sequence of execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the outer program begins&lt;/li&gt;
&lt;li&gt;it reaches the CPI&lt;/li&gt;
&lt;li&gt;the inner program is invoked&lt;/li&gt;
&lt;li&gt;the inner program validates its accounts&lt;/li&gt;
&lt;li&gt;an instruction succeeds or fails&lt;/li&gt;
&lt;li&gt;the result propagates back through the call stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sequence tells us which program was running when the failure occurred.&lt;/p&gt;

&lt;p&gt;Most CPI bugs come from one of two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The caller assembled the request incorrectly.&lt;/li&gt;
&lt;li&gt;The callee enforced a rule that the supplied request did not satisfy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which program produced the error?&lt;/li&gt;
&lt;li&gt;Which inner instruction was being called?&lt;/li&gt;
&lt;li&gt;Did the caller provide every required account?&lt;/li&gt;
&lt;li&gt;Were the signer and writable privileges correct?&lt;/li&gt;
&lt;li&gt;Did the signer seeds derive the expected PDA?&lt;/li&gt;
&lt;li&gt;Did Anchor identify a failed constraint?&lt;/li&gt;
&lt;li&gt;Was the intended program actually invoked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A CPI failure is usually the on-chain equivalent of an API request that did not match the receiving service’s contract.&lt;/p&gt;

&lt;p&gt;The logs help us find the point where that mismatch occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the explanation forced us to choose the important part
&lt;/h2&gt;

&lt;p&gt;The final two days moved from implementation to communication.&lt;/p&gt;

&lt;p&gt;CPIs include enough moving parts that it is tempting to explain all of them at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;program ownership&lt;/li&gt;
&lt;li&gt;nested instructions&lt;/li&gt;
&lt;li&gt;account privileges&lt;/li&gt;
&lt;li&gt;wallet signers&lt;/li&gt;
&lt;li&gt;PDA signing&lt;/li&gt;
&lt;li&gt;IDLs&lt;/li&gt;
&lt;li&gt;atomicity&lt;/li&gt;
&lt;li&gt;transaction logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can produce an explanation that is technically complete but hard to follow.&lt;/p&gt;

&lt;p&gt;The challenge instead asked us to choose one clear thesis.&lt;/p&gt;

&lt;p&gt;One possible framing was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A CPI is a function call with a guest list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The called instruction needs the right program, the right accounts, and the right authority.&lt;/p&gt;

&lt;p&gt;Another focused on the major conceptual jump:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signer seeds are how a program proves control of its PDA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The seeds are not secret credentials. They let the runtime reconstruct the PDA under the calling program’s ID and grant it signer privilege for one inner invocation.&lt;/p&gt;

&lt;p&gt;The public explanation needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one clear mental model&lt;/li&gt;
&lt;li&gt;one small example&lt;/li&gt;
&lt;li&gt;the three ingredients of a CPI&lt;/li&gt;
&lt;li&gt;one real error encountered during the week&lt;/li&gt;
&lt;li&gt;evidence from the working program&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That evidence might be passing test output, a GitHub repository, a concise implementation excerpt, or a devnet transaction.&lt;/p&gt;

&lt;p&gt;As in the previous arcs, publishing the work was part of the learning process.&lt;/p&gt;

&lt;p&gt;Explaining where authority comes from, which program owns the operation, and why a broken CPI fails reveals gaps that successful tests alone may not expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 11 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 11 moved us from isolated programs to composed systems.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had seen how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call the System Program from an Anchor instruction&lt;/li&gt;
&lt;li&gt;transfer SOL through a CPI&lt;/li&gt;
&lt;li&gt;call Token-2022 to mint tokens&lt;/li&gt;
&lt;li&gt;supply the program, accounts, and authority required by a callee&lt;/li&gt;
&lt;li&gt;pass wallet signer authority into an inner instruction&lt;/li&gt;
&lt;li&gt;understand the limits on signer and writable privileges&lt;/li&gt;
&lt;li&gt;authorise an inner call with a PDA&lt;/li&gt;
&lt;li&gt;control on-chain assets without storing a private key&lt;/li&gt;
&lt;li&gt;call one custom Anchor program from another&lt;/li&gt;
&lt;li&gt;use an IDL as the contract between programs&lt;/li&gt;
&lt;li&gt;treat instruction and account changes as compatibility decisions&lt;/li&gt;
&lt;li&gt;break CPIs with incorrect seeds, accounts, and program IDs&lt;/li&gt;
&lt;li&gt;read transaction logs as a record of the program call stack&lt;/li&gt;
&lt;li&gt;explain program composition through a clear, concrete example&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The enduring lesson is that Solana programs are interoperable pieces of infrastructure.&lt;/p&gt;

&lt;p&gt;A program that needs to transfer SOL can call the System Program.&lt;/p&gt;

&lt;p&gt;A program that needs to mint tokens can call Token-2022.&lt;/p&gt;

&lt;p&gt;A program that needs behaviour exposed by another custom program can call that program through its public instruction interface.&lt;/p&gt;

&lt;p&gt;Each callee keeps control of its accounts and applies its own authorisation rules.&lt;/p&gt;

&lt;p&gt;The caller gains the capability without copying the implementation or bypassing the program that owns the state.&lt;/p&gt;

&lt;p&gt;That is what CPIs make possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana programs can combine trusted on-chain capabilities into one atomic transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 11 challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 71:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f137c-fad9-d4c3-1645-e60101686d4b" rel="noopener noreferrer"&gt;Call the System Program from your own program to transfer SOL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 72:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1411-f45c-f3ea-c369-7d3fccce3e9e" rel="noopener noreferrer"&gt;Use a CPI to mint tokens through Token-2022&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 73:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1439-7cef-b445-1226-94ae3bfd1656" rel="noopener noreferrer"&gt;Build a PDA-controlled SOL vault and sign a CPI with seeds&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 74:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f17eb-4a8d-1431-746a-0b2e037b6c31" rel="noopener noreferrer"&gt;Call one custom Anchor program from another through its IDL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 75:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1806-4c0d-27a9-66f4-599a3c4c4f79" rel="noopener noreferrer"&gt;Break working CPIs and debug signer, account, and program errors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 76:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1826-6ce3-acfb-f786-2f34571083f4" rel="noopener noreferrer"&gt;Write a clear public explanation of how Cross-Program Invocations work&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 77:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1835-8e6d-27d6-4264-d40d141c8257" rel="noopener noreferrer"&gt;Share your composed program with code, tests, and on-chain evidence&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Arc 10 Catch-Up: Designing Solana State with PDAs</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:18:42 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-10-catch-up-designing-solana-state-with-pdas-44if</link>
      <guid>https://dev.to/100daysofsolana/arc-10-catch-up-designing-solana-state-with-pdas-44if</guid>
      <description>&lt;p&gt;Arc 10 covered Days 64–70 of Epoch 3, and it was all about Program Derived Addresses.&lt;/p&gt;

&lt;p&gt;Arc 9 gave us our first Solana program.&lt;/p&gt;

&lt;p&gt;We built a counter, stored its state in an account, restricted updates to the correct authority, and used LiteSVM to prove those rules worked.&lt;/p&gt;

&lt;p&gt;But the counter account had a limitation.&lt;/p&gt;

&lt;p&gt;It used a randomly generated keypair for its address.&lt;/p&gt;

&lt;p&gt;That is manageable in a small exercise. The client creates the account, keeps hold of its public key, and passes that address back whenever it wants to increment the counter.&lt;/p&gt;

&lt;p&gt;It becomes awkward once we want one counter per user.&lt;/p&gt;

&lt;p&gt;Where do we store all those addresses? How does a user find their counter from another device? How does the program know that the account it received is the correct counter for that wallet?&lt;/p&gt;

&lt;p&gt;Arc 10 answered those questions with Program Derived Addresses, or PDAs.&lt;/p&gt;

&lt;p&gt;A PDA gives a Solana program a predictable way to create and locate state. Its address comes from the program ID and a set of seeds chosen by the program. For our counter, that meant deriving an address from the word &lt;code&gt;counter&lt;/code&gt; and the user's public key.&lt;/p&gt;

&lt;p&gt;The same inputs always produce the same address. Different users produce different addresses.&lt;/p&gt;

&lt;p&gt;That makes PDA design part of both the program's state model and its security model.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA gives program state a predictable address
&lt;/h2&gt;

&lt;p&gt;Before Arc 10, our client created the counter account using a fresh keypair.&lt;/p&gt;

&lt;p&gt;That gave the account a unique address, but there was no relationship between the address and what the account represented.&lt;/p&gt;

&lt;p&gt;The address did not tell us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which program used the account&lt;/li&gt;
&lt;li&gt;which user owned the counter&lt;/li&gt;
&lt;li&gt;whether it was the correct counter for a particular instruction&lt;/li&gt;
&lt;li&gt;how to find it again without saving the address somewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PDAs solve this by deriving an address from known inputs.&lt;/p&gt;

&lt;p&gt;In JavaScript, we can derive one using &lt;code&gt;findProgramAddressSync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counterPda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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;The inputs here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program ID&lt;/li&gt;
&lt;li&gt;the seed &lt;code&gt;counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a bump value found during derivation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given those same inputs, we get the same PDA every time.&lt;/p&gt;

&lt;p&gt;That is the first important property: determinism.&lt;/p&gt;

&lt;p&gt;We can close the script, run it again tomorrow, and derive the same address without storing it in a database or configuration file.&lt;/p&gt;

&lt;p&gt;The second important property is that every byte matters.&lt;/p&gt;

&lt;p&gt;Changing the seed from &lt;code&gt;counter&lt;/code&gt; to &lt;code&gt;alice&lt;/code&gt; produces a completely different address:&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;alicePda&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;alice&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;Changing it again to &lt;code&gt;bob&lt;/code&gt; produces another:&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;bobPda&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;bob&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;Restoring the original seed gives us the original PDA again.&lt;/p&gt;

&lt;p&gt;That small experiment made the model tangible.&lt;/p&gt;

&lt;p&gt;A PDA is a deterministic address calculated from program-specific inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA has no private key
&lt;/h2&gt;

&lt;p&gt;There is another important difference between a PDA and an ordinary wallet address.&lt;/p&gt;

&lt;p&gt;A normal Solana keypair contains a public key and a private key. The private key can sign transactions on behalf of the public key.&lt;/p&gt;

&lt;p&gt;A PDA is deliberately derived so that it falls outside the ed25519 curve used for Solana keypairs.&lt;/p&gt;

&lt;p&gt;That means there is no corresponding private key.&lt;/p&gt;

&lt;p&gt;Nobody can generate a secret key for the PDA and sign as it directly.&lt;/p&gt;

&lt;p&gt;Instead, the program associated with the PDA can authorise actions for it by supplying the same seeds and bump used to derive it.&lt;/p&gt;

&lt;p&gt;This is especially useful when a program needs to control assets or call another program through a Cross-Program Invocation. The PDA can act as an authority without requiring someone to store and protect another private key.&lt;/p&gt;

&lt;p&gt;We did not need all of that capability for our counter, but the underlying rule matters:&lt;/p&gt;

&lt;p&gt;The program controls the derivation logic, and the derivation logic determines which address the program recognises.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA is an address before it is an account
&lt;/h2&gt;

&lt;p&gt;One easy mistake is to talk about deriving a PDA as though we have created an account.&lt;/p&gt;

&lt;p&gt;We have not.&lt;/p&gt;

&lt;p&gt;Derivation only calculates an address.&lt;/p&gt;

&lt;p&gt;We can derive a PDA that has never held any data, lamports, tokens, or executable code. Until an instruction creates an account at that address, there is nothing there to fetch.&lt;/p&gt;

&lt;p&gt;That gives us two separate steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address.&lt;/li&gt;
&lt;li&gt;Initialise an account at that address.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anchor helps us combine those steps when the account is first created.&lt;/p&gt;

&lt;p&gt;But the distinction is useful when debugging.&lt;/p&gt;

&lt;p&gt;If we derive the correct PDA and &lt;code&gt;getAccountInfo&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, that does not necessarily mean the derivation failed. It may simply mean the account has not been initialised yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the user creates one counter per wallet
&lt;/h2&gt;

&lt;p&gt;The fixed seed &lt;code&gt;[b"counter"]&lt;/code&gt; gives us one address for the entire program.&lt;/p&gt;

&lt;p&gt;That could be useful for a global account, but it cannot give every user their own counter.&lt;/p&gt;

&lt;p&gt;To do that, we added the user's public key to the seed list:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the PDA depends on both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the fixed namespace &lt;code&gt;counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the public key of the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wallet A gets an address derived from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"counter" + wallet A public key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wallet B gets an address derived from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"counter" + wallet B public key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the public keys differ, the resulting PDAs differ.&lt;/p&gt;

&lt;p&gt;That gives us a predictable one-counter-per-wallet model.&lt;/p&gt;

&lt;p&gt;The client does not need to create a random keypair for the counter.&lt;/p&gt;

&lt;p&gt;It does not need to store the counter address in local storage.&lt;/p&gt;

&lt;p&gt;It does not need to query a registry to discover which account belongs to the user.&lt;/p&gt;

&lt;p&gt;It can derive the address again whenever it needs it.&lt;/p&gt;

&lt;p&gt;In Web2 terms, it is similar to locating a row using a known composite key. The program defines the namespace, the wallet identifies the user, and the combination identifies the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor can create the account at the derived address
&lt;/h2&gt;

&lt;p&gt;We expressed that seed scheme inside our Anchor accounts struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="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;InitializeCounter&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;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;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;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;Several constraints work together here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; tells Anchor to create the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;payer = user&lt;/code&gt; says the user will fund the account's rent-exempt lamport deposit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;space = 8 + Counter::INIT_SPACE&lt;/code&gt; allocates enough storage for the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seeds&lt;/code&gt; declares how the PDA should be derived.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bump&lt;/code&gt; tells Anchor to find and use the canonical bump for that derivation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;space&lt;/code&gt; value deserves a moment of attention. With &lt;code&gt;InitSpace&lt;/code&gt;, Anchor calculates the size of the account's fields for us, but the allocation still needs an extra eight bytes for Anchor's account discriminator. The discriminator identifies which Anchor account type the data belongs to, and it helps prevent one account type from being deserialised as another simply because its bytes happen to have a compatible shape. Getting this wrong can stop the account from initialising or leave too little space for its data.&lt;/p&gt;

&lt;p&gt;The account itself stores the user, the count, and the bump:&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;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initialisation handler can then populate those fields:&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_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InitializeCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.bumps.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="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;Anchor has already checked the account address and created the account before this handler runs.&lt;/p&gt;

&lt;p&gt;The handler only needs to write the initial state.&lt;/p&gt;

&lt;p&gt;That follows the same pattern we saw in Arc 9:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the accounts struct describes the required accounts and validates them&lt;/li&gt;
&lt;li&gt;the handler performs the instruction's state change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bump completes the derivation
&lt;/h2&gt;

&lt;p&gt;The bump can feel mysterious when we first encounter it.&lt;/p&gt;

&lt;p&gt;Solana needs PDAs to be off the ed25519 curve so that no private key exists for them.&lt;/p&gt;

&lt;p&gt;The derivation process starts with the program ID and the seeds, then tries bump values until it finds an address that satisfies that requirement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findProgramAddressSync&lt;/code&gt; returns both results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counterPda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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;user&lt;/span&gt;&lt;span class="p"&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;toBuffer&lt;/span&gt;&lt;span class="p"&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;Anchor normally handles this search for us.&lt;/p&gt;

&lt;p&gt;When we write a bare &lt;code&gt;bump&lt;/code&gt; during initialisation, Anchor runs the search, finds the canonical bump, and makes it available through &lt;code&gt;ctx.bumps.counter&lt;/code&gt; so we can store it in the account.&lt;/p&gt;

&lt;p&gt;That stored value is why later instructions use a different form:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Anchor: do not run the search again. Take the stored bump, perform a single derivation with it, and check the result against the supplied account.&lt;/p&gt;

&lt;p&gt;The search loop costs compute units, and it would otherwise run on every instruction that touches the account. Storing the bump once at initialisation and reusing it turns that repeated search into a single calculation.&lt;/p&gt;

&lt;p&gt;So the bump is part of how Solana finds a valid off-curve address, and storing it is a small optimisation the program pays for once and benefits from on every subsequent instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeds are validation rules
&lt;/h2&gt;

&lt;p&gt;At first, PDAs can look like an address-generation convenience.&lt;/p&gt;

&lt;p&gt;They save the client from keeping track of random account addresses.&lt;/p&gt;

&lt;p&gt;That is useful, but it is only half of the story.&lt;/p&gt;

&lt;p&gt;The same seeds also let Anchor verify that an instruction received the correct account.&lt;/p&gt;

&lt;p&gt;Our increment instruction used the counter's PDA constraints again:&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(&lt;/span&gt;
        &lt;span class="nd"&gt;mut,&lt;/span&gt;
        &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="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;user&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;Before the handler runs, Anchor can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the user's public key.&lt;/li&gt;
&lt;li&gt;Combine it with the &lt;code&gt;counter&lt;/code&gt; seed.&lt;/li&gt;
&lt;li&gt;Re-derive the expected PDA.&lt;/li&gt;
&lt;li&gt;Compare that address with the supplied counter account.&lt;/li&gt;
&lt;li&gt;Check that the account's stored &lt;code&gt;user&lt;/code&gt; field matches the signer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of those checks fail, the transaction is rejected before the instruction changes state.&lt;/p&gt;

&lt;p&gt;Strictly speaking, the seed constraint alone already binds this counter to the signer, because the signer's public key is part of the derivation. The &lt;code&gt;has_one = user&lt;/code&gt; check overlaps with it here, acting as defence in depth. Where &lt;code&gt;has_one&lt;/code&gt; really earns its keep is when the seeds do not include the related key, and the stored relationship is the only thing tying the account to a signer. We will see that shortly with the config account.&lt;/p&gt;

&lt;p&gt;Either way, the program does not need to repeat address checks inside every handler.&lt;/p&gt;

&lt;p&gt;The account constraints declare the invariant once:&lt;/p&gt;

&lt;p&gt;This instruction accepts the counter derived for this user, and that counter must record the same user internally.&lt;/p&gt;

&lt;p&gt;The handler can then remain focused on the state change:&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 seed constraint proves we received the expected address.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;has_one&lt;/code&gt; constraint proves the account data records the expected relationship.&lt;/p&gt;

&lt;p&gt;Together, they protect the counter from substitution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A spoofing attempt showed the security boundary
&lt;/h2&gt;

&lt;p&gt;We tested those constraints by deliberately passing the wrong account.&lt;/p&gt;

&lt;p&gt;Wallet A had its own counter PDA.&lt;/p&gt;

&lt;p&gt;Wallet B had a different counter PDA.&lt;/p&gt;

&lt;p&gt;Then Wallet A called &lt;code&gt;increment&lt;/code&gt; while supplying Wallet B's counter account.&lt;/p&gt;

&lt;p&gt;Without PDA validation, the program might have accepted any account with the right data shape.&lt;/p&gt;

&lt;p&gt;With the seed constraint in place, Anchor derived the counter address expected for Wallet A and compared it with the supplied address.&lt;/p&gt;

&lt;p&gt;They did not match.&lt;/p&gt;

&lt;p&gt;The instruction failed before the handler executed.&lt;/p&gt;

&lt;p&gt;That test made the security role of PDAs much clearer.&lt;/p&gt;

&lt;p&gt;The PDA is not secure merely because its address looks unusual. The protection comes from the program declaring the correct derivation and validating that derivation whenever the account is used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global state needs a different seed scheme
&lt;/h2&gt;

&lt;p&gt;The per-user counter gave each wallet its own state.&lt;/p&gt;

&lt;p&gt;Real applications often need another category of state: information that applies to the whole program.&lt;/p&gt;

&lt;p&gt;For that, we added a configuration PDA:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this derivation does not include a user public key, every caller derives the same address.&lt;/p&gt;

&lt;p&gt;That makes it suitable for singleton state.&lt;/p&gt;

&lt;p&gt;Our config account stored fields such as:&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;Config&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;admin&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;paused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;total_counters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the program had two kinds of state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one global configuration account&lt;/li&gt;
&lt;li&gt;one counter account per user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The config account could hold program-wide policy.&lt;/p&gt;

&lt;p&gt;The counter account could hold user-specific data.&lt;/p&gt;

&lt;p&gt;The two can also meet in a single instruction. Our &lt;code&gt;initialize_counter&lt;/code&gt; handler incremented &lt;code&gt;config.total_counters&lt;/code&gt; alongside creating the user's counter, so one transaction touched both the global account and the per-user account. That pattern, one instruction updating global and scoped state together, appears constantly in real programs.&lt;/p&gt;

&lt;p&gt;A program might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one global marketplace configuration&lt;/li&gt;
&lt;li&gt;one position per trader&lt;/li&gt;
&lt;li&gt;one vault per token mint&lt;/li&gt;
&lt;li&gt;one profile per wallet&lt;/li&gt;
&lt;li&gt;one pool account per asset pair&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The seed scheme tells us which state is shared and which state is scoped to a particular entity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints let us express relationships and business rules
&lt;/h2&gt;

&lt;p&gt;As the program gained more state, Anchor constraints became its main validation language.&lt;/p&gt;

&lt;p&gt;For example:&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;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"config"&lt;/span&gt;&lt;span class="nd"&gt;]&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;config&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;admin&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;config&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;Config&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;admin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;has_one = admin&lt;/code&gt; checks that the public key stored in &lt;code&gt;config.admin&lt;/code&gt; matches the signer passed as &lt;code&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice that the config seeds contain no user key, so the derivation alone cannot tell us who is allowed to administer the account. The stored &lt;code&gt;admin&lt;/code&gt; field is the only link, and &lt;code&gt;has_one&lt;/code&gt; is what enforces it. This is the case where &lt;code&gt;has_one&lt;/code&gt; is doing essential work rather than doubling up on the seed check.&lt;/p&gt;

&lt;p&gt;If the counter accounts are rows keyed by user ID, the config account is the settings table: one row, program-wide, with a recorded owner. And &lt;code&gt;has_one&lt;/code&gt; behaves much like a foreign-key check, verifying that a stored reference actually points at the account the instruction received. The difference from a Web2 database is that these checks execute on-chain, as part of every transaction.&lt;/p&gt;

&lt;p&gt;We could also enforce the pause rule directly on the account. To do that, the increment instruction's accounts struct gains the config account alongside the counter:&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;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"config"&lt;/span&gt;&lt;span class="nd"&gt;]&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;config&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;constraint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.paused&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;config&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;Config&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;Because the config account is now part of the instruction's required accounts, its constraints run during account validation, and an increment fails while the program is paused.&lt;/p&gt;

&lt;p&gt;The handler does not need to contain a separate conditional:&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;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.paused&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nd"&gt;err!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CounterError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ProgramPaused&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;That does not mean every business rule belongs in an account constraint. Some rules are clearer inside the handler.&lt;/p&gt;

&lt;p&gt;But Arc 10 showed how much of the program's structure can be expressed declaratively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seeds&lt;/code&gt; identifies the expected PDA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bump&lt;/code&gt; completes its derivation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_one&lt;/code&gt; checks a stored relationship&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constraint&lt;/code&gt; applies a custom condition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mut&lt;/code&gt; marks accounts whose state may change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signer&lt;/code&gt; proves that the required wallet authorised the transaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints run before the handler.&lt;/p&gt;

&lt;p&gt;The instruction only reaches its state-changing logic after the required accounts and relationships have been validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Singleton initialisation needs an explicit governance decision
&lt;/h2&gt;

&lt;p&gt;Our exercise allowed the first caller of &lt;code&gt;init_config&lt;/code&gt; to become the administrator.&lt;/p&gt;

&lt;p&gt;That kept the initialisation code approachable and let us focus on PDAs and constraints.&lt;/p&gt;

&lt;p&gt;It should not be copied into a production program without considering the consequences.&lt;/p&gt;

&lt;p&gt;If anyone can initialise the singleton config account, then whoever reaches it first may become the program administrator.&lt;/p&gt;

&lt;p&gt;A production deployment should define who is allowed to perform that initial setup.&lt;/p&gt;

&lt;p&gt;Depending on the program, that authority might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a known deployment wallet&lt;/li&gt;
&lt;li&gt;the program's upgrade authority&lt;/li&gt;
&lt;li&gt;a multisig&lt;/li&gt;
&lt;li&gt;an existing governance account&lt;/li&gt;
&lt;li&gt;another PDA with its own initialisation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct design depends on how the program will be governed.&lt;/p&gt;

&lt;p&gt;The important lesson is that a singleton PDA gives us one canonical address. It does not decide who should be allowed to create or control the account at that address.&lt;/p&gt;

&lt;p&gt;That decision still belongs to the program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing an account completes the state lifecycle
&lt;/h2&gt;

&lt;p&gt;Creating an account costs lamports.&lt;/p&gt;

&lt;p&gt;The payer deposits enough lamports to make the account rent-exempt, and those lamports remain attached to the account while it exists.&lt;/p&gt;

&lt;p&gt;When the state is no longer needed, the program can close the account and return those lamports.&lt;/p&gt;

&lt;p&gt;We added a &lt;code&gt;close_counter&lt;/code&gt; instruction using Anchor's &lt;code&gt;close&lt;/code&gt; constraint:&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;CloseCounter&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;mut,&lt;/span&gt;
        &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;close&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="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;user&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 familiar constraints still protect the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; verify that this is the user's canonical counter PDA.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_one = user&lt;/code&gt; checks the ownership relationship stored inside the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;close = user&lt;/code&gt; tells Anchor where to send the account's remaining lamports.&lt;/p&gt;

&lt;p&gt;The handler itself can be empty:&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;close_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CloseCounter&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="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;Once the instruction completes, Anchor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfers the account's lamports to the user&lt;/li&gt;
&lt;li&gt;invalidates the account data&lt;/li&gt;
&lt;li&gt;leaves no live account at that PDA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can still derive the same address later because the derivation inputs have not changed.&lt;/p&gt;

&lt;p&gt;But fetching the account will show that it no longer exists.&lt;/p&gt;

&lt;p&gt;That makes closing a PDA-backed account feel different from deleting a row in a normal database. We are releasing on-chain storage and returning its refundable deposit.&lt;/p&gt;

&lt;p&gt;It also shows the full lifecycle of program state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address.&lt;/li&gt;
&lt;li&gt;Initialise the account.&lt;/li&gt;
&lt;li&gt;Read and mutate its data.&lt;/li&gt;
&lt;li&gt;Enforce who may use it.&lt;/li&gt;
&lt;li&gt;Close it when it is no longer needed.&lt;/li&gt;
&lt;li&gt;Recover the lamports that funded it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Seed design defines the state namespace
&lt;/h2&gt;

&lt;p&gt;The experimentation day made us change the seeds and observe the consequences.&lt;/p&gt;

&lt;p&gt;First, we derived counters for two users using:&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two wallets produced two different addresses.&lt;/p&gt;

&lt;p&gt;That is the behaviour we wanted.&lt;/p&gt;

&lt;p&gt;Then we removed the user public key and derived both counters from:&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="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both wallets now produced the same address.&lt;/p&gt;

&lt;p&gt;There was no cryptographic collision. The derivation worked exactly as designed.&lt;/p&gt;

&lt;p&gt;The problem was our design.&lt;/p&gt;

&lt;p&gt;We had asked the program for one global &lt;code&gt;counter&lt;/code&gt; address, then tried to use it as though every user had their own.&lt;/p&gt;

&lt;p&gt;Only the first account could be initialised there. The second user would attempt to initialise an address that was already occupied.&lt;/p&gt;

&lt;p&gt;This is one of the most important lessons from the arc.&lt;/p&gt;

&lt;p&gt;Seed design determines the namespace of the program's state.&lt;/p&gt;

&lt;p&gt;Including a user public key creates user-scoped state.&lt;/p&gt;

&lt;p&gt;Including a mint creates mint-scoped state.&lt;/p&gt;

&lt;p&gt;Including two asset addresses could create one account per pair.&lt;/p&gt;

&lt;p&gt;Using only a fixed seed creates a singleton.&lt;/p&gt;

&lt;p&gt;The program will follow the scheme exactly, even when the scheme does not match the application we intended to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Almost identical seeds are still different seeds
&lt;/h2&gt;

&lt;p&gt;We also tried small changes to the seed bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;counter
counters
Counter
counter\0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each produced a different PDA.&lt;/p&gt;

&lt;p&gt;Those values may look related to a person, but the derivation process does not interpret their meaning.&lt;/p&gt;

&lt;p&gt;It only sees bytes.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uppercase and lowercase values differ&lt;/li&gt;
&lt;li&gt;singular and plural values differ&lt;/li&gt;
&lt;li&gt;invisible terminators or extra bytes matter&lt;/li&gt;
&lt;li&gt;changing seed order changes the result&lt;/li&gt;
&lt;li&gt;changing the program ID changes the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes seed conventions worth planning.&lt;/p&gt;

&lt;p&gt;Once a program is deployed and accounts have been created, changing a seed can make the new version derive different addresses from the accounts that already exist.&lt;/p&gt;

&lt;p&gt;The old state is still on-chain, but the updated program may no longer look for it at the same address.&lt;/p&gt;

&lt;p&gt;So seeds should be treated as part of the program's persistent interface, rather than temporary labels we can rename without consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the explanation exposed the gaps
&lt;/h2&gt;

&lt;p&gt;The last two days moved away from program code.&lt;/p&gt;

&lt;p&gt;First, we wrote a long-form explanation of PDAs.&lt;/p&gt;

&lt;p&gt;That meant describing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why random account keypairs become inconvenient&lt;/li&gt;
&lt;li&gt;how seeds and program IDs produce deterministic addresses&lt;/li&gt;
&lt;li&gt;what the bump does&lt;/li&gt;
&lt;li&gt;why PDAs have no private keys&lt;/li&gt;
&lt;li&gt;why deriving an address does not create an account&lt;/li&gt;
&lt;li&gt;how Anchor validates PDA constraints&lt;/li&gt;
&lt;li&gt;how seed design affects ownership and account scope&lt;/li&gt;
&lt;li&gt;how PDA-backed accounts can be closed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we turned that explanation into a shorter social post or thread.&lt;/p&gt;

&lt;p&gt;The public version needed a useful example and some evidence from the work: code, test output, terminal output, or an Explorer screenshot.&lt;/p&gt;

&lt;p&gt;That final step matters because PDAs can feel clear while we are following an exercise and become much harder to explain without the scaffolding.&lt;/p&gt;

&lt;p&gt;Writing the model in plain language forces us to answer the questions that code alone can hide.&lt;/p&gt;

&lt;p&gt;Why is the address predictable?&lt;/p&gt;

&lt;p&gt;Why can nobody hold its private key?&lt;/p&gt;

&lt;p&gt;Why does adding a wallet public key produce one account per user?&lt;/p&gt;

&lt;p&gt;Why does a seed constraint prevent account substitution?&lt;/p&gt;

&lt;p&gt;Why does the address remain derivable after the account is closed?&lt;/p&gt;

&lt;p&gt;If we cannot answer those questions clearly, we probably need another pass through the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 10 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 10 turned account addresses into part of the program design.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had seen how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derive a PDA from a program ID, seeds, and a bump&lt;/li&gt;
&lt;li&gt;verify that the same inputs always produce the same address&lt;/li&gt;
&lt;li&gt;create one state account per user&lt;/li&gt;
&lt;li&gt;initialise an Anchor account at a PDA&lt;/li&gt;
&lt;li&gt;distinguish a derived address from an initialised account&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; to validate an account&lt;/li&gt;
&lt;li&gt;store the bump once and reuse it to save compute&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;has_one&lt;/code&gt; to bind stored state to a signer&lt;/li&gt;
&lt;li&gt;combine global configuration with per-user state&lt;/li&gt;
&lt;li&gt;enforce a pause rule before the handler runs&lt;/li&gt;
&lt;li&gt;close an account and reclaim its lamports&lt;/li&gt;
&lt;li&gt;test account-substitution attempts&lt;/li&gt;
&lt;li&gt;observe how incomplete seed schemes create logical address conflicts&lt;/li&gt;
&lt;li&gt;explain PDA design in public&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The counter still performed the same basic job as it did in Arc 9.&lt;/p&gt;

&lt;p&gt;It initialised a number, stored an owner, and allowed that number to be incremented.&lt;/p&gt;

&lt;p&gt;But the surrounding design became much more useful.&lt;/p&gt;

&lt;p&gt;Each wallet could find its counter without saving a random address.&lt;/p&gt;

&lt;p&gt;The program could prove that the supplied counter belonged in that user's namespace.&lt;/p&gt;

&lt;p&gt;Global policy lived at one predictable config address.&lt;/p&gt;

&lt;p&gt;User state lived at a different predictable address for every wallet.&lt;/p&gt;

&lt;p&gt;And when the state was no longer needed, the user could close it and recover the lamports used to create it.&lt;/p&gt;

&lt;p&gt;That is the lasting lesson from Arc 10.&lt;/p&gt;

&lt;p&gt;PDA design is state-model design.&lt;/p&gt;

&lt;p&gt;The seeds decide what state exists, how many instances can exist, what each instance belongs to, and how the program can locate and validate it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 10 challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 64:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019eefab-9ea1-8eb3-2534-15e1ae96435d" rel="noopener noreferrer"&gt;Derive your first Program Derived Address and see how seeds change the result&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 65:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019eefe7-365e-7146-2928-4ce4f6270721" rel="noopener noreferrer"&gt;Replace random counter keypairs with one predictable PDA per user&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 66:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef011-f8d2-14bc-753f-f67456e0039c" rel="noopener noreferrer"&gt;Add a global configuration PDA and enforce admin and pause rules&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 67:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef02b-7b80-7dc2-e9b7-8a0875c844b1" rel="noopener noreferrer"&gt;Close a user's counter and return its rent-exempt lamports&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 68:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef03d-6aed-7a5e-bc15-fb93f3992732" rel="noopener noreferrer"&gt;Break the seed scheme and test whether account spoofing succeeds&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 69:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f0424-79a6-431c-f492-9aa6ced5f3ac" rel="noopener noreferrer"&gt;Write a long-form explanation of how PDAs shape program state&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 70:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f04a8-8ebc-44f2-ec7d-d3e642ec1d97" rel="noopener noreferrer"&gt;Turn your PDA explanation into a public post with evidence from your work&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>learning</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Welcome to Epoch 4: Shipping and Exploring</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:34:40 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/welcome-to-epoch-4-shipping-and-exploring-1080</link>
      <guid>https://dev.to/100daysofsolana/welcome-to-epoch-4-shipping-and-exploring-1080</guid>
      <description>&lt;p&gt;Arc 13 of 100 Days of Solana starts today and it opens Epoch 4: Ship and Explore.&lt;/p&gt;

&lt;p&gt;Every tutorial has the same ending. The program builds, the tests pass, it runs on devnet, and the article stops. Which is a strange place to stop, because that's exactly where a Web2 developer would say the interesting part begins: the production deploy.&lt;/p&gt;

&lt;p&gt;Arc 13 is seven days on that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mainnet is not just another cluster
&lt;/h2&gt;

&lt;p&gt;On devnet, everything is free and nothing is permanent. Airdropped SOL, throwaway deploys, program addresses nobody will ever call. It's staging, and it behaves like staging.&lt;/p&gt;

&lt;p&gt;Mainnet-beta changes four things at once. The deploy costs real SOL — program accounts are rent-exempt, and for a non-trivial Anchor program that's a real (if modest) amount of money. The program address becomes public infrastructure: anyone can find it, read it, and call it. Whoever holds the upgrade authority can replace the program's code entirely, which makes that keypair equivalent to root credentials on a production box. And there's no access control at the network level — strangers can send transactions to your program the moment it lands.&lt;/p&gt;

&lt;p&gt;None of this is scary if you've run production systems before. It's the same discipline — deploy intentionally, control who holds the keys, publish a contract for integrators, handle failure states in front of users — wearing different names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rosetta Stone
&lt;/h2&gt;

&lt;p&gt;If you know the Web2 column, Arc 13 teaches you the Solana column:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Web2&lt;/th&gt;
&lt;th&gt;Solana&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Staging → production&lt;/td&gt;
&lt;td&gt;Devnet → mainnet-beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release artifact&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anchor build&lt;/code&gt; output (the &lt;code&gt;.so&lt;/code&gt; file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production deploy rights&lt;/td&gt;
&lt;td&gt;Upgrade authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch protection / required reviewers&lt;/td&gt;
&lt;td&gt;Multisig upgrade authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI / GraphQL schema&lt;/td&gt;
&lt;td&gt;IDL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated typed SDK&lt;/td&gt;
&lt;td&gt;Codama client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Sign in with Google"&lt;/td&gt;
&lt;td&gt;Wallet Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public changelog / release page&lt;/td&gt;
&lt;td&gt;Explorer link&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one that surprises most people is the IDL. Solana programs don't self-describe the way a REST API with an OpenAPI spec does — unless you publish the IDL, at which point other developers can generate typed clients, inspect your instructions, and integrate without reading your source. Publishing it is the difference between a deployed program and a usable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven days
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 85&lt;/strong&gt; — Deploy an Anchor program to mainnet-beta. The staging-to-production promotion, done deliberately rather than by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 86&lt;/strong&gt; — Decide who is allowed to upgrade the program. Keep the authority, transfer it to a multisig, or burn it entirely — each is a real product decision with real trade-offs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 87&lt;/strong&gt; — Publish the IDL and generate a typed client, so other developers can call the program without guessing at byte layouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 88&lt;/strong&gt; — Connect a wallet from the frontend and send a transaction. This is where a human finally meets the program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 89&lt;/strong&gt; — Write useful messages for failed transactions. Wallet errors are production UX, not an edge case to be polished later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 90&lt;/strong&gt; — Write a production launch checklist. The artifact future-you actually needs at 11pm before the next deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 91&lt;/strong&gt; — Launch publicly, with an explorer link. A deploy isn't done until someone else can verify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you'll have at the end
&lt;/h2&gt;

&lt;p&gt;A program on mainnet-beta, a frontend that talks to it, and a public launch post backed by a verifiable on-chain receipt. Not "I completed a course" — "here's my program ID, go look."&lt;/p&gt;

&lt;p&gt;That receipt matters more than it might seem. In Web2, a side project claim is just a claim unless it's deployed somewhere. On Solana, the explorer link is proof: the deploy transaction, the upgrade authority, the program's activity, all publicly inspectable. It's the strongest possible artifact for a portfolio or a launch post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Joining
&lt;/h2&gt;

&lt;p&gt;Interested but not started yet? No problem. You can learn the fundamentals at your own pace with our previous 84 challenges!&lt;/p&gt;

&lt;p&gt;Start here: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;https://mlh.link/solana-100&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>learning</category>
    </item>
    <item>
      <title>From Devnet to Mainnet: What Changes When Your Solana Program Goes Live</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 10 Jul 2026 21:46:13 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/from-devnet-to-mainnet-what-changes-when-your-solana-program-goes-live-hg2</link>
      <guid>https://dev.to/100daysofsolana/from-devnet-to-mainnet-what-changes-when-your-solana-program-goes-live-hg2</guid>
      <description>&lt;p&gt;There's a moment in every Solana project where the work stops being about whether the program &lt;em&gt;works&lt;/em&gt; and starts being about whether it's &lt;em&gt;ready&lt;/em&gt;. You've tested it, the logic holds, the constraints are tight. Then you point it at mainnet, and a different set of questions shows up: questions about money, permanence, and strangers.&lt;/p&gt;

&lt;p&gt;This post is about that transition. Not the commands, which are short and well documented, but the shift in what you're responsible for once real users can touch your code. If you've been building on devnet and you're starting to think about a live launch, this is the mental model to carry in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Devnet was a sandbox. Mainnet is not.
&lt;/h2&gt;

&lt;p&gt;Devnet is a practice field. The SOL is free, you airdrop more whenever you run low, and if you deploy something broken, the only casualty is your afternoon. That safety is the whole point of devnet: it lets you fail cheaply and often, which is exactly how you should be learning.&lt;/p&gt;

&lt;p&gt;Mainnet removes the safety net, and three things change the moment you cross over.&lt;/p&gt;

&lt;p&gt;The SOL is real. Deploying a program allocates an on-chain account sized to your compiled binary, and you pay rent for that space in actual SOL. Larger programs cost more. This isn't a huge sum for a typical program, but it's real money leaving a real wallet, and that alone tends to sharpen how carefully you check things before you hit deploy.&lt;/p&gt;

&lt;p&gt;The audience is real. On devnet the only person calling your program is you. On mainnet, anyone can find your program and send it any transaction they like, the moment it's live. Everything from the security arc stops being theoretical: the accounts strangers pass in, the inputs you didn't expect, the edge cases you hoped no one would hit. Mainnet is where "every account is attacker-controlled until proven otherwise" becomes a live condition rather than a lesson.&lt;/p&gt;

&lt;p&gt;The mistakes are visible. A bad devnet deploy disappears into the noise. A bad mainnet deploy is a public event, on a permanent ledger, in front of the users you were hoping to attract. This is a feature as much as a risk: the same permanence that raises the stakes is what lets users trust the program without trusting you personally.&lt;/p&gt;

&lt;p&gt;None of this should scare you off. It's just the difference between a rehearsal and opening night, and the way you prepare for opening night is by knowing which decisions are reversible and which are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-way door: upgrade authority
&lt;/h2&gt;

&lt;p&gt;Here's the single most important concept to understand before you launch, because it's the one decision on Solana that you genuinely cannot take back.&lt;/p&gt;

&lt;p&gt;When you deploy a program, your wallet becomes its &lt;strong&gt;upgrade authority&lt;/strong&gt;. That's the key that lets you push new versions of the program later, fixing bugs, adding features, changing logic. As long as you hold the upgrade authority, your deployed program is mutable: you can replace its code.&lt;/p&gt;

&lt;p&gt;That power cuts both ways, and it's the central tension of shipping on Solana:&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;keep&lt;/em&gt; the upgrade authority, you can fix bugs after launch, which is a real safety valve. But your users have to trust that you won't push a malicious update that drains them. An upgradeable program is only as trustworthy as whoever holds its authority.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;revoke&lt;/em&gt; the upgrade authority, the program becomes immutable and can never be changed again. Users get maximum trust, the code is the code, forever, but you've also given up your ability to patch a bug you discover next week. Revoking is permanent: there is no undo, no recovery, no support ticket that brings it back.&lt;/p&gt;

&lt;p&gt;This is why teams rarely treat it as a binary. Many keep the authority early, when bugs are most likely, then move it to a multisig so no single person can push an update alone, and only consider full immutability once the program has been battle-tested and audited. The point isn't which choice is correct, it's that you make it deliberately, understanding that revocation is a door that only opens one way.&lt;/p&gt;

&lt;p&gt;You can always check who holds a program's authority, which matters because it's also how &lt;em&gt;you&lt;/em&gt; evaluate anyone else's program before trusting it with your funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your program is now a public API
&lt;/h2&gt;

&lt;p&gt;On devnet your program was a thing you called from your own test file. On mainnet it becomes infrastructure that other people's code talks to, and that reframes a few things worth previewing.&lt;/p&gt;

&lt;p&gt;Other developers need to know how to call your program. The typed interface that describes your instructions and accounts, the IDL, becomes a public contract. Publishing it lets others generate clients and build on top of you, the same way a REST API publishes its schema. Your program stops being private code and becomes something composable.&lt;/p&gt;

&lt;p&gt;Real users connect through wallets, not keypairs in a file. On devnet you signed with a local keypair. Real users have browser wallets, and connecting to them means speaking a shared standard so any wallet works, not just the one you happen to use. The interface between your app and a stranger's wallet is now part of your product.&lt;/p&gt;

&lt;p&gt;Users see errors you didn't write. When a transaction fails on mainnet, the user doesn't see your clean &lt;code&gt;require!&lt;/code&gt; message, they might see a raw RPC error, a rejected signature, an insufficient-funds failure. Part of shipping is catching those and turning them into something a human can understand, so a failed transaction is a clear message rather than a scary wall of text. The difference between a program and a product is often just this layer of care.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mindset to carry in
&lt;/h2&gt;

&lt;p&gt;The whole shift can be summed up in one sentence: on devnet you were proving the program works, and on mainnet you're taking responsibility for it in the world.&lt;/p&gt;

&lt;p&gt;That responsibility is mostly about knowing which decisions are reversible. Deploying is reversible, you can upgrade, redeploy, or close and reclaim your SOL while you hold the authority. Revoking the upgrade authority is not reversible. Neither is the ledger's memory of a launch. So the craft of shipping is sequencing: do the reversible things freely, approach the irreversible ones slowly, and never let a permanent decision be the one you made in a hurry.&lt;/p&gt;

&lt;p&gt;A short version of what "ready" looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your program does what it should, and refuses what it shouldn't, because mainnet is where the second half gets tested by strangers.&lt;/li&gt;
&lt;li&gt;You've decided, on purpose, what happens to your upgrade authority, and when.&lt;/li&gt;
&lt;li&gt;The people who'll build on or use your program can actually reach it: a published interface, a wallet connection, and errors that make sense.&lt;/li&gt;
&lt;li&gt;You know which of your launch steps you can undo and which you can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deployment feels like the finish line, but it's better to think of it as the handoff, the moment your program stops being yours alone and starts being something the world can use. Everything up to now was making it correct. Shipping is making it ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/programs/deploying" rel="noopener noreferrer"&gt;Solana program deployment docs&lt;/a&gt; cover the actual commands for deploying, transferring the upgrade authority, and making a program immutable, including how to check who currently holds authority over any program. The &lt;a href="https://solana.com/docs/core/programs/program-deployment" rel="noopener noreferrer"&gt;program deployment overview&lt;/a&gt; explains the upgrade mechanism itself: buffer accounts, the program data account, and what setting the authority to &lt;code&gt;None&lt;/code&gt; does. For the client side, &lt;a href="https://solana.com/docs/programs/codama/clients" rel="noopener noreferrer"&gt;Codama's client generation docs&lt;/a&gt; walk through turning a program's IDL into a typed client others can build with.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc walks this exact path end to end: promoting a program to mainnet, handling the upgrade-authority decision, publishing a client, wiring up a wallet, and launching with on-chain proof. Reading this first means the stakes will already make sense when you get there. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>learning</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Account Security on Solana, Made Simple</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 03 Jul 2026 20:21:08 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/account-security-on-solana-made-simple-154d</link>
      <guid>https://dev.to/100daysofsolana/account-security-on-solana-made-simple-154d</guid>
      <description>&lt;p&gt;When you write a normal backend, you mostly trust your own database. A row you wrote is a row you can read back, and the data is what you put there. Solana works differently, and the difference is the single most important thing to understand about writing safe programs.&lt;/p&gt;

&lt;p&gt;On Solana, your program is handed a list of accounts with every instruction, and any of those accounts can be anything the caller wants. The caller picks them. An account is just an address, a balance, some bytes, and a field saying which program owns it, and a caller is free to hand your program an account they built themselves, filled with whatever bytes serve them. Your program has no trusted edge. Every account is attacker-controlled until your code proves otherwise.&lt;/p&gt;

&lt;p&gt;That sounds alarming, but it collapses into something manageable. Most account security on Solana comes down to two questions you ask about every account in every instruction. Learn to ask them and a whole class of expensive bugs stops being mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift: from "what did I mean" to "what did I forget to forbid"
&lt;/h2&gt;

&lt;p&gt;When you're building a feature, you think about the happy path: a user calls this, the program does that, everyone's content. Security asks a different question. Not "what did I intend this to do," but "what did I forget to forbid." Those are not the same question, and the gap between them is where exploits live.&lt;/p&gt;

&lt;p&gt;A useful way to hold it: when you write a program, you imagine the user you designed for. An attacker is every user you didn't. They will pass the account you didn't expect, sign with a key you didn't intend, and send the number you assumed no one would. Your job is to make the program say no to all of them, out loud, before it touches anything.&lt;/p&gt;

&lt;p&gt;The good news is you don't need to imagine every attack. Two questions cover most of the ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question one: who owns this account?
&lt;/h2&gt;

&lt;p&gt;If your program reads data off an account and trusts it, the program needs to know which program owns that account.&lt;/p&gt;

&lt;p&gt;Here's why it matters. Say your program has a &lt;code&gt;Config&lt;/code&gt; account that stores an admin's public key, and your withdraw logic reads &lt;code&gt;config.admin&lt;/code&gt; to decide who's allowed. If you read those bytes without checking who owns the account, an attacker can create their own account, write a &lt;code&gt;Config&lt;/code&gt; into it that names &lt;em&gt;themselves&lt;/em&gt; as admin, and hand it to your program. The bytes deserialize perfectly. The struct says what they want it to say. Your check passes, and they walk out with the funds.&lt;/p&gt;

&lt;p&gt;The defense is an owner check: confirm the account is owned by the program that's supposed to own it. A real &lt;code&gt;Config&lt;/code&gt; your program created is owned by your program. The attacker's forgery is owned by the System Program (or whatever they chose), so an owner check rejects it on sight.&lt;/p&gt;

&lt;p&gt;This exact gap, an account trusted without verifying what it was, is behind some of the largest losses in Solana's history. It isn't exotic cryptography. It's a missing question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question two: did the authority actually sign?
&lt;/h2&gt;

&lt;p&gt;The second question is about permission. If an account decides whether an action is allowed, an account named &lt;code&gt;authority&lt;/code&gt; or &lt;code&gt;admin&lt;/code&gt; or &lt;code&gt;owner&lt;/code&gt;, the program has to confirm that account actually signed the transaction.&lt;/p&gt;

&lt;p&gt;The trap here is subtle and worth slowing down for. It's tempting to check authority by comparing public keys: "does the admin field on this account equal the admin pubkey I expect?" But a public key is public. Anyone can put anyone's pubkey in a transaction. Comparing pubkeys only proves that someone &lt;em&gt;knew&lt;/em&gt; a public key, which is no proof at all, since they're visible on chain to everyone.&lt;/p&gt;

&lt;p&gt;What you actually need to know is that the holder of the matching &lt;em&gt;private&lt;/em&gt; key approved this transaction. That's what a signature proves and a pubkey comparison doesn't. So the rule is: an authority must sign, not just match. Checking the pubkey without checking the signature is the precise mistake behind one of the most famous bridge exploits, where the program compared an account but never confirmed it had signed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Anchor answers both questions for you
&lt;/h2&gt;

&lt;p&gt;If you're using Anchor, the reassuring part is that the account &lt;em&gt;types&lt;/em&gt; answer these questions automatically. You often get the checks for free, as long as you reach for the right type.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; answers the owner question. When you type an account as &lt;code&gt;Account&amp;lt;'info, Config&amp;gt;&lt;/code&gt;, Anchor verifies the account is owned by your program and that its first eight bytes match the &lt;code&gt;Config&lt;/code&gt; type's discriminator, before your handler runs. The forged account from question one never makes it through.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; answers the signer question. Typing an account as &lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; makes Anchor confirm it actually signed the transaction. No pubkey-comparison trap, because you're checking the signature itself.&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;Withdraw&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="c1"&gt;// owner + discriminator checked automatically&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;config&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;Config&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// signature checked automatically&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 danger lives in the escape hatch. When you reach for &lt;code&gt;UncheckedAccount&amp;lt;'info&amp;gt;&lt;/code&gt; or &lt;code&gt;AccountInfo&amp;lt;'info&amp;gt;&lt;/code&gt;, Anchor checks &lt;em&gt;nothing&lt;/em&gt;: no owner, no signer, no type. Sometimes you need that, but every time you use it you've quietly promised to do the validation by hand. Anchor even makes you write a &lt;code&gt;/// CHECK:&lt;/code&gt; comment above it to acknowledge the promise. The bugs tend to be the moments someone reached for an unchecked account to make something compile, then forgot they'd taken on that promise.&lt;/p&gt;

&lt;p&gt;So a lot of Anchor security is simply: use the typed account instead of the raw one, and let the framework ask the two questions for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binding accounts together: the constraints
&lt;/h2&gt;

&lt;p&gt;The types answer "is this a real account my program owns" and "did this account sign." One question they can't answer on their own is whether two accounts &lt;em&gt;belong together&lt;/em&gt;. Does this &lt;code&gt;config&lt;/code&gt; belong to this &lt;code&gt;authority&lt;/code&gt;? For that, Anchor gives you constraints that live right on 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;#[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;Withdraw&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;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="nd"&gt;#[account(&lt;/span&gt;
        &lt;span class="nd"&gt;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="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;config&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;Config&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; checks that the &lt;code&gt;authority&lt;/code&gt; field stored inside &lt;code&gt;config&lt;/code&gt; equals the &lt;code&gt;authority&lt;/code&gt; account that signed. It binds the on-chain record to the live signer, so a valid signer can't operate on a config that isn't theirs. There are more of these (&lt;code&gt;seeds&lt;/code&gt;/&lt;code&gt;bump&lt;/code&gt; to confirm a PDA, &lt;code&gt;address&lt;/code&gt; to pin an exact key, &lt;code&gt;constraint&lt;/code&gt; for any boolean you like), but they all share one idea: declare the rule next to the account, and the runtime enforces it before your handler runs. A rule on the struct can't be forgotten in a refactor the way a check buried in handler logic can.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model to carry in
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing in one frame. A Web2 API often validates once at the edge and trusts the request afterward. A Solana program has no edge. Every account on every instruction is untrusted input, every time. Security is the practice of declaring your assumptions in a place the runtime checks them, so the program refuses bad input by construction rather than by you remembering to write an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you read an instruction with that lens, the checklist is short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For every account whose data you trust, ask: is its owner verified? (&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; does this.)&lt;/li&gt;
&lt;li&gt;For every account that authorizes an action, ask: did it actually sign? (&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; does this.)&lt;/li&gt;
&lt;li&gt;For accounts that must belong together, ask: are they bound? (&lt;code&gt;has_one&lt;/code&gt;, &lt;code&gt;seeds&lt;/code&gt;, &lt;code&gt;constraint&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;For every &lt;code&gt;UncheckedAccount&lt;/code&gt;, ask: did I really mean to skip all of that, and did I validate it by hand?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's most of account security, and none of it is clever. It's a posture: assume nothing the caller hands you is what it claims, and make the program prove each piece before it acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.anchor-lang.com/docs/references/account-types" rel="noopener noreferrer"&gt;Anchor account types reference&lt;/a&gt; documents exactly what each type checks, and the &lt;a href="https://www.anchor-lang.com/docs/references/account-constraints" rel="noopener noreferrer"&gt;account constraints reference&lt;/a&gt; lists &lt;code&gt;has_one&lt;/code&gt;, &lt;code&gt;seeds&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;constraint&lt;/code&gt;, and the rest. For the attack patterns these defend against, &lt;a href="https://github.com/coral-xyz/sealevel-attacks" rel="noopener noreferrer"&gt;coral-xyz/sealevel-attacks&lt;/a&gt; pairs a deliberately vulnerable program with its Anchor fix for each common exploit class, owner checks, signer checks, account substitution, and more. It's the clearest way to see each mistake and its correction side by side.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc turns this posture into practice: you'll audit a program for these exact gaps, close them with constraints, and write tests that prove the locks hold. Reading this first means the two questions will already feel like second nature. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>learning</category>
    </item>
    <item>
      <title>Cross-Program Invocations: How One Solana Program Calls Another</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:00:33 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/cross-program-invocations-how-one-solana-program-calls-another-1o70</link>
      <guid>https://dev.to/100daysofsolana/cross-program-invocations-how-one-solana-program-calls-another-1o70</guid>
      <description>&lt;p&gt;Last time we ended on a promise. A Program Derived Address has no private key, so no human can sign for it, yet a program still needs to move tokens out of a vault it owns or release funds from an escrow. The mechanism that makes that work is the Cross-Program Invocation, and it's what this post is about.&lt;/p&gt;

&lt;p&gt;CPIs are also the feature that makes Solana composable. A program on its own is a closed box. A program that can call other programs can build on everything already deployed: the System Program to create accounts and move SOL, the Token Program to mint and transfer tokens, any other program someone has shipped. If you've ever wondered how a single Anchor instruction manages to transfer tokens when your program clearly doesn't contain transfer logic, the answer is a CPI into the Token Program.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: your program can't do everything itself
&lt;/h2&gt;

&lt;p&gt;Say your program runs a vault that holds SOL for a user, and later releases it under some condition. To move SOL between system-owned accounts, the transfer has to be performed by the System Program, since that's the program that owns those accounts and your program can only debit lamports from accounts it owns directly. So when the source is a system-owned account, your program needs a way to &lt;em&gt;call&lt;/em&gt; the System Program mid-instruction.&lt;/p&gt;

&lt;p&gt;That's one wall. There's a second one as soon as the account being moved from is a PDA your program controls: authorizing the action requires a signature for an address that has no private key. CPIs solve the first wall, letting your program invoke another program's instruction. PDA signing, layered on top, solves the second. The same pattern shows up constantly with tokens, where your program's PDA is the authority over a token account and has to authorize a transfer through the Token Program, which is the example we'll build below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: one instruction calls another program's instruction
&lt;/h2&gt;

&lt;p&gt;A Cross-Program Invocation is one program calling an instruction on another program during its own execution. The calling program (the caller) builds an instruction aimed at the callee program, hands it the accounts that instruction needs, and invokes it. The callee runs to completion, then control returns to the caller, which continues where it left off.&lt;/p&gt;

&lt;p&gt;The key thing to hold onto is that this is the same kind of instruction a normal client would send from outside. You are assembling a target program ID, an account list, and instruction data, exactly what a transaction contains, except your program is the one issuing it instead of a wallet. The runtime treats it as a nested call within the same transaction.&lt;/p&gt;

&lt;p&gt;There's a depth limit worth knowing: CPIs can only nest so far. Program A can call B, B can call C, and so on, but the runtime caps how deep the chain goes to keep execution bounded. The current limit is 5 levels (raised to 9 under a newer runtime change, SIMD-0268), and you'll rarely come close in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  invoke and invoke_signed: the two functions
&lt;/h2&gt;

&lt;p&gt;At the native level, Solana gives you two functions for making a CPI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;invoke&lt;/code&gt; is for calls where the signatures you need are already present on the transaction. The privileges of the accounts passed into your program extend to the program you call, so if a user signed the outer transaction, that signature carries through to the CPI. This is what you use to act on a normal user-owned account, like asking the System Program to transfer SOL the user already authorized.&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;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;invoke_signed&lt;/code&gt; is for when your program needs a PDA to sign. Since the PDA has no private key, you instead pass the seeds (plus the bump) used to derive it. The runtime re-derives the address from those seeds and your program's ID, and if it matches an account in the call, it treats that account as having signed.&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;invoke_signed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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="n"&gt;signers_seeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail that demystifies the whole thing: &lt;code&gt;invoke&lt;/code&gt; is just &lt;code&gt;invoke_signed&lt;/code&gt; with an empty seeds array. They run through the same path; the only difference is whether you supply signer seeds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;invoke_signed&lt;/span&gt;&lt;span class="p"&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;account_infos&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that matters most: PDA signing is not cryptographic
&lt;/h2&gt;

&lt;p&gt;This is the conceptual center of the post, so it's worth slowing down.&lt;/p&gt;

&lt;p&gt;When a normal account signs a transaction, a private key produces a cryptographic signature. A PDA has no private key, so nothing like that can happen. Instead, PDA signing is a runtime construct. You hand &lt;code&gt;invoke_signed&lt;/code&gt; the seeds, the runtime re-derives the PDA from those seeds and the calling program's ID, and if the derived address matches an account in the call, the runtime marks that account as signed for the duration of the CPI.&lt;/p&gt;

&lt;p&gt;No signature is ever computed. The "signature" is the runtime agreeing that because your program supplied the correct seeds, and those seeds plus your program ID derive to this exact address, your program is allowed to authorize actions on it. This is the cash-out of everything from the PDA post: the address is derived from your program, so only your program can present the seeds that unlock it. The absence of a private key isn't a gap that PDA signing patches over. It's the whole reason the scheme is secure, because no external party can ever produce these credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it looks in Anchor
&lt;/h2&gt;

&lt;p&gt;Anchor wraps the raw functions in a &lt;code&gt;CpiContext&lt;/code&gt;, which bundles the program you're calling with the accounts that instruction needs. You build the context, then call the typed helper for the instruction.&lt;/p&gt;

&lt;p&gt;A plain CPI, transferring SOL via the System Program using the user's own signature. Here &lt;code&gt;Transfer&lt;/code&gt; is the System Program's transfer, which takes just &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&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;use&lt;/span&gt; &lt;span class="nn"&gt;anchor_lang&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;transfer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.system_program&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Transfer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.sender&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="n"&gt;to&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.recipient&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="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A signed CPI, where a PDA is the authority. This one transfers SPL tokens through the Token Program, so the &lt;code&gt;Transfer&lt;/code&gt; here is the token program's version, which also takes an &lt;code&gt;authority&lt;/code&gt;, the account that must sign. That authority is our PDA, and &lt;code&gt;new_with_signer&lt;/code&gt; plus the seeds is how the PDA signs:&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;use&lt;/span&gt; &lt;span class="nn"&gt;anchor_spl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;token&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transfer&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;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.vault.bump&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"vault"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authority_key&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;]]];&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_context&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_with_signer&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.token_program&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="n"&gt;Transfer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.vault_token_account&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="n"&gt;to&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.recipient_token_account&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="n"&gt;authority&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.vault&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="n"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the difference in plain terms. &lt;code&gt;CpiContext::new&lt;/code&gt; says "make this call using the signatures already on the transaction." &lt;code&gt;CpiContext::new_with_signer&lt;/code&gt; says "make this call, and also present these seeds so the runtime will let my PDA sign." The seeds you pass are the same ones you'd use to derive the PDA, with the bump as the final element.&lt;/p&gt;

&lt;p&gt;If you're calling another Anchor program rather than a built-in one, you add it as a dependency with the &lt;code&gt;cpi&lt;/code&gt; feature, which generates typed instruction builders for it. That's a setup detail the lessons will walk through; the mental model is the same either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things that trip people up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting an account the callee needs.&lt;/strong&gt; A CPI passes accounts to the callee, and the callee validates them just like any instruction. If the inner instruction needs the System Program or a token program account, it has to be in your account list and in your &lt;code&gt;#[derive(Accounts)]&lt;/code&gt; struct. A missing account shows up as a failure inside the CPI, not in your own logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong seeds in &lt;code&gt;invoke_signed&lt;/code&gt;.&lt;/strong&gt; If the seeds you pass don't re-derive to the PDA the callee expects as a signer, the runtime won't mark it signed and the call fails. The seeds, their order, and the bump all have to match the derivation exactly, the same discipline as the PDA constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for &lt;code&gt;invoke_signed&lt;/code&gt; when &lt;code&gt;invoke&lt;/code&gt; would do.&lt;/strong&gt; If the authority is a normal user who already signed the transaction, you don't need signer seeds. Signing with a PDA is only for accounts your program controls that have no key of their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing "who signs" with "who owns."&lt;/strong&gt; A program can sign for a PDA derived from its own ID. It can't sign for arbitrary accounts just because they're passed in. The seeds prove the PDA belongs to the calling program, which is what authorizes the signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CPI is one program calling another program's instruction mid-execution, which is what makes Solana composable.&lt;/li&gt;
&lt;li&gt;You build a target instruction, accounts, and data, much like a client would, and your program issues it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoke&lt;/code&gt; uses signatures already on the transaction; &lt;code&gt;invoke_signed&lt;/code&gt; lets a PDA sign by supplying its seeds.&lt;/li&gt;
&lt;li&gt;PDA signing isn't cryptographic: the runtime re-derives the PDA from the seeds and your program ID and marks it signed if it matches.&lt;/li&gt;
&lt;li&gt;In Anchor, &lt;code&gt;CpiContext::new&lt;/code&gt; is a plain CPI and &lt;code&gt;CpiContext::new_with_signer&lt;/code&gt; adds the PDA seeds.&lt;/li&gt;
&lt;li&gt;CPIs nest only so deep: the stack limit is 5 levels (9 under SIMD-0268).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/core/cpi" rel="noopener noreferrer"&gt;Solana CPI documentation&lt;/a&gt; covers both functions, privilege extension, and the execution flow in detail, and the &lt;a href="https://solana.com/docs/intro/quick-start/cross-program-invocation" rel="noopener noreferrer"&gt;quick-start CPI tutorial&lt;/a&gt; builds a vault that signs with a PDA end to end. For the native signature, the &lt;a href="https://docs.rs/solana-cpi/latest/solana_cpi/fn.invoke_signed.html" rel="noopener noreferrer"&gt;&lt;code&gt;invoke_signed&lt;/code&gt; reference on docs.rs&lt;/a&gt; spells out exactly how the runtime treats PDA signing.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc puts this to work, and since you already understand PDAs, the signing half should click rather than mystify. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Epoch 2 content creator challenge</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Fri, 26 Jun 2026 11:22:47 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/epoch-2-content-creator-challenge-5e88</link>
      <guid>https://dev.to/100daysofsolana/epoch-2-content-creator-challenge-5e88</guid>
      <description>&lt;p&gt;In Epoch 2 of &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt;, we moved from getting to grips with the fundamentals to creating our own on-chain assets.&lt;/p&gt;

&lt;p&gt;That gave us a lot to work with: tokens, mints, metadata, authorities, extensions, and the choices developers make when they create an asset on Solana.&lt;/p&gt;

&lt;p&gt;And this challenge is all about seeing those ideas explained clearly for developers coming from Web2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prize categories
&lt;/h2&gt;

&lt;p&gt;For the Epoch 2 Content Challenge, we’re awarding prizes in two categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best DEV post&lt;/li&gt;
&lt;li&gt;Best YouTube video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To enter, you’ll need to have completed at least five Epoch 2 challenges. Missed a few? No problem. You can catch up now and still take part.&lt;/p&gt;

&lt;p&gt;Your post or video should use something from Epoch 2 to help Web2 developers understand Solana. That might mean explaining tokens through a Web2 comparison, showing how NFT metadata works, walking through a Token-2022 extension, or using something you built during the challenges to make an unfamiliar idea easier to grasp.&lt;/p&gt;

&lt;p&gt;This shouldn’t just be a diary of what you completed. The goal is to create something useful for developers who are Solana-curious but mostly coming from a Web2 background.&lt;/p&gt;

&lt;p&gt;There are a couple of other rules, too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your entry will also need to reach at least 50 views.&lt;/li&gt;
&lt;li&gt;Entries must be submitted by the end of Sunday, July 5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your post or video is live, submit it &lt;a href="https://forms.gle/TtHBb5KhrcUeQKc3A" rel="noopener noreferrer"&gt;using this form&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll announce the winners on DEV the week after the challenge closes!&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>writing</category>
    </item>
    <item>
      <title>Arc 9 Catch-Up: Writing Your First Solana Program</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:49:27 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/-arc-9-catch-up-writing-your-first-solana-program-12nl</link>
      <guid>https://dev.to/100daysofsolana/-arc-9-catch-up-writing-your-first-solana-program-12nl</guid>
      <description>&lt;p&gt;With Arc 9 of &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt;, we enter the third big phase — or Epoch, as we call them — of the learning program.&lt;/p&gt;

&lt;p&gt;And we make a pretty big shift.&lt;/p&gt;

&lt;p&gt;Up until now, we have been getting hands-on with Solana mostly through JavaScript. We have created wallets, sent transactions, minted tokens, configured Token-2022 extensions, and inspected on-chain state from the client side.&lt;/p&gt;

&lt;p&gt;Arc 9 changes the angle.&lt;/p&gt;

&lt;p&gt;Rust is the native language of Solana programs, and this arc is our first step into writing Solana logic ourselves.&lt;/p&gt;

&lt;p&gt;The program we built was deliberately simple: a counter.&lt;/p&gt;

&lt;p&gt;Initialize it. Store who owns it. Increment it. Reject anyone else who tries.&lt;/p&gt;

&lt;p&gt;This was a great way to learn a little about the shape of Solana program dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instructions as program entry points&lt;/li&gt;
&lt;li&gt;accounts as state&lt;/li&gt;
&lt;li&gt;account constraints as guardrails&lt;/li&gt;
&lt;li&gt;tests that prove both success and failure paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, let's get into the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor gives us a framework for Solana programs
&lt;/h2&gt;

&lt;p&gt;In Web2, we use frameworks like Next.js, Ruby on Rails, Django, and .NET.&lt;/p&gt;

&lt;p&gt;The principal Solana equivalent is &lt;a href="https://www.anchor-lang.com/docs" rel="noopener noreferrer"&gt;Anchor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Like its Web2 equivalents, Anchor gives us a project structure, a way to define instructions, a way to describe the accounts each instruction needs, and a testing setup that makes local development much more approachable.&lt;/p&gt;

&lt;p&gt;And that really helps when we're taking our first steps with Rust development for Solana. We don't need to be learning low-level details at this stage. Instead, we are trying to get a working program in place, understand its shape, and build enough confidence to change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A counter is enough to learn the shape
&lt;/h2&gt;

&lt;p&gt;The first program we built was a counter because it gives us just enough state and behavior to make the Solana program model visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create an account&lt;/li&gt;
&lt;li&gt;store some data in it&lt;/li&gt;
&lt;li&gt;update that data later&lt;/li&gt;
&lt;li&gt;restrict who is allowed to update it&lt;/li&gt;
&lt;li&gt;write tests that prove those rules work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The program starts with an &lt;code&gt;initialize&lt;/code&gt; instruction.&lt;/p&gt;

&lt;p&gt;That instruction creates a new counter account, stores the wallet that created it as the authority, and sets the count to zero.&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;Even if you're brand new to Rust, you can probably get the idea of what's happening here.&lt;/p&gt;

&lt;p&gt;We get the counter account from &lt;code&gt;ctx.accounts.counter&lt;/code&gt;, store the authority's public key, set the count to &lt;code&gt;0&lt;/code&gt;, and return &lt;code&gt;Ok(())&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But there is a lot happening around that small handler.&lt;/p&gt;

&lt;p&gt;That setup lives in the accounts struct.&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the first big Anchor lessons.&lt;/p&gt;

&lt;p&gt;The instruction handler tells us what the program does.&lt;/p&gt;

&lt;p&gt;The accounts struct tells us what accounts the instruction needs, what permissions they need, and what checks Anchor should run before the handler executes.&lt;/p&gt;

&lt;p&gt;In this case, it says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a new &lt;code&gt;counter&lt;/code&gt; account&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;authority&lt;/code&gt; to pay for it&lt;/li&gt;
&lt;li&gt;make sure &lt;code&gt;authority&lt;/code&gt; signed the transaction&lt;/li&gt;
&lt;li&gt;allocate enough space for the account data&lt;/li&gt;
&lt;li&gt;use the System Program to create the account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handler is short because the accounts struct is doing a lot of the setup work.&lt;/p&gt;

&lt;p&gt;That is the shape we keep coming back to in Arc 9: instruction logic in one place, account requirements in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The account is the program's state
&lt;/h2&gt;

&lt;p&gt;The counter data itself lives in a custom 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;#[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;This is the first bit that feels different if you are coming from normal web development.&lt;/p&gt;

&lt;p&gt;In a Web2 app, you might expect this state to live in a database row. You might have a &lt;code&gt;counters&lt;/code&gt; table with an &lt;code&gt;owner_id&lt;/code&gt; column and a &lt;code&gt;count&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;In this program, the state lives in a Solana account.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;count&lt;/code&gt; field stores the number.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;authority&lt;/code&gt; field stores the wallet that is allowed to update it.&lt;/p&gt;

&lt;p&gt;That second field is what makes the example useful. Without it, anyone could increment the counter. With it, the program has a rule it can check later.&lt;/p&gt;

&lt;p&gt;So even though the counter is simple, it already has the ingredients of a real program:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stored state&lt;/li&gt;
&lt;li&gt;ownership&lt;/li&gt;
&lt;li&gt;an update path&lt;/li&gt;
&lt;li&gt;a rule about who is allowed to use that path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Incrementing the counter introduces authorization
&lt;/h2&gt;

&lt;p&gt;Once the counter exists, the next step is to update it.&lt;/p&gt;

&lt;p&gt;That happens through an &lt;code&gt;increment&lt;/code&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;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;Again, the handler itself is easy enough to follow.&lt;/p&gt;

&lt;p&gt;It gets the counter account, adds one to the count, and returns &lt;code&gt;Ok(())&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;checked_add(1)&lt;/code&gt; part is worth noticing. Rather than adding blindly, it checks for overflow and returns an error if the number cannot safely be increased.&lt;/p&gt;

&lt;p&gt;But the more important part of this instruction is not the arithmetic.&lt;/p&gt;

&lt;p&gt;It is the account constraint.&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 key line is 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(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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mut&lt;/code&gt; says the counter account can be changed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_one = authority&lt;/code&gt; says the &lt;code&gt;authority&lt;/code&gt; field stored in the counter account must match the wallet signing this instruction.&lt;/p&gt;

&lt;p&gt;That is the rule that stops one wallet from incrementing someone else's counter.&lt;/p&gt;

&lt;p&gt;In a web app, we might write that as an authorization check inside a controller or route handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if counter.owner_id != current_user.id:
    reject the request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Anchor, that rule is declared on the account instead.&lt;/p&gt;

&lt;p&gt;So the instruction has two parts working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the handler says what happens when the instruction is allowed to run&lt;/li&gt;
&lt;li&gt;the accounts struct says what must be true before it runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this tiny counter example is useful. It is not just changing a number. It is changing a number only when the right signer is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  LiteSVM makes the tests feel like normal development
&lt;/h2&gt;

&lt;p&gt;Once we had &lt;code&gt;initialize&lt;/code&gt; and &lt;code&gt;increment&lt;/code&gt;, we needed to prove they worked together.&lt;/p&gt;

&lt;p&gt;That is where LiteSVM came in.&lt;/p&gt;

&lt;p&gt;LiteSVM lets us run the program locally in an in-process Solana environment. We do not need to deploy to devnet, request SOL, wait for confirmations, or debug against a remote cluster.&lt;/p&gt;

&lt;p&gt;For this arc, that was exactly what we needed.&lt;/p&gt;

&lt;p&gt;We could build the program, run the tests, change the code, and run the tests again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;anchor build
cargo &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; counter &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--nocapture&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first useful test followed the happy path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a local Solana test environment&lt;/li&gt;
&lt;li&gt;load the compiled counter program&lt;/li&gt;
&lt;li&gt;create a counter account&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;initialize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;increment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;read the counter account back&lt;/li&gt;
&lt;li&gt;check that the count is now &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That test matters because it is not just calling a Rust function directly.&lt;/p&gt;

&lt;p&gt;It sends transactions to the program.&lt;/p&gt;

&lt;p&gt;The program receives accounts.&lt;/p&gt;

&lt;p&gt;The account data changes.&lt;/p&gt;

&lt;p&gt;Then the test reads the account back and checks what actually happened.&lt;/p&gt;

&lt;p&gt;So even though the test runs locally, it still teaches the right execution model.&lt;/p&gt;

&lt;p&gt;That is the main value of LiteSVM here: it gives us a fast feedback loop without pretending that Solana programs work like ordinary local functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The happy path is not enough
&lt;/h2&gt;

&lt;p&gt;At this point, the program worked.&lt;/p&gt;

&lt;p&gt;We could initialize a counter. We could increment it. We could read the account back and check that the count was &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a good start, but it only proves the program works when everything is used correctly.&lt;/p&gt;

&lt;p&gt;It does not prove the program refuses to do the wrong thing.&lt;/p&gt;

&lt;p&gt;That matters because Solana programs are public. Anyone can call them. Anyone can try different accounts, different signers, and different transaction shapes.&lt;/p&gt;

&lt;p&gt;So the next test was more interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wallet A creates the counter&lt;/li&gt;
&lt;li&gt;wallet B tries to increment it&lt;/li&gt;
&lt;li&gt;the program rejects the transaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That test proves the authority rule is actually doing something.&lt;/p&gt;

&lt;p&gt;The important line was still 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(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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The happy-path test proves the right wallet can increment the counter.&lt;/p&gt;

&lt;p&gt;The failure test proves the wrong wallet cannot.&lt;/p&gt;

&lt;p&gt;Without that second test, we could remove &lt;code&gt;has_one = authority&lt;/code&gt; and the happy-path test would still pass.&lt;/p&gt;

&lt;p&gt;That is the trap Arc 9 was trying to show us.&lt;/p&gt;

&lt;p&gt;A green test suite is not enough if it only checks the path where everything goes right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking the program proved the tests mattered
&lt;/h2&gt;

&lt;p&gt;The best part of the arc was breaking the program on purpose.&lt;/p&gt;

&lt;p&gt;That might sound strange, but it is a useful habit.&lt;/p&gt;

&lt;p&gt;Once the tests were passing, we made small changes that should have broken the program, then checked whether the tests caught them.&lt;/p&gt;

&lt;p&gt;First, we removed the authorization rule.&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(mut)]&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;Now the counter was still mutable, but Anchor was no longer checking that the signer matched the stored authority.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of bug the wrong-wallet test should catch.&lt;/p&gt;

&lt;p&gt;And it did.&lt;/p&gt;

&lt;p&gt;Then we changed the arithmetic so the counter added &lt;code&gt;2&lt;/code&gt; instead of &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The transaction still succeeded. Nothing crashed. But the stored value was wrong, so the happy-path assertion caught it.&lt;/p&gt;

&lt;p&gt;That is a different kind of bug.&lt;/p&gt;

&lt;p&gt;The program runs, but writes the wrong state.&lt;/p&gt;

&lt;p&gt;Finally, we broke initialization by not storing the authority.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; instruction still looked like it worked. The account was created. The count was set to zero.&lt;/p&gt;

&lt;p&gt;But the next instruction failed, because the program later tried to check an authority value that had never been stored correctly.&lt;/p&gt;

&lt;p&gt;That was probably the most useful debugging lesson in the arc.&lt;/p&gt;

&lt;p&gt;In Solana programs, one instruction might write state and another instruction might validate that state later.&lt;/p&gt;

&lt;p&gt;So the error does not always appear where the bug was introduced.&lt;/p&gt;

&lt;p&gt;Sometimes you have to work backwards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which account failed validation?&lt;/li&gt;
&lt;li&gt;which field was wrong?&lt;/li&gt;
&lt;li&gt;which instruction wrote that field?&lt;/li&gt;
&lt;li&gt;what did the account actually store?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The counter example was small, but the lesson scales.&lt;/p&gt;

&lt;p&gt;If you can understand that pattern here, you are better prepared for real programs later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 9 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 9 was not really about building a counter.&lt;/p&gt;

&lt;p&gt;The counter was just the smallest useful program for learning the shape of Anchor development.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had seen how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scaffold a Solana program with Anchor&lt;/li&gt;
&lt;li&gt;write instructions in Rust&lt;/li&gt;
&lt;li&gt;create and store data in an account&lt;/li&gt;
&lt;li&gt;use an accounts struct to describe what an instruction needs&lt;/li&gt;
&lt;li&gt;protect state with an authority rule&lt;/li&gt;
&lt;li&gt;test real transactions locally with LiteSVM&lt;/li&gt;
&lt;li&gt;write both happy-path and failure-path tests&lt;/li&gt;
&lt;li&gt;break the program on purpose to prove the tests were meaningful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a meaningful shift from the earlier arcs.&lt;/p&gt;

&lt;p&gt;Before this, we were mostly using Solana from the outside. We were creating wallets, sending transactions, minting tokens, configuring extensions, and inspecting what existing programs did.&lt;/p&gt;

&lt;p&gt;In Arc 9, we started writing the program logic ourselves.&lt;/p&gt;

&lt;p&gt;The next limitation is addressability.&lt;/p&gt;

&lt;p&gt;In this arc, the counter account used a fresh keypair address. That works for a first program, but it means the client has to remember where each user's counter lives.&lt;/p&gt;

&lt;p&gt;Most real Solana programs need a more predictable way to find program-owned state.&lt;/p&gt;

&lt;p&gt;That is where Program Derived Addresses come in.&lt;/p&gt;

&lt;p&gt;So Arc 9 gives us the foundation: write a program, store state, enforce a rule, and test it properly.&lt;/p&gt;

&lt;p&gt;The next arc builds on that by making the account address itself part of the program design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 9 challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ecbff-efdb-8cee-beac-ee5b63da7d40" rel="noopener noreferrer"&gt;Day 57: Install Anchor and scaffold your first program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ecfb9-01f7-d84a-d1cf-371f82e38bff" rel="noopener noreferrer"&gt;Day 58: Add state and write your first LiteSVM test&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed017-1cee-5914-4ebb-6b11bee30f51" rel="noopener noreferrer"&gt;Day 59: Add an increment instruction and test both calls end to end&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed035-9f27-0707-4e7a-f9b7b0fde822" rel="noopener noreferrer"&gt;Day 60: Add failure tests so green checks actually mean something&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed03c-f8bd-8c41-6a87-4cccbfe5312b" rel="noopener noreferrer"&gt;Day 61: Break your program on purpose and watch the tests catch it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed046-5f60-0285-0ccc-f4417061a2a3" rel="noopener noreferrer"&gt;Day 62: Write the post that proves you understand your first Anchor program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed04c-b375-c55d-55cd-ae40182ad292" rel="noopener noreferrer"&gt;Day 63: Turn your counter program into a post that lands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>learning</category>
    </item>
    <item>
      <title>Learn Solana the Fast Way</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:32:02 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/learn-solana-the-fast-way-4kg4</link>
      <guid>https://dev.to/100daysofsolana/learn-solana-the-fast-way-4kg4</guid>
      <description>&lt;p&gt;As a web or mobile developer, Web3 can seem harder to get to grips with than it really is.&lt;/p&gt;

&lt;p&gt;Part of the problem is the vocabulary. Wallets, accounts, mints, programs, signers, authorities, transactions. It can sound as though everything you already know has been replaced by a completely different model.&lt;/p&gt;

&lt;p&gt;But many Solana concepts have useful parallels with traditional development.&lt;/p&gt;

&lt;p&gt;A transaction is not exactly an API request. A program is not exactly a backend service. A token mint is not exactly a database table. But those comparisons are close enough to give you a way in.&lt;/p&gt;

&lt;p&gt;Once you have those bridges, Solana starts to feel less like a wall of jargon and more like another development environment with its own rules, constraints, and patterns.&lt;/p&gt;

&lt;p&gt;That is the fast way to learn it: build small things, connect each new idea to something familiar, and let the mental model grow from there.&lt;/p&gt;

&lt;p&gt;In this post, we’ll use familiar Web2 ideas as bridges into the Solana model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity and authentication&lt;/strong&gt; → wallets, keypairs, and signatures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API calls and commands&lt;/strong&gt; → transactions and instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database records and stored state&lt;/strong&gt; → accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset definitions and balances&lt;/strong&gt; → tokens, mints, and token accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media files and metadata records&lt;/strong&gt; → NFT metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware and business rules&lt;/strong&gt; → Token-2022 extensions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend logic and tests&lt;/strong&gt; → programs and Anchor&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiemc217zbl5qucaudnow.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiemc217zbl5qucaudnow.jpg" alt="Matching Web2 concepts to Web3" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each one, we’ll explain the Web2 idea first, map it to the Solana concept, then point you to a small challenge where you can try it yourself.&lt;/p&gt;

&lt;p&gt;You can read this as a quick map of Solana, but it works better if you pick one section and do the linked challenge.&lt;/p&gt;

&lt;p&gt;Each challenge is part of 100 Days of Solana. Sign up, complete the task, and submit your work so your progress counts.&lt;/p&gt;

&lt;p&gt;The goal is not to recreate the whole curriculum in one post.&lt;/p&gt;

&lt;p&gt;The goal is to give you enough practical understanding that Solana starts to make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Identity and authentication: wallets, keypairs, and signatures
&lt;/h2&gt;

&lt;p&gt;In a traditional web or mobile app, identity usually starts with a user account.&lt;/p&gt;

&lt;p&gt;Someone signs up with an email address, a password, a social login, or a passkey. Your application stores a user record, manages sessions, and decides what that user is allowed to do.&lt;/p&gt;

&lt;p&gt;Solana starts from a different place.&lt;/p&gt;

&lt;p&gt;Your wallet is not a user account in the usual Web2 sense. It is a cryptographic identity. More specifically, it is controlled by a keypair: a public key that acts like an address, and a private key that proves you are allowed to sign actions for that address.&lt;/p&gt;

&lt;p&gt;The closest Web2 parallel is probably SSH keys or API keys.&lt;/p&gt;

&lt;p&gt;If you have used SSH keys with GitHub, the idea is familiar: your public key identifies you, and your private key lets you prove that you are really the person allowed to act as that identity.&lt;/p&gt;

&lt;p&gt;On Solana, that proof happens through signatures.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu39jlhui4kbb49obbm2c.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu39jlhui4kbb49obbm2c.jpg" alt="Illustration representing the idea of a transaction authorized by a key" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you send a transaction, your wallet signs it. That signature tells the network: “The holder of this private key authorized this action.”&lt;/p&gt;

&lt;p&gt;That is why wallets matter so much. They are not just where tokens live. They are how users approve actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019daa0b-8aaa-b52d-6765-3cb47e97e0ba" rel="noopener noreferrer"&gt;Create your devnet wallet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll create a wallet, request devnet SOL, and check your balance.&lt;/p&gt;

&lt;p&gt;The point is not to understand every detail of wallet security yet. The point is to make the first idea concrete:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Solana wallet is an identity that can sign actions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you’re done, submit your result through 100 Days of Solana.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. API calls and commands: transactions and instructions
&lt;/h2&gt;

&lt;p&gt;In Web2, when your frontend wants something to happen, it usually sends an API request.&lt;/p&gt;

&lt;p&gt;That request might create a user, submit a payment, update a profile, or trigger some backend logic.&lt;/p&gt;

&lt;p&gt;Solana transactions are not the same as API requests, but the comparison is useful.&lt;/p&gt;

&lt;p&gt;A transaction is a signed message sent to the network. Inside that transaction are one or more instructions. Each instruction tells a Solana program what work to perform.&lt;/p&gt;

&lt;p&gt;So the rough mapping is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the transaction is the package being submitted&lt;/li&gt;
&lt;li&gt;the instruction is the command inside it&lt;/li&gt;
&lt;li&gt;the program is the code that runs&lt;/li&gt;
&lt;li&gt;the accounts are the state the program reads or writes&lt;/li&gt;
&lt;li&gt;the signer is the identity authorizing the action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why Solana transactions can feel strange at first. They are not just “send money from A to B”. They can contain several instructions that run together.&lt;/p&gt;

&lt;p&gt;For example, a transaction might create an account and then initialize it. Or it might approve an action and then perform a transfer. If one required part fails, the whole transaction fails.&lt;/p&gt;

&lt;p&gt;That gives Solana transactions an important property: they can represent a complete unit of work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019df771-2eb8-957d-a0a3-23de7f655409" rel="noopener noreferrer"&gt;Send and inspect a devnet transaction&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll send a real transaction on devnet, inspect what happened, and connect the abstract idea of “instructions” to something you can actually see.&lt;/p&gt;

&lt;p&gt;The key idea is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A transaction is not just movement of data. It is a signed bundle of instructions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Submit your completed challenge so your progress is visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Database records and stored state: accounts
&lt;/h2&gt;

&lt;p&gt;In Web2, you are used to applications storing state in databases.&lt;/p&gt;

&lt;p&gt;A user has a row. An order has a row. A payment has a row. Your backend reads and writes that data when something happens.&lt;/p&gt;

&lt;p&gt;Solana also stores state, but the model is different.&lt;/p&gt;

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

&lt;p&gt;That word can be confusing because “account” sounds like “user account”. On Solana, an account is more general than that. It is a place where data can live.&lt;/p&gt;

&lt;p&gt;An account might hold SOL. It might hold token data. It might store configuration for a program. It might represent part of your application’s state.&lt;/p&gt;

&lt;p&gt;A useful Web2 comparison is a database record, but with an important difference: Solana programs do not secretly reach into their own private database. The accounts a program needs are passed into the transaction.&lt;/p&gt;

&lt;p&gt;That means the transaction says not only what you want to do, but also which pieces of state the program is allowed to read or write.&lt;/p&gt;

&lt;p&gt;This is one of the biggest shifts in the Solana mental model.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr924mlojghwqy5pzyd3v.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr924mlojghwqy5pzyd3v.jpg" alt="Solana accounts shown as state containers passed into an on-chain program." width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You are not calling a backend endpoint that then queries whatever it wants from a database. You are sending an instruction to a program and explicitly providing the accounts involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019e166e-5a4c-44fc-5568-aa0bf3f491f7" rel="noopener noreferrer"&gt;Inspect a Solana account&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll look at a real account and identify its address, owner, and data.&lt;/p&gt;

&lt;p&gt;The point is to stop thinking of “account” as only meaning “user account” and start thinking of it as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A place where on-chain state lives.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Asset definitions and balances: tokens, mints, and token accounts
&lt;/h2&gt;

&lt;p&gt;In Web2, if you were building a system with credits, points, tickets, or balances, you might start with a few database tables.&lt;/p&gt;

&lt;p&gt;One table might define the asset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name&lt;/li&gt;
&lt;li&gt;symbol&lt;/li&gt;
&lt;li&gt;decimal precision&lt;/li&gt;
&lt;li&gt;total supply rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another table might track balances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user ID&lt;/li&gt;
&lt;li&gt;asset ID&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solana tokens use a similar separation, but the terms are different.&lt;/p&gt;

&lt;p&gt;A mint defines the token.&lt;/p&gt;

&lt;p&gt;A token account holds a balance of that token for a particular owner.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;The mint is not your personal balance. It is the source of truth for the token itself: its identity, decimals, supply, and authorities.&lt;/p&gt;

&lt;p&gt;A token account is where an owner’s balance of that specific token lives.&lt;/p&gt;

&lt;p&gt;So if Alice and Bob both hold the same token, they do not share one balance field on the mint. They each have token accounts connected to that mint.&lt;/p&gt;

&lt;p&gt;This is one of the fastest ways to make Solana feel less abstract. Once you create a mint, mint supply, create token accounts, and transfer tokens, you have used several of Solana’s core ideas together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;This idea takes two short challenges to see end to end.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019e3b40-0aaf-110f-9cbd-29dc2abee2dd" rel="noopener noreferrer"&gt;create your first token&lt;/a&gt;. You’ll create a mint, create a token account, and mint supply into it.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019e4a57-072c-e8e7-14bd-1a3500378031" rel="noopener noreferrer"&gt;put it to work&lt;/a&gt;. You’ll create a token with built-in transfer rules, send it between wallets, and watch the mint and token accounts update.&lt;/p&gt;

&lt;p&gt;As you do it, keep this model in mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mint defines the asset. Token accounts hold balances.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you submit this one, you have more than a screenshot. You have proof that you understand one of Solana’s core building blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Media files and metadata records: NFT metadata
&lt;/h2&gt;

&lt;p&gt;In Web2, you rarely store everything directly in one database field.&lt;/p&gt;

&lt;p&gt;If you are building a product catalogue, a media app, or a user profile system, you often split things up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the database stores IDs, names, references, and structured fields&lt;/li&gt;
&lt;li&gt;media files live in object storage or a CDN&lt;/li&gt;
&lt;li&gt;the frontend combines those pieces into something users can see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NFTs use a similar split.&lt;/p&gt;

&lt;p&gt;The token itself gives you the scarce asset: usually a mint with supply of one and zero decimals. But that alone does not tell a wallet what to display.&lt;/p&gt;

&lt;p&gt;For that, you need metadata.&lt;/p&gt;

&lt;p&gt;Metadata gives clients human-readable information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name&lt;/li&gt;
&lt;li&gt;symbol&lt;/li&gt;
&lt;li&gt;description&lt;/li&gt;
&lt;li&gt;image&lt;/li&gt;
&lt;li&gt;attributes&lt;/li&gt;
&lt;li&gt;collection information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of that information may live on-chain. Some may point to off-chain JSON or media files. The exact mechanics vary depending on the token standard and tooling, but the broad idea is familiar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The on-chain asset provides ownership and identity. Metadata helps apps understand what the asset represents.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why NFTs are worth learning even if you are not especially interested in NFT markets. They teach you how Solana combines ownership, metadata, clients, and off-chain content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019e87d6-0876-e1d2-51ad-f2bb9c8f9f03" rel="noopener noreferrer"&gt;Add metadata to an asset&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll connect an on-chain asset to human-readable metadata and see how wallets know what to display.&lt;/p&gt;

&lt;p&gt;Look for the split between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what lives on-chain&lt;/li&gt;
&lt;li&gt;what points off-chain&lt;/li&gt;
&lt;li&gt;what the wallet or client chooses to show&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to understand this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token becomes meaningful to users when apps can connect it to metadata.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Middleware and business rules: Token-2022 extensions
&lt;/h2&gt;

&lt;p&gt;In Web2, business rules usually live in your application.&lt;/p&gt;

&lt;p&gt;If you want to charge a fee on every payment, you write that logic in your backend. If you want to prevent a user from transferring something, you add a rule to your application. If you want a balance to grow over time, you might run a scheduled job or calculate the value when the user views it.&lt;/p&gt;

&lt;p&gt;That model works, but it depends on your application being the place where the rule is enforced.&lt;/p&gt;

&lt;p&gt;Token-2022 changes that.&lt;/p&gt;

&lt;p&gt;With Token-2022, some behaviors can be configured directly on the token mint itself. The rule is not just wrapped around the asset by your app. It becomes part of how the asset works.&lt;/p&gt;

&lt;p&gt;For example, a token can be configured to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;charge a fee when it transfers&lt;/li&gt;
&lt;li&gt;accrue interest in the displayed UI amount&lt;/li&gt;
&lt;li&gt;refuse to transfer at all&lt;/li&gt;
&lt;li&gt;require additional checks before movement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Web2 bridge is middleware, but with an important twist.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvwhpu3r43fbzdloc7pj.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvwhpu3r43fbzdloc7pj.jpg" alt="A token shown with built-in rule modules representing Token-2022 extensions." width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a web app, middleware sits in the request path. It runs because traffic passes through your server.&lt;/p&gt;

&lt;p&gt;With Token-2022, the middleware-like behavior is inside the asset. The token program enforces it whenever the token is used.&lt;/p&gt;

&lt;p&gt;A wallet cannot simply forget to apply the fee. A marketplace cannot route around the rule by calling a different endpoint. The behavior is configured on-chain and enforced by the token program.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ea7f3-231d-10ff-6d6d-664fe7ee2cce" rel="noopener noreferrer"&gt;Create a fee-bearing token&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll configure a Token-2022 mint with a transfer fee, send the token, and see that the rule is enforced by the token program rather than by application code.&lt;/p&gt;

&lt;p&gt;As you work through it, answer three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where is the fee configured?&lt;/li&gt;
&lt;li&gt;what happens when the token is transferred?&lt;/li&gt;
&lt;li&gt;who enforces the rule?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token-2022 lets you put certain business rules into the asset itself, not just into the app around it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Submit this one if you want a good “aha” moment from the challenge. It is one of the clearest examples of Solana doing something differently from a traditional app stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Backend logic and tests: programs and Anchor
&lt;/h2&gt;

&lt;p&gt;In Web2, your backend is where application logic usually lives.&lt;/p&gt;

&lt;p&gt;It validates input, checks permissions, updates state, triggers side effects, and returns results. You might write tests to prove that the logic works and that failure cases are handled correctly.&lt;/p&gt;

&lt;p&gt;On Solana, that logic lives in programs.&lt;/p&gt;

&lt;p&gt;A program is deployed code that runs on-chain. Users and clients interact with it by sending transactions containing instructions.&lt;/p&gt;

&lt;p&gt;Anchor is a framework that makes it easier to write Solana programs in Rust.&lt;/p&gt;

&lt;p&gt;The rough Web2 bridge is backend logic deployed to a shared runtime. That comparison is not perfect, because Solana programs work with explicitly provided accounts rather than a private database connection. But it is close enough to help you begin.&lt;/p&gt;

&lt;p&gt;With Anchor, you define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the instructions your program supports&lt;/li&gt;
&lt;li&gt;the accounts each instruction expects&lt;/li&gt;
&lt;li&gt;the rules those accounts must satisfy&lt;/li&gt;
&lt;li&gt;the handler logic that runs&lt;/li&gt;
&lt;li&gt;tests that prove the behavior works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple counter program is a good starting point. It teaches the basic flow without too much business logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initialize a counter account&lt;/li&gt;
&lt;li&gt;increment the counter&lt;/li&gt;
&lt;li&gt;test that the value changed&lt;/li&gt;
&lt;li&gt;test that invalid actions fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the Solana model starts to come together. Wallets sign transactions. Transactions contain instructions. Instructions call programs. Programs read and write accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;This one builds up over two short challenges.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ecbff-efdb-8cee-beac-ee5b63da7d40" rel="noopener noreferrer"&gt;build and test an Anchor counter program&lt;/a&gt;. You’ll scaffold the program, initialize a counter, increment it, and write tests that prove your on-chain logic behaves the way you expect.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ed035-9f27-0707-4e7a-f9b7b0fde822" rel="noopener noreferrer"&gt;make the tests earn their keep&lt;/a&gt;. You’ll add failure tests, then deliberately break the program and confirm the test suite catches it.&lt;/p&gt;

&lt;p&gt;The goal is to understand this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Solana program is where your application logic runs, and Anchor gives you a practical structure for writing and testing it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a strong challenge to submit because it shows real progress: you are no longer only using Solana from the outside. You are starting to write logic that runs on-chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep going with 100 Days of Solana
&lt;/h2&gt;

&lt;p&gt;You do not need to understand all of Solana before you start building.&lt;/p&gt;

&lt;p&gt;In fact, trying to learn everything upfront is usually the slowest path.&lt;/p&gt;

&lt;p&gt;The faster route is to build small things that make each concept real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a wallet&lt;/li&gt;
&lt;li&gt;send a transaction&lt;/li&gt;
&lt;li&gt;inspect an account&lt;/li&gt;
&lt;li&gt;mint a token&lt;/li&gt;
&lt;li&gt;add metadata&lt;/li&gt;
&lt;li&gt;try a Token-2022 extension&lt;/li&gt;
&lt;li&gt;run an Anchor program&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one gives you a piece of the model.&lt;/p&gt;

&lt;p&gt;The challenges above are taken from &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt;, a practical challenge series for developers who want to learn Solana by building.&lt;/p&gt;

&lt;p&gt;Sounds good? Great, &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;get started and learn Solana today &amp;gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>learning</category>
    </item>
    <item>
      <title>Understanding Program Derived Addresses: The Solana Address That Has No Private Key</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:23:35 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/understanding-program-derived-addresses-the-solana-address-that-has-no-private-key-1mcg</link>
      <guid>https://dev.to/100daysofsolana/understanding-program-derived-addresses-the-solana-address-that-has-no-private-key-1mcg</guid>
      <description>&lt;p&gt;Every Solana program eventually hits the same question: where do I put my data, and how do I find it again later?&lt;/p&gt;

&lt;p&gt;Programs are stateless, so a program's data lives in separate accounts, each at an address. The moment you store something, you owe an answer to a problem databases tend to hide from you: what address does this live at, and how does the program find it again tomorrow? Program Derived Addresses are Solana's answer. The name scares people off, but the idea is mostly "an address you compute instead of remember, that only your program can control."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, in code
&lt;/h2&gt;

&lt;p&gt;Say each user gets a counter account. The normal way to make an account is to generate a fresh keypair and store data at its public key:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Keypair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@solana/web3.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&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;Keypair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// counter.publicKey is something random, e.g. 7Hx4...9fT&lt;/span&gt;
&lt;span class="c1"&gt;// create the account at that address, write count = 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. But the address is random, so nothing connects &lt;em&gt;this user&lt;/em&gt; to &lt;em&gt;that address&lt;/em&gt;. Tomorrow, when the user comes back to increment, how does your program find their counter? You're forced to keep a lookup table somewhere:&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="c1"&gt;// the mapping you now have to store and never lose&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9fYL...user1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7Hx4...9fT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B2k9...user2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Qz1p...4dR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...times ten thousand users&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lose that table, lose the data, even though the accounts are right there on chain. You're storing files in a warehouse and writing the shelf number on a sticky note.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: compute the address from what you already know
&lt;/h2&gt;

&lt;p&gt;What if the address were a &lt;em&gt;function&lt;/em&gt; of the user instead of random? Give a function the word &lt;code&gt;"counter"&lt;/code&gt; and the user's public key, and it hands back a fixed address. Same inputs, same address, every time. No table.&lt;/p&gt;

&lt;p&gt;That's a PDA. &lt;strong&gt;PDAs are 32-byte addresses derived deterministically from a program ID and a set of seeds.&lt;/strong&gt; The seeds are the meaningful inputs you pick (here, &lt;code&gt;"counter"&lt;/code&gt; + the user's key). With &lt;code&gt;@solana/web3.js&lt;/code&gt;, the library Anchor's client uses:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@solana/web3.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counterPda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="c1"&gt;// a label&lt;/span&gt;
    &lt;span class="nx"&gt;userPublicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;    &lt;span class="c1"&gt;// the user's pubkey&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;MY_PROGRAM_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// counterPda is the SAME every time for this user. No mapping needed.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapping table is gone: any time you need a user's counter, you re-derive it, and the address itself encodes who it belongs to. With Anchor you'll often skip the explicit call entirely and let the client derive PDAs from the IDL, but under the hood this is what runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: it has no private key
&lt;/h2&gt;

&lt;p&gt;Normal addresses are public keys sitting on the Ed25519 elliptic curve, and every point on that curve has a matching private key. That private key is what signs transactions. But we just &lt;em&gt;hashed&lt;/em&gt; a counter address into existence without making a keypair, so who holds its private key?&lt;/p&gt;

&lt;p&gt;If the hash happened to land on the curve, some stranger might, which would let outsiders sign for your program's accounts. So Solana guarantees a PDA sits &lt;strong&gt;off the curve&lt;/strong&gt;, meaning no private key exists for it. That is the title: an address with no private key. It is the whole point, because it means only the program that derived it can control it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bump, in one example
&lt;/h2&gt;

&lt;p&gt;A given set of seeds plus the program ID produces a valid off-curve address only about half the time, so the derivation includes one more input: a single byte called the &lt;strong&gt;bump&lt;/strong&gt;. The search starts at 255 and counts down, hashing the seeds, the program ID, and the current bump together on each attempt until the result lands off the curve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash(seeds + program_id + 255) -&amp;gt; on curve?  try 254
hash(seeds + program_id + 254) -&amp;gt; on curve?  try 253
hash(seeds + program_id + 253) -&amp;gt; off curve! use this.  bump = 253
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bump is part of the hash from the very first attempt at 255, not something added only after a bump-less hash fails. The first value that lands off-curve, the highest one that works, is the &lt;strong&gt;canonical bump&lt;/strong&gt;. &lt;code&gt;findProgramAddressSync&lt;/code&gt; returns it as the second value, the &lt;code&gt;bump&lt;/code&gt; above. You'll want to use the canonical bump rather than any other valid one, because the same seeds can produce &lt;em&gt;different&lt;/em&gt; valid addresses under different bumps, and accepting any of them would let an attacker slip a counterfeit account past your checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Anchor: derivation becomes a constraint
&lt;/h2&gt;

&lt;p&gt;On the program side, Anchor does the derivation and the check for you. Creating the counter (storing the bump for later):&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;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;authority&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="c1"&gt;// Anchor finds the canonical 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;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;Then later, validating it on every other 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="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(&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;authority&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="c1"&gt;// reuse the stored 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;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;Read the &lt;code&gt;seeds&lt;/code&gt; line as a rule: &lt;em&gt;this account's address must derive from &lt;code&gt;"counter"&lt;/code&gt; and this authority's key.&lt;/em&gt; When the instruction runs, Anchor re-derives the address and checks that the passed-in account matches. Pass a different account and it's rejected before your code runs. The address is the proof, so there's no mapping left to tamper with. (Store the bump in your &lt;code&gt;Counter&lt;/code&gt; struct at init: &lt;code&gt;pub bump: u8&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;The difference between the two structs is the whole lifecycle: &lt;code&gt;init&lt;/code&gt; &lt;em&gt;creates&lt;/em&gt; the account at the derived address the first time and lets Anchor search for the canonical bump, while the plain &lt;code&gt;seeds&lt;/code&gt; + &lt;code&gt;bump = counter.bump&lt;/code&gt; form &lt;em&gt;validates&lt;/em&gt; an account that already exists, reusing the stored bump so it doesn't pay to re-search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that trip people up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seed mismatch between client and program.&lt;/strong&gt; The seeds, their order, and their byte encoding need to match on both sides. &lt;code&gt;Buffer.from("counter")&lt;/code&gt; in the client has to line up with &lt;code&gt;b"counter"&lt;/code&gt; in the program, in the same position. One reordered or mistyped seed derives a different address, and the constraint rejects it. A lot of "my PDA doesn't match" bugs come down to this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not storing the bump.&lt;/strong&gt; If you don't save the canonical bump at init, every later instruction has to re-derive it by searching, which burns compute for no reason. Store it once in the account and reuse it with &lt;code&gt;bump = counter.bump&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the rejection as a bug.&lt;/strong&gt; When you pass the wrong account and the &lt;code&gt;seeds&lt;/code&gt; constraint throws, that's the security model working, not a failure. The point is that only the correctly derived address gets accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deriving without the canonical bump.&lt;/strong&gt; Other bump values can produce valid but &lt;em&gt;different&lt;/em&gt; addresses from the same seeds. Sticking with the canonical one, which is what Anchor's bare &lt;code&gt;bump&lt;/code&gt; and &lt;code&gt;findProgramAddressSync&lt;/code&gt; give you by default, keeps client and program in agreement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The payoff: a program that signs for itself
&lt;/h2&gt;

&lt;p&gt;No private key means no human can sign for a PDA, so how does a program move tokens out of an escrow it owns? The runtime gives the owning program a special power: the program whose ID derived the PDA can sign for it, through &lt;code&gt;invoke_signed&lt;/code&gt; during a cross-program invocation. The program's code becomes the authority, with no key anywhere.&lt;/p&gt;

&lt;p&gt;That's the foundation under most escrows, vaults, and lending pools on Solana: a program holding assets no human can drain, releasing them only by its own logic. We'll go deep on &lt;code&gt;invoke_signed&lt;/code&gt; and CPIs in the next post. For now, hold the shape: the missing private key isn't a weakness, it's what lets a program act as a trustless custodian.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stateless programs store data in accounts at addresses; remembering random addresses doesn't scale.&lt;/li&gt;
&lt;li&gt;A PDA derives the address from seeds + program ID, so you recompute it instead of storing it.&lt;/li&gt;
&lt;li&gt;It's forced off the Ed25519 curve, so no private key exists and no outsider can sign for it.&lt;/li&gt;
&lt;li&gt;The canonical bump (counting down from 255) is the reproducible address you'll normally use.&lt;/li&gt;
&lt;li&gt;In Anchor, &lt;code&gt;seeds&lt;/code&gt; + &lt;code&gt;bump&lt;/code&gt; constraints derive and validate the account for you.&lt;/li&gt;
&lt;li&gt;The deriving program can sign for its own PDA (&lt;code&gt;invoke_signed&lt;/code&gt;), which is what powers escrows and vaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/core/pda" rel="noopener noreferrer"&gt;Solana PDA docs&lt;/a&gt; cover derivation, the canonical bump, and the off-curve guarantee, with runnable examples on the &lt;a href="https://solana.com/docs/core/pda/pda-derivation" rel="noopener noreferrer"&gt;derivation page&lt;/a&gt; (the "Legacy" tab is the &lt;code&gt;@solana/web3.js&lt;/code&gt; version Anchor's client uses). The &lt;a href="https://www.anchor-lang.com/docs/basics/pda" rel="noopener noreferrer"&gt;Anchor PDA guide&lt;/a&gt; walks through the &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; constraints you'll actually write, and the full &lt;a href="https://www.anchor-lang.com/docs/references/account-constraints" rel="noopener noreferrer"&gt;account constraints reference&lt;/a&gt; lists every constraint in one place.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc puts all of this to work, and the challenges should now read like something you already understand. Not joined yet? It's not too late: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Arc 8 Catch-Up: Middleware Inside the Token</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Mon, 15 Jun 2026 15:16:14 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-8-catch-up-middleware-inside-the-token-4fe8</link>
      <guid>https://dev.to/100daysofsolana/arc-8-catch-up-middleware-inside-the-token-4fe8</guid>
      <description>&lt;p&gt;Arc 8 of &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt; was about Token-2022.&lt;/p&gt;

&lt;p&gt;That might sound odd at first, because Token-2022 had already shown up in the previous arcs.&lt;/p&gt;

&lt;p&gt;Arc 6 used token extensions to explore fees, interest, frozen accounts, and revocable credentials. Arc 7 used Token Extensions to build NFTs from the same token primitives.&lt;/p&gt;

&lt;p&gt;Arc 8 made the model explicit.&lt;/p&gt;

&lt;p&gt;The whole arc hangs off one idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Token extensions are like middleware that lives inside the asset.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Web2 developers, that is the shift worth noticing.&lt;/p&gt;

&lt;p&gt;In a normal app, rules usually sit around the asset. You write backend code. You add middleware. You call a payment processor. You run a cron job. You trust every integration to go through the right path.&lt;/p&gt;

&lt;p&gt;With Token-2022, some of those rules can live on the mint itself.&lt;/p&gt;

&lt;p&gt;That changes the shape of the system.&lt;/p&gt;

&lt;p&gt;A token is no longer just a balance that moves between accounts. It can be a balance with transfer fees, interest display, transfer restrictions, or other behavior attached.&lt;/p&gt;

&lt;p&gt;The middleware is not next to the token.&lt;/p&gt;

&lt;p&gt;It is part of the token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token rules should not depend on every app remembering them
&lt;/h2&gt;

&lt;p&gt;Most Web2 developers have built systems where rules sit outside the thing being moved.&lt;/p&gt;

&lt;p&gt;A marketplace might charge a platform fee.&lt;br&gt;&lt;br&gt;
A fintech app might show yield.&lt;br&gt;&lt;br&gt;
A loyalty system might prevent points from being transferred.&lt;br&gt;&lt;br&gt;
A membership product might issue a badge that cannot be sold.  &lt;/p&gt;

&lt;p&gt;Usually, the rule lives somewhere in your application.&lt;/p&gt;

&lt;p&gt;You might write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculateFee()
applyInterest()
rejectTransfer()
checkMembershipStatus()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works as long as every relevant path uses the same logic.&lt;/p&gt;

&lt;p&gt;But that is the weak point.&lt;/p&gt;

&lt;p&gt;One backend service might apply the fee. Another script might forget it. An integration partner might call the wrong endpoint. An admin tool might bypass the normal flow. A scheduled job might fail. A frontend might display one thing while the ledger stores another.&lt;/p&gt;

&lt;p&gt;Token-2022 takes a different approach.&lt;/p&gt;

&lt;p&gt;When the extension is configured on the mint, the Token-2022 program enforces the behavior consistently. Every wallet, CLI, dApp, and program that interacts with the token has to deal with the same rule.&lt;/p&gt;

&lt;p&gt;That is why the middleware analogy is useful, but only if you take it one step further.&lt;/p&gt;

&lt;p&gt;This is not middleware sitting in your app stack.&lt;/p&gt;

&lt;p&gt;It is middleware baked into the asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer fees are enforced by the mint
&lt;/h2&gt;

&lt;p&gt;Arc 8 started with a fee-bearing token.&lt;/p&gt;

&lt;p&gt;The exercise was simple: create a mint under the Token-2022 program, add a transfer fee configuration, mint some supply, and inspect the result.&lt;/p&gt;

&lt;p&gt;The important part was not the percentage.&lt;/p&gt;

&lt;p&gt;It was where the rule lived.&lt;/p&gt;

&lt;p&gt;The transfer fee was not a line of backend code. It was not a webhook. It was not a checkout rule. It was not a convention that wallets were expected to follow voluntarily.&lt;/p&gt;

&lt;p&gt;The fee configuration lived on the mint.&lt;/p&gt;

&lt;p&gt;That means the token itself carried the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When this token moves, apply this fee.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Web2 developers, the contrast is familiar.&lt;/p&gt;

&lt;p&gt;If you charge a fee through Stripe, you usually build fee logic around the payment flow. You decide how much to collect, when to collect it, how to reconcile it, and which integrations are allowed to move value.&lt;/p&gt;

&lt;p&gt;With Token-2022, the fee rule is part of the asset’s behavior. The transfer goes through the token program, and the token program applies the rule.&lt;/p&gt;

&lt;p&gt;That is a meaningful difference.&lt;/p&gt;

&lt;p&gt;You are not trusting every app to remember the fee. You are configuring a token that cannot move without the fee logic being considered.&lt;/p&gt;

&lt;p&gt;That is the first big Arc 8 lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The rule travels with the token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fee lifecycle is transfer, withhold, withdraw
&lt;/h2&gt;

&lt;p&gt;Creating a fee-bearing mint is only the first part.&lt;/p&gt;

&lt;p&gt;Arc 8 then put the token in motion and followed the fee lifecycle end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transfer → withhold → withdraw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lifecycle matters because transfer fees do not behave like a normal payment processor settlement flow.&lt;/p&gt;

&lt;p&gt;When a fee-bearing token is transferred, the recipient does not simply receive the full amount and then send a fee somewhere else.&lt;/p&gt;

&lt;p&gt;Instead, the fee is withheld in the recipient’s token account.&lt;/p&gt;

&lt;p&gt;For example, with a 1% transfer fee, a transfer of 1,000 tokens gives the recipient 990 spendable tokens. The remaining 10 are recorded as withheld tokens on the recipient’s token account.&lt;/p&gt;

&lt;p&gt;Those withheld tokens are not spendable by the recipient. They sit there until the withdraw authority collects them.&lt;/p&gt;

&lt;p&gt;That is a very different mental model from a Web2 marketplace fee.&lt;/p&gt;

&lt;p&gt;In a normal app, you might have a payment ledger, a treasury balance, a settlement job, a reconciliation process, and a dashboard showing fees owed.&lt;/p&gt;

&lt;p&gt;Here, the withheld amount is visible in the token account itself. The fee authority can later withdraw it using the Token-2022 program.&lt;/p&gt;

&lt;p&gt;The important thing is what you did not build.&lt;/p&gt;

&lt;p&gt;No custom program.&lt;br&gt;&lt;br&gt;
No payment processor flow.&lt;br&gt;&lt;br&gt;
No webhook.&lt;br&gt;&lt;br&gt;
No cron job.&lt;br&gt;&lt;br&gt;
No separate fee table.  &lt;/p&gt;

&lt;p&gt;The protocol enforced the fee and stored the withheld amount.&lt;/p&gt;

&lt;p&gt;That is the kind of concrete behavior that makes Token-2022 easier to understand. It is not an abstract extension system. It is a rule you can observe on devnet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Composability means extensions share the same mint
&lt;/h2&gt;

&lt;p&gt;Arc 8 then combined transfer fees with interest-bearing behavior.&lt;/p&gt;

&lt;p&gt;This is where the middleware analogy becomes more powerful.&lt;/p&gt;

&lt;p&gt;A token can have more than one rule.&lt;/p&gt;

&lt;p&gt;The mint can say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Charge a fee when tokens move.
Display balances with interest over time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those behaviors are different, but they can coexist on the same mint.&lt;/p&gt;

&lt;p&gt;That matters because Web2 developers often expect separate systems for separate behaviors.&lt;/p&gt;

&lt;p&gt;A fee might belong to a payments service.&lt;br&gt;&lt;br&gt;
Interest might belong to a financial calculation service.&lt;br&gt;&lt;br&gt;
Metadata might belong to an asset database.&lt;br&gt;&lt;br&gt;
Restrictions might belong to an access control system.  &lt;/p&gt;

&lt;p&gt;Token-2022 lets those behaviors be configured as extensions on one mint.&lt;/p&gt;

&lt;p&gt;The technical reason is that extensions are stored as structured data on the account. The mint has enough space allocated for the extensions, and the Token-2022 program knows how to read and enforce them.&lt;/p&gt;

&lt;p&gt;But the product lesson is simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A token can be configured with multiple behaviors at once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fee and interest example also made an important distinction clear.&lt;/p&gt;

&lt;p&gt;The transfer fee works on the raw token amount. It moves real token units and withholds part of the transfer.&lt;/p&gt;

&lt;p&gt;Interest-bearing behavior affects the displayed amount. It does not continuously mint new tokens in the background. The raw balance does not change every second. The UI amount is computed from the stored amount, rate, and elapsed time.&lt;/p&gt;

&lt;p&gt;That is why the two features can coexist cleanly.&lt;/p&gt;

&lt;p&gt;The fee changes what happens during transfer.&lt;br&gt;&lt;br&gt;
The interest extension changes how the balance is interpreted when read.  &lt;/p&gt;

&lt;p&gt;Those are different layers of behavior on the same asset.&lt;/p&gt;
&lt;h2&gt;
  
  
  Interest is a view, not a background job
&lt;/h2&gt;

&lt;p&gt;The interest-bearing extension is one of the easiest places for Web2 instincts to mislead you.&lt;/p&gt;

&lt;p&gt;In a conventional app, if a balance grows, you usually assume something wrote a new value somewhere.&lt;/p&gt;

&lt;p&gt;A scheduled job updated the balance.&lt;br&gt;&lt;br&gt;
A database row changed.&lt;br&gt;&lt;br&gt;
A ledger entry was added.&lt;br&gt;&lt;br&gt;
A batch process applied interest overnight.  &lt;/p&gt;

&lt;p&gt;Token-2022 does not need to work that way.&lt;/p&gt;

&lt;p&gt;With the interest-bearing extension, the raw token amount stays the same unless an actual token instruction changes it. What changes is the displayed amount.&lt;/p&gt;

&lt;p&gt;The program can calculate the UI amount based on the interest rate and time elapsed.&lt;/p&gt;

&lt;p&gt;That is why Arc 8 deliberately used a high rate on devnet. At realistic rates, the change over a short challenge window would be too small to notice. A dramatic rate makes the model visible.&lt;/p&gt;

&lt;p&gt;The lesson is not “high yield tokens are good.”&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Display can be computed from state without constantly rewriting state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Web2 developers, that is a useful mental shift.&lt;/p&gt;

&lt;p&gt;The token account stores raw units. The extension changes how those units are displayed. If you assume “displayed balance increased” means “new tokens were minted,” you will misunderstand what the program is doing.&lt;/p&gt;

&lt;p&gt;Arc 8 forced that distinction into the open.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading mint configuration is part of building
&lt;/h2&gt;

&lt;p&gt;One of the most useful Arc 8 exercises was not creating another mint.&lt;/p&gt;

&lt;p&gt;It was auditing the mints already created.&lt;/p&gt;

&lt;p&gt;That matters because Token-2022 configuration is public state. You can inspect a mint and see which extensions it uses. You can read the fee configuration. You can see whether interest-bearing behavior exists. You can check whether a token is non-transferable.&lt;/p&gt;

&lt;p&gt;That is the Solana version of inspecting a production schema.&lt;/p&gt;

&lt;p&gt;In Web2, the rules of a system are often scattered across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application code&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;li&gt;environment variables&lt;/li&gt;
&lt;li&gt;payment processor settings&lt;/li&gt;
&lt;li&gt;admin dashboards&lt;/li&gt;
&lt;li&gt;scheduled jobs&lt;/li&gt;
&lt;li&gt;private documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Solana, much of the token’s behavior is visible in the mint account.&lt;/p&gt;

&lt;p&gt;That does not mean the whole application is transparent. But it does mean the token’s configuration can be read by anyone.&lt;/p&gt;

&lt;p&gt;That is part of the Token-2022 pitch.&lt;/p&gt;

&lt;p&gt;The behavior is public.&lt;br&gt;&lt;br&gt;
The configuration is verifiable.&lt;br&gt;&lt;br&gt;
The rules cannot be silently swapped inside a private backend.  &lt;/p&gt;

&lt;p&gt;That is also why auditing matters.&lt;/p&gt;

&lt;p&gt;You should not just create a mint and trust that you typed the command correctly. You should read it back. Check the extensions. Check the authorities. Check the raw configuration. Explain what each extension does in plain English.&lt;/p&gt;

&lt;p&gt;That last part is important.&lt;/p&gt;

&lt;p&gt;If you cannot describe the behavior without looking it up, you probably do not understand the asset yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Non-transferable tokens are useful because they fail
&lt;/h2&gt;

&lt;p&gt;Arc 8 ended the build sequence with a token that refuses to move.&lt;/p&gt;

&lt;p&gt;That might sound strange because tokens usually imply transferability. Money moves. Points move. Assets move.&lt;/p&gt;

&lt;p&gt;But not every token-like thing should be transferable.&lt;/p&gt;

&lt;p&gt;A course certificate should not be sellable.&lt;br&gt;&lt;br&gt;
A membership badge might need to stay with the person who earned it.&lt;br&gt;&lt;br&gt;
A compliance credential might need to remain attached to one wallet.&lt;br&gt;&lt;br&gt;
A proof-of-attendance token might lose meaning if it can be traded.  &lt;/p&gt;

&lt;p&gt;The non-transferable extension lets the mint encode that rule.&lt;/p&gt;

&lt;p&gt;The challenge deliberately tried to break it. Create the token. Mint it. Create a recipient account. Attempt a transfer.&lt;/p&gt;

&lt;p&gt;The transfer fails.&lt;/p&gt;

&lt;p&gt;That failure is the feature.&lt;/p&gt;

&lt;p&gt;In a Web2 app, you might prevent transferability through an API check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if token.non_transferable:
    reject transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that depends on every path using the same API.&lt;/p&gt;

&lt;p&gt;With Token-2022, the token program itself rejects the transfer. The rule is part of the mint’s behavior, and any client interacting with the program has to respect it.&lt;/p&gt;

&lt;p&gt;That makes non-transferable tokens different from fees and interest.&lt;/p&gt;

&lt;p&gt;Fees and interest are still money-like features. They affect value movement and balance display.&lt;/p&gt;

&lt;p&gt;Non-transferability changes the category of the asset. The token starts to look less like currency and more like identity, membership, status, or proof.&lt;/p&gt;

&lt;p&gt;Same mint model. Same token accounts. Same CLI.&lt;/p&gt;

&lt;p&gt;Different product meaning.&lt;/p&gt;

&lt;p&gt;That is why the failed transfer matters so much. It proves the extension is not just descriptive metadata. It changes what the token can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token-2022 is configuration, but not casual configuration
&lt;/h2&gt;

&lt;p&gt;A tempting takeaway from Arc 8 would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Token-2022 lets you add features with flags.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is true, but incomplete.&lt;/p&gt;

&lt;p&gt;The better takeaway is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Token-2022 lets you design token behavior through mint configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Extensions feel easy because they can be created from the CLI. But they are not throwaway settings. They shape what the asset is, who can use it, and how every integration will experience it.&lt;/p&gt;

&lt;p&gt;A fee-bearing token is different from a normal token.&lt;/p&gt;

&lt;p&gt;An interest-bearing token is different from a normal token.&lt;/p&gt;

&lt;p&gt;A non-transferable token is very different from a normal token.&lt;/p&gt;

&lt;p&gt;Once those behaviors are on the mint, they are part of the asset’s design.&lt;/p&gt;

&lt;p&gt;That brings Arc 8 back to the product questions underneath the technical work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should every transfer charge a fee?
Who can withdraw withheld fees?
Should balances display with interest?
Should this asset be transferable at all?
Which authorities control the rules?
Will wallets and integrations understand these extensions?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are not just CLI questions.&lt;/p&gt;

&lt;p&gt;They are product, governance, and integration questions.&lt;/p&gt;

&lt;p&gt;Token-2022 gives you reusable building blocks, but you still have to choose the right ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing turns extensions into patterns
&lt;/h2&gt;

&lt;p&gt;Arc 8 ended by turning the week into a public post and social thread.&lt;/p&gt;

&lt;p&gt;That fits the arc well because Token-2022 is easy to explain badly.&lt;/p&gt;

&lt;p&gt;You can list extensions all day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfer fees&lt;/li&gt;
&lt;li&gt;interest-bearing&lt;/li&gt;
&lt;li&gt;non-transferable&lt;/li&gt;
&lt;li&gt;default account state&lt;/li&gt;
&lt;li&gt;permanent delegate&lt;/li&gt;
&lt;li&gt;confidential transfers&lt;/li&gt;
&lt;li&gt;memo transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But a list is not a mental model.&lt;/p&gt;

&lt;p&gt;A useful post needs to show the pattern.&lt;/p&gt;

&lt;p&gt;For Arc 8, the pattern was a trilogy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A token that charges a fee.
A token that charges a fee and displays interest.
A token that refuses to move.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more memorable than “I tried some Token-2022 extensions.”&lt;/p&gt;

&lt;p&gt;It shows the range of what extensions can do.&lt;/p&gt;

&lt;p&gt;One changes transfer economics.&lt;br&gt;&lt;br&gt;
One composes transfer economics with display behavior.&lt;br&gt;&lt;br&gt;
One changes whether transfer is allowed at all.  &lt;/p&gt;

&lt;p&gt;That is the story.&lt;/p&gt;

&lt;p&gt;The best write-up would include what actually happened on devnet: the fee being withheld, the interest-adjusted UI amount changing without a transaction, and the failed transfer error from the non-transferable token.&lt;/p&gt;

&lt;p&gt;Those details matter because they prove the learning.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;“I learned Token-2022.”&lt;/p&gt;

&lt;p&gt;More like:&lt;/p&gt;

&lt;p&gt;“I created a token that charged a transfer fee, watched the fee sit as a withheld amount on the recipient account, stacked interest on the same mint, then built a token that refused to transfer at all.”&lt;/p&gt;

&lt;p&gt;That is useful developer storytelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 8 sets up
&lt;/h2&gt;

&lt;p&gt;Strip Arc 8 back to its core and the main ideas are clear:&lt;/p&gt;

&lt;p&gt;Token-2022 lets token behavior live on the mint. Transfer fees are enforced by the token program. Fees follow a visible lifecycle: transfer, withhold, withdraw. Interest-bearing behavior changes displayed amounts without constantly rewriting raw balances. Extensions can compose on the same mint. Mint configuration is public and inspectable. Non-transferable tokens show that extensions are not only about money; they can turn tokens into identity, credential, or membership objects.&lt;/p&gt;

&lt;p&gt;That is the real shift.&lt;/p&gt;

&lt;p&gt;Arc 5 taught us to create and manage tokens.&lt;/p&gt;

&lt;p&gt;Arc 6 taught us to design token behavior with extensions.&lt;/p&gt;

&lt;p&gt;Arc 7 showed that NFTs are built from the same token primitives.&lt;/p&gt;

&lt;p&gt;Arc 8 pulled the model together: Token-2022 is the extension system that makes many of those behaviors possible without custom on-chain programs.&lt;/p&gt;

&lt;p&gt;From here, the question becomes more practical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do you choose the right token behavior for the product you are building?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the mindset to carry forward.&lt;/p&gt;

&lt;p&gt;Use this post as the map, revisit the Arc 8 challenges when you want the hands-on version, and remember the central lesson: with Token-2022, the rule does not sit beside the asset. The rule can live inside the asset.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>learning</category>
    </item>
    <item>
      <title>Arc 7 Catch-Up: Building NFTs from First Principles</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Mon, 15 Jun 2026 14:28:59 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-7-catch-up-building-nfts-from-first-principles-5ae3</link>
      <guid>https://dev.to/100daysofsolana/arc-7-catch-up-building-nfts-from-first-principles-5ae3</guid>
      <description>&lt;p&gt;Arc 7 of &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt; was about building NFTs from first principles.&lt;/p&gt;

&lt;p&gt;Instead of starting with marketplaces, profile pictures, or NFT culture, the arc stripped the model back to the underlying token mechanics: supply, decimals, mint authority, metadata, collections, and provenance.&lt;/p&gt;

&lt;p&gt;After Arc 6, we already knew that Token-2022 can turn tokens into programmable objects with rules: interest, fees, frozen accounts, credentials, revocation, and metadata.&lt;/p&gt;

&lt;p&gt;Arc 7 took that model into NFTs.&lt;/p&gt;

&lt;p&gt;But the useful surprise was that NFTs are not a completely separate world.&lt;/p&gt;

&lt;p&gt;The whole arc hangs off one idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Solana NFT is built from the same token primitives you already know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Web2 developers, that is the shift worth noticing.&lt;/p&gt;

&lt;p&gt;An NFT is not magic. It is not “a JPEG on the blockchain.” It is not a special category of database record maintained by some mysterious NFT system.&lt;/p&gt;

&lt;p&gt;At its simplest, an NFT is a token mint with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;supply locked at one&lt;/li&gt;
&lt;li&gt;zero decimals&lt;/li&gt;
&lt;li&gt;mint authority disabled&lt;/li&gt;
&lt;li&gt;metadata attached&lt;/li&gt;
&lt;li&gt;optional collection membership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the foundation Arc 7 built up, one layer at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  NFTs are not separate from tokens
&lt;/h2&gt;

&lt;p&gt;Arc 7 started by stripping the NFT model down to its smallest useful form.&lt;/p&gt;

&lt;p&gt;Before metadata, images, attributes, marketplaces, or collections, an NFT is just a token that cannot be split and cannot be duplicated.&lt;/p&gt;

&lt;p&gt;That means two choices matter immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;decimals = 0
supply = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero decimals means the token cannot be divided into fractions. You cannot own 0.5 of it.&lt;/p&gt;

&lt;p&gt;Supply of one means there is only one unit.&lt;/p&gt;

&lt;p&gt;But supply of one is not enough by itself. If the mint authority still exists, someone could mint another unit later.&lt;/p&gt;

&lt;p&gt;So the final step is to disable the mint authority.&lt;/p&gt;

&lt;p&gt;That is the moment the token becomes meaningfully non-fungible. The network can see that the supply is one, the decimals are zero, and no authority remains that can create a second copy.&lt;/p&gt;

&lt;p&gt;For Web2 developers, a useful comparison is a unique database row with a primary key.&lt;/p&gt;

&lt;p&gt;But the analogy only goes so far.&lt;/p&gt;

&lt;p&gt;In a normal database, the application owner can usually edit the row, clone it, delete it, or change the surrounding rules. On Solana, once the mint authority is disabled, the supply constraint is enforced by the token program.&lt;/p&gt;

&lt;p&gt;The uniqueness is not a UI convention. It is not a marketplace promise. It is part of the token state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata turns a token into something recognizable
&lt;/h2&gt;

&lt;p&gt;The first NFT in Arc 7 was intentionally plain.&lt;/p&gt;

&lt;p&gt;It had no name, no image, no creator story, no attributes, and no collection. It was non-fungible, but not especially meaningful to a human.&lt;/p&gt;

&lt;p&gt;That is where metadata comes in.&lt;/p&gt;

&lt;p&gt;Arc 7 used Token-2022 metadata to give the NFT a name, symbol, and URI. The URI pointed to an off-chain JSON file containing richer information such as the description, image, and attributes.&lt;/p&gt;

&lt;p&gt;That gives us a two-layer model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On-chain:
mint, supply, decimals, authority, name, symbol, URI

Off-chain:
image, description, attributes, richer display data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split matters.&lt;/p&gt;

&lt;p&gt;The expensive, consensus-critical parts live on-chain. The heavier display information usually lives off-chain. Wallets and explorers follow the pointer: read the mint, get the URI, fetch the JSON, render the asset.&lt;/p&gt;

&lt;p&gt;For Web2 developers, this is familiar once you stop treating the NFT as mysterious.&lt;/p&gt;

&lt;p&gt;The on-chain metadata is like the core record. The URI is like a foreign key or external reference. The JSON is like the richer document the UI uses to render the page.&lt;/p&gt;

&lt;p&gt;The important difference is that the ownership, supply, and core metadata live on a shared network rather than inside one application database.&lt;/p&gt;

&lt;p&gt;Arc 7 also made an important historical point. In older Solana NFT workflows, metadata often lived in a separate Metaplex Token Metadata account next to the mint. In this arc, Token Extensions put metadata directly on the mint account itself.&lt;/p&gt;

&lt;p&gt;That is why the exercise was useful. It showed the foundation before introducing convenience tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  An NFT collection is an on-chain relationship
&lt;/h2&gt;

&lt;p&gt;Once the arc had created a single NFT, the next step was collections.&lt;/p&gt;

&lt;p&gt;In Web2 terms, this is easy to understand.&lt;/p&gt;

&lt;p&gt;A product belongs to a catalog.&lt;br&gt;
A ticket belongs to an event.&lt;br&gt;
A badge belongs to an award program.&lt;br&gt;
A collectible belongs to a set.&lt;/p&gt;

&lt;p&gt;In a database, you might model that with a foreign key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nft.collection_id = collection.id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arc 7 built the Solana equivalent using Token-2022 group and member extensions.&lt;/p&gt;

&lt;p&gt;Strictly speaking, there are four related pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GroupPointer&lt;/code&gt;, which points a collection mint at the account that stores group data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TokenGroup&lt;/code&gt;, which stores the group data itself&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GroupMemberPointer&lt;/code&gt;, which points an NFT mint at the account that stores member data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TokenGroupMember&lt;/code&gt;, which stores the member’s group address and member number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the arc, those pointers point back to the mint accounts themselves, so the group and member data live directly on the relevant mints.&lt;/p&gt;

&lt;p&gt;That is why it is fair to talk about a collection mint and member mints, but the underlying model is a little more precise than “one group extension and one member extension.”&lt;/p&gt;

&lt;p&gt;The collection mint represents the group. Each member NFT stores membership data that links it to that group.&lt;/p&gt;

&lt;p&gt;The important part is that this relationship is not just a label in a marketplace database. It is data stored on-chain and readable by wallets, explorers, and programs.&lt;/p&gt;

&lt;p&gt;The arc’s useful mental model was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collection mint = the group
member mint = the individual NFT
member data = the link back to the group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the NFT version of a foreign key relationship.&lt;/p&gt;

&lt;p&gt;But there is an extra trust step. A pointer by itself is not proof. A malicious mint can point at something it should not. So a client should not stop at “this member says it belongs to this collection.”&lt;/p&gt;

&lt;p&gt;It needs to resolve the pointer and verify that the relationship is internally consistent.&lt;/p&gt;

&lt;p&gt;That means checking that the member identifies the collection, and that the account being pointed to actually identifies the mint back.&lt;/p&gt;

&lt;p&gt;And the creation step matters too. Initializing a group member is not just something any random token creator can do unilaterally. The collection’s authority has to authorize membership.&lt;/p&gt;

&lt;p&gt;That is the stronger provenance claim: the collection relationship is not just stored on-chain; it is created through an authorized on-chain action.&lt;/p&gt;

&lt;p&gt;As with Arc 6, the schema decisions matter. A mint must be created with the right extensions. You cannot casually retrofit yesterday’s NFT into a collection if it was not created with the member structure it needs.&lt;/p&gt;

&lt;p&gt;That constraint can feel annoying, but it is also part of what makes the structure verifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provenance is something you can inspect
&lt;/h2&gt;

&lt;p&gt;One of the most valuable Arc 7 exercises was not creating another asset.&lt;/p&gt;

&lt;p&gt;It was auditing the collection.&lt;/p&gt;

&lt;p&gt;That matters because NFT language often gets vague. Provenance, authenticity, ownership, rarity, official collections — these terms can become marketing fog very quickly.&lt;/p&gt;

&lt;p&gt;Arc 7 made the idea concrete.&lt;/p&gt;

&lt;p&gt;You inspect the collection mint.&lt;br&gt;
You inspect the member NFT.&lt;br&gt;
You check the member data.&lt;br&gt;
You verify that the collection relationship resolves correctly.&lt;/p&gt;

&lt;p&gt;That last step matters.&lt;/p&gt;

&lt;p&gt;A one-way check is not enough. It is not sufficient to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This NFT points to collection C.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer check is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This NFT points to member data.
That member data names this NFT mint.
That member data names collection C.
The group data for collection C names collection C.
The membership was created under the collection’s authority.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what makes the relationship more than vibes.&lt;/p&gt;

&lt;p&gt;The collection claim is not true merely because a website says so. It is not true merely because a field contains a familiar collection address. It is true when the on-chain relationship resolves correctly and the collection authority has authorized that membership.&lt;/p&gt;

&lt;p&gt;For Web2 developers, this is like checking that a foreign key resolves correctly and that the row was created by a service with permission to write to that table.&lt;/p&gt;

&lt;p&gt;The difference is that the database is public, shared, and inspectable by anyone.&lt;/p&gt;

&lt;p&gt;That is a big part of the NFT value proposition when you remove the hype.&lt;/p&gt;

&lt;p&gt;The asset can carry enough information for independent verification, but only if clients verify the relationship properly. Pointers are useful because they let accounts reference each other. They are dangerous if treated as proof on their own.&lt;/p&gt;

&lt;p&gt;That is also why the CLI work matters.&lt;/p&gt;

&lt;p&gt;It is tempting to think of NFTs through wallets, marketplaces, and images. But the more useful developer habit is to read the underlying state.&lt;/p&gt;

&lt;p&gt;What is the supply?&lt;br&gt;
What are the decimals?&lt;br&gt;
Is the mint authority disabled?&lt;br&gt;
Is metadata present?&lt;br&gt;
What URI is stored?&lt;br&gt;
Is there group member data?&lt;br&gt;
Does it name the expected collection?&lt;br&gt;
Does the pointed-to data name this mint back?&lt;br&gt;
Was the membership authorized by the collection authority?&lt;/p&gt;

&lt;p&gt;That is how NFTs stop feeling like magic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mutability is a design choice
&lt;/h2&gt;

&lt;p&gt;Arc 7 then moved from creation to mutation.&lt;/p&gt;

&lt;p&gt;Because the creator still held the metadata update authority, they could rename the NFT, add a custom field, remove that field, or point the URI at a new JSON file.&lt;/p&gt;

&lt;p&gt;That is an important lesson because “on-chain” does not always mean “unchangeable.”&lt;/p&gt;

&lt;p&gt;Some parts of the NFT were locked. The supply was one. The decimals were zero. The mint authority had been disabled.&lt;/p&gt;

&lt;p&gt;But the metadata could still change while the update authority existed.&lt;/p&gt;

&lt;p&gt;That is not automatically good or bad. It is a design choice.&lt;/p&gt;

&lt;p&gt;A game asset might need metadata updates as it levels up.&lt;br&gt;
A ticket might need status changes.&lt;br&gt;
A credential might need expiry data.&lt;br&gt;
A dynamic collectible might intentionally evolve over time.&lt;/p&gt;

&lt;p&gt;But for other assets, mutability might undermine trust.&lt;/p&gt;

&lt;p&gt;If someone buys an NFT because of its image, description, or attributes, they care about whether those things can change later. If a collection promises permanence, the update authority becomes part of the trust model.&lt;/p&gt;

&lt;p&gt;Arc 7 also showed a practical split between on-chain and off-chain updates.&lt;/p&gt;

&lt;p&gt;Changing the on-chain name or URI can happen quickly. Changing the image behind the URI depends on off-chain hosting and caching. Wallets and explorers may continue showing an old image for a while, even after the metadata has changed.&lt;/p&gt;

&lt;p&gt;That is a useful reminder: NFTs are not purely on-chain objects.&lt;/p&gt;

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

&lt;p&gt;The token, ownership, supply, and metadata pointer can live on-chain. The image and richer media often live somewhere else.&lt;/p&gt;

&lt;p&gt;So the storage decision matters. A temporary GitHub Gist is fine for devnet learning. A serious project needs to think harder about durable storage: Arweave is designed for pay-once permanent storage, while IPFS content remains available only while someone continues to pin or serve it.&lt;/p&gt;
&lt;h2&gt;
  
  
  NFTs are records with ownership, display, and history
&lt;/h2&gt;

&lt;p&gt;The practical Web2 bridge for Arc 7 was not “NFTs are images.”&lt;/p&gt;

&lt;p&gt;A better bridge is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NFTs are unique records with ownership, display metadata, and provenance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing is much more useful.&lt;/p&gt;

&lt;p&gt;A normal application might have unique records everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event tickets&lt;/li&gt;
&lt;li&gt;certificates&lt;/li&gt;
&lt;li&gt;software licenses&lt;/li&gt;
&lt;li&gt;game items&lt;/li&gt;
&lt;li&gt;collectible cards&lt;/li&gt;
&lt;li&gt;access passes&lt;/li&gt;
&lt;li&gt;membership badges&lt;/li&gt;
&lt;li&gt;digital art editions&lt;/li&gt;
&lt;li&gt;proof-of-attendance records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Web2, those records usually live inside one company’s database. Ownership and transfer rules are whatever that application says they are.&lt;/p&gt;

&lt;p&gt;On Solana, the unique record can be represented as a token mint. Ownership can be held by a wallet. Supply can be locked. Metadata can be attached. Collection membership can be verified.&lt;/p&gt;

&lt;p&gt;That does not mean every unique record should be an NFT.&lt;/p&gt;

&lt;p&gt;It does mean the NFT model is easier to reason about once you stop starting with the JPEG.&lt;/p&gt;

&lt;p&gt;Start with the record.&lt;/p&gt;

&lt;p&gt;Then ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should this record be unique?
Should ownership be visible?
Should it be transferable?
Should it belong to a collection?
Should its metadata be mutable?
Should the media be permanent?
Who should hold the update authority?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are product questions, not just blockchain questions.&lt;/p&gt;

&lt;p&gt;That is the thread connecting Arc 7 back to Arc 6.&lt;/p&gt;

&lt;p&gt;Token design is product design. NFT design is product design too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing forces the model to become simple
&lt;/h2&gt;

&lt;p&gt;Arc 7 ended, like the earlier arcs, by writing and sharing.&lt;/p&gt;

&lt;p&gt;That matters because NFTs are surrounded by baggage. People bring assumptions from marketplaces, profile pictures, speculation, scams, and culture wars.&lt;/p&gt;

&lt;p&gt;A useful developer write-up has to cut through that.&lt;/p&gt;

&lt;p&gt;The best post from this arc would not start with “NFTs are back” or “NFTs are dead.” It would explain what was actually built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I created a token with supply 1 and zero decimals.
I disabled the mint authority.
I added metadata.
I pointed the metadata at a JSON file.
I created a collection mint.
I linked member NFTs to the collection.
I inspected the on-chain relationship.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The point is not to defend NFTs as a category. The point is to understand the technical model.&lt;/p&gt;

&lt;p&gt;A good write-up might focus on one concrete surprise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an NFT is just a token mint with specific constraints&lt;/li&gt;
&lt;li&gt;metadata can live directly on the mint with Token Extensions&lt;/li&gt;
&lt;li&gt;collection membership can be verified on-chain&lt;/li&gt;
&lt;li&gt;provenance is an authorized relationship, not just a marketplace claim&lt;/li&gt;
&lt;li&gt;metadata can be mutable if the update authority remains active&lt;/li&gt;
&lt;li&gt;off-chain media is only as durable as the place it is hosted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That kind of explanation is useful because it gives the next Web2 developer a handle on the system.&lt;/p&gt;

&lt;p&gt;Not hype.&lt;/p&gt;

&lt;p&gt;A model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 7 sets up
&lt;/h2&gt;

&lt;p&gt;Strip Arc 7 back to its core and the main ideas are clear:&lt;/p&gt;

&lt;p&gt;A Solana NFT is built from token primitives. Non-fungibility comes from zero decimals, supply of one, and disabled mint authority. Metadata makes the asset recognizable. Off-chain JSON gives wallets and explorers richer display information. Collections are on-chain relationships between group and member data. Provenance is stronger than a pointer: the member relationship must be authorized by the collection authority and verified in both directions so pointer spoofing does not masquerade as legitimacy. Metadata mutability depends on who controls the update authority.&lt;/p&gt;

&lt;p&gt;That is the real shift.&lt;/p&gt;

&lt;p&gt;Arc 5 taught us to create and manage tokens.&lt;/p&gt;

&lt;p&gt;Arc 6 taught us to design token behavior with extensions.&lt;/p&gt;

&lt;p&gt;Arc 7 showed that NFTs are not a separate technology stack. They are the same token model, shaped into unique digital assets with metadata and provenance.&lt;/p&gt;

&lt;p&gt;From here, the question becomes more practical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do these assets interact with real programs, wallets, apps, and users?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where the next arc can take the model beyond minting and inspection, and into more realistic application behavior.&lt;/p&gt;

&lt;p&gt;Use this post as the map, revisit the Arc 7 challenges when you want the hands-on version, and carry the NFT mental model into what comes next.&lt;/p&gt;

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