{
  "openapi": "3.1.0",
  "info": {
    "title": "Trends Monitor API",
    "version": "1.0.0",
    "description": "Public read-only API, no key. Uses deployed snapshots; never triggers upstream collection. Product schedule: 09:30 and 21:30 HKT; GitHub: 09:00 HKT, available after successful deployment. Check snapshot_at and sources for freshness; evidence.fresh and signals are evaluated at collection time. If snapshot_at changes while paginating, restart at offset 0. Repeated parameters are rejected."
  },
  "servers": [
    {
      "url": "https://trends-monitor.pages.dev"
    }
  ],
  "security": [],
  "paths": {
    "/api/v1/products": {
      "get": {
        "operationId": "listProducts",
        "summary": "Product Radar",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring search on title and description.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Zero-based result offset.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0
            }
          },
          {
            "name": "signal",
            "in": "query",
            "required": false,
            "description": "Product-wide aggregate signal; not scoped to source/market.",
            "schema": {
              "type": "string",
              "enum": [
                "hot",
                "rising",
                "steady",
                "watch"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Evidence family: appstore, googleplay, producthunt, hn, itch. Must match the same evidence as market.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "market",
            "in": "query",
            "required": false,
            "description": "Evidence market, e.g. HK, US, TW, GLOBAL; case-insensitive.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Exact extra.category match, case-insensitive, e.g. 生產力.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Exact extra.np_type match, e.g. app, web, web_game.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published snapshot; original ordering/ranks preserved. Cache: browser 60 seconds, shared 300 seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoardResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid, empty, duplicate or unknown query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Read-only API; use GET or HEAD.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Published snapshot unavailable. Retry-After: 300.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "head": {
        "summary": "Same status and headers as GET, without a body.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring search on title and description.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Zero-based result offset.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0
            }
          },
          {
            "name": "signal",
            "in": "query",
            "required": false,
            "description": "Product-wide aggregate signal; not scoped to source/market.",
            "schema": {
              "type": "string",
              "enum": [
                "hot",
                "rising",
                "steady",
                "watch"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Evidence family: appstore, googleplay, producthunt, hn, itch. Must match the same evidence as market.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "market",
            "in": "query",
            "required": false,
            "description": "Evidence market, e.g. HK, US, TW, GLOBAL; case-insensitive.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Exact extra.category match, case-insensitive, e.g. 生產力.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Exact extra.np_type match, e.g. app, web, web_game.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published snapshot; original ordering/ranks preserved. Cache: browser 60 seconds, shared 300 seconds."
          },
          "400": {
            "description": "Invalid, empty, duplicate or unknown query parameter."
          },
          "404": {
            "description": "Unknown endpoint."
          },
          "405": {
            "description": "Read-only API; use GET or HEAD."
          },
          "503": {
            "description": "Published snapshot unavailable. Retry-After: 300."
          }
        }
      },
      "options": {
        "summary": "CORS preflight. Public origins; GET, HEAD, OPTIONS.",
        "responses": {
          "204": {
            "description": "No content; Access-Control-Allow-Origin: *."
          }
        }
      }
    },
    "/api/v1/github-trending": {
      "get": {
        "operationId": "listGitHubTrending",
        "summary": "GitHub daily Trending",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring search on title and description.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Zero-based result offset.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Case-insensitive exact language filter on the captured global daily chart; not a language-specific upstream chart.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published snapshot; original ordering/ranks preserved. Cache: browser 60 seconds, shared 300 seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoardResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid, empty, duplicate or unknown query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Read-only API; use GET or HEAD.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Published snapshot unavailable. Retry-After: 300.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "head": {
        "summary": "Same status and headers as GET, without a body.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring search on title and description.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Zero-based result offset.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Case-insensitive exact language filter on the captured global daily chart; not a language-specific upstream chart.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published snapshot; original ordering/ranks preserved. Cache: browser 60 seconds, shared 300 seconds."
          },
          "400": {
            "description": "Invalid, empty, duplicate or unknown query parameter."
          },
          "404": {
            "description": "Unknown endpoint."
          },
          "405": {
            "description": "Read-only API; use GET or HEAD."
          },
          "503": {
            "description": "Published snapshot unavailable. Retry-After: 300."
          }
        }
      },
      "options": {
        "summary": "CORS preflight. Public origins; GET, HEAD, OPTIONS.",
        "responses": {
          "204": {
            "description": "No content; Access-Control-Allow-Origin: *."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Item": {
        "type": "object",
        "required": [
          "item_id",
          "title",
          "url",
          "rank",
          "score",
          "status",
          "rank_delta",
          "extra"
        ],
        "properties": {
          "item_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "rank": {
            "type": "integer",
            "minimum": 1
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "description": "GitHub: stars gained today; products: no common numeric score."
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "up",
              "down",
              "same"
            ]
          },
          "rank_delta": {
            "type": [
              "integer",
              "null"
            ]
          },
          "extra": {
            "type": "object",
            "additionalProperties": true,
            "description": "Products: signals, category, np_type, evidence, history. GitHub: repository, language, desc, stars, forks, stars_today. Optional source fields may be omitted or null."
          }
        }
      },
      "BoardResponse": {
        "type": "object",
        "required": [
          "api_version",
          "dataset",
          "snapshot_at",
          "previous_snapshot_at",
          "timezone",
          "total",
          "count",
          "limit",
          "offset",
          "next_offset",
          "filters",
          "sources",
          "methodology",
          "items"
        ],
        "properties": {
          "api_version": {
            "const": "v1"
          },
          "dataset": {
            "enum": [
              "products",
              "github-trending"
            ]
          },
          "snapshot_at": {
            "type": "string",
            "description": "Collection timestamp for products, HKT date for GitHub. Not request time."
          },
          "previous_snapshot_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "const": "Asia/Hong_Kong"
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          },
          "offset": {
            "type": "integer",
            "minimum": 0
          },
          "next_offset": {
            "type": [
              "integer",
              "null"
            ]
          },
          "filters": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "Product source status and last success times; empty for GitHub."
          },
          "methodology": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "api_version",
          "error"
        ],
        "properties": {
          "api_version": {
            "const": "v1"
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}
