Engine Room

How a Volume Engine Works, Component by Component

A volume engine is not a trading strategy with a nice interface. It is a queue, a pool of signing accounts and a transaction builder, wrapped in enough accounting to tell you what actually happened.

The Engine Room Desk 2386 words 11 min read Updated 13 August 2026

Component sheet

Component class
Whole-system overview
Inputs
A budget, a time window, a token and a venue mix
Outputs
Signed transactions, confirmed signatures, a run ledger
Hardest boundary
Sender to ledger, because ambiguity lives there

A volume engine on Solana is six components with five boundaries between them: a planner that turns a budget into intents, a wallet fleet that supplies signing accounts, a scheduler that decides release timing, a builder that converts an intent into a signed transaction, a sender that owns the network, and a ledger that records what really happened. Everything else is presentation.

That decomposition is not a style choice. The protocol forces it. A transaction has to be signed by a keypair, so something must hold and lease keys. It has to fit inside a fixed-size packet, so something must build it under a size constraint. It has to reference a recent blockhash that expires, so something must own timing. And its result is not always observable, so something must reconcile intent against evidence. Name the parts differently if you like, but you will find all six in any system that works.

Six responsibilities, not six features

The quickest way to tell a designed engine from an accreted one is to ask which component owns a given failure. In a well-drawn system, a rate limit is the sender's problem, an expired blockhash is the builder's problem, and an account with no balance is the fleet's problem. In an accreted one, all three are handled in the same retry loop and every incident report begins with an hour of reading logs to work out which layer actually broke.

ComponentOwnsHands offIts characteristic failure
PlannerBudget, venue mix, campaign shapeAn ordered list of intents with sizesProducing more value than the fleet can fund
Wallet fleetKeys, balances, per-account stateA leased account plus its current stateLeasing an account that is already in flight
SchedulerTiming, jitter, concurrency, pauseA release signal per intentEmitting a regular interval nobody asked for
BuilderRoute, instructions, fees, size limitOne serialised, signed transactionSilently exceeding the packet limit
SenderEndpoint credits, submission, resendA signature and an attempt recordRebuilding instead of resending
LedgerIntent, attempt, outcome, costRun state and every metricCounting submissions as successes

Read the last column as a checklist for any system you are evaluating, including your own. Five of those six failures are invisible from the outside: the run keeps producing transactions, the dashboard keeps incrementing, and only the reconciliation at the end reveals that a third of the value never moved or moved twice. That is why the ledger is a component rather than a logging concern.

What crosses each boundary

A component contract is easier to hold in your head than a diagram, because it names the data rather than the arrows. The following sketch is the interface, not an implementation, and it is deliberately free of any language that would let you run it.

planner.plan(budget, window, venues)  -> Intent[]
  Intent { id, venue, side, size, earliest, latest }

fleet.lease(intent)                   -> Lease | None
  Lease { account, balance, in_flight, released_at }

scheduler.release(Intent[], policy)   -> (Intent, Lease) at time t

builder.build(intent, lease, route)   -> SignedTx
  SignedTx { bytes, signature, blockhash, valid_until, cu_limit, cu_price }

sender.submit(SignedTx, deadline)     -> Attempt[]
  Attempt { at, endpoint, result, error_class }

ledger.record(Intent, Attempt[], Outcome)
  Outcome { landed, slot, fee_lamports, amount_in, amount_out }

Two details in that sketch carry most of the design. The first is that SignedTx includes valid_until, which means the deadline travels with the transaction instead of being recomputed by whoever happens to be retrying it. The second is that sender.submit returns a list of attempts rather than a boolean. A single success flag throws away the only evidence that would let you explain a duplicate later.

Notice also what the fleet returns. A lease is not just an account; it carries the balance the fleet believed the account had and the number of transactions it thinks are in flight. That belief is frequently wrong, and making it explicit is what allows the ledger to detect the disagreement instead of inheriting it.

Why the queue is the interesting part

People expect the clever engineering in a trading system to sit in the swap. It does not. Building a swap instruction against a public automated market maker is a solved, documented problem, and any competent implementation produces roughly the same bytes. The difficulty is that you want to do it several hundred times, from different accounts, without any two attempts colliding, without exceeding a rate limit, and without losing track of a single one.

That is a queueing problem with a settlement problem attached. The queue decides how many units of work are in flight at once, which directly sets both your throughput and your exposure to a network slowdown. Too shallow and the engine idles while confirmations trickle back. Too deep and a congestion event turns into a wall of expired blockhashes, every one of which has to be resolved before you know what you spent.

