Quote Updates for Solana Market Makers

Why cancel-replace is the expensive part, how stale quotes get picked off, and what to measure when your fills are worse than your model says.

BoltTx Team··10 min read
solanamarket-makingquotestransaction-landinglatencytrading-bot

Market making on Solana is not primarily a pricing problem. Most teams have a model that works. What costs them money is the gap between deciding a quote is wrong and having the replacement on chain.

During that gap your old quote is still live, still executable, and now mispriced. Someone will take it.

Where the Money Leaks

The sequence for every quote update:

1. price moves
2. ★you detect it★
3. you decide
4. ★you submit cancel + replace★
5. ★it lands★
6. new quote is live

★Steps 2, 4, and 5 are all latency, and only step 2 gets attention.★ Teams buy faster data feeds and leave submission on a general-purpose path, then wonder why adverse selection did not improve.

The arithmetic is unforgiving. If detection takes a fraction of a slot and landing takes three slots, the overwhelming majority of your exposure window is on the submission side. Halving detection barely moves the total.

Measure the Window, Not the Parts

The number that matters is the full exposure window, and it is easy to instrument:

const priceMovedAtSlot = detectedSlot;

const sig = await sendReplacement(newQuote);
const status = await waitForLanding(sig);

log({
  detectLag: detectedSlot - eventSlot,      // your data feed
  decideLag: submitSlot - detectedSlot,     // your logic
  landLag: status.slot - submitSlot,        // ★your submission path★
  exposureSlots: status.slot - eventSlot,   // ★what actually costs you★
});

★exposureSlots is the number to optimise.★ It is also the one that maps directly to losses: the longer your stale quote is live, the more of your flow is someone taking the wrong side.

Track it as a distribution. A market maker whose median exposure is two slots and whose tail is fifteen is being picked off in the tail, and the median tells them everything is fine.

Cancel-Replace Has an Ordering Problem

The obvious implementation has a subtle bug:

// ★Do not assume these execute in this order.★
await send(cancelTx);
await send(placeTx);

Separately submitted Solana transactions have no guaranteed ordering. The place can land before the cancel. For a moment you have both quotes live — the stale one and the new one — which is worse than having only the stale one.

★Combine them into one transaction whenever the instructions fit.★

const tx = new Transaction().add(
  cancelOrderIx(oldOrderId),
  placeOrderIx(newQuote),
);
// ★One transaction: atomic, ordered, one signature, one fee.★

This is the single highest-value change for most market-making bots, and it is a code shape rather than an infrastructure purchase. When they do not fit — many orders across many markets — batch what you can and accept that separate transactions need their own reasoning about overlap.

If your quote logic is right and the exposure window is still wide, a free BoltTx key is one line to test the submission half.

Update Frequency Is a Cost Decision

Every update costs a base fee plus whatever priority fee you attach. Quoting a hundred markets and updating each frequently is a meaningful daily expense.

// ★Update on threshold, not on every tick.★
const drift = Math.abs(fairValue - quotedValue) / quotedValue;
if (drift < updateThreshold) return;   // still inside tolerance

★Setting updateThreshold is the central tradeoff of the whole strategy.★

Too tight — you update constantly, pay fees constantly, and spend most of your time with transactions in flight.

Too loose — your quotes are stale more often, and stale quotes are exactly what informed flow is looking for.

The threshold that is correct depends on your exposure window. ★If your updates land in two slots, you can afford a tighter threshold than if they land in eight★, because each update costs you less time exposed. This is the mechanism by which submission speed changes your strategy rather than just your metrics.

Adverse Selection Is Measurable

The diagnostic that tells you whether stale quotes are the problem:

// For each fill, mark the price shortly afterwards.
const markout = (priceAfter - fillPrice) * side;

// ★Bucket by how long the quote had been live.★
metrics.markoutByQuoteAge.observe(quoteAgeSlots, markout);

★If markout gets systematically worse as quote age increases, you are being picked off, and the fix is a shorter exposure window rather than a wider spread.★

Widening the spread is the reflex, and it treats the symptom. It reduces your loss per pick-off while also reducing your fill rate on the flow you actually want. A shorter window improves both.

Fees Under Contention

Market makers write to the same accounts repeatedly — your own order accounts, the market's book — so getRecentPrioritizationFees on those specific accounts is the right input rather than a network-wide figure:

