Order Slip Channel
Topic: order_slip:{user_id}
Costs an order before you place it. Register the order you are considering, and this channel replies with what it would cost you (risk, fee, how much of it would fill immediately and at which prices), then re-pushes those numbers every time the book moves underneath it.
Registering an order here places nothing. Nothing is sent to the matching
engine, no funds are committed, and the numbers are a projection of the book as
it stands. Place the order through POST /api/v1/orders when you want it live.
Money and quantities on order_slip: follow the standard
wire format: money as a decimal string with
a minimum of four decimal places and any further precision preserved, quantities
as decimal strings with a minimum of two. Parse both with a variable-scale
decimal type.
Registering an order
Section titled “Registering an order”add_order
Section titled “add_order”| Field | Required | Type | Meaning |
|---|---|---|---|
market_id |
yes | string | The market’s UUID. |
qty |
yes | number | Contracts you are considering. Must be positive. |
side |
yes | string | "buy" or "sell". |
max_price |
yes | number | That market’s max_price, in dollars: 1 for a market whose REST max_price is "1.0000". Sets the price ceiling the risk and fee are computed against. |
limit_price |
no | number | Your limit, in dollars (0.55). Must be positive if given. Omit it for a market order; the projection then sweeps the book at any price. |
Reply carries a ref identifying this entry. Keep it: it is echoed on every
push, and it is what remove_order takes.
["3","4","order_slip:<user_id>","add_order",{"market_id":"687e9cdb-a391-4118-aa80-1122bb14779f","qty":100,"side":"buy","limit_price":0.55,"max_price":1}]{"status":"ok","response":{"ref":"8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33","self_match":false}}You may hold 10 registered entries at once. An eleventh is refused with
max_orders_reached. Remove one first.
Entries live only as long as the channel does. They are not restored after a disconnect: rejoin, then re-add everything you were tracking.
remove_order
Section titled “remove_order”["3","5","order_slip:<user_id>","remove_order",{"ref":"8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33"}]{"status":"ok","response":{}}Rejected registrations
Section titled “Rejected registrations”add_order and remove_order reply {"status":"error","response":{"reason":"..."}}.
reason |
Cause |
|---|---|
missing required field |
One of market_id, qty, side or max_price is absent. |
market_id must be a string |
market_id was sent as something other than a string. |
invalid market_id |
market_id is a string but not a UUID. |
side must be 'buy' or 'sell' |
Any other value. |
qty must be a number, max_price must be a number, limit_price must be a number |
That field was not a number or a numeric string. 5 and "5" are both accepted; true, null, a list or an object are not. |
invalid decimal |
The value is a string or float but could not be read as a decimal: "abc", "". |
qty must be positive, max_price must be positive, limit_price must be positive |
Zero or negative. |
max_orders_reached |
You already hold 10 entries. |
limit_exceeded |
The market’s order book refused the registration because this connection already holds its per-market maximum. Both limits are 10 by default, so max_orders_reached is normally hit first and this is not seen; it becomes reachable when the two are configured apart. Remove an entry on that market. |
market_unavailable |
That market has no order book right now. Transient on pre_open, open, closed and cancelled, where a book exists and may be restarting; retry. Permanent on scheduled, resulted and voided, where no book is ever started, so retrying cannot succeed. |
fee_unavailable |
Fees could not be determined for your account on that market. |
not_found |
remove_order was given a ref that is not registered. |
ref required |
remove_order was sent without a ref. |
invalid ref |
ref is not a UUID. |
Pushed as the book moves
Section titled “Pushed as the book moves”order_numbers_batch
Section titled “order_numbers_batch”Every registered entry whose numbers changed, in one frame.
[null, null, "order_slip:<user_id>", "order_numbers_batch", {"updates": [OrderNumbers]}]ref: The entry these numbers are for.market_id: The market the entry is on.self_match: See Self-match below.risk: What the order would cost you if it filled as projected: the filling portion plus, for a limit order, the remainder left resting.fee: Fee on the same basis. The filling portion is charged the taker rate, any resting remainder the maker rate.risk_with_fee:risk+fee. What to show as the total.fill_qty: Contracts that would fill immediately.unfilled_qty: Contracts that would not. For a limit order this is what rests on the book; for a market order it is what the book cannot cover.est_fill_at_price: The immediate fill, broken down by price level, best price first. Each entry isprice,qtyandfee. A resting remainder is not a level here; this lists only what fills now.
[ null, null, "order_slip:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "order_numbers_batch", { "updates": [ { "ref": "8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "self_match": false, "risk": "54.2000", "fee": "0.5000", "risk_with_fee": "54.7000", "fill_qty": "60.00", "unfilled_qty": "40.00", "est_fill_at_price": [ {"price": "0.5300", "qty": "40.00", "fee": "0.2000"}, {"price": "0.5500", "qty": "20.00", "fee": "0.1000"} ] } ] }]Working that example through, for a buy of 100 at a limit of 0.5500 on a
market whose max_price is 1:
| Quantity | Price | Risk | Fee | |
|---|---|---|---|---|
| Fills now | 40 | 0.5300 |
21.2000 |
0.2000 taker |
| Fills now | 20 | 0.5500 |
11.0000 |
0.1000 taker |
| Rests | 40 | 0.5500 |
22.0000 |
0.2000 maker |
| Total | 54.2000 |
0.5000 |
risk counts the resting remainder, so it does not equal the sum of
est_fill_at_price; that list is only what fills now. risk_with_fee is
54.2000 + 0.5000.
Risk per contract is the price for a buy and max_price - price for a
sell: what you stand to lose either way, not what you pay.
A newly registered entry is costed immediately; its first
order_numbers_batch arrives on registration, not on the next tick.
After that, nothing is pushed while the book is still, so silence normally means the projection has not changed.
self_match_batch
Section titled “self_match_batch”[null, null, "order_slip:<user_id>", "self_match_batch", {"updates": [{"ref": "...", "self_match": true}]}]Only entries whose flag actually flipped are included.
Self-match
Section titled “Self-match”self_match is true when you already hold an open order on the opposite side
within crossing range of the entry (for a market order, any opposite-side
order at all).
The exchange rejects a self-crossing order outright rather than filling around
it. So when this flag is true, the numbers alongside it describe a fill that
cannot currently happen, and placing the order would be refused.
The entry stays registered anyway, because the condition is yours to clear:
cancel the order that is in the way and the flag clears by itself. That is what
self_match_batch tells you: the book need not have moved for an entry to
become placeable, so this is the only signal that it did.
Treat the flag as advisory. It is read from a cache that trails the exchange by a moment, and it fails open: if it cannot be determined at registration, the entry is returned unflagged rather than blocked. The engine remains the authority at placement.
Use Cases
Section titled “Use Cases”| Use case | Message to send |
|---|---|
| Join | ["3","3","order_slip:<user_id>","phx_join",{}] |
| Cost a limit order | ["3","4","order_slip:<user_id>","add_order",{"market_id":"<uuid>","qty":100,"side":"buy","limit_price":0.55,"max_price":1}] |
| Cost a market order | ["3","5","order_slip:<user_id>","add_order",{"market_id":"<uuid>","qty":100,"side":"buy","max_price":1}] |
| Stop tracking one entry | ["3","6","order_slip:<user_id>","remove_order",{"ref":"<ref>"}] |
| Check the connection is alive | ["3","7","order_slip:<user_id>","ping",{}] |
Joining
Section titled “Joining”["3","3","order_slip:<user_id>","phx_join",{}]{"status":"ok","response":{}}No snapshot and no filter. The join reply is empty and nothing follows until
you register an entry. Joining a topic whose user id is not yours fails with
{"reason":"unauthorized"}.

