Skip to main content
An open and an add_collateral carry no price. Each one takes the stake when it lands and becomes an order that waits for the venue’s next price. A close is a request that carries no price, and anyone may complete it with a priced settle. This page is the contract a client or a bot builds against. A bot that opens, adds and requests closes needs a wallet and an RPC endpoint. It needs no access to the price feed, no token and no key from us. Only settle, stamp_book, set_take_profit and prove_price carry a quote: see getting prices.
Build an open with ix.open(accounts, args, priceAccountsFor(market, config, 'open')), as opening a position shows, an add with addCollateralFor(market, config, accounts, args) and a close request with requestCloseFor(market, accounts, seq). Each reads the market’s feed kind and the governed config: decode both on every request. On a feed-kind-3 market (isKind3Market(market)) the open, the add and the close request each carry the governance table, and the program refuses them without it (GovTableRequired). These three add it. Never build an open with openForMode: it leaves out the market’s fee-terms account and the governance table.

The life of a position

An open lands as an order. A fill makes it a live position, or the order is returned with the whole stake. A live position ends at a close request, its barrier, its target or a timer, and a settle closes it. How a position ends has every close reason, and the glossary has the words.

The timing settings

Every wait on this page is a count of slots, and every count is a governed setting. Read them through the SDK on each request. Do not compile one into a client. Each setting can change by a governed write with no program upgrade. The SDK is the source:

Opening: an order, then a fill

  1. Send open. It carries no quote. Its remaining accounts are the governance table on a feed-kind-3 market, then the market’s fee-terms account when the market has terms (priceAccountsFor(market, config, 'open')). When it lands, the stake is taken, the slot holds the order with entryPrice 0, and PositionOpened carries the sequence number. It is an order, not yet a position.
  2. The entry price is the first price of the venue’s price feed at or after d slots from the slot in which the open landed. On a feed-kind-3 market the delay is clock time from the second the open landed, and the entry is the first signed tick at or after it. A listed filler writes it. Read it from the event EntryFilled (entryPrice, quoteSlot), or from a non-zero entryPrice on your own book. On a feed-kind-3 market quoteSlot is the slot the fill landed in.
  3. Or the order is returned. The event is OpenVoided: cause, refundTotal, feeKept and delivered. An order is returned when the first price came too late or did not meet the venue’s quality rule, when the market closed, or when nothing filled it inside d + T_fill slots of the order (for example, when the pool could not cover it). refundTotal is the whole stake, and feeKept reads 0. delivered false means the payout destination could not receive the refund, and it is held on the book: claim it with claimPayout.
  4. You cannot fill your own order (NotAListedFiller, 6205). You need not return it either: after d + T_fill slots anyone may send void_open, and the venue’s keepers do.
voidReasonOf(cause) names the cause, and VOID_REASON_TEXT has the words for a trader. The fill is the step that judges the price. So the price rows of openGates do not decide an order, and the quote refusals of quotes and freshness cannot refuse one. Build with ix.open and priceAccountsFor(market, config, 'open'), send, and then watch the book. The EntryFilled or OpenVoided event is in the filler’s or the keeper’s transaction, not in yours:

Doubling down

Send add_collateral with no quote. CollateralCommitted is in your own transaction: the added stake is taken (addedCollateral, totalDebit), the timer extension is written at once (timerAfterS), and addTarget is the slot d after the add. Then one of two:
  • CollateralAdded. A filler priced the add at the first price at or after addTarget. It carries the new entry (entryPrice) and the entry before it (entryPriceBefore).
  • AddReversed. The added stake is returned in full (refundTotal), as for a returned order. cause is one of the open’s causes, or 8 (the price was outside the position’s limits: at the add’s price the position was already at its barrier or its target) or 9 (the position was knocked out first).
Rules a client must handle:
  • Until one of the two arrives, and then for M_add slots after the price of a filled add, a close request, a retarget, an expiry mark and a second add return AddPending (6211). closableFromSlotOf includes that time.
  • A reversed add keeps the time it added to the timer. Its stake comes back.
  • A knockout price from before the add still counts in that time. A price that breached the barrier of the entry in force at its slot can still prove the knockout. KnockoutBeforeAdd reports it, the position is knocked out on its old stake, and the added stake comes back (AddReversed, cause 9).
  • A touch of the take-profit target while an add waits for its price cannot be stamped, and it is lost if the add is then filled.
  • addCollateralFor refuses to build an add while adds are off (AddsDisabled, 6212). On a feed-kind-3 market the add carries the governance table: build it with addCollateralFor, not addCollateralForMode, which refuses a kind-3 list.

Closing: a request, then a settle

  1. Send request_close, signed by the owner or a session key. It carries no quote, so it cannot fail on a stale price. On a feed-kind-3 market it carries the governance table (requestCloseFor adds it). CloseRequested.exitTarget is the slot d after the request; on a feed-kind-3 market it reads 0, because that market’s exit target is a clock second, not a slot.
  2. The exit price is the first price of the venue’s price feed at or after exitTarget. The close is done when SettlementPriced and SettlementReceipt arrive.
  3. Anyone may send that settle, and the venue’s keepers do. isPermissionless('voluntary', phase) is true once the phase is ExitRequested.
  4. The window rule. If nobody settles inside W slots of the target, the price becomes the first price of the next window of W slots, and so on. SettlementReceipt.window is the count of windows that rolled: 0 for a close settled in its first window.
  5. A request cannot be withdrawn. A second request returns ExitPending (6213): treat it as success.
  6. SettlementPriced.unverified reads true while a market lists one price feed. A price from a single feed is unverified by construction; it does not mean the price was wrong. Do not show a warning or a badge from it.
