Skip to main content
Every close except a voluntary one is provable by public information: the two price reasons by the price, the two expiries by the clock, and the no-price backstop by the clock and the absence of a price. So anyone can submit them. A voluntary close starts with the owner’s request_close, and the settle that completes it is open to anyone too: see request or mark, then settle. Submitting one pays you nothing. No fee, no share of the settlement, no gas rebate: budget the SOL as a cost. Removing a reward is not removing a right: the guarantee is that a close cannot be blocked, not that it earns.

The six reasons

No reason pays its submitter anything.
voluntary is the one reason that needs the position owner’s authority: the owner’s signature, or a session key the owner authorised. That authority signs only the request, which carries no quote, so a wallet can sign it and the Close button is one attempt. The settle that completes the request is sent by anyone with the window quote, and the venue’s keepers do.

The no-price close

lastResort exists for a sustained oracle outage. It carries no quote and settles a live position at its own entry price, with the exit fee waived: the trader gets the stake back. A position already stamped as knocked out or as having reached its rung is paid that stamped level instead. It is legal only when all of these hold:
  • either the market is dark: if a market has had no posted price for 24 hours (the governed dark_after_min, read as GovTable.darkAfterMin), anyone can close any position on it, and the position gets its stake back. Anyone can post a signed price with provePrice to reset the clock. Or the position’s own frozen tenor plus the grace in config.lastResortGraceS has elapsed since it opened, which only a position with a finite tenor can reach. A position with the indefinite tenor has only the dark arm. Earlier, it is refused LastResortNotYetDue (6192);
  • no fresh price is available for the market. With one available it is refused LastResortPriceAvailable (6193): settle at market with a priced reason instead;
  • the oracle is not frozen (OracleFrozen, 6190);
  • the position is not an unfilled order (EntryNotFilled, 6203) and no add waits for its price (AddPending, 6211).
On a market priced by a signed quote, the lastResort transaction carries neither the ed25519 instruction nor the Instructions sysvar: it is settle with reason 5. The program asks the dark arm only when the governance table is among the remaining accounts, so a refund that omits it can never fire on the indefinite tenor (LastResortNotYetDue). Build it with the table, the market’s fee-terms account and the pool’s accounts. On a market with trading hours, a position that is not settled before its session’s close ends at par.

Settles can be stopped

Two switches can stop closes on the venue. Only the config authority clears either one.
  • Settles disabled (config.settlesDisabled): settle refuses every reason, knockout and lastResort included, with SettlesDisabled (6191). stamp_book and set_take_profit refuse too. Poll the flag rather than retrying on a timer, then send the same settle once it reads false.
  • Oracle frozen (config.oracleFrozen): opens and adds refuse with OracleFrozen (6190), and so does lastResort. Every other settle reads no price and fails NoFreshPrice (6007). Read the flag before you retry a NoFreshPrice that repeats.
settleGates does not model either switch. Read both flags off the config before you send.

The minimum hold

voluntary, takeProfit and userExpiry require the position to have lived at least the governed minimum hold, in slots, since openedSlot: resolveMinHoldSlots(config.minHoldSlots). Inside it they are refused MinimumHoldNotMet (6103). knockout and lastResort are never gated this way. tenorExpiry is gated only on a market with a session calendar. A market can add its own, longer hold (market.closeHoldSlots; 0 is none), and the larger of the two is in force. It gates a close request at the slot the request lands and a take-profit at the slot of the price that proves it, and returns MarketHoldNotMet (6236) when only the market’s hold is not met. It never gates a mark or a knockout. The hold counts from the entry price, and after a filled add from the add’s price. If your interface offers a close button the instant a position opens, it will revert. So will a keeper closing on a user timer just after an add, because a filled add re-stamps openedSlot. settlePreview takes no slot argument and does not model the hold: closableFromSlotOf(position, config, market) gives the first slot at which a close request can land.

1. Ask which reason applies

Do not guess. settlePreview prices the five priced reasons at one price and one instant and names the one a keeper would submit. It does not model lastResort: decide that one from the clock and from whether you can obtain a price at all.
A knockout settles at the barrier however far past it the price ran, so it needs no exit price to be priced, and a slow submitter cannot make it worse.

2. Check the gates the engine cannot see

settlePreview models the reason’s legality, because that is engine arithmetic. It does not model the refusals either side of the engine call: the market pin, the price, the entry exponent and the solvency guard. settleGates does.
Always pass reason. The type allows leaving it out, but without it the report can print ok on every row for a transaction the chain will refuse.

3. Build the close

owner_token is required and it is the only account a settlement pays. It must be the book’s recorded payoutDestination; a settle naming any other account escrows the payout on the book rather than paying it, and so does one whose destination has been closed or frozen. The trader then claims it with claimPayout; see reading positions. WrongPayoutDestination (6150) is reserved and raised by nothing. Do not branch on it.

Send it with its quote

A priced settle carries its quote: the quote’s ed25519 instruction immediately before settle, the compute-budget pair after. submitBenchmarkQuoted takes the quote as it arrives (its assembled ed25519 instruction data and the ed25519 program id) and what the signed payload decodes to (quote). It evaluates the protocol gates you hand it first, then composes, signs, measures the bytes, checks the distinct-signer count against market.minSignatures, and simulates. Every check is a Gate, so a failure still prints a table that explains itself. To obtain the quote, see Getting prices.
dryRun: true is the posture to default to. It builds and signs exactly what would be sent, measures the real byte count, simulates with signature verification on, and stops. It is safe against mainnet. A lastResort settle has no quote to carry: compose settle and the compute-budget pair, and simulate before you send.

