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_id | 1 (margin) — else margined_requires_margin_sub | 0 or omitted — else event_on_margin_sub |
side | yes (the API's single side); action buys or sells | yes or no; a YES sell rests as a NO bid at 10000 − price |
order_type | limit only — else margined_limit_only | limit or market (max_cost required on market buys) |
price_pips | signed ticks inside [min_price_ticks, max_price_ticks] and the band around mark | 1–9999 |
| flags | post_only, reduce_only, stp_qualifier | not 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_uusdcandbreakeven_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 restingpost_onlyorder pays none of it.margin.free_collateral_afterandutilization_after_bps— what the position would do to the margin sub-account.im_deltais zero forreduce_onlyorders.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; omitinstrument_idto sweep the whole account. Asynchronous: the response is an estimate, completion arrives asmass_cancel_completeon the account channel.get_fills— your fills withfee_uusdc(signed) andtag(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, andpredicted_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_fundingshows the public rate history.get_funding_payments— the booked legs afterwards, one per boundary and instrument, with the signedpayment_uusdc, the snapshot position, the mark and the notional the venue used, and pagetotals.
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
| Code | What it means | Do |
|---|---|---|
outside_price_band | Limit price beyond the admission band around mark | Move inside limit_band_*_bps from get_contract_spec |
post_only_would_cross | The resting order would have taken | Rest behind the touch, or drop post_only |
insufficient_funds | Free collateral (or balance) below the hold | Read get_margin; reduce size |
too_many_open_orders | Per-instrument open-order cap | Cancel stale orders first |
no_mark_yet | No mark price since listing/restart | Wait; check get_market |
instrument_halted / circuit_breaker_halt / kill_switch_active | Venue or instrument halt | get_venue_status; wait |
confirm_token_invalid | Token expired, reused, or fields changed | Stage again |
instrument_not_allowed / order_notional_cap / daily_notional_cap / open_orders_cap | The credential's own scope | get_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.