Bin-Based Liquidity — Meteora DLMM

The tick-based DEX is integrated and running. Ticks, tick arrays, liquidity that changes at every boundary — all of it now simulates correctly and builds valid swap instructions. Concentrated liquidity, while architecturally complex, at least follows a coherent model: discretize the price axis into points, assign liquidity between those points, walk through them during a swap.

Now I'm looking at Meteora DLMM, and the model is different again.

Not radically different in purpose — this is still a concentrated liquidity DEX. Liquidity providers still choose where to place their capital. Swaps still move through regions of varying liquidity depth. The goal is still capital efficiency. But the fundamental unit of organization isn't a tick. It's a bin.

The distinction sounds cosmetic. It is not. It changes the math inside a swap, the data structures on-chain, the traversal algorithm, and the failure modes I need to handle. Same destination, different highway entirely.

Vending Machine Pricing

The easiest way to understand bins is a vending machine — not the kind in a break room with twelve identical sodas behind the same glass, but one of those airport vending machines that stocks everything from water bottles to noise-canceling headphones, each item in its own labeled slot at its own price.

Each slot is a bin. The slot labeled "$1.50" holds water. The slot labeled "$3.00" holds energy drinks. The slot labeled "$29.99" holds earbuds. When you buy from a slot, you pay exactly the labeled price. No negotiation, no sliding scale. The price is what it is. If you want three waters, you pay $1.50 three times — zero deviation per unit, as long as that slot still has inventory.

But what happens when the $1.50 slot runs out of water? You move to the next available slot. Maybe it's the $1.75 slot with a different brand. The price jumps discretely — $1.50 to $1.75, no prices in between. You don't gradually slide from $1.50 to $1.75 as the first slot empties. You're at $1.50 until the last bottle is gone, then you're at $1.75.

This is exactly how a bin-based AMM works. Each bin represents a specific price. Within a bin, swaps execute at that bin's price with zero slippage — the math is constant-sum, meaning x + y = k within a single bin. Every unit of token you buy from that bin costs the same amount. The moment the bin's liquidity is exhausted, the swap moves to the next bin at a discretely different price.

Compare this to the tick-based model I just integrated. In a tick-based DEX, ticks are price points — boundaries between regions. Between two adjacent ticks, the pool behaves like a constant-product AMM with fixed liquidity. The price slides continuously as you trade within that range. It's not flat. A swap that consumes half the liquidity between two ticks pays a different effective price per unit than one that consumes a quarter. The curve is smooth within each segment.

Bins are price ranges, but the range within each bin is treated as a single flat price. No curve. No sliding. Buy one token from a bin or buy the last hundred — same price per unit until the bin is empty. Then the price steps to the next bin. The price chart looks like a staircase. The tick-based price chart looks like a series of smooth curves stitched together.

This difference — staircase versus stitched curves — is the fundamental architectural distinction between Meteora DLMM and tick-based DEXs like Orca Whirlpool.

The Ruler's Resolution

Each bin has an ID — an integer. Adjacent bins have adjacent IDs. The price of bin N is determined by a formula that involves something called bin_step, which defines how much the price changes from one bin to the next.

Think of bin_step like choosing between a ruler marked in inches versus one marked in millimeters. Both rulers measure length. The millimeter ruler gives finer resolution — more markings, more precision, smaller increments. The inch ruler is coarser — fewer markings, bigger jumps between measurement points.

bin_step is specified in basis points. A bin_step of 10 means adjacent bins differ in price by 0.1%. A bin_step of 100 means a 1% difference between neighbors. The actual price of any bin is calculated as (1 + bin_step / 10000) raised to the power of the bin's ID. This creates an exponential price ladder where each rung is proportionally spaced.

Why does this matter for an arbitrage bot? Because bin_step determines how coarse or fine the price grid is, which directly affects simulation accuracy.

A pool with bin_step of 1 — the finest resolution, 0.01% per step — has bins packed incredibly close together. A 1% price move crosses about 100 bins. That's a hundred discrete constant-sum calculations during swap simulation. The math is simple per bin (constant-sum is just addition), but the iteration count adds up.