const fees = await connection.getRecentPrioritizationFees({
  lockedWritableAccounts: [marketAccount, ...yourOrderAccounts],
});

★A network-wide average fee is the wrong input.★ A quiet market where you are the only quoter needs almost nothing. A market in a volatility event where every maker is repricing at once is a different situation entirely, and it is the situation where landing late is most expensive.

Cancels deserve their own fee policy. A cancel that fails to land leaves a stale quote executable. ★In a fast move, landing the cancel matters more than landing the replacement★ — pulling a bad quote is defensive, placing a new one is optional.

Compute and Size Constraints

Batched order operations run into the same two ceilings as any complex transaction:

1232 bytes. Each order references accounts at 32 bytes per key. Cancel-replace across many orders adds up quickly, and address lookup tables are worth it here because your account set is stable and reusable.

Compute units. Each cancel and place costs compute. Set the limit from simulation of the actual batch shape you send, since a batch of eight is not eight times the cost of one.

const sim = await connection.simulateTransaction(tx, {
  replaceRecentBlockhash: true,
  sigVerify: false,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);

★Market makers are the one group who should build lookup tables early★, because unlike launch snipers, they know their accounts in advance and reuse them thousands of times.

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

★For a market maker that number is an exposure window.★ Every millisecond of it is time your previous quote was live at a price you had already decided was wrong.

Where BoltTx Fits

We handle the hop after your signing. Not pricing, not order management.

Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a cancel is not observable in transit before it lands — which matters when the alternative is broadcasting your intent to pull a quote to anyone who wants to take it first.

You sign locally. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.

Get a free API key. No monthly fee:

const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

How fast should a Solana market maker update quotes? Fast enough that your exposure window is shorter than the time it takes informed flow to react. Measure slots from price move to replacement landing, and treat that window rather than update frequency as the target.

Should cancel and replace be one transaction or two? One, whenever the instructions fit. Separately submitted transactions have no guaranteed ordering, so a place can land before its cancel and leave both quotes live at once.

Why are my fills worse than my model predicts? Usually adverse selection from stale quotes. Bucket your markout by how long the quote had been live — if it degrades with quote age, the problem is your exposure window, not your pricing.

Should I widen my spread if I am being picked off? It reduces the loss per pick-off while also reducing fills on the flow you want. Shortening the exposure window improves both, so it is worth exhausting that first.

How do I measure adverse selection on Solana? Record the price shortly after each fill and compute markout signed by side. Bucketed by quote age in slots, it separates stale-quote losses from ordinary pricing error.

What priority fee should a market maker pay? One derived from recent fees on the market and order accounts you write to. A network-wide average misses that your contention is specific to the market you quote.

Should cancels have a higher fee than placements? Often yes. A cancel that does not land leaves an executable stale quote, while a placement that does not land only means you are not quoting. Pulling is defensive, placing is optional.

How do I batch quote updates across markets? Combine what fits within 1232 bytes and use address lookup tables to compress account references. Anything requiring ordering must be in the same transaction, since separate ones have no ordering guarantee.

Do market makers benefit from address lookup tables? More than most. Your account set is stable and reused thousands of times, so the setup cost amortises immediately — unlike launch sniping, where the accounts do not exist beforehand.

What update threshold should I use? It depends on your exposure window. Landing in two slots supports a tighter threshold than landing in eight, because each update costs less exposure. Deriving it from measured latency beats picking a number.

Why does my cancel land after my replacement? Because separately submitted transactions have no ordering guarantee on Solana. Put both instructions in one transaction so atomicity gives you the ordering.

How much does quoting cost in fees per day? Base fee plus priority fee per update, times your update count across all markets. Threshold-based updating rather than tick-based updating is usually the largest lever on that number.

Should I use skipPreflight for quote updates? Yes. Preflight adds a round trip to the most latency-sensitive operation you run, and it simulates against the current slot rather than the one you will land in.

How do I stop quoting safely during an outage? Have a cancel-all path that does not depend on your normal update loop, and make sure it uses a fee policy aggressive enough to land during whatever conditions caused the outage.

Does landing speed change my strategy or just my metrics? Your strategy. A shorter exposure window supports a tighter update threshold and a narrower spread, so the improvement compounds into what you can profitably quote.

What compute limit should batched order operations use? One measured by simulating the actual batch shape you plan to send, with a margin. Costs are not linear in order count, so extrapolating from a single order over- or under-shoots.

← Back to all posts