Operations

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:

MethodPathTypePurpose
POST/strategies/{strategy_id}/enrollenrollmentFirst buy for one or more participants. Creates a Trader per first-time enrollee.
POST/strategies/{strategy_id}/top-uptop_upAdd fiat to existing Traders and buy per supplied allocations.
POST/strategies/{strategy_id}/partial-sellpartial_sellReturn fiat to participants by selling proportionally to supplied allocations.
POST/strategies/{strategy_id}/rebalancerebalanceRealign in-scope Traders to new target weights.
POST/strategies/{strategy_id}/liquidateliquidationFully exit listed Traders (quantity-based sells, no buy leg).
GET/strategies/operations/{operation_id}Retrieve one Operation.
GET/strategies/operationsList 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
StatusMeaning
validatingRequest accepted; payload and scope validation in progress.
pricingAtomic credit check (rebalances) and pricing.
settlingExecutions being booked at the locked uniform price.
settledTerminal. May carry status_reason: partial_success when Executions mix settled, skipped, and rejected.
skippedTerminal. No Executions produced any work (for example, an already-aligned rebalance).
failedTerminal. 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-ID header (UUID v4). The five Operation endpoints return 202 on create and 200 on 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 }
  ]
}
FieldTypeRequiredDescription
allocations[]arrayyesTarget weights for this enrollment. Must sum to 1.00.
participants[]arrayyesOne or more entries. Single-user requests use a one-element array.
participant_codestring (≤ 6)yesPlatform's end-user identifier.
amountdecimal stringyesFiat to spend.
auto_rebalancebooleanyesInitial 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" }
  ]
}
FieldTypeRequiredDescription
allocations[]arrayyesNew target weights. Must sum to 1.00. A weight of "0" fully exits that asset (quantity-based sell).
participants[]arraynoArray 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.