> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rekt.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Settling a position

> Choosing the reason, checking the gates the engine cannot see, and building the close. Settlement is permissionless, and it pays its submitter nothing.

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](#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

| Reason | Code | Proved by | A stranger may submit it | Carries a quote |
| - | - | - | - | - |
| `voluntary` | 0 | the owner's request, then the window | yes, once the owner has requested it | yes |
| `knockout` | 1 | the price | yes | yes |
| `takeProfit` | 2 | the price | yes | yes |
| `userExpiry` | 3 | the clock | yes | yes |
| `tenorExpiry` | 4 | the clock | yes | yes |
| `lastResort` | 5 | the clock, and no price being available | yes | **no** |

No reason pays its submitter anything.

```ts theme={"dark"}
import { CLOSE_REASON_BY_CODE, CLOSE_REASON_CODE, isPermissionless } from '@rekt/1000x';

export const codeOf = CLOSE_REASON_CODE; // name -> on-chain discriminant
export const nameOf = CLOSE_REASON_BY_CODE; // discriminant -> name
export const permissionless = isPermissionless; // everything except an unrequested `voluntary`
```

`voluntary` is the one reason that needs the position owner's authority: the owner's signature, or
a [session key](/sdk/sessions) 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](#3-build-the-close) 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.

```ts theme={"dark"}
import { settlePreview, type ConfigView, type PositionView } from '@rekt/1000x';

export function chooseReason(
  position: PositionView,
  config: ConfigView,
  exitPrice: bigint,
  nowUnix: bigint,
  submitter: string,
  owner: string,
) {
  const preview = settlePreview(position, config, exitPrice, nowUnix, submitter, owner);

  // null when nothing is legal yet: that is a wait, not a failure.
  const reason = preview.suggested;
  const row = preview.reasons.find((r) => r.reason === reason) ?? null;

  return { preview, reason, payouts: row?.payouts ?? null, capped: row?.capped ?? false };
}
```

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.

```ts theme={"dark"}
import {
  settleGates,
  type ConfigView,
  type GateReport,
  type MarketView,
  type Payouts,
  type PositionView,
  type QuoteView,
  type SettlePreview,
} from '@rekt/1000x';

/**
 * What LEAVES the vault when this position closes: the quantity the program's solvency guard
 * compares against the position's reserve. `poolDelta` stays inside the vault. `p.liquidator` reads
 * 0n on every settlement; keep it in the sum so this is the program's arithmetic, and never render
 * it as money a submitter receives.
 */
const totalOutflow = (p: Payouts): bigint => p.trader + p.liquidator + p.insurance + p.protocol;

export function gatesForSettle(args: {
  config: ConfigView;
  market: MarketView;
  marketAddress: string;
  position: PositionView;
  quotes: QuoteView[];
  preview: SettlePreview;
  reason: string;
  ownerIsSubmitter: boolean;
}): GateReport {
  const row = args.preview.reasons.find((r) => r.reason === args.reason) ?? null;

  return settleGates({
    config: args.config,
    market: args.market,
    // The market the SUBMITTER intends to use, passed deliberately so a caller that has two
    // markets confused is caught by the first gate rather than by the chain.
    marketAddress: args.marketAddress,
    position: args.position,
    quotes: args.quotes,
    outflow: row?.payouts ? totalOutflow(row.payouts) : null,
    // Always false: no settlement pays a submitter.
    liquidatorPaid: false,
    liquidatorTokenNamed: false,
    ownerIsSubmitter: args.ownerIsSubmitter,
    // Pass the reason. Omit it and you get no row for the one thing most likely to be wrong.
    reason: {
      name: args.reason,
      legal: row?.legal ?? false,
      why: row?.why ?? 'the engine names no such reason',
    },
  });
}
```

<Warning>
  **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.
</Warning>

## 3. Build the close

```ts theme={"dark"}
import { PublicKey } from '@solana/web3.js';
import {
  CLOSE_REASON_CODE,
  assetId,
  configPda,
  ix,
  lastResortTableRemaining,
  marketPda,
  priceAccountsFor,
  toHex,
  traderPda,
  vaultPda,
  withCapitalRiders,
  withFeeTermsTable,
  type BuiltInstruction,
  type CloseReasonName,
  type ConfigView,
  type MarketView,
  type TraderAccountView,
} from '@rekt/1000x';

export function buildSettle(args: {
  /** The decoded config, read for this request. */
  config: ConfigView;
  symbol: string;
  market: MarketView;
  /** True when the pool is on: `capitalActive(vaultBytes, capitalStateBytes)`. */
  poolOn: boolean;
  submitter: PublicKey;
  owner: PublicKey;
  /** The owner's book, decoded: the payout destination is read from it. */
  book: TraderAccountView;
  seq: bigint;
  reason: CloseReasonName;
  protocolToken: PublicKey;
  vaultToken: PublicKey;
  /** The session key's `TradeDelegate` PDA, or null when the OWNER is closing. */
  delegate: PublicKey | null;
}): BuiltInstruction {
  const built = ix.settle(
    {
      submitter: toHex(args.submitter),
      config: toHex(configPda().address),
      market: toHex(marketPda(assetId(args.symbol)).address),
      positions: toHex(traderPda(args.owner).address),
      vault: toHex(vaultPda().address),
      vault_token: toHex(args.vaultToken),
      // The book's recorded payout destination, and nothing else. Any other account is not paid:
      // the payout is held on the book instead.
      owner_token: args.book.payoutDestination,
      protocol_token: toHex(args.protocolToken),
      // Optional as a pair: both or neither. Pass neither.
      insurance: null,
      insurance_token: null,
      delegate: args.delegate ? toHex(args.delegate) : null,
    },
    args.seq, // the handle, never the slot index
    CLOSE_REASON_CODE[args.reason],
    args.reason === 'lastResort'
      ? // No quote and no sysvar: the governance table (it is how the program asks the dark arm),
        // then the market's fee-terms account, which rides writable.
        withFeeTermsTable(lastResortTableRemaining(), args.market, args.config, 'last_resort')
      : // A priced reason carries the Instructions sysvar, the governance table, and the market's
        // fee-terms account when it has terms. Without the table the program refuses the settle
        // (`GovTableRequired`, 6221).
        priceAccountsFor(args.market, args.config, 'settle'),
  );
  // The pool's accounts come last: `CapitalState` and the queue ring, both writable.
  return withCapitalRiders(built, 'settle', args.poolOn);
}
```

<Note>
  **`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](/sdk/positions#a-payout-that-has-not-been-collected).
  **`WrongPayoutDestination` (6150) is reserved and raised by nothing. Do not branch on it.**
</Note>

### 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](/sdk/prices).

```ts theme={"dark"}
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import {
  SenderRefusal,
  computeUnitLimit,
  submitBenchmarkQuoted,
  type BuiltInstruction,
  type GateReport,
  type MarketView,
  type PostedQuote,
  type SubmitOutcome,
} from '@rekt/1000x';

/** A signed quote for one market, as the send needs it. */
export interface SignedQuote {
  /** The quote's assembled ed25519 instruction data, hex, exactly as it arrived. */
  ed25519InstructionDataHex: string;
  /** The ed25519 program id the quote names. */
  ed25519ProgramId: PublicKey;
  /** What the signed payload decodes to: price, publish slot, distinct signers. */
  quote: PostedQuote;
}

export async function sendSettle(args: {
  connection: Connection;
  payer: Keypair;
  built: BuiltInstruction;
  signed: SignedQuote;
  market: MarketView;
  protocolGates: GateReport;
  priorityFeeMicroLamports: number;
  /** Leave true until you have watched this path work. It sends nothing. */
  dryRun: boolean;
}): Promise<SubmitOutcome> {
  try {
    const out = await submitBenchmarkQuoted({
      connection: args.connection,
      commitment: 'confirmed',
      ed25519InstructionDataHex: args.signed.ed25519InstructionDataHex,
      ed25519ProgramId: args.signed.ed25519ProgramId,
      quote: args.signed.quote,
      built: args.built,
      payer: args.payer,
      signers: [args.payer],
      priorityFeeMicroLamports: args.priorityFeeMicroLamports,
      protocolGates: args.protocolGates,
      minSignatures: args.market.minSignatures,
      dryRun: args.dryRun,
    });

    console.log(
      `${args.built.name}: ${out.bytes} bytes, ${out.simulatedCu} CU of ${computeUnitLimit(args.built.name)} declared`,
    );
    // Read `confirmation` before `revert`: a send the cluster never confirmed (`expired`,
    // `unconfirmed`) also carries `revert: null`. Read the chain before you send it again.
    if (out.signature === null) console.log('dry run: nothing was sent');
    else if (out.confirmation !== 'confirmed') console.log(`sent ${out.signature}, ${out.confirmation}`);
    else if (out.revert !== null) console.log(`reverted ${out.signature}: ${JSON.stringify(out.revert)}`);
    else console.log(`landed ${out.signature}`);
    return out;
  } catch (e) {
    // A refusal before signing: a failed gate, a short quote, or a packet that does not fit.
    if (e instanceof SenderRefusal) console.error(`refused before signing: ${e.message}`);
    throw e;
  }
}
```

<Note>
  **`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.
</Note>

### Request or mark, then settle

Three reasons are two steps.

| Reason | Step 1, which carries no quote | Who sends step 1 | Step 2 |
| - | - | - | - |
| `voluntary` | `request_close` (`ix.requestClose`) | the owner or a session key | `settle`, sent by anyone |
| `userExpiry` | `mark_expiry` with reason 3 (`ix.markExpiry`) | anyone, from the deadline | `settle`, sent by anyone |
| `tenorExpiry` | `mark_expiry` with reason 4 | anyone, from the deadline | `settle`, sent by anyone |

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](/sdk/orders) 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:

