Skip to main content
Each example is a complete script. They read settings from environment variables: All amounts are in atoms (the token’s smallest unit). The scripts use top-level await, so run them as ES modules: save each one with an .mts extension (or set "type": "module" in your package.json) and run it with npx tsx script.mts. Examples that read other variables list them in their introduction.

Create a Launch Pool

A launch pool needs no config and anyone can create one, as long as the payer holds some of both tokens. This one charges a fixed 0.8% plus a dynamic fee, collects fees in USDC only, and turns on liquidity mining.
The script rounds the starting tick to a multiple of the tick spacing so the price starts on a usable tick. The program does not require this; any price within the bounds works. A pool can only be created once per pair this way; Pool Creation and Activation covers static and dynamic configs.

Create a Pool From a Static Config

A static config fixes a pool’s tick spacing, base fee schedule, dynamic fee, fee token, activation type and extensions, so a pool needs only the config, the two mints and a starting price. This script creates a pool from a published config. Both mints and the config must already exist. The payer funds the rent of the pool, its oracle and its two reserve accounts and signs. The config’s pool creator authority signs too when it is not the default key. Published configs use the default key, so any wallet can create a pool from one, and the script stops on a restricted config. Pool Fee Configs lists every published config and the formula that turns a choice of tier, fee schedule, variant and extension into an index. Index 0 has tick spacing 1, a fixed 0.01% base fee, fees in the output token, the dynamic fee on and no extension. On a config whose fee token is OnlyY, token Y must be wrapped SOL, USDC or a mint with a token badge, see Fee Token Rule. The script reads RPC_URL, WALLET_SECRET_KEY, TOKEN_X_MINT, TOKEN_Y_MINT, UI_PRICE (the price of one whole token X in whole token Y) and, optionally, CONFIG_INDEX (default 0).
The pool prices atoms of Y per atom of X, so the script scales the UI price by 10 ** (decimalsY - decimalsX), takes the logarithm in base 1.0001 to get a tick and rounds it to a multiple of the config’s tick spacing. getSqrtPriceFromTickId turns that tick into initialSqrtPrice, a square root price in Q64.64. The pool starts at the price of that tick, which is within one tick spacing of the price you asked for. With tick spacing 1 each tick is 0.01% apart. tickUiPrice prints the price the pool will start at. getSqrtPriceFromTickId throws a PoolError for InvalidParameters when the tick is not an integer or lies outside MIN_TICK_ID to MAX_TICK_ID. The script checks the bounds first so that its own message can name the price that caused it. SDK versions before 0.1.2 did not check: a tick beyond 524,287 wrapped, so tick 621729 returned the price of tick 97441, which is in range, and the pool would have started at the wrong price. Use 0.1.2 or newer. You choose which mint is token X. The pool address comes from the config and the two mints in either order, so a config allows one pool per pair, and UI_PRICE is always token Y per token X. Swapping the mints means passing the inverse price. What to expect on-chain:
  • One transaction creates the pool, its oracle with one observation and a reserve token account for each mint. When the starting tick is outside the inline bitmap, which covers tick array indexes -512 to 511, the builder also passes the bitmap extension account and the program creates it.
  • The pool has no tick arrays and no liquidity. Quotes and swaps fail with LiquidityIsZero until someone opens a position and deposits, as in the position examples above.
  • A static config pool has no activation delay and no partner. It accepts deposits and swaps as soon as it exists.
Errors you are likely to hit:
  • Account not found with an address: the config does not exist on this cluster. fetchStaticConfig throws it for an index that was never created.
  • A failed simulation with custom program error: 0x0 and a log line saying the account is already in use: a pool for this config and pair exists. This is a system program error, not a PoolErrorCode. Creating the same pair with the mints swapped fails the same way.
  • InvalidTokenBadge: the config collects fees in token Y, and token Y is not wrapped SOL or USDC. The builder throws it as a PoolError, and the program uses the same code (6005). Pass tokenBadgeY: deriveTokenBadge(tokenYMint) for a mint that has a badge.
  • UnsupportedMint (6006): a Token 2022 mint has an extension other than transfer fee, metadata or a transfer hook without program or authority, and no tokenBadgeX or tokenBadgeY was passed.
  • InvalidPoolCreatorAuthority (6046): the config is restricted and its authority did not sign.
Other configs change the pool without changing the script. A larger tick spacing makes the start price coarser. A scheduled fee starts counting when the pool is created. A config with the LiquidityMining extension creates a pool with two reward slots. Token 2022 mints work without changes: the builder reads each mint’s token program.

Create a Pool From a Dynamic Config

A dynamic config does not fix the pool’s settings. It authorizes one wallet, its pool creator authority, to create pools with settings passed in the instruction, and it makes that wallet the pool’s partner. Meteora creates dynamic configs, see Dynamic Configs, so you are normally given an index for your authority wallet. The config at deriveDynamicConfig(authority, index) and both mints must already exist. Two wallets sign: the payer, who funds the new accounts, and the pool creator authority, which must sign every creation from the config. They can be the same wallet. The script reads RPC_URL, WALLET_SECRET_KEY (the payer, recorded as the pool creator), AUTHORITY_SECRET_KEY, TOKEN_X_MINT, TOKEN_Y_MINT, INITIAL_TICK (as in the scheduled launch below) and, optionally, CONFIG_INDEX (default 0).
The parameters are the InitializePoolWithDynamicParameters that initializeLaunchPool takes in the examples above: fee schedule, dynamic fee, tick spacing, fee token, starting price, activation type and point, extensions and activation rules. This pool has a fixed 0.3% fee, tick spacing 10 and fees in the output token, and it opens at creation because activationPoint is null. Get INITIAL_TICK from a price with the tick computation in the static config example. What to expect on-chain:
  • The pool has type Dynamic. Its partner is the pool creator authority, and pool.partnerFeeBps is the share of the protocol fee set by the config.
  • Swaps accrue the partner’s share on the pool. Claim Partner Fees pays it out.
  • As with a static config pool, there is no liquidity until someone deposits.
