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 asGovTable.darkAfterMin), anyone can close any position on it, and the position gets its stake back. Anyone can post a signed price withprovePriceto reset the clock. Or the position’s own frozen tenor plus the grace inconfig.lastResortGraceShas 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 refusedLastResortNotYetDue(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).
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):settlerefuses every reason,knockoutandlastResortincluded, withSettlesDisabled(6191).stamp_bookandset_take_profitrefuse 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 withOracleFrozen(6190), and so doeslastResort. Every other settle reads no price and failsNoFreshPrice(6007). Read the flag before you retry aNoFreshPricethat 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.
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.
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 beforesettle, 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 needsCapitalState 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.
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.
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
confirmedorfinalizedwitherr === nullis a success. Aprocessedstatus is not. - A receipt with
complete: falsecame from a missing or truncated log. Print no figure from it. - A payout is in the owner’s wallet only when
ownerDeltashows it. A settle whose destination cannot receive escrows the payout instead, and the transaction then carriesPayoutEscrowed.
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 asettle 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.