Mini Capstone: Kit Client for an Anchor Program
Ship one real client slice: connect a wallet, read state, send a real transaction, submit a real program action, and verify the outcome.
This capstone should feel smaller than a full product and more real than a toy.
That is the right target.
You are not building ten pages.
You are building one vertical slice that proves your client architecture is sound.
A good vertical slice does four things cleanly:
- acquires signer access
- reads chain state
- submits a real transaction
- proves the result after the write path finishes
If one of those is fake, the slice is incomplete.
What You Are Building
Build one page that composes the pieces from the earlier lessons:
- wallet connection
- account inspection
- one real SOL transfer card
- one real Anchor program action button
That is enough.
Do not add tabs, dashboards, or multi-program routing.
The point is correctness, not surface area.
Step 1: Prepare One Real Devnet Wallet And One Real Recipient
This page only becomes runnable if the browser wallet and the local setup script both use the same authority.
Use one dedicated devnet wallet for that.
The cleanest path is to use a mnemonic and derive the first Solana account from it in both places.
- create or import one dedicated devnet wallet in your browser extension
- keep that wallet connected to devnet
- store its mnemonic locally in
.env.local
CAPSTONE_AUTHORITY_MNEMONIC="word1 word2 word3 ... word12"Now install the one extra dependency needed to derive the same wallet locally:
pnpm add bip39 micro-ed25519-hdkeyNext, create one real recipient address you can inspect later:
mkdir -p keys
solana-keygen new --no-bip39-passphrase -o keys/kit-capstone-recipient.json
solana address -k keys/kit-capstone-recipient.jsonCopy the printed recipient address.
You will use it for the SOL transfer card.
Step 2: Create One Real Counter Account With The Same Authority
Create scripts/create-capstone-counter.ts:
import fs from "node:fs";
import * as bip39 from "bip39";
import { HDKey } from "micro-ed25519-hdkey";
import {
airdropFactory,
address,
appendTransactionMessageInstructions,
createKeyPairSignerFromBytes,
createKeyPairSignerFromPrivateKeyBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { getInitializeInstruction } from "../lib/generated/counter";
const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL ?? "https://api.devnet.solana.com";
const RPC_WS_URL = process.env.NEXT_PUBLIC_RPC_WS_URL ?? "wss://api.devnet.solana.com";
const SYSTEM_PROGRAM_ADDRESS = address("11111111111111111111111111111111");
async function getAuthorityFromMnemonic(mnemonic: string) {
const seed = bip39.mnemonicToSeedSync(mnemonic);
const hd = HDKey.fromMasterSeed(seed.toString("hex"));
const child = hd.derive("m/44'/501'/0'/0'");
return createKeyPairSignerFromPrivateKeyBytes(new Uint8Array(child.privateKey));
}
async function loadRecipient() {
const recipientBytes = Uint8Array.from(
JSON.parse(fs.readFileSync("keys/kit-capstone-recipient.json", "utf8")),
);
return createKeyPairSignerFromBytes(recipientBytes);
}
async function main() {
const mnemonic = process.env.CAPSTONE_AUTHORITY_MNEMONIC;
if (!mnemonic) {
throw new Error("Missing CAPSTONE_AUTHORITY_MNEMONIC in .env.local");
}
const rpc = createSolanaRpc(RPC_URL);
const rpcSubscriptions = createSolanaRpcSubscriptions(RPC_WS_URL);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
const authority = await getAuthorityFromMnemonic(mnemonic);
const recipient = await loadRecipient();
const counter = await generateKeyPairSigner();
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: authority.address,
lamports: lamports(2_000_000_000n),
commitment: "confirmed",
});
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const initializeInstruction = getInitializeInstruction({
authority: authority.address,
counter,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
});
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(authority, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([initializeInstruction], tx),
);
const signedTransaction = await signTransactionMessageWithSigners(
transactionMessage,
);
await sendAndConfirmTransaction(signedTransaction, {
commitment: "confirmed",
});
console.log("AUTHORITY_ADDRESS=", authority.address);
console.log("TRANSFER_DESTINATION=", recipient.address);
console.log("COUNTER_ACCOUNT=", counter.address);
console.log(
"INITIALIZE_SIGNATURE=",
getSignatureFromTransaction(signedTransaction),
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});Run it with:
pnpm tsx scripts/create-capstone-counter.tsThis script does four important jobs:
- derives the same authority that your browser wallet should be using
- funds that authority on devnet
- creates a brand new counter account
- prints the exact addresses the capstone page needs
If the authority address printed here does not match the connected browser wallet, stop and fix that first.
Do not keep going with mismatched authorities.
Step 3: Save The Printed Values In One Config File
Create lib/solana/capstoneConfig.ts:
export const CAPSTONE_TRANSFER_DESTINATION = "PASTE_TRANSFER_DESTINATION_HERE";
export const CAPSTONE_COUNTER_ACCOUNT = "PASTE_COUNTER_ACCOUNT_HERE";Paste the exact values printed by create-capstone-counter.ts.
That makes the page deterministic.
No fake placeholders. No guessing.
Step 4: Put The Capstone On A Semantic Route
Create app/kit-client-capstone/page.tsx:
"use client";
import React from "react";
import { WalletPanel } from "@/components/solana/WalletPanel";
import { AccountInspector } from "@/components/solana/AccountInspector";
import { TxActionCard } from "@/components/solana/TxActionCard";
import { CounterActionButton } from "@/components/solana/CounterActionButton";
import {
CAPSTONE_COUNTER_ACCOUNT,
CAPSTONE_TRANSFER_DESTINATION,
} from "@/lib/solana/capstoneConfig";
export default function KitClientCapstonePage() {
return (
<main className="max-w-3xl mx-auto p-6 space-y-6">
<h1 className="text-2xl font-bold">Kit Client Capstone</h1>
<section>
<h2 className="text-lg font-semibold mb-2">Wallet</h2>
<WalletPanel />
</section>
<section>
<h2 className="text-lg font-semibold mb-2">Read Chain State</h2>
<AccountInspector />
</section>
<section>
<h2 className="text-lg font-semibold mb-2">Send A Real Transaction</h2>
<TxActionCard destination={CAPSTONE_TRANSFER_DESTINATION} />
</section>
<section>
<h2 className="text-lg font-semibold mb-2">Submit A Real Program Action</h2>
<CounterActionButton counterAddress={CAPSTONE_COUNTER_ACCOUNT} />
</section>
</main>
);
}This route name matters.
The curriculum is not week-based anymore.
Your app structure should stop pretending it is.
Step 5: Verify The Slice End To End
A successful capstone run should let you prove all of these:
- the wallet panel shows a real connected session
- the account inspector fetches a real account from the configured cluster
- the transaction card returns a real signature after sending SOL
- the program action button returns a real signature for the Anchor instruction
- you can inspect both signatures in Explorer
That is the real checkpoint.
Not whether the page "looks complete".
The Main Failure Modes
The wallet connects but writes still fail
That usually means one of these:
- wrong cluster
- wrong destination address
- not enough devnet SOL
- the connected wallet does not match the mnemonic used by the setup script
The read panel works but the program action fails
That usually means your generated instruction builder or account wiring is wrong.
This is exactly why the service layer exists.
The failure should be isolated to the program action path, not spread through the page.
The signatures appear but you never verify them
Then the capstone is incomplete.
A real client flow ends with verification.
Use Explorer or your own read-back path and confirm that the state change actually happened.
What You Can Do After This
You can build one real client page that combines wallet access, reads, writes, program actions, and verification without collapsing into ad hoc code.
That is enough foundation to start a larger Solana frontend responsibly.
Quick Check
Why does the capstone insist that the browser wallet and the setup script use the same authority?
What makes the capstone incomplete even if the page appears to work?
Up Next in Solana Kit Clients
Solana Kit Clients
Return to the module overview and use it as the reference entry point for the full modern client stack you just assembled lesson by lesson.
Sources
- https://solana.com/docs/frontend/client
- https://solana.com/docs/frontend/react-hooks
- https://solana.com/developers/cookbook/wallets/restore-from-mnemonic
- https://solana.com/developers/cookbook/development/load-keypair-from-file
- https://solana.com/developers/cookbook/development/test-sol
- https://www.anchor-lang.com/docs/clients/typescript