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 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.- Settles disabled (
config.settlesDisabled): opens, adds, close requests, expiry marks,settlewith every reason (knockoutandlastResortincluded),stamp_book,set_take_profitand the return of an unfilled order or add (void_open,reverse_add) all refuse withSettlesDisabled(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_bookandlastResortrefuse withOracleFrozen(6190), as does the return of an order or add that needs a price. Every othersettle, andset_take_profit, 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 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.
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 pool’s accounts
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, 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.
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
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. 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 asettle 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.