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

# Reading positions

> The position handle, the trader account, the two price levels, the two clocks, and what every close would pay right now.

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

```ts theme={"dark"}
import { type PositionRef } from '@rekt-trading/1000x';

// { trader: string; slot: number; seq: bigint }
export function describe(ref: PositionRef): string {
  return `${ref.trader.slice(0, 8)} slot ${ref.slot} seq ${ref.seq}`;
}
```

`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

```ts theme={"dark"}
import { Connection, PublicKey } from '@solana/web3.js';
import {
  INITIAL_POSITION_SLOTS,
  decodeTraderAccount,
  freeSlots,
  heldPositions,
  slotCapacity,
  traderPda,
  type HeldPosition,
} from '@rekt-trading/1000x';

export interface Book {
  positions: HeldPosition[];
  free: number;
  capacity: number;
  /** A payout already settled but not yet claimed, and when it becomes sweepable. */
  pendingPayout: bigint;
  pendingSince: bigint | null;
}

export async function readBook(connection: Connection, owner: PublicKey): Promise<Book> {
  const info = await connection.getAccountInfo(traderPda(owner).address, 'confirmed');

  // A trader who has never traded has no account. That is a state, not an error.
  if (!info) {
    return {
      positions: [],
      free: INITIAL_POSITION_SLOTS,
      capacity: INITIAL_POSITION_SLOTS,
      pendingPayout: 0n,
      pendingSince: null,
    };
  }

  const account = decodeTraderAccount(new Uint8Array(info.data));
  // Capacity comes from the account's ALLOCATION, not from `slots.length` — the vector holds only
  // as many entries as have ever been used, so a four-slot account reports 1 after its first close.
  const capacity = slotCapacity(info.data.length);

  return {
    positions: heldPositions(owner.toBase58(), account),
    free: freeSlots(account, capacity),
    capacity,
    pendingPayout: account.pendingPayout,
    pendingSince: account.pendingPayout > 0n ? account.pendingSince : null,
  };
}
```

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

> [!NOTE]
>
> `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](https://docs.rekt.money/how-it-works.md#adding-to-a-position).

| Field | Meaning |
| - | - |
| `seq` | The handle. |
| `market` | The market address this position belongs to. Check it before you price anything. |
| `collateral` | The stake. |
| `tier` | The leverage it opened at. |
| `direction` | `0` long, `1` short — the raw on-chain discriminant. |
| `entryPrice`, `entryExpo` | Where it opened, and at what exponent. **0 while the position is an order.** |
| `barrierMicroBp`, `takeProfitMicroBp` | The two levels, as distances from entry. |
| `exitFeeBps`, `payoutCapMultiple` | Frozen at open, not read from live config. `exitFeeBps` is the exit fee **bound**: a position with a fee-terms slot pays the rate its terms entry gives, never above it. |
| `borrowRateBpsPerHour` | The funding rate this position pays, frozen at open. The funding rate is 0, so nothing accrues. Read this field, never `config.params.borrowRateBpsPerHour`, which is what a *new* position would freeze. |
| `borrowGraceS` | The free window before funding accrues, frozen at open. A stored `0n` resolves to the live `config.params.borrowGraceS`. |
| `openedAt`, `openedSlot`, `tMaxSeconds`, `userTimerS` | The two clocks and their origins. |
| `reserved` | The reserve the cover rule counts for this position: its maximum payout and closing fee. Grows on an add. |

> [!WARNING]
>
> **`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.

> [!WARNING]
>
> **`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)`:

| Phase | What it is |
| - | - |
| `PendingOpen` | An order. `entryPrice` is 0. No close, add or level exists yet. |
| `Live` | Filled, nothing pending. |
| `AddPending` | An add waits for its price. |
| `AddMemo` | An add was filled, and its short hold (`M_add` slots) is still running. |
| `ExitRequested` | The owner requested a close. Anyone may settle it from the exit target. |
| `ExitMarked` | An expiry was marked. Anyone may settle it from the exit target. |
| `StampedKnockedOut`, `StampedTookProfit` | A level is stamped. The settle pays it. |
| `StampedAddPending` | Stamped knocked out, with an add still to return. |

**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](https://docs.rekt.money/sdk/orders.md).

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

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

export function lines(position: PositionView): { barrier: bigint; takeProfit: bigint | null } {
  const l = levels(position);
  return {
    barrier: l.barrierPrice,
    // null when the position has no rung set, or the level would be a zero price.
    takeProfit: l.takeProfitPrice,
  };
}
```

