A wallet monitor stops being simple when one transaction can match several customers, one customer can own several addresses, and a reconnect can replay an event already processed.
The answer is not one polling loop per address. Yellowstone gRPC carries the live path; resolve each transaction completely, store one canonical event, and attach every relevant wallet through a separate match record.
wallet registry
-> filter shards
-> Yellowstone streams
-> bounded queues
-> transaction decoder
-> canonical transaction + wallet-match edges
-> notifications, balances, or strategy signals
^
|
bounded reconciliationDefine what a wallet match means
"Watch this wallet" is not a complete requirement. A transaction can involve an address as:
- Fee payer.
- Transaction signer.
- Writable account.
- Read-only account.
- Token-account owner discovered from state.
- Authority referenced by an instruction.
- Address loaded through an address lookup table.
Decide which roles the product exposes. A copy-trading product may care about signers and decoded swaps. A payment product may care about token-account owners and settlement. A risk monitor may care about any mention but label the role precisely.
Yellowstone's transaction filter can narrow traffic by accounts. In the upstream
SubscribeRequestFilterTransactions,
account_include, account_exclude, and account_required are separate fields. Inclusion is a
transport filter. It is not proof that the included address signed or initiated the transaction.
Shard filters by measured load
Providers may cap accounts per filter, filters per request, streams per connection, message size, or replay depth. Do not embed one vendor's current limit into the architecture.
Start with a configurable shard size and keep stable wallet assignment. Stable shards make reconnect and incident traces easier to understand.
export type WalletShard = {
id: string;
wallets: readonly string[];
};
export function buildWalletShards(
wallets: readonly string[],
maxWalletsPerShard: number,
): WalletShard[] {
if (!Number.isInteger(maxWalletsPerShard) || maxWalletsPerShard < 1) {
throw new Error("maxWalletsPerShard must be a positive integer");
}
const unique = [...new Set(wallets)].sort();
const shards: WalletShard[] = [];
for (let start = 0; start < unique.length; start += maxWalletsPerShard) {
const members = unique.slice(start, start + maxWalletsPerShard);
shards.push({ id: `wallets-${start / maxWalletsPerShard}`, wallets: members });
}
return shards;
}Sorted fixed-size chunks are sufficient for an initial deployment. High-volume wallets can later move to dedicated shards, but only after telemetry shows that one address dominates bytes or queue work.
For each shard, track:
- Connected state and reconnect generation.
- Last update receive time.
- Highest observed slot by commitment.
- Updates and bytes per second.
- Queue depth and age of the oldest item.
- Decode errors, duplicate observations, and dropped work.
- Wallet count and the configuration version that produced the filter.
Wallet count alone is a weak capacity signal. Five hundred dormant addresses may be cheaper than a small group of active program or routing accounts.
Subscribe once, classify after receipt
One filter entry can carry the shard's wallet list:
import { CommitmentLevel, type SubscribeRequest } from "@triton-one/yellowstone-grpc";
import type { WalletShard } from "./wallet-shards";
export function subscriptionFor(shard: WalletShard): SubscribeRequest {
return {
accounts: {},
slots: {},
transactions: {
[shard.id]: {
vote: false,
failed: false,
signature: undefined,
accountInclude: [...shard.wallets],
accountExclude: [],
accountRequired: [],
},
},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: [],
commitment: CommitmentLevel.PROCESSED,
ping: undefined,
};
}The upstream Yellowstone gRPC README documents the subscription client, filter model, pings, and replay-related interfaces. Confirm the generated client version and the provider's supported schema before deploying copied code.
processed gives the live path an early view. It also means the application needs a policy for
commitment upgrades and fork reconciliation. Products that can wait for confirmed or finalized
state should choose that commitment deliberately instead of inheriting the example.
Resolve the complete account list
Versioned Solana transactions can load addresses from lookup tables. Matching only static message keys can miss a monitored wallet or assign the wrong index to an instruction.
Build the effective key list in runtime order:
static message keys
+ loaded writable addresses
+ loaded read-only addressesThen derive roles from the message header and instruction account indices. Do not label every included wallet as a signer. The first static account is generally the fee payer, while the message header defines how many leading static keys are required signers. Loaded addresses are not transaction signers.
For token transfers, the token account in the transaction may differ from the customer wallet stored in your registry. Resolve and cache token-account ownership from account state, invalidate the cache when ownership or account lifecycle changes, and retain the evidence used for the match.
Store transactions once and wallet matches separately
A swap between two monitored wallets should create one transaction and two wallet relationships, not two copies of the transaction.
Model two durable records:
type CanonicalTransaction = {
signature: string;
slot: bigint;
commitment: "processed" | "confirmed" | "finalized";
transactionBytes: Uint8Array;
executionError: string | null;
};
type WalletMatch = {
signature: string;
wallet: string;
role: "fee_payer" | "signer" | "writable" | "readonly" | "token_owner";
sourceShard: string;
};Make the canonical transaction key the signature. Make the match key at least
(signature, wallet, role). Instruction-level events should also include the outer and
inner instruction positions in that event key.
Upserts should allow a later observation to strengthen commitment without repeating downstream actions. Put notification or strategy execution behind its own idempotency key.
Bound the queue before it becomes an outage
The stream reader should not perform database writes, remote metadata requests, and webhooks inside the receive callback. Decode the envelope, add it to a bounded queue, and return to reading.
Backpressure policy must be explicit:
- Alert on queue age before memory pressure becomes critical.
- Slow or pause optional enrichments first.
- Preserve the raw observation or a recoverable cursor when possible.
- Reconnect only when the client or transport requires it, not as a queue-clearing strategy.
- Mark the uncertain slot interval for reconciliation if any update may have been lost.
Scaling consumers only helps when work can be partitioned without violating order. Route the same signature to one partition so duplicate observations serialize against the same idempotency key.
Our Yellowstone bandwidth guide shows how update size and rate turn into network cost. Record bytes after transport decoding as well as the size of stored and enriched records. All three affect the capacity plan.
Reconnect by shard and reconcile the uncertain interval
A reconnect creates a new observation generation. It does not create a new business event.
Persist per-shard state with the last observed slot, last processed signature, reconnect generation, disconnect time, and configuration version. Request replay from the earliest uncertain slot where the provider supports it. Deduplicate everything the replay returns.
RPC remains useful for repair. Solana's official
getSignaturesForAddress returns confirmed
signatures for an address in reverse chronological order. Apply it to a bounded wallet and time or
slot interval, then fetch and compare the missing transactions.
Solana's payment indexing guide describes address-signature lookup followed by transaction retrieval for indexing payment activity. That is a reasonable repair primitive. Running it continuously for every wallet turns the live architecture back into a polling fleet.
Reconciliation should produce an auditable result:
- Expected signatures in the interval.
- Signatures already stored.
- Missing records recovered.
- Stored records invalidated or updated after stronger commitment.
- Duplicate side effects prevented.
- Any interval that remains unresolved.
Our production reconnect and replay guide covers the transport loop in more detail.
Protect the wallet registry
Solana addresses are public. The mapping between an address, a customer, notification preferences, and internal risk labels is not.
Provider filter logs should omit customer identifiers. Restrict registry access, encrypt secrets, and apply retention limits to raw payloads and enriched activity. Customer removal of an address must propagate the configuration change to streams, caches, backfill jobs, and notification routes.
Version the registry. Every stream observation should be attributable to the shard configuration that admitted it, especially when a wallet moves between shards during a rebalance.
Use the Wallet Stream Capacity Sheet
Fill one row per shard under a production-shaped load:
| Measure | Observed value | Acceptance limit | Action when exceeded |
|---|---|---|---|
| Wallets and active wallets | Split or isolate a hot wallet | ||
| Updates per second, p50 and peak | Add consumer capacity or reduce scope | ||
| Inbound bytes per second | Review filters, region, and plan | ||
| Oldest queue item age | Pause enrichment or add workers | ||
| Decode error rate | Quarantine payload and page owner | ||
| Duplicate observation rate | Inspect reconnect or overlapping shards | ||
| Stream-to-store delay | Find transport, queue, or database bottleneck | ||
| Reconciliation duration | Reduce interval or increase repair capacity | ||
| Missing records after repair | Keep shard degraded and investigate |
Run the sheet at average and burst traffic. Repeat after adding a new wallet cohort, changing a filter, moving regions, or altering decoded payloads. The right shard size is the one that stays inside the application's budgets with room for reconnect and repair.
To test this design against RPCEdge Yellowstone streams, choose an RPCEdge plan and bring a representative wallet set, expected activity mix, and acceptance sheet.