Request or mark, then settle

Three reasons are two steps. Step 1 writes the exit target, d slots after it lands (CloseRequested.exitTarget, ExpiryMarked.exitTarget). Step 2 is the settle above with the recorded reason and the window quote: the first price of the venue’s price feed at or after exitTarget + K × W, where K is the count of whole windows of W slots since the target. Only that quote prices it, so a settler chooses nothing. A settle before the target returns ExitQuoteNotYetDue (6216), another quote NotWindowQuote (6217), another reason ExitReasonMismatch (6215), and an expiry with no mark ExitNotAnchored (6214). A request cannot be withdrawn, and a second one returns ExitPending (6213): treat it as success. knockout and takeProfit stay one step. Their price can be up to W_ko slots old, and after a request or a mark only a price that is not later than the exit price proves a level. Orders and closes has the timing settings and the whole contract.

The accounts of the liquidity pool

A settle needs CapitalState and the queue account, both writable, as trailing remaining accounts found by address; without them it is refused. The same accounts ride on a few other instructions: withCapitalRiders(built, kind, active) appends them, in the order the program reads them, after every other remaining account; capitalActive(vaultBytes, capitalStateBytes) says whether they are needed. Build with the SDK, and use its shared lookup table, which carries them. SettlementPriced.trader is the amount paid now. When part of a win was queued, SettlementQueued carries the amount owed, the amount paid now and the amount queued, with the line range the queued part occupies; QueuePaid carries each delivery with the same range, so every delivered unit joins exactly one settle.

4. Check the money afterwards

The settlement’s legs must add up. isConserving is a real check, not a tautology: the pool delta is computed independently by each settlement path, so a path that miscounts a fee or drops a rounding remainder makes the two sides disagree.
The inflow is the position’s collateral. The entry fee is not part of it: it is taken at open, from inside the budget, and never reaches settlement. SettlementPriced.trader is what was paid now. A win that was partly queued is owed in full: add the queued amount from SettlementQueued to the trader leg before you compare the legs with the position’s collateral. The SDK does it: readTradeHistory and closedTradesInTransaction pair the SettlementQueued of the same transaction, verify the settlement over the owed legs, and expose the split on each ClosedTrade as paidNow and queued.
The SDK’s settlement mirror does not model the borrow fee. settlePreview, settle’s breakdown and reconstructTrade compute the loss tax, the exit fee and the cap truncation, and none of them has a borrow line.While a position’s own borrowRateBpsPerHour is zero that is exact, because a rate of zero accrues nothing. For a position frozen at a non-zero rate the preview over-states the trader’s payout by whatever the position has accrued, settleGates’ outflow is short by the same amount, and a conservation check built on either disagrees with the chain. Read the position’s rate before you trust a quoted payout.

5. Confirm, then read what the close paid from its own transaction

confirmSignature waits for a signature and returns the chain’s verdict with the landed transaction. closedTradesInTransaction reads the settlement out of that transaction; it is the same function readTradeHistory files each closed trade with, so a close’s receipt and its history row agree field for field.
confirmSignature polls getSignatureStatuses every pollMs. For a long-lived process (a keeper, a server) you can pass push: connectionSignaturePush(conn): a signatureSubscribe at confirmed wakes the poll the moment the cluster confirms, and between hints the poll runs every PUSH_POLL_MS (1 s) and reads the block height at most every 2 s. The hint only wakes the poll; the poll still reads the status before it returns. A channel that is silent or wrong costs up to one second of latency and never a wrong answer, and a channel that closes puts the poll back at pollMs. It is off by default. Do not use it inside a serverless function. The rules this follows:
  • Only confirmed or finalized with err === null is a success. A processed status is not.
  • A receipt with complete: false came from a missing or truncated log. Print no figure from it.
  • A payout is in the owner’s wallet only when ownerDelta shows it. A settle whose destination cannot receive escrows the payout instead, and the transaction then carries PayoutEscrowed.
A position a keeper settled is found the same way, from the newest transactions on the book:
A seq it does not find is absent from the result; readTradeHistory is the full scan. Both reads fetch one transaction per signature, and a landed transaction never changes. If you look more than once (a poll for a close, or the history list beside a receipt), wrap the reader once:
memoReader keeps only a transaction the node returned. A null or a thrown read is asked again, and a before page or a minContextSlot look goes straight to the node. Reads use maxSupportedTransactionVersion: 1; a version-1 transaction that @solana/web3.js cannot parse is read by one raw call, so a stranger’s transaction that names a trader’s account does not stop the scan.

Anyone can settle

Every close ends with a settle that anyone may send, and nobody is paid for sending one. The venue’s keepers send every settle and every stamp, so a position becomes closable and is closed without you. Send one yourself only to close your own position faster than the venue’s keepers do. The builders in this page and submitBenchmarkQuoted are all you need: build settle with the reason the position’s phase names, attach the window quote, simulate, send. A refusal you meet on the way is on error handling.