## The two clocks

```ts theme={"dark"}
import { deadlines, isIndefiniteTenor, type PositionView } from '@rekt-trading/1000x';

export function countdown(position: PositionView, nowUnix: bigint) {
  const d = deadlines(position);
  // A position that froze the indefinite tenor has no protocol clock: show "no limit", never a date.
  const noTenor = isIndefiniteTenor(position.tMaxSeconds);
  return {
    // The protocol's tenor, from the position's own frozen value. null means no limit.
    tenorIn: noTenor ? null : d.tenorExpiresAt - nowUnix,
    // The trader's own timer, if they set one.
    userIn: d.userExpiresAt === null ? null : d.userExpiresAt - nowUnix,
    // The earlier of the two: when a stranger may first close this position. Under the indefinite
    // tenor it is your own timer, or null when you set none.
    closableBy: noTenor ? d.userExpiresAt : d.firstExpiryAt,
  };
}
```

## 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](https://docs.rekt.money/sdk/settling.md#the-close-reasons).

**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](https://docs.rekt.money/limits.md) 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.

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

export function preview(
  position: PositionView,
  config: ConfigView,
  exitPrice: bigint,
  nowUnix: bigint,
  signer: string,
  owner: string,
  feeTermsTable: FeeTermsTableView | null,
): SettlePreview {
  const out = settlePreview(position, config, exitPrice, nowUnix, signer, owner, { feeTermsTable });

  // How far the move has travelled toward the barrier, 0 to 1. Above 1 means knocked out.
  console.log(`barrier fraction ${out.barrierFraction.toFixed(3)}`);
  console.log(out.knockedOut ? 'knocked out' : out.takeProfitReached ? 'rung reached' : 'live');

  for (const r of out.reasons) {
    const name: CloseReasonName = r.reason;
    console.log(
      r.legal
        // `payouts.liquidator` reads 0n on every settlement: nobody is paid to submit a close.
        ? `${name}: trader ${r.payouts?.trader}, protocol ${r.payouts?.protocol}${r.capped ? ' (CAPPED)' : ''}`
        : `${name}: not legal: ${r.why}`,
    );
  }

  // `out.suggested` is the reason a keeper would submit, or null when none is legal yet.
  return out;
}
```

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

> [!WARNING]
>
> **`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.

```ts theme={"dark"}
import { ix, toHex, traderPda, vaultPda, type BuiltInstruction } from '@rekt-trading/1000x';
import { PublicKey } from '@solana/web3.js';

export function buildSetDestination(args: {
  owner: PublicKey;
  vaultToken: PublicKey;
  newDestination: PublicKey;
}): BuiltInstruction {
  return ix.setPayoutDestination({
    owner: toHex(args.owner),
    positions: toHex(traderPda(args.owner).address),
    vault: toHex(vaultPda().address),
    vault_token: toHex(args.vaultToken),
    payout_destination: toHex(args.newDestination),
  });
}
```

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.

```ts theme={"dark"}
import { ix, toHex, traderPda, vaultPda, type BuiltInstruction } from '@rekt-trading/1000x';
import { PublicKey } from '@solana/web3.js';

export function buildClaim(args: {
  payer: PublicKey;
  owner: PublicKey;
  ownerToken: PublicKey;
  vaultToken: PublicKey;
}): BuiltInstruction {
  return ix.claimPayout({
    payer: toHex(args.payer),
    owner_token: toHex(args.ownerToken),
    positions: toHex(traderPda(args.owner).address),
    vault: toHex(vaultPda().address),
    vault_token: toHex(args.vaultToken),
  });
}
```

* **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](https://docs.rekt.money/sdk/settling.md).


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