| Instruction | Capital accounts it needs |
| - | - |
| `settle`, every reason | `CapitalState` and the queue account, both writable |
| `void_open`, `reverse_add`, `set_take_profit` | `CapitalState` (writable) |
| `pay_queue`, anyone | the config (read), `CapitalState`, the queue account, the vault and its token account (all writable), then pairs of an account's book and its destination |
| `open` and `add_collateral` | **none** |

`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.

```ts theme={"dark"}
import { isConserving, type Payouts, type PositionView } from '@rekt/1000x';

export function assertConserved(payouts: Payouts, position: PositionView): void {
  // poolDelta === collateral - (every leg that leaves the vault)
  if (!isConserving(payouts, position.collateral)) {
    throw new Error('the settlement legs do not conserve: do not send another trade until this is explained');
  }
}
```

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`.

<Warning>
  **The SDK's settlement mirror does not model the [borrow fee](/how-it-works#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.
</Warning>

## 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.

```ts theme={"dark"}
import { Connection, PublicKey } from '@solana/web3.js';
import {
  closedTradesInTransaction,
  confirmSignature,
  connectionSignatureReader,
  receiptOf,
  type ConfigView,
} from '@rekt/1000x';

export async function closeResult(
  conn: Connection,
  signature: string,
  lastValidBlockHeight: number,
  owner: PublicKey,
  mint: PublicKey,
  config: ConfigView,
  seq: bigint,
) {
  const out = await confirmSignature(connectionSignatureReader(conn), signature, lastValidBlockHeight, {
    waitMs: 12_000,
  });
  // `unconfirmed` may still land: look the signature up again before you send anything else.
  if (out.confirmation !== 'confirmed') return { verdict: out.confirmation } as const;
  if (out.err !== null) return { verdict: 'reverted', err: out.err } as const;
  if (out.landed === null) return { verdict: 'landed' } as const;
  const receipt = receiptOf(out.landed, { tokenOwners: [owner.toBase58()], mint: mint.toBase58() });
  // Match the seq: one transaction can settle several of the owner's positions (a keeper batch).
  const trade = closedTradesInTransaction(out.landed, owner, config).find((t) => t.seq === seq) ?? null;
  // `receipt.ownerDelta` is what reached the owner's token accounts in this transaction.
  return { verdict: 'landed', trade, paidToOwner: receipt.ownerDelta, complete: receipt.complete } as const;
}
```

`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:

```ts theme={"dark"}
import { connectionReader, readSettlementForSeq } from '@rekt/1000x';

// `minContextSlot`: the slot of the read that showed the position gone.
const [trade] = await readSettlementForSeq(connectionReader(conn), owner, [seq], config, { minContextSlot });
```

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:

```ts theme={"dark"}
import { connectionReader, memoReader, readTradeHistory } from '@rekt/1000x';

const reader = memoReader(connectionReader(conn)); // keep it for the life of the process
const first = await readTradeHistory(reader, owner, config); // 1 list + one read per transaction
const again = await readTradeHistory(reader, owner, config); // 1 list call when nothing is new
```

`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](/sdk/errors).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.