{
  "openapi": "3.0.3",
  "info": {
    "title": "aff.top Public API",
    "version": "1.0.0",
    "description": "Открытый REST API базы спамеров Telegram и каталога CPA/iGaming-партнёрок.\n\nВсе эндпоинты публичные: без ключей, без авторизации, без регистрации. Ответ — всегда JSON, запросы — GET (кроме `/api/fingerprint/match`).\n\n**Лицензия данных:** [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). Используйте в ботах, таблицах, антифрод-пайплайнах — просим только ссылку на источник.\n\n**Rate limits:** по умолчанию применяется общий throttle `api-public`. Детальные лимиты — в каждом эндпоинте.",
    "contact": {
      "name": "aff.top bot",
      "url": "https://t.me/SPAMpartnersBOT"
    },
    "license": {
      "name": "CC-BY 4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    }
  },
  "servers": [
    {
      "url": "https://aff.top",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Спамеры",
      "description": "Проверка юзернеймов и список известных спамеров"
    },
    {
      "name": "Компании",
      "description": "Каталог партнёрок, сервисов, рекламных сетей и игровых провайдеров"
    },
    {
      "name": "Поиск",
      "description": "Live-поиск и глобальный поиск по всем сущностям (Meilisearch)"
    },
    {
      "name": "Виджеты",
      "description": "Данные для встраиваемых виджетов и бейджей"
    },
    {
      "name": "Фингерпринт",
      "description": "Матч TLS/canvas/WebGL-сигналов против библиотеки известных профилей"
    }
  ],
  "paths": {
    "/api/check/{identifier}": {
      "get": {
        "summary": "Проверить username в базе спамеров",
        "description": "Принимает Telegram username (с `@` или без). Если на пользователя есть жалобы — возвращает полную статистику: число репортов, уровень доверия, категории, связанные компании, тренд за неделю. Если пользователь чист — `found: false`, `reports_count: 0`. **`found: false` означает «в базе не нашли», не «гарантированно не спамер»**.",
        "operationId": "checkUsername",
        "tags": ["Спамеры"],
        "parameters": [
          {
            "name": "identifier",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "example": "spammer_username" },
            "description": "Telegram username. Знак `@` необязателен — отрезается автоматически."
          }
        ],
        "responses": {
          "200": {
            "description": "Результат проверки",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string", "example": "public, max-age=300" },
                "description": "Результат кэшируется 5 минут"
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "found": {
                    "summary": "Спамер найден",
                    "value": {
                      "found": true,
                      "username": "spammer_username",
                      "reports_count": 23,
                      "unique_reports": 18,
                      "confidence_max": 75,
                      "first_seen": "2026-01-15T10:23:00Z",
                      "last_seen": "2026-03-10T18:41:00Z",
                      "week_trend": 12.5,
                      "last_7_days": 5,
                      "categories": ["gambling", "crypto"],
                      "companies": [
                        { "name": "Mostbet", "slug": "mostbet", "url": "https://aff.top/company/mostbet" }
                      ],
                      "url": "https://aff.top/reports?q=spammer_username"
                    }
                  },
                  "not_found": {
                    "summary": "Не найден",
                    "value": {
                      "found": false,
                      "username": "clean_user",
                      "reports_count": 0
                    }
                  }
                },
                "schema": { "$ref": "#/components/schemas/CheckResponse" }
              }
            }
          }
        }
      }
    },
    "/api/companies": {
      "get": {
        "summary": "Список компаний с числом жалоб",
        "description": "Рейтинг компаний (партнёрки, сервисы, рекламные сети, игровые провайдеры) отсортированный по числу жалоб. Есть пагинация и фильтр по slug.",
        "operationId": "listCompanies",
        "tags": ["Компании"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 },
            "description": "Записей на страницу. Максимум 200."
          },
          {
            "name": "page",
            "in": "query",
            "schema": { "type": "integer", "default": 1, "minimum": 1 },
            "description": "Номер страницы."
          },
          {
            "name": "slug",
            "in": "query",
            "schema": { "type": "string", "example": "mostbet" },
            "description": "Фильтр по конкретной компании."
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка компаний",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": 1,
                      "name": "Mostbet",
                      "slug": "mostbet",
                      "reports_count": 142,
                      "url": "https://aff.top/company/mostbet"
                    }
                  ],
                  "total": 8091,
                  "page": 1,
                  "pages": 162
                },
                "schema": { "$ref": "#/components/schemas/CompaniesResponse" }
              }
            }
          }
        }
      }
    },
    "/api/spammers": {
      "get": {
        "summary": "Список спамеров",
        "description": "Уникальные username с хотя бы одной жалобой. Можно фильтровать по категории или искать конкретный аккаунт. Счётчик `total` кэшируется 5 минут (дорогой COUNT DISTINCT).",
        "operationId": "listSpammers",
        "tags": ["Спамеры"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 },
            "description": "Записей на страницу."
          },
          {
            "name": "username",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Поиск конкретного пользователя по точному совпадению."
          },
          {
            "name": "category",
            "in": "query",
            "schema": { "$ref": "#/components/schemas/Category" },
            "description": "Фильтр по категории спама."
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка спамеров",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "username": "spammer123",
                      "reports_count": 14,
                      "last_seen": "2026-03-10T18:41:00Z",
                      "category": "gambling",
                      "primary_category": "gambling",
                      "categories": ["gambling", "crypto"],
                      "url": "https://aff.top/reports?q=spammer123"
                    }
                  ],
                  "total": 2261
                },
                "schema": { "$ref": "#/components/schemas/SpammersResponse" }
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "summary": "Live-поиск по компаниям, спамерам и людям",
        "description": "Простой SQL-поиск (ILIKE) по трём типам сущностей сразу. Возвращает до 7 компаний, 6 людей и 4 спамеров. Для продвинутого поиска с typo-tolerance — используйте `/api/search/global`.",
        "operationId": "liveSearch",
        "tags": ["Поиск"],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "minLength": 2, "maxLength": 200 },
            "description": "Поисковый запрос. Минимум 2 символа. Поддерживает компании, `@username`, домены."
          }
        ],
        "responses": {
          "200": {
            "description": "Найденные сущности, сгруппированные по типу",
            "content": {
              "application/json": {
                "example": {
                  "people": [
                    {
                      "name": "Артём Кравченко",
                      "url": "https://aff.top/faces/artem-kravchenko",
                      "avatar": "/storage/avatars/p/artem-kravchenko.webp",
                      "reports_count": 0
                    }
                  ],
                  "companies": [
                    {
                      "name": "Mostbet",
                      "url": "https://aff.top/company/mostbet",
                      "count": 142,
                      "cat": "gambling",
                      "last": "3 дня назад"
                    }
                  ],
                  "spammers": [
                    {
                      "username": "mostbet_agent",
                      "url": "https://aff.top/reports?q=mostbet_agent",
                      "count": 5
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/search/global": {
      "get": {
        "summary": "Глобальный поиск через Meilisearch",
        "description": "Поиск с typo-tolerance и prefix-matching по 8 группам сущностей: компании, люди, глоссарий, видео, YT-каналы, TG-каналы, TG-чаты, посты блога. Типичное время ответа — 5-30 мс. Если запрос короче 2 символов — вернёт пустой массив.",
        "operationId": "globalSearch",
        "tags": ["Поиск"],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "minLength": 2, "maxLength": 120 },
            "description": "Поисковый запрос."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 50 },
            "description": "Общий лимит на каждую группу (override дефолтов 3-8)."
          }
        ],
        "responses": {
          "200": {
            "description": "Результаты, сгруппированные по типам",
            "content": {
              "application/json": {
                "example": {
                  "query": "mostbet",
                  "total": 12,
                  "results": [
                    {
                      "type": "company",
                      "label": "Компании",
                      "count": 1,
                      "items": [
                        {
                          "type": "company",
                          "title": "Mostbet",
                          "url": "/company/mostbet",
                          "subtitle": "gambling · 142 жалобы"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/company/{slug}": {
      "get": {
        "summary": "Подробная информация о компании",
        "description": "Полная карточка компании: категория, тренд за неделю, алиасы (alt-names), ссылки на badge SVG и карточку. Принимает slug или точное имя компании (ILIKE).",
        "operationId": "getCompany",
        "tags": ["Компании"],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "example": "mostbet" },
            "description": "Slug компании (или точное имя)."
          }
        ],
        "responses": {
          "200": {
            "description": "Найдена",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string", "example": "public, max-age=300" }
              }
            },
            "content": {
              "application/json": {
                "example": {
                  "found": true,
                  "name": "Mostbet",
                  "slug": "mostbet",
                  "category": "gambling",
                  "category_label": "Gambling",
                  "reports_count": 142,
                  "first_report_date": "2024-08-12",
                  "last_report_date": "2026-04-18",
                  "week_trend": 12.5,
                  "last_7_days": 9,
                  "aliases": ["мосбет", "mostbet.com"],
                  "badge_url": "https://aff.top/api/badge/mostbet.svg",
                  "card_url": "https://aff.top/card/mostbet",
                  "url": "https://aff.top/company/mostbet"
                }
              }
            }
          },
          "404": {
            "description": "Компания не найдена",
            "content": {
              "application/json": {
                "example": { "found": false, "query": "unknown-slug" }
              }
            }
          }
        }
      }
    },
    "/api/widget/{slug}": {
      "get": {
        "summary": "Данные для встраиваемого виджета",
        "description": "Компактные данные для внешних виджетов: агрегаты по жалобам, график за 7 дней (массив `[day-6, ..., day-0]`), тренд, ссылки на бейдж/embed/страницу. Кэш 5 минут. Отдаётся с `Access-Control-Allow-Origin: *` — можно использовать из любого домена.",
        "operationId": "getWidget",
        "tags": ["Виджеты"],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Slug компании."
          }
        ],
        "responses": {
          "200": {
            "description": "Данные виджета",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string", "example": "public, max-age=300" }
              },
              "Access-Control-Allow-Origin": {
                "schema": { "type": "string", "example": "*" }
              }
            },
            "content": {
              "application/json": {
                "example": {
                  "found": true,
                  "name": "Mostbet",
                  "slug": "mostbet",
                  "reports_count": 142,
                  "trend": "+12.5%",
                  "trend_direction": "up",
                  "last_7_days": [3, 1, 0, 2, 1, 0, 2],
                  "top_category": "gambling",
                  "badge_url": "https://aff.top/api/badge/mostbet.svg",
                  "card_url": "https://aff.top/card/mostbet",
                  "embed_url": "https://aff.top/embed/counter-dark/mostbet",
                  "page_url": "https://aff.top/company/mostbet"
                }
              }
            }
          },
          "404": {
            "description": "Не найдена",
            "content": {
              "application/json": {
                "example": {
                  "found": false,
                  "slug": "unknown",
                  "reports_count": 0,
                  "page_url": "https://aff.top/company/unknown"
                }
              }
            }
          }
        }
      }
    },
    "/api/fingerprint/match": {
      "post": {
        "summary": "Матч отпечатка браузера/бота против библиотеки",
        "description": "Принимает набор сигналов (JA4 TLS, canvas hash, WebGL renderer, audio hash и др.) и возвращает известный профиль: браузер, антидетект, бот или HTTP-клиент. Stateless — наблюдения не сохраняются. Cache 5 минут. Видны только публичные профили (trust ≥ community).\n\n**Лимит:** 60 req/min с одного IP. Максимум 20 сигналов в запросе, каждый ≤ 255 символов.",
        "operationId": "fingerprintMatch",
        "tags": ["Фингерпринт"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FingerprintRequest" },
              "example": {
                "signals": {
                  "ja4": "t13d1516h2_8daaf6152771_02713d6af862",
                  "webgl_renderer": "ANGLE (Apple, Apple M2)"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict + найденный профиль (или null)",
            "content": {
              "application/json": {
                "examples": {
                  "high_confidence": {
                    "summary": "Профиль найден (high confidence)",
                    "value": {
                      "match": {
                        "profile": {
                          "id": 42,
                          "label": "Chrome 147 / macOS",
                          "kind": "browser",
                          "name": "Chrome",
                          "os_family": "macos",
                          "trust_level": "community",
                          "observations": 342
                        },
                        "confidence": 0.87,
                        "verdict": "high",
                        "matched_signals": ["ja4"],
                        "unmatched_signals": [],
                        "top_candidates": []
                      },
                      "cached": true
                    }
                  },
                  "unknown": {
                    "summary": "Неизвестный профиль",
                    "value": {
                      "match": {
                        "profile": null,
                        "verdict": "unknown",
                        "confidence": 0,
                        "matched_signals": [],
                        "top_candidates": []
                      },
                      "cached": false
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Невалидные данные (нет `signals`, больше 20 ключей и т.д.)"
          },
          "429": {
            "description": "Rate limit (60 req/min)"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Category": {
        "type": "string",
        "enum": ["gambling", "crypto", "nutra", "traffic", "affiliate", "job", "adult", "forex", "tools", "payments", "infobiz", "other"],
        "description": "Категория оффера/спама"
      },
      "Company": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "example": 1 },
          "name": { "type": "string", "example": "Mostbet" },
          "slug": { "type": "string", "example": "mostbet" },
          "reports_count": { "type": "integer", "example": 142 },
          "url": { "type": "string", "format": "uri", "example": "https://aff.top/company/mostbet" }
        }
      },
      "CompaniesResponse": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Company" } },
          "total": { "type": "integer", "example": 8091 },
          "page": { "type": "integer", "example": 1 },
          "pages": { "type": "integer", "example": 162 }
        }
      },
      "Spammer": {
        "type": "object",
        "properties": {
          "username": { "type": "string", "example": "spammer123" },
          "reports_count": { "type": "integer", "example": 14 },
          "last_seen": { "type": "string", "format": "date-time" },
          "category": { "$ref": "#/components/schemas/Category", "nullable": true },
          "primary_category": { "$ref": "#/components/schemas/Category", "nullable": true },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "SpammersResponse": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Spammer" } },
          "total": { "type": "integer", "example": 2261, "description": "Кэшируется 5 минут" }
        }
      },
      "CheckResponse": {
        "oneOf": [
          {
            "type": "object",
            "required": ["found", "username", "reports_count", "first_seen", "last_seen"],
            "properties": {
              "found": { "type": "boolean", "enum": [true] },
              "username": { "type": "string" },
              "reports_count": { "type": "integer" },
              "unique_reports": { "type": "integer" },
              "confidence_max": { "type": "integer", "minimum": 0, "maximum": 100 },
              "first_seen": { "type": "string", "format": "date-time" },
              "last_seen": { "type": "string", "format": "date-time" },
              "week_trend": { "type": "number", "description": "% change week-over-week" },
              "last_7_days": { "type": "integer" },
              "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } },
              "companies": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string" },
                    "slug": { "type": "string" },
                    "url": { "type": "string", "format": "uri" }
                  }
                }
              },
              "url": { "type": "string", "format": "uri" }
            }
          },
          {
            "type": "object",
            "required": ["found", "username", "reports_count"],
            "properties": {
              "found": { "type": "boolean", "enum": [false] },
              "username": { "type": "string" },
              "reports_count": { "type": "integer", "enum": [0] }
            }
          }
        ]
      },
      "FingerprintRequest": {
        "type": "object",
        "required": ["signals"],
        "properties": {
          "signals": {
            "type": "object",
            "description": "Ключ → значение сигнала. Поддерживаемые ключи: `ja4`, `ja3`, `ja4_t`, `webgl_renderer`, `canvas_geom`, `canvas_text`, `audio`, `fonts_hash`, `akamai_h2`. Всего не более 20 сигналов, каждое значение до 255 символов.",
            "additionalProperties": { "type": "string" },
            "example": {
              "ja4": "t13d1516h2_8daaf6152771_02713d6af862",
              "webgl_renderer": "ANGLE (Apple, Apple M2)"
            }
          }
        }
      }
    }
  }
}
