Lamport-exact Meteora DLMM swap-quote library, validated against the on-chain program.
Project description
meteora-dlmm
Price Meteora DLMM swaps in Python — exactly, down to the lamport.
Give it a pool's on-chain account bytes and it returns the same amount_out the Meteora
program would: the bin-by-bin walk, the variable fee that ramps as a swap crosses bins, and
on-chain limit orders. It's pure Python with no dependencies, and every quote is validated
against the program's own swapQuote — a check that ships inside the package, so you can prove
it yourself:
pip install meteora-dlmm
python -m meteora_dlmm.selftest
in sdk_out lib_out diff
1000000000 79109650 79109650 0
10000000000 790983110 790983110 0
100000000000 7901542802 7901542802 0
1000000000000 58859103727 58859103727 0
max |diff| = 0 lamports -> PASS: library reproduces the on-chain program exactly.
Why
- It's Python. Most Solana DLMM tooling is TypeScript or Rust. If you work in Python — quant research, backtests, data pipelines — this fills the gap.
- One fetch, then local. Decode a pool once from raw account bytes, then price as many swaps as you like in-process, with no further RPC round-trips. Good for routing, liquidation math, backtests, and dashboards.
- No dependencies. The library is pure standard library.
Install
pip install meteora-dlmm
The RPC example additionally needs a Solana client:
pip install "meteora-dlmm[rpc]"
Quickstart
from meteora_dlmm import PoolState, quote
# bytes from getAccountInfo(pool) and getMultipleAccounts(bin_arrays)
pool = PoolState.from_accounts(lb_pair_bytes, bin_array_byte_list, decimals_x=9, decimals_y=6)
result = quote(pool, amount_in=1_000_000_000, swap_for_y=True) # sell 1 SOL (X) for USDC (Y)
print(result.amount_out, result.bins_crossed)
swap_for_y=True spends token X (price falls); False spends token Y (price rises).
quote() also takes an optional timestamp (unix seconds) for the fee's decay reference and
defaults to now.
Fetching a live pool
You bring the RPC bytes; the library does the rest. With any Solana client:
from solana.rpc.api import Client
from solders.pubkey import Pubkey
from meteora_dlmm import PoolState, quote, array_index_of
rpc = Client("https://mainnet.helius-rpc.com/?api-key=YOUR_KEY")
pool_addr = Pubkey.from_string("HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR")
lb_pair = rpc.get_account_info(pool_addr).value.data
# derive + fetch the BinArrays around the active bin (see the repo example for the full helper),
# then:
pool = PoolState.from_accounts(lb_pair, bin_array_bytes, decimals_x=9, decimals_y=6)
print(quote(pool, 1_000_000_000, swap_for_y=True).amount_out)
Runnable end-to-end versions, including the BinArray-fetch helper, are in examples/ in the repo.
You must fetch enough BinArrays
quote() only knows about the bins you hand it. If a swap is big enough to walk past the last
BinArray you fetched, the honest answer is "I don't know" — not a smaller number. So it raises:
from meteora_dlmm import quote, InsufficientBinArrays, array_index_of
try:
result = quote(pool, amount_in, swap_for_y=True)
except InsufficientBinArrays as e:
# e.bin_id - the first bin we couldn't see
# e.remaining_in - input still unfilled
# e.partial - the Quote we got before running out (amount_out is a LOWER BOUND)
fetch_more(array_index_of(e.bin_id)) # then re-quote
Prefer a partial over an exception? Pass strict=False and check the result:
result = quote(pool, amount_in, swap_for_y=True, strict=False)
if not result.complete:
print(f"lower bound only; {result.remaining_in} unfilled, need bin {result.missing_bin_id}")
If you fetched every BinArray the pool has (arrays exist only where liquidity was placed),
there's no window to run off the end of — a swap that runs short means the pool is genuinely
drained. Say so with exhaustive=True and quote() stops raising:
pool = PoolState.from_accounts(lb_pair, all_bin_arrays, 9, 6, exhaustive=True)
result = quote(pool, huge_amount, swap_for_y=True)
# result.complete is True and result.remaining_in > 0 -> pool drained; this fill is exact
Read the two fields together:
complete |
remaining_in |
Meaning |
|---|---|---|
True |
0 |
Full fill. amount_out is exact. |
True |
> 0 |
Pool drained. amount_out is exact — it's all the pool had. |
False |
> 0 |
Ran out of data, not liquidity. amount_out is a lower bound. Fetch more, re-quote. |
API
PoolState.from_accounts(lb_pair, bin_arrays, decimals_x, decimals_y, lb_pair_key=None, exhaustive=False)— decode a pool from raw bytes. Passlb_pair_key(32 bytes) to assert the BinArrays belong to that pool. Passexhaustive=Trueifbin_arraysis every array the pool has, so a short fill is reported as a drained pool rather than raising.quote(pool, amount_in, swap_for_y, timestamp=None, support_limit_order=True, strict=True)→Quote(amount_out, bins_crossed, complete, remaining_in, missing_bin_id). RaisesInsufficientBinArrayswhenstrictand the walk leaves the loaded bin window.PoolState.is_loaded(bin_id),PoolState.loaded_bin_range(),array_index_of(bin_id)— which bins you actually hold.total_fee(...),StaticParams,VariableParams— the fee model, if you want it directly.DecodeErroron malformed or mismatched accounts.
Accuracy & scope
python -m meteora_dlmm.selftest proves the exact match on a committed 1bp SOL/USDC capture
(X→Y, full fills across the fee ramp) with no RPC key. Against real executed on-chain swaps the
library scores a median 0.0001% (both directions).
Not yet backed by a committed fixture, so don't take them on trust: other bin steps and pairs
(the math is bin-step-independent, so they should pass), the Y→X direction against
swapQuote, and the limit-order processed_order tier. Token-2022 transfer-fee pools are out
of scope — the library assumes a zero transfer fee, which is correct for SOL/USDC and most
pairs but not for mints with a transfer-fee extension.
Full detail: LIMITATIONS.md, validation/README.md, CHANGELOG.md.
License
MIT. Not affiliated with Meteora.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file meteora_dlmm-0.3.0.tar.gz.
File metadata
- Download URL: meteora_dlmm-0.3.0.tar.gz
- Upload date:
- Size: 276.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfa46247e2e78ad38390403cd6a9ffcb3445ce9bc97810161135e0d8695da7a5
|
|
| MD5 |
acb71ec6a14fb62f8ead418a7f748f3d
|
|
| BLAKE2b-256 |
b675875ee6929e5cedb079fc491bfe6dfc46bcfe3547d92ad4efee8dffff7215
|
File details
Details for the file meteora_dlmm-0.3.0-py3-none-any.whl.
File metadata
- Download URL: meteora_dlmm-0.3.0-py3-none-any.whl
- Upload date:
- Size: 139.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5121af28125855358501a789c04d374fe88206f96e23204471405d798c89427
|
|
| MD5 |
ce1cc5f59da2880e85b952cb355e440b
|
|
| BLAKE2b-256 |
6f19cd74d5981dd93ad859f3452b8db106b781aade7e74a3b070bcc4f93c1a1e
|