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.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 isOnlyY, 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).
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
LiquidityIsZerountil 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.
Account not foundwith an address: the config does not exist on this cluster.fetchStaticConfigthrows it for an index that was never created.- A failed simulation with
custom program error: 0x0and 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 aPoolErrorCode. 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 aPoolError, and the program uses the same code (6005). PasstokenBadgeY: 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 notokenBadgeXortokenBadgeYwas passed.InvalidPoolCreatorAuthority(6046): the config is restricted and its authority did not sign.
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 atderiveDynamicConfig(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).
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.partnerFeeBpsis 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.
Account not found: the config does not exist for this authority and index. A different authority derives a different config address.Signature verification failedwithMissing 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. AnactivationPointorpoolActivationwithout theActivationAccessextension givesInvalidParameters, and fee settings outside their limits are rejected too.InvalidTokenBadgeandUnsupportedMint: the same token rules as for static configs. A config whose permission has theCreatePoolWithoutMintValidationbit 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: 0x0and an account already in use: a pool for this config and pair exists.
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.
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 readsRPC_URL, POOL_ADDRESS and POSITION_NFT.
- A position is a
PositionArrayaccount with one slot per range.positionTotalLiquidityadds the slot’s unlocked, vesting and permanently locked liquidity. getAmountsFromLiquidityconverts 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 negativeliquidityto round down, as a withdrawal does.getClaimableFeescomputes 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. Passbitmapto 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
pendingAmountfor each reward slot as of its last refresh, and the SDK has no estimate of what has accrued since. To bring the figure current, sendrefreshFeesAndRewards(any claim refreshes first) and read again. withRewardmust match the pool’sLiquidityMiningbit, as the script does. A mismatch usually throwsAccount 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.
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
maxAmountXandmaxAmountYcap 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.
ExceededSlippage: the deposits plus any transfer fee are abovemaxAmountXormaxAmountY. 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.PoolErrorwithLiquidityIsZero: 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 inliquidities.PoolErrorwithInvalidParameters:liquiditiesdoes not have one entry for each bit ofbitmap.InvalidLayoutVersion: the position changed since you readlayoutVersion. Read it again, or passnullto skip the check.InvalidActivationAccess: deposits are not open yet for this wallet on a pool with activation rules.
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:
ExceededSlippagewhen 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 outsidesqrtPriceSlippageor the minted liquidity is belowminLiquidity.AmountIsZerowhen 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 throwsAmountIsZeroas aPoolErrorfor a cap of 0.PoolErrorwithInvalidParameterswhen both caps arenull.InvalidLayoutVersionwhenexpectedLayoutVersionis stale.
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.
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: aliquiditiesentry is more than the slot’s unlocked liquidity.ExceededSlippage: the tokens received, after any transfer fee, are belowminAmountXorminAmountY. Set the minimums fromgetAmountsFromLiquiditywith a negative liquidity, minus your tolerance.InvalidParameters:removeLiquidityByBpsfinds no unlocked liquidity to take, for example after a 100% permanent lock.InvalidLayoutVersion: slots were added or removed since you readlayoutVersion.PoolErrorwithInvalidParameters:bpsoutside 1 to 10,000 orliquiditiesthat 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.
- 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.
tickRangesforremovePositionmust be exactly the ranges of the slots inbitmap. The tick arrays they use are passed so the program can free ticks that nothing else references.expectedLayoutVersionmakes the transaction fail withInvalidLayoutVersionif the layout changed since you read it. Passnullto skip the check.
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 lacksUpdatePositionRange, 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 readsRPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS, POSITION_NFT and, optionally, LOCK_BPS (default 10,000, which locks everything).
LiquidityIsZero: no slot has unlocked liquidity to move, for example locking a second time at 100%.PoolErrorwithInvalidParameters:bpsoutside 1 to 10,000.InvalidLayoutVersion: the layout changed since you read it.PositionIsNotEmptywhen 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 readsRPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
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:
PoolErrorwithInvalidParametersfrom 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, orbpsis 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.
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 readsRPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
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 2022approve 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.
- 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.
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 passeduserTokenXoruserTokenYthat 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.
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.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 withgetProgramAccounts. It reads RPC_URL, POOL_ADDRESS and WALLET_ADDRESS. Nothing is signed.
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.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 withInvalidParameters 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.
- A bid deposits token Y, an ask deposits token X.
maxAmountcaps the total deposit including any Token 2022 transfer fee, so add the fee if the mint has one. tickshas 1 to 25 entries (LIMIT_ORDER_MAX_TICK), strictly ascending byid, each a multiple of the tick spacing and each with a non-zero amount. The builder checks all of this and throwsPoolErrorwithInvalidParameters.- 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.
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 abovemaxAmount, or, withrelativeTick, 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.
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).
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:closeLimitOrderwhile some ticks are still not canceled. Cancel them all first.PoolErrorwithInvalidParameters:tickIdsempty, not strictly ascending or not integers.ExceededSlippage: the amount received after transfer fees is belowminAmountXorminAmountY.
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 2022approve 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.
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 theLiquidityMining 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.
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 theClaimReward 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.
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 forrewardIndexdoes not exist, because the slot has not been set up or the pool has noLiquidityMiningextension. The script skips slots that are not set up. InvalidRewardMint:rewardMintis not the mint of that slot.InvalidPermissionorInvalidAuthorityfor 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 readsRPC_URL, WALLET_SECRET_KEY (the creator), POOL_ADDRESS and NEW_REWARD_AUTHORITY.
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 withinitializeReward.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 withUpdateRewardAuthoritycould sign instead.InvalidPoolType: the pool has noLiquidityMiningextension.
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 readsRPC_URL, AUTHORITY_SECRET_KEY and POOL_ADDRESS.
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:withdrawIneligibleRewardsbefore an emission was set or before it ended, orsetRewardEmissionwhile 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:rewardMintis not the slot’s mint. The script reads the mint from the pool.InvalidParameters: anopenTimein the past or too far ahead, a duration outside the limits, or anemissionPerSecondof 0.ExceededSlippage: the funding with its transfer fee is abovemaxAmount, or the withdrawal after a transfer fee is belowminAmount.
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.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 anOnlyY 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).
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:
PoolErrorwithInvalidAccount: the referral account does not exist.custom program error: 0x3from the token program: the referral account holds another mint than the fee token.NotConsumingFullAmountIn,ExceededSlippageandLiquidityIsZeroas 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 readsRPC_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).
- 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
userTokenInoruserTokenOutis 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.
- The first loop prints three quotes from one set of reads. An
amountLeftabove zero, which a size larger than the loaded liquidity gives, means the tick arrays ran out: a swap of that size would fail withNotConsumingFullAmountIn. Load more arrays withmaxTickArraysor quote a smaller size. - With
amountLeftat 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.
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 readsRPC_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.
- The pool account has a fixed part and a tail for its extensions.
client.program.coder.accounts.decodereads the fixed part, anddecodePoolExtensionsFromDatareads the tail: reward slots with theLiquidityMiningextension and activation rules withActivationAccess. - A tick array has a header and a list of ticks. The coder decodes the header, and
decodeTicksFromDatadecodes the ticks. Its second argument must match the pool’sLiquidityMiningbit, because pools with rewards use larger ticks. - Each entry of the
tickArraysmap has the shape the purequoteExactIntakes, so the values, in the order ofselection.tickArrays, feed the quote in Quote From Cached Accounts.
- The tick arrays a swap crosses change as the price moves. Re-run
tickArraysForSwapwhen 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
isBitmapExtensionInitializedon the pool tells you. - Read the clock with
readClockwhen you quote, as in the previous example. - Remove subscriptions with
connection.removeAccountChangeListenerusing the id eachonAccountChangecall 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).
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).
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:
PoolErrorwithInvalidParametersfromincreaseOracleLengthfor a length of 0. The program rejects a new length above 500 withInvalidParametersas 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.
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 belowminAmountXorminAmountY.
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 thatsendAndConfirmTransaction 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 readsRPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and, optionally, PRIORITY_MICRO_LAMPORTS (the price of a compute unit in micro-lamports, default 5,000).
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 readsRPC_URL, WALLET_SECRET_KEY, POOL_ADDRESS and POSITION_NFT.
createLookupTabletakes 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, andextendLookupTablefills 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.sendTransactiontakes aVersionedTransaction, which you sign withsign, andgetAddressLookupTablereturns the account thatcompileToV0Messageneeds.
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 throwsPoolError 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).
- 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 aSendTransactionError, 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 theerrof its status or ofgetTransaction(...).meta, andPoolErrorCode[number]gives the name. - An error with no code in the 6000 range comes from another program, such as
custom program error: 0x0from 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.PoolErrorCodehas no name for either.
NotConsumingFullAmountIn is the usual first one: quote again with maxTickArrays raised or a smaller input, and always check quote.amountLeft.
Related
SDK Reference
Every service method and helper, with signers and defaults.
Positions
How slots, shapes, locks and delegates work.