Errors you are likely to hit:
  • Account not found: the config does not exist for this authority and index. A different authority derives a different config address.
  • Signature verification failed with Missing signature for public key: web3.js throws this before anything is sent when the authority’s keypair is not among the signers.
  • PoolError: the builder rejects invalid parameters before sending. An activationPoint or poolActivation without the ActivationAccess extension gives InvalidParameters, and fee settings outside their limits are rejected too.
  • InvalidTokenBadge and UnsupportedMint: the same token rules as for static configs. A config whose permission has the CreatePoolWithoutMintValidation bit skips the token badge and Token 2022 extension checks for its pools, but not the fee token rule.
  • A failed simulation with custom program error: 0x0 and an account already in use: a pool for this config and pair exists.
Pass any other fee schedule with timeSchedulerBaseFeeParameters or marketCapSchedulerBaseFeeParameters, or add ActivationAccess with activation rules, exactly as in the launch pool examples. The config only adds the authority and the partner share. An operator creates the config itself. The SDK does not wrap operator instructions, so this script calls the program through client.program.methods. The signer needs an Operator account with the CreateConfigKey permission, and permission is a bitmask of DynamicConfigPermission. You cannot run it without an operator wallet, so on the Meteora deployment you receive a config instead. It is useful for a local program or your own deployment. The script reads RPC_URL, OPERATOR_SECRET_KEY, AUTHORITY_ADDRESS and CONFIG_INDEX (optional, default 0).

Open a Position With a Curve Shape

Build the shape first: its ranges become the position’s slots, and the by-weight deposit funds them in one transaction. multiplierFromAmounts finds the largest deposit that fits both budgets, including Token 2022 transfer fees.
A Curve over 41 tick spacings stacks into 21 ranges with the horizontal layout. Pass maxRanges to buildLiquidityShape if you want to leave slots free for later.

Read a Position

This script prints what a position holds: every slot with its range, liquidity, token amounts, claimable fees and pending rewards. Nothing is signed. It reads RPC_URL, POOL_ADDRESS and POSITION_NFT.
How to read the output:
  • A position is a PositionArray account with one slot per range. positionTotalLiquidity adds the slot’s unlocked, vesting and permanently locked liquidity.
  • getAmountsFromLiquidity converts liquidity into token amounts at the pool’s current price. A slot whose upper tick is at or below the current tick holds only token Y, a slot above the current tick holds only token X, and a slot that spans it holds both. The amounts round up, as a deposit does. Pass a negative liquidity to round down, as a withdrawal does.
  • getClaimableFees computes what a claim would pay right now from the current pool and tick state, per slot and in total, before any Token 2022 transfer fee. It sends nothing and does not need a refresh first. Pass bitmap to choose which slots are brought up to date in the calculation. Slots outside it still count with the fees already credited to them.
  • Rewards are different. A slot stores pendingAmount for each reward slot as of its last refresh, and the SDK has no estimate of what has accrued since. To bring the figure current, send refreshFeesAndRewards (any claim refreshes first) and read again.
  • withReward must match the pool’s LiquidityMining bit, as the script does. A mismatch usually throws Account data length does not align to PositionArray element size. When the byte lengths happen to line up it does not throw and returns wrong values.
Errors you are likely to hit: Account not found for an NFT that is not a position, and the script’s own error when the position belongs to another pool.

Deposit With Explicit Liquidity

addLiquidity funds chosen slots of an existing position with liquidity amounts you compute, where addLiquidityByWeight in the curve example derives them from a shape. Use it to top up slots by an exact amount of liquidity or to copy another position’s amounts. getLiquidityFromAmounts finds the most liquidity a slot can take within a token budget at the current price. The position and its slots must already exist, and the wallet must hold the tokens. The owner signs, or a delegate with the AddLiquidity permission. Missing token accounts are created, and SOL is wrapped for you. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, POSITION_NFT, BUDGET_X and BUDGET_Y (atoms each slot may take of each token) and, optionally, SLOT_INDEXES (slot numbers, comma separated and ascending, default 0).
bitmap selects slots, where bit i is slot i, and liquidities holds one amount for each selected slot in ascending slot order. tickRanges lists the ranges of the selected slots so the builder can find their tick arrays, and expectedLayoutVersion makes the transaction fail if a delegate has added or removed slots since you read the position. What to expect on-chain:
  • Each slot takes liquidity from the wallet in the ratio the current price sets. A slot above the price takes only token X, a slot below it only token Y, and a slot that spans it takes both. The deposits round up, and maxAmountX and maxAmountY cap the total across all selected slots.
  • The same budget buys different liquidity in different slots, because liquidity per token depends on the width and position of the range. The script prints the liquidity added to each slot.
Errors you are likely to hit:
  • ExceededSlippage: the deposits plus any transfer fee are above maxAmountX or maxAmountY. The price moving between the read and the landing changes what a spanning slot needs. Leave headroom in the caps, or use the imbalanced deposit with a price band.
  • PoolError with LiquidityIsZero: the budget funds no liquidity, for example a zero budget of the one token a slot needs. The builder also throws it for a zero entry in liquidities.
  • PoolError with InvalidParameters: liquidities does not have one entry for each bit of bitmap.
  • InvalidLayoutVersion: the position changed since you read layoutVersion. Read it again, or pass null to skip the check.
  • InvalidActivationAccess: deposits are not open yet for this wallet on a pool with activation rules.
For a Token 2022 mint with a transfer fee, pass getLiquidityFromAmounts the amount the pool receives, calculateTransferFeeExcludedAmount, and keep the fee-inclusive amounts in maxAmountX and maxAmountY.

Deposit One Side or an Imbalanced Amount

