Skip to main content

A slot index is not a position

The mistake that costs a builder their second trade rather than their first. Positions live in numbered slots inside one trader account, and slots are reused. Index 3 today and index 3 tomorrow are different trades. So the SDK never hands you a bare index:
seq is what settle is called with, and the program searches by it rather than indexing — so a stale handle finds nothing instead of closing a neighbour. Persist seq. Treat slot as a rendering detail.

Read a trader’s book

heldPositions returns live positions only, each already bound to its handle: { ref, position }. Free slots are not returned — an empty slot is not a thing a user has.
PositionSlotsFull is neither a validation error nor an outage. It means close something first. A slot frees when a settle for its position lands. A book is created at up to 12 slots, the most it can hold; resizeTraderAccount helps only a book created smaller.

What a position remembers

The terms are frozen at open, against governance. A position carries its own barrier, rung, exit fee bound, cap, tenor, funding rate and funding grace, and the fee terms entry it froze (feeTermsSlot(position.flags)). Settlement reads those rather than live config. A governance change does not reach a position that already exists. The freeze is against governance, not against the owner. The owner may add collateral to a live position. An add moves collateral, entryPrice, openedSlot and reserved, and writes userTimerS if the add sets one. Nothing writes a level, because the barrier and the take-profit are distances from the entry: writing entryPrice is the whole of “recompute the barrier from the new average entry”. So a cached levels() result goes stale on an add even though no level was written. See adding to a position.
openedAt and openedSlot are two different clocks and an add moves only one of them.openedSlot is the slot of the price that last priced the position: the order’s slot until a filler writes the entry, then the slot of the entry price, and after a filled add the slot of the add’s price. The minimum hold counts from it, so a double-down restarts the hold before a close the owner asks for.openedAt is not re-stamped by an add.
position.market is an address. A client holding two markets must check it before pricing — an interface that prices a SOL position against another market’s median quotes the wrong payout and submits the wrong market to settle. The program catches only the second of those.

Orders and pending steps

A slot can hold an order as well as a position, and a position can be waiting for a step: an add that waits for its price, a short hold after a filled add, or an exit target from a close request or an expiry mark. Read the state with phase(position, nowSlot): Read the pending steps through the accessors, never through the raw fields. The same last words of a position hold a pending add, the memo of a filled add, or nothing, depending on a flag, and several store a slot as an offset from openedSlot. isUnfilled, pendingAdd, addTargetSlot, addMemo, memoIsLive, exitTargetSlot, exitReason and tpFloorSlot decode them as the program does. closableFromSlotOf(position, config, market) gives the first slot at which a close request can land, or null while there is none. Do not draw levels for an order: levels needs an entry. See orders and closes.

The two price levels

barrierMicroBp and takeProfitMicroBp are distances. levels turns them into the prices a chart can draw. Each price is the program’s own threshold: the first price at which the barrier counts as touched, or the take-profit as reached — never one base unit short of it. takeProfitPrice is null when the position has no take-profit, and when the level would fall at a price of zero, which no oracle reports.

The two clocks

What every close would pay, right now

settlePreview prices the six priced close reasons at one price and one instant, using the same settlement the chain runs, and says which are legal now and why the others are not. It does not model lastResort, the no-price close; see settling a position. What a close pays. The program takes these steps in this order, with the terms the position froze at open:
  1. On a gain, the stake plus the part of the gain you keep. On a loss, the stake less the loss.
  2. On a loss, a loss tax of 99.96 % of the realised loss comes out of that. It is taken before the exit fee, so a close at a loss returns about the stake less twice the loss, before the fee.
  3. The exit fee, on the exit value that is left after the tax.
  4. Funding, from the position’s frozen rate. The rate is 0, so a quote has no funding line.
What remains is paid, up to the payout cap. A knockout pays 0 and takes no fee. Fees and limits has the rates. Pass the market’s fee-terms account, decoded, for a position with a fee-terms slot (feeTermsSlot(position.flags) != 0). The close is then priced with that slot’s entry, as the chain prices it. Without the account such a close reads FeeTermsTableRequired. client.settlePreview takes the same { feeTermsTable } as its last argument.
signer and owner are separate arguments because voluntary is the one reason that needs the owner’s authority behind it: their own signature, or a session key they authorised. Every other reason is proved by the price or by the clock and is therefore permissionless. Conflating the two is how an interface offers a button that cannot work.
settlePreview marks voluntary legal only when signer === owner. The program also admits an unexpired session key whose owner is the position’s owner, so a Close button gated on this report alone stays hidden for a close a session key can sign. NotPositionOwner (6072) is raised only when no session key is supplied.

Where settlements pay

Every settlement of a position pays the book’s recorded payoutDestination. It is set at initTraderAccount and the owner may move it with setPayoutDestination, signed by the owner alone: no session key and no other authority can. The new account must be a USDC account the owner owns, on the vault’s mint, and not frozen. Moving it does not touch a payout already held on the book, and does not restart that payout’s expiry clock.
A settle that names any other account as owner_token does not pay it: the payout is held on the book instead. Read the destination off the book (decodeTraderAccount(bytes).payoutDestination) whenever you build a settle.

A payout that has not been collected

When a settlement cannot deliver, because the destination was closed, frozen or not the recorded one, the payout is held on the trader’s book as pendingPayout. Show it: it is money the user already has and may not know about.
  • To the recorded destination, anyone may claim. No owner signature is needed, and the money can go nowhere the settlement would not have sent it.
  • To any other account, the owner must be the payer and sign. Otherwise the claim is refused (ClaimDestinationRequiresOwner, 6155). The destination must still be a USDC account the owner owns.
A payout nobody claims within config.params.unclaimedPayoutExpiryS of when it was held (pendingSince) may be swept to the liquidity pool by anyone, with sweepUnclaimedPayout. Next: settling a position.