Build a Solana Copy-Trading Bot with Yellowstone gRPC
A code-first TypeScript guide to watching a wallet with Yellowstone gRPC, decoding candidate swaps, controlling risk, and reconciling every action against Solana RPC.
Rrpc edge Team·Jul 22, 2026·12 min read
Frequently asked questions
How do I monitor a Solana wallet in real time?
Open a Yellowstone gRPC transaction subscription with the wallet in accountInclude. The server then sends transactions that mention that account. Exclude vote and failed transactions, keep the filter narrow, and decode only the programs your application supports.
Should a copy-trading bot use processed or confirmed commitment?
Processed is useful for early detection, but it is not final. Treat it as a candidate signal, cap exposure, and reconcile the target and mirror signatures at confirmed or finalized commitment. Use confirmed directly if your strategy values stronger state over earlier notification.
Does accountInclude prove that the watched wallet made the trade?
No. It only proves that the transaction mentions the wallet. Your decoder must verify that the wallet is a signer or otherwise has the exact role your policy requires, identify the supported swap instruction, and reject unrelated transfers or account maintenance.
How do I prevent a reconnect from copying the same transaction twice?
Use the target signature as an idempotency key in durable storage. Record a candidate before submitting a mirror transaction, update the record with the mirror signature, and make every retry read that record before acting.
Can Yellowstone gRPC guarantee that a copied trade is profitable?
No. Yellowstone provides a filtered stream, not a trading outcome. Profitability depends on the target, decoder correctness, market movement, fees, slippage, execution, and risk controls. Backtest and paper trade before using capital.
This is the code-first companion to
Copy-Trading on Solana: The Infrastructure Behind It.
That guide explains the detect, decide, and send architecture. This one builds the detection worker
and the state machine around it.
A production bot still needs a program-specific decoder, quote and route construction, signing,
secure key management, and a transaction sender. Those boundaries stay explicit below. The code
does not pretend that a generic transaction can be converted safely into a mirror trade.
Do not put all five jobs inside one stream callback. A burst of updates can create unbounded async
work, memory growth, and duplicate submissions. The callback should normalize the event and hand it
to a bounded queue or durable worker.
The examples use Triton One's maintained Node client and to render Solana signatures:
bs58
terminal
npm install @triton-one/yellowstone-grpc bs58
Yellowstone is a bidirectional gRPC subscription built on Solana's Geyser interface. Its official
repository documents transaction filters for included, excluded, and required accounts, plus the
processed, confirmed, and finalized commitment levels. See the
Yellowstone repository and the
subscription protobuf
for the wire contract.
Store the endpoint, API key, and target wallet outside source control:
The following worker opens one stream and writes one named transaction filter. accountInclude
matches a transaction when any listed account is present. It does not prove that the wallet signed
the transaction or initiated a swap. That verification belongs in the decoder.
watch-wallet.ts
import Client, { CommitmentLevel } from "@triton-one/yellowstone-grpc";import bs58 from "bs58";const endpoint = required("YELLOWSTONE_ENDPOINT");const apiKey = required("YELLOWSTONE_API_KEY");const targetWallet = required("TARGET_WALLET");function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Missing ${name}`); return value;}const request
The promise chain is the simplest bounded behavior for a tutorial because it processes one update
at a time. A production system should use a queue with an explicit capacity, processing deadline,
and overflow policy. If the queue age rises, stop opening risk rather than acting on stale signals.
An included wallet may be a signer, fee payer, token-account authority, lookup-table account, or
merely an account referenced by an instruction. A safe decoder needs to prove the role you care
about and recognize the exact program instruction.
Define a narrow output contract:
decode.ts
type SwapIntent = { programId: string; inputMint: string; outputMint: string; inputAmount: bigint; minimumOutput: bigint | null;};function decodeTargetSwap(update: unknown, targetWallet: string): SwapIntent | null { // 1. Read static and address-lookup-table account keys. // 2. Verify targetWallet has the signer or authority role your policy requires. // 3. Find an instruction for one allowlisted swap program. // 4. Decode its discriminant and accounts with that program's current layout. // 5. Reject transfers, liquidity changes, unknown versions, and partial decodes. return null;}
Returning null is a normal outcome. Most transactions that mention a watched wallet should not
be mirrored. Maintain one decoder per program and test it against captured transactions, program
upgrades, versioned transactions, address lookup tables, and failed executions.
Never infer a swap only from token balance changes. Fees, wrapped SOL, account creation, routed
swaps, and unrelated transfers can produce similar balance patterns.
Paper trade this policy against recorded stream data before it can sign transactions. Copy-trading
does not remove strategy risk. It adds dependency on another wallet whose intent, inventory, and
risk tolerance you do not know.
An executor timeout is uncertain, not failed. The sender may have accepted the bytes before the
response was lost. Query the signature before rebuilding or resubmitting. If you cannot derive or
persist the mirror signature before submission, fix that contract first.
Test a wallet-filtered Yellowstone stream
Compare your current detection path with RPCEdge using the same wallet set, commitment, decoder, and measurement window.
Track the last observed slot and the disconnect interval. A resumed live stream does not prove you
received everything that happened during the gap. Depending on your provider and retention policy,
you may be able to request replay from a prior slot. Otherwise, query canonical RPC history for the
watched wallet and mark recovered transactions as late. Do not submit time-sensitive mirror trades
from a catch-up scan.
The official Yellowstone documentation also describes subscription pings for intermediaries that
close idle connections. Follow your provider's client and keepalive contract rather than inventing
application messages.
At processed commitment, a transaction has been observed by a node but is not final. Your ledger
needs separate states for detected, submitted, confirmed, finalized, failed on-chain, expired, and
unknown.
Use getSignatureStatuses to check batches of signatures. Fetch getTransaction when you need the
canonical transaction metadata for accounting or decoder verification. Solana documents both in
the official RPC reference:
The last valid block height returned with a recent blockhash gives the executor an explicit expiry
boundary. Do not label a transaction expired from wall-clock time alone.
Reconciliation should answer four independent questions:
Question
Evidence
Did the target transaction survive?
Target signature status at confirmed or finalized
Did the mirror reach the network?
Mirror signature status, not only sender acknowledgement
Did the mirror execute successfully?
Status error and transaction metadata
What position do we hold?
Canonical token accounts and balances after settlement
If the target disappears but the mirror lands, the bot owns an unmatched position. That is a risk
event with a predefined response, not an edge case to handle later.
Record gap, recover for accounting, do not copy stale trades
Unknown program version
Reject and alert
Wallet mentioned but not the required actor
Reject
Decoder succeeds but quote is stale
Reject before signing
Sender times out after accepting bytes
Mark uncertain and reconcile signature
Target drops while mirror lands
Trigger unmatched-position policy
Mirror lands but confirmation worker restarts
Resume from durable submitted state
Queue grows beyond the action deadline
Shed work and stop new exposure
RPC and stream disagree temporarily
Preserve both observations and wait for commitment policy
Add metrics for stream reconnects, last observed slot, queue age, decode rejection reasons,
candidate-to-submit count, uncertain submissions, confirmation duration, and unmatched positions.
For a broader measurement model, see
Observability for a Solana Trading Stack.
A Yellowstone subscription is the cleanest part of a copy-trading bot. The difficult work is proving
what the watched wallet did, deciding whether it fits your own risk, ensuring one signal can cause at
most one submission, and repairing state after uncertainty.
Start with one wallet, one allowlisted swap program, one strict decoder, and one durable state
machine. Use processed updates for detection only when your risk model allows it. Use confirmed or
finalized RPC evidence for reconciliation. That separation keeps the bot understandable when the
network, provider, program, or your own process behaves differently from the happy path.
=
{
accounts: {},
slots: {},
transactions: {
watchedWallet: {
vote: false,
failed: false,
signature: undefined,
accountInclude: [targetWallet],
accountExclude: [],
accountRequired: [],
},
},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: [],
commitment: CommitmentLevel.PROCESSED,
};
type Candidate = {
targetSignature: string;
observedSlot: bigint;
update: unknown;
};
async function subscribeOnce(onCandidate: (item: Candidate) => Promise<void>) {
const client = new Client(endpoint, apiKey, undefined);