A pool with bin_step of 100 — a full 1% per step — has much coarser spacing. A 1% move crosses a single bin boundary. Fewer calculations, but each bin represents a wider price range, which means the flat-price-within-a-bin approximation is coarser.

It's the tradeoff you see in every discretization scheme: higher resolution means more computational work per operation but finer granularity. Lower resolution means faster traversal but chunkier price steps. For simulation accuracy, I need to respect whatever bin_step the pool uses and iterate accordingly.

Warehouse Shelf Zones

On-chain, bins don't each get their own separate account. That would mean tens of thousands of individual accounts per pool, each requiring rent, each needing to be referenced in transaction account lists. The cost would be absurd and the transaction size limitations would make swaps impossible.

Instead, bins are grouped into bin arrays. Each bin array is a PDA — a Program Derived Address — that holds a batch of consecutive bins. If the tick-based DEX's tick arrays were parking garage levels holding 88 parking spots each, then bin arrays are warehouse shelf zones — each zone contains a row of shelves, and each shelf holds a specific product at a specific price.

Walk into a warehouse. The zones are labeled: Zone A covers aisles 1 through 20. Zone B covers aisles 21 through 40. To find the product in aisle 35, you go to Zone B and walk to the right shelf. You don't need to load the inventory list for Zone A or Zone C — only Zone B's manifest matters for your current task.

The swap instruction works the same way. It only needs to reference the bin arrays that the swap will actually traverse. A small swap that stays within the active bin's bin array needs only that one bin array account. A larger swap that crosses into the next zone of bins requires both bin array accounts. The transaction builder has to predict — before execution — which bin arrays will be needed, derive their PDAs, and include them in the account list.

This is the same chicken-and-egg challenge I faced with tick arrays, wearing different clothes. I need to simulate the swap to know which bin arrays to include, but I need the bin array data to simulate accurately. The approach is similar: start from the active bin, estimate the price impact, include the necessary bin arrays plus a safety margin.

The PDA derivation is deterministic — given the pool address and the bin array index, I can compute the exact address of any bin array without fetching anything from the chain. The challenge isn't finding the address. It's knowing which bin arrays the swap will touch, and making sure those accounts are actually initialized on-chain before I reference them.

And that word — initialized — is where one of the nastier bugs lives. But I'll get to that.

The Sparse Bin Problem

Here's a reality of concentrated liquidity that both tick-based and bin-based DEXs share: most of the possible price range is empty. No liquidity provider in their right mind places capital at a price 1000x above or below the current market. The vast majority of bins hold zero liquidity.

In a constant product pool, this doesn't matter. The formula works across the entire price range with a single calculation. In a bin-based pool, I need to skip empty bins efficiently. If the active bin empties during a swap and the next 500 bins are also empty, I can't iterate through all 500, checking each one — that would burn through compute units and likely exceed the transaction's compute budget.

This is where the bitmap comes in.

Think of a massive grocery store. Thousands of shelf positions, most of them empty at any given time — products are consolidated into certain aisles, and huge sections of the store are dark, unstocked, waiting for seasonal inventory. The store manager doesn't walk every aisle to find where the cereal is. There's an inventory board at the entrance: "Cereal: Aisle 7. Beverages: Aisle 12. Everything between aisles 8 and 11: empty." One glance at the board, and you skip straight from Aisle 7 to Aisle 12.

The bitmap serves this exact purpose. Each bit in the bitmap corresponds to a bin. A 1 means the bin has liquidity. A 0 means it's empty. To find the next bin with liquidity after the current one empties, the program scans the bitmap for the next set bit instead of loading and checking each bin individually. Bit scanning is fast — a few CPU instructions to find the next 1 in a sequence of 0s.

But there's a coverage problem. A single bitmap has finite size. It can only track so many bins. For pools with small bin_step values or for prices far from the current active price, the base bitmap runs out of room. The price range it covers simply isn't wide enough.

Enter bitmap_extension. This is exactly what it sounds like — an additional bitmap account that extends the coverage beyond the base bitmap. It's like the grocery store adding a second inventory board because the store grew beyond what the first board could track. "For aisles 1-500, check Board A. For aisles 501-1000, check Board B."

