Models & attribution
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_idon later buys to add to it. - Order → one leg on one venue.
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).
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" }'
{
"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.
{
"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 samemodel_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.
/flow/v1/auth/performanceCurrency-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').
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| group_by | query | enum | no | Use model to compare models. |
| from | query | iso8601 | no | Window start. Default: 90 days ago. |
| to | query | iso8601 | no | Window end. Default: now. |
/flow/v1/auth/performance?group_by=modelUse model to compare models.
Window start. Default: 90 days ago.
Window end. Default: now.
/flow/v1/auth/models/:model_id/performanceP&L for one model. group_by works here too — e.g. group_by=league to see where an all-league agent earns.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| model_id | path | string | yes | |
| group_by | query | enum | no | |
| from | query | iso8601 | no | |
| to | query | iso8601 | no |
/flow/v1/auth/models/%3Amodel_id/performance?group_by=noneOrder 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.
/flow/v1/auth/modelsYour models, newest first. Archived models are hidden unless asked for.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| status | query | enum | no | Only this status. |
| include_archived | query | enum | no | Return active and archived together. |
/flow/v1/auth/modelsOnly this status.
Return active and archived together.
/flow/v1/auth/models/:model_idOne model, archived included.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| model_id | path | string | yes |
/flow/v1/auth/models/%3Amodel_idPATCH /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)
PATCH with status is rejected. Models are never deleted, so a model's past orders always resolve to its name.