Skip to main content
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 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.

Your service signs

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.

A scoped delegate

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.

The trader signs each

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.
Whichever you choose, the downstream facts are the same: the program checks the signer. The pages on opening, orders and closes and reading gates do not assume a particular signing model. Gas is a separate decision — see who pays the fee below.

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:

It never holds funds

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.

Its trading has a hard deadline

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.

Its trading has a spend rate

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.

It is revocable, with one exception

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.
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. Size the allowance for one session, and revoke on logout rather than letting it expire.

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

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.
session.ts
Persist the secret BEFORE the authorisation is sent, not after it is confirmed. The two errors are not symmetric: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.
2

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.
authorise.ts
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.
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.
3

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). Either way it needs lamports. The simplest shape funds the session key in the same moment you authorise it.
fund.ts
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.
4

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.
trade-with-session.ts
5

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.
revoke.ts
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.

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.
session-gates.ts

The three clocks

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.
renew.ts
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.

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.
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.
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.
lookup-table.ts
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.
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.
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.

Closing: a session key can do it

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.

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 250approved,exactly250 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.
1

Read the delegate PDA

delegatePda(owner) — seeds [b"delegate", owner], one per trader. decodeTradeDelegate gives you expiresAt, delegate, windowLimit, windowSpent, windowSince, restrictions and tokenAccount.
2

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

Ask for the standing

sessionStanding names the combination, because the combination is what decides what to do.
sessions-state.ts
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.

The four pathways

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

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

Quotes and freshness

What a quote is, why a settle can be refused with 6007, and what to do about it.

Opening a position

The transaction shape, the two hard walls, and building the instruction.

Reading gates

Every protocol refusal, before you send, with its measured value.

Error handling

The full catalogue, and whether retrying can ever help.