Skip to content

feat(meteora): open a price range wider than one transaction can fund - #696

Merged
cardosofede merged 5 commits into
developmentfrom
feat/meteora-wide-range-positions
Sep 16, 2026
Merged

cardosofede merged 5 commits into
developmentfrom
feat/meteora-wide-range-positions

Conversation

@fengtality

@fengtality fengtality commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Meteora openPosition rejected any price range wider than 69 bins as exceeding what "a Meteora DLMM position holds". That is not the constraint, and the range did not need to be refused.

The 69 is a transaction limit, not a position limit

Read out of the DLMM IDL's own constants:

constant value
POSITION_MAX_LENGTH 1400
DEFAULT_BIN_PER_POSITION 70
MAX_RESIZE_LENGTH 70 (91 on SDK 1.9.x)

A position spans up to 1400 bins. 69 is roughly what the one-shot initializePositionAndAddLiquidityByStrategy path can create and fund inside a single transaction, bounded by DEFAULT_BIN_PER_POSITION and Solana's 10,240-byte CPI allocation limit. So a wide range was being refused for being wide, when the real cost is that it takes more than one transaction.

Worth noting for anyone who reads this expecting position splitting: getPositionCountByBinCount is ceil(binCount / 1400), so initializeMultiplePositionAndAddLiquidityByStrategy returns one position for any realistic range. What grows is the transaction count — the deposit is chunked at DEFAULT_BIN_PER_POSITION bins per transaction by chunkBinRange.

What changed

openPosition sends those transactions instead of rejecting the range. Four consequences that are easy to get wrong, handled explicitly:

  • Amounts are summed over every transaction that funded the position. Reading only the first reports a fraction of what was added.
  • Rent is read from the position account, not the creating transaction — a chunked open grows the position as it funds, so the first transaction's balance is only what it started with.
  • A chunk failing partway leaves the position open holding what landed. That throws an error naming the position, how many chunks funded, and every signature, so the caller can add the rest or close it rather than owning an account it never saw created. This is the real cost of the feature: the open is no longer atomic, and it cannot be, since the deposit does not fit in one transaction.
  • Past POSITION_MAX_LENGTH the SDK returns several positions, which this response cannot describe — it carries one positionAddress. Refused outright rather than returning one and orphaning the rest.

quote-liquidity reports positionCount and transactionCount, so a caller sees what a range costs in transactions before opening rather than learning it from a rejection. Both fields optional, omitted by venues that always open in one transaction.

Note the SDK's quoteCreatePosition().transactionCount counts deposit chunks only — ceil(bins / DEFAULT_BIN_PER_POSITION) — and says nothing about creating the position, which openPosition sends as its own transaction whenever the range is chunked. Passing it through unchanged quoted a wide range one transaction short of what the open sends (a 200-bin range said 3, sent 4), so the create is added back here. Below the chunking threshold the position is created and funded together and the SDK's count is already right. Tested against the open's own arithmetic at the threshold, one bin past it, and at 200 bins.

add-liquidity now chunks too. Every other route on the position lifecycle already looped over a Transaction[] — remove-liquidity, close-position and collect-fees all do. add-liquidity was the exception, calling the non-chunkable addLiquidityByStrategy. That was survivable while opens were capped at 69 bins; once wide positions became creatable it was a hole in the lifecycle, and the partial-failure error above tells the caller to "add the rest with add-liquidity" — advice into a route that could not do it.

One constant for the position width. The connector enforced two: openPosition compared the inclusive width against a local 69, while Meteora.getPriceToBinIds compared maxBinId - minBinId against MAX_BINS = 70, so they disagreed by two bins and neither named the other. MAX_BINS was also deciding how many bins either side of the active bin pool-info reports — a question about how much of the book to show, sharing a constant with the position rule only by coincidence, so a change to either meaning would have silently moved the other. Split into MAX_POSITION_BIN_WIDTH and POOL_LIQUIDITY_BIN_RANGE.

Corrected the position-width messages in the Solana error parser. They said a DLMM position "holds at most 69 bins" in two places — false, per the constants above. The realloc message likewise blamed the range for being wide when the fault is growing the account that far in a single instruction, and advised narrowing the range or splitting into positions, neither of which is needed now that the deposit chunks.

The single-transaction width is 70, measured

