List your own markets on STX
An approved app can list its own player-prop markets on STX events. You name the event, the player, the stat and the line; STX builds the market, settles it from its stats feed, and tells you what happened to each one you asked for.
How this differs from adding STX accounts
Section titled “How this differs from adding STX accounts”This is separate from adding STX accounts to your app. You can do either without the other.
| Adding STX accounts | Creating markets | |
|---|---|---|
| Who acts | Your app, for a member who approved it | Your app, as itself. No member, no login, no consent screen |
| Token | A member’s access token | An app token (client_credentials) |
| Access | Any registered app | Granted by STX on request, per app |
| Where calls come from | Your allow-listed addresses, when you hold a write scope | Your allow-listed addresses, always |
An API key cannot create markets. A member token cannot either.
Get access
Section titled “Get access”Ask your STX contact to enable market creation for your app, on the demo exchange first. STX then:
- adds the
markets.writeapp scope to your client. Only STX can grant it; - sets your client’s IP allow-list to the addresses your servers call from. A call from any other address is refused.
1. Get an app token
Section titled “1. Get an app token”Every call on this page is authenticated the same way: an app token in the Authorization header. It is not a member’s token and not an API key, and the call must come from one of your allow-listed addresses.
Request the token with the client_credentials grant, authenticating with your client_id and client_secret over HTTP Basic. Ask for markets.write, and events for the read calls in steps 2 to 4. $STX in the examples is the base URL of the exchange you are calling. Environments lists them.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=client_credentials \ -d scope="events markets.write"{ "access_token": "stxapp_at_…", "token_type": "bearer", "expires_in": 3600, "scope": "events markets.write" }Keep access_token as $APP_TOKEN for the calls below. There is no refresh token: request a new one when it expires. See App tokens.
2. See what you can create
Section titled “2. See what you can create”curl -s "$STX/api/v1/types" -H "Authorization: Bearer $APP_TOKEN"The response has one entry per rule. This is the player_stat_line entry, with the other rules, event types and stats left out:
{ "player_stat_line": { "partner_create": true, "fields": { "player_id": { "type": "string", "description": "A player_id from GET /api/v1/events/{event_id}/players." }, "stat": { "type": "string", "description": "One of the event type's stats." }, "line": { "type": "number", "description": "A positive half line, e.g. 19.5." } }, "event_types": { "basketball_game": { "sport": "Basketball", "stats": [ { "name": "Assists", "stat": "ASSISTS" }, { "name": "Points", "stat": "POINTS" }, { "name": "Steals", "stat": "STEALS" } ] } } }, "spread_r1": { "event_types": ["baseball_game", "football_game", "soccer_game"], "partner_create": false }}GET /api/v1/types lists the market rules, keyed by rule. A rule with partner_create: true can be created through the API, and lists the fields an entry takes and, for each event type, the stats on offer.
Today one rule can be created: player_stat_line, over or under a player’s stat. Read /types once and cache it; it changes only when STX adds a rule or a stat.
3. Find the event
Section titled “3. Find the event”curl -s "$STX/api/v1/events?status=scheduled" -H "Authorization: Bearer $APP_TOKEN"GET /api/v1/events lists upcoming events. Take the event’s id. Markets can be created only while the event is scheduled and has not started.
4. Get the players
Section titled “4. Get the players”curl -s "$STX/api/v1/events/$EVENT_ID/players" -H "Authorization: Bearer $APP_TOKEN"Shortened to two players and three stats:
{ "event_id": "25f19747-3398-4333-81c3-2e457f83bda1", "teams": [ { "team": "Orlando Magic", "players": [ { "name": "Alex Morales", "position": "G", "jersey": "30", "player_id": "4593034" }, { "name": "Anthony Black", "position": "G", "jersey": "0", "player_id": "4712849" } ] } ], "stats": [ { "name": "Assists", "stat": "ASSISTS" }, { "name": "Points", "stat": "POINTS" }, { "name": "Steals", "stat": "STEALS" } ]}GET /api/v1/events/{event_id}/players returns each team’s roster, and the stats on offer for this event.
- Rosters are the teams’ current rosters, not game-day actives, and can be up to 15 minutes old.
teamsis empty until a roster is published. Try again later.- Send
player_idback exactly as returned.
5. Create the markets
Section titled “5. Create the markets”POST /api/v1/markets takes one event and 1 to 25 markets. Send it with the app token from step 1, from an allow-listed address:
curl -s -X POST "$STX/api/v1/markets" \ -H "Authorization: Bearer $APP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event_id": "25f19747-3398-4333-81c3-2e457f83bda1", "markets": [ { "rule": "player_stat_line", "player_id": "4593034", "stat": "STEALS", "line": 0.5 } ] }'| Field | Value |
|---|---|
event_id |
The event, from step 3. |
dry_run |
Optional. true checks everything and creates nothing; see Dry run. Defaults to false. |
rule |
Required. player_stat_line. |
player_id |
Required. From step 4. |
stat |
Required. One of the event’s stats, such as STEALS. |
line |
Required. A positive half line, such as 0.5 or 19.5. |
The response is 200 with one outcome per entry, in request order:
{ "event_id": "25f19747-3398-4333-81c3-2e457f83bda1", "markets": [ { "index": 0, "reason": null, "status": "created", "market_id": "cbcc7ea5-a3c4-4c38-a00b-17bdccd04816" } ]}index is the entry’s position in your request, from 0. All four fields are always present.
Send the same request again and the market is not created twice. It answers exists, with the same market_id:
{ "event_id": "25f19747-3398-4333-81c3-2e457f83bda1", "markets": [ { "index": 0, "reason": null, "status": "exists", "market_id": "cbcc7ea5-a3c4-4c38-a00b-17bdccd04816" } ]}status |
Meaning | market_id |
|---|---|---|
created |
A new market. STX sets its title, rules and price range. | The new market |
exists |
A market for the same player, stat and line is already on the event. Nothing new is created. | The existing market |
valid |
Dry run only: the entry passed every check and would be created. | null |
rejected |
Not created. reason says why. |
null |
Reject reasons
Section titled “Reject reasons”reason |
Why | What to do |
|---|---|---|
invalid_market |
The entry is malformed: it is not an object, has no rule or no string player_id, or holds a field other than rule, player_id, stat and line. An entry in the older stat_detail form gets this too. |
Fix the entry. |
unsupported_rule |
rule is not one the API creates. |
Use a rule with partner_create: true in /types. |
unsupported_stat |
The stat is not on offer for this event. | Use one of the event’s stats. |
invalid_line |
The line is not a positive half, such as 19.5. |
Fix the line. |
unknown_player |
player_id is not on the event’s rosters. |
Read the players again. |
event_not_open |
The event started while the request ran. | Stop; markets cannot be added to it. |
not_created |
The entry was valid, but the save failed. | Resend the same request; see below. |
A request can mix outcomes. Here the first entry is created, the second already exists, and the next three are rejected: a whole-number line, a player who is not on the event, and a stat the event does not offer.
{ "event_id": "cf1bfed8-106e-44ea-a3f1-351be4765728", "markets": [ { "index": 0, "reason": null, "status": "created", "market_id": "a3868943-bc39-477b-9c02-d3496514167d" }, { "index": 1, "reason": null, "status": "exists", "market_id": "787ec653-a60a-4c1d-8cc1-87523e3d207e" }, { "index": 2, "reason": "invalid_line", "status": "rejected", "market_id": null }, { "index": 4, "reason": "unknown_player", "status": "rejected", "market_id": null }, { "index": 5, "reason": "unsupported_stat", "status": "rejected", "market_id": null } ]}The entries at index 3 and 6 to 11 of that response are left out here.
Sending a request again is safe
Section titled “Sending a request again is safe”- A market you already created answers
existswith the samemarket_id, whether you resend it or repeat it within one request. It is never created twice. - The valid entries of a request are saved together, all or nothing. If that save fails, every one of them answers
rejectedwithnot_createdand none is created, so you can send the same request again as it is.
Dry run
Section titled “Dry run”With "dry_run": true, every check runs and nothing is created. An entry that would be created answers valid, and so does a repeat of it in the same request. A market that is already listed answers exists with its market_id. Anything else answers rejected with its reason. Use it to check a batch before you list it.
The request from step 5 with "dry_run": true, before the market existed:
{ "event_id": "25f19747-3398-4333-81c3-2e457f83bda1", "markets": [ { "index": 0, "reason": null, "status": "valid", "market_id": null } ]}When the whole request fails
Section titled “When the whole request fails”| Status | Body | Meaning |
|---|---|---|
400 |
{"error": "markets must be a list of 1 to 25 markets"} |
markets is missing, empty or longer than 25. |
400 |
{"error": "dry_run must be true or false"} |
dry_run is not a boolean; the string "true" is refused. |
400 |
{"error": "event_id is required"} |
event_id is missing. |
401 |
{"error": "Missing or invalid API key credentials"} |
No token was sent. |
403 |
{"error": "insufficient scope for this operation", "scope": "markets.write"} |
The app token does not carry markets.write. |
403 |
{"error": "creating markets needs an OAuth app token with the markets.write scope"} |
An API key or a member token was used. |
403 |
{"error": "ip_not_allowed", "error_description": "this app does not allow requests from this IP address"} |
The call came from an address outside your allow-list. |
404 |
{"error": "event not found"} |
Unknown event. |
422 |
{"error": "event_not_open"} |
The event is not scheduled, or has already started. |
422 |
{"error": "unsupported_event"} |
No player props for this event’s type. |
422 |
{"error": "roster_unavailable"} |
The event’s rosters are not published yet. |
How long it takes
Section titled “How long it takes”Measured on the demo exchange, from one client, with a few samples each. About 0.14 seconds of every figure is the network round trip.
| Request | Time to answer |
|---|---|
| 1 market, dry run | About 0.16 seconds |
| 1 market, created | About 0.17 seconds |
| 1 market that already exists | About 0.15 seconds |
| 25 new markets, dry run | 0.33 to 0.44 seconds |
A created market was returned by GET /api/v1/markets?market_ids=<market_id> on the first request sent after the create answered.
Treat these as what to expect, not as a guarantee: the samples are few, and a first request on a new connection adds the time to connect.
On a busy system a batch can take much longer, and a request that times out or answers a 5xx may still have created its markets. Send the same request again, or send it with dry_run: true: entries that were created answer exists with their market_id, and nothing is created twice. Allow at least 30 seconds before your client gives up on a request.
6. Quote the markets
Section titled “6. Quote the markets”The new markets behave like any other STX market. Find them with GET /api/v1/markets and the market data channels (to look one up by id, use ?market_ids=<market_id>; there is no /markets/{id} route), and quote them with your market-making account’s API key through the orders endpoints. STX settles them from its stats feed when the game ends.
With the TypeScript SDK
Section titled “With the TypeScript SDK”@stxapp/stx-typescript 0.8.2 and later wraps these calls: npm install @stxapp/stx-typescript.
import { AppScopes, OAuthClient } from "@stxapp/stx-typescript/oauth";
const oauth = new OAuthClient({ clientId, clientSecret });const stx = oauth.appClient([AppScopes.EVENTS, AppScopes.MARKETS_WRITE]);
const roster = await stx.eventPlayers(eventId);const player = roster.teams[0].players[0];const markets = [{ rule: "player_stat_line", playerId: player.player_id, stat: roster.stats[0].stat, line: 19.5 }];
// Check first, then create.const checked = await stx.createMarkets(eventId, markets, { dryRun: true });if (checked.every((r) => r.status === "valid" || r.status === "exists")) { const results = await stx.createMarkets(eventId, markets); for (const r of results) console.log(r.index, r.status, r.market_id ?? r.reason);}createMarkets() returns one result per market, in the order you passed them, with the response’s index, status, market_id and reason. { dryRun: true } sends dry_run. { validate: true } checks players, stats and lines on your side before sending. stx.marketTypes() reads GET /api/v1/types.

