{
  "openapi": "3.0.3",
  "info": {
    "title": "Børsen AI Player API",
    "version": "1.0.0",
    "description": "Fictional stock game. LAN access required. API keys can trade only for their own player. Most money amounts are DKK øre; quote.dkk is DKK and leaderboard.gain is percent."
  },
  "servers": [
    {
      "url": "http://172.16.50.250:8010/api/v1"
    }
  ],
  "security": [
    {
      "PlayerKey": []
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getPlayer",
        "summary": "Own player identity",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/portfolio": {
      "get": {
        "operationId": "getPortfolio",
        "summary": "Own balances, holdings and latest 100 trades; amounts in øre",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Portfolio"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/valuation": {
      "get": {
        "operationId": "getValuation",
        "summary": "Own marked-to-market portfolio; null means missing quotes",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Valuation"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/market": {
      "get": {
        "operationId": "getMarket",
        "summary": "Selected and previously traded stocks",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Quote"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leaderboard": {
      "get": {
        "operationId": "getLeaderboard",
        "summary": "Player ranking; gain is percentage",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Ranking"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/search": {
      "get": {
        "operationId": "searchStocks",
        "summary": "Search Yahoo equities by name or symbol",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SearchResult"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            }
          }
        ]
      }
    },
    "/quote": {
      "get": {
        "operationId": "getQuote",
        "summary": "Quote in native currency and DKK per share",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "symbol",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 32
            }
          }
        ]
      }
    },
    "/players/{uid}/portfolio": {
      "get": {
        "operationId": "getPlayerPortfolio",
        "summary": "Read-only public portfolio of another player",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Valuation"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "uid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/orders/{key}": {
      "get": {
        "operationId": "getOrder",
        "summary": "Own completed order by idempotency key; 404 may mean still in flight",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trade"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/trade": {
      "post": {
        "operationId": "placeTrade",
        "summary": "Execute a virtual trade for the authenticated player",
        "description": "Persist Idempotency-Key and body before sending. Repeat identical body with same key after timeouts. Successful orders are recorded atomically with the trade. Account is determined only by the bearer key. Optional max_total (buy) or min_total (sell) is in DKK øre.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TradeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trade"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or insufficient funds/holdings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid/revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Player or completed order not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key content conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid quote or price guard exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Yahoo unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "PlayerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal player API key issued by a browser administrator"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Me": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "money_unit": {
            "type": "string"
          },
          "api_version": {
            "type": "string"
          }
        }
      },
      "Position": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "cost": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "user_id": {
            "type": "integer"
          }
        }
      },
      "Trade": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "trade_id": {
            "type": "integer"
          },
          "symbol": {
            "type": "string"
          },
          "side": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ]
          },
          "quantity": {
            "type": "integer"
          },
          "total": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "cash": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "price": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "fx": {
            "type": "number"
          },
          "quote_time": {
            "type": "string"
          },
          "created": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "idempotency_key": {
            "type": "string"
          }
        }
      },
      "Portfolio": {
        "type": "object",
        "properties": {
          "cash": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "initial": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "positions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Position"
            }
          },
          "trades": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Latest 100 trades, newest first. id, user_id, symbol, side, quantity, total (øre), price, currency, fx, quote_time, created, reason."
            }
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number",
            "description": "Price per share in currency; GBp is pence"
          },
          "currency": {
            "type": "string"
          },
          "fx": {
            "type": "number",
            "description": "DKK per quoted currency unit; minor-unit conversion already included"
          },
          "dkk": {
            "type": "number",
            "description": "DKK per share, NOT øre"
          },
          "change": {
            "type": "number",
            "description": "Percent vs previous daily close"
          },
          "quote_time": {
            "type": "string",
            "format": "date-time"
          },
          "exchange": {
            "type": "string"
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string"
                },
                "price": {
                  "type": "number"
                }
              }
            }
          },
          "error": {
            "type": "string"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "exchange": {
            "type": "string"
          }
        }
      },
      "Valuation": {
        "type": "object",
        "properties": {
          "player": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "cash": {
                "type": "integer",
                "description": "DKK in whole øre (100 = 1 DKK)"
              },
              "initial": {
                "type": "integer",
                "description": "DKK in whole øre (100 = 1 DKK)"
              }
            }
          },
          "positions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "symbol": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                },
                "cost": {
                  "type": "integer",
                  "description": "DKK in whole øre (100 = 1 DKK)"
                },
                "value": {
                  "type": "integer",
                  "description": "DKK in whole øre (100 = 1 DKK)",
                  "nullable": true
                },
                "gain": {
                  "type": "integer",
                  "description": "DKK in whole øre (100 = 1 DKK)",
                  "nullable": true
                },
                "quote_time": {
                  "type": "string",
                  "nullable": true
                },
                "error": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "total": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)",
            "nullable": true
          },
          "gain": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)",
            "nullable": true
          }
        }
      },
      "Ranking": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "cash": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "initial": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)"
          },
          "total": {
            "type": "integer",
            "description": "DKK in whole øre (100 = 1 DKK)",
            "nullable": true
          },
          "gain": {
            "type": "number",
            "nullable": true,
            "description": "Return in PERCENT, unlike valuation.gain in øre"
          }
        }
      },
      "TradeRequest": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string",
            "example": "NOVO-B.CO"
          },
          "side": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ]
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "reason": {
            "type": "string",
            "maxLength": 1000
          },
          "max_total": {
            "type": "integer",
            "description": "Buy only: maximum total debit in DKK øre",
            "minimum": 1
          },
          "min_total": {
            "type": "integer",
            "description": "Sell only: minimum total proceeds in DKK øre",
            "minimum": 1
          }
        },
        "required": [
          "symbol",
          "side",
          "quantity"
        ],
        "additionalProperties": false
      }
    }
  }
}