The bitmap_extension is a separate PDA. During swap simulation, the code first checks the base bitmap. If the search for the next active bin goes beyond the base bitmap's range, it switches to the bitmap_extension to continue the search. The core logic remains the same — skip empty, find the next bin with liquidity — but now it spans a much wider price range.

For the bot, this means the swap simulation code has to handle bitmap lookups that may span two different accounts. The traversal function scans bits, crosses a bitmap boundary, loads the extension, continues scanning. Getting this right means tracking exactly where the base bitmap ends and the extension begins, which depends on the pool's bin_step and the bin ID ranges each bitmap covers. Off-by-one errors here are silent and deadly — the simulation finishes but produces a wrong answer because it skipped a bin that actually had liquidity, or found a bin that the on-chain program wouldn't find.

Fees That Move

Every DEX charges a swap fee. Constant product pools have a fixed fee — 0.25%, 0.30%, whatever the pool was created with. Tick-based pools also typically use fixed fee tiers. You parse the fee rate once when you index the pool and use it forever.

Meteora DLMM breaks this pattern. The fees are dynamic.

The fee on a given swap is not a fixed percentage. It has two components: a base fee that's set at pool creation and a variable fee that changes based on recent market conditions. When trading activity is calm and prices are stable, the variable component is low. When the market turns volatile — prices whipping back and forth, bin boundaries being crossed rapidly — the variable component increases.

The design logic is straightforward: during high volatility, there's more impermanent loss risk for LPs, more demand from arbitrageurs exploiting rapid price movements, and more reason to charge a premium. The pool effectively raises its prices when demand surges, like surge pricing for rideshares. A quiet Tuesday at 3 AM, a ride from downtown to the airport costs $25. New Year's Eve at midnight, the same ride costs $80. Same service, same route — the price reflects the intensity of the moment.

For an arbitrage bot, dynamic fees add a layer of uncertainty to profit calculation that doesn't exist with fixed-fee pools. With a fixed fee, I compute the swap output deterministically: input amount minus fee equals the amount that goes through the formula. The fee percentage never changes between the moment I simulate and the moment the transaction executes.

With dynamic fees, the fee I calculate during simulation might differ from the fee the on-chain program charges at execution time. If several trades hit the pool between my simulation and my transaction landing, the volatility accumulator has changed, the variable fee has shifted, and my profit estimate is off. It might still be profitable, just less so. Or the fee increase might eat the entire margin, turning a profitable opportunity into a loss.

I handle this by incorporating a fee buffer — overestimating the expected fee slightly to account for the possibility that it increases between simulation and execution. This is conservative. It means I pass on some marginally profitable opportunities that might have worked. But the alternative — underestimating fees and landing transactions that lose money — is worse. In MEV, a missed opportunity costs nothing. A landed transaction that loses money costs the loss plus the transaction fee plus the Jito tip. Conservative fee estimation is the correct tradeoff.

The internal mechanics of how exactly the variable fee accumulator works and what formulas drive the volatility calculation — that's the kind of implementation detail I keep to myself. The concept matters for understanding the architecture. The exact parameters are competitive knowledge.

Tick vs. Bin — The Practical Comparison

With both architectures running in the bot, the differences become concrete — not just theoretical, but felt in every line of integration code.

Price model within a segment:

In a tick-based DEX, the price slides continuously between two ticks. The math is constant-product within each tick range — x * y = k with a fixed liquidity value L. As you swap, the price moves along the curve. Buy more, the price goes up more. The relationship is smooth and nonlinear.

In a bin-based DEX, the price is flat within a bin. The math is constant-sum — x + y = k. Buy one unit or buy a thousand units from the same bin, the price per unit is identical. The price doesn't move until the bin empties and the swap crosses to the next bin.

This means bin-based swap simulation is actually simpler per step. Constant-sum is just addition and subtraction. Constant-product requires multiplication and square roots. But bin-based swaps might cross more bins for the same price movement, depending on the bin_step, which can make the total iteration count higher.

Price resolution parameter:

