An Operation is any asynchronous mutation against a Strategy. Every inflow, outflow, or realignment is an Operation, tracked through a single status machine and reported back over webhooks.
There are five Operation entry points, plus read endpoints to retrieve and list them:
| Method | Path | Type | Purpose |
|---|---|---|---|
POST | /strategies/{strategy_id}/enroll | enrollment | First buy for one or more participants. Creates a Trader per first-time enrollee. |
POST | /strategies/{strategy_id}/top-up | top_up | Add fiat to existing Traders and buy per supplied allocations. |
POST | /strategies/{strategy_id}/partial-sell | partial_sell | Return fiat to participants by selling proportionally to supplied allocations. |
POST | /strategies/{strategy_id}/rebalance | rebalance | Realign in-scope Traders to new target weights. |
POST | /strategies/{strategy_id}/liquidate | liquidation | Fully exit listed Traders (quantity-based sells, no buy leg). |
GET | /strategies/operations/{operation_id} | Retrieve one Operation. | |
GET | /strategies/operations | List Operations (paginated, filterable). |
Each of the five mutations returns 202 Accepted with the Operation in validating. Every participant in the request becomes an Execution and is processed independently: one participant's rejection never affects the others.
Operation lifecycle
validating → pricing → settling → settled | skipped | failed
| Status | Meaning |
|---|---|
validating | Request accepted; payload and scope validation in progress. |
pricing | Atomic credit check (rebalances) and pricing. |
settling | Executions being booked at the locked uniform price. |
settled | Terminal. May carry status_reason: partial_success when Executions mix settled, skipped, and rejected. |
skipped | Terminal. No Executions produced any work (for example, an already-aligned rebalance). |
failed | Terminal. Validation failed, or every Execution was rejected. |
status_reason is one of "", no_executions, all_skipped, all_rejected, or partial_success.
Idempotency and concurrency
- Idempotency: every mutation requires an
X-REQUEST-IDheader (UUID v4). The five Operation endpoints return202on create and200on an idempotent replay. - Single in-flight Operation per Strategy: only one Operation (of any type) can be in progress for a Strategy at a time. A new request while another is in-flight is rejected. There is no internal queue or FIFO ordering; retry after the active Operation reaches a terminal status is the caller's responsibility.
Allocations
Enroll, top-up, partial-sell, and rebalance all take an allocations[] array of {underlying_currency, weight} entries. Weights are decimal strings and must sum to 1.00. Each underlying_currency may appear at most once and must form a valid instrument with the Strategy's quoted_currency.
Enroll
POST /strategies/{strategy_id}/enroll
{
"allocations": [
{ "underlying_currency": "BTC", "weight": "0.50" },
{ "underlying_currency": "ETH", "weight": "0.30" },
{ "underlying_currency": "SOL", "weight": "0.20" }
],
"participants": [
{ "participant_code": "CUST01", "amount": "10000.00", "auto_rebalance": true },
{ "participant_code": "CUST02", "amount": "5000.00", "auto_rebalance": false }
]
}| Field | Type | Required | Description |
|---|---|---|---|
allocations[] | array | yes | Target weights for this enrollment. Must sum to 1.00. |
participants[] | array | yes | One or more entries. Single-user requests use a one-element array. |
participant_code | string (≤ 6) | yes | Platform's end-user identifier. |
amount | decimal string | yes | Fiat to spend. |
auto_rebalance | boolean | yes | Initial rebalance-inclusion flag for the new Trader. Accepted on enroll only. |
First-time enrollees produce a Trader and a trade_strategy.trader.created webhook. Buy-side credit check: direct model checks the participant's fiat balance, novated model checks the platform float.
Top-up
POST /strategies/{strategy_id}/top-up
Same shape as enroll, but every participant must already be a Trader, and auto_rebalance is not accepted (use PATCH on the Trader to change it).
{
"allocations": [
{ "underlying_currency": "BTC", "weight": "0.40" },
{ "underlying_currency": "ETH", "weight": "0.40" },
{ "underlying_currency": "SOL", "weight": "0.20" }
],
"participants": [
{ "participant_code": "CUST01", "amount": "5000.00" }
]
}Partial sell
POST /strategies/{strategy_id}/partial-sell
amount is the fiat the Trader wants back. Sells are sized proportionally to the caller-supplied allocations, not to the Trader's current holdings split. To scale a position down without realigning it, derive the weights from current holdings and pass them in. A per-asset slice below the instrument minimum produces an Instruction rejected with status_reason: below_instrument_minimum.
{
"allocations": [
{ "underlying_currency": "BTC", "weight": "0.583" },
{ "underlying_currency": "ETH", "weight": "0.217" },
{ "underlying_currency": "SOL", "weight": "0.200" }
],
"participants": [
{ "participant_code": "CUST01", "amount": "100.00" }
]
}Rebalance
POST /strategies/{strategy_id}/rebalance
Sets new target weights and adjusts in-scope Traders from their current portfolio weights through a series of coordinated transactions.
{
"allocations": [
{ "underlying_currency": "BTC", "weight": "0.40" },
{ "underlying_currency": "ETH", "weight": "0.40" },
{ "underlying_currency": "SOL", "weight": "0.20" }
],
"participants": [
{ "participant_code": "CUST01" },
{ "participant_code": "CUST02" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
allocations[] | array | yes | New target weights. Must sum to 1.00. A weight of "0" fully exits that asset (quantity-based sell). |
participants[] | array | no | Array of {participant_code} objects. Omit to target every Trader with auto_rebalance = true and non-zero balances. Provide it to target exactly the listed Traders, which overrides auto_rebalance = false. |
Liquidate
POST /strategies/{strategy_id}/liquidate
Sells everything the listed Traders currently hold. If participants list is omitted, the entire strategy is liquidated and all Traders following it (auto_rebalance = true). No allocations input and no buy leg.
{
"participants": [
{ "participant_code": "CUST01" },
{ "participant_code": "CUST02" }
]
}Traders with no holdings produce an Execution skipped with status_reason: no_holdings; the Trader record is preserved for audit. Liquidation ignores the auto_rebalance flag if the participant is specified but still respects the single-in-flight-Operation rule.
Pricing and atomicity guarantees
- Uniform pricing. Per asset, every validated Trader in an Operation receives the identical price. No user gets an advantage within a single Operation.
- Atomic credit check (rebalances). A Trader either passes every leg or is excluded entirely, so no Trader is left in a partial-rebalance state.
- Per-user isolation. Each participant's Execution is independent; one rejection does not cascade to the rest of a batch.
Retrieve and list Operations
GET /strategies/operations/{operation_id}
{
"id": "d1e2f3a4-b5c6-4789-abcd-ef0123456789",
"strategy_id": "b1c2d3e4-f5a6-4789-bcde-f01234567890",
"request_id": "e2f3a4b5-c6d7-4789-abcd-ef0123456789",
"type": "rebalance",
"status": "settled",
"status_reason": "partial_success",
"allocations": [
{ "underlying_currency": "BTC", "weight": "0.40" },
{ "underlying_currency": "ETH", "weight": "0.40" },
{ "underlying_currency": "SOL", "weight": "0.20" }
],
"created_at": "2026-04-17T12:00:00Z",
"updated_at": "2026-04-17T12:00:25Z"
}GET /strategies/operations lists Operations. Filters: strategy_id, request_id, type, status, page, page_size.
Webhooks
Each Operation status transition emits one trade_strategy.operation.status_changed event. Per-Trader outcomes arrive as trade_strategy.execution.status_changed (see Executions). The snapshot endpoints above are authoritative for reconciliation.