addLiquidityImbalance deposits into one slot from whatever the wallet holds: the program works out the liquidity from the amounts and the current price. It takes at most each cap, never more than the wallet holds, and when both sides are given it uses the smaller of the two liquidities, so the leftover of the richer token stays in the wallet. This is how compoundFees deposits. The slot must exist. The owner signs, or a delegate with AddLiquidity. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, POSITION_NFT, optionally SLOT_INDEX (default 0) and the caps MAX_AMOUNT_X and MAX_AMOUNT_Y in atoms. An empty or missing cap skips that side.
sqrtPriceSlippage fails the transaction if the pool price has left the band when it lands. minLiquidity fails it if less than that liquidity would be minted from the caps. Either can be null. A single-sided deposit works only when the slot needs just that token at the current price. Pass MAX_AMOUNT_X alone for a slot above the current tick, or MAX_AMOUNT_Y alone for a slot below it. For a slot that spans the price, pass both. Errors you are likely to hit:
  • ExceededSlippage when you skip a side the slot needs: a slot that spans the price needs both tokens, so a deposit with only X fails. It also fails when the price is outside sqrtPriceSlippage or the minted liquidity is below minLiquidity.
  • AmountIsZero when the only side you pass is a token the slot does not take, for example X alone into a slot below the price. The builder also throws AmountIsZero as a PoolError for a cap of 0.
  • PoolError with InvalidParameters when both caps are null.
  • InvalidLayoutVersion when expectedLayoutVersion is stale.
To compound fees in one transaction instead, use compoundFees from the fee example.

Withdraw Part of a Position

A partial withdrawal leaves the position open. removeLiquidity takes liquidity amounts you choose for the slots in a bitmap. removeLiquidityByBps takes a share in basis points of the unlocked liquidity of each selected slot, or of every slot when bitmap is null. The script does one of each. Withdrawing everything and closing is in the earlier example. The owner signs, or a delegate with the RemoveLiquidity permission. Tokens go to the owner’s associated token accounts by default, created when missing. A delegate cannot choose another account. When a side of the pool is wrapped SOL, the builder unwraps it in the same transaction if the owner signs. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT. The script assumes the position has at least two slots, each with unlocked liquidity.
The second call reads the position again because the first call changed the liquidity. removeLiquidityByBps refreshes the vesting schedule first, so liquidity that has vested counts as unlocked. Permanently locked liquidity is never withdrawn, and the slots stay in the position when they are empty. Fees and rewards already earned stay claimable, and the liquidity left in the position keeps earning. Errors you are likely to hit:
  • MathOverflow: a liquidities entry is more than the slot’s unlocked liquidity.
  • ExceededSlippage: the tokens received, after any transfer fee, are below minAmountX or minAmountY. Set the minimums from getAmountsFromLiquidity with a negative liquidity, minus your tolerance.
  • InvalidParameters: removeLiquidityByBps finds no unlocked liquidity to take, for example after a 100% permanent lock.
  • InvalidLayoutVersion: slots were added or removed since you read layoutVersion.
  • PoolError with InvalidParameters: bps outside 1 to 10,000 or liquidities that does not match the bitmap.

Resize a Position

A position can grow and shrink after it is opened. expandPosition appends empty slots, and removePosition deletes empty slots. Both bump the position’s layoutVersion, and every bitmap or slot index you computed before is then stale. This script appends two slots below the price and then removes them again. The owner signs, or a delegate with the UpdatePositionRange permission, who must name the owner as rentReceiver when removing. The payer of expandPosition funds the larger account and any new tick arrays. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
The rules:
  • New slots go to the end, so existing slot numbers do not change when you expand. A position has at most 45 slots.
  • A removed slot must be empty: no liquidity, no pending fees and no pending rewards. At least one slot must stay.
  • The remaining slots keep their order and shift down to fill the gaps, so after a removal the slot numbers of everything above the removed slots drop. Read the position again before you build another bitmap.
  • tickRanges for removePosition must be exactly the ranges of the slots in bitmap. The tick arrays they use are passed so the program can free ticks that nothing else references.
  • expectedLayoutVersion makes the transaction fail with InvalidLayoutVersion if the layout changed since you read it. Pass null to skip the check.
Errors you are likely to hit:
  • PositionIsNotEmpty: a slot you are removing still holds liquidity, fees or rewards. Withdraw and claim first.
  • InvalidParameters: the position would exceed 45 slots, or the removal would leave no slot.
  • InvalidLayoutVersion: another transaction resized the position first.
  • InvalidPermission: a delegate lacks UpdatePositionRange, or removes slots with a rent receiver other than the owner.

Lock Liquidity Permanently

A permanent lock moves a share of every slot’s unlocked liquidity into permanently locked liquidity. That liquidity can never be withdrawn, and it keeps earning fees and rewards. Only the owner can lock. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, POSITION_NFT and, optionally, LOCK_BPS (default 10,000, which locks everything).
The share applies to each slot separately and rounds down, so locking 5,000 bps of an odd amount leaves the extra atom unlocked. The program first releases any vesting that is due. Fees stay claimable after a lock, and a position with permanently locked liquidity cannot be closed. See Locks. Errors you are likely to hit:
  • LiquidityIsZero: no slot has unlocked liquidity to move, for example locking a second time at 100%.
  • PoolError with InvalidParameters: bps outside 1 to 10,000.
  • InvalidLayoutVersion: the layout changed since you read it.
  • PositionIsNotEmpty when you later try to close the position: locked liquidity counts as liquidity.

Lock Liquidity With a Vesting Schedule

