Every trading action starts with prepare: BotRadix validates the action against the market, your account and the contracts, simulates it, and returns exactly what must be signed or sent. Nothing is executed by prepare.
The flow#
POST /orders/preparewith an action, its payload and an idempotency key.- Check the response:
execution.readymust betrue. - Execute it, depending on
execution.status:
execution.status | What to do next |
|---|---|
signature_ready | Sign execution.typed_data (EIP-712) with the wallet and send it to the relay. Used today to open and close positions |
ready | Sign and broadcast each transaction in execution.transactions, in order, from the wallet |
preflight_failed | Not executable now. execution.reason says why (for example withdraw_margin_insufficient or oracle_unavailable) |
contract_pending | The market or contract is not available for this action yet |
Warning
prepare answers HTTP 200 even when the action cannot be executed. Always check execution.ready and execution.status before signing anything.
A prepared action expires: 120 seconds for trading actions, 180 seconds for collateral actions and 300 seconds for signed intents. Prepare again if it expires.
Path 1 — Signed intent (open and close positions)#
For open_position, close_position_market and close_position_limit, the response contains an EIP-712 message instead of transactions:
execution.modeissigned_intentandexecution.transactionsis empty.- The typed data uses the domain
BotradixDEX, version1, chain ID8453, and the DEX router as verifying contract. - The wallet signs it (a signature, not a transaction), and you send it to the relay:
curl -X POST https://app.botradix.com/api/dex/actions/relay \
-H "Content-Type: application/json" \
-d '{"wallet":"0xYourWallet","intent_id":"dx_…","signature":"0x…"}'
# → {"tx_hash":"0x…","state":"submitted"}BotRadix's executor then broadcasts the transaction. Its network cost is charged in USDC from your DEX collateral and is shown beforehand in execution.costs, so the wallet does not need ETH for these actions.
Note
The relay endpoint is currently part of the unversioned web API (/api/dex/actions/relay). It is the supported way to submit signed intents today.
Path 2 — Wallet transactions#
For the other actions (collateral, TP/SL updates, isolated-margin changes, cancel and replace), execution.transactions lists one or more transactions:
{ "kind": "action", "from": "0xYourWallet", "to": "0x…", "data": "0x…", "value": "0x0", "deadline": 1790950518 }A kind: "approval" entry (an exact-amount token approval) may come first for deposits. Sign and broadcast them in order from the same wallet; they need a little ETH on Base for gas.
USDC deposit by permit (no gas). When GET /api/dex/config reports collateral_model.approval.permit: true, add "permit": true next to action for an add_collateral of USDC. The response then has execution.mode: "permit_deposit" with EIP-2612 typed_data: sign it with the wallet and send {"wallet","intent_id","signature"} to POST /api/dex/actions/relay. A protocol executor submits the deposit and pays the gas. Without the flag, or for BAQ, you receive the approval and deposit transactions.
If you broadcast transactions yourself, report the hash to POST /api/dex/actions/receipt ({"wallet","intent_id","tx_hash","state"}) to receive transaction.* webhooks for it; otherwise BotRadix only learns about the trade when it is indexed from the chain.
Path 3 — Delegated execution#
POST /orders/execute submits actions through an executor bound to the API key (scope trade:execute; orders:manage for cancel, replace and TP/SL primitives). The response is 202 with delegated.command_id, delegated.tx_hash and a status_url; follow it with GET /executions/{command_id}. Send "prepare_only": true to validate without submitting.
Accepted actions include open_position, batch (1 to 32 actions), open_market, create_limit, create_stop, increase, reduce, close, cancel, replace, set_tp, clear_tp, set_sl, clear_sl, invert and isolated_manage. If no executor is bound to the key, the API answers 403 delegated_executor_binding_required.
Idempotency#
/orders/prepare requires an idempotency key, taken from (in this order) the Idempotency-Key header, payload.idempotency_key or payload.client_order_id. It must match ^[A-Za-z0-9._:-]{8,128}$.
- Keys are scoped to wallet, action and key, and are remembered for 24 hours.
- The same key with the same payload returns the original preparation (
idempotent_replay: true). - The same key with a different payload returns
409 IDEMPOTENCY_KEY_CONFLICT; a reusedclient_order_idwith a different payload returns409 DUPLICATE_CLIENT_ORDER_ID. - The request is matched on what you sent (
payload, pluspermitfor deposits), for every action includingopen_position. The body is validated before the key is looked up, so a retry while the market is stale or moved returns that validation error. A replayed preparation can already be expired: checkexpires_at_ms.
/orders/execute reads the key only from payload.idempotency_key or payload.client_order_id and follows the same rules: the same key with the same payload replays the original response, and a different payload returns 409 IDEMPOTENCY_KEY_CONFLICT.
open_position payload#
| Field | Rules |
|---|---|
pair_id | Market id from /markets |
side | long or short |
order_type | market (default), limit or stop |
margin_mode | cross (default) or isolated |
leverage | Integer from 1 up to the market maximum (see Leverage and margin) |
order_value_usdc | Position size (notional) in USDC, up to 6 decimals, within the market minimum and maximum |
reference_price | Required for market orders. Rejected with PRICE_DEVIATION_EXCEEDED if more than 1.5% from the server price, or MARKET_DATA_STALE if prices are older than 8 seconds |
max_slippage_bps | Required for market orders, 1 to 1000 (0.01% to 10%) |
limit_price / trigger_price | Required for limit / stop orders |
time_in_force | gtc, ioc or fok; market orders are always ioc |
rest_if_unfilled | Market orders only: keep the unfilled part as a limit order |
post_only | Limit orders only |
take_profit, stop_loss | Optional; must be on the correct side of the entry price |
client_order_id | Up to 96 characters; generated if omitted |
Example#
curl -X POST https://app.botradix.com/api/dex/v1/orders/prepare \
-H "X-BotRadix-API-Key: $BOTRADIX_API_KEY" \
-H "Idempotency-Key: mm01-2026-10-02-0001" \
-H "Content-Type: application/json" \
-d '{
"action": "open_position",
"payload": {
"pair_id": 1,
"side": "long",
"order_type": "market",
"margin_mode": "cross",
"order_value_usdc": "100",
"leverage": 5,
"reference_price": "2500.00",
"max_slippage_bps": 50,
"client_order_id": "mm01-2026-10-02-0001"
}
}'The response contains intent_id, the normalized payload, execution (status, typed data or transactions, simulation, costs and deadline) and expires_at_ms.
After execution#
GET /transactionslists your intents and their on-chain state.GET /native/executions/{tx_hash}reports finality:PENDING,PENDING_FINALITY(fewer than 8 confirmations),REVERTEDorFINAL.- Webhooks notify order, position, collateral and transaction changes.