The job is state maintenance, not fast reads alone
A DLMM automation loop may rebalance a position, monitor a price range, classify swaps, or prepare some other bounded action. Whatever the strategy, the infrastructure has the same responsibilities:
- Discover the pool and load a coherent starting snapshot.
- Observe the accounts that can change the decision.
- Apply updates in slot order and reject duplicates.
- Detect disconnects, dead slots, and local state gaps.
- Reconcile the local view against an authoritative RPC read.
- Build, simulate, sign, submit, and verify an action under explicit limits.
The strategy decides whether to act. The infrastructure decides whether the state is trustworthy enough to permit that action.
A low-latency stream is useful only when the bot can prove what it has seen, what it may have missed, and which snapshot its next transaction is based on.
This guide uses the official Meteora DLMM TypeScript SDK for pool access and the official Yellowstone gRPC interface for streaming concepts. Read those projects as the source of truth for current package versions, protobuf fields, and program behavior.
A practical two-path architecture
Do not force one interface to do every job.
| Path | Use it for | Do not assume |
|---|---|---|
| JSON-RPC plus Meteora SDK | Pool discovery, startup snapshot, quotes, transaction construction, simulation, reconciliation | Repeated polling will provide a stable low-latency event feed |
| Yellowstone gRPC | Account writes, relevant transactions, slots, and live incremental updates | Every connection has infinite retention or can replace authoritative state |
| Transaction sender | Submission and route policy for a signed transaction | Acceptance means the transaction landed |
| Signature and state reconciliation | Confirming outcome and rebuilding local truth | A successful simulation guarantees inclusion |
The shape is:
RPC bootstrap -> local pool model
|
Yellowstone updates -> reducer -> strategy gate -> build and simulate -> sign -> send
| | |
+-> gap detector +-> reject +-> verify
|
periodic RPC snapshot -----+-> reconcile or pauseFor a broader comparison of polling, WebSockets, gRPC, and preprocessed signals, see Which Solana Stream Do You Actually Need?.
Step 1: bootstrap with the official Meteora SDK
Install the SDK and its Solana dependencies using the versions listed in the official repository:
npm install @meteora-ag/dlmm @solana/web3.jsCreate the pool client from a known LB pair address, then read the initial pool state. Keep the RPC endpoint and the gRPC endpoint in the same region when possible, so the snapshot and live stream are less likely to describe materially different tips of the chain.
import DLMM from "@meteora-ag/dlmm";
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection(process.env.SOLANA_RPC_URL!, {
commitment: "confirmed",
wsEndpoint: process.env.SOLANA_WS_URL,
});
const lbPair = new PublicKey(process.env.METEORA_LB_PAIR!);
const pool = await DLMM.create(connection, lbPair);
const activeBin = await pool.getActiveBin();
const bootstrap = {
lbPair: lbPair.toBase58(),
activeBinId: activeBin.binId,
price: activeBin.price,
observedAtMs: Date.now(),
};
console.log(bootstrap);This is a bootstrap, not a permanent cache. Save the slot associated with the snapshot if your RPC method exposes it. Your stream consumer needs a boundary that answers a basic question: which updates happened after the state I loaded?
At startup, also resolve the bin array accounts that cover the range your system cares about. DLMM distributes liquidity across discrete bins, so watching only the pair account may tell you that the active bin moved without giving you every liquidity value needed for a decision. Derive the relevant bin arrays from current pool state and the strategy range, then update that watch set when the active range moves.
Step 2: subscribe to the smallest useful account set
Yellowstone supports account filters by public key, owner, data size, and memory comparison. It also
supports transaction filters using account_include, account_exclude, and account_required.
The exact semantics are documented in the
Yellowstone repository.
For a DLMM state loop, begin with explicit account keys:
- the LB pair account;
- the bin array accounts around the active range;
- position accounts owned by the bot, if position state is part of the decision;
- token accounts needed to enforce inventory limits.
Avoid subscribing to the entire program unless you genuinely need every pool. A broad subscription increases bandwidth, decoding work, memory pressure, and the chance that useful updates wait behind irrelevant ones in your own process.
The request below uses the official Yellowstone client shape. watchedAccounts should come from the
Meteora bootstrap and range selection step, not from a hardcoded production list.
import Client, {
CommitmentLevel,
SubscribeRequest,
} from "@triton-one/yellowstone-grpc";
const client = new Client(
process.env.SOLANA_GRPC_URL!,
process.env.SOLANA_GRPC_TOKEN!,
{ "grpc.max_receive_message_length": 64 * 1024 * 1024 },
);
const stream = await client.subscribe();
const request: SubscribeRequest = {
accounts: {
dlmmState: {
account: watchedAccounts,
owner: [],
filters: [],
},
},
slots: {
slotStatus: { filterByCommitment: false },
},
transactions: {},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: [],
commitment: CommitmentLevel.PROCESSED,
ping: undefined,
fromSlot: undefined,
};
stream.write(request);The official protobuf uses snake case on the wire while generated TypeScript clients commonly use camel case. Confirm field names against the installed client version before deploying.
When transaction subscriptions help
Account writes are the right default when the action depends on current pool state. Add a filtered transaction stream when you need to classify swaps, attribute state changes, or measure which instructions preceded a move.
Use the Meteora program ID in account_include, then narrow the result in your process to the LB pair
and instruction variants you support. Set vote and failed explicitly. A transaction stream adds
context, but it should not silently become the source of truth for balances or current bin state.
Step 3: reduce updates into an ordered local model
Do not call the strategy directly from the gRPC event handler. The handler should validate, record, and reduce each update into a local model. The strategy reads only a model marked healthy.
type AccountUpdate = {
pubkey: string;
slot: bigint;
writeVersion: bigint;
data: Uint8Array;
};
type Version = { slot: bigint; writeVersion: bigint };
const versions = new Map<string, Version>();
function shouldApply(update: AccountUpdate): boolean {
const previous = versions.get(update.pubkey);
if (!previous) return true;
if (update.slot > previous.slot) return true;
return update.slot === previous.slot &&
update.writeVersion > previous.writeVersion;
}
function apply(update: AccountUpdate) {
if (!shouldApply(update)) return;
const decoded = decodeKnownDlmmAccount(update.pubkey, update.data);
localPoolState.apply(decoded, update.slot);
versions.set(update.pubkey, {
slot: update.slot,
writeVersion: update.writeVersion,
});
}The pair of account key, slot, and write version gives the reducer an idempotency boundary. A replay or reconnect may deliver data the process has already seen. Dropping an exact duplicate is correct. Dropping a newer write because it shares a slot is not.
Keep the raw account bytes or a hash long enough to diagnose a bad decode. Parser failures should quarantine the affected pool and alert an operator. They should never fall through to a default price or empty liquidity value.
Step 4: handle disconnects, replay, and forks
A production stream will disconnect eventually. Plan the recovery path before enabling execution.
Yellowstone includes a ping field for keeping long-lived subscriptions active behind load
balancers. Its current protobuf also includes from_slot for replay where the server supports a
retained range. Neither feature removes the need for a fresh state check.
Use this recovery sequence:
- Mark the local model unhealthy as soon as the stream closes or stops advancing.
- Disable new execution, but continue verifying already submitted signatures.
- Reconnect with bounded exponential backoff and jitter.
- Request replay from the last safely applied slot when supported.
- Deduplicate replayed account writes in the reducer.
- Fetch a fresh SDK or RPC snapshot for the pair and relevant bin arrays.
- Compare the snapshot with the reduced state.
- Re-enable execution only after the watch set and state agree.
Slot notifications also expose status changes. A slot observed at processed can later be marked
dead. Keep processed data useful for responsiveness, but reconcile consequential decisions at the
commitment level your risk policy requires. The distinctions are covered in
Solana Commitment Levels: Processed vs Confirmed vs Finalized.
Step 5: reconcile even when the stream looks healthy
Silence is not proof of correctness. Schedule a periodic RPC snapshot and compare at least:
- active bin ID;
- pool parameters used by quoting or range logic;
- balances and position state;
- watched bin array membership;
- the most recent processed and confirmed slots;
- the last verified transaction outcome.
If a mismatch changes the next action, pause and rebuild the model. Do not patch one field and assume the rest of the snapshot is coherent.
A simple health gate can make the requirement explicit:
type Health = {
streamConnected: boolean;
gapDetected: boolean;
lastUpdateAgeMs: number;
rpcReconciledAtMs: number;
watchedRangeCurrent: boolean;
};
function mayExecute(health: Health, nowMs: number): boolean {
const snapshotAgeMs = nowMs - health.rpcReconciledAtMs;
return health.streamConnected &&
!health.gapDetected &&
health.lastUpdateAgeMs < 2_000 &&
snapshotAgeMs < 15_000 &&
health.watchedRangeCurrent;
}The values above are examples, not universal defaults. Choose thresholds from the cadence and failure budget of your application, then test them during deliberate disconnects and delayed RPC responses.
Need RPC, Yellowstone gRPC, and a sender for one Solana bot?
rpc edge provides the read, streaming, and transaction submission paths. Bring the pool, region, expected filters, and workload so the plan can be sized around the actual control loop.
Step 6: build and submit with explicit safety limits
The official Meteora SDK can construct pool instructions and transactions. Keep construction behind a policy boundary. Before signing, check:
- the pool and token mints match the configured allowlist;
- the quote or active bin is no older than the strategy permits;
- inventory after the action remains inside a hard limit;
- minimum received, maximum spent, and price impact bounds are explicit;
- the recent blockhash and fee policy are fresh;
- simulation succeeds against an appropriate commitment;
- only the intended instructions and accounts are present.
Sign locally. Never put a private key in a stream filter, URL, log line, or support request.
After submission, distinguish these states:
accepted by sender != received by leader
received by leader != executed
executed != confirmed
confirmed != finalizedAn accepted response means the sender accepted the bytes and began its configured route policy. It does not prove inclusion. Track the signature, inspect the execution error, refresh affected accounts, and make retries idempotent. See Why Solana Transactions Fail and Landing Transactions on Solana for the full send and verification path.
Do not retry a state-dependent DLMM instruction blindly. The active bin, balances, blockhash, and valid price boundary may have changed since the first attempt. A retry should return to the policy gate and rebuild from fresh state.
What to measure in production
Measure the complete loop instead of one endpoint round trip:
| Stage | Useful metric |
|---|---|
| Stream | update age, disconnect count, reconnect duration, replayed updates |
| Reducer | duplicate count, out-of-order count, decode failures, watched range changes |
| Reconciliation | snapshot mismatch count, time spent unhealthy, processed to confirmed drift |
| Decision | decisions evaluated, policy rejections, stale-state rejections |
| Submission | accepted count, landed count, execution errors, expiry, confirmation time |
| Outcome | inventory drift, realized fees and costs, slippage, strategy-specific risk measures |
Keep provider latency separate from local decode, decision, signing, network, and confirmation time. Otherwise an optimization in one stage can hide a regression in another. A fuller telemetry model is available in Observability for a Solana Trading Stack.
A safe implementation order
Build the system in stages:
- Bootstrap one pool and print a verified active bin.
- Stream the pair and relevant bin arrays with execution disabled.
- Persist versions and prove duplicate handling with replayed fixtures.
- Disconnect the stream deliberately and verify that the health gate closes.
- Add periodic RPC reconciliation and dead-slot handling.
- Build and simulate transactions without signing or sending.
- Add local signing and a strict allowlist.
- Submit small bounded actions and verify every signature.
- Expand the pool set only after per-pool state and metrics are isolated.
This order makes unsafe ambiguity visible early. It also separates a data correctness problem from a strategy problem and a sender problem.
Bottom line
A Meteora DLMM bot is a stateful Solana application. Bootstrap through RPC and the official Meteora SDK. Stream the smallest relevant account set through Yellowstone gRPC. Reduce updates idempotently, detect gaps, reconcile on a schedule, and close the execution gate whenever state is uncertain. Then build and simulate against fresh state, sign locally, submit through a measured path, and verify the result independently.
That architecture does not promise a profitable strategy. It gives the strategy a controlled, observable, and recoverable way to interact with a live DLMM pool.