# List your own markets on STX

> Create player-prop markets on STX events from your server, check them with a dry run, then quote them like any other market.

Source: https://docs.stxapp.io/apps/creating-markets/

> **Availability.** Available on US demo now. See [Environments](https://docs.stxapp.io/environments/).

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

This is separate from [adding STX accounts to your app](/oauth/). 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

Ask your STX contact to enable market creation for your app, on the demo exchange first. STX then:

- adds the `markets.write` app 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

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](/environments/) lists them.

```bash
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \
  -d grant_type=client_credentials \
  -d scope="events markets.write"
```

```json
{ "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](/oauth/authorization-flow/#app-tokens-client-credentials).

## 2. See what you can create

```bash
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:

```json
{
  "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`](/apps/api/list-market-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

```bash
curl -s "$STX/api/v1/events?status=scheduled" -H "Authorization: Bearer $APP_TOKEN"
```

[`GET /api/v1/events`](/api/rest/events/list-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

```bash
curl -s "$STX/api/v1/events/$EVENT_ID/players" -H "Authorization: Bearer $APP_TOKEN"
```

Shortened to two players and three stats:

```json
{
  "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`](/apps/api/list-event-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.
- `teams` is empty until a roster is published. Try again later.
- Send `player_id` back exactly as returned.

## 5. Create the markets

[`POST /api/v1/markets`](/apps/api/create-markets/) takes one event and 1 to 25 markets. Send it with the app token from step 1, from an allow-listed address:

```bash
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](#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:

```json
{
  "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`:

```json
{
  "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

| `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.

```json
{
  "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

- A market you already created answers `exists` with the same `market_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 `rejected` with `not_created` and none is created, so you can send the same request again as it is.

### 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:

```json
{
  "event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
  "markets": [
    { "index": 0, "reason": null, "status": "valid", "market_id": null }
  ]
}
```

### 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

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

The new markets behave like any other STX market. Find them with [`GET /api/v1/markets`](/api/rest/markets/list-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](/api/rest/orders/). STX settles them from its stats feed when the game ends.

## With the TypeScript SDK

[`@stxapp/stx-typescript`](/sdks/typescript/) 0.8.2 and later wraps these calls: `npm install @stxapp/stx-typescript`.

```ts
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`.