A vesting lock moves a share of each slot’s unlocked liquidity into a schedule: some of it is released at a cliff and the rest in equal steps. Released liquidity becomes unlocked and can be withdrawn. A position can have one schedule in its lifetime, and only the owner can lock. The script locks 80% of every slot, with 20% of that released after a one day cliff and then 10% every week for 8 weeks. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
The cliff and the period length use the pool’s activation type, which is seconds for Timestamp and slots for Slot, so the script reads the current point and the length of a day in the right unit. On a Slot pool the script converts with MS_PER_SLOT, the program’s nominal 250 ms slot. Real slot times differ, so the wall-clock dates are approximate. Measure the cluster with getRecentPerformanceSamples if the dates matter. cliffUnlockBps + bpsPerPeriod * numberOfPeriod must equal 10,000. Pass cliffPoint: null to start the cliff now, and a cliffUnlockBps of 10,000 with zero periods for a single release at the cliff. Errors you are likely to hit:
  • PoolError with InvalidParameters from the builder: the shares do not add to 10,000, only some of the three period values are set, a period value is out of range, or bps is outside 1 to 10,000. The program checks the cliff and the total duration as well.
  • PositionAlreadyHadVesting: the position had a schedule before, even one that has fully released. Lock from another position.
  • LiquidityIsZero: no slot has unlocked liquidity to move.
  • InvalidLayoutVersion: the layout changed since you read it.
Locks has the release formula and a worked schedule.

Check Vesting Progress

The account does not release vesting by itself. Liquidity moves from vesting to unlocked when something touches the slot: a refresh, a deposit, a withdrawal or a permanent lock. To read how much has been released, refresh every slot that holds liquidity, then read the position. The script prints the schedule and, for each slot, the released and still vesting liquidity with what the released part is worth at the current price. The owner signs the refresh, or a delegate that is approved on the NFT. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
Before the cliff nothing is released, and the refresh changes nothing. After it, releasedVestingLiquidity grows by the schedule and unlockedLiquidity grows with it. The refresh needs the ranges of every selected slot that holds liquidity, which is why the script filters on total liquidity. A slot without liquidity is skipped. The same refresh brings accrued fees and rewards up to date.

Delegate a Position

A delegate can manage a position without owning the NFT. Delegation takes two things on the NFT and the position: a Token 2022 approve of amount 0 on the NFT account, and a permission bitmask on the position. The script runs three steps and needs both wallets. The owner approves and grants ClaimFee, the delegate claims the fees to the owner, and the owner revokes. In real use the owner and the delegate sign separately. The position must have fees to claim. The script reads RPC_URL, WALLET_SECRET_KEY (the owner), DELEGATE_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
What to expect on-chain:
  • The approve and the permission update can share a transaction, and only the owner signs it.
  • The delegate signs and pays the fee for the claim, but the tokens go to the owner’s associated token accounts, which the builder creates when missing. The delegate does not receive anything.
  • A delegate can also add or remove liquidity, claim rewards and resize the position when the matching permission bit is set, see Delegates. It can never lock, close the position or change permissions. Combine bits with positionDelegatePermissionMask.
Permissions live on the position, not on the delegate. Revoking the Token 2022 approval removes the delegate but leaves the permission set, so the script clears it as well. Errors you are likely to hit:
  • InvalidAuthority: the signer is not the owner and the NFT account has no approval for it.
  • InvalidPermission: the delegate is approved but the permission bit for the instruction is not set.
  • DelegatedAmountNonZero: the approval was made with an amount above 0. Approve again with 0.
  • IncorrectATA: a delegate passed userTokenX or userTokenY that are not the owner’s associated token accounts. Leave them out.

Claim or Compound Fees

getClaimableFees refreshes nothing on-chain; it computes what a claim would pay right now and returns the slot bitmap and ranges to pass to claimFee. compoundFees claims and deposits into one slot in the same transaction. The deposit is an imbalanced deposit, so for a slot that holds both tokens it adds as much as the slot’s current token ratio allows, and anything that does not fit stays in the wallet.
If the target slot needs both tokens at the current price and the position only earned one of them, the program rejects the compound deposit with ExceededSlippage. Pass maxAmountX or maxAmountY to top it up from the wallet. On an OnlyY pool, such as the launch pool above, fees are always token Y, so a slot that spans the current price always needs a maxAmountX top-up. With nothing to claim, compoundFees throws AmountIsZero.

Withdraw Everything and Close a Position

A position can only be closed when every slot is empty: no liquidity, no unclaimed fees and no unclaimed rewards.
Permanently locked liquidity can never be withdrawn, so a position holding any cannot be closed. Vesting liquidity becomes withdrawable as it vests, so a position with a vesting lock can be closed once the schedule has finished.

Find Positions and Limit Orders

There is no index of positions by owner, so you read the wallet’s NFTs. Position and limit order NFTs are Token 2022 mints with 0 decimals, and each one has a program account at a PDA of the mint. The script lists a wallet’s positions and limit orders from its token accounts, then lists every position and limit order of a pool with getProgramAccounts. It reads RPC_URL, POOL_ADDRESS and WALLET_ADDRESS. Nothing is signed.
For a wallet, the script takes the Token 2022 accounts that hold exactly 1 token of a 0-decimal mint, then checks which of those mints have a program-owned account at derivePosition(mint) or deriveLimitOrder(mint). A wallet that only holds a delegation does not own the NFT, so it does not appear. For a pool, the filters are the account discriminator at offset 0 and the pool at offset 8, and dataSlice returns only the NFT mint at offset 40. The discriminators are bf7fb653f015fda0 for PositionArray and 89b7d45b731d8de3 for LimitOrder, and the script passes them in base58. The offsets are in Finding Accounts. To list the limit orders that name a referral, filter on offset 72 as well. Some public RPC endpoints limit or disable getProgramAccounts, so use your own endpoint for a pool scan.

Place a Limit Order Ladder

This places five bids below the price, sized by a Bid-Ask shape so the deepest bids are the largest. The order is relative, so if the price moves more than two tick spacings before it lands, it fails instead of resting at the wrong prices.
Canceling collects everything an order holds: the unfilled amount, the token received from fills and the order’s fee share. Cancel some ticks with tickIds to keep the rest of the order resting.

