{
  "openapi": "3.1.0",
  "info": {
    "title": "Investment Bets API",
    "version": "1.0.0",
    "description": "API used by the Investment Bets web app. Authenticated operations use an HttpOnly JWT cookie named auth_token."
  },
  "servers": [
    {
      "url": "https://api.investment-bets.com",
      "description": "Production API"
    }
  ],
  "security": [
    {
      "cookieAuth": []
    }
  ],
  "paths": {
    "/": {
      "get": {
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "description": "API health response"
          }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "summary": "Discover API descriptions and service documentation",
        "security": [],
        "responses": {
          "200": {
            "description": "RFC 9727 linkset for the Investment Bets API",
            "content": {
              "application/linkset+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCatalog"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "summary": "Describe OAuth support for the API resource",
        "description": "Machine-readable auth metadata for agents. Investment Bets currently advertises no OAuth authorization servers, scopes, bearer-token methods, dynamic client registration, or machine-to-machine credentials.",
        "security": [],
        "responses": {
          "200": {
            "description": "OAuth Protected Resource metadata with empty OAuth capability lists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthProtectedResourceMetadata"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "Return the OpenAPI schema",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 schema for the Investment Bets API",
            "content": {
              "application/openapi+json": {
                "schema": {
                  "type": "object",
                  "description": "OpenAPI 3.1 document for the Investment Bets API."
                }
              }
            }
          }
        }
      }
    },
    "/register": {
      "post": {
        "summary": "Create an account",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created. No session cookie is set; call /login to authenticate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "description": "Username or email already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Database error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/login": {
      "post": {
        "summary": "Start a cookie session",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Login succeeded and auth_token cookie was set",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/logout": {
      "post": {
        "summary": "Clear the current cookie session",
        "security": [],
        "responses": {
          "200": {
            "description": "Logged out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponse"
                }
              }
            }
          }
        }
      }
    },
    "/me": {
      "get": {
        "summary": "Return the current user",
        "responses": {
          "200": {
            "description": "Current user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/leaderboard": {
      "get": {
        "summary": "List ranked users",
        "security": [],
        "responses": {
          "200": {
            "description": "Leaderboard rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LeaderboardUser"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/portfolio": {
      "get": {
        "summary": "List active bets for the current user",
        "responses": {
          "200": {
            "description": "Active bets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PortfolioBet"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/history": {
      "get": {
        "summary": "List closed bets for the current user (paginated, newest first)",
        "responses": {
          "200": {
            "description": "Closed bets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ClosedBet"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size for history rows, newest first (default 100, capped at 500).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of history rows to skip (default 0, max 10000).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "maximum": 10000
            }
          }
        ]
      }
    },
    "/feed": {
      "get": {
        "summary": "List activity from followed users",
        "responses": {
          "200": {
            "description": "Feed rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FeedItem"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/following": {
      "get": {
        "summary": "List user IDs followed by the current user",
        "responses": {
          "200": {
            "description": "Followed user IDs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/users/search": {
      "get": {
        "summary": "Search users by username",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Username substring to match. A missing or empty value returns an empty list.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching users",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PublicUser"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/user/{user_id}/public": {
      "get": {
        "summary": "Get a public user profile",
        "security": [],
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size for history rows, newest first (default 100, capped at 500).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of history rows to skip (default 0, max 10000).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public profile, active portfolio, and trade history",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProfileResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/price/{ticker}": {
      "get": {
        "summary": "Fetch the current server-side price for a ticker",
        "parameters": [
          {
            "name": "ticker",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ticker price",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TickerPriceResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/bet/open": {
      "post": {
        "summary": "Open a paper-trading bet",
        "description": "User action. Requires explicit user authorization.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenBetRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/BetLimitReached"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "Bet opened",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenBetResponse"
                }
              }
            }
          }
        }
      }
    },
    "/bet/close": {
      "post": {
        "summary": "Close an open bet",
        "description": "User action. Requires explicit user authorization.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CloseBetRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bet closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CloseBetResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "description": "Bet not found, already closed, or not owned by the caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Could not fetch current price",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bet/{bet_id}": {
      "delete": {
        "summary": "Cancel a recently opened bet",
        "description": "User action. Requires explicit user authorization. Marks an open bet owned by the caller as CANCELLED: it is recorded in the owner's trade history at a hardcoded 0% gain and excluded from the leaderboard, so a mistaken entry never affects scoring. Allowed only within 5 minutes of opening; after that the bet must be closed via /bet/close. Followers who were notified when the bet opened receive a cancellation notification.",
        "parameters": [
          {
            "name": "bet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bet cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelBetResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "description": "Bet is not open (already closed) and cannot be cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The cancellation window has expired; close the bet instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelWindowExpiredError"
                }
              }
            }
          },
          "404": {
            "description": "Bet not found or not owned by the caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/follow": {
      "post": {
        "summary": "Follow or unfollow another user",
        "description": "User action. Requires explicit user authorization.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FollowRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Follow state changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FollowSuccessResponse"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/FollowLimitReached"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/stripe/plans": {
      "get": {
        "summary": "List public subscription plans",
        "description": "Public read endpoint for subscription offers. Agents may use it to compare plan names, prices, billing intervals, extra follow slots, and extra open-bet capacity before asking the user to choose a plan.",
        "security": [],
        "responses": {
          "200": {
            "description": "Subscription plans",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StripePlansResponse"
                }
              }
            }
          },
          "500": {
            "description": "Failed to fetch plans",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stripe/checkout": {
      "post": {
        "summary": "Create a Stripe checkout session",
        "description": "User billing action. Requires an authenticated cookie session and explicit user authorization for the chosen price_id. Returns a Stripe-hosted Checkout URL where the human user confirms and completes payment; the URL itself is not proof of purchase.",
        "responses": {
          "200": {
            "description": "Checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "description": "Already subscribed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to create checkout session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              }
            }
          }
        }
      }
    },
    "/api/stripe/portal": {
      "post": {
        "summary": "Create a Stripe customer portal session",
        "description": "User billing action. Requires explicit user authorization.",
        "responses": {
          "200": {
            "description": "Portal URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "description": "No Stripe customer found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to create portal session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/telegram/status": {
      "get": {
        "summary": "Return Telegram notification link status",
        "responses": {
          "200": {
            "description": "Telegram link status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelegramStatusResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/telegram/link-code": {
      "post": {
        "summary": "Create a Telegram link code",
        "description": "User action. Opens a Telegram linking flow.",
        "responses": {
          "200": {
            "description": "Link code and deep link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelegramLinkCodeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/telegram/unlink": {
      "post": {
        "summary": "Disable Telegram notifications",
        "description": "User action. Requires explicit user authorization.",
        "responses": {
          "200": {
            "description": "Telegram unlinked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelegramUnlinkResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "auth_token",
        "description": "HttpOnly JWT session cookie set by /login."
      }
    },
    "responses": {
      "Error": {
        "description": "Error response",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "FollowLimitReached": {
        "description": "Follow limit reached for the current plan.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/FollowLimitError"
            }
          }
        }
      },
      "BetLimitReached": {
        "description": "Open bet limit reached for the current plan.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/FollowLimitError"
            }
          }
        }
      }
    },
    "schemas": {
      "RegisterRequest": {
        "type": "object",
        "required": [
          "username",
          "email",
          "password"
        ],
        "properties": {
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 15
          }
        }
      },
      "LoginRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "OpenBetRequest": {
        "type": "object",
        "required": [
          "ticker",
          "direction"
        ],
        "properties": {
          "ticker": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "LONG",
              "SHORT"
            ]
          },
          "target_date": {
            "type": "string",
            "format": "date",
            "description": "Optional target close date (YYYY-MM-DD). Must not be in the past (UTC); same-day is allowed and settles after that day's market close."
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "follow_capacity": {
            "type": "integer"
          },
          "bet_capacity": {
            "type": "integer"
          },
          "stripe_subscription_id": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "username",
          "email",
          "follow_capacity",
          "bet_capacity"
        ],
        "additionalProperties": false
      },
      "PublicUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "username"
        ],
        "additionalProperties": false
      },
      "LeaderboardUser": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PublicUser"
          },
          {
            "type": "object",
            "properties": {
              "total_score": {
                "type": "number"
              },
              "total_trades": {
                "type": "integer"
              }
            },
            "required": [
              "total_score",
              "total_trades"
            ]
          }
        ],
        "unevaluatedProperties": false
      },
      "PortfolioBet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "ticker": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "LONG",
              "SHORT"
            ]
          },
          "entry": {
            "type": "number"
          },
          "current": {
            "type": "number"
          },
          "unrealized_gain": {
            "type": "number"
          },
          "target_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "entry_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelable_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC instant until which this bet may be cancelled via DELETE /bet/{bet_id}. After this, cancellation returns 403 and the bet must be closed."
          }
        },
        "required": [
          "id",
          "ticker",
          "direction",
          "entry",
          "current",
          "unrealized_gain",
          "target_date",
          "entry_date",
          "cancelable_until"
        ],
        "additionalProperties": false
      },
      "ClosedBet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "ticker": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "LONG",
              "SHORT"
            ]
          },
          "entry_price": {
            "type": "number"
          },
          "exit_price": {
            "type": "number"
          },
          "percentage_gain": {
            "type": "number"
          },
          "exit_date": {
            "type": "string",
            "format": "date-time"
          },
          "entry_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "ticker",
          "direction",
          "entry_price",
          "exit_price",
          "percentage_gain",
          "exit_date",
          "entry_date"
        ],
        "additionalProperties": true
      },
      "FeedItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ClosedBet"
          },
          {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "integer"
              },
              "username": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "OPEN",
                  "CLOSED",
                  "CANCELLED"
                ]
              }
            },
            "required": [
              "user_id",
              "username",
              "status"
            ],
            "additionalProperties": true
          }
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "MessageResponse": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "ApiCatalogLink": {
        "type": "object",
        "required": [
          "href",
          "type"
        ],
        "properties": {
          "href": {
            "type": "string",
            "format": "uri"
          },
          "type": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "ApiCatalogEntry": {
        "type": "object",
        "required": [
          "anchor",
          "service-desc",
          "service-doc",
          "oauth-protected-resource",
          "status"
        ],
        "properties": {
          "anchor": {
            "type": "string",
            "format": "uri"
          },
          "service-desc": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCatalogLink"
            }
          },
          "service-doc": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCatalogLink"
            }
          },
          "oauth-protected-resource": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCatalogLink"
            }
          },
          "status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCatalogLink"
            }
          }
        },
        "additionalProperties": false
      },
      "OAuthProtectedResourceMetadata": {
        "type": "object",
        "required": [
          "resource",
          "authorization_servers",
          "scopes_supported",
          "bearer_methods_supported",
          "service_documentation",
          "resource_documentation",
          "resource_name",
          "resource_policy_uri"
        ],
        "properties": {
          "resource": {
            "type": "string",
            "format": "uri"
          },
          "authorization_servers": {
            "type": "array",
            "description": "Empty until Investment Bets supports OAuth authorization server discovery.",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "scopes_supported": {
            "type": "array",
            "description": "Empty because session authorization is cookie-based, not OAuth-scope based.",
            "items": {
              "type": "string"
            }
          },
          "bearer_methods_supported": {
            "type": "array",
            "description": "Empty because bearer-token authorization is not currently supported.",
            "items": {
              "type": "string"
            }
          },
          "service_documentation": {
            "type": "string",
            "format": "uri"
          },
          "resource_documentation": {
            "type": "string",
            "format": "uri"
          },
          "resource_name": {
            "type": "string"
          },
          "resource_policy_uri": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": false
      },
      "ApiCatalog": {
        "type": "object",
        "required": [
          "linkset"
        ],
        "properties": {
          "linkset": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCatalogEntry"
            }
          }
        },
        "additionalProperties": false
      },
      "PublicProfileResponse": {
        "type": "object",
        "required": [
          "user",
          "portfolio",
          "history",
          "history_total"
        ],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/PublicUser"
          },
          "portfolio": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortfolioBet"
            }
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClosedBet"
            }
          },
          "history_total": {
            "type": "integer",
            "description": "Total closed bets for the user, independent of the returned history page."
          }
        },
        "additionalProperties": false
      },
      "TickerPriceResponse": {
        "type": "object",
        "required": [
          "ticker",
          "price"
        ],
        "properties": {
          "ticker": {
            "type": "string"
          },
          "price": {
            "type": "number"
          }
        },
        "additionalProperties": false
      },
      "OpenBetResponse": {
        "type": "object",
        "required": [
          "message",
          "bet_id",
          "entry_price"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "bet_id": {
            "type": "integer"
          },
          "entry_price": {
            "type": "number"
          }
        },
        "additionalProperties": false
      },
      "CloseBetRequest": {
        "type": "object",
        "required": [
          "bet_id"
        ],
        "properties": {
          "bet_id": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "CloseBetResponse": {
        "type": "object",
        "required": [
          "message",
          "ticker",
          "gain_percent",
          "exit_price"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "ticker": {
            "type": "string"
          },
          "gain_percent": {
            "type": "number"
          },
          "exit_price": {
            "type": "number"
          }
        },
        "additionalProperties": false
      },
      "CancelBetResponse": {
        "type": "object",
        "required": [
          "message",
          "bet_id"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "bet_id": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "CancelWindowExpiredError": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "FollowRequest": {
        "type": "object",
        "required": [
          "followed_id",
          "action"
        ],
        "properties": {
          "followed_id": {
            "type": "integer"
          },
          "action": {
            "type": "string",
            "enum": [
              "follow",
              "unfollow"
            ]
          }
        },
        "additionalProperties": false
      },
      "FollowSuccessResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "FollowLimitError": {
        "type": "object",
        "required": [
          "error",
          "message",
          "limit"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "LIMIT_REACHED"
          },
          "message": {
            "type": "string"
          },
          "limit": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "StripePlan": {
        "type": "object",
        "required": [
          "price_id",
          "amount",
          "currency",
          "interval",
          "extra_slots",
          "extra_bets",
          "name",
          "description"
        ],
        "properties": {
          "price_id": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "description": "Amount in the smallest currency unit."
          },
          "currency": {
            "type": "string"
          },
          "interval": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month",
              "year"
            ]
          },
          "extra_slots": {
            "type": "integer"
          },
          "extra_bets": {
            "type": "integer"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": false
      },
      "StripePlansResponse": {
        "type": "object",
        "required": [
          "plans"
        ],
        "properties": {
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StripePlan"
            }
          }
        },
        "additionalProperties": false
      },
      "CheckoutRequest": {
        "type": "object",
        "required": [
          "price_id"
        ],
        "properties": {
          "price_id": {
            "type": "string",
            "description": "Stripe price id from GET /api/stripe/plans."
          }
        },
        "additionalProperties": false
      },
      "UrlResponse": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": false
      },
      "TelegramStatusResponse": {
        "type": "object",
        "required": [
          "linked"
        ],
        "properties": {
          "linked": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "TelegramLinkCodeResponse": {
        "type": "object",
        "required": [
          "code",
          "deep_link"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "deep_link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        },
        "additionalProperties": false
      },
      "TelegramUnlinkResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      }
    }
  }
}
