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 needs a wallet and an RPC endpoint. It needs no access to the price feed, no token and no key from us.
Build an open with ix.open(accounts, args, priceAccountsFor(market, config, 'open')), as opening a position shows, and an add with addCollateralForMode. Both read the governed config: decode it on every request. Never build an open with openForMode: it leaves out the market’s fee-terms account, so the position freezes the legal maximum of the exit fee and no rung check runs.

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 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. A listed filler writes it. Read it from the event EntryFilled (entryPrice, quoteSlot), or from a non-zero entryPrice on your own book.
  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 no filler landed inside d + T_fill slots of the order. 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.
  • addCollateralForMode refuses to build an add while adds are off (AddsDisabled, 6212).

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. CloseRequested.exitTarget is the slot d after the request.
  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', { quotePost: false }).

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.
  • 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 and after an add (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 every settle and every stamp.
  • 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.

Three more rules

  • Show “side full” before the tap when a limit is set. A market can carry a limit on the stake of one side: an amount, a share of the pool, or both, and the smaller one is in force. With no limit set, nothing refuses. sideRoom(direction, market, vault, vaultBalance) reads the room from the market’s two stake counters, its two limits and the pool’s assets. On a full side whoever lands first gets the room: there is no waiting list for it. A full side returns SideStakeCapReached (6224). (The queue of how a win is paid is a different thing: it orders payouts, not room.)
  • 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. addCollateralForMode 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 starts with owner, market, seq and 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. Closes, timer ends and knockouts keep working, and an order already placed is filled or returned as usual. See risk.