Models & attribution

POST /flow/v1/auth/models · model_id on /auth/orders/buy · /auth/performance?group_by=model
Live

Label orders with a model you own, group them into opportunities, and read performance per model — e.g. to run several agents side by side and see which one is actually making money.

How it fits together

A model is a name you file orders under. OpenMarkets doesn't run it — your strategy, agent, or LLM lives entirely on your side. We store the label, stamp it on every order you place with it, and report performance grouped by it.

  • Model → the strategy or agent ("Claude agent", "GPT agent", "NBA market maker").
  • Opportunity → a group of related orders inside a model (a hedge, a position you scale into). Each buy opens one; pass its opportunity_id on later buys to add to it.
  • Order → one leg on one venue.
Practice (paper) and real-money orders both carry the model, and performance never blends them — every rollup is partitioned by currency (ATLAS vs USD). Paper trading is the natural way to race agents against each other before any real money moves.

Create a model

POST /flow/v1/auth/models — requires trade:execute. league_id is optional; leave it out for an agent that trades across leagues. Unknown fields are rejected with 400 unknown_parameter. Send an Idempotency-Key header so a retry doesn't create a duplicate. An account may hold up to 50 active models (409 model_limit_reached).

Request
curl -X POST https://api.openmarkets.ai/flow/v1/auth/models \
  -H "X-API-Key: $OM_KEY" \
  -H "Idempotency-Key: create-claude-agent" \
  -H "Content-Type: application/json" \
  -d '{ "model_name": "Claude agent", "model_description": "Opus, value bets only" }'
Response.data (201)
{
  "model_id": "m_...",
  "model_name": "Claude agent",
  "model_description": "Opus, value bets only",
  "league_id": null,
  "league_name": null,
  "status": "active",               // "active" | "archived"
  "created_at": "2026-09-21T14:02:11.000Z",
  "updated_at": "2026-09-21T14:02:11.000Z"
}

Place orders under a model

Add model_id at the top level of POST /orders/buy. The response returns the opportunity_id the orders landed in — send it back on the next buy to keep grouping under the same opportunity.

POST /flow/v1/auth/orders/buy
{
  "model_id": "m_...",
  "opportunity_id": "opp_...",       // optional — omit to open a new opportunity
  "orders": [
    { "liquidity_hash": "...", "amount": 5, "max_price": 0.42, "mode": "paper" }
  ]
}

The ids are checked before anything reaches a venue, so a bad one costs you nothing:

  • 404 model_not_found / 404 opportunity_not_found — doesn't exist, or isn't yours (the two are indistinguishable by design).
  • 409 model_archived — unarchive it first.
  • 400 model_mismatch — an opportunity belongs to one model. Adding to it requires the same model_id (or none, if it has none).
  • 400 currency_mismatch — a real order can't join a practice opportunity, or vice versa.

Read performance

Compare models head to head with group_by=model, or drill into one. Archived models keep their name and history in every read.

GET/flow/v1/auth/performance

Currency-partitioned P&L across all your orders. With group_by=model, each currency bucket carries one group per model (orders with no model group under 'No model').

NameInTypeRequiredDescription
group_byqueryenumnoUse model to compare models.
fromqueryiso8601noWindow start. Default: 90 days ago.
toqueryiso8601noWindow end. Default: now.
Try it
GET/flow/v1/auth/performance?group_by=model

Use model to compare models.

Window start. Default: 90 days ago.

Window end. Default: now.

GET/flow/v1/auth/models/:model_id/performance

P&L for one model. group_by works here too — e.g. group_by=league to see where an all-league agent earns.

NameInTypeRequiredDescription
model_idpathstringyes
group_byqueryenumno
fromqueryiso8601no
toqueryiso8601no
Try it
GET/flow/v1/auth/models/%3Amodel_id/performance?group_by=none

Order and opportunity history accept a model_id filter and return model_id + model_name on each row: GET /flow/v1/auth/orders?model_id=…, GET /flow/v1/auth/opportunities?model_id=….

Manage models

Reads need account:read; changes need trade:execute.

GET/flow/v1/auth/models

Your models, newest first. Archived models are hidden unless asked for.

NameInTypeRequiredDescription
statusqueryenumnoOnly this status.
include_archivedqueryenumnoReturn active and archived together.
Try it
GET/flow/v1/auth/models

Only this status.

Return active and archived together.

GET/flow/v1/auth/models/:model_id

One model, archived included.

NameInTypeRequiredDescription
model_idpathstringyes
Try it
GET/flow/v1/auth/models/%3Amodel_id
Update, archive, unarchive
PATCH /flow/v1/auth/models/:model_id          { "model_name"?, "model_description"?, "league_id"? }
POST  /flow/v1/auth/models/:model_id/archive  # stops new orders; name + history kept; idempotent
POST  /flow/v1/auth/models/:model_id/unarchive # accepts orders again (counts toward the 50-model cap)
Status only changes through archive / unarchive — PATCH with status is rejected. Models are never deleted, so a model's past orders always resolve to its name.