{
  "openapi": "3.1.1",
  "info": {
    "title": "BetLigPrototype | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.betrivals.ge/"
    }
  ],
  "paths": {
    "/api/auth/register": {
      "post": {
        "tags": [
          "Auth"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RegisterRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RegisterRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RegisterRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTokenResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/getUser": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "What the player frontend calls on load to decide whether it is signed in.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/Identity/login": {
      "post": {
        "tags": [
          "Identity"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfLoginResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfLoginResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfLoginResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/Identity/getUser": {
      "get": {
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{roomId}/insights/participants": {
      "get": {
        "tags": [
          "Insights"
        ],
        "summary": "PARTICIPANTS PICKS — every ticket, one by one, grouped by contest.",
        "description": "The screen a small room draws: each player's row under each contest they picked, carrying\nonly the picks they made on it.\n\n<b>Above the threshold this answers the other screen instead.</b> A room of four thousand has\nno ticket-by-ticket screen — the design does not draw one, and as a payload it would be some\ntwenty thousand rows. So rather than refuse, or send something nobody can render, it returns\nwhat that room's screen actually is and says so in `useTopPicks`. Either endpoint is\nsafe to call at any size; the flag says which of the two you were given.\n\nContests nobody picked come back with empty lists rather than left out, so a missing contest\nmeans a missing contest and not a quiet zero.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{roomId}/insights/top-picks": {
      "get": {
        "tags": [
          "Insights"
        ],
        "summary": "EXPLORE TOP PICKS — a row per selection with a head-count, grouped by contest.",
        "description": "The screen a big room draws, and available at any size: a small room's crowd is a smaller\ncrowd, not an absent one.\n\nThe counts are not paged and do not need to be. A selection row exists only where somebody\npicked, so they are bounded by what the room offers rather than by its seat count — the\nleaderboard grows with players, this grows with distinct opinions, and those run out fast.\nFour thousand players still argue over the same handful of markets.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfInsights"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms": {
      "get": {
        "tags": [
          "Lobby"
        ],
        "summary": "The lobby list.",
        "description": "Ordered by day, newest first, and inside each day upcoming before live before ended. Not\nsortable: the order is the product's, not a column header, so there is no `sort` here.\n\nPaged the usual way. The screen has no page numbers — it loads more as you scroll — so send\n`size` and walk `page`; `totalCount` says when to stop.\n\nUnpublished rooms are never returned, and cancelled ones only to a player who entered them.",
        "parameters": [
          {
            "name": "Sport",
            "in": "query",
            "description": "football, basketball or mma. Repeated or comma-separated, like every other filter.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "CompetitionId",
            "in": "query",
            "description": "A chip. Absent means \"All\".",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "GameType",
            "in": "query",
            "description": "winner_takes_all or top_3 — the \"Room Type\" dropdown.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "EntryFeeFrom",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "EntryFeeTo",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "MaxWinFrom",
            "in": "query",
            "description": "Runs on the figure the row labels MAX — first prize at a full room.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "MaxWinTo",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "TargetX",
            "in": "query",
            "description": "The \"target X\" chips: a room's seat capacity. `targetX=10` is the ten-seat rooms;\n`targetXFrom=1000` is every room of a thousand seats or more. Exact wins over the range\nwhen both are sent.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "TargetXFrom",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "TargetXTo",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "AsOf",
            "in": "query",
            "description": "What \"now\" means for this request. Omit on the first page and send it back on every later\npage of the same scroll.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "Page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Skip",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfLobbyRoom"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfLobbyRoom"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfLobbyRoom"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{id}": {
      "get": {
        "tags": [
          "Lobby"
        ],
        "summary": "One room, with its terms, its contests and this player's ticket.",
        "description": "The whole of the room screen in one call. The contests are part of it rather than their own\nendpoint — a room holds a handful, and they are the reason the screen exists, so splitting\nthem out would mean two round trips before anything renders.\n\n`ticket` is null until this player has placed one, which is how the screen knows\nwhether to offer picking or show what they hold.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPlayerRoomDetail"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{id}/ticket": {
      "post": {
        "tags": [
          "Lobby"
        ],
        "summary": "Places this player's ticket, which is also how they join the room.",
        "description": "Takes selection ids and nothing else. One id names the contest, the market, the selection\nand the price at once, because it is the key of the room's own frozen row — so a pick can\nnever carry a price the room did not offer, and there is no market name to mis-spell.\n\nThe seat, the fee and the picks are written together. A ticket without its payment would be\nworse than a refusal.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PlaceTicketModel"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PlaceTicketModel"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PlaceTicketModel"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicket"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{id}/ticket/check": {
      "post": {
        "tags": [
          "Lobby"
        ],
        "summary": "Which selections the picks held so far rule out — so the screen can disable them before the\ntap, not refuse them after it.",
        "description": "Sent after every change to the picks, with the ids currently held. Nothing is blocked on an\nempty ticket; a selection is blocked only as a consequence of a pick, and names that pick in\n`because`, so un-picking frees it again. The same rule runs again at\n`POST .../ticket`, which is where it is actually enforced.\n\nRead-only and idempotent, but a POST because the held picks travel in the body — a dozen\nGUIDs do not belong in a query string.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/TicketCheckModel"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/TicketCheckModel"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/TicketCheckModel"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfTicketCheck"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/competitions": {
      "get": {
        "tags": [
          "Lobby"
        ],
        "summary": "The competition chips above the list.",
        "description": "Its own call rather than a field on the list: the chips describe the whole filtered set, and\nbuilding them from the current page would silently drop every competition whose rooms fell\npast the page boundary.\n\nOnly competitions with a room this player can see, so a chip never leads to an empty list.\nThat guarantee is why `mine` exists: My Tournaments lists rooms the player is already\nin, and the lobby's chips count every league they could join — so reusing them there would\noffer a league with no entry in it and an empty list behind the tap.",
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "mine",
            "in": "query",
            "description": "True for My Tournaments: count only leagues this player holds an entry in. False or absent\nfor the lobby: every league with a room they could join.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfLobbyCompetition"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfLobbyCompetition"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfLobbyCompetition"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/competitions": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "summary": "Competitions with a contest on one date, discovered live, plus any already imported.",
        "description": "Nothing is written. A competition already carrying rooms still appears and can take more —\nimporting it for one room does not consume it, which roomCount exists to make obvious. One\nwith nothing on the requested date still appears with a zero contest count, since those are\nthe ones existing rooms are built on.",
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "description": "football, basketball, mma or tennis. Tennis has no provider.",
            "schema": {
              "default": 0,
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "ISO yyyy-MM-dd, inclusive. Defaults to today, UTC.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "ISO yyyy-MM-dd, inclusive. Defaults to from, so naming only one bound\nasks for that single day.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Deprecated single-day form, kept working. Ignored if from or to is sent.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfCompetitionDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfCompetitionDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfCompetitionDto"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/matches": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "summary": "One competition's contests on one date, live from the provider.",
        "description": "Read-only: nothing is stored, and no odds are fetched, so there is no priced flag here.\nWhether a contest is priced is discovered when the room is created, which reports any it\nhad to drop. Send these `contestId` values to `POST /api/admin/rooms`.",
        "parameters": [
          {
            "name": "competitionId",
            "in": "query",
            "description": "The provider's competition id, from the competitions lookup.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sport",
            "in": "query",
            "description": "Must match the competition's sport.",
            "schema": {
              "default": 0,
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "ISO yyyy-MM-dd, inclusive. Defaults to today, UTC.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "ISO yyyy-MM-dd, inclusive. Defaults to from.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Deprecated single-day form, kept working. Ignored if from or to is sent.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfContestDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfContestDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfContestDto"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/matches/{id}/markets": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "summary": "Every stored market on one contest, grouped by market. Takes the local id, not the\nprovider's.",
        "description": "These are the contest's own prices, which are shared between every room using it and move\nwith the feed. For what a particular room offers, use\n`GET /api/admin/rooms/{roomId}/matches/{matchId}/markets` — a published room answers\nfrom its frozen snapshot instead.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/matches/{id}/markets/extended": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "summary": "The same markets, each trimmed to the two or three selections that represent it.",
        "description": "The player app's \"Extended Markets\" view. Each market still reports its true\n`selectionCount`, so the screen can show \"3 of 12\" and link to the full view.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/entries": {
      "get": {
        "tags": [
          "PlayerEntries"
        ],
        "summary": "The rooms this player is in.",
        "description": "Ordered exactly as the lobby is — day descending, and inside a day upcoming before live\nbefore ended — because it is the same mental model and two orders would be one to learn too\nmany. Not sortable, for the same reason.\n\nPaged the shared way: send `size`, walk `page`, stop on `totalCount`.",
        "parameters": [
          {
            "name": "Sport",
            "in": "query",
            "description": "football, basketball or mma. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "CompetitionId",
            "in": "query",
            "description": "One league. A chip, like the lobby's — absent means \"All\".",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "RoomState",
            "in": "query",
            "description": "open, full, live, ended or cancelled. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "PlayerState",
            "in": "query",
            "description": "joined, won, lost or refunded. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "Page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Skip",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfPlayerEntry"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfPlayerEntry"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfPlayerEntry"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{roomId}/matches/{matchId}/markets": {
      "get": {
        "tags": [
          "PlayerMarkets"
        ],
        "summary": "Every market on the contest, with every selection — the \"view all\" screen.",
        "description": "Each market names the tab it belongs under in `category`, since the provider gives names\nand no grouping of its own.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{roomId}/matches/{matchId}/markets/extended": {
      "get": {
        "tags": [
          "PlayerMarkets"
        ],
        "summary": "The short list — the rows behind \"Extend Markets\".",
        "description": "The room screen already carries this for every contest it holds, so the player app does not\nneed it: expanding a card is client-side. Kept for callers that hold a contest id without\nhaving loaded the room.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/popups": {
      "get": {
        "tags": [
          "Popups"
        ],
        "summary": "Every win still owed a celebration, oldest settlement first.",
        "description": "Oldest first because a player back after a week should be walked forward through what\nhappened rather than backwards from the most recent. A player in several rooms that settled\nwhile they were away gets several, and works through them one at a time.\n\nCalled wherever a popup could be shown — the lobby, the home screen — and on\n`balanceChanged` with reason `payout`, which is the live signal that a new one has\njust arrived.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/popups/{roomId}/seen": {
      "post": {
        "tags": [
          "Popups"
        ],
        "summary": "Marks one win as shown and hands back what is still waiting.",
        "description": "Returns the remaining queue rather than a count, so a client closing one popup can open the\nnext without another round trip — which is the whole interaction: close, next, close, next.\n\n<b>Going to the room counts as being shown.</b> The client calls this before it navigates,\nso the popup does not reappear when the player comes back to the lobby.\n\nMarking a win that is already marked is not an error, and neither is marking a room this\nplayer never won. Both answer with the queue as it stands. A dismissal is a state, and\nsetting a state twice should leave it where the caller wanted it — a popup that errored on a\ndouble-tap would be a worse bug than the one it reported.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfIReadOnlyListOfPopup"
                }
              }
            }
          }
        }
      }
    },
    "/api/player/rooms/{roomId}/ranking": {
      "get": {
        "tags": [
          "Ranking"
        ],
        "summary": "Every entry in the room, best first.",
        "description": "Paged, because a room can hold ten thousand seats. The caller's own row is **not** promoted\ninto the page — it is on the room detail's `ticket`, which the screen already has, and\npinning it here would mean a page that sometimes holds one row more than it was asked for.\n\nA row's `picks` arrive only once the room has left `open`. Everyone in a room that\ncan still be joined would otherwise be handing their ticket to whoever joins next.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Skip",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRankingRow"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRankingRow"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRankingRow"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRankingRow"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRankingRow"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRankingRow"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/entries": {
      "get": {
        "tags": [
          "RoomEntries"
        ],
        "summary": "One row per player. A player enters once, so one entry is one ticket.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "rank (default), score or joinedAt. A leading minus means descending, so\n`sort=-rank` and `sort=rank&amp;order=desc` are equivalent — the same convention\nthe rooms and players lists use. An unrecognised key falls back to rank.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "asc or desc. Wins over a sign on the sort key when both are sent.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfEntryDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfEntryDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfEntryDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/analytics": {
      "get": {
        "tags": [
          "RoomEntries"
        ],
        "summary": "Figures an admin reads off a room. Every one is computed from entries, picks and the\nroom's own terms — none of it is stored, so none of it can disagree with the rows.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomAnalytics"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomAnalytics"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomAnalytics"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/matches/{matchId}/markets": {
      "get": {
        "tags": [
          "RoomMarkets"
        ],
        "summary": "Every market this room offers on one contest, with every selection.",
        "description": "Reads the room's frozen snapshot once it is published, and the contest's live feed prices\nbefore that. Those differ on purpose: contests are shared and their prices keep moving,\nwhile a published room's copies do not — so this is the only view that answers \"what is\nthis room actually offering\", rather than \"what does the feed say right now\".",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/matches/{matchId}/markets/extended": {
      "get": {
        "tags": [
          "RoomMarkets"
        ],
        "summary": "The same markets, each trimmed to the two or three selections that represent it.",
        "description": "The player app's \"Extended Markets\" view: one row per market rather than every line, so a\ncontest carrying 200 selections across a dozen markets is readable at a glance. Each market\nstill reports its true `selectionCount`, so the screen can show \"3 of 12\" and link to\nthe full view.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfContestMarketsDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/results/random": {
      "post": {
        "tags": [
          "RoomResults"
        ],
        "summary": "Random results for every contest in the room that has none yet.",
        "description": "Contests already finished are left exactly as they are and reported under\n`alreadyHadResults`. So this is the \"finish the rest\" button: safe to press after\nsetting some results by hand, and safe to press twice.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{roomId}/results/random-selected": {
      "post": {
        "tags": [
          "RoomResults"
        ],
        "summary": "Random results for named contests only.",
        "description": "Takes local contest ids — a room detail's `matches[].id`. Contests not in this room are\nrefused rather than ignored, since a mistyped id would otherwise look like it worked.",
        "parameters": [
          {
            "name": "roomId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SettleSelectedModel"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SettleSelectedModel"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SettleSelectedModel"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "summary": "The list an admin opens to answer \"what needs me now\". Filters AND together, values inside\none filter OR, and with nothing passed it is every status by soonest kick-off.",
        "description": "Sortable on startDate (default), entryFee, participants and prizePool. Sorting by title is\ndeliberately absent — this list is scanned by when a room starts or what it is worth, not\nalphabetically. An unrecognised key falls back to the default rather than erroring.",
        "parameters": [
          {
            "name": "Search",
            "in": "query",
            "description": "Matches the room's title or subtitle, contains, case-insensitive.",
            "schema": {
              "maxLength": 255,
              "minLength": 0,
              "type": "string"
            }
          },
          {
            "name": "Sport",
            "in": "query",
            "description": "football, basketball, mma or tennis. Repeated or comma-separated. A room's sport comes from\nits competition, so this filters on that.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "Status",
            "in": "query",
            "description": "Ongoing, Locked, Ended or Cancelled. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "GameType",
            "in": "query",
            "description": "winner_takes_all or top_3. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "CompetitionId",
            "in": "query",
            "description": "Only rooms belonging to this competition.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "IsPublished",
            "in": "query",
            "description": "True for live rooms, false for drafts held back from players.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "EntryFeeFrom",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "EntryFeeTo",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "ParticipantsFrom",
            "in": "query",
            "schema": {
              "maximum": 2147483647,
              "minimum": 0,
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "ParticipantsTo",
            "in": "query",
            "schema": {
              "maximum": 2147483647,
              "minimum": 0,
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "PrizePoolFrom",
            "in": "query",
            "description": "Filters the room's max prize figure, not its current one.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "PrizePoolTo",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "StartDateFrom",
            "in": "query",
            "description": "Earliest kick-off to include. A plain date is read as UTC.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "StartDateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "Page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Skip",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Order",
            "in": "query",
            "schema": {
              "pattern": "(?i)^(asc|desc)$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRoomListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRoomListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfRoomListItem"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Create a room",
        "description": "Takes the provider's own contest ids and imports them: the competition, the contests and\ntheir odds are written in one transaction. The competition is resolved from the contests\nrather than passed in — they must resolve to exactly one — and neither it nor the sport can\nbe changed afterwards.\n\nA contest the provider has not priced is left out of the room and named in\n`droppedContests`. Only a selection where *nothing* is priced is refused, because there\nwould be no room to save.\n\n`requiredPicks` is deliberately not checked against the number of contests. A ticket may\nhold several picks on one contest -- see TicketRules for which combinations\nare refused -- so a single-match room can honestly require three.\n\nRejects with 400 and field-keyed errors when: an id is unknown to the provider, the\ncontests span more than one competition, the sport does not match the competition's,\nentry fee is not above zero, rake is outside 0-100, seats are fewer than two, or the game\ntype pays more places than there are seats.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomCreateModel"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomCreateModel"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomCreateModel"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Rooms"
        ],
        "summary": "Update. Carries no sport, competition or publish flag: the first two are fixed at\ncreation and publishing is its own command. Name and description are editable at any\ntime; everything else — including the match selection — is editable only while nobody\nhas entered, and afterwards returns 409 naming the field.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomUpdateModel"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomUpdateModel"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/RoomUpdateModel"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{id}": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "summary": "Get one room with its derived figures",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRoomDetail"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Rooms"
        ],
        "summary": "Deletes a room outright, as though it had never been built.",
        "description": "<b>Not the way out of a room that has gone wrong in front of players</b> — that is\n`cancel`, which refunds everyone and leaves the record standing. This is for a room that\nshould not exist at all, and it leaves no record, which is the point.\n\nIt erases money history: every wallet row naming the room, entry fees and payouts alike, so\nbalances read as though it was never built. And because contests are shared between rooms, it\nwinds the room's contests back to unplayed and un-settles any other room that had been paid\nout on them — otherwise a neighbour would be left holding a scoreline invented by a room\nnobody can look at any more.\n\nA prototype tool. It has no business in anything holding real money.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRemoveResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/setStatus": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "The on switch. Turning it on runs the publish checks; turning it off runs none but is\nrefused once anyone has entered, since hiding a room somebody paid into would take away\ntheir only way of seeing their own ticket.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetStatusRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetStatusRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetStatusRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetStatusResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{id}/cancel": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Calls a room off and gives every player their entry fee back.",
        "description": "The way out of a room that has gone wrong, and the only one — there is no delete, because a\nroom somebody paid into has to keep existing for them to see what happened to it. A cancelled\nroom pays nobody and refunds everybody, with no ranking and no house cut.\n\nUntil this existed the poller could cancel a room and an admin could not: a room with the\nwrong fixtures on it, or one the operator simply wanted to withdraw, had to be edited in the\ndatabase by hand — which is not something to discover during a demonstration.\n\nEach refund is exactly what that player was charged, not the room's current fee. A settled\nroom is refused rather than unwound: the prizes are already in people's balances.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CancelRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CancelRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CancelRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCancelResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{id}/pin": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Pins a room to the top of the room list and the players' lobby.",
        "description": "Editorial, and nothing more: it says \"look at this one first\". <b>Five at once</b>, and a\nsixth is refused naming the five in the way rather than quietly releasing the oldest — two\noperators on one panel should not be able to undo each other by pressing a button that\nalways appears to work.\n\n<b>Pinning reorders, it never admits.</b> Every filter on both lists still applies, so a\npinned room outside what the caller asked for is not shown. Pinning an unpublished room is\nallowed and does nothing for players until it is published, which is the same rule the\nlobby already follows.\n\nPressing it on an already-pinned room is not an error. The control is a state, and setting\na state twice should leave it where the operator wanted it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Rooms"
        ],
        "summary": "Unpins a room. Doing it to a room that is not pinned is not an error.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPinResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{id}/fill": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Fills a room with players holding random tickets, for a demonstration.",
        "description": "A room with two entries has a leaderboard nobody can read anything into. This adds up to two\nhundred at a time, each with a ticket drawn at random from everything the room offers — every\nmarket in its frozen snapshot, not the handful the screen shows first — and at most one pick\nper market, so no ticket holds both sides of the same question.\n\n<b>They join by a player's rules.</b> A room that is cancelled, settled, started or full\nrefuses this exactly as it refuses a person, so there is no way to seat somebody where a\nplayer could not sit. They pay the entry fee from a balance funded for the purpose, which\nkeeps the pot, the prize table and the balances on screen consistent.\n\nSynchronous, and fast enough to be: the accounts carry no password — nobody signs in as one\n— so there is no hashing, and two hundred tickets are a single insert measured under 200ms.\n\nA prototype tool. It has no business in anything holding real money.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FillModel"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/FillModel"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/FillModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfFillResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/{id}/refreshMarkets": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Reads the odds again for a published room's contests and adds every selection they have\ngained since the room was frozen. Nothing already offered changes.",
        "description": "Bookmakers add markets as kick-off nears — corners about three days out, cards on match\nday — so a room built early is missing them. This is the operator's way to pick them up\nwithout deleting the room. One provider request per contest.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfRefreshMarketsResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/rooms/prizePreview": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "summary": "What a room on these terms would pay, before it is saved.",
        "description": "For the room builder, which needs to show the prize table while the operator is still typing\nthe entry fee and the seat count. Nothing is read or written — it is the same arithmetic the\nroom detail returns, run on numbers that are not a room yet.\n\nThe figure it exists for is `clearsEntryAt`. On 70 / 20 / 10 the lower places are small,\nand an operator building a ten-seat room at a 2.00 entry needs to know before they publish\nthat third place will pay 1.86 against a 2.00 buy-in. The rule is not changed to hide it: the\nwarning is shown and the operator decides.",
        "parameters": [
          {
            "name": "entryFee",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "maxParticipants",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "rakePercent",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double",
              "default": 7
            }
          },
          {
            "name": "gameType",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfPrizePreview"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Balance and entry count are computed in SQL, so filtering, sorting and paging on them stay\none query rather than a page-then-filter pass.",
        "parameters": [
          {
            "name": "Search",
            "in": "query",
            "description": "Matches the username or email, contains, case-insensitive.",
            "schema": {
              "maxLength": 255,
              "minLength": 0,
              "type": "string"
            }
          },
          {
            "name": "BalanceFrom",
            "in": "query",
            "description": "Lowest balance to include, as a decimal string.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "BalanceTo",
            "in": "query",
            "description": "Highest balance to include.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
              "type": [
                "number",
                "string"
              ],
              "format": "double"
            }
          },
          {
            "name": "EntriesFrom",
            "in": "query",
            "description": "Fewest confirmed entries across all rooms.",
            "schema": {
              "maximum": 2147483647,
              "minimum": 0,
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "EntriesTo",
            "in": "query",
            "description": "Most confirmed entries across all rooms.",
            "schema": {
              "maximum": 2147483647,
              "minimum": 0,
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "CreatedAtFrom",
            "in": "query",
            "description": "Earliest signup to include. A plain date is read as UTC.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "CreatedAtTo",
            "in": "query",
            "description": "Latest signup to include.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "Page",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Size",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Skip",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "Sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Order",
            "in": "query",
            "schema": {
              "pattern": "(?i)^(asc|desc)$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserListItem"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a player",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfUserListItem"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}/adjust": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Tops up or debits a balance. Appends a transaction rather than setting a number — there\nis no way to edit a balance, only to move it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The player.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "A signed, non-zero amount.",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/AdjustPointsRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/AdjustPointsRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/AdjustPointsRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfAdjustPointsResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}/password": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Sets a player's password.",
        "description": "The only way back from a forgotten one. There is no self-service reset, and there cannot be a\ngeneral email-based one while email is optional at signup — a player who never gave an address\nhas nowhere for a link to go.\n\nThe old password is not required and not checked: an admin resetting an account by definition\ndoes not have it. That makes this the most dangerous endpoint in the admin API, and the reason\nit takes no old password is worth being explicit about rather than discovering.\n\nAny non-empty password is accepted. Length and complexity rules were dropped for the\nprototype, deliberately.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetPasswordRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetPasswordRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/SetPasswordRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSetPasswordResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}/transactions": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Append-only history. Nothing here can be edited or removed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The player.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "signup_grant, admin_adjustment, entry_fee, refund or payout. Repeated or comma-separated.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Rows per page, default 50, clamped to 1-100.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfWalletTransactionDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}/entries/{entryId}/win": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Settles the room behind one of this player's entries so that the entry takes a prize.",
        "description": "A prototype tool. Every contest in that room is given a result chosen to bring this entry's\nown picks in, and the contests it did not pick are turned against whoever is closest behind\nit. The room then ranks and pays out.\n\nBest effort, not a guarantee. Winning every pick it made is the most a ticket can score, but\na rival holding longer odds on contests this entry left alone can still finish above it — so\nread the `entries` in the response for the rank and prize it actually got. Landing\nsecond or third with a prize is a success; the point is that it is paid, not that it is\nfirst.\n\nContests are shared, so this also finishes them for any other room holding them.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The player. The entry must be theirs.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "entryId",
            "in": "path",
            "description": "Which of their entries to favour, from the entries list.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "resetResults",
            "in": "query",
            "description": "Re-rolls contests that already have results. Off by default: contests are shared, so\nre-rolling one rewrites it for every other room holding it. Without this, a room whose\ncontests are all already settled is refused rather than settled unpaid — there is nothing\nleft to decide.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfSettleResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/users/{id}/entries": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "The rooms this player entered, with the ticket they hold in each.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The player.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Rows per page, default 25, clamped to 1-100.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedApiResponseOfUserEntryDto"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AdjustPointsRequest": {
        "required": [
          "points"
        ],
        "type": "object",
        "properties": {
          "points": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "A signed decimal string: negative debits, positive credits. Must not be zero. A debit\nlarger than the balance is allowed and takes it negative — the server does not refuse it,\nso warn rather than block if that was not intended.",
            "format": "double"
          }
        },
        "description": "Moves points into or out of a player's wallet."
      },
      "AdjustPointsResult": {
        "required": [
          "userId",
          "points",
          "balance"
        ],
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "points": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "balance": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          }
        }
      },
      "ApiResponseOfAdjustPointsResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/AdjustPointsResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfCancelResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/CancelResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfContestMarketsDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ContestMarketsDto"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfFillResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/FillResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfInsights": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Insights"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfIReadOnlyListOfCompetitionDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/CompetitionDto"
            }
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfIReadOnlyListOfContestDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/ContestDto"
            }
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfIReadOnlyListOfLobbyCompetition": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/LobbyCompetition"
            }
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfIReadOnlyListOfPopup": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/Popup"
            }
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfLoginResponse": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/LoginResponse"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfPinResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PinResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfPlayerResponse": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PlayerResponse"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfPlayerRoomDetail": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PlayerRoomDetail"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfPrizePreview": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PrizePreview"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfRankingRow": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RankingRow"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfRefreshMarketsResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RefreshMarketsResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfRemoveResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RemoveResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfRoomAnalytics": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RoomAnalytics"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfRoomDetail": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RoomDetail"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfSetPasswordResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SetPasswordResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfSetStatusResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SetStatusResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfSettleResult": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SettleResult"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfTicket": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Ticket"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfTicketCheck": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/TicketCheck"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfTokenResponse": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/TokenResponse"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfUserListItem": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/UserListItem"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfUserResponse": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/UserResponse"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "ApiResponseOfValidationErrors": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "response": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ValidationErrors"
              }
            ]
          }
        },
        "description": "The SilkRewards response envelope. Every endpoint returns this shape so the\nexisting frontend auth code transfers with only the base URL changed."
      },
      "BlockedSelection": {
        "required": [
          "selectionId",
          "because",
          "reason"
        ],
        "type": "object",
        "properties": {
          "selectionId": {
            "type": "string",
            "format": "uuid"
          },
          "because": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": "string",
            "description": "`same_family` · `contradictory` · `redundant`"
          }
        },
        "description": "Every selection in the room that could not be added to the picks held, and why."
      },
      "CancelRequest": {
        "required": [
          "reason"
        ],
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Required and non-empty. A cancel without one is unexplainable a week later, and this is\nrecorded on the room alongside who cancelled it and the refund figures."
          }
        },
        "description": "Takes a room out of play and refunds everyone. There is no delete."
      },
      "CancelResult": {
        "required": [
          "status",
          "refundsQueued",
          "refundTotal"
        ],
        "type": "object",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/RoomStatus"
          },
          "refundsQueued": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "refundTotal": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          }
        }
      },
      "CompetitionDto": {
        "required": [
          "id",
          "name",
          "sport",
          "externalId",
          "country",
          "contestCount",
          "roomCount"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Local id, set once anything has been imported for this competition. Null means nothing has\nbeen pulled yet — it is <b>not</b> a signal that the competition is unusable. Creating a\nroom does not need it; it exists so the rooms list can be filtered by competition.",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "externalId": {
            "type": "string",
            "description": "The provider's id, or an MMA event slug. Text."
          },
          "country": {
            "type": [
              "null",
              "string"
            ]
          },
          "contestCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Contests on the requested date.",
            "format": "int32"
          },
          "roomCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Rooms already built on this competition. Informational only — a competition is never\nconsumed, so any number here still allows another room. This replaced an isImported flag\nthat read as \"already taken\" and had clients disabling the option.",
            "format": "int32"
          },
          "pricedCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many of `ContestCount` the provider currently quotes odds on — the number that\ndecides whether a room can be built here at all.",
            "format": "int32",
            "default": 0
          },
          "unknownCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many could not be established either way. Football answers for every contest in the\nwindow from one list, so this is normally zero; basketball and MMA have no such list, so it\nis normally all of them. <b>Unknown is not unpriced</b> — a competition reading 0 priced and\n12 unknown has not been judged, while 0 priced and 0 unknown has.\n\nThe second of those is never returned: a competition with contests, all judged and none\npriced, is left out of the list entirely. So a row that arrives is always one a room could\nbe built on, or one nothing is known about yet.",
            "format": "int32",
            "default": 0
          }
        }
      },
      "CompetitionRef": {
        "required": [
          "id",
          "name"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ContestDto": {
        "required": [
          "contestId",
          "sideA",
          "sideB",
          "startsAt",
          "eventName",
          "priced"
        ],
        "type": "object",
        "properties": {
          "contestId": {
            "type": "string",
            "description": "The provider's id. This is what a room is created with."
          },
          "sideA": {
            "type": "string"
          },
          "sideB": {
            "type": "string"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time"
          },
          "eventName": {
            "type": [
              "null",
              "string"
            ],
            "description": "Round, week or weight class, as the provider labels it."
          },
          "priced": {
            "type": [
              "null",
              "boolean"
            ],
            "description": "Whether the provider currently quotes odds on this contest. Null means it could not be\nestablished — never read that as unpriced. Advisory: it can go stale, and a room create\nstill reports what it actually had to drop."
          }
        }
      },
      "ContestMarketsDto": {
        "required": [
          "contestId",
          "externalMatchId",
          "homeTeam",
          "awayTeam",
          "matchDate",
          "bookmaker",
          "fetchedAt",
          "source",
          "view",
          "marketCount",
          "selectionCount",
          "markets"
        ],
        "type": "object",
        "properties": {
          "contestId": {
            "type": "string",
            "format": "uuid"
          },
          "externalMatchId": {
            "type": "string"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "matchDate": {
            "type": "string",
            "format": "date-time"
          },
          "bookmaker": {
            "type": [
              "null",
              "string"
            ]
          },
          "fetchedAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "description": "`frozen` when these are the room's own snapshot, taken at publish and never re-read;\n`live` when the room has no snapshot yet, so these are the contest's current feed prices\nand will move. A draft room is always live, a published one always frozen."
          },
          "view": {
            "type": "string",
            "description": "`full` when every stored selection is present; `extended` when each market carries only\nthe two or three that represent it. Both report the same `MarketCount` and\n`SelectionCount` — those describe the contest, not the payload — so in the extended view a\nmarket's `Selections` is shorter than its own `SelectionCount`, which is how a caller\nknows there is more to show."
          },
          "marketCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Distinct markets — comparable with the room detail's marketCount.",
            "format": "int32"
          },
          "selectionCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Individual priced lines stored for this contest, whichever view was asked for. An order of\nmagnitude larger than the market count, and capped at 200 per contest.",
            "format": "int32"
          },
          "markets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MarketGroupDto"
            }
          }
        },
        "description": "Every market on one contest, grouped rather than flat."
      },
      "EntriesByHour": {
        "required": [
          "hour",
          "entries"
        ],
        "type": "object",
        "properties": {
          "hour": {
            "type": "string",
            "format": "date-time"
          },
          "entries": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      },
      "EntryDto": {
        "required": [
          "id",
          "player",
          "joinedAt",
          "entryFeeCharged",
          "score",
          "maxScore",
          "rank",
          "isTied",
          "prize",
          "status",
          "picks"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "player": {
            "$ref": "#/components/schemas/PlayerRef"
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time"
          },
          "entryFeeCharged": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "isTied": {
            "type": "boolean"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "status": {
            "$ref": "#/components/schemas/EntryStatus"
          },
          "picks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PickDto"
            }
          }
        },
        "description": "One player's ticket in one room. A player can enter a room once, so one entry means one\nset of picks — a 3-pick room gives three items here for one player."
      },
      "EntryStatus": {
        "type": "integer"
      },
      "FillModel": {
        "required": [
          "count"
        ],
        "type": "object",
        "properties": {
          "count": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Clamped to the room's free seats and to 200 a press, so asking for more than fits is not an\nerror — the response says how many actually joined. One joins at once; the rest arrive one a\nsecond (int RoomTrickleJob.SeatEveryMs) so everyone watching sees the room fill.",
            "format": "int32"
          }
        },
        "description": "How many demonstration players to add. Nothing else: the pace is the backend's."
      },
      "FillResult": {
        "required": [
          "roomId",
          "added",
          "requested",
          "participants",
          "seatsLeft",
          "seating"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "added": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Tickets actually placed.",
            "format": "int32"
          },
          "requested": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "What was asked for, before the seat count and the ceiling.",
            "format": "int32"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Confirmed entries in the room afterwards.",
            "format": "int32"
          },
          "seatsLeft": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Seats still free afterwards.",
            "format": "int32"
          },
          "seating": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many more are still to arrive, on a trickle. Zero when everyone is already in. These are not\ncounted in `added` or `participants` — they have not joined yet — and each one arrives\nas its own `roomChanged`.",
            "format": "int32"
          }
        }
      },
      "GameType": {
        "type": "integer",
        "description": "Stored on the room. The number of paying places and the split both come from this,\nso the two can never disagree. See PayoutSplits."
      },
      "GameTypeWire": {
        "enum": [
          "winner_takes_all",
          "top_3"
        ],
        "description": "The admin enum re-exported so the lobby contract does not depend on the admin one."
      },
      "Insights": {
        "required": [
          "locked",
          "unlocksAt",
          "participants",
          "useTopPicks",
          "contests"
        ],
        "type": "object",
        "properties": {
          "locked": {
            "type": "boolean",
            "description": "True while the room can still be joined. Everything else is empty when it is."
          },
          "unlocksAt": {
            "type": [
              "null",
              "string"
            ],
            "description": "The room's kick-off, or null when it holds no contests. The room can unlock earlier by filling\nup — this is the late bound, not a promise.",
            "format": "date-time"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Confirmed entries, the denominator for a selection's share.",
            "format": "int32"
          },
          "useTopPicks": {
            "type": "boolean",
            "description": "Which of the two designed screens this room's data is for — false for PARTICIPANTS PICKS, true\nfor EXPLORE TOP PICKS. <b>This is the one thing to branch on.</b>"
          },
          "contests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InsightsContest"
            }
          }
        },
        "description": "The Insights tab: what the room picked, grouped by contest."
      },
      "InsightsContest": {
        "required": [
          "matchId",
          "selections",
          "participants"
        ],
        "type": "object",
        "properties": {
          "matchId": {
            "type": "string",
            "format": "uuid"
          },
          "selections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InsightsSelection"
            },
            "description": "A row per selection with a head-count — the EXPLORE TOP PICKS screen. Filled when\n`useTopPicks` is true. Most-taken first."
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InsightsParticipant"
            },
            "description": "A row per player showing only their picks on this contest — the PARTICIPANTS PICKS screen.\nFilled when `useTopPicks` is false, best-ranked first."
          }
        },
        "description": "One contest and what was picked on it."
      },
      "InsightsParticipant": {
        "required": [
          "rank",
          "isTied",
          "username",
          "isMe",
          "score",
          "totalX",
          "picks"
        ],
        "type": "object",
        "properties": {
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "isTied": {
            "type": "boolean"
          },
          "username": {
            "type": "string"
          },
          "isMe": {
            "type": "boolean"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "totalX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "picks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TicketPick"
            }
          }
        },
        "description": "One player, and only the picks they made on the contest being asked about."
      },
      "InsightsSelection": {
        "required": [
          "marketName",
          "marketLabel",
          "selection",
          "odds",
          "takers",
          "result",
          "mine"
        ],
        "type": "object",
        "properties": {
          "marketName": {
            "type": "string"
          },
          "marketLabel": {
            "type": [
              "null",
              "string"
            ]
          },
          "selection": {
            "type": "string"
          },
          "odds": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The room's frozen price. Every pick of a selection carries the same one, because the room\nsnapshots its market when it is built and never looks the price up again.",
            "format": "double"
          },
          "takers": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many entries hold it. Against `Participants` for a share.",
            "format": "int32"
          },
          "result": {
            "type": "string",
            "description": "pending · won · lost · cancelled — the same spelling a ticket pick uses."
          },
          "mine": {
            "type": "boolean",
            "description": "On the caller's own ticket."
          }
        },
        "description": "A selection, and how many of the room took it."
      },
      "LobbyCompetition": {
        "required": [
          "id",
          "name",
          "sport",
          "roomCount"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "roomCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Visible rooms in it — what a chip can show as a count.",
            "format": "int32"
          }
        },
        "description": "One competition with rooms a player can currently see."
      },
      "LobbyRoom": {
        "required": [
          "id",
          "title",
          "subtitle",
          "sport",
          "competitionId",
          "competitionName",
          "competitionLogo",
          "startDate",
          "entryFee",
          "gameType",
          "participants",
          "maxParticipants",
          "currentWin",
          "maxWin",
          "currentX",
          "maxX",
          "roomState",
          "playerState",
          "prize",
          "isPinned"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "null",
              "string"
            ]
          },
          "sport": {
            "type": "string"
          },
          "competitionId": {
            "type": "string",
            "format": "uuid"
          },
          "competitionName": {
            "type": "string"
          },
          "competitionLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "startDate": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "gameType": {
            "$ref": "#/components/schemas/GameTypeWire"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "First prize at the players in so far.",
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "First prize if every seat sells.",
            "format": "double"
          },
          "currentX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The prize pool now, as a multiple of the entry fee: players in, less the rake. Four players at\na ten per cent rake is 3.60x whatever the stake and whatever the payout split -- the figure the\nrow shows where it used to show money; the money stays beside it.",
            "format": "double"
          },
          "maxX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same at a full room: seats, less the rake. Ten seats at ten per cent is 9.00x.",
            "format": "double"
          },
          "roomState": {
            "$ref": "#/components/schemas/RoomState"
          },
          "playerState": {
            "$ref": "#/components/schemas/PlayerState"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "What this player won. Null unless PlayerState is won.",
            "format": "double"
          },
          "isPinned": {
            "type": "boolean"
          }
        },
        "description": "One lobby row. Every field here is drawn on the row; nothing else belongs."
      },
      "LoginRequest": {
        "required": [
          "username",
          "password"
        ],
        "type": "object",
        "properties": {
          "username": {
            "type": [
              "null",
              "string"
            ]
          },
          "password": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "LoginResponse": {
        "required": [
          "accessToken"
        ],
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string"
          }
        }
      },
      "MarketGroupDto": {
        "required": [
          "name",
          "label",
          "category",
          "selectionCount",
          "selections"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The provider's market name: \"Match Winner\", \"Goals Over/Under\"."
          },
          "label": {
            "type": [
              "null",
              "string"
            ],
            "description": "What the screen calls it — \"Goals\", \"Cards\". Always present in the extended view, where the list\nis ours and so is the wording. In the full view it is set only where the provider's own name\nneeds a reading (`RTG_H1`, `Home/Away`) and null otherwise — null means show the name."
          },
          "category": {
            "type": "string",
            "description": "Which tab this market belongs under — main, goals, corners, cards, shots, fouls, offsides,\nplayer or other. Derived from the provider's market name, because the provider gives names and\nno grouping of its own."
          },
          "selectionCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Every selection the market has, not how many are returned. In the extended view a row carries\ntwo or three of them, and this is how the screen knows there are more.",
            "format": "int32"
          },
          "selections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SelectionDto"
            }
          }
        },
        "description": "One market and the selections priced under it."
      },
      "MatchStatus": {
        "type": "integer"
      },
      "PagedApiResponseOfEntryDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/EntryDto"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfLobbyRoom": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/LobbyRoom"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfPlayerEntry": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/PlayerEntry"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfRankingRow": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/RankingRow"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfRoomListItem": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/RoomListItem"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfUserEntryDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/UserEntryDto"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfUserListItem": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/UserListItem"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PagedApiResponseOfWalletTransactionDto": {
        "required": [
          "isError",
          "errorMessage",
          "traceId",
          "totalCount",
          "response"
        ],
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "traceId": {
            "type": [
              "null",
              "string"
            ]
          },
          "totalCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "response": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/WalletTransactionDto"
            }
          }
        },
        "description": "The same envelope with `totalCount` beside `response`, which every paginated\nlist carries. The count is after filters but before paging, so the frontend can page."
      },
      "PickDto": {
        "required": [
          "matchId",
          "homeTeam",
          "awayTeam",
          "marketName",
          "selection",
          "odds",
          "result"
        ],
        "type": "object",
        "properties": {
          "matchId": {
            "type": "string",
            "format": "uuid"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "marketName": {
            "type": "string"
          },
          "selection": {
            "type": "string"
          },
          "odds": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "result": {
            "$ref": "#/components/schemas/PickResult"
          }
        }
      },
      "PickResult": {
        "type": "integer"
      },
      "PinResult": {
        "required": [
          "roomId",
          "isPinned",
          "pinned"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "isPinned": {
            "type": "boolean"
          },
          "pinned": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Every pinned room's id, oldest pin first. At most five."
          }
        },
        "description": "Which rooms are pinned, and the room just pinned or unpinned."
      },
      "PlaceTicketModel": {
        "required": [
          "selectionIds"
        ],
        "type": "object",
        "properties": {
          "selectionIds": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "description": "A ticket, as the client sends it: which selections, and nothing else."
      },
      "PlayerEntry": {
        "required": [
          "roomId",
          "title",
          "subtitle",
          "sport",
          "competitionId",
          "competitionName",
          "competitionLogo",
          "startDate",
          "roomState",
          "playerState",
          "entryFee",
          "gameType",
          "participants",
          "maxParticipants",
          "currentWin",
          "maxWin",
          "entryId",
          "placedAt",
          "picks",
          "picksResolved",
          "totalX",
          "score",
          "rank",
          "prize"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "null",
              "string"
            ]
          },
          "sport": {
            "type": "string"
          },
          "competitionId": {
            "type": "string",
            "format": "uuid"
          },
          "competitionName": {
            "type": "string"
          },
          "competitionLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "startDate": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "roomState": {
            "$ref": "#/components/schemas/RoomState"
          },
          "playerState": {
            "$ref": "#/components/schemas/PlayerState"
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "gameType": {
            "$ref": "#/components/schemas/GameTypeWire"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "entryId": {
            "type": "string",
            "description": "This player's entry. What a \"view my ticket\" link is built from.",
            "format": "uuid"
          },
          "placedAt": {
            "type": "string",
            "format": "date-time"
          },
          "picks": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many selections the ticket holds — its length, not its contents.",
            "format": "int32"
          },
          "picksResolved": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many have a result yet. With Picks this is the progress of the ticket:\n\"2 of 3 settled\", and the reason a score can still move.",
            "format": "int32"
          },
          "totalX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Every pick multiplied — what the ticket pays if all of it comes in.",
            "format": "double"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same product over the picks that have <em>won</em>. `0.00` until something does, and the\nnumber a leaderboard ranks on.",
            "format": "double"
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "description": "Where this ticket finished. **Null until the room is settled** — it is written at settlement and\nnot derived, so a live room has no rank to report even when the scores are already meaningful.",
            "format": "int32"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "What this entry won. Null unless it placed.",
            "format": "double"
          }
        },
        "description": "One row of My Tournaments: a room this player entered, and how their ticket is doing in it."
      },
      "PlayerRef": {
        "required": [
          "id",
          "username"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          }
        }
      },
      "PlayerResponse": {
        "required": [
          "id",
          "username",
          "email",
          "balance"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": [
              "null",
              "string"
            ]
          },
          "balance": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          }
        }
      },
      "PlayerRoomDetail": {
        "required": [
          "id",
          "title",
          "subtitle",
          "sport",
          "competitionName",
          "competitionLogo",
          "startDate",
          "roomState",
          "playerState",
          "entryFee",
          "gameType",
          "requiredPicks",
          "betCap",
          "rakePercent",
          "participants",
          "maxParticipants",
          "useTopPicks",
          "currentWin",
          "maxWin",
          "currentX",
          "ifYouJoinX",
          "prizeList",
          "matches",
          "ticket",
          "cancelReason"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "null",
              "string"
            ]
          },
          "sport": {
            "type": "string"
          },
          "competitionName": {
            "type": "string"
          },
          "competitionLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "startDate": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "roomState": {
            "$ref": "#/components/schemas/RoomState"
          },
          "playerState": {
            "$ref": "#/components/schemas/PlayerState"
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "gameType": {
            "$ref": "#/components/schemas/GameTypeWire"
          },
          "requiredPicks": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "betCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "rakePercent": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "useTopPicks": {
            "type": "boolean",
            "description": "Which Insights screen this room has: false for PARTICIPANTS PICKS, true for EXPLORE TOP PICKS.\nIt rides here rather than only on the Insights responses because this is the call the room\nscreen already makes, so the client knows which of the two endpoints to open the tab with\n<b>before</b> it opens it. See InsightsRules."
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "currentX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What a buy-in turns into if the room ended now — first prize divided by the entry fee. The\nfigure the screen labels CURRENT X.",
            "format": "double"
          },
          "ifYouJoinX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "The same figure with one more player in, so someone deciding can see what their own entry does\nto it. Null once they are in, or once the room is full — there is no next player.",
            "format": "double"
          },
          "prizeList": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PrizeListRow"
            }
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoomContest"
            }
          },
          "ticket": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "Null until this player has placed one.",
                "$ref": "#/components/schemas/Ticket"
              }
            ]
          },
          "cancelReason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Why the room was called off, and null in every other state. A player who paid into a room that\nnever played is owed the reason, not just the word CANCELLED — and the reason names the actual\ncause, whether that is an abandoned fixture, too few players at kick-off, or an operator\nwithdrawing the room by hand."
          }
        },
        "description": "Everything the room screen shows: the terms, the contests, and the ticket this player holds."
      },
      "PlayerState": {
        "type": "integer",
        "description": "What this player has to do with a room."
      },
      "Popup": {
        "required": [
          "roomId",
          "roomTitle",
          "competitionLogo",
          "rank",
          "isTied",
          "participants",
          "prize",
          "settledAt"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "description": "Where \"Go to room\" goes.",
            "format": "uuid"
          },
          "roomTitle": {
            "type": "string"
          },
          "competitionLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Where they finished. 1 is first.",
            "format": "int32"
          },
          "isTied": {
            "type": "boolean",
            "description": "They shared this place, so the prize is a split of the places tied for."
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Confirmed entries, so the screen can say \"2nd of 47\".",
            "format": "int32"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What they won, as a decimal string like every other money field.",
            "format": "double"
          },
          "settledAt": {
            "type": "string",
            "description": "When the room paid out. The queue is ordered by this, oldest first.",
            "format": "date-time"
          }
        },
        "description": "A win this player has not been shown yet."
      },
      "PrizeListRow": {
        "required": [
          "place",
          "percent",
          "amount",
          "maxAmount",
          "clearsEntryAt"
        ],
        "type": "object",
        "properties": {
          "place": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "1 for first, 2 for second, and so on.",
            "format": "int32"
          },
          "percent": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "That place's share of the pool, as a percentage.",
            "format": "double"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What this place takes at the players in so far — the CURRENT figure.",
            "format": "double"
          },
          "maxAmount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What it would take if every seat sold — the MAX figure beside it.",
            "format": "double"
          },
          "clearsEntryAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "description": "How many players have to join before this place is worth at least the entry fee — the figure the\nroom builder warns on. Null when the room can never get there, which happens when the place's\nshare is so small that even a full room pays it less than it cost to enter.\n\nOn 70 / 20 / 10 the lower places are small: a 2.00 entry with a 7% rake needs eleven players\nbefore third place beats the entry, and six before second does. That is a real property of the\nsplit rather than a fault, but an operator setting a ten-seat room should be told before they\npublish it, not after somebody finishes third and is out of pocket.",
            "format": "int32"
          }
        },
        "description": "One paying place and what it would take at the room's current pool. Not a list of winners —\nit describes the split before anyone has won anything, and every row is present from the\nmoment the room is created."
      },
      "PrizePreview": {
        "required": [
          "minimumPlayers",
          "prizePool",
          "houseCut",
          "prizeList"
        ],
        "type": "object",
        "properties": {
          "minimumPlayers": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The fewest players this room can be played with. Below it the room is cancelled at kick-off and\neveryone refunded in full: one player alone is not a contest, and a room paying three places\ncannot pay three places to two people.",
            "format": "int32"
          },
          "prizePool": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The pool at a full room, after the house cut.",
            "format": "double"
          },
          "houseCut": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What the house keeps at a full room.",
            "format": "double"
          },
          "prizeList": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PrizeListRow"
            }
          }
        },
        "description": "What a room would pay, for terms that have not been saved yet."
      },
      "RankingRow": {
        "required": [
          "rank",
          "isTied",
          "username",
          "isMe",
          "score",
          "totalX",
          "wouldTake",
          "prize",
          "picks"
        ],
        "type": "object",
        "properties": {
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "1-based, competition ranking — tied players share a rank and the next one skips: 1, 2, 2, 4.\nLive, recomputed on every request, and the same number settlement will eventually write down.",
            "format": "int32"
          },
          "isTied": {
            "type": "boolean",
            "description": "Someone else holds this exact rank."
          },
          "username": {
            "type": "string",
            "description": "Masked to an initial for everyone but the caller — `n***`. The caller's own row carries\ntheir real name, which they already know."
          },
          "isMe": {
            "type": "boolean",
            "description": "The caller's row. **Pin it by this, never by matching the username** — every other name is\nmasked, and two players can mask to the same thing."
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The product of this ticket's winning picks. What the rank is built on.",
            "format": "double"
          },
          "totalX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What the ticket still could reach — every pick that has not been cancelled, multiplied. The\nfigure the design labels `Total 13.25x`.",
            "format": "double"
          },
          "wouldTake": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "What this row takes if the room ended now — the design's \"+$235.00 if it holds\". Null for a row\noutside the prizes. Tied rows split their combined shares, exactly as a settlement would pay\nthem: two players tied for first take first and second added together and halved.",
            "format": "double"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "What was actually paid. Null until the room settles, and the number to show instead of\nWouldTake once it is there.",
            "format": "double"
          },
          "picks": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/TicketPick"
            },
            "description": "The player's selections, for the expanded row. **Null while the room is still open** — a late\nentrant could otherwise copy the leader's ticket. Non-null for the caller's own row throughout,\nsince they can already see their own."
          }
        },
        "description": "One row of the Ranking tab: where a player stands, and what they stand to take."
      },
      "RefreshMarketsResult": {
        "required": [
          "roomId",
          "contests",
          "reached",
          "linesFetched",
          "linesAdded"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "contests": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Contests still to kick off, whose odds were asked for again.",
            "format": "int32"
          },
          "reached": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Of those, how many the provider answered for.",
            "format": "int32"
          },
          "linesFetched": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Selections the provider had that the contests did not.",
            "format": "int32"
          },
          "linesAdded": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Selections added to this room's offer — usually the same number, less anything already frozen\nhere from an earlier refresh of a shared contest.",
            "format": "int32"
          }
        },
        "description": "What a market refresh did to a published room."
      },
      "RegisterRequest": {
        "required": [
          "username",
          "email",
          "password"
        ],
        "type": "object",
        "properties": {
          "username": {
            "type": [
              "null",
              "string"
            ]
          },
          "email": {
            "type": [
              "null",
              "string"
            ],
            "description": "Optional. Validated only when given."
          },
          "password": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "RemoveResult": {
        "required": [
          "roomId",
          "entriesDeleted",
          "walletRowsDeleted",
          "pointsReversed",
          "contestsCleared",
          "roomsUnsettled",
          "contestsDeleted"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "entriesDeleted": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Tickets that went with the room.",
            "format": "int32"
          },
          "walletRowsDeleted": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Money rows erased, the reversed payouts included.",
            "format": "int32"
          },
          "pointsReversed": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Prize money taken back out of balances.",
            "format": "double"
          },
          "contestsCleared": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Contests wound back to unplayed but kept, because another room still holds them. They can be\nsettled again; they are not new.",
            "format": "int32"
          },
          "roomsUnsettled": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Other rooms whose settlement was undone with them.",
            "format": "int32"
          },
          "contestsDeleted": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Contests removed outright, odds and all, because no room holds them any more. Importing one of\nthese again is a first import — a fresh row, priced from the feed.",
            "format": "int32"
          }
        },
        "description": "What a removal destroyed. Prototype tool — see `RoomRemoval`."
      },
      "RoomAnalytics": {
        "required": [
          "roomId",
          "name",
          "status",
          "participants",
          "maxParticipants",
          "fillRate",
          "entryFee",
          "rakePercent",
          "prizePool",
          "houseCut",
          "currentWin",
          "maxWin",
          "pointsStaked",
          "pointsPaidOut",
          "currentLeader",
          "leaderScore",
          "matchesTotal",
          "matchesSettled",
          "entriesByHour"
        ],
        "type": "object",
        "properties": {
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/RoomStatus"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "fillRate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "rakePercent": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "prizePool": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "houseCut": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "pointsStaked": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "pointsPaidOut": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "currentLeader": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PlayerRef"
              }
            ]
          },
          "leaderScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "matchesTotal": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "matchesSettled": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "entriesByHour": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntriesByHour"
            }
          }
        },
        "description": "Room figures an admin reads rather than sets. All computed — nothing here is stored."
      },
      "RoomContest": {
        "required": [
          "id",
          "homeTeam",
          "awayTeam",
          "homeTeamLogo",
          "awayTeamLogo",
          "matchDate",
          "status",
          "homeScore",
          "awayScore",
          "source",
          "marketCount",
          "selectionCount",
          "markets"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "homeTeamLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "awayTeamLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "matchDate": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "homeScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "awayScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "source": {
            "type": "string",
            "description": "`frozen` when these are the room's own snapshot, taken at publish and never re-read — which\nis what a published room always shows, and the reason the price on the button is the price the\npick will carry. `live` only for a contest the room holds no snapshot for."
          },
          "marketCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Distinct markets in <em>this room's</em> lines, not in whatever the feed now carries. What the\n\"view all N markets\" label counts.",
            "format": "int32"
          },
          "selectionCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Every priced line on the contest — an order of magnitude larger.",
            "format": "int32"
          },
          "markets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MarketGroupDto"
            },
            "description": "The short list: this sport's fixed rows, in its fixed order, each trimmed to the two or three\nselections that stand for it. The first row is the one the card shows collapsed; the rest are\nwhat \"Extend Markets\" reveals. Each market reports its own true `selectionCount`, so a row\ncan say \"2 of 14\" without asking.\n\nEmpty when the contest is unpriced. Every selection carries the id a ticket is built from."
          },
          "minute": {
            "type": [
              "null",
              "string"
            ]
          },
          "elapsed": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "extra": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          }
        },
        "description": "One contest in the room, with its short market list already priced."
      },
      "RoomCreateModel": {
        "required": [
          "title",
          "sport",
          "contestIds"
        ],
        "type": "object",
        "properties": {
          "title": {
            "maxLength": 255,
            "minLength": 1,
            "type": [
              "null",
              "string"
            ],
            "description": "Shown as the room's title everywhere, for example \"Premier League Round 18\". Editable at\nany time, even once players have entered."
          },
          "subtitle": {
            "maxLength": 255,
            "minLength": 0,
            "type": [
              "null",
              "string"
            ],
            "description": "The narrower part, such as \"Round 12\". Optional, shown under the title."
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": [
              "null",
              "string"
            ],
            "description": "One or two lines shown to players under the title. Editable at any time."
          },
          "sport": {
            "description": "`football`, `basketball`, `mma` or `tennis`. One room covers one\nsport — never a mix. Must match the sport of `competitionId`, or the request is\nrejected on this field.",
            "$ref": "#/components/schemas/Sport"
          },
          "entryFee": {
            "maximum": 1000000,
            "minimum": 0.01,
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "What one player pays to join, as a decimal string such as \"50.00\". Must be above zero.",
            "format": "double"
          },
          "rakePercent": {
            "maximum": 100,
            "minimum": 0,
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The house cut as a percentage, taken off the pool before it is split. Between 0 and 100.",
            "format": "double"
          },
          "maxParticipants": {
            "maximum": 100000,
            "minimum": 2,
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Seats. The room locks when they fill. At least two — a one-seat room would always hit the\nautomatic refund for having too few players.",
            "format": "int32"
          },
          "requiredPicks": {
            "maximum": 1000,
            "minimum": 1,
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Exactly how many selections a ticket must contain, not a minimum.",
            "format": "int32"
          },
          "gameType": {
            "description": "Sets how many places pay and how the pool splits, both from constants in the backend:\n`winner_takes_all` pays one place at 100%, `top_3` pays three at 50/30/20. The\nplaces it pays cannot exceed the seats.",
            "$ref": "#/components/schemas/GameType"
          },
          "betCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Optional ceiling on a single selection's odds, such as \"3.00\". Must be above 1.00 when it\nis sent at all.",
            "format": "double"
          },
          "isPublished": {
            "type": "boolean",
            "description": "Publish immediately. Honoured only if the publish checks also pass, so this cannot be\nused to get a published room without satisfying them. Afterwards, publishing and\nunpublishing are `POST /api/admin/rooms/setStatus` only."
          },
          "contestIds": {
            "minItems": 1,
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "The contests this room is played over, as the <b>provider's own ids</b> from\n`GET /api/admin/matches` — not local ids, which do not exist until this call."
          }
        },
        "description": "Creates a room. A room belongs to exactly one sport and one competition within it, and\nneither can change afterwards — pick them here or not at all."
      },
      "RoomDetail": {
        "required": [
          "id",
          "title",
          "subtitle",
          "description",
          "competitionId",
          "entryFee",
          "rakePercent",
          "maxParticipants",
          "requiredPicks",
          "gameType",
          "betCap",
          "isPublished",
          "contestIds",
          "sport",
          "competition",
          "status",
          "statusDetail",
          "startDate",
          "participants",
          "prizePool",
          "currentWin",
          "maxWin",
          "isSettled",
          "matches",
          "prizeList",
          "cancelReason",
          "cancelledAt",
          "droppedContests"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "null",
              "string"
            ]
          },
          "description": {
            "type": [
              "null",
              "string"
            ]
          },
          "competitionId": {
            "type": "string",
            "format": "uuid"
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "rakePercent": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "requiredPicks": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "gameType": {
            "$ref": "#/components/schemas/GameType"
          },
          "betCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "isPublished": {
            "type": "boolean"
          },
          "contestIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The provider's own ids for this room's contests — the same values an update sends back, so the\nform round-trips without translating between id spaces."
          },
          "sport": {
            "type": "string"
          },
          "competition": {
            "$ref": "#/components/schemas/CompetitionRef"
          },
          "status": {
            "$ref": "#/components/schemas/RoomStatus"
          },
          "statusDetail": {
            "type": [
              "null",
              "string"
            ]
          },
          "startDate": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "prizePool": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "isSettled": {
            "type": "boolean"
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoomMatchDto"
            }
          },
          "prizeList": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PrizeListRow"
            }
          },
          "cancelReason": {
            "type": [
              "null",
              "string"
            ]
          },
          "cancelledAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "droppedContests": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contests asked for but left out because the provider had no odds for them. Populated on create\nand on update; empty when reading a room back, since by then the room holds only what it kept."
          }
        },
        "description": "The full room: the write model's fields first, so the form can bind them, then the\nderived ones. Sending the derived block back is harmless — the server ignores it."
      },
      "RoomListItem": {
        "required": [
          "id",
          "title",
          "subtitle",
          "sport",
          "competitionName",
          "entryFee",
          "participants",
          "maxParticipants",
          "currentWin",
          "maxWin",
          "gameType",
          "status",
          "statusDetail",
          "startDate",
          "isPublished",
          "isSettled",
          "isPinned"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "null",
              "string"
            ]
          },
          "sport": {
            "type": "string"
          },
          "competitionName": {
            "type": "string",
            "description": "The competition's name, without its id. The list only ever displays it — filtering by\ncompetition uses the id from the competitions lookup, not one echoed back on every row."
          },
          "entryFee": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "participants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "maxParticipants": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "currentWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxWin": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "gameType": {
            "$ref": "#/components/schemas/GameType"
          },
          "status": {
            "$ref": "#/components/schemas/RoomStatus"
          },
          "statusDetail": {
            "type": [
              "null",
              "string"
            ]
          },
          "startDate": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "isPublished": {
            "type": "boolean"
          },
          "isSettled": {
            "type": "boolean"
          },
          "isPinned": {
            "type": "boolean"
          }
        },
        "description": "One row in the rooms list. Everything after `maxParticipants` is derived."
      },
      "RoomMatchDto": {
        "required": [
          "id",
          "externalMatchId",
          "homeTeam",
          "awayTeam",
          "matchDate",
          "status",
          "homeScore",
          "awayScore",
          "marketCount",
          "selectionCount"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "externalMatchId": {
            "type": "string"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "matchDate": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "homeScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "awayScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "marketCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Distinct markets <em>this room</em> offers on this contest — its frozen snapshot once published,\nand the contest's live prices before that. The same figure the market screen reports, so opening\na contest cannot contradict the row that led there.",
            "format": "int32"
          },
          "selectionCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Individual selections across those markets, an order of magnitude larger: one\n\"Goals Over/Under\" market alone carries dozens of lines.",
            "format": "int32"
          }
        },
        "description": "One match inside a room, expanded so the screen can show teams without a second call."
      },
      "RoomState": {
        "type": "integer",
        "description": "Where a room is in its life, as a player sees it."
      },
      "RoomStatus": {
        "type": "integer",
        "description": "Derived from entries, start date and settlement — never stored. A room with\n`IsPublished = false` sitting at RoomStatus.Ongoing is a draft."
      },
      "RoomUpdateModel": {
        "required": [
          "id",
          "title",
          "contestIds"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The room to update.",
            "format": "uuid"
          },
          "title": {
            "maxLength": 255,
            "minLength": 1,
            "type": [
              "null",
              "string"
            ],
            "description": "Editable at any time, even once players have entered."
          },
          "subtitle": {
            "maxLength": 255,
            "minLength": 0,
            "type": [
              "null",
              "string"
            ],
            "description": "The narrower part, such as \"Round 12\". Optional, shown under the title."
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": [
              "null",
              "string"
            ],
            "description": "Editable at any time."
          },
          "entryFee": {
            "maximum": 1000000,
            "minimum": 0.01,
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Frozen once one confirmed entry exists.",
            "format": "double"
          },
          "rakePercent": {
            "maximum": 100,
            "minimum": 0,
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Frozen once one confirmed entry exists.",
            "format": "double"
          },
          "maxParticipants": {
            "maximum": 100000,
            "minimum": 2,
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Frozen once one confirmed entry exists.",
            "format": "int32"
          },
          "requiredPicks": {
            "maximum": 1000,
            "minimum": 1,
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Frozen once one confirmed entry exists.",
            "format": "int32"
          },
          "gameType": {
            "description": "Frozen once one confirmed entry exists.",
            "$ref": "#/components/schemas/GameType"
          },
          "betCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Frozen once one confirmed entry exists.",
            "format": "double"
          },
          "contestIds": {
            "minItems": 1,
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "The contests this room is played over, as the provider's own ids — the one structural\nthing an update may change, and only while nobody has entered. They must still resolve to\nthe room's own competition, which this call cannot change. Replacing the selection discards\nany odds snapshot, since it no longer describes the room."
          }
        },
        "description": "Updates a room. Carries no `sport`, `competitionId` or `isPublished`: the first\ntwo are fixed at creation, and publishing is `POST /api/admin/rooms/setStatus`."
      },
      "SelectionDto": {
        "required": [
          "id",
          "label",
          "odds",
          "sortOrder"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The stored row's own key — a room's frozen copy, or a contest's live line where no room is in\nplay. This is what a ticket is built from: one id names the match, the market, the selection and\nthe price at once, so a pick can never reference a price the room did not offer.",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "description": "The provider's own value: \"Home\", \"Over 2.5\", \"1 To Score Exactly 2 Goals\"."
          },
          "odds": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Decimal odds as a string. This is the selection's multiplier — the \"x\" the player app renders as\n`1.85x` — and what a winning pick multiplies into the ticket score. There is no separate x\nfield because there is no separate number.",
            "format": "double"
          },
          "sortOrder": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The provider's own order of importance, preserved.",
            "format": "int32"
          },
          "family": {
            "type": [
              "null",
              "string"
            ],
            "description": "The question this selection answers, without its line — `FT.match.result`,\n`FT.match.goals`. Two selections on one contest with the same family are the same question\nasked twice, and a ticket holds one of them: the screen can grey the rest the moment one is\npicked, with nothing more than a string compare. Null on rows written before keys existed."
          }
        },
        "description": "One priced selection — the thing a player actually picks."
      },
      "SetPasswordRequest": {
        "type": "object",
        "properties": {
          "password": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "description": "A new password for a player, set by an admin."
      },
      "SetPasswordResult": {
        "required": [
          "userId",
          "username"
        ],
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          }
        },
        "description": "Who was reset. Deliberately carries nothing about the password itself."
      },
      "SetStatusRequest": {
        "required": [
          "id",
          "isPublished"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The room to switch.",
            "format": "uuid"
          },
          "isPublished": {
            "type": "boolean",
            "description": "True runs the publish checks first: at least one match, picks required not exceeding the\nmatches, every match priced by the feed, the earliest kick-off still in the future, and the\nplaces paid not exceeding the seats. Any failure is a 400 keyed to the field.\nFalse runs no checks but is refused with 409 once anyone has entered, because hiding a room\nsomebody paid into would take away their only way of seeing their own ticket."
          }
        },
        "description": "Turns a room's visibility on or off. SilkRewards style — the action is named and the id\ntravels in the body rather than the path."
      },
      "SetStatusResult": {
        "required": [
          "id",
          "isPublished"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "isPublished": {
            "type": "boolean"
          }
        }
      },
      "SettledContest": {
        "required": [
          "matchId",
          "homeTeam",
          "awayTeam",
          "homeScore",
          "awayScore",
          "picksWon",
          "picksLost",
          "guessed"
        ],
        "type": "object",
        "properties": {
          "matchId": {
            "type": "string",
            "format": "uuid"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "homeScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Points in basketball, goals in football. For MMA it is 1 for the winner and 0 for the loser:\na fight has a winner, not a score, and the screen should read it as one.",
            "format": "int32"
          },
          "awayScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "picksWon": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "picksLost": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "guessed": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Selections no result could decide — exact score, player props, anything scoped to a quarter or a\nhalf — settled by coin flip and counted here so the guess is never silent.",
            "format": "int32"
          }
        }
      },
      "SettledEntry": {
        "required": [
          "entryId",
          "userId",
          "username",
          "score",
          "rank",
          "isTied",
          "prize"
        ],
        "type": "object",
        "properties": {
          "entryId": {
            "type": "string",
            "format": "uuid"
          },
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Decimal, so the money converter renders it the same way every other figure on the wire is\nrendered. Formatting it here produced a comma on a machine whose locale uses one.",
            "format": "double"
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "isTied": {
            "type": "boolean"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          }
        }
      },
      "SettleResult": {
        "required": [
          "settled",
          "alreadyHadResults",
          "roomSettled",
          "entries"
        ],
        "type": "object",
        "properties": {
          "settled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettledContest"
            },
            "description": "Contests given a result by this call."
          },
          "alreadyHadResults": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Contests skipped because they were already finished. A second call never re-rolls a result —\nthat is what makes \"random the rest\" repeatable after \"random these few\"."
          },
          "roomSettled": {
            "type": "boolean",
            "description": "True when every contest now has a result and the room was ranked and paid out."
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettledEntry"
            }
          },
          "roomCancelled": {
            "type": "boolean",
            "description": "True when the last result finished the room and it was <b>refunded rather than paid</b>, because\nit had fewer players than it has paying places.",
            "default": false
          },
          "cancelReason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Why, when it was. Null otherwise."
          }
        },
        "description": "What a settle call did, so the caller can show it rather than re-query."
      },
      "SettleSelectedModel": {
        "required": [
          "matchIds"
        ],
        "type": "object",
        "properties": {
          "matchIds": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Local contest ids, from a room detail's matches[].id."
          }
        }
      },
      "Sport": {
        "type": "integer",
        "description": "Fixed list from the admin overview. Only football is reachable today — the feed\nis API-Football — but the others are named so a competition row can carry them."
      },
      "Ticket": {
        "required": [
          "id",
          "placedAt",
          "entryFeeCharged",
          "totalX",
          "score",
          "rank",
          "prize",
          "picks"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "placedAt": {
            "type": "string",
            "format": "date-time"
          },
          "entryFeeCharged": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "totalX": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Every pick that can still count, multiplied — what the ticket pays if it all comes in. A\ncancelled pick is not in it, whether its contest was called off or its line landed exactly on\nthe result (a push): neither can be part of a ceiling the ticket might still reach.",
            "format": "double"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same product over the picks that have <em>won</em>. Zero until something does, and the number\na leaderboard ranks on.",
            "format": "double"
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "description": "Where this ticket currently stands — live while the room plays, and the settled position once it\nis over. Null only when the room holds no entries to rank against.",
            "format": "int32"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "picks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TicketPick"
            }
          }
        }
      },
      "TicketCheck": {
        "required": [
          "blocked"
        ],
        "type": "object",
        "properties": {
          "blocked": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlockedSelection"
            }
          }
        }
      },
      "TicketCheckModel": {
        "required": [
          "selectionIds"
        ],
        "type": "object",
        "properties": {
          "selectionIds": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "description": "The picks a player currently holds while building a ticket."
      },
      "TicketPick": {
        "required": [
          "matchId",
          "homeTeam",
          "awayTeam",
          "homeTeamLogo",
          "awayTeamLogo",
          "matchDate",
          "marketName",
          "marketLabel",
          "selection",
          "odds",
          "result"
        ],
        "type": "object",
        "properties": {
          "matchId": {
            "type": "string",
            "format": "uuid"
          },
          "homeTeam": {
            "type": "string"
          },
          "awayTeam": {
            "type": "string"
          },
          "homeTeamLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "awayTeamLogo": {
            "type": [
              "null",
              "string"
            ]
          },
          "matchDate": {
            "type": "string",
            "format": "date-time"
          },
          "marketName": {
            "type": "string"
          },
          "marketLabel": {
            "type": [
              "null",
              "string"
            ]
          },
          "selection": {
            "type": "string"
          },
          "odds": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Frozen when the pick was made and never looked up again.",
            "format": "double"
          },
          "result": {
            "type": "string"
          }
        }
      },
      "TokenResponse": {
        "required": [
          "accessToken"
        ],
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string"
          }
        }
      },
      "UserEntryDto": {
        "required": [
          "entryId",
          "roomId",
          "roomName",
          "roomStatus",
          "entryFeeCharged",
          "joinedAt",
          "score",
          "maxScore",
          "rank",
          "prize",
          "picks"
        ],
        "type": "object",
        "properties": {
          "entryId": {
            "type": "string",
            "format": "uuid"
          },
          "roomId": {
            "type": "string",
            "format": "uuid"
          },
          "roomName": {
            "type": "string"
          },
          "roomStatus": {
            "$ref": "#/components/schemas/RoomStatus"
          },
          "entryFeeCharged": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time"
          },
          "score": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "maxScore": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "rank": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "prize": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "picks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PickDto"
            }
          }
        },
        "description": "A room this player entered, from the player's side. Carries no entry status: settlement is\nnot built yet, so every entry that exists today is confirmed and applied."
      },
      "UserListItem": {
        "required": [
          "id",
          "username",
          "email",
          "balance",
          "entryCount",
          "createdAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": [
              "null",
              "string"
            ]
          },
          "balance": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The sum of this player's wallet transactions, computed on read. Never a stored field, so it\ncannot drift from the rows that explain it — and never settable.",
            "format": "double"
          },
          "entryCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Confirmed entries across all rooms.",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "One player as the admin list and detail endpoint both return them."
      },
      "UserResponse": {
        "required": [
          "id",
          "email"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        }
      },
      "ValidationErrors": {
        "required": [
          "errors"
        ],
        "type": "object",
        "properties": {
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "description": "Field-keyed validation failures, so each message lands on its own input rather than\nin a single banner. Returned as 400; a state conflict is a 409 instead."
      },
      "WalletTransactionDto": {
        "required": [
          "id",
          "type",
          "points",
          "roomId",
          "roomName",
          "createdAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "$ref": "#/components/schemas/WalletTransactionType"
          },
          "points": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "roomId": {
            "type": [
              "null",
              "string"
            ],
            "description": "Set when the movement came from a room — an entry fee, a refund or a payout. Null for a\nsignup grant or an admin adjustment.",
            "format": "uuid"
          },
          "roomName": {
            "type": [
              "null",
              "string"
            ],
            "description": "The room's title, so the history reads without a second call per row. Null exactly when\nRoomId is."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "One movement in a player's wallet."
      },
      "WalletTransactionType": {
        "type": "integer"
      }
    }
  },
  "tags": [
    {
      "name": "Auth"
    },
    {
      "name": "Identity"
    },
    {
      "name": "Insights"
    },
    {
      "name": "Lobby"
    },
    {
      "name": "Lookups"
    },
    {
      "name": "PlayerEntries"
    },
    {
      "name": "PlayerMarkets"
    },
    {
      "name": "Popups"
    },
    {
      "name": "Ranking"
    },
    {
      "name": "RoomEntries"
    },
    {
      "name": "RoomMarkets"
    },
    {
      "name": "RoomResults"
    },
    {
      "name": "Rooms"
    },
    {
      "name": "Users"
    }
  ]
}