Tick-based DEXs use tick_spacing — determines which ticks are valid LP boundaries. Common values: 1, 64, 128, 256.

Bin-based DEXs use bin_step — determines the price increment between adjacent bins. Common values range from 1 to 100+ basis points.

These parameters serve the same conceptual purpose (controlling price granularity) but are mechanically different. tick_spacing filters which ticks exist. bin_step determines how far apart bin prices are. Finer granularity in both cases means more iterations during swap simulation.

Finding non-empty positions:

Tick-based: each tick array stores a bitmap of which ticks within it are initialized. Finding the next initialized tick means scanning this bitmap, then potentially loading the next tick array.

Bin-based: the bitmap and bitmap_extension provide a global view of which bins have liquidity. The search can span large ranges efficiently without loading bin arrays that contain only empty bins.

Both approaches solve the same problem — skip empty space quickly — but the bin-based bitmap search, with its potential bitmap_extension crossing, adds a layer of complexity that the tick array bitmap doesn't have.

LP position model:

Tick-based: LPs define a lower tick and upper tick. Their liquidity is uniformly distributed between those boundaries. Every tick in the range gets the same L value contribution.

Bin-based: LPs can allocate different amounts to different individual bins. An LP might put 50% of their capital in the bin closest to the current price, 30% in the next bin, and 20% in the bin after that. This creates a custom distribution shape, not just a uniform rectangle. More flexible for the LP, more unpredictable for the bot simulating expected liquidity.

Fee model:

Tick-based (Whirlpool): fixed fee rate per pool. One parse, use forever.

Bin-based (Meteora DLMM): dynamic fees with base + variable components. Must estimate at simulation time, may differ at execution time.

The dynamic fee is the single biggest practical difference from a bot-building perspective. Everything else — the data structure differences, the traversal algorithm differences, the PDA derivation differences — is manageable complexity. Different code paths, different math, but fundamentally the same engineering challenge of "parse the state, simulate the swap, build the instruction."

Dynamic fees introduce genuine uncertainty. And uncertainty, in a business where profit margins are measured in fractions of a percent, is expensive.

The Implementation Nightmares

The theory of bins is elegant. Discrete price levels, constant-sum math within each level, bitmap for fast traversal. If I were designing it on a whiteboard, I'd call it clean.

The on-chain implementation is where elegance meets reality, and reality wins.

Uninitialized bin arrays. This is the first trap. Not every bin array that could exist actually exists on-chain. A bin array is only created — initialized — when someone first adds liquidity to the bins it covers. If no LP has ever placed capital in that price range, the bin array PDA has no account. The address is derivable, but deriving an address and having an initialized account at that address are two very different things.

In the tick-based DEX, I hit a similar issue with tick arrays. But the failure mode was explicit — the transaction fails with a clear "account not found" error if I reference an uninitialized tick array. Painful but diagnosable.

With bin arrays, the failure mode can be subtler. During simulation, if my code tries to read a bin array that doesn't exist on-chain, it gets a null response from the RPC. If I don't handle that null correctly — if my code assumes every derivable bin array has data — the simulation produces garbage results without throwing an error. The swap output looks plausible, the transaction gets built, it gets submitted, and it fails on-chain with an error that doesn't immediately point back to "you referenced a bin array that doesn't exist."

I now explicitly check initialization status for every bin array I load. Derivable does not mean initialized. This is obvious in retrospect. It was not obvious when the swap simulation was silently returning wrong outputs and I spent hours tracing the discrepancy.

Swap direction and account indexing. A swap instruction references the user's token accounts — the account to send tokens from and the account to receive tokens into. Which account is at which index in the instruction's account list depends on the swap direction: am I swapping token X for token Y, or token Y for token X?