The previous cap of 69 came from a mainnet simulation (7dd9bf1e) that verified 69 worked but never tested 70, and described the program as capping positions "at ~70 bins" — so 69 was the value known to work, not a measured ceiling. It also sat one below DEFAULT_BIN_PER_POSITION, which the SDK's own chunker passes to initializePosition verbatim.

Rather than reason about it further, simulated it against the Meteora SOL/USDC pool (2sf5NYcY...) on mainnet:

width initializePosition combined create + fund
68 OK —
69 OK OK
70 OK OK (819 message bytes)
71 InvalidPositionWidth InvalidPositionWidth
72, 80 InvalidPositionWidth —

InvalidPositionWidth is 6040 / 0x1798, thrown at programs/lb_clmm/src/instructions/position/common.rs:30. The combined path's message is 819 bytes at width 70 against a 1232-byte limit, so neither transaction size nor the 10,240-byte CPI allocation limit binds before the program's own check does.

So the cap is exactly DEFAULT_BIN_PER_POSITION, and 69 was leaving a bin on the table — a 70-bin range was being funded over three transactions when it fits in one.

One consequence worth naming, since it otherwise reads as an off-by-one: the threshold and the deposit chunk size are now the same number. A 71-bin range is one past what a single transaction holds but needs two chunks of 70 to cover, so the narrowest chunked open is three transactions, not two. There is no one-chunk chunked open.

A note for anyone building on this

openPosition is no longer atomic for a wide range, and cannot be — the deposit does not fit in one transaction. Callers that assumed open-position either fully succeeded or did nothing need to treat the partial-failure error as "the position may exist" rather than "nothing happened"; retrying blindly opens a second position.

This also does not compose with a build/sign/submit flow that returns a single unsigned transaction, where the capture would fire on the position creation and hand back a transaction that opens an empty position with no error. There is no such flow in development today, so nothing here is affected, but it is the obvious trap for anyone adding one: the fix is to refuse a chunked range at the build boundary rather than return its first transaction, since an open cannot be resumed by repeating the call — each call mints a fresh position keypair.

Verification

pnpm build, pnpm typecheck and pnpm lint clean. Full suite 161 suites / 1460 tests passing (development baseline: 159/1452). openapi.json regenerated and matching, so the CI spec check passes — the two new optional fields are the only change.

New tests pin narrow-range-unchanged, wide-range chunking, amounts summed across transactions, rent read from the account, the partial-chunk failure message, and the >1400-bin refusal.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx

fengtality and others added 3 commits September 8, 2026 20:23
The connector enforced two different position-width limits. openPosition
compared the inclusive width against a local 69, while Meteora.getPriceToBinIds
compared `maxBinId - minBinId` against MAX_BINS = 70 — so the two disagreed by
two bins on what a position could hold, and neither named the other.

MAX_BINS was also doing a second, unrelated job: getPoolLiquidity used it to
decide how many bins either side of the active bin pool-info reports. That is a
question about how much of the book to show, not about what a position can hold;
the two shared a constant only by coincidence, so a change to either meaning
would have silently moved the other.

Split them. MAX_POSITION_BIN_WIDTH is the width rule, now public and used by both
sites, and POOL_LIQUIDITY_BIN_RANGE is the display range, keeping its current
value of 69 either side. The comment on the old constant claimed 70 aligned with
DEFAULT_BIN_PER_POSITION for single-transaction operations, which is the right
idea attached to the wrong number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx
openPosition rejected any range over 69 bins as exceeding what "a Meteora DLMM
position holds". That is not the constraint. The program's POSITION_MAX_LENGTH is
1400 bins; 69 is what the one-shot
initializePositionAndAddLiquidityByStrategy path can create and fund inside a
single transaction, bounded by DEFAULT_BIN_PER_POSITION and Solana's 10,240-byte
CPI allocation limit. So a wide range was refused for being wide, when the real
cost is only that it takes more than one transaction.

Send those transactions. Above the single-transaction width the route now builds
the work with initializeMultiplePositionAndAddLiquidityByStrategy, which sizes it
from the same strategy and returns the position init plus the deposit split into
chunks of DEFAULT_BIN_PER_POSITION bins. Note this is still ONE position — the
SDK splits into several only past POSITION_MAX_LENGTH — so what grows is the
transaction count, not the number of position NFTs.

