Skip to main content

MCP order flow

Every order through MCP is two-phase: stage_order asks the venue to preview the order and returns a short-lived confirm_token; place_order submits with that token. The server never sends an order that was not staged, so a human reviewing an agent transcript — or a supervising agent — always sees the venue's own cost, fee and margin figures before commitment.

All examples are illustrative shapes; run them against the staging venue.

1. Find the market​

// list_markets { "class": "perp" }
{ "markets": [
{ "instrument_id": 1011, "symbol": "NTH-RT-PERP", "display_name": "ERCOT North RT perpetual",
"class": "perp", "state": "trading", "unit": "MWh", "contract_size": 1,
"maker_fee_rate_bps": 0, "taker_fee_rate_bps": 0, "manifest_hash": "…" } ] }

get_market adds the contract spec (tick size, margin bps, price bands) and the top of book; get_orderbook shows depth. For a margined instrument a price is a signed tick count — price_pips: 505 on a tick_size_uusdc: 50000 instrument is $25.25/MWh.

2. Stage​

// stage_order
{ "instrument_id": 1011, "side": "yes", "action": "buy", "order_type": "limit",
"price_pips": 505, "count": 5, "sub_account_id": 1, "post_only": true }

Field rules the venue enforces (and stage_order mirrors as would_reject before you commit):

Perps and futures (margined)Event contracts (YES/NO)
sub_account_id1 (margin) — else margined_requires_margin_sub0 or omitted — else event_on_margin_sub
sideyes (the API's single side); action buys or sellsyes or no; a YES sell rests as a NO bid at 10000 − price
order_typelimit only — else margined_limit_onlylimit or market (max_cost required on market buys)
price_pipssigned ticks inside [min_price_ticks, max_price_ticks] and the band around mark1–9999
flagspost_only, reduce_only, stp_qualifiernot supported — unsupported_order_flags

The preview:

{ "engine_seq_applied": 481902, "instrument_id": 1011, "margined": true,
"book": { "…": "the walk your order would take" },
"execution": { "…": "expected fills and resting remainder" },
"fees": { "maker_fee_rate_bps": 0, "taker_fee_rate_bps": 0,
"est_taker_fee_uusdc": 0, "est_maker_fee_uusdc": 0,
"est_total_cost_uusdc": 0, "breakeven_pips": 0 },
"margin": { "sub_account_id": 1, "equity": 100000000000, "im_held_now": 12625000000,
"im_instrument_after": 6312500000, "im_delta": 6312500000,
"im_held_after": 18937500000, "free_collateral_after": 81062500000,
"utilization_after_bps": 1894, "position_now": 0, "position_if_filled": 5, "im_bps": 5000 },
"would_reject": null, "warnings": [],
"confirm_token": "ct_…", "confirm_expires_at_ns": 1790121600000000000 }

What to read before committing:

  • would_reject — the admission check the engine would apply (outside_price_band, insufficient_funds, post_only_would_cross, …). Null means admissible at this snapshot; the engine remains authoritative.
  • fees.est_total_cost_uusdc and breakeven_pips — taker fee on the crossing part, maker fee on the resting part, and cost through mid. On thin books crossing the spread usually costs more than the fee; a resting post_only order pays none of it.
  • margin.free_collateral_after and utilization_after_bps — what the position would do to the margin sub-account. im_delta is zero for reduce_only orders.
  • warnings — max_cost_limits_fill, one_sided_or_empty_book, and similar.

3. Place​

Send the same fields plus the token, within 60 seconds:

// place_order
{ "instrument_id": 1011, "side": "yes", "action": "buy", "order_type": "limit",
"price_pips": 505, "count": 5, "sub_account_id": 1, "post_only": true,
"confirm_token": "ct_…", "client_order_id": "agent-7-0042" }

The token is bound to the order's canonical fields; changing count or price_pips after staging fails with confirm_token_invalid — stage again. client_order_id makes a retry idempotent for about 15 minutes: the venue returns the original outcome instead of placing twice.

{ "order_id": 90412, "status": "accepted_rested", "filled_count": 0, "resting_count": 5,
"side": "yes", "action": "buy", "order_type": "limit", "price_pips": 505, "count": 5,
"client_order_id": "agent-7-0042",
"edge_received_at_ns": 1790121540123456789, "sequencer_committed_at_ns": 1790121540123789012,
"engine_applied_at_ns": 1790121540124012345 }

status is one of accepted_rested, accepted_fully_filled, accepted_ioc_canceled, or rejected with a reject_reason. Pass any reject reason to explain_reject_code.

Approval mode​

A credential minted with approval_mode: require_human never submits on its own. place_order then returns:

{ "pending_id": 91, "status": "pending_approval", "instrument_id": 1011,
"side": "yes", "action": "buy", "order_type": "limit", "price_pips": 505, "count": 5,
"notional_uusdc": 126250000, "expires_at_ns": 1790122440000000000,
"credential_id": 512, "agent": "spike-scalper" }

Nothing rests. The account's terminal (or any credential of the account whose own mode is auto) sees the order on GET /v1/orders/pending and on the private order channel as a pending_approval frame, and approves or rejects it within 15 minutes. Approval submits the stored order exactly as place_order would and attributes it to the agent's credential; the agent learns the outcome through get_open_orders, get_fills, or the approval_approved / approval_rejected frames. get_pending_orders lists what is still waiting; a retry with the same client_order_id while a row is pending returns the same pending_id.

In the Sphinx Terminal the human side lives in two places: the APPROVALS tab of the blotter (a live count in the tab label, approve / reject per row, a countdown to expiry) and the Agents card on the Account page, which also shows each key's scope, today's notional against its daily cap, and an emergency stop that cancels every resting order and revokes every scoped key.

stage_order warns ahead of time: a scoped credential's preview carries scope (caps and today's usage) and the warning approval_required; a cap the order would hit appears as would_reject with the warning credential_scope.

4. Manage​

  • get_open_orders — resting orders, optionally per instrument.
  • cancel_order { order_id } — one order.
  • cancel_all_orders { instrument_id } — sweep one book; omit instrument_id to sweep the whole account. Asynchronous: the response is an estimate, completion arrives as mass_cancel_complete on the account channel.
  • get_fills — your fills with fee_uusdc (signed) and tag (normal, liquidation, adl).
  • get_positions / get_margin — exposure and the margin block.

Funding while you hold​

A perpetual position pays or receives funding at every 8 h boundary (00:00, 08:00, 16:00 UTC): the engine applies the boundary's settled rate to your snapshot position at the boundary mark, longs pay when the rate is positive. Two tools cover it:

  • get_funding_summary — per perpetual you hold: net funding over 24 h / 7 d / all, the last settled rate, and predicted_next_uusdc, an estimate that applies the last rate to your current position (negative = you would pay). Read it before deciding to hold through a boundary; get_funding shows the public rate history.
  • get_funding_payments — the booked legs afterwards, one per boundary and instrument, with the signed payment_uusdc, the snapshot position, the mark and the notional the venue used, and page totals.

Live, the private account channel carries a funding_payment frame per leg and the public market_lifecycle channel a funding_rate frame per market boundary. History begins at the venue's first boundary after API 0.15.0.

5. The dead-man switch​

An agent that crashes mid-loop must not leave orders behind. Arm the switch and refresh it every iteration:

// set_deadman { "ttl_ms": 30000 }
{ "armed": true, "ttl_ms": 30000, "expires_at_ns": 1790121570000000000, "fired_count": 0, "last_fired_at_ns": 0 }

If the venue does not hear a refresh within ttl_ms (1–300 s) it cancels every resting order account-wide, once, and disarms. ttl_ms: 0 disarms. get_deadman shows whether it has ever fired.

A minimal agent loop​

arm set_deadman { ttl_ms: 30000 }
loop every N seconds:
read get_venue_status; get_market / get_orderbook / get_candles as needed
decide (your model, your rules — outside the venue)
stage stage_order { … }
check would_reject == null, est_total_cost within budget, free_collateral_after above floor
place place_order { …, confirm_token }
refresh set_deadman { ttl_ms: 30000 }
manage get_open_orders; cancel_order stale ones
on exit cancel_all_orders {}; set_deadman { ttl_ms: 0 }

Two design notes from running agents on this venue. First, cadence: models that decide every tick overtrade and pay the spread; decide on minutes, rest with post_only, and let the book come to you. Second, keep the credential narrow — trade entitlement only, an expires_in_s, and revoke it when the run ends.

Reject reasons you will meet first​

CodeWhat it meansDo
outside_price_bandLimit price beyond the admission band around markMove inside limit_band_*_bps from get_contract_spec
post_only_would_crossThe resting order would have takenRest behind the touch, or drop post_only
insufficient_fundsFree collateral (or balance) below the holdRead get_margin; reduce size
too_many_open_ordersPer-instrument open-order capCancel stale orders first
no_mark_yetNo mark price since listing/restartWait; check get_market
instrument_halted / circuit_breaker_halt / kill_switch_activeVenue or instrument haltget_venue_status; wait
confirm_token_invalidToken expired, reused, or fields changedStage again
instrument_not_allowed / order_notional_cap / daily_notional_cap / open_orders_capThe credential's own scopeget_limits shows the caps and today's usage; ask the operator for a wider scope

The full table, with every code, is the sphx://reject-codes resource and the explain_reject_code tool.