Never show “your close price is locked”. Until the settle lands, the next window’s price can apply, and a knockout price in between counts.
request_close, mark_expiry and open need no quote instruction: send each one with the compute-budget pair. computeUnitLimit(built.name) gives the declared limit, and for the open computeUnitLimit('open').

The minimum hold

A position cannot be closed by its owner in the first slots after its entry price.
  • The venue has one hold for every market: resolveMinHoldSlots(config.minHoldSlots). A market can have a longer one: market.closeHoldSlots (0 is none). The hold in force is the larger of the two (holdInForce).
  • It counts from the entry price: EntryFilled.quoteSlot, and after a filled double-down from the add’s price, which is the position’s openedSlot on the book.
  • A close request inside it returns MinimumHoldNotMet (6103) for the venue’s hold or MarketHoldNotMet (6236) for the market’s. Neither one is a failure of the position.
  • “Can close in N seconds”, with no guess: closableFromSlotOf(position, config, market) is the first slot at which a request can land, from three public accounts. It is null while the position is an order or an add waits. slotsUntil(slot, nowSlot) counts the slots, and estimatedSecondsUntil(slots, slotMs) turns them into an estimate that you label as one. Pass a slot time you measured (getRecentPerformanceSamples), not config.params.slotMs, which is longer than a mainnet slot.
  • The request must land at or after that slot.
  • A take-profit proof must also clear both holds, on the slot of its own price.
  • A knockout is never held back. A market’s own hold never holds back a timer end.

Timers

  • From the deadline, anyone may send mark_expiry with reason 3 (userExpiry) or 4 (tenorExpiry). ExpiryMarked.exitTarget is the slot d after the mark, and the exit price is the first price at or after it, by the same window rule as a close.
  • A settle with an expiry reason on a position that has no exit target returns ExitNotAnchored (6214): mark it first.
  • A wrapper such as Minute sends the mark and then its own settle from slot exitTarget on: two transactions, at least d slots apart.
  • A timer is 0 or at least 26 seconds at the open. After an add, the position’s deadline must be at least 26 seconds after the add lands (TimerBelowFloor, 6225).

Knockouts and take-profits

  • Anyone may prove a level with any signed price that is newer than the entry and at most W_ko slots old. So a position can be knocked out by a price up to W_ko slots old.
  • After a close request or an expiry mark, only a price that is not later than the exit price can prove a level (ExitPending otherwise).
  • While an add waits for its price, a touch of the take-profit target cannot be stamped, and it is lost if the add is then filled.
  • After a take-profit retarget, only a price at least d slots after the retarget proves the new target (TakeProfitProofTooEarly, 6220).
  • stamp_book records the level on the book; the settle that pays it follows. It is the only stamp that can prove a knockout with a price from before a filled double-down.

Settling or stamping for others

This is optional. The venue’s keepers send settles and stamps.
  • A settle of a requested or marked exit needs the window quote: the first price of the venue’s price feed at or after the start of the current window, exitTarget + K × W, where K is the count of whole windows since the target. Any other quote returns NotWindowQuote (6217).
  • stamp_book needs any signed price that breaches the level, inside the W_ko look-back.
  • To obtain a quote, see Getting prices.

Two more rules

  • A client may settle its own requested close in the first window, with the window quote. That keeps the first price. If nobody settles in W slots, the next window’s price applies, and a knockout price in between counts. The count of rolled exits is public: SettlementReceipt.window is not 0 for them.
  • Read a position through the SDK’s accessors: phase, isUnfilled, pendingAdd, addTargetSlot, addMemo, memoIsLive, exitTargetSlot, exitReason and tpFloorSlot. The same raw bytes of a position mean different things in different states. See reading positions.

What a client must never assume

  • That the price on screen at the tap is the entry or the exit. It is the next price after the delay.
  • That a landed open is a position. It is an order until EntryFilled.
  • That a landed request_close is a close. It is a close at SettlementPriced.
  • That PositionOpened carries the entry. It has no price field. A position’s entryPrice on the book is 0 until EntryFilled. Take entries from EntryFilled and CollateralAdded, and returns from OpenVoided and AddReversed.
  • That a price in one of Minute’s round events is a price. See integrating Minute.
  • That a second transaction is safe after a timeout. Never sign a second order for the same intent inside one blockhash life. Send the same signed bytes again.
  • That adds are on. addCollateralFor refuses to build one while they are off.
  • That a void or a reversal is an error. It is a normal outcome, and the stake comes back (refundTotal).
  • That two events in one slot are in time order. Order by quoteSlot.
Join every venue event on openerTag.

Events

Each has a decoder: decodeEntryFilled, decodeOpenVoided, decodeCollateralCommitted, decodeAddReversed, decodeCloseRequested, decodeExpiryMarked, decodeKnockoutBeforeAdd and decodeSettlementReceipt. On a landed transaction, receiptOf and venueEventsNamed(receipt, 'EntryFilled') read them all. Every event carries owner, market and seq, and the events of a position carry its openerTag.

Refusals a client can get

The codes are fixed. retryClassOf(code) gives the class of each one, and RETRY_CLASS_LABEL the words: see error handling. When the venue stops taking orders, open and add_collateral return ProtocolPaused (6046) and config.paused is true, as for any pause. set_take_profit returns it too. Closes, timer ends and knockouts keep working, and an order already placed is filled or returned as usual. See risk.