Place Limit Orders at Absolute Ticks

The ladder above uses relative ticks, which follow the price. Absolute ticks name tick IDs outright, so the order rests at exactly those prices. The cost is that nothing protects the order from a price that moves before it lands. A bid must sit at or below the current tick and an ask above it, and the program rejects the order with InvalidParameters when it does not. For an order that should tolerate price movement, keep relativeTick. This script places three bids, or three asks with SIDE=ask, on consecutive ticks next to the price. The owner and the new NFT mint sign. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, AMOUNT_PER_TICK (atoms of the deposit token on each tick), optionally SIDE and optionally REFERRAL_ADDRESS.
The rules:
  • A bid deposits token Y, an ask deposits token X. maxAmount caps the total deposit including any Token 2022 transfer fee, so add the fee if the mint has one.
  • ticks has 1 to 25 entries (LIMIT_ORDER_MAX_TICK), strictly ascending by id, each a multiple of the tick spacing and each with a non-zero amount. The builder checks all of this and throws PoolError with InvalidParameters.
  • A tick holds one side at a time. An order on a tick that still has unfilled orders of the other side fails with InvalidParameters.
  • The order stores referral. When the order is canceled, that wallet earns 20% (LIMIT_ORDER_REFERRAL_FEE_BPS) of the order’s fee share, paid in the fee token, if the canceler passes the referral’s token account. An order whose referral is its owner pays no referral fee, and there is nothing to pay until the order has been filled. The next examples cancel orders.
Errors you are likely to hit:
  • InvalidParameters: a bid above the current tick or an ask at or below it when the transaction lands, or ticks that are not on the tick spacing, not ascending or empty.
  • ExceededSlippage: the deposit plus its Token 2022 transfer fee is above maxAmount, or, with relativeTick, the price has left the band.
  • TickArrayBitmapExtensionAccountIsNotProvided: a tick lies outside tick array indexes -512 to 511 and the extension account is missing. The builder passes it for you when the pool has one.

Read Limit Order Fill State

An order does not tell you how much of it has filled. The state lives on its ticks. Each tick has a batch counter, orderAge, and an order records the counter it was placed at. The gap says how far the tick has got: 0 means the order is still resting, 1 means the tick is being filled and the order has its share of what is left, and 2 or more means the order is fully filled. See How Orders Fill. The script reads the order and the ticks it sits on and prints one line per tick. It reads RPC_URL, POOL_ADDRESS and LIMIT_ORDER_NFT. Nothing is signed.
For a tick in the middle of being filled, the unfilled part of the order is its amount times processedOrderRemainingAmount over totalProcessingOrderAmount, rounded up, and the rest is filled. The amounts are in the deposit token: token Y for a bid, token X for an ask. They are what the order put in, not what it has earned. The proceeds in the other token are worked out at the tick’s price when the order is canceled. Canceled ticks stay in the order with amount 0 and the script skips them. A tick’s counter resets to 0 when it has no orders left, so comparing it with a canceled order’s age would be meaningless.

Cancel Part of a Limit Order

cancelLimitOrder cancels every tick of an order by default, as in the ladder. Pass tickIds to cancel only some ticks and leave the rest resting. Canceling a tick pays the unfilled deposit, the proceeds of what filled and the tick’s share of the order’s fee, to the owner’s associated token accounts. The owner signs, or a delegate with the CancelLimitOrder permission. This script cancels the ticks in TICK_IDS and pays the referral when the order has one. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, LIMIT_ORDER_NFT and TICK_IDS (tick IDs of the order, comma separated and strictly ascending).
The referral is paid only when you pass its token account. The fee token is token Y on an OnlyY pool. On an OutputToken pool it is the token a fill pays out: token Y for a bid and token X for an ask. The referral gets 20% of the order’s fee share and the owner the rest. Leave referralToken out and the owner keeps the whole fee, because the program pays the referral only when the canceler passes its account. The token account must belong to the referral wallet, or no referral fee is paid. The script passes it only when the account exists, because the builder throws InvalidAccount for a missing one. Errors you are likely to hit:
  • CancelNonEmptyLimitOrder: a tick you named is already canceled. The name is the opposite of what it checks.
  • CannotFindLimitOrderByTickId: a tick ID that is not part of the order.
  • LimitOrderIsNotEmpty: closeLimitOrder while some ticks are still not canceled. Cancel them all first.
  • PoolError with InvalidParameters: tickIds empty, not strictly ascending or not integers.
  • ExceededSlippage: the amount received after transfer fees is below minAmountX or minAmountY.

Delegate a Limit Order

A delegate for a limit order can cancel its ticks and nothing else. The setup is the same as for a position: a Token 2022 approve of amount 0 on the order’s NFT account, and a permission on the order. The script grants CancelLimitOrder and has the delegate cancel every tick. The deposit and the proceeds go to the owner’s associated token accounts, and the owner still has to close the order. It reads RPC_URL, WALLET_SECRET_KEY (the owner), DELEGATE_SECRET_KEY, POOL_ADDRESS and LIMIT_ORDER_NFT.
The permission is stored on the order, so it survives a revoke. Clear it with permission: 0 and revoke the approval with createRevokeInstruction when you remove a delegate, as in Delegate a Position. See Delegates. Errors you are likely to hit are the same as for a position delegate: InvalidAuthority without the approval, InvalidPermission without the bit, DelegatedAmountNonZero for an approval above 0 and IncorrectATA for a payout to any account but the owner’s associated token account.

Run a Reward Campaign

The pool must have been created with the LiquidityMining extension. The pool creator sets up slot 0, and the reward authority funds the whole emission upfront. Unless it has a token badge (passed as tokenBadge), the reward mint cannot have a freeze authority, and wrapped SOL and USDC are always accepted. See Reward Mints.
LPs claim with client.reward.claimReward. After the emission ends, the authority calls withdrawIneligibleRewards to recover anything emitted while no liquidity was in range, and can then fund the slot again.

