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, the funding reason by the price and the funding owed, 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. Anyone may submit a close, and nobody is paid for it.
On a feed-kind-3 market (isKind3Market(market)) the venue’s keepers settle. Its priced instructions carry a signed tick and the market’s own accounts, not a quote from the price service. The flow on this page and submitBenchmarkQuoted apply to a kind-2 market. A client of a kind-3 market requests the close (requestCloseFor) and reads SettlementPriced.

The close reasons

No reason pays its submitter anything. borrowSpent closes a position whose funding has used up its payout. The trader receives 0, as for a knockout. It cannot occur while the funding rate is 0.
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 pays 0, and one stamped as having reached its rung is paid that level with the exit fee, as for a take-profit. 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).
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.
  • Settles disabled (config.settlesDisabled): opens, adds, close requests, expiry marks, settle with every reason (knockout and lastResort included), stamp_book, set_take_profit and the return of an unfilled order or add (void_open, reverse_add) all refuse with SettlesDisabled (6191). Poll the flag rather than retrying on a timer, then send the same instruction once it reads false.
  • Oracle frozen (config.oracleFrozen): opens, adds, close requests, expiry marks, stamp_book and lastResort refuse with OracleFrozen (6190), as does the return of an order or add that needs a price. Every other settle, and set_take_profit, 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 six 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 position that froze fee terms at open (feeTermsSlot(position.flags) != 0) is priced with the entry at that slot of its market’s fee-terms account: the share of the gain the trader keeps, the fee rate at the exit move, and the fee base. Pass that account, decoded, as feeTermsTable. Without it, every close of such a position that pays a gain or a fee reads FeeTermsTableRequired, the refusal the program gives a settle that leaves the account out. A close at the barrier is priced either way.
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. A close that is not a knockout pays in the order set out under what a close pays: a loss tax on a loss comes before the exit fee.

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 pool’s accounts

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, true) appends them, in the order the program reads them, after every other remaining account. No lookup table carries them: they ride as static keys, 32 bytes each, so measure the signed transaction. 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 compares the pool delta with the legs, so a fee miscounted or a rounding remainder dropped 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 includes funding. settlePreview, settle’s breakdown and reconstructTrade charge it from the position’s frozen rate (borrowRateBpsPerHour), after the exit fee. The funding rate is 0, so nothing is charged.

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. A long-lived process (a keeper, a server) can pass push: connectionSignaturePush(conn): a signatureSubscribe at confirmed wakes the poll when the cluster confirms. The poll still reads the status before it returns, so a silent or wrong channel costs latency and never a wrong answer. 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. A transaction that @solana/web3.js cannot parse is read by one raw call, so an unrelated 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 settles and stamps. Send one yourself only to close your own position faster than they do. The builders on 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.