The loop
Five steps, in this order. Skipping step 2 or 3 is how a client ends up learning the protocol from reverted transactions.1
Read the protocol
Config, vault and market, decoded from the chain — Read the protocol. Addresses are
derived locally with
configPda, vaultPda, marketPda, so you never trust an address printed
on a page. Never cache config.params: every field in it is governed and can move without a
program upgrade.2
Price it
openPreview / costToOpen tell you what the trader is actually charged and the gross payout at
the target, which is before the exit fee and the share of the gain the terms withhold. Do not recompute protocol arithmetic yourself — a second derivation of a number is how
a client and a chain come to disagree.3
Ask what would refuse it
openGates and settleGates return every gate with the measured value and the threshold —
Reading gates. Branch on the first failing gate.Order is a per-function property. openGates follows the program’s order for an open.
settleGates emits its own sequence and does not reproduce the program’s settle order: the
program checks the reason before the price, and settleGates appends the reason at the end.
Neither report reads the guardian flags; read config.oracleFrozen and
config.settlesDisabled yourself. An agent that sends to find out is paying for information it
could have had for free.4
Send, then wait for the step to end
An open, an add and a close request carry no quote. Build an open with
ix.open(accounts, args, priceAccountsFor(market, config, 'open')), an add with
addCollateralForMode and a close request with ix.requestClose, sign, send, and then wait for
the event that ends each step: EntryFilled or OpenVoided for an order, CollateralAdded or
AddReversed for an add, SettlementPriced for a close. A landed transaction is an order or a
request, never the result. Orders and closes is the contract, and
Opening has the code.A quote matters only if you send an instruction that carries one yourself: a settle, a
stamp_book or a set_take_profit. Those age out in slots, not seconds, judged when the
program runs rather than when you obtained the quote. Obtain a new quote immediately before
signing, sign locally, and never put a human approval between the two. See
Settling and Quotes and freshness, and
Getting prices to obtain one.5
Reconcile
Compare what you predicted against what the chain moved. A client that never checks is a client
that will be wrong quietly. Decode the events rather than inferring from balances: the SDK
exports
decodeSettlementPriced, decodePositionOpened, decodePositionStamped,
decodeEntryFilled, decodeOpenVoided, decodeAddReversed and decodeSettlementReceipt, and
readTradeHistory reads them back off a cluster. Join every event on openerTag.Refusals: branch on retryable, never on the name
The error catalogue is generated from the SDK’s client IDL, so it cannot drift from what the chain
raises — Error handling. The one field that matters to an autonomous client is
retryable:
Resolve with
errorForCode / errorForName. Do not pattern-match error strings. The names are
an ABI and the codes are stable; your regex is neither. For the order and close codes, 6203 to 6238,
bind to retryClassOf(code): it says “done”, “send the other instruction” and “retry with the right
quote”, which retryable cannot.
Two refusals that mean something different from what they sound like:
PositionSlotsFull— the trader’s book is full, not the venue. A book holds a fixed number of positions, chosen when it was created and changeable afterwards.SideStakeCapReached— a market’s limit on one side’s stake is set and that side is full. It clears as other positions settle. Back off; do not spin. A pool that cannot cover an order is not a refusal: the order is returned with the whole stake.
What constrains you, whatever you are trying to do
These hold on chain and no client can talk its way past them:- The cover rule. A fill is admitted only if the pool can cover it. The number of concurrent positions the venue admits depends on the pool and the queue. An order that does not fit is returned with the whole stake and no fee: treat it as a state, not a fault.
- A win may be paid later. A win is paid in full, but part of it may be paid later, in order, and
SettlementPriced.traderis only what was paid now. Never present it as the amount won: readSettlementQueued. - Money in = money out on every path. The program never mints and never burns.
- The pauses close the entry, not the exit. A pause of the venue or of one market stops opens,
adds and take-profit retargets;
settleand LP exits continue. An order already placed is still filled or returned. - An order is not a position, and a request is not a close. The entry is the venue’s next price after a short delay, and the exit is the first price after the close’s target, by the window rule. Never show a price from the tap as the entry or the exit.
- A stop can freeze both prices and closes. An oracle freeze refuses opens, adds and
lastResort, and starves every priced close of a price. A settle stop refuses every close reason. Only the config authority lifts either. Readconfig.oracleFrozenandconfig.settlesDisabled, and treat either as a stop, not a retry. - A market with trading hours only accepts opens and adds during its own session. The calendar is on
the market account (
market.sessions). Read it, or callavailability()for the state and the next open, and do not assume that a market is always open. See markets.
Units
- USDC in base units, 6 decimals.
250000is $0.25. - A payout can be large relative to collateral. Format money as dollars in anything a human reads; a
figure printed as
25007500will be misread as twenty-five million. - Rates and limits are in basis points or micro-basis-points on chain. Read them from the account, never from a constant you copied.
Who signs
An open, an add and a close request carry no quote, so a person can approve each one in a wallet, and an autonomous client signs locally. The instructions that carry a quote areset_take_profit
and any settle, stamp_book or prove_price you send yourself. A quote ages out in slots, so a
human approval between the quote and the send cannot land: sign those locally.
If you are trading a book that is not your own, one option is for the owner to authorise a
session-key delegate your agent signs with; if you are trading your own, you are already a local
signer. How you sign is your decision. Put any human approval on the authorisation, which
carries no price and races nothing.
If you choose the program’s session-key delegate, it is four moves — session keys
has working code for each:
- Authorise.
authorizeSession— one instruction the owner signs, setting the expiry, the spend window and the SPL token allowance together. - Fund it. The fee payer must sign, so on this route it is not the human — the session key itself, or a separate sponsoring fee payer you add. Give whichever pays its own lamports and watch the balance.
- Sign every trade with it.
authorityis the session key,delegateisdelegatePda(owner).address, and the book, the token account and the money all stay the owner’s. - Revoke when the mandate ends.
endSessioncloses the account and drops the SPL allowance in one instruction, in the same slot, and returns the rent.
delegateGates covers the four refusals openGates does not model, and
sessionAllowanceGates covers the SPL allowance — a total that draws down and never refills,
which is what actually ends most long sessions, arriving from the token program as
InsufficientFunds in none of this program’s error shapes.
A session key can open AND request a close, unless its grant carries the no-close
restriction. The program authorises a
request_close for the owner or for an unexpired session key
bound to them, and the request saves a wallet prompt and races nothing. The settle that completes
it is sent by anyone, and the other five close reasons are permissionless and need no signature
from either.Five things an autonomous client must never do
- Never route around a refusal. A guard that refuses you knows something you do not. Read the gate, fix the request.
- Never cache governed state.
config.params, market parameters and the session calendar all move on chain. Re-read them, and show how old your copy is. - Never put a credentialled RPC endpoint in a command line, a log, a commit or a screenshot. Reference it by variable name — Installation.
- Never publish a model output as a fact. Implied odds, projections and annualisations are yours to compute and yours to label. What the chain settled is a fact; what your model expects is not.
- Never sign a second order for the same intent after a timeout. Inside one blockhash life, send the same signed bytes again. A second signature is a second order, and both can land.
