Kit without a client
Use Kit's full power without the client abstraction
This guide shows how to achieve the same capabilities a Kit client gives you, but using Kit's primitives directly. The page is organized as a reference: each section takes one piece of what a fully composed client offers and shows the manual equivalent, so you can pick and choose the exact bits you need.
When to skip the client
Going client-less is the right choice when you want the smallest possible bundles, full control over every step of the transaction lifecycle, or when you are writing library code that should not assume a Kit client. The cost is a little more boilerplate and a less prescriptive structure. The client model, on the other hand, gives you consistent ergonomics and a curated set of plugin interfaces other plugins can build on.
If you need ergonomics on top of a curated set of primitives, consider creating a custom plugin bundle instead. A bundle plugin is just a regular plugin that composes several smaller ones, so you keep the client model while owning exactly which primitives are wired up.
Set up RPC
Kit ships two factories for talking to a Solana RPC node: createSolanaRpc for HTTP requests and createSolanaRpcSubscriptions for the WebSocket channel. Both return typed proxies with the full Solana JSON-RPC API surface available on them.
The objects returned by these factories are exactly what client.rpc and client.rpcSubscriptions expose under the hood, so any code that takes an Rpc or RpcSubscriptions works in either setup. See RPC requests and RPC subscriptions for the user-facing API.
Create a signer
The most common way to create a signer is to generate a fresh in-memory keypair with generateKeyPairSigner. The returned object is a KeyPairSigner that satisfies both the TransactionSigner and MessageSigner interfaces.
If you already have a keypair stored as a JSON byte array — for example, a Solana CLI keypair file — you can rebuild a signer from it with createKeyPairSignerFromBytes. See Setting up signers for the broader signer setup story and Advanced guides — Signers for the underlying signer interfaces.
Build a transaction message
Kit transaction messages are immutable: each helper returns a new object with a narrower type, so TypeScript can enforce that required fields are set before the message is signed or sent. The pipe helper threads a value through a sequence of transformations and lets the type narrow at each step.
This is the same composition the bundled clients use internally. See Advanced guides — Transactions for the full transaction message API, including durable-nonce lifetimes and other configuration helpers.
Estimate compute units
Setting an explicit compute unit limit close to what your transaction actually consumes increases the chance of inclusion, packs more transactions per block, and reduces priority fees. Kit ships everything you need for this without leaving @solana/kit: estimateComputeUnitLimitFactory simulates the message to estimate the cost, and estimateAndSetComputeUnitLimitFactory wraps that estimator to write the result back onto the message in one step.
Bundled clients such as solanaRpc and litesvm already do this for you when sending transactions.
Sign the transaction
Once a transaction message is fully configured, signTransactionMessageWithSigners extracts every signer attached to it (the fee payer plus any signer-typed instruction inputs) and produces a signed Transaction.
Two assertions are typically useful at this point: assertIsSendableTransaction verifies the transaction has every required signature and fits within the size limit, and assertIsTransactionWithBlockhashLifetime narrows the lifetime so it can be passed to sendAndConfirmTransactionFactory below. The transaction identifier on Solana is the fee payer's signature, which is fully determined as soon as the fee payer signs — getSignatureFromTransaction(signedTransaction) returns that identifier directly, without needing to send the transaction first.
Send and confirm a transaction
sendAndConfirmTransactionFactory builds a function that sends a signed transaction and waits for the configured commitment. It needs both an rpc and an rpcSubscriptions because confirmation listens to slot and signature notifications.
For durable-nonce transactions, use sendAndConfirmDurableNonceTransactionFactory instead, which knows how to confirm against a nonce account instead of a recent blockhash. If you don't need confirmation at all, sendTransactionWithoutConfirmingFactory returns a fire-and-forget sender. All three return Promise<void>; pair them with getSignatureFromTransaction to obtain the signature for logging or display.
Fetch and decode accounts
To read onchain state, fetchEncodedAccount wraps getAccountInfo and returns a MaybeEncodedAccount whose shape is consistent regardless of whether the account exists. assertAccountExists narrows the result so the rest of your code can rely on a fully populated account.
For typed reads, the @solana-program/* packages export fetchX and decodeX standalone helpers that take any Rpc and return decoded Account<T> values without needing a Kit client. See Fetching accounts for the full helper line-up and Codecs for decoding raw bytes when no helper is available.
Next steps
- Plugins — opt back in to the client model when you want it.
- Plugins — Creating custom plugins — wrap a curated set of primitives into a reusable bundle.
- Advanced guides — Transactions — the full transaction message API.
- Advanced guides — Signers — the signer interfaces in depth.