RFC: Reduce-Only Order Placement Permissions

Date: 2026-06-23

Status: Stub problem statement only. Captures a gap noticed while account-keying the order-gateway (A-3402); intentionally defers the design.

Background

account_permissions carries five independent bits per (user, account): can_list, can_read, can_set_limits, can_reduce_or_close, can_trade (rs/sdk-internal/db/src/entities.rs). The order-gateway authorizes money routes against these via ax_db::entities::authorize(user, account, Action).

There are, confusingly, two unrelated "close-only" notions in the gateway today:

  1. user.is_close_only a per-user risk flag. Enforced on the place path: can_send_order evaluate_order_using_ep3_position rejects with OrderDecision::CloseOnlyViolation unless is_closing() holds for the order. (is_closing() already returns exactly "does this order reduce/close the account's position," so the predicate we need exists.)

  2. account_permissions.can_reduce_or_close an account-level permission. Consulted only on cancel paths (resolve_cancelable_order, cancel_all_orders, admin cancel-all). It is never consulted when placing an order.

The problem

Placing an order authorizes Action::Trade only (place_order_common). So a user holding can_reduce_or_close = true, can_trade = false on an account can cancel orders there but cannot place a closing order even though the permission's name reads as "may place orders that reduce or close." The account-permission close side is simply not wired into placement; the only close-only enforcement on placement comes from the unrelated per-user is_close_only flag.

This matches the multi-accounts plan's step 6 (#2180), which centralized money routes on a single authorize(Action::Trade). So "placing requires can_trade" was deliberate, and reduce-only placement was never a feature this RFC is about whether it should be one.

Why it matters

The "reduce-only key for the de-risking bot / liquidation agent" workflow is a standard exchange capability and is implied by the existence of a distinct can_reduce_or_close bit. Granting such a delegate can_trade to let them place closing orders over-grants (they can also open new risk).

Sketch of a direction (not a decision)

A plausible shape: at the place edge, admit the order when can_trade || (can_reduce_or_close && order_is_closing), folding the account-permission close case into the existing is_close_only enforcement so there is one close-only code path rather than two. is_closing() already supplies order_is_closing.

Open questions

Non-goals (for now)

No behavior change. This RFC only records the gap so the eventual design is deliberate rather than incidental.