In my initial implementation, I hardcoded the source token account index. It worked perfectly for X-to-Y swaps. Y-to-X swaps silently used the wrong account as the source, which either caused an on-chain error (if the wrong account didn't have enough tokens) or worse, tried to pull tokens from an account that happened to have a balance, causing an unintended swap.

The fix is trivial — swap the indices based on the direction flag. The bug was trivial. Finding it was not, because the error message on-chain was a generic token program error that didn't say "wrong source account" — it said "insufficient funds" or "owner mismatch," depending on the exact account state. The symptom pointed in the wrong direction.

Error code collisions. This one still irritates me. Meteora DLMM's program defines its own error codes — numeric constants that map to specific error conditions. The Anchor framework, which Meteora DLMM is built on, also defines its own set of error codes. These two sets overlap.

Error code 6002 from the Meteora DLMM program means one thing — a specific DLMM-defined error condition. Error code 6002 from the Anchor framework means something completely different — a generic Anchor constraint violation. When a transaction fails and I see "Custom Error: 6002" in the logs, I have to figure out which 6002 it is. Is it the DLMM telling me something specific about the swap state? Or is it Anchor telling me that an account constraint failed?

The same number. Two different meanings. The error logs don't always disambiguate which program emitted the error. So I'm left cross-referencing the instruction that failed, the program it called, the context of the error within the instruction's account list, and sometimes resorting to simulation with verbose logging to reproduce the error in a context where I can inspect the full program trace.

Imagine calling 911 and hearing "Error 7." Does that mean "all units busy" from the dispatch system? Or "invalid address" from the GPS lookup? Or "phone line degraded" from the carrier's network? Same number, different system, different meaning, no label telling you which system generated it. That's error code debugging across layered programs on Solana.

I've built a lookup table now — DLMM error codes on one side, Anchor error codes on the other, with notes about which instruction context typically triggers which interpretation. It's a hack. It works. I hate it.

The Constant-Sum Payoff

Despite the implementation headaches, bin-based liquidity has a property that makes it worth the pain for arbitrage: zero slippage within a bin.

In a constant-product pool, every trade moves the price. There's no such thing as a free trade — even a tiny swap shifts the reserves and changes the exchange rate. In a tick-based pool, the price slides continuously within each tick range. Again, every unit traded changes the price.

In a bin-based pool, buying one unit from a bin or buying 99% of its liquidity happens at the same price per unit. The price doesn't budge until the bin is empty. For an arbitrageur, this means that within a single bin, the expected output exactly matches the simulated output. There's no slippage to account for, no curve to model, no "the price moved against me between the first unit and the last unit" concern. The bin either has enough liquidity at that price, or it doesn't.

This property makes the profit calculation for small trades — trades that stay within a single bin — trivially accurate. The simulation exactly matches execution. For larger trades that cross multiple bins, the staircase effect still produces a predictable output: you get exact-price execution on each bin, stepping through bins in sequence. The total output is the sum of exact amounts from each bin.

In practice, this means my profit estimates for bin-based swaps tend to be more accurate than for tick-based swaps, where the continuous curve means the exact execution price depends on the precise liquidity at the moment of execution. Whether that accuracy translates into more landed profitable trades depends on many other factors — the dynamic fee estimation, the competition from other bots, the latency to the validator — but having one less source of error in the simulation pipeline is welcome.

Staircase and Smooth Curves

The mental model I'm settling into is this: tick-based DEXs draw smooth curves on a chalkboard, stitched together at tick boundaries. Bin-based DEXs draw staircases on graph paper. Both approximate the ideal of continuous liquidity distribution. Both make tradeoffs between precision and computational tractability. Both force the integrator to deal with chunked data structures, bitmap traversal, PDA derivation, and initialization checking.

The differences are real but not philosophical. They're engineering differences. Different data layouts, different math per step, different fee models, different error modes. The core challenge of building an arbitrage bot — simulate accurately, build instructions correctly, submit quickly — remains the same regardless of whether the DEX discretizes into ticks or bins.

What I wonder, now that I have both integrated and running, is whether the dynamic fee model creates a fundamentally different competitive landscape. With fixed-fee DEXs, every bot simulating the same pool with the same state gets the same expected output. The competition is purely about speed and transaction construction. With dynamic fees, there's a prediction element — bots that better forecast the fee at execution time have an edge beyond raw latency. Does that favor more sophisticated bots? Or does the uncertainty just add noise that makes everyone's estimates less reliable, benefiting no one?

I don't know the answer yet. The data will tell me.

Disclaimer

This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.