The settlement half is what makes the queue unusual. In a normal task queue, a worker that dies can safely retry its task. Here, a worker that dies may have already broadcast a transaction that will land thirty seconds later, and the only safe way to retry is to resend the exact same signed bytes so that the network deduplicates the work for you. That constraint reaches back up into the builder, which must therefore be deterministic and must persist what it produced before the sender touches the network.

Engines, bundlers and snipers are not the same tool

Three tool classes get grouped under one label, and the confusion produces bad architecture decisions. A sniper optimises for latency on a single event: it is a fast path from a trigger to one transaction, and almost all of its engineering goes into landing that transaction before someone else's. A bundler optimises for atomicity: it groups several transactions so they execute together or not at all, which matters when a partial result is worse than no result.

A volume engine optimises for neither. It optimises for sustained, paced throughput over a window, and the metric it is judged on is cost per successful swap rather than milliseconds to landing. That difference cascades: a sniper can justify aggressive priority fees on every attempt because it only sends a handful, while an engine paying the same fee across hundreds of swaps is simply burning its budget on the fee line. If you are trying to work out which class of tool a given problem calls for, the comparison of volume bot vs bundler is the right question to settle before you start writing anything, because the two classes want opposite things from the scheduler.

Trade-off: latency versus cost

Every millisecond you buy at the pipeline level is bought with priority fees, redundant submission, or both. On a single transaction that is cheap and obviously worth it. Across a sustained run, the same setting multiplies by attempt count and becomes the largest controllable line in the budget.

The corollary is that an engine borrowing a sniper's defaults will land beautifully and cost far more per successful swap than it needs to. Copying settings across tool classes is one of the most common expensive mistakes in this space.

A worked sizing example

The following arithmetic is illustrative. It uses numbers you supply rather than any measured result, and its purpose is to show which quantity constrains which component, not to predict an outcome.

Suppose you plan a run that should move 60 SOL of notional volume over four hours, in swaps of 0.15 SOL. That is 400 swaps, or roughly 100 per hour, or one every 36 seconds on average. Nothing about that rate is demanding for the network. It becomes demanding once you add the constraints each component brings.

  1. Confirmation time sets concurrency. If a swap takes on average 8 seconds from submission to a confirmed result, and you want one release every 36 seconds, a single account is theoretically enough. But averages hide the tail: during a congestion window, the same swap may take 40 seconds or expire entirely. Sizing for the average produces an engine that stalls exactly when the network is busiest.
  2. Tail behaviour sets fleet size. If you want the run to keep its pace while up to five swaps are stuck in a slow window, you need at least six accounts able to sign independently, plus spares for accounts you decide to retire mid-run. The fleet is sized by the tail, not the mean.
  3. Attempts, not swaps, set endpoint load. Four hundred swaps with an average of two submission attempts each, plus a confirmation poll every two seconds for the lifetime of each attempt, is a very different request volume from four hundred calls. Sizing the endpoint budget from the swap count is how engines discover rate limits at hour three.
  4. Fees scale with attempts too. The base fee is charged per signature on a landed transaction, and priority fees are charged on the compute you requested. An attempt that expires costs nothing on chain, but an attempt that lands twice costs twice, which is another reason the resend path matters more than it looks.

Work the same exercise with your own numbers before writing any code and you will discover the actual constraint early. In most designs it is not compute and it is not wallet count; it is the request budget of whatever endpoint you are pointing at.

Where state is allowed to live

Distributed state is where these systems rot. The rule that keeps an engine reconcilable is simple to state and easy to violate: the fleet owns account state, the ledger owns run truth, and nothing else is allowed to persist anything. Every screen, every metric, every alert is a projection of the ledger.

The violation always looks reasonable at the time. The scheduler keeps a counter so it can display progress. The sender caches a balance so it can skip a lookup. Six weeks later the progress bar disagrees with the ledger, and there is no principled way to decide which is right, because both were written by code that believed itself. A single writer per fact costs a little performance and saves the entire class of "which number is real" incidents.

There is a real trade-off here and it is worth naming. A strict single-writer design means more reads from the ledger on the hot path, and at high throughput that becomes a bottleneck you have to engineer around with caching that is explicitly marked as a cache. The alternative, letting each component keep its own view, is faster on day one and unauditable by month three. For an engine whose entire product is an honest record of what happened, that is not a close call.

Failure modes by component

