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

# Session keys

> A scoped, revocable key that signs trades with no wallet prompt: who signs a trade, how to authorise, fund, use, renew and revoke a session key, how to gate before you send, and what a leaked key costs.

An `open`, an `add_collateral` and a `request_close` carry no quote, so a person can approve each one
in a wallet. A **session key** removes the prompt on every trade: the trader authorises a
short-lived, scoped, revocable key once, and that key then signs locally. This page says who can
sign a trade, and documents the session key the program provides.

The instructions that carry a quote are `set_take_profit`, and any `settle`, `stamp_book` or
`prove_price` a client sends itself. A quote ages out in slots, so a person reading a wallet dialog
cannot land one of those: sign it locally. Read [quotes](/sdk/quotes) for the age rule.

## Who can sign a trade

Who signs is an integration decision, and there is more than one reasonable answer. Weigh them for
your own product; this documentation does not prescribe one.

<CardGroup cols={3}>
  <Card title="Your service signs" icon="server">
    Your own backend holds, or is delegated, the authority to sign and signs each trade
    programmatically with no per-trade prompt. You choose the custody model and where the key lives.
    The trade-off is that whatever signs holds that authority for as long as it holds the key.
  </Card>

  <Card title="A scoped delegate" icon="key">
    The program provides a **session-key delegate**: the trader authorises a short-lived, scoped,
    revocable key once, and that key then signs locally with no dialog. The rest of this page is its
    reference — it is one option, not a requirement.
  </Card>

  <Card title="The trader signs each" icon="wallet">
    The trader approves every open, add and close request in their wallet. None of them carries a
    quote, so each is a single attempt and no clock runs against the prompt. The cost is a prompt on
    every trade.
  </Card>
</CardGroup>

<Note>
  Whichever you choose, the downstream facts are the same: the program checks the signer. The pages
  on [opening](/sdk/opening), [orders and closes](/sdk/orders) and [reading gates](/sdk/gates) do not
  assume a particular signing model. Gas is a separate decision — see
  [who pays the fee](#who-pays-the-fee) below.
</Note>

### Who pays the fee

A separate consideration, and also yours to decide. A Solana transaction has a **fee payer**, and
the fee payer signs. That gives you a second axis, independent of who signs the trade:

* **The user pays their own fee.** Simplest, but the user needs lamports on the key that pays, and
  in the delegate model below that is the delegate key, not the wallet.
* **You sponsor the fee.** A fee payer you control is added to the transaction so the user needs no
  SOL. What sponsoring costs you, and how you fund it, is an operational choice this documentation
  does not make for you.

The point is only that the fee payer is a role you place deliberately. It does not have to be the
same key that signs the trade, and placing it on a human wallet puts the prompt back on every trade.

## The program's option: a session-key delegate

The program ships a `TradeDelegate` — a short-lived, scoped, rate-limited key that the trader
authorises once and that then signs locally, **in microseconds**, with no dialog. If you take this
route, the human is out of the trading path. The rest of this page is its reference.

Four properties, and each one is load-bearing:

<CardGroup cols={2}>
  <Card title="It never holds funds" icon="wallet">
    The money stays in the trader's own token account. The session spends against an SPL allowance
    the trader grants, and that allowance is the ceiling on what a leaked key can **take** — the
    number you choose.

    It is not the ceiling on what a leak can **cost**. A key that cannot take more than the
    allowance can still close a position at a moment of its choosing, or move a target; the section
    below sets out that harm.
  </Card>

  <Card title="Its trading has a hard deadline" icon="clock">
    An absolute expiry on the `TradeDelegate`, 24 hours by default, after which the key can neither
    open nor close here. **The SPL allowance has no clock and does not expire with it.** Revoke;
    do not wait.
  </Card>

  <Card title="Its trading has a spend rate" icon="gauge">
    A decaying budget per window on what the key may OPEN here, metered on the open's `budget` — the
    fee-inclusive total, the money actually at risk, not the resolved collateral. It
    does not meter the SPL allowance — and it is set equal to that allowance by default, so it is not
    what bounds a leak.
  </Card>

  <Card title="It is revocable, with one exception" icon="ban">
    One instruction, effective in the same slot, and it returns the account's rent.

    **The exception is the case you would reach for it in.** If the USDC account the allowance was
    granted over is frozen, closed, or has been handed to somebody else, the program cannot clear
    the approval — it ends the session and the allowance survives, and comes back the moment the
    account works again. Clearing it then takes its own signature against that account.

    The token account is a **required** account on the revoke: pass `TradeDelegate.tokenAccount`,
    read off the chain.
  </Card>
</CardGroup>

<Warning>
  **A session key is an SPL delegate over the trader's own USDC.** Authorising a session CPIs
  `approve(trader_token, delegate_key, allowance)` under the trader's own signature, which writes a
  delegate key and an amount onto the trader's token account. **A token approval names an amount and
  a key. It does not name a program, and it carries no clock.** A holder of the key can therefore
  sign one SPL `Transfer` of up to the remaining allowance to any account it chooses — this venue is
  not in that path, and neither the spend window, the expiry, nor `restrictions` is in it either.

  * The window meters what the key may open *here*; it never meters the approval. It is also set to
    the allowance itself by default (`allowance ?? terms.windowLimit`), so it cannot bind first.
  * Expiry ends the trading rights. The approval stands until it is revoked, whatever the session's
    age.
  * SPL Token does not scope an approval to a program.

  **The allowance is the ceiling on what a leaked key can TAKE**, not on what a leak can COST: a key
  held to the allowance can still close a position at a moment of the holder's choosing, or move a
  target — see [what a leak costs](#what-a-session-key-costs-you-if-it-leaks). Size the allowance for
  one session, and revoke on logout rather than letting it expire.
</Warning>

## Using the delegate, step by step

If you take this route, it is four moves. The funding step is the one most often skipped — a session
that authorises correctly and then cannot send anything.

<Steps>
  <Step title="Generate a session key in the client">
    An ordinary keypair, created where the trades will be signed. It is not the trader's wallet and
    must never be derived from it.

    ```ts session.ts theme={"dark"}
    import { Keypair } from '@solana/web3.js';

    /**
     * Generate it here. **WRITE THE SECRET BEFORE YOU SEND THE AUTHORISATION** — step 2 — and
     * remove it only on a decoded REFUSAL, never on an unknown. See the warning below; this is
     * not a preference.
     *
     * Scope the storage to the session and clear it on logout, at which point you also call
     * `endSession` (step 4). A session key in durable storage outlives the trader's intent.
     */
    export const sessionKey = Keypair.generate();
    ```

    <Warning>
      **Persist the secret BEFORE the authorisation is sent, not after it is confirmed.** The two
      errors are not symmetric:

      | what happens | what it costs |
      | - | - |
      | a key stored for an authorisation that **never landed** | 64 unused bytes nobody reads |
      | a key **discarded** for an authorisation that **DID land** | the chain holds a `TradeDelegate` naming a public key **whose secret exists nowhere**, with the session's fuel SOL on it and a standing SPL approve against the trader's USDC |

      A confirmation poll that times out on a transaction that landed produces exactly the second
      case. Do not then read a chain delegate without a local key as *"a session started in another
      browser"* and invite the trader to mint a second orphan.

      **Remove the stored key only on a decoded refusal.** A blockhash expiring bounds how long a
      transaction can be *accepted* for; it says nothing whatever about whether one already was, so
      a send you could not confirm must KEEP the key. See
      [the failures that are not the program's](/sdk/errors#errors-that-are-not-the-programs).
    </Warning>
  </Step>

  <Step title="Authorise it — one transaction, one wallet prompt, ever">
    `authorizeSession` builds the authorisation. **One instruction sets all three limits**: the
    expiry, the spend window, and the SPL token allowance — the program performs the SPL `approve`
    itself, so the half the token program enforces cannot go missing.

    ```ts authorise.ts theme={"dark"}
    import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
    import {
      DEFAULT_SESSION_TERMS,
      authorizeSession,
      configPda,
      delegatePda,
      ourInstruction,
      toHex,
    } from '@rekt/1000x';

    export async function authorise(args: {
      connection: Connection;
      owner: PublicKey;
      /** The trader's own USDC account. The allowance is written against THIS account. */
      traderToken: PublicKey;
      sessionKey: Keypair;
      /** How much this session may spend in total before it must be renewed. */
      allowance: bigint;
      sign: (tx: Transaction) => Promise<Transaction>;
    }): Promise<string> {
      const built = authorizeSession({
        owner: toHex(args.owner),
        delegateKey: toHex(args.sessionKey.publicKey),
        delegate: toHex(delegatePda(args.owner).address),
        traderToken: toHex(args.traderToken),
        // Required: the program reads the governed maximum stake off the config account.
        config: toHex(configPda().address),
        // 24 hours, a 24-hour spend window, and the window's ceiling. Override deliberately.
        terms: DEFAULT_SESSION_TERMS,
        // Required — `SessionArgs.restrictions` is a `number` with no `?`, so omitting it is a
        // build error. `0` grants Close and Retarget only, NOT LP; `DELEGATE_NO_CLOSE` withholds the
        // close, and `DELEGATE_ALLOW_LP` (opt-in) adds the LP triggers.
        restrictions: 0,
        // The SPL allowance is a TOTAL that draws down and never refills. Size it for how long
        // the trader means to be at the desk; the window limit only bounds the rate.
        allowance: args.allowance,
      });

      const tx = new Transaction().add(...built.map(ourInstruction));
      tx.feePayer = args.owner;
      tx.recentBlockhash = (await args.connection.getLatestBlockhash('confirmed')).blockhash;

      // The ONE wallet prompt in the whole arrangement. The trader may take as long as they like
      // reading it.
      const signed = await args.sign(tx);

      // STORE THE SECRET HERE, before the send — not after a confirmation. See step 1.
      // On a refusal you can delete it. On a timeout you cannot know, and the branch where it
      // landed is the expensive one.
      persistSessionSecret(args.owner, args.sessionKey.secretKey);

      return args.connection.sendRawTransaction(signed.serialize());
    }
    ```

    <Warning>
      **The chain holds a delegate and this device does not hold its key — what now?**

      It is the state a confirmation timeout produces. The recovery is **`endSession`** (step 4): it closes the delegate account,
      drops the SPL allowance, is signed by the *trader* rather than by the lost key, and returns
      the rent. It works precisely because the delegate's own signature is not needed and because
      `TradeDelegate.tokenAccount` is read off the chain rather than re-derived.

      Do **not** offer "start a new session" as the recovery. `authorizeSession` writes to one PDA
      per owner, so a second authorisation overwrites the record of the first — and the fuel and
      the approve on the first do not come back. End it, then start.
    </Warning>

    <Note>
      Show the trader what they are signing: the expiry, the per-window ceiling and the allowance,
      in dollars. This transaction is the whole of the authority they are handing over, and it is
      the only moment they get to look at it.
    </Note>
  </Step>

  <Step title="Fund it — the session pays its own fee">
    A Solana transaction is paid for by a signer, and the fee payer must sign. If the *owner* pays,
    the owner signs, and the wallet prompt is back on every trade — put there by an accounting
    detail. So on this route the fee is paid by a key that is not the
    human: the session key itself (shown here), or a separate sponsoring fee payer you add to the
    transaction (see [who pays the fee](#who-pays-the-fee)). Either way it needs lamports. The
    simplest shape funds the session key in the same moment you authorise it.

    ```ts fund.ts theme={"dark"}
    import { LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';

    /**
     * Enough SOL for the session's own transaction fees. Send it alongside the authorisation so a
     * trader signs once for both.
     *
     * Return the remainder to the owner when you revoke — a session key that keeps a dust balance
     * after logout is a wallet nobody is watching.
     */
    export function fundSession(owner: PublicKey, sessionKey: PublicKey, sol = 0.01): Transaction {
      return new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: owner,
          toPubkey: sessionKey,
          lamports: BigInt(Math.round(sol * LAMPORTS_PER_SOL)),
        }),
      );
    }
    ```

    <Warning>
      A session that is authorised but unfunded fails at the network layer, before any of this
      protocol's refusals are reached — so none of the gate reports on these pages will explain it.
      Check the session key's lamport balance at start-up and top it up before it reaches zero.
    </Warning>
  </Step>

  <Step title="Sign every trade with the session key">
    The session **signs and pays**. The book, the token account and the money all stay the owner's:
    `authority` is whoever signs, and `delegate` names the session's account so the program can
    check it.

    ```ts trade-with-session.ts theme={"dark"}
    import {
      ComputeBudgetProgram,
      Connection,
      Keypair,
      PublicKey,
      TransactionMessage,
      VersionedTransaction,
    } from '@solana/web3.js';
    import {
      assetId,
      computeUnitLimit,
      configPda,
      delegatePda,
      ix,
      marketPda,
      ourInstruction,
      priceAccountsFor,
      toHex,
      traderPda,
      vaultPda,
      type ConfigView,
      type MarketView,
    } from '@rekt/1000x';

    export async function openWithSession(args: {
      connection: Connection;
      config: ConfigView;
      symbol: string;
      market: MarketView;
      /** The trader. Signs NOTHING here. */
      owner: PublicKey;
      /** The session. Signs and pays. */
      sessionKey: Keypair;
      traderToken: PublicKey;
      vaultToken: PublicKey;
      protocolToken: PublicKey;
      /** The fee-inclusive TOTAL to spend. The debit is at most this. */
      budget: bigint;
      direction: 'long' | 'short';
      rungBps: number;
      userTimerS: number;
      /** The mandatory floor on the position SIZE — `sizeForTotal(budget, config.params).collateral`.
       *  `0n` is refused (`MinCollateralOutRequired`). */
      minCollateralOut: bigint;
      /** A durable key you choose, or `0n`. Read by nothing on chain. */
      openerTag: bigint;
    }): Promise<string> {
      const built = ix.open(
        {
          // Whoever signs. With a session that is the session key, not the owner.
          authority: toHex(args.sessionKey.publicKey),
          trader_token: toHex(args.traderToken),
          config: toHex(configPda().address),
          market: toHex(marketPda(assetId(args.symbol)).address),
          vault: toHex(vaultPda().address),
          vault_token: toHex(args.vaultToken),
          protocol_token: toHex(args.protocolToken),
          // The OWNER's book, always. The session never gets one of its own.
          positions: toHex(traderPda(args.owner).address),
          // `null` here with a signer who is not the owner is refused by the program, so this
          // cannot be used to open against somebody else's book.
          delegate: toHex(delegatePda(args.owner).address),
        },
        {
          // Fee-inclusive TOTAL; the venue takes its fee from inside it.
          budget: args.budget,
          direction: args.direction === 'long' ? 0 : 1,
          rung_bps: args.rungBps,
          user_timer_s: args.userTimerS,
          // Mandatory floor on the position SIZE. Set it to `sizeForTotal`'s predicted collateral,
          // never a token value; `0n` is refused.
          min_collateral_out: args.minCollateralOut,
          referrer: null,
          // `0` is the market's own tier — see `/sdk/opening`. A session key may name a lower
          // leverage exactly as the owner may; the delegate checks authorise the SIGNER, and the
          // tier is a property of the trade.
          tier: 0,
          // A durable key you choose, read by nothing on chain. `0n` is the absence.
          opener_tag: args.openerTag,
        },
        // The market's fee-terms account when it has terms. An order carries no quote.
        priceAccountsFor(args.market, args.config, 'open'),
      );

      const { blockhash } = await args.connection.getLatestBlockhash('confirmed');
      const message = new TransactionMessage({
        // The session pays. An owner fee payer means an owner signature means a wallet prompt.
        payerKey: args.sessionKey.publicKey,
        recentBlockhash: blockhash,
        instructions: [
          ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit('open', { quotePost: false }) }),
          ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1_000 }),
          ourInstruction(built),
        ],
      }).compileToV0Message();
      const tx = new VersionedTransaction(message);
      tx.sign([args.sessionKey]);
      // Simulate first, as on /sdk/opening: a refusal costs nothing and arrives as a name.
      const sim = await args.connection.simulateTransaction(tx, { commitment: 'confirmed' });
      if (sim.value.err !== null) throw new Error(JSON.stringify(sim.value.err));
      return args.connection.sendTransaction(tx);
    }
    ```
  </Step>

  <Step title="Revoke on logout">
    `endSession` closes the delegate account **and** drops the SPL allowance, in one instruction
    signed by the trader. It takes effect in the same slot and returns the account's rent, so
    revoking costs less than nothing.

    ```ts revoke.ts theme={"dark"}
    import { Connection, PublicKey, Transaction } from '@solana/web3.js';
    import { decodeTradeDelegate, delegatePda, endSession, ourInstruction, toHex } from '@rekt/1000x';

    export async function revoke(args: {
      connection: Connection;
      owner: PublicKey;
      sign: (tx: Transaction) => Promise<Transaction>;
    }): Promise<string | null> {
      const address = delegatePda(args.owner).address;
      const info = await args.connection.getAccountInfo(address, 'confirmed');
      // No account is not an error — it is a trader with no session, which is the goal state.
      if (!info) return null;

      const delegate = decodeTradeDelegate(new Uint8Array(info.data));

      const built = endSession({
        owner: toHex(args.owner),
        delegate: toHex(address),
        // MUST be the account the allowance was granted over. `TradeDelegate.tokenAccount` is
        // pinned by the chain — read it rather than re-deriving the trader's associated token
        // account and hoping the two agree.
        traderToken: delegate.tokenAccount,
      });

      const tx = new Transaction().add(...built.map(ourInstruction));
      tx.feePayer = args.owner;
      tx.recentBlockhash = (await args.connection.getLatestBlockhash('confirmed')).blockhash;
      const signed = await args.sign(tx);
      return args.connection.sendRawTransaction(signed.serialize());
    }
    ```

    <Note>
      Revoke on logout, on wallet disconnect, and on tab close if you can reach it. Also revoke
      when the trader asks — put the control somewhere they can find it, showing the expiry and the
      remaining allowance, not buried in a settings page.
    </Note>
  </Step>
</Steps>

## Gate before you send

Three separate clocks can stop a session, and only two of them belong to this program. Check all
three before you build a transaction — a refusal predicted costs nothing, and a refusal discovered
costs a fee and a confusing failure.

```ts session-gates.ts theme={"dark"}
import {
  decodeTradeDelegate,
  delegateGates,
  delegatePda,
  sessionAllowanceGates,
  sessionState,
  toHex,
  type GateReport,
} from '@rekt/1000x';
import { Connection, PublicKey } from '@solana/web3.js';

export async function sessionReports(args: {
  connection: Connection;
  owner: PublicKey;
  sessionKey: PublicKey;
  traderToken: PublicKey;
  collateral: bigint;
  nowUnix: bigint;
}): Promise<{ authority: GateReport; allowance: GateReport; expiresInS: number }> {
  const info = await args.connection.getAccountInfo(delegatePda(args.owner).address, 'confirmed');
  const delegate = info ? decodeTradeDelegate(new Uint8Array(info.data)) : null;

  // The SPL allowance is the clock NO account of this program can see. Read it from the token
  // account itself. Pass `null` if you could not — `sessionAllowanceGates` then REFUSES to call
  // itself fine, because an unread limit reported as green is how this arrangement fails quietly.
  const parsed = await args.connection.getParsedAccountInfo(args.traderToken, 'confirmed');
  const data = parsed.value?.data;
  const allowance =
    data && 'parsed' in data ? BigInt(data.parsed.info.delegatedAmount?.amount ?? '0') : null;

  const ctx = {
    delegate,
    owner: toHex(args.owner),
    signer: toHex(args.sessionKey),
    nowUnix: args.nowUnix,
    allowance,
  };

  return {
    authority: delegateGates(ctx, args.collateral),
    allowance: sessionAllowanceGates(ctx, args.collateral),
    expiresInS: delegate ? sessionState(delegate, args.nowUnix).expiresInS : 0,
  };
}
```

### The three clocks

| Clock | Who enforces it | How it ends | How you restore it |
| - | - | - | - |
| **Expiry** | this program | an absolute deadline, 24 hours by default | `renewSession` — one trader signature |
| **Spend window** | this program | a decaying budget per window, so it refills continuously rather than resetting on a boundary | wait, or `renewSession` with a higher limit |
| **SPL allowance** | the SPL Token program | a **total** that draws down and never refills | `renewSession`, which carries the `approve` with it |

<Warning>
  **The allowance is the one that will surprise you.** A long-running session usually stops not
  because it expired and not because it hit its per-window ceiling, but because the SPL allowance
  reached zero — and that refusal arrives from the *token program* as `InsufficientFunds`, carrying
  none of this program's error shape. It reads to a trader as the venue being broken.

  The rule that removes the edge: **renew the session and the allowance together, always.**
  `renewSession` does exactly that in one instruction, which is why you should use it rather than
  hand-building a renewal.
</Warning>

```ts renew.ts theme={"dark"}
import { DEFAULT_SESSION_TERMS, configPda, delegatePda, renewSession, toHex } from '@rekt/1000x';
import { PublicKey } from '@solana/web3.js';

/** Renew well before the deadline — a session that expires mid-trade fails the trade, not the tick. */
export function buildRenewal(
  owner: PublicKey,
  sessionKey: PublicKey,
  traderToken: PublicKey,
  /** Read off `TradeDelegate.restrictions` — a renewal re-states the byte rather than keeping it. */
  restrictions: number,
) {
  return renewSession({
    owner: toHex(owner),
    delegateKey: toHex(sessionKey),
    delegate: toHex(delegatePda(owner).address),
    traderToken: toHex(traderToken),
    config: toHex(configPda().address),
    terms: DEFAULT_SESSION_TERMS,
    restrictions,
  });
}
```

<Warning>
  **`restrictions` is an argument, not a default.** A renewal re-states the byte: `renewSession`
  writes what it is given, so a hardcoded `0` returns a Close button to a session that had it
  withheld. Read it off `TradeDelegate.restrictions` and pass it back.
</Warning>

## The byte wall

A session open costs about **128 bytes** more than an owner-signed one — the `TradeDelegate`
account (32 B), the session's own signature (64 B) and its pubkey in the account list (32 B). An
order carries no quote, so it still fits Solana's 1,232-byte packet with room to spare. A wrapped open
names Minute's accounts as well as the venue's, and an instruction that carries a quote (a keeper's
`settle`) carries the quote too: those compose a v0 message with the market's address lookup table.
Measure your real byte count with a signed transaction before you send it.

<Warning>
  **The 1,232-byte wall is a CLIENT limit as well as a protocol limit — and it still stops you.**

  Solana's Transaction V1 work (SIMD-0296 / SIMD-0385) is **active on mainnet** and raises the
  network's packet ceiling. The number above did not change for anyone building with
  `@solana/web3.js` v1, which hard-codes `PACKET_DATA_SIZE = 1280 - 40 - 8` and allocates a buffer
  of exactly that size to serialise a message. A larger transaction throws `ERR_OUT_OF_RANGE`
  locally and **never reaches the network**, so there is nothing to observe on chain and no error
  code to catch.

  Two consequences worth designing around:

  * **The wall is still 1,232 for a web3.js v1 client**, and the lookup table is still the answer.
  * **Do not plan new capacity against the raised ceiling until your client can emit it.** Reaching
    it needs `@solana/kit` (web3.js v2) or hand-rolled serialisation, which is a real piece of work
    rather than a configuration change.

  A protocol limit and a library limit are two different walls with the same number written on
  them. Check which one you are standing against before sizing anything.
</Warning>

Choose the lookup table with `chooseMarketTable` (Rust: `lookup_table::choose_market_table`). It
tries the market's own table when the SDK names one, then the **shared table** `SHARED_KIND2_TABLE`,
and returns the first one that passes the SDK's check. It never skips a market for want of a table,
so a market listed after your SDK version still gets one. You do not need a new release.

Every table is **frozen**: `authority = NONE`, so nobody, including us, can change what it resolves.
No table is ever extended. A new table address reaches you only in an SDK release: either an
optional table of a market's own, or a new shared table when a protocol-wide change adds an account to
`open` or `settle`. `chooseMarketTable` picks up either one. Read a table's
contents once and cache them by address for the life of your process.

Pass as `required` the protocol accounts your transaction names: `config` and `vault` (from the SDK's
`configPda()` and `vaultPda()`) and, if you read it, `vault_token`. Pass `protocol_token`
(`config.treasury`) as `rotatable`, never in `required`: the treasury can change after a table is
frozen, and a table that lacks it costs 31 bytes and a warning, never the table. Never pass a trader's
or a position's account.

```ts lookup-table.ts theme={"dark"}
import { Connection, PublicKey } from '@solana/web3.js';
import type { AddressLookupTableAccount } from '@solana/web3.js';
import { chooseMarketTable, configPda, vaultPda } from '@rekt/1000x';

/** Contents by table address. A frozen table never changes, so this cache never goes stale. */
const cache = new Map<string, AddressLookupTableAccount>();

export async function marketTable(
  connection: Connection,
  marketAddress: PublicKey,
  treasury: PublicKey, // config.treasury, from your own config read
): Promise<AddressLookupTableAccount[]> {
  const chosen = await chooseMarketTable({
    market: marketAddress,
    // The protocol accounts this transaction names. Never a trader's or a position's account.
    required: [
      { name: 'config', address: configPda().address },
      { name: 'vault', address: vaultPda().address },
    ],
    // Can change after a table is frozen: a warning when a table lacks it, never a refusal.
    rotatable: [{ name: 'protocol_token', address: treasury }],
    fetchTable: async (address) => {
      const hit = cache.get(address.toBase58());
      if (hit !== undefined) return hit;
      const got = (await connection.getAddressLookupTable(address, { commitment: 'confirmed' })).value;
      if (got !== null) cache.set(address.toBase58(), got);
      return got;
    },
  });
  // Bytes, never a refusal: an account composed as a static key, or the shared table in for a market's own.
  for (const note of [chosen.standIn, ...chosen.warnings]) if (note !== null) console.warn(note);
  return chosen.table === null ? [] : [chosen.table]; // []: compose legacy, and measure it with dryRun: true
}
```

<Note>
  **The two kinds of table are checked differently.** A market's own table must contain that market
  and every `required` account, so a table address copied from another market is refused. The shared
  table does not contain the markets it serves, so it is checked against
  `SHARED_TABLE_PROTOCOL_ACCOUNTS` instead, and a `required` account that it lacks composes as a static
  key (+31 B). A `rotatable` account that the chosen table lacks is a static key (+31 B) on either
  kind. When a market's own table is refused, the shared table stands in for it (`chosen.standIn`
  says why). The shared-table rules apply only when `shared: true` is passed for `SHARED_KIND2_TABLE`:
  `chooseMarketTable` does this for you, and `marketTableRefusal` and `marketTableRefusalSized` never
  do, so they refuse the shared address for any market it does not hold.
</Note>

<Note>
  `submitBenchmarkQuoted`, the sender for instructions that carry a quote, refuses a lookup table on a
  legacy message rather than dropping it silently, so "I passed a table" and "the table was used"
  cannot come apart. Measure your real byte count with `dryRun: true` — it builds and signs exactly
  what would be sent, and sends nothing.
</Note>

<Note>
  If you check a table yourself instead of calling `chooseMarketTable`, hold each table to its OWN
  size. Call `marketTableCheckSized({ market, address, table, required, shared })` (Rust:
  `lookup_table::market_table_check_sized`) on each of `marketTableCandidates(market)`, passing that
  candidate's `shared`, and keep `protocol_token` out of `required`. It looks up the size and refuses a
  table the SDK has no size for. Tables differ
  in size, so if you pass one minimum for every table (such as `MAINNET_MARKET_TABLE_ADDRESSES`) to
  `marketTableRefusal`, it refuses a good table, and you compose legacy without knowing why.
</Note>

## Closing: a session key can do it

<Warning>
  **A session key CAN request a close, and it can move a take-profit target**, unless its grant
  carries `DELEGATE_NO_CLOSE`.

  `request_close` and `set_take_profit` share one authoriser, so a session key authorised for the
  position reaches both. The settle that completes a requested close is sent by anyone, and the five
  other close reasons — knockout, take-profit, the two expiries and `lastResort` — are permissionless
  and need no signature from the trader at all.

  A grant with `DELEGATE_NO_CLOSE` cannot request a close and cannot move a target.
</Warning>

## What a session key costs you if it leaks

State this to yourself plainly before you ship, because the arrangement is deliberately **weaker**
than signing each trade with the wallet:

* **The exposure is the SPL allowance, and the allowance is spendable to any destination.** A
  leaked key can open positions until the allowance, the window budget or the expiry stops it — and
  it can skip the **window and the expiry**, because those are this program's and the approval is
  the **token program's** grant: one `Transfer` signed by the key moves up to the remaining
  allowance wherever the holder likes, without touching this program at all. **It cannot skip the
  allowance.** That one is the token program's own ceiling and it binds every transfer — measured
  rather than argued: with $250 approved, exactly $250 moved, and with the approval removed the
  identical instruction moved nothing. SPL Token scopes an approval to an amount and a delegate key,
  never to a program, so revoking one is a separate act from letting a session expire.
* **It CAN close a position and CAN move a take-profit target — unless you withhold that.** Both go
  through the same authoriser as a session-signed close. A leaked key can therefore close a trader
  out at a moment of the attacker's choosing, or cap a winning position's upside by lowering its
  target to just above the current price. That is real harm and you should size the session for it.

  `authorize_delegate` and `renew_delegate` take `(duration_s, window_seconds, window_limit,
  allowance, restrictions)`. **Read `TradeDelegate.restrictions` back after every authorisation and
  every renewal**; `sessionStanding` returns it as `restrictions`.

  Pass `restrictions: DELEGATE_NO_CLOSE` to `authorizeSession` and the session can neither close nor
  retarget (`DelegateMayNotClose`, 6156). **Passing `0` grants close and
  retarget only — NOT LP.** LP is opt-IN: OR in `DELEGATE_ALLOW_LP` to let the session key trigger
  `requestRedeem` and `armRedeem`, and leave it out — the default — to keep the key clear
  of the trader's pool stake. **`restrictions` is required — there is no default**, on `authorizeSession` and on
  `renewSession` alike; omitting it is a build error, and with the types erased it throws. **A
  renewal re-states the byte rather than keeping it** — renew a session with `0` and you have granted
  it a Close button and stripped any LP grant. Read the current value off
  `TradeDelegate.restrictions` and pass it in.

  A bit the program does not know is refused (`DelegateRestrictionUnknown`, 6157), so check
  `DELEGATE_RESTRICTIONS_KNOWN` before you send.

  The owner is never restricted by this: a trader signing with their own wallet can close, unless a
  guardian stop is set (`config.settlesDisabled`, or `config.oracleFrozen`, which leaves a priced
  close no price).
* **It CAN spend the SPL allowance you granted it, to any destination.** Authorising a session
  approves the session key as an SPL delegate over the trader's USDC account for the amount you
  choose. That approval is a token-program grant: a holder of the key can transfer up to the
  remaining allowance without this program being involved, so none of the venue's own limits — the
  spend window, the session expiry — apply to it. **The allowance is the ceiling on what a leaked
  key can take. Size it for one session.**
* **Revoking clears both halves.** `revokeDelegate` ends the session and withdraws the SPL approval
  in the same instruction, so revoke rather than waiting for expiry — **expiry ends the trading
  rights and does not withdraw the approval.** If the trader's token account was frozen or closed
  when you revoked, send a bare SPL `revoke` against it once it is usable again.
* **Settlement proceeds cannot be redirected.** A settle pays the destination recorded on the book,
  and no session key can change that record.
* **LP actions are opt-IN, and even when granted the value stays the owner's.** By default a session
  key cannot touch the trader's pool stake at all. Grant `DELEGATE_ALLOW_LP` and it may TRIGGER
  `requestRedeem` and `armRedeem` — but the shares and every payout stay the owner's,
  pinned by the account structs, so nothing reaches the session key. The bounded harm of a leaked
  LP-enabled key is that it can request a redemption at a value trough and arm it, forfeiting the
  owner's unbonding-window yield to the other LPs and trapping the owner at the trough until the
  ticket matures — bounded by the expiry, the unbonding window, this opt-in, and your revoke.
  Authorising another delegate still needs the owner.
* **Size the allowance for the session, not for the year.** The SDK's default is
  `DEFAULT_DELEGATE_WINDOW_LIMIT`, **\$1,000** (`allowance ?? terms.windowLimit`). Granting a large
  allowance to save a renewal prompt is trading the trader's money for your convenience. A small
  allowance has a cost too: a trader who exhausts it meets a wallet prompt at exactly the moment the
  session existed to remove one. **Pass `allowance` explicitly.** Omitting it takes a decision you
  have not made.

  *And note the two quantities are not the same thing.* "One window's worth" is a per-window
  **rate ceiling** used as a lifetime **total** — a coincidence of construction, not a design. The
  three-clocks table above is the authority: the allowance is *a total that draws down and never
  refills*.
* **Revoke on logout.** It is one instruction, it is instant, and it refunds rent.

## Tell what state a session is in, from the chain

Two accounts, and one call.

<Steps>
  <Step title="Read the delegate PDA">
    `delegatePda(owner)` — seeds `[b"delegate", owner]`, one per trader. `decodeTradeDelegate`
    gives you `expiresAt`, `delegate`, `windowLimit`, `windowSpent`, `windowSince`, `restrictions`
    and `tokenAccount`.
  </Step>

  <Step title="Read TradeDelegate.tokenAccount, and never a guess">
    `revokeDelegate` pins its `trader_token` to that exact address and refuses any other with
    `WrongDelegateTokenAccount` (6122). `decodeTokenDelegate` reads the SPL side of it: the
    account's state, its delegate slot, and the allowance still standing.
  </Step>

  <Step title="Ask for the standing">
    `sessionStanding` names the combination, because the combination is what decides what to do.
  </Step>
</Steps>

```ts sessions-state.ts theme={"dark"}
import { chainNowSeconds, decodeTradeDelegate, delegatePda, sessionStanding, unhex } from '@rekt/1000x';
import { PublicKey } from '@solana/web3.js';

const [pda] = await connection.getMultipleAccountsInfo([delegatePda(owner).address]);
const view = pda ? decodeTradeDelegate(new Uint8Array(pda.data)) : null;
const [tok] = view
  ? await connection.getMultipleAccountsInfo([new PublicKey(unhex(view.tokenAccount))])
  : [null];

const standing = sessionStanding({
  delegate: view,
  tokenAccount: tok ? new Uint8Array(tok.data) : null,
  // The chain's clock: the program judges a session's expiry against `Clock.unix_timestamp`.
  nowUnix: BigInt(await chainNowSeconds(connection)),
});
```

| `kind` | the delegate | the SPL approval | `action` |
| - | - | - | - |
| `none` | no account | none | nothing to do |
| `active` | live | live, to this key | `resume` |
| `allowance-exhausted` | live | spent or cleared | `renew` |
| `displaced` | live | live, to **another program's key** | `renew` |
| `expired-approval-live` | **expired** | **live** | `revoke` |
| `expired` | expired | gone | `revoke` |
| `orphaned-approval` | **no account** | live | `revoke-spl-only` |
| `frozen` | either | unreachable | `revoke`, then a bare SPL `revoke` after the thaw |
| `unknown` | — | **not read** | read it |

<Warning>
  **`expired-approval-live` is the state to alert on, and no error code will tell you about it.**
  The venue side has a clock; the SPL approval does not, and an expiry does not close the account.
  A trade attempted by an expired key is refused with `DelegateExpired` (6108) — which reads as
  *renew and carry on* and says nothing about a standing authority over the trader's USDC.

  Pass `tokenAccount: null` for an account you read and found closed. Leave it `undefined` only
  when you have genuinely not looked — `sessionStanding` then answers `unknown` rather than
  calling the session active.
</Warning>

### The four pathways

| pathway | send | who signs | cost | when |
| - | - | - | - | - |
| **resume** | nothing | — | nothing | `kind: 'active'` |
| **renew** | `renewSession` | the **trader**, never the key | one signature, rent unchanged | expired, budget bound, allowance spent, approval displaced, or the key must be rotated |
| **close and reopen** | `endSession`, then `authorizeSession` | the trader, twice | two signatures; rent returned then re-paid | you are changing the token account — a renewal may not move it (`DelegateTokenAccountMayNotChange`, 6137) |
| **revoke** | `endSession` | the trader | **less than nothing** — the account closes to the trader and returns its rent | logout, a leaked key, or any expired session |

* **A renewal resets the spend meter and re-states `restrictions`.** Read the current byte off
  `TradeDelegate.restrictions` and pass it back; `0` is not *unchanged*, it is *this session may
  close positions*.
* **A renewal re-approves the SPL allowance in the same instruction**, so it is the fix for an
  exhausted or displaced approval as well as for an expiry.
* **Rotating a leaked key is a renewal, not a reopen.** `renewSession` takes a different
  `delegateKey`, and the approval moves to it in the same instruction.
* **`endSession` is safe on a session that already expired, and safe to send twice.** Revoking a
  token account that holds no delegate is a no-op in the token program.

### What breaks loudly, and what breaks silently

Monitor the silent column. React to the loud one.

| loud — the transaction fails and names it | silent — nothing fails until later, somewhere else |
| - | - |
| an instruction sent with **too few** argument bytes → `InvalidInstructionData` | an instruction sent with **too many** argument bytes → the extra bytes are discarded, and it succeeds |
| a now-required account left out → `NotEnoughAccountKeys` | another program's `approve` taking the token account's single delegate slot |
| a revoke naming the wrong token account → `WrongDelegateTokenAccount` (6122) | the allowance drawing down to zero, which also clears the delegate slot |
| an account shorter than the struct → `AccountDataTooSmall` | the delegate expiring while its approval stands |
| an account whose discriminator moved → `InvalidAccountData` | a renewal writing `restrictions: 0` over a restriction the trader granted |

<Note>
  **A token account holds one delegate slot.** SPL Token's `approve` writes it unconditionally, so
  any other program the trader approves takes it from this session, with nothing emitted. The
  session keeps passing every gate this program can check and fails inside the token program on the
  next open. `sessionStanding` reports it as `displaced`; a renewal takes the slot back.
</Note>

## The refusals a session meets

None of these appear in `openGates` — they are checked by the program very early, before the other
gates in that report. `delegateGates` predicts the first
four; read it.

| Error | Code | Retry | What it means, and the fix |
| - | - | - | - |
| `DelegateExpired` | 6108 | never | The absolute deadline passed. Do not retry with this key. The trader sends `renewSession`, or signs the request directly. |
| `DelegateWindowLimitReached` | 6109 | after-state-changes | This window's ceiling is used up. The budget decays continuously, so waiting genuinely works — or renew with a higher limit. |
| `NotTheDelegate` | 6110 | never | The signer is not this trader's authorised key. Re-authorise. |
| `DelegateMayNotSign` | 6115 | never | The owner signed **and** passed the delegate account. Pass `delegate: null` when the owner signs for themselves, or the session budget is charged for a trade it did not authorise. |
| `DelegateMayNotClose` | 6156 | never | The session's grant carries `DELEGATE_NO_CLOSE`, and this was a close or a retarget. The owner may close with their own signature, unless a guardian stop is set (`config.settlesDisabled`, or `config.oracleFrozen`, which leaves a priced close no price). |

And one that is not this program's at all: `InsufficientFunds` from the SPL Token program, when the
allowance is exhausted. `sessionAllowanceGates` is the only thing that predicts it.

## Where to go next

<CardGroup cols={2}>
  <Card title="Quotes and freshness" icon="satellite-dish" href="/sdk/quotes">
    What a quote is, why a settle can be refused with `6007`, and what to do about it.
  </Card>

  <Card title="Opening a position" icon="paper-plane" href="/sdk/opening">
    The transaction shape, the two hard walls, and building the instruction.
  </Card>

  <Card title="Reading gates" icon="traffic-light" href="/sdk/gates">
    Every protocol refusal, before you send, with its measured value.
  </Card>

  <Card title="Error handling" icon="bug" href="/sdk/errors">
    The full catalogue, and whether retrying can ever help.
  </Card>
</CardGroup>


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