Consequences that are easy to get wrong, so they are handled explicitly:

- Amounts are summed over every transaction that funded the position. Reading
  only the first, as the single-transaction path could, reports a fraction of
  what was added.
- Rent is read from the position account rather than from the creating
  transaction. A chunked open grows the position as it funds each chunk, so the
  balance in the first transaction is only what it started with.
- A chunk failing partway leaves the position open holding what landed. That
  throws an error naming the position, how many chunks funded, and every
  signature, so the caller can add the rest or close it — rather than being left
  owning an account it never saw created. This is the real cost of the feature:
  the open is no longer atomic, and it cannot be, since the deposit does not fit
  in one transaction.
- Past POSITION_MAX_LENGTH the SDK returns several positions, which this
  response cannot describe — it carries one position address. That is refused
  outright rather than returning one position and orphaning the rest.

quote-liquidity now reports positionCount and transactionCount from the SDK's own
quoteCreatePosition, sized from the same strategy, so a caller sees what a range
will cost in transactions before opening rather than learning it from a
rejection. Both fields are optional and omitted by venues that always open in one
transaction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx
…ction

Every route on a position's lifecycle already sent whatever the SDK handed
back — remove-liquidity, close-position and collect-fees all loop over a
Transaction[]. add-liquidity was the exception: it called the non-chunkable
addLiquidityByStrategy, which returns a single transaction and assumes the
position's range fits in one.

That was survivable while openPosition refused anything over ~69 bins. Now that
it opens wider positions, it is a hole in the lifecycle — and the error this
route's sibling raises on a partially funded open tells the caller to "add the
rest with add-liquidity", which would have been advice into a route that could
not do it. Switch to addLiquidityByStrategyChunkable and send the chunks, with
the amounts summed over them and a partial failure naming what landed, matching
what openPosition does.

Also correct the position-width messages in the Solana error parser. They said a
DLMM position "holds at most 69 bins" — that is not the constraint. A position
spans up to POSITION_MAX_LENGTH (1400) bins; 69 is roughly what one transaction
can create and fund, which is a limit on the deposit, not on the position. The
realloc message likewise blamed the range for being wide when the fault is
growing the account that far in a single instruction, and advised narrowing the
range or splitting into positions, neither of which is needed now that the
deposit is chunked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx
@greptile-apps

greptile-apps Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge because no eligible blocking failure or outstanding prior finding remains.

Summary

  • Adds chunked wide-range position opening and chunked add-liquidity execution.
  • Aggregates fees and deposited amounts across submitted transactions and reports partial failures with the affected position and completed signatures.
  • Exposes estimated position and transaction counts in liquidity quotes.
  • Separates the position-width and pool-liquidity display constants and updates Solana error descriptions.

Diagram

sequenceDiagram
    participant Caller
    participant Gateway
    participant MeteoraSDK
    participant Solana
    Caller->>Gateway: Quote wide price range
    Gateway->>MeteoraSDK: quoteCreatePosition(range)
    MeteoraSDK-->>Gateway: positionCount, deposit transactionCount
    Gateway-->>Caller: quote + total transactionCount
    Caller->>Gateway: openPosition(range, amounts)
    Gateway->>MeteoraSDK: Build position and chunk instructions
    MeteoraSDK-->>Gateway: Init instructions + liquidity chunks
    Gateway->>Solana: Create position
    loop Each liquidity chunk
        Gateway->>Solana: Add liquidity
    end
    Gateway-->>Caller: Position address, first signature, aggregate amounts and fees
Loading

fengtality and others added 2 commits September 8, 2026 21:04
quote-liquidity passed the SDK's transactionCount straight through, and that
number counts deposit chunks only — ceil(bins / DEFAULT_BIN_PER_POSITION). It
says nothing about creating the position, which openPosition sends as its own
transaction whenever the range is chunked, because a full chunk is already sized
to fill a transaction and folding the position init in alongside it risks
overflowing one.

So a wide range was quoted one transaction short of what the open would send: a
200-bin range reported 3 and sent 4. The whole point of the field is to let a
caller see the cost before opening, and it is the sort of off-by-one nobody
notices until they are reconciling signatures.

Add the create when the range is chunked. Below the threshold the position is
created and funded together and the SDK's count is already right.