Claim Rewards for a Position

A position earns rewards for every reward slot that has an emission while its slots hold liquidity in range. This script claims every reward slot that is set up. The claim refreshes the selected slots first, so it passes the same bitmap and ranges that the fee claim uses. The owner signs, or a delegate with the ClaimReward permission, and the rewards go to the owner’s associated token account for each reward mint. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
One transaction claims one reward slot, because each has its own mint and vault. A claim with nothing accrued sends nothing. The bitmap only chooses which slots are refreshed before the claim. The claim pays the pending reward of every slot, so a smaller bitmap does not leave rewards unclaimed: slots outside it pay what they were credited at their last refresh. Set minAmount to fail with ExceededSlippage if the amount received, after any transfer fee, is lower. Errors you are likely to hit:
  • Anchor AccountNotInitialized (3012, custom program error: 0xbc4): the reward vault for rewardIndex does not exist, because the slot has not been set up or the pool has no LiquidityMining extension. The script skips slots that are not set up.
  • InvalidRewardMint: rewardMint is not the mint of that slot.
  • InvalidPermission or InvalidAuthority for a delegate without the approval or the bit.

Hand Over the Reward Authority

The reward authority of slot 0 funds the emission, and it is the only wallet that can set a new emission or withdraw ineligible rewards. The pool creator sets it when initializing the slot and can re-assign it afterwards, whoever the current authority is. This is how a creator hands a campaign to a treasury or a program-controlled wallet. The creator signs. The script reads RPC_URL, WALLET_SECRET_KEY (the creator), POOL_ADDRESS and NEW_REWARD_AUTHORITY.
Only slot 0 is the creator’s, which CREATOR_REWARD_INDEX names. The other slot is managed by operators. After the change the old authority gets InvalidPermission from setRewardEmission and withdrawIneligibleRewards. Errors you are likely to hit:
  • RewardUninitialized: the slot has not been set up with initializeReward.
  • InvalidParameters: the new authority is already the authority.
  • MissingOperatorAccount: the signer is not the pool creator recorded on the pool. The builder passes no operator account, and only an operator with UpdateRewardAuthority could sign instead.
  • InvalidPoolType: the pool has no LiquidityMining extension.

Withdraw Ineligible Rewards and Fund the Next Period

When no liquidity is in range, the reward emitted in that time has no one to go to. The pool keeps count instead of losing it. After the emission ends, the reward authority withdraws that amount and can fund the slot again. See Ineligible Rewards. The reward authority signs both steps and, in this script, also funds the next period from its own token account. It reads RPC_URL, AUTHORITY_SECRET_KEY and POOL_ADDRESS.
The withdrawal pays the authority’s associated token account for the reward mint, created when missing. It can go to any token account of the mint with receiverTokenAccount, which need not belong to the wallet that funded the emission. Funding a new period works like the first one in Run a Reward Campaign: the new openTime has to be now or later and at most 30 days ahead, and the period between 1 and 365 days (MIN_REWARD_DURATION, MAX_REWARD_DURATION and MAX_REWARD_START_DELAY). Rewards that positions earned but have not claimed stay in the vault. Errors you are likely to hit:
  • InvalidRewardState: withdrawIneligibleRewards before an emission was set or before it ended, or setRewardEmission while the current emission is still running, or after it ended but before the ineligible rewards were withdrawn. The script stops early with its own error when the period has not ended.
  • InvalidPermission: the signer is not the slot’s reward authority.
  • InvalidRewardMint: rewardMint is not the slot’s mint. The script reads the mint from the pool.
  • InvalidParameters: an openTime in the past or too far ahead, a duration outside the limits, or an emissionPerSecond of 0.
  • ExceededSlippage: the funding with its transfer fee is above maxAmount, or the withdrawal after a transfer fee is below minAmount.

Schedule a Launch

This launch pool opens for trading in one hour. The creator, as the whitelisted depositor, can add liquidity straight away; everyone else can deposit, withdraw and swap from the swap point. The fee starts at 50% and decays to 1% over the first 30 minutes of trading.
Fee schedulers count from the activation point, which must equal the time-based swap point. Every activation point has to be within 30 days of pool creation.

Swap With a Referral

A swap can pay a referral account a share of the protocol fee. The referral account is a token account that must hold the token the fee is taken in, which is token Y on an OnlyY pool and otherwise the output token of the swap. It can be any token account, including the swapper’s own. The host fee is 20% of the protocol fee (HOST_FEE_BPS), and it comes out of the protocol’s share, so the swapper receives the same amount with or without a referral. The swapper signs. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, REFERRAL_ADDRESS (a wallet), INPUT_AMOUNT (atoms of the input token) and, optionally, SWAP_FOR_Y (false to sell token Y).
Quote with hasReferral: true so quote.hostFee is the referral’s amount and quote.feesOnTokenX says which token it is paid in. hostFee is what the swap sends to the referral account, in the fee token. The builder reads the referral account to find its mint, so the account has to exist. The script creates the referral wallet’s associated token account in a separate transaction when it is missing, paid by the swapper. If the referral account is the wrong mint, the swap fails inside the token program (custom program error: 0x3) whenever the host fee is above zero. A swap too small to produce a host fee passes the token check, which makes a wrong account easy to miss in testing. Errors you are likely to hit:
  • PoolError with InvalidAccount: the referral account does not exist.
  • custom program error: 0x3 from the token program: the referral account holds another mint than the fee token.
  • NotConsumingFullAmountIn, ExceededSlippage and LiquidityIsZero as for any swap, see Getting Started.

Swap With Wrapped SOL