Use this as a review checklist against a design document or an existing system. Each item is a question that has a yes or no answer; anything that cannot be answered is the thing to work on next.

  • Can the planner produce a programme the fleet cannot fund, and is that caught before the first release rather than at swap two hundred?
  • Can the fleet lease the same account twice while a transaction from the first lease is still in flight?
  • Does the scheduler have a pause that stops new releases without cancelling in-flight work, and does anything actually call it?
  • Does the builder verify the serialised size against the packet limit, or does it discover the limit through failures?
  • Does the sender resend identical bytes, or does it call the builder again and produce a second distinct transaction?
  • Does the ledger distinguish submitted, expired, landed and unknown, or does it collapse them into success and failure?
  • If the process is killed mid-run, can the engine reconstruct what was in flight from persisted state alone?
  • Is there any number shown to an operator that is not derived from the ledger?

The last two are the ones that separate a demo from something you would leave running. Crash recovery is not a feature you add later, because it constrains what the builder must persist and when, and retrofitting it means rewriting the hot path.

Build it or run one

Once the component model is clear, the build-or-run decision becomes a question about which problems you want to own rather than a question about capability. Building means you own the ledger, the pacing model and the keys, and you own the several weeks it takes to make retries genuinely idempotent, to survive an endpoint that starts refusing you, and to reconcile a run that was interrupted. None of that is exotic work, but none of it is quick, and it is all invisible until it fails.

Running a hosted engine moves those problems rather than removing them. You no longer own the queue, which also means you cannot inspect it; you inherit someone else's endpoint budget, retry policy and definition of a completed swap. That is a reasonable trade when the alternative is a first implementation with an optimistic retry loop, and a bad trade if you need the run record to be yours. A hosted Solana volume bot is best evaluated on exactly the boundaries described above: what it tells you about attempts rather than successes, whether it reports signatures you can verify yourself, and what it does when the network makes a result ambiguous.

What this model deliberately leaves out

Three things are missing from the six-component picture, and each is missing on purpose. There is no strategy layer, because a volume engine executes a programme rather than forming a view; if you add signal generation, you have built a different system with different correctness requirements. There is no custody layer, because key handling deserves its own treatment and folding it into the fleet component hides the most consequential decisions behind an interface.

And there is no network transport detail. Whether you submit through a standard endpoint, a staked connection or a bundle relay changes the sender's implementation and its cost profile, but it does not change the contract the sender owes the ledger. Keeping that boundary clean is what lets you swap transport later without rewriting the accounting, which is the single most valuable property this architecture buys you.

Read the pipeline note next if you want the inside of the builder and the sender, or the fleet note if the account model is what you are designing right now. Both assume the vocabulary set out here.

Questions this note gets asked

What does a volume bot on Solana actually do?

It executes many small swaps against a token across one or more venues, on a schedule, from a pool of wallets it controls, and records the result of each one. Every part of that sentence is a component: the schedule is a scheduler, the pool is a wallet fleet, the swap is a transaction builder plus a sender, and the record is a ledger. There is no additional magic layer underneath.

Is a volume engine the same thing as a market maker?

No. A market maker quotes both sides and manages inventory risk continuously, usually on an order book, and its profit comes from spread. A volume engine executes a bounded programme of swaps against an automated market maker to produce trade activity, and the value it moves is a cost rather than a position it intends to hold. The engineering overlaps in execution; the objective does not.

Why is the confirmation step harder than the submission step?

Submission has two outcomes you can see immediately, accepted or rejected by the endpoint. Confirmation has three: landed, expired, and unknown. Unknown is the expensive one, because a transaction whose result you never observed may still have executed, and any engine that treats unknown as failure will eventually send the same value twice.

How many wallets does an engine need?

That depends entirely on the pacing model and the venue, and any specific number quoted without those two inputs is decoration. What the architecture fixes is the shape of the answer: enough accounts that no single account is asked to sign faster than its confirmations return, plus enough headroom that removing a stuck account does not stall the run.

Does an engine need its own RPC infrastructure?

Not to work, but usually to work at any size. The sender and the confirmation tracker are the two heaviest consumers of endpoint credits, and a shared public endpoint will rate limit them long before compute or wallet count becomes the constraint. Whether you pay for capacity or design around scarcity is one of the earliest cost decisions in the build.

Is any of this specific to one product?

No. The six components come from what the protocol requires: something must hold keys, something must build a transaction that fits in a packet, something must decide when to send, and something must reconcile results. Any working system contains those responsibilities under some set of names, whether it is a script or a hosted service.

Filed under Architecture by The Engine Room Desk. Arithmetic on this page is labelled illustrative and built from protocol constants or values you supply yourself. How the desk sources and corrects a note is set out in the editorial policy.