Tests pin the number against the open's own arithmetic at the threshold, one bin
past it, and for a wide range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx
…hain

The connector capped a one-transaction open at 69 bins. That number came from a
mainnet simulation (7dd9bf1) which verified 69 worked but never tested 70, and
described the program as capping positions "at ~70 bins" — so 69 was the value
known to work, not a measured ceiling. It also sat one below
DEFAULT_BIN_PER_POSITION, which the SDK's own chunker passes to
initializePosition verbatim, and which would be a strange constant for the
program to reject.

Simulated it against the Meteora SOL/USDC pool on mainnet rather than reasoning
about it further. initializePosition succeeds at 68, 69 and 70 bins and fails at
71, 72 and 80 with InvalidPositionWidth (6040 / 0x1798, thrown at
programs/lb_clmm/src/instructions/position/common.rs:30). The combined
create-and-fund path behaves identically — succeeds at 70, fails at 71 — and its
message is 819 bytes at width 70 against a 1232-byte limit, so neither
transaction size nor the 10,240-byte CPI allocation limit binds before the
program's own check does.

So the cap is exactly DEFAULT_BIN_PER_POSITION, and 69 was leaving a bin on the
table: a 70-bin range was being funded over three transactions when it fits in
one.

One consequence worth naming, since it reads as an off-by-one otherwise: the
threshold and the deposit chunk size are now the same number. A range of 71 bins
is one past what a single transaction holds but needs two chunks of 70 to cover,
so the narrowest chunked open is three transactions, not two. There is no
one-chunk chunked open.

Parser messages updated from "about 69" to 70 now that it is measured rather
than approximate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvS83dMFobrZtpcW1LRpWx
@rapcmia rapcmia moved this to Under Review in Pull Request Board Sep 14, 2026
@rapcmia rapcmia added this to the v2.17 milestone Sep 14, 2026
@rapcmia rapcmia self-assigned this Sep 14, 2026
@rapcmia

rapcmia commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

e4bdb81

  • Review PR and compare 69bin behavior with dev branch

Meteora quote transaction counts ✅

#### get the live SOL-USDC price used for the 3 USD SOL amount
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/pool-info?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3' | jq '{address, price, binStep, activeBinId}'
{
  "address": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3",
  "price": "100.40669134509386",
  "binStep": 100,
  "activeBinId": -231
}

#### quote the 68-bin SOL-USDC range
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/quote-liquidity?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3&lowerPrice=72.303167140576&upperPrice=140.828114538060&baseTokenAmount=0.029878486780&quoteTokenAmount=3' | jq '{baseTokenAmount, quoteTokenAmount, positionCount, transactionCount}'
{
  "baseTokenAmount": "0.026102019",
  "quoteTokenAmount": "3",
  "positionCount": 1,
  "transactionCount": 1
}

#### quote the 69-bin SOL-USDC range
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/quote-liquidity?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3&lowerPrice=71.587294198591&upperPrice=140.828114538060&baseTokenAmount=0.029878486780&quoteTokenAmount=3' | jq '{baseTokenAmount, quoteTokenAmount, positionCount, transactionCount}'
{
  "baseTokenAmount": "0.026102019",
  "quoteTokenAmount": "3",
  "positionCount": 1,
  "transactionCount": 1
}

#### show a rounded boundary value using the chunked path
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/quote-liquidity?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3&lowerPrice=70.878509107515&upperPrice=140.828114538060&baseTokenAmount=0.029878486780&quoteTokenAmount=3' | jq '{baseTokenAmount, quoteTokenAmount, positionCount, transactionCount}'
{
  "baseTokenAmount": "0.024651908",
  "quoteTokenAmount": "3",
  "positionCount": 1,
  "transactionCount": 3
}

#### quote the actual 70-bin SOL-USDC range
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/quote-liquidity?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3&lowerPrice=70.87851&upperPrice=140.8281145380602&baseTokenAmount=0.029878486780&quoteTokenAmount=3' | jq '{baseTokenAmount, quoteTokenAmount, positionCount, transactionCount}'
{
  "baseTokenAmount": "0.025358967",
  "quoteTokenAmount": "3",
  "positionCount": 1,
  "transactionCount": 1
}