When one side of a pool is wrapped SOL and the swapper uses its default token account, the builder handles the wrapping. For a sale that receives SOL, it creates the wallet’s wrapped SOL account for the output and closes it after the swap, so the proceeds arrive as SOL. For a purchase paid in SOL, it tops the wrapped SOL account up to the input amount before the swap and closes the account after. The swapper signs. The script sells some of the token and then buys some back, and reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS (a pool with wrapped SOL as one side), TOKEN_AMOUNT (atoms of the other token to sell) and SOL_AMOUNT (lamports to spend).
What to expect on-chain:
  • The wallet’s SOL balance changes by the swap amount and the transaction fee. No wrapped SOL account is left behind.
  • Closing the account returns everything in it to the wallet, including wrapped SOL that was in the associated account before the swap. The builder only tops up what is missing, but the close takes the whole balance.
  • An account you pass as userTokenIn or userTokenOut is used as it is, with no wrapping or closing.

Quote From Cached Accounts

client.pool.quoteExactIn reads the pool, tick arrays, mints and clock on every call. When you quote often, for example a ladder of sizes or a router comparing routes, read the accounts once and call the pure quoteExactIn export, which runs the program’s swap math over what you hold. The swapper signs the swap. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and UNIT_AMOUNT (atoms of token X in one step of the ladder). The balance comparison assumes token Y is not wrapped SOL, because the builder closes the wrapped SOL account after the swap.
What to expect:
  • The first loop prints three quotes from one set of reads. An amountLeft above zero, which a size larger than the loaded liquidity gives, means the tick arrays ran out: a swap of that size would fail with NotConsumingFullAmountIn. Load more arrays with maxTickArrays or quote a smaller size.
  • With amountLeft at zero the quote equals what the swap pays against the same accounts and clock: the script prints the quoted and the received amounts, and they match when nothing else touched the pool and the fee did not change in between.
  • A quote does not modify the copy it reads, so the copy keeps quoting as of the moment it was read. Read the accounts again after a swap, or after any other transaction that touches the pool, as the script does in step 3.
The clock matters. Dynamic and scheduled fees depend on the time, so the quote uses the slot and timestamp you pass. A copy read some time ago quotes at that time. Read the clock again with readClock when you quote later. A Token 2022 mint on either side must be passed as an unpacked Mint, as the script does with getMint for both. The pure quote throws InvalidParameters without it, and BitmapExtensionAccountIsNotProvided when the traversal leaves the inline bitmap and the pool’s extension was not passed. For the list of tick arrays, tickArraysForSwap returns them in traversal order and says whether the bitmap extension is needed. To keep a copy current without polling, see Subscribe to Pool Changes.

Subscribe to Pool Changes

A quoting service can keep its copy of the pool and its tick arrays current from account subscriptions instead of polling. This script starts from a read, then subscribes to the pool and to the tick arrays a sell of token X would cross, decodes each update and prints the new state. Nothing is signed. It reads RPC_URL and POOL_ADDRESS. The connection opens a websocket for the subscriptions, so the endpoint must support one, or you can set wsEndpoint on the connection.
How the decoding works:
  • The pool account has a fixed part and a tail for its extensions. client.program.coder.accounts.decode reads the fixed part, and decodePoolExtensionsFromData reads the tail: reward slots with the LiquidityMining extension and activation rules with ActivationAccess.
  • A tick array has a header and a list of ticks. The coder decodes the header, and decodeTicksFromData decodes the ticks. Its second argument must match the pool’s LiquidityMining bit, because pools with rewards use larger ticks.
  • Each entry of the tickArrays map has the shape the pure quoteExactIn takes, so the values, in the order of selection.tickArrays, feed the quote in Quote From Cached Accounts.
Things to handle in a real service:
  • The tick arrays a swap crosses change as the price moves. Re-run tickArraysForSwap when the pool’s tick moves into a new array and subscribe to the new ones. Do the same for the buy direction.
  • A pool with a bitmap extension needs that account too, and isBitmapExtensionInitialized on the pool tells you.
  • Read the clock with readClock when you quote, as in the previous example.
  • Remove subscriptions with connection.removeAccountChangeListener using the id each onAccountChange call returns.

Grow the Oracle and Read a Time-Weighted Average Price

A new pool has an oracle with one observation, so it holds only the latest point. To keep history for time-weighted prices, grow it. Anyone can do it and pays the rent of the added observations, up to 500 in total (MAX_OBSERVATION_LENGTH). The payer signs. The script reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and, optionally, LENGTH_TO_ADD (default 100).
Only swaps write to the oracle. Each swap adds the time since the last update, multiplied by the pool’s tick before the swap, to the cumulative tick. A swap at least 15 seconds after the latest observation was created starts a new observation. A swap sooner updates the latest one in place. An observation with timestamp 0 is an empty slot, and the buffer is a ring, so the order of slots is not the order of time. The average tick over an interval is the difference of two cumulative ticks divided by the seconds between them. The script below takes the newest observation that is at least WINDOW_SECONDS old as the start, and extends the latest cumulative tick to now at the pool’s current tick, which is how the program would accumulate it. That gives an average up to the present, not only up to the last swap. It reads RPC_URL, POOL_ADDRESS and, optionally, WINDOW_SECONDS (default 3,600).
The average tick is an integer, and tickUiPrice turns it into a price of token Y per token X in UI units. An average of ticks is a geometric average of prices, see Time-Weighted Average Tick. If the window is longer than the history, the script uses the oldest observation, and the seconds it prints are the span it really used. Errors you are likely to hit:
  • PoolError with InvalidParameters from increaseOracleLength for a length of 0. The program rejects a new length above 500 with InvalidParameters as well.
  • The script’s own error when the oracle holds fewer than two observations: only swaps create them, at least 15 seconds apart, and a new oracle needs a first swap to start.
Growing the oracle does not erase observations. The program reorders the ring so that the new slots follow the newest observation.

Claim Partner Fees

