Building Transaction Messages
Compose versioned transaction messages with fee payer, lifetime, and instructions.
Recipe
Quick-reference recipe card - copy-paste ready.
import {
createTransactionMessage,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
} from "@solana/kit";When to reach for this:
- Any custom instruction bundle (DeFi, NFT, game).
- Multi-instruction atomic transactions.
- Version 0 messages with lookup tables.
- Client-side flows before wallet signing.
Working Example
import {
createSolanaRpc,
createTransactionMessage,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
address,
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
const rpc = createSolanaRpc(process.env.RPC_URL!);
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const budgetIxs = [
getSetComputeUnitLimitInstruction({ units: 200_000 }),
getSetComputeUnitPriceInstruction({ microLamports: 5_000n }),
];
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(walletSigner, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) => appendTransactionMessageInstructions(budgetIxs, m),
(m) => appendTransactionMessageInstructions(programIxs, m)
);What this demonstrates:
- Compute budget instructions typically come first.
pipekeeps transforms readable and immutable.- Blockhash lifetime must be fresh per submit.
Deep Dive
How It Works
- Messages list accounts, instructions, and signatures required on wire.
- Version 0 enables address lookup tables to shrink static keys.
- Fee payer pays network fee; additional signers sign if writable/signing.
- Lifetime can be blockhash or durable nonce (advanced).
Message Checklist
| Step | API |
|---|---|
| Version | createTransactionMessage({ version: 0 }) |
| Fee payer | setTransactionMessageFeePayerSigner |
| Lifetime | setTransactionMessageLifetimeUsingBlockhash |
| Instructions | appendTransactionMessageInstruction(s) |
Gotchas
- Stale blockhash - confirmation fails after ~60-90s. Fix: refetch and rebuild.
- Missing signer in message - simulation error. Fix: attach all required signers.
- Instruction order - budget before program ixs. Fix: prepend compute budget.
- Account list mismatch - program expects different metas. Fix: use generated builders.
- v0 without ALT when needed - tx too large. Fix: see Address Lookup Tables.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Codama builders | Typed program ixs | One-off system transfer |
| gill helpers | Faster bootstrap | Custom account metas |
CLI solana transfer | Manual SOL send | App integration |
FAQs
Does this work on devnet?
Yes - point RPC and wallet at the same cluster.
Can I mix kit and web3.js?
Avoid in one module - migrate wholesale per route.
What about wallet adapters?
Wallets provide signers; kit builds messages.
How do I pin @solana/kit?
Lock 7.0.0 in package.json and CI.
Where are priority fees?
Compute budget instructions before send.
How to test?
Surfpool 0.12.0 or local validator with devnet clones.
Lamports or SOL?
Always bigint lamports in kit code.
Which commitment?
confirmed for UI; finalized for treasury.
Next.js server components?
Reads on server; signing in client components.
Debug failed txs?
Simulate, read logs, verify account owners.
Related
- Signing & Sending - sign and confirm
- Address Lookup Tables - shrink v0 txs
- Codecs & Serialization - custom data
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.