#### quote the 71-bin SOL-USDC range
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/quote-liquidity?connector=meteora&chainNetwork=solana-mainnet-beta&poolAddress=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3&lowerPrice=70.176741690609&upperPrice=140.828114538060&baseTokenAmount=0.029878486780&quoteTokenAmount=3' | jq '{baseTokenAmount, quoteTokenAmount, positionCount, transactionCount}'
{
  "baseTokenAmount": "0.023985641",
  "quoteTokenAmount": "3",
  "positionCount": 1,
  "transactionCount": 3
}
  • Gateway source was tested against the Meteora SOL-USDC pool with 3 USD worth of SOL and 3 USDC. Each quote returned one position.
  • The 68-bin, 69-bin, and actual 70-bin inputs returned one transaction.
  • The 71-bin input returned three transactions, confirming that the new multi-transaction path starts above the 70-bin limit.

Wide Meteora position lifecycle ✅

#### open the live wide SOL-USDC position
curl -sS --max-time 90 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/open' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "poolAddress": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3",
  "lowerPrice": 70.878509107515,
  "upperPrice": 140.828114538060,
  "baseTokenAmount": 0.029878486780,
  "quoteTokenAmount": 3,
  "strategyType": 0
}' | jq
{
  "signature": "41iHU3zeDUFQ9EtoxpcxdDK3faa6V2z6rtCeShbzFVR2EbWzrSPPvWsrsFzje2KRcimrtFCsjo3ut1H2ngt4PFwE",
  "status": 1,
  "data": {
    "positionAddress": "C7YnSRWD6Ji6NXS8EhBUgrvV1EuHWQofzco8LY4LHU7t",
    "baseTokenAmountAdded": "0.029878212",
    "quoteTokenAmountAdded": "2.9999969999999996"
  }
}

#### confirm the funded wide position belongs to the wallet
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/positions-owned?connector=meteora&chainNetwork=solana-mainnet-beta&walletAddress=AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2' | jq '.[] | select(.address == "C7YnSRWD6Ji6NXS8EhBUgrvV1EuHWQofzco8LY4LHU7t") | {address, lowerBinId, upperBinId, baseTokenAmount, quoteTokenAmount}'
{
  "address": "C7YnSRWD6Ji6NXS8EhBUgrvV1EuHWQofzco8LY4LHU7t",
  "lowerBinId": -267,
  "upperBinId": -197,
  "baseTokenAmount": "0.030682615",
  "quoteTokenAmount": "2.919159"
}

#### close the live wide SOL-USDC position after 60 seconds
curl -sS --max-time 90 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/close' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "positionAddress": "C7YnSRWD6Ji6NXS8EhBUgrvV1EuHWQofzco8LY4LHU7t"
}' | jq
{
  "signature": "3k1ZHpi6N9jGJw7eZwfX3t3Jm4q7UQayW71Bg9eLjACUsgm29RLiULHG2a8xC6Tcqxb23sZ5YBkiMVDgjhb8CLzE",
  "status": 1,
  "data": {
    "positionAddress": "C7YnSRWD6Ji6NXS8EhBUgrvV1EuHWQofzco8LY4LHU7t",
    "positionRentRefunded": "0.0424688"
  }
}

#### confirm no Meteora position remains for the wallet
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/positions-owned?connector=meteora&chainNetwork=solana-mainnet-beta&walletAddress=AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2' | jq
[]

#### Gateway log proof for the chunked open and close
2026-09-15 00:29:41 | info | Bin IDs: min=-267, max=-197, active=-231, width=71
2026-09-15 00:29:43 | info | Create position transaction `41iHU3...ngt4PFwE` sent; confirmed at 00:29:45.
2026-09-15 00:29:46 | info | Add liquidity 1/2 transaction `5i535d...F2oFzSo` sent; confirmed at 00:29:48.
2026-09-15 00:29:50 | info | Add liquidity 2/2 transaction `2YUr1L...u4AktU` sent; confirmed at 00:29:53.
2026-09-15 00:29:55 | info | Position opened over 3 transaction(s): 0.0299 SOL, 3.0000 USDC, rent: 0.042469 SOL.
2026-09-15 00:31:19 | info | Close transaction 1/2 `4gZquu...TiKAUkpH` sent; confirmed at 00:31:21.
2026-09-15 00:31:22 | info | Close transaction 2/2 `3k1ZHp...hb8CLzE` sent; confirmed at 00:31:24.
2026-09-15 00:31:25 | info | Position closed successfully; 0.042469 SOL rent refunded.
2026-09-15 00:31:36 | info | Found 0 pools with positions for the wallet.
  • Opened a 71-bin SOL-USDC position. Gateway created one position using three confirmed transactions.
  • Confirmed the position was funded and visible.
  • Closed the position using two confirmed transactions. No position remained afterward.
  • Gateway briefly logged a position lookup error during close, but the close completed successfully.

