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

> Every refusal the program can raise, before you send — with the value it measured and the threshold it was judged against.

This is the reason the SDK exists rather than a generated IDL client.

A generated client lets you build a transaction and find out afterwards. `openGates` tells you
**first**: every gate that can refuse the action, with its measured value and its threshold, in
**the program's order for an open**. On a Minute book, `MinuteDisabled` comes first, then the
guardian freeze. The table below lists the rest in that order. An open carries no quote, and the
fill judges the price: see [the other gate reports](#the-other-gate-reports) and
[orders and closes](/sdk/orders).

> `CollateralOutOfRange` teaches nothing.
> `stake $0.10 is below the minimum $0.20` teaches everything.

## What a gate is

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

export function render(gate: Gate): string {
  return [
    gate.ok ? 'PASS' : 'FAIL',
    gate.label,             // a short name for the rule
    gate.value,             // what was measured, already formatted for a human
    gate.threshold,         // what it had to clear
    gate.error ?? '',       // the ErrorCode name the program raises when this gate fails
    gate.detail,            // one sentence on what this gate is protecting
  ].join('  ');
}
```

<Warning>
  **Never parse `value` back into a number.** `value` and `threshold` are strings formatted for a
  reader. If you need a magnitude — for a progress bar, a colour, a chart — use `gate.scale`, which
  carries `measured`, `limit` and a `headroom` from 0 to 1. Recovering `0.115` from `"11.5867 bp"`
  means recomputing a protocol quantity in your presentation layer, which is exactly how an
  interface and a chain come to disagree.
</Warning>

`scale` is absent on categorical gates — paused or not, above the take-profit floor or below it —
because a switch has no headroom and inventing one would put a half-full bar under a fact that is
simply true or false.

```ts theme={"dark"}
import { APPROACHING, approaching, headroom, type Gate } from '@rekt/1000x';

export function bar(gate: Gate): { fill: number | null; warn: boolean } {
  return {
    // null on a categorical gate: draw a tick or a cross, not a bar.
    fill: headroom(gate),
    // Still passing, but inside the last APPROACHING of its room. Paint it.
    warn: approaching(gate),
  };
}

export const warnBand = APPROACHING;
```

## Building the context for an open

`openGates` needs the state that the refusals are judged against. Everything in it is a read you
already know how to do.

```ts theme={"dark"}
import { Connection, PublicKey } from '@solana/web3.js';
import {
  INITIAL_POSITION_SLOTS,
  decodeTraderAccount,
  openGates,
  slotCapacity,
  traderPda,
  type ConfigView,
  type GateReport,
  type MarketView,
  type OpenRequest,
  type PriceReport,
  type TraderAccountView,
  type VaultGauges,
  type VaultView,
} from '@rekt/1000x';

export async function gatesForOpen(
  connection: Connection,
  state: {
    config: ConfigView;
    market: MarketView;
    vault: VaultView;
    gauges: VaultGauges;
    price: PriceReport;
    nowUnix: bigint;
  },
  owner: PublicKey,
  traderToken: PublicKey,
  req: OpenRequest,
): Promise<GateReport> {
  const traderAddress = traderPda(owner).address;
  const info = await connection.getAccountInfo(traderAddress, 'confirmed');

  // `null` is a legitimate state: a trader who has never traded. It is not an error.
  const trader: TraderAccountView | null = info
    ? decodeTraderAccount(new Uint8Array(info.data))
    : null;

  const balance = await connection.getTokenAccountBalance(traderToken, 'confirmed');

  return openGates(
    {
      config: state.config,
      market: state.market,
      vault: state.vault,
      gauges: state.gauges,
      // The quotes the price report admitted, plus the ones it did not — pass what you read. The
      // price rows do not decide an order: the fill judges the price.
      quotes: state.price.admitted,
      trader,
      // The account's ALLOCATION, not the length of its position vector. They differ the moment
      // anything settles, and using the vector's length refuses opens the program would admit.
      slotCapacity: info ? slotCapacity(info.data.length) : INITIAL_POSITION_SLOTS,
      traderBalance: BigInt(balance.value.amount),
      nowUnix: state.nowUnix,
      // True when the pool is on (`capitalActive`): the capacity and directional-cap rows are then
      // left out, because the fill admits cover.
      capitalMode: true,
    },
    req,
  );
}
```

<Note>
  `OpenRequest` is `{ collateral: bigint; direction: 'long' | 'short'; rungBps: number; userTimerS: number; tier?: number }`.
  `rungBps` is basis points of stake — `20_000` is 2.00x, the floor — not a whole multiple. Set
  `userTimerS` to `0` for no timer of your own.

  **`tier` is optional and `0` or absent means the market's own.** It matters because a market's
  `tier` is a CEILING rather than the only leverage it sells: every distance the quote returns is `something / L`, so a
  report derived from `market.tier` while the request names 250x on a 1000x market shows a barrier
  four times too tight and calls every gate green. Pass the same `tier` to `openPreview` — it takes
  one as its seventh argument — or use `client.openGates`, which threads the whole request through
  both.
</Note>

## The refusals, in the order the program reaches them

| Gate | Refuses with |
| - | - |
| a Minute book opens on a market where Minute is off — not in `report.gates`; read `market.minuteEnabled` | `MinuteDisabled` |
| the oracle is frozen by the guardian — not in `report.gates`; read `config.oracleFrozen` | `OracleFrozen` |
| protocol paused | `ProtocolPaused` |
| market paused | `MarketPaused` |
| the market's session calendar is shut, or less than a whole minimum tenor remains | `MarketClosed`, `SessionEndsBeforeTenor` |
| inside a scheduled event window | `InsideEventWindow` |
| user timer past the tenor | `TimerExceedsTenor` |
| `minCollateralOut` left at zero | `MinCollateralOutRequired` |
| collateral outside the governed bounds, or a budget above the ceiling | `CollateralOutOfRange`, `BudgetExceedsMaxCollateral` |
| the budget buys less than the `minCollateralOut` you signed | `BudgetBelowCollateralFloor` |
| leverage above the market's tier — the tier is a CEILING, not the only leverage sold | `LeverageAboveMarketTier` |
| leverage below the venue's floor for this market — the floor is `min(config.minTier, market.tier)`, so resolve it against the market's own tier, never from `minTier` alone | `LeverageBelowMinTier` |
| leverage the program does not offer | `RateOutOfRange` |
| rung below the take-profit floor (2.00x) | `RungBelowFloor` |
| rung above the payout cap | `RungAbovePayoutCap` |
| the cover rule does not admit the fill — the order is returned at its fill, with the whole stake and no fee | none: a returned order is an outcome, not a refusal; not in `report.gates` |
| this side of the market is at its limit, when a limit is set | `SideStakeCapReached` |
| the trader has no free slot | `PositionSlotsFull` |

Most of these are **states, not faults**. A frozen oracle, a paused market, a shut session, a market
inside an event window, a pool at capacity and a trader whose book is full are all the protocol
working. Show the
sentence, not a stack trace — and see [error handling](/sdk/errors) for which ones retrying can ever
fix.

`openGates` also reports `open.balance` last, which is not a refusal of this program at all: it is
the trader's SPL balance against the total debit, and the token program raises `SplInsufficientFunds`.
It is in the report because a client that shows twelve green protocol gates and then fails on funds
has explained nothing.

<Note>
  **A market with no session calendar always passes the session row.** On a market that has one,
  a position must be **dead** before the close, with the market's own settlement buffer to spare, so
  no position is ever live while the market is shut. An open near the close is given a shorter
  tenor rather than refused, until less than a whole minimum is left. `MarketClosed` means there is
  no session open now; `SessionEndsBeforeTenor` means there is one, but not enough of it left.

  **`open.timer` comes before the collateral rows.** A timer past the tenor and a zero
  `minCollateralOut` together are refused `TimerExceedsTenor`, which is the program's order.
</Note>

<Warning>
  **`openGates` does not model a session key's own refusals — a second report does.** If
  `authority` is a session key rather than the trader's own wallet, the program checks it very
  early, before the other gates in the table above, and can
  refuse with `DelegateExpired`, `DelegateWindowLimitReached`, `NotTheDelegate` or
  `DelegateMayNotSign`. None of them appear in the table above or in `report.gates`.

  Call **`delegateGates(ctx, collateral)`** for those four, and **`sessionAllowanceGates(ctx,
      collateral)`** for the SPL token allowance — the one clock no account of this program can see,
  and the one that ends most long sessions. A clean `openGates` report says nothing about either.
  See [session keys](/sdk/sessions#gate-before-you-send) for the whole shape.
</Warning>

## Rendering the whole table

Show every gate, not only the failure. A user who can see that every gate but one passes, and that
the one still passing is close to binding, understands the system; a user who sees a single red
error learns nothing. Render `report.gates` rather than a count of your own — the row set depends on
the market (a single-feed market has no quorum row).

```ts theme={"dark"}
import { approaching, headroom, type GateReport } from '@rekt/1000x';

export function table(report: GateReport): string[] {
  return report.gates.map((g) => {
    const room = headroom(g);
    const mark = !g.ok ? 'x' : approaching(g) ? '!' : '.';
    const pct = room === null ? '' : ` ${Math.round(room * 100)}% room`;
    return `${mark} ${g.label.padEnd(38)} ${g.value} / ${g.threshold}${pct}`;
  });
}
```

## The other gate reports

The same shape covers everything the program can refuse:

| Function | Answers |
| - | - |
| `openGates(ctx, req)` | can this position be opened |
| `addGates(ctx, req)` | can this position take this add |
| `delegateGates(ctx, collateral)` | may this session key sign this open — owner, identity, expiry, spend window |
| `sessionAllowanceGates(ctx, collateral)` | is the SPL token allowance still large enough for this trade |
| `settleGates(ctx)` | can this close be submitted, by this submitter, on this market |
| `priceGates(quotes, market)` | is there an admissible price at all, and what is the median |
| `redeemGates(ctx)` | can this redemption ticket be executed now |
| `armGates(ctx)` | may this ticket take the capacity ratchet |
| `cancelGates(ctx)` | may this ticket be cancelled, and by whom |
| `sideRoom(direction, market, vault, vaultBalance)` | how much stake this side of the market can still take, before the tap |
| `closableFromSlotOf(position, config, market)` | the first slot at which a close request can land, or `null` while there is none |

Settlement's gates are covered in [settling a position](/sdk/settling); the redemption three in
[providing liquidity](/liquidity); the two session reports in [session keys](/sdk/sessions).

<Note>
  **An open and an add read no price.** The rows that judge a quote (`quote.*`, `price.*`) and the
  forecast rows (`open.forecast`, `add.forecast`) do not refuse an order: the fill judges the first
  price, and a price that fails returns the order. Pass `sideRoom` in the context, and `openGates`
  and `addGates` add the side-limit row. [Orders and closes](/sdk/orders) has the rest.
</Note>

<Note>
  **`addGates` is the report for `add_collateral`.** An add takes `open`'s account list, `open`'s
  entry fee and `open`'s side limit, so it is refused by nearly everything `openGates` covers, plus its own rows: the position must be live and on this market,
  the engine bounds the increment against the position's total after the blend, and `add.forecast`
  compares the position's **remaining** life with the oracle's forecast of the close.
</Note>

## Why this shape and not exceptions

A refusal here is **data**, never a thrown string. Three reasons, all of them learned the hard way:

* An interface renders several gates at once, and a user deliberately provoking one — trying a
  stake above the cap to see where the cap is — is the normal case, not the exceptional one.
* The measured value is the whole explanation. An exception carrying only a name throws away the
  half of the message that helps.
* Predicting the refusal client-side costs nothing and saves a transaction fee, a round trip, and
  a confusing failure at the worst moment.


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