The pool creator authority of a dynamic config is the partner of the pools created from it, and it earns a share of the protocol fee on every swap (partnerFeeBps of the config, at most 50%). The share accrues on the pool in the token the fee is taken in. Static config and launch pools have no partner. The partner claims what has accrued with claimPartnerFee, and the tokens go to the partner’s associated token accounts, created when missing. Only the partner can sign. The script reads RPC_URL, PARTNER_SECRET_KEY and POOL_ADDRESS.
maxAmountX and maxAmountY cap the claim before any Token 2022 transfer fee, and the program pays the smaller of the cap and what has accrued, so passing the pending amounts claims everything. Use minAmountX and minAmountY to require a minimum after the transfer fee. With nothing accrued the instruction succeeds and sends nothing. Errors you are likely to hit:
  • InvalidAuthority: the signer is not the pool’s partner. The script checks first and throws its own error.
  • ExceededSlippage: the amount received after a transfer fee is below minAmountX or minAmountY.
The operator can change partnerFeeBps on a pool later, see Dynamic Configs.

Read Events From a Transaction

Builders return transactions, not results, so the amounts a swap, deposit or claim actually moved are in the events the program emits. Most events are delivered as inner instructions, and two are written to the log, so one decoder has to read both. Reading Events has the complete script, and the same page lists every event with its fields. Pass the signature that sendAndConfirmTransaction returns. For example EvtSwap gives the amounts in and out, the fees and the referral amount of a swap, and EvtClaimFee the fees a claim paid.

Set a Compute Budget and Priority Fee

Builders add no compute budget instructions, so a transaction runs with the default limit of 200,000 compute units for its instruction and no priority fee. This script opens a position with one range for each of the 41 tick spacings from 20 below the price to 20 above it, measures the builder’s transaction with a simulation, and then adds a compute unit limit and a unit price ahead of the builder’s instructions. The owner and the new NFT mint sign. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and, optionally, PRIORITY_MICRO_LAMPORTS (the price of a compute unit in micro-lamports, default 5,000).
The simulation signs with the keypairs and reports unitsConsumed. The script sets the limit 20% above it. A tighter limit also lowers the priority fee: the fee is the unit price times the limit you request, not the units used, so a limit of 100,000 at 5,000 micro-lamports costs 500 lamports on top of the base fee and 200,000 costs 1,000. A limit that is too low fails the transaction when the program runs out of units, so keep headroom, and measure again when the position gets more ranges. Put the budget instructions first. A builder’s instruction list is plain tx.instructions, so unshift puts them ahead of it, and anything the builder prepends, such as token account creation or SOL wrapping, runs after them. Positions with many ranges, and any transaction that creates tick arrays, are the ones to measure. A position that adds many new ticks to one tick array can also fail with ReallocExceedMaxLengthPerInstruction: one instruction can grow a tick array by at most 10,240 bytes, which is 64 new ticks, or 53 on LiquidityMining pools. Open it with fewer ranges and add the rest with expandPosition. The simulation runs under the default limit of 200,000 compute units, so it cannot measure a transaction that needs more. For those, simulate with ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }) in place, then replace it with the measured limit.

Send a Versioned Transaction With a Lookup Table

A legacy transaction is limited to 1,232 bytes, and every account costs 32 of them. A position spread over many tick arrays touches a tick array per range end, so a withdrawal from a wide position on a fine tick spacing can exceed the limit. A versioned transaction can read most of those accounts from an address lookup table instead, at about one byte each. This script takes an existing position that spans many tick arrays, builds the withdrawal of all its slots with the builder, and prints the legacy size. It then creates a lookup table holding the accounts the instructions use, waits for the table to activate and sends the same instructions as a v0 message that reads the table. The owner signs everything. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
How it works:
  • createLookupTable takes a recent slot of the cluster, which is why the script reads a finalized slot. The table is a new account owned by the wallet, and extendLookupTable fills it. A transaction that adds addresses is itself limited in size, so the script adds 20 at a time.
  • Signers and program IDs stay in the message itself, so the script leaves signers out of the table, and the compiler keeps the programs static.
  • The script waits for the slot to advance past the one that extended the table before it reads the table and sends.
  • connection.sendTransaction takes a VersionedTransaction, which you sign with sign, and getAddressLookupTable returns the account that compileToV0Message needs.
The table is an account that stays on-chain and holds rent, so reuse it for further transactions on the same position. deactivateLookupTable and closeLookupTable retire it. Compute budget instructions go into the message like any other.

Handle Errors

Two kinds of failure reach your code, and both use the program’s error numbering. A builder throws PoolError before anything is sent, when an argument is invalid. A transaction that the program rejects fails at simulation or on-chain, and web3.js throws SendTransactionError with the program’s logs. The code of a PoolError and the custom error number of a failed transaction use the same numbers, and PoolErrorCode maps a number to its name in both directions. The script triggers one of each and prints the error name. The swapper signs. It reads RPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and HUGE_INPUT_AMOUNT (an input larger than the pool’s liquidity within one tick array).
Mapping a failed transaction:
  • The program logs Program <id> failed: custom program error: 0x... for the error it returned. The script matches the DLMM Pro program ID so that an error of another program, such as the token program in a transfer, is not read as one of ours. parseInt(hex, 16) gives the number.
  • error.getLogs(connection) returns the logs of a SendTransactionError, from the error itself when they are already there and by signature otherwise.
  • A landed transaction reports the error as InstructionError: [index, { Custom: number }] in the err of its status or of getTransaction(...).meta, and PoolErrorCode[number] gives the name.
  • An error with no code in the 6000 range comes from another program, such as custom program error: 0x0 from the system program for an account that already exists, or from Anchor’s account checks inside the program, such as 3012 (0xbc4) for an account that does not exist yet. PoolErrorCode has no name for either.
Program Errors lists every code with its usual cause. A swap that fails with NotConsumingFullAmountIn is the usual first one: quote again with maxTickArrays raised or a smaller input, and always check quote.amountLeft.

SDK Reference

Every service method and helper, with signers and defaults.

Positions

How slots, shapes, locks and delegates work.