Add liquidity to a wide Meteora position ✅

#### create the 80-bin SOL-USDC position
curl -sS --max-time 90 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/open' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "poolAddress": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3",
  "lowerPrice": 67.43847940,
  "upperPrice": 148.0117637161349,
  "baseTokenAmount": 0.029878486780,
  "quoteTokenAmount": 3
}' | jq
{"status":1,"data":{"positionAddress":"HgTbrVpPRKTMNmConhv6ygqPUYxixiW7PPukm4XfFV3W","baseTokenAmountAdded":"0.029878216","quoteTokenAmountAdded":"2.99997"}}

#### add the same liquidity to the wide position
curl -sS --max-time 90 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/add' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "positionAddress": "HgTbrVpPRKTMNmConhv6ygqPUYxixiW7PPukm4XfFV3W",
  "baseTokenAmount": 0.029878486780,
  "quoteTokenAmount": 3
}' | jq
{"status":1,"data":{"baseTokenAmountAdded":"0.029776417","quoteTokenAmountAdded":"2.99997"}}

#### confirm the added liquidity and current pool state
curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/position-info?connector=meteora&chainNetwork=solana-mainnet-beta&positionAddress=HgTbrVpPRKTMNmConhv6ygqPUYxixiW7PPukm4XfFV3W' | jq '{lowerBinId, upperBinId, baseTokenAmount, quoteTokenAmount}'
{"lowerBinId":-271,"upperBinId":-192,"baseTokenAmount":"0.059756945","quoteTokenAmount":"5.999844"}

#### close the wide position and confirm it is removed
curl -sS --max-time 90 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/close' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "positionAddress": "HgTbrVpPRKTMNmConhv6ygqPUYxixiW7PPukm4XfFV3W"
}' | jq
{"status":1,"data":{"positionRentRefunded":"0.04758944"}}

curl -sS --max-time 30 -H 'Authorization: Bearer XXX' 'http://localhost:15888/trading/clmm/positions-owned?connector=meteora&chainNetwork=solana-mainnet-beta&walletAddress=AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2' | jq
[]

#### Gateway log proof for the wide add-liquidity lifecycle
2026-09-15 00:43:23 — Create position transaction `48Rupk...UyH9Qhx` sent; confirmed at 2026-09-15 00:43:25.
2026-09-15 00:43:27 — Open funding chunk 1/2 `iw7iqX...LWquvh7` sent; confirmed at 2026-09-15 00:43:28.
2026-09-15 00:43:30 — Open funding chunk 2/2 `2jJbu4...qTfPXQB` sent; confirmed at 2026-09-15 00:43:32.
2026-09-15 00:43:33 — Gateway recorded the 80-bin position as opened over 3 transactions with 0.0299 SOL and 3.0000 USDC.
2026-09-15 00:45:11 — Existing-position add transaction 1/2 `5r17Gg...8BGSQy` sent; confirmed at 2026-09-15 00:45:13.
2026-09-15 00:45:15 — Existing-position add transaction 2/2 `2ahkb2...41t9fJm` sent; confirmed at 2026-09-15 00:45:16.
2026-09-15 00:45:17 — Gateway recorded 0.0298 SOL and 3.0000 USDC added.
2026-09-15 00:45:24 — Position-info route completed; pool-info followed at 2026-09-15 00:45:25.
2026-09-15 00:45:40 — Close transaction 1/2 `5nQqhJ...W2xNPG` sent; confirmed at 2026-09-15 00:45:43.
2026-09-15 00:45:46 — Close transaction 2/2 `3WDKTG...Z8ERCu` sent; confirmed at 2026-09-15 00:45:47.
2026-09-15 00:45:48 — Gateway recorded the position as closed; the owned-position lookup found no positions.
  • Opened one 80-bin SOL-USDC position with 3 USD of SOL and 3 USDC. Gateway completed the wide open in three transactions.
  • Added the same amount to that position. Gateway confirmed the add operation and position info showed approximately 0.0598 SOL and 5.9998 USDC.
  • Pool info remained available with the same active bin. The position was then closed and no owned position remained.
  • Gateway logs confirm the wide open, successful add, position and pool checks, and successful close.

Observation - wide Meteora partial-failure recovery and balance impact 👀

#### attempt the 500-bin SOL-USDC open
curl -sS --max-time 240 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/open' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "poolAddress": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3",
  "lowerPrice": 1.0325966706,
  "upperPrice": 148.0117637161349,
  "baseTokenAmount": 0.029878486780,
  "quoteTokenAmount": 3
}' | jq
{"statusCode":500,"error":"HttpError","message":"Position GKhsEaRopsTfddtzVpmCHrthR9mYaRXmsYJH6D2nrrkA was opened, but only 2 of 8 liquidity chunks were funded before Insufficient funds for transaction."}

#### close the partially funded position
curl -sS --max-time 120 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/trading/clmm/close' -d '
{
  "connector": "meteora",
  "chainNetwork": "solana-mainnet-beta",
  "walletAddress": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "positionAddress": "GKhsEaRopsTfddtzVpmCHrthR9mYaRXmsYJH6D2nrrkA"
}' | jq
{"status":1,"data":{"positionRentRefunded":"0.08172704"}}

#### compare balances before and after the partial-failure recovery
Before: {"SOL":0.285181851,"USDC":16.315502}

curl -sS --max-time 30 -X POST -H 'Authorization: Bearer XXX' -H 'Content-Type: application/json' 'http://localhost:15888/chains/solana/balances' -d '
{
  "network": "mainnet-beta",
  "address": "AbCpsXy2HAC5yWe3YwfysYk4x4FSd13WduKkCooREZK2",
  "tokens": ["SOL", "USDC"]
}' | jq
{"balances":{"SOL":0.128543032,"USDC":16.315502}}

#### Gateway log proof for the partial-failure recovery
2026-09-15 01:05:25 — Position-creation transaction `RsGpzx...Xuw6joA` confirmed; Gateway began funding chunk 1/8.
2026-09-15 01:05:34 — Gateway recorded chunk 3/8 as failed for insufficient funds after two liquidity chunks had landed. The error returned the position address and three landed signatures.
2026-09-15 01:05:50 — Gateway began closing the partially funded position.
2026-09-15 01:05:56 — Close transaction `5GKQHH...ndpBPAh` confirmed and Gateway recorded the position as closed.
2026-09-15 01:05:57 — Gateway recorded 0.4555 USDC removed and 0.081727 SOL rent refunded.
  • A 500-bin SOL-USDC quote with 3 USD on each side predicted one position and nine transactions.
  • The wallet held 0.285181851 SOL and 16.315502 USDC before opening. Gateway created the position and funded two of eight liquidity chunks before a later transaction failed for insufficient SOL.
  • Gateway returned the created position address and the three confirmed signatures. The partial position was closed successfully, its 0.08172704 SOL rent was refunded, and no owned position remained.
  • Wallet balance before the test: 0.285181851 SOL and 16.315502 USDC. Balance after close: 0.128543032 SOL and 16.315502 USDC.
  • Net observed change: -0.156638819 SOL and 0 USDC. At the observed SOL-USDC price of 100.40669134509386, this is approximately $15.73 in SOL.
  • This SOL reduction is larger than the route-reported transaction fees, so the exact cause still needs reconciliation.

User note: Before opening a wide position, keep extra SOL beyond the liquidity amount for position rent and every planned transaction. If Gateway reports a partial open, do not retry open; use the returned position address to add the remaining liquidity or close the position first.

@cardosofede
cardosofede merged commit 0c3bb60 into development Sep 16, 2026
6 checks passed
@cardosofede
cardosofede deleted the feat/meteora-wide-range-positions branch September 16, 2026 14:33
@rapcmia rapcmia moved this from Under Review to Development 2.17.0 in Pull Request Board Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development 2.18.0

Development

Successfully merging this pull request may close these issues.

3 participants