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

# Error handling

> The refusals a client meets, with a cause, a fix, and the one thing a name cannot tell you: whether retrying can ever help.

The error catalogue is **generated from the SDK's client IDL**, so it cannot drift from what the
chain actually raises. What is hand-written is the part a name cannot carry: what caused it, what
to do instead, and whether retrying is useful or an infinite loop. This page lists the refusals a
client meets. `catalogue()` holds every code the program can return.

> [!WARNING]
>
> An agent that retries `RungBelowFloor` loops forever. One that gives up on `NoFreshPrice` fails
> a trade that would have landed on the next slot. **Branch on `retryable`, never on the name.**

## The four retry classes

| `retryable` | Meaning | What a client should do |
| - | - | - |
| `never` | The request itself is wrong. | Fix the request. Show the user what to change. |
| `after-state-changes` | The request is fine; the world is not ready, and it will get ready on its own — a clock, a settle, the price, a feed. | Re-evaluate when state moves. Back off for seconds to minutes. |
| `after-governance` | The request is fine, and only an instruction signed by the venue's config authority (`config.authority`) clears the refusal — a pause lifted, a flag set. Nothing changes on its own. | Do not retry on a timer. Poll the flag the `fix` names, back off slowly, and tell an operator. The venue's status page is [status.rekt.money](https://status.rekt.money). |
| `immediately` | Transient. | Retry on the next slot. These are almost all price freshness. |

Switch on all four. A client that folds `after-governance` into a default branch retries a venue-wide
stop every few seconds.

> [!NOTE]
>
> **The codes of orders and closes (6203 to 6238) have a finer class.** `retryClassOf(code)` returns one of
> nine classes for every code the program can raise, and `RETRY_CLASS_LABEL` has the words: for
> example "retry after the state changes", "retry with the right quote", "send the other
> instruction" and "treat as done". `contextOf(code)` adds the note for a code whose action depends on what
> was sent. Bind to the class, not to a remembered number. The table a client meets is under
> [orders, closes and stamps](#orders-closes-and-stamps).

## Resolving an error

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

export function explain(code: number): string {
  // null for a code this program cannot produce — do not assume every custom code is ours.
  const info: ProgramErrorInfo | null = errorForCode(code);
  if (!info) return `custom program error ${code} did not come from this program`;

  return [
    `${info.name} (${info.code})`,
    info.msg ?? '',
    `Cause: ${info.cause}`,
    `Do this: ${info.fix}`,
    `Retry? ${info.retryable}`,
  ].join('\n');
}

export const byName = errorForName('NoFreshPrice');
```

## Catching a refusal

Every write path in the SDK raises `ProgramRefusal`, which carries the whole catalogue entry rather
than a string — because an interface has to render the fix next to the failure, and a caller has to
branch on `retryable` without parsing prose.

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

export async function withRetry<T>(action: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; ; i += 1) {
    try {
      return await action();
    } catch (e) {
      if (!(e instanceof ProgramRefusal)) throw e;

      switch (e.retryable) {
        case 'never':
          // The request is wrong. Retrying is a loop; surface the fix instead.
          throw new Error(`${e.info.name}: ${e.info.fix}`);
        case 'after-governance':
          // Only the config authority clears it, and nothing changes on its own. Do not retry on a
          // timer: surface the fix, which names the instruction and the key, and alert an operator.
          throw new Error(`${e.info.name}: ${e.info.fix}`);
        case 'after-state-changes':
        case 'immediately':
          break;
      }

      if (i >= attempts) throw e;

      // `immediately` is a slot away; `after-state-changes` deserves real backoff. A slot is not
      // a constant — `config.params.slotMs` is governed (`0` reads as 400) — so read it if the
      // margin matters to you.
      const waitMs = e.retryable === 'immediately' ? 400 : 2_000 * (i + 1);
      await new Promise((r) => setTimeout(r, waitMs));
    }
  }
}
```

## The refusals a client actually meets

Everything below is a real entry in the catalogue. Names, codes and messages come from the client IDL.

### Orders, closes and stamps

An open and an add are orders, and a close is a request. The rules are on
[orders and closes](https://docs.rekt.money/sdk/orders.md). These are the refusals a client meets on that path.

| Instruction | Error | What the client does |
| - | - | - |
| `open` | `SideStakeCapReached` (6224) | Raised only on a market that has a side limit set. No market has one set. If one is set and the side is full: try later, or the other side. |
| `open`, `add_collateral` | `TimerBelowFloor` (6225) | Use a timer of 26 seconds or more, or none. |
| `open`, `add_collateral` | `ProtocolPaused` (6046), `MarketPaused` (6047), `OracleFrozen` (6190), `SettlesDisabled` (6191) | The venue or the market is stopped. Do not retry in a loop. |
| `add_collateral` | `AddsDisabled` (6212), `EntryNotFilled` (6203), `AddPending` (6211), `ExitPending` (6213) | Adds are off; wait for the entry; wait for the earlier add; a close is on its way. |
| `request_close` | `MinimumHoldNotMet` (6103), `MarketHoldNotMet` (6236) | Wait for the hold. `closableFromSlotOf` gives the first slot. |
| `request_close` | `ExitPending` (6213) | The close is already requested. Treat it as success. |
| `request_close` | `ModeBitOff` (6228) | Close requests are switched off. Do not retry in a loop. |
| `request_close`, `mark_expiry`, `stamp_book` | `OracleFrozen` (6190), `SettlesDisabled` (6191) | The venue is stopped. Do not retry in a loop. |
| `settle` | `ExitNotAnchored` (6214), `ExitQuoteNotYetDue` (6216), `NotWindowQuote` (6217), `ExitReasonMismatch` (6215) | Request or mark first; wait for the target slot; use the quote of the current window; send the recorded reason. |
| `settle` | `FeeTermsTableRequired` (6239), `QuoteOutsideSession` (6295) | The position froze fee terms and this close pays a gain or a fee: append the market's fee-terms account (`priceAccountsFor` adds it). On a market with trading hours, a quote signed at or after the close of the session that holds the deadline cannot price the position: do not retry with a later quote, the position ends at par by `lastResort` once it is due. |
| `stamp_book` | `ProofNotAfterEntry` (6218), `ProofTooOld` (6219), `TakeProfitProofTooEarly` (6220) | That quote can never prove it: it is not after the entry, it is too old, or it is before the target was moved. |

A returned order (`OpenVoided`) and a returned add (`AddReversed`) are not refusals. They are normal
outcomes, and the stake comes back (`refundTotal`).

### Opening

An open carries no quote, so no row below can judge one: the fill judges the first price, and a price
that fails returns the order whole.

| Error | Code | Retry | What it means |
| - | - | - | - |
| `OracleFrozen` | 6190 | after-governance | The guardian has frozen the venue's oracle. Opens and adds are refused, ahead of every other row; `SettlesDisabled` (6191) refuses them right after it. Only the config authority lifts either. Read `config.oracleFrozen` and `config.settlesDisabled`. |
| `ProtocolPaused` | 6046 | after-governance | Governance has stopped new positions protocol-wide. The pause stops opens, adds and take-profit changes. A close and a settle still run. Only the config authority lifts it; the pause has no expiry. |
| `MarketPaused` | 6047 | after-governance | This one market is closed to new positions and to take-profit changes. Others may be open. A close and a settle still run. Only the config authority reopens it; the pause has no expiry. |
| `MarketClosed` | 6094 | after-state-changes | The market's session calendar is shut. Opens resume when the next window opens: `availability()` reports that instant as `opensAt`, and `nextSessionOpenS(market, now)` returns it. `openGates` reports this on `open.session`. |
| `SessionEndsBeforeTenor` | 6095 | after-state-changes | The session is open, but after the market's settlement buffer less than five minutes of life is left to sell. Open earlier, or in the next session. `openTenorS` gives the life an open would get now; `openGates` reports this on `open.session`. |
| `InsideEventWindow` | 6048 | after-state-changes | A scheduled event window. Opens resume when it ends. |
| `CollateralOutOfRange` | 6049 | never | The stake is outside the governed bounds. Read them from the config account. |
| `LeverageAboveMarketTier` | 6148 | never | The `tier` argument is above this market's own. Every live market sells 1000x, so every open is 1000x: send `tier: 0`, which asks for the market's own. Read `market.tier` and offer nothing above it. |
| `LeverageBelowMinTier` | 6181 | never | The `tier` argument is below the venue's leverage floor for this market, so a lower leverage than 1000x lands here. The floor is `min(config.minTier, market.tier)` — resolve it against the market's own tier, not from `config.minTier` alone. `tier: 0` asks for the market's own and can never land here. |
| `RungBelowFloor` | 6116 | never | The rung is tighter than `RUNG_FLOOR_BPS` (20,000 bps = 2.00x). `rung_bps` is basis points of stake, not a whole multiple — passing `2` asks for 2 bps and lands here. |
| `RungAbovePayoutCap` | 6117 | never | The rung is above `payout_cap_multiple` — **the one take-profit ceiling**. Clamp to it, read from the config account. |
| `PositionSlotsFull` | 6056 | after-state-changes | Every slot is in use. A slot frees only when a settle for it lands: close one, or grow the book with `resizeTraderAccount`. |

A market with trading hours — see [markets](https://docs.rekt.money/markets.md) — refuses an open or an add when its session is
closed (`MarketClosed`) or too close to its end (`SessionEndsBeforeTenor`). Both clear on their own,
need no governance, and are not a mistake in the request. `availability()` says when the market opens.

### Settling

| Error | Code | Retry | What it means |
| - | - | - | - |
| `PositionNotFound` | 6057 | never | No live position carries this sequence number. Your handle is stale. |
| `NoFreshPrice` | 6007 | immediately | No quote cleared the slot age, the millisecond age and the signature floor **at the moment the program ran**. The refusal a settle meets most — [quotes and freshness](https://docs.rekt.money/sdk/quotes.md) is the page about it. Re-quote and resend; do not report it as a failed trade. |
| `SettlesDisabled` | 6191 | after-governance | The guardian has disabled settles. **Every close reason is refused**, `knockout` and `lastResort` included, and so are opens, adds, close requests, expiry marks, `stamp_book`, `set_take_profit` and the return of an unfilled order or add. Only the config authority lifts it. Poll `config.settlesDisabled`. |
| `NotPositionOwner` | 6072 | never | A `voluntary` close is authorised by the owner **or** by an unexpired session key bound to them. 6072 is raised only when no delegate account was supplied and the signer is not the owner. |
| `MinimumHoldNotMet` | 6103 | after-state-changes | `voluntary`, `takeProfit` and `userExpiry` must wait the minimum hold from the open or the last add: `resolveMinHoldSlots(config.minHoldSlots)` slots. `knockout` and `lastResort` are never gated. `tenorExpiry` is gated only on a market with a session calendar; on a market with no session calendar it is not. |
| `LastResortNotYetDue` | 6192 | after-state-changes | A `lastResort` close is legal on two grounds only. The market is dark: no posted price for 24 hours (`GovTable.darkAfterMin`), which needs the governance table among the remaining accounts. Or the position's finite frozen tenor plus the last-resort grace (`config.lastResortGraceS`, which falls back to the program's default of 24 hours when unset) has elapsed. A position with the indefinite tenor has only the dark ground. While a fresh price exists the close is refused `LastResortPriceAvailable` (6193): settle at market with a priced reason. |

While the oracle is frozen, a `lastResort` close is refused `OracleFrozen` (6190) and every priced
close fails `NoFreshPrice` (6007). If `NoFreshPrice` repeats on a settle, read `config.oracleFrozen`
before you retry again.

### Payout destination

| Error | Code | Retry | What it means |
| - | - | - | - |
| `PayoutDestinationWrongMint` | 6151 | never | The destination is not a token account on the vault's mint. |
| `PayoutDestinationFrozen` | 6154 | never | The destination is frozen and could not receive a settlement. Offer an unfrozen account the trader owns. |

### Adding to a position

`add_collateral` is an open in every respect but one — it grows a slot instead of creating one — so
**every refusal in the *Opening* table above can also refuse an add**, including the guardian freeze,
the pauses and the event window. These are its own. An add carries no quote: the freshness gate
does not apply, and an add whose price is at or through a level is returned (`AddReversed`) rather
than refused.

| Error | Code | Retry | What it means |
| - | - | - | - |
| `MinCollateralOutRequired` | 6168 | never | `min_collateral_out` (open) or `min_added_collateral_out` (add) was `0`. It is **mandatory**: the caller signs a floor on the position SIZE, not a debit ceiling, and `0` is refused by name. Set it to your predicted collateral — never a token value like `1`, which satisfies the check and protects nothing. |
| `BudgetBelowCollateralFloor` | 6169 | never | The budget resolved to a smaller position than your floor — a fee rose between quote and execution. Nothing was charged. Re-quote and raise the budget, or lower the floor (never to zero). |
| `BudgetExceedsMaxCollateral` | 6170 | never | The budget buys more than `max_collateral`. Over-budget is **refused, not clamped**. Lower the budget to at most the cost of a max-collateral position; `sizeForTotal` reports that total when it refuses. |

> [!WARNING]
>
> **`CollateralOutOfRange` (6049) answers three different facts on an add, and only one of them is
> worth retyping.** The minimum binds on the **increment**; the maximum binds on the **total**. So
> the code means the increment was under the floor, *or* the new total would be over the ceiling,
> *or* — the case that traps a user — the position already holds more than
> `maxCollateral − minCollateral`, so **no legal increment exists at all** and every value the
> trader types will be refused.
>
> A client must compute the admissible interval and render it, rather than showing one code and a
> text box. For example, with a minimum stake of $0.20 and a maximum of $1,000, a position holding
> more than **\$999.80** can take no add.

### Sessions, the pool and Minute

The refusals of a session key are on [session keys](https://docs.rekt.money/sdk/sessions.md#the-refusals-a-session-meets), and
`delegateGates` predicts them. A session usually ends at the token program with `InsufficientFunds`
when its allowance is spent, in none of the shapes on this page: `sessionAllowanceGates` is the only
thing that predicts it, and `renewSession` is the only call that resets all three clocks — expiry,
window and allowance — together. `MinuteDisabled` is on
[integrating Minute](https://docs.rekt.money/minute/integrating.md#markets-the-venues-per-market-gate). Minute raises codes of
its own, 6000 to 6050, and those numbers overlap the venue's: decode a code by the program that
raised it (`minuteErrorForCode` for Minute). The venue's own 6050 (`RungAboveMarketCap`) is reserved
and raised by nothing. `catalogue()` holds every other code the venue can
return, each with its cause, its fix and its retry class.

## Errors that are not the program's

**Three** failure classes reach you that have nothing to do with the protocol. Conflating them
sends people to debug the wrong thing, and the first one costs the most.

> [accordion] The send you cannot confirm
>
> **You broadcast a transaction and could not determine its outcome.** This is not a refusal and
> it is not a failure. It is the absence of an answer, and it is the class every client gets
> wrong first.
>
> **A blockhash expiring bounds how long a transaction can be ACCEPTED for. It says nothing about
> whether one already was.** So *"the blockhash expired, therefore nothing happened"* is not an
> inference — it is a guess.
>
> **Three rules:**
>
> 1. **Look the signature up before you give up.** `getSignatureStatuses` searches only a node's
>    *recent status cache* and answers `null` for a signature that landed but has aged out — and
>    if your RPC pool races providers, `lastValidBlockHeight` can come from one node and
>    `getBlockHeight` from another further ahead, expiring your hash early. **Ask
>    `getTransaction`, which finds a signature wherever it is.** A hit at `confirmed` is
>    definitive. Unknown is the honest answer only when it IS the answer.
> 2. **Say "unknown", never "nothing happened".** A client may say *refused*, it may say
>    *unknown*, and it may say *nothing moved* **only with a decoded program error code in
>    hand**. Carry the signature in the message so the trader can go and look.
> 3. **Keep local state on an unknown.** Delete a session secret, clear a pending record or free
>    a nonce only on a decoded refusal. The branch where it landed is the expensive one — see
>    [sessions](https://docs.rekt.money/sdk/sessions.md).
>
> **And decode before you pattern-match the text.** A client that tests the error message for
> `/reject|denied|cancel/i` before asking for the program error code can misread the program's
> own words, because a program message may carry *refused* or *cancel* in plain use. A landed, fee-paying revert read that way
> comes back to a trader as *"you declined it in your wallet"*, which is false. **Ask for the code
> first.** A wallet decline can never carry one, because the wallet refuses before a transaction
> exists.

> [accordion] RPC failure
>
> `AllProvidersFailed` when every configured endpoint failed. With `readCached` you get the last
> good value marked `stale` instead — render it with its age rather than showing a spinner that
> never resolves.

> [accordion] Composition failure
>
> `SenderRefusal` from the sender: the transaction could not be built or bound correctly. This is
> a bug in the caller, or a quote that does not match what the program expects. It is not a
> threshold and retrying does not help.

## The codes of the liquidity pool

The liquidity pool has refusals of its own for the cover rule, the queue and the wind-down. They are in
`catalogue()`, each with its retry class. Your `open` is never refused for cover. The filler judges
the cover rule at the fill and skips an order that does not fit (`CoverMarketFull` 6267,
`CoverGlobalFull` 6268); the order stays pending, and the timeout return gives back the whole stake
with no fee. Show "try again when the pool has room". A transaction that lacks a capital account is a
build error, and the SDK adds those accounts for you.

## Predict, do not discover

The best error handling is the error you never raise. Most of the *Opening* table above is predicted
by [`openGates`](https://docs.rekt.money/sdk/gates) before you spend a fee, with the value it measured and the threshold it
was judged against, and most of the *Settling* table by `settleGates` and `settlePreview`. Neither
report reads the guardian flags: read `config.oracleFrozen` and `config.settlesDisabled` yourself.

> [!WARNING]
>
> **`MinimumHoldNotMet` (6103) is in the *Settling* table and neither report predicts it.** The
> gate's only input is the position's opened slot, and neither `settleGates` nor `settlePreview` reads
> that field. `settleGates` emits its own rows — market pin, fresh price,
> exponent, reason legality, the position's frozen fee terms where it has any, outflow against
> reserve, liquidator token — and none of them is the hold; `settlePreview`'s `voluntary` and `takeProfit` legality is the signer and the price, with no
> slot term in it. A client that treats green gates as "the chain accepts it" meets 6103 on chain.
> An add re-stamps the position's opened slot with the current slot, so a close within the hold of
> an add meets it as well as one within the hold of the open.

The *Adding* table's shared refusals are predicted by `addGates`, the report for an add.

Use the catalogue for the cases you could not predict. Use the gates for everything else.


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