vike
Tools

submit_order

Place one order on a running vike-tradehub node — preview first, execute only on a second call carrying both confirm true and the single-use preview_token that preview returned.

submit_order is the write tool that opens risk. Everything else in the roster cancels, reduces or reports, with one exception — modify can raise a resting order's quantity, which is why the node sizes it against the same notional ceiling; this one puts a brand-new order on a venue. It is annotated destructiveHint: true and idempotentHint: false, and it needs vike-cli mcp --node <host:port> plus VIKE_TRADEHUB_CONTROL_KEY.

Arguments

ParameterTypeRequiredNotes
venuestringyesVenue id
symbolstringyesInstrument symbol
sideintegeryes1 buy, -1 sell
qtynumberyesOrder quantity
order_typestringnomarket (default), limit, stop, take_profit
pricenumbernoLimit price
trigger_pricenumbernoStop / take-profit trigger
reduce_onlybooleannoDefaults to false
client_order_idstringnoRead by the builder, though the advertised schema does not list it
reasonstringnoRationale for the node's audit trail
confirmbooleannoMust be the JSON boolean true and be accompanied by preview_token — either one missing returns a preview
preview_tokenstringnoThe token this tool's preview returned. Fires once, expires after 60s, bound to the command it previewed

A missing required field is a tool error raised in verbs::verb_from_tool_args before any command exists — submit_order_missing_required_field_is_a_tool_error pins it with a call that omits symbol. The order kinds and what each one means have their own pages.

Two calls, and the second one carries two things

Call one previews. Server::call_tool builds the wire command, mints a client order id, asks the node for its own dry-run, and returns preview_of(...) — will_execute: false, the fully resolved wire_command, the advisory guardrail, the node_verdict and whether it was obtained, the reason that would be recorded, a note, and the preview_token that confirms this exact command. With no node configured it opens no socket at all: submit_order_without_confirm_only_previews_and_touches_no_network proves a preview succeeds in that state, and checks the guardrail's notional is 50.0 for a limit buy of 0.5 at 100.0.

Call two is the one that sends — and it needs both confirm: true and that preview_token:

{ "venue": "binance", "symbol": "BTCUSDT", "side": 1, "qty": 0.001,
  "confirm": true, "preview_token": "pv-1" }

The token fires once, expires after 60 seconds and is bound to the command it previewed; Live node sets out all four rules and why the token is deliberately not a secret.

confirm: true without a token is a preview, not an error

Both conditions are read together — a token that arrives without confirm: true, and a confirm that arrives without a token, are the same case: the call returns a fresh preview, will_execute: false, a new token, and nothing on the wire. confirm_alone_no_longer_executes_it_returns_a_preview pins exactly that. So an agent that treats its second call as proof it traded, without reading will_execute, believes it holds an order it never placed. confirm is also read as a JSON boolean and nothing else, so "true", 1 and "yes" fall through the same way.

A bad token is the loud case: a tool error carrying the reason. There are three messages, not four — the store cannot tell an unknown token from an already-used one, because take removed the used one, so both come back as unknown or already used. Read that one as possibly-already-sent and settle it with node_snapshot rather than re-confirming. Expired and mismatched each have their own message. Confirming with a valid token and no node reaches the connection and fails there — a clean error, not a panic, and the fact that it got that far is what a_preview_token_fires_at_most_once uses to prove the token passed the gate.

The client order id

The node refuses a remote submit whose client_order_id is empty. lower_command says why: an empty one would let the runtime mint, which is fine in-process, but a remote peer then has no stable handle to cancel or dedup by. So verbs::fill_client_order_id mints one before the preview branch, and the preview shows a real id.

The mint is verbs::coid_minter — the live core's own generator and wire form, <8-hex-session><seq>. Its doc argues four properties: unique per order (the sequence increments), no collision across sessions (the prefix is drawn from OS entropy per process), typeable at 9–11 characters, and venue-safe against ^[A-Za-z0-9]{1,32}$, the strictest common denominator across the big crypto venues.

The id in the preview is the id that gets sent. A confirming call does mint a fresh one while rebuilding the command — the mint runs on every call — but the rebuilt command is only ever compared: same_intent excludes the client_order_id for exactly this reason, and the server then executes the stored command. That is a stronger property than comparing the ids would have been, because what reaches the venue is byte-for-byte what the preview displayed and the node dry-ran. The client_order_id in the accepted response is still the authoritative one to keep.

(Passing the previewed id back explicitly is harmless and changes nothing — same_intent ignores the field either way.)

A supplied id is not charset-checked on this surface

The human trade REPL refuses a --coid outside that charset. This tool does not: verbs::verb_from_tool_args passes an agent-supplied client_order_id through unvalidated, and the node's lower_command requires only that it be non-empty. Re-sending the same id proves nothing and is not free: the core registry is coid-keyed, so a repeat leaves one row — but gate_and_register inserts over it rather than rejecting the duplicate, and submit_order calls client.submit both times, so on the default paper mount two orders rest.

What checks the size

The preview's guardrail is advisory and computed on your machine (verbs::guardrail_check). max_qty comes from VIKE_MAX_ORDER_QTY, a local typo-catcher with no enforcing counterpart anywhere; max_notional from this machine's policy.toml (max_notional_per_order). A market order carries no price, so its notional is null and cannot be checked client-side at all.

What refuses is server-side. ControlLimits::vet consumes a rate token on every command and then applies the notional ceiling to a submit that carries a price — and to a modify naming both a new qty and a new price — against the node's own max_notional_per_order, which is unset by default: notional_reason opens on that Option, so a node whose policy.toml names no ceiling skips this half entirely. Notional there is a magnitude — the check takes .abs() of every factor, because a site that absed the quantity only let a negative price produce a negative notional that could never trip the ceiling. Beneath it sits the core RiskGate. Neither is reachable from an agent, and the ceiling is a settings key: see policy.toml and Risk and arming.

Arming is upstream of all of it. A venue with no credentials mounts as paper, and policy.venues.<venue> defaults to paper and can only ever refuse.

Outcomes

Server::execute awaits the ticket that send returned and waits ACK_WAIT, two seconds — a deadline, not a sleep. Accepted returns sent: true, outcome: "accepted" and the authoritative client_order_id. A refusal is an error result carrying the node's reason. A timeout returns outcome: "unknown"; a dropped control connection is an error result saying the outcome is unknown and it may have executed. For both ambiguous cases, call node_snapshot before retrying.

Then watch it: order lifecycle describes the states the order now moves through, and cancel_order, modify and flatten are what act on it.

Preview a limit buy of 0.5 BTCUSDT at 60,000 on binance, then place it if the guardrail is clean.

On this page