# Корпус отпечатков: зачем, как собирать и как не испортить

> **Состояние на 7 августа 2026: корпус собирает.** За первые сутки — 31
> устройство, из них 28 чужих. Код — `src/mantia/corpus/`, порядок работы —
> раздел «Как это устроено сейчас» в конце документа, первые выводы — раздел
> «Что уже собрано» там же.

Замысел пользователя: снять отпечатки с **настоящих устройств** (домашний
макбук с Safari и Chrome, разные Windows-машины, машины друзей), отдельно снять
их же с популярных антидетект-браузеров и, сравнив, натачивать наши профили.

Замысел правильный, и у него оказался лучший источник, чем друзья: **своя
сетка сайтов с живым трафиком**. Магазины дают «как выглядит обычный человек»,
`spoof.expert` — «как выглядит антидетект», потому что туда ходят проверять
прокси. Разница между этими двумя множествами и есть предмет корпуса, а метка
популяции ставится сама, по домену.

Ниже — что с замыслом делать, чтобы он дал результат, и чего делать нельзя.

## Главная ловушка: копировать отпечатки нельзя

Соблазн очевиден: взять частый настоящий отпечаток и раздавать его профилям.
Это ошибка, и именно она превращает антидетект в «штампованное».

Настоящий отпечаток уникален не значениями, а тем, что он **один такой**.
Скопировав его на сто профилей, мы получаем сто машин с одинаковым canvas,
одинаковым набором шрифтов и одинаковым renderer'ом — признак, которого в
природе не существует и который ищется одним `GROUP BY`. Площадке даже не
нужно знать, что именно подделано: достаточно увидеть повторяющийся набор.

Поэтому корпус — не источник значений. Корпус — источник **распределений и
связей** между значениями, и главное — **детектор наших собственных отличий**.

## Что корпус реально может изменить у нас

Наша архитектура не подделывает canvas, шрифты и WebGL: они приходят от
настоящего браузера на настоящей машине. Значит, «подкрутить» их корпусом
нельзя. Зато он даёт четыре вещи, каждая из которых применима сразу:

1. **Список невозможных сочетаний.** Для этого хватает десятка устройств.
   Ни одно настоящее устройство не отдаёт macOS в UA вместе с `Win32` в
   `navigator.platform`; не бывает `deviceMemory: 8` при `hardwareConcurrency: 2`
   на ноутбуке; не бывает мобильного UA при `maxTouchPoints: 0`. Каждое такое
   правило — проверка, которую можно прогонять на своих же профилях.
2. **Свои торчащие места.** Прогнать наши профили тем же зондом и сравнить с
   облаком настоящих. Отличие по одной оси — это то, за что нас поймают, и
   починить его дешевле, чем узнать о нём после блокировки.
3. **Выбор того, чем мы управляем.** Размер окна, локаль, языки, версия
   сборки в пуле — всё это мы задаём сами, и брать значения стоит из реального
   распределения, а не из головы. Отдельно: насколько наша пиннутая версия
   отстала от той, что реально ходит по сети (отставание — когортный признак).
4. **Оценка машины пользователя до боя.** Хост без GPU отдаёт SwiftShader;
   корпус показывает, насколько это редкость среди настоящих устройств, и
   значит — насколько сильный это признак. Человеку это надо сказать до того,
   как он потратит аккаунт, а не после.

## Как собирать, чтобы данные были сравнимы

**Один и тот же зонд.** Настоящие устройства и наши профили обязаны сниматься
**одним кодом**. Два разных скрипта дадут разницу, которой в отпечатках нет, —
и половина находок окажется артефактом измерения. У нас зонд уже есть:
`api/static/probe/fingerprint.html` плюс сбор в `identity/fingerprint_check.py`.
Для внешнего сайта нужен тот же сбор, вынесенный в один JS-файл.

**Разметка обязательна.** Без неё выборка — шум. У каждого замера: тип
устройства, ОС и её версия, браузер и версия, настоящее это устройство или
антидетект, какой именно антидетект, кто снимал. Замер без разметки не хуже
других — он просто бесполезен.

**Повторные замеры с той же машины.** Часть значений плавает между сессиями
(canvas зависит от состояния драйвера, аудио — от частоты дискретизации
устройства). Без двух-трёх замеров с одной машины невозможно отличить
«разные устройства» от «одно устройство в разные дни», а это ровно та ошибка,
которая делает выводы обратными.

**Сырое хранить целиком.** Нормализованный вид пригодится завтра, сырой —
через полгода, когда появится вопрос, которого сегодня нет.

## Чего ждать от размера выборки

| Выборка | Что даёт |
|---|---|
| 5–15 настоящих устройств | список невозможных сочетаний, грубые границы правдоподобия |
| 30–60 | распределения по управляемым нами осям (окно, языки, версии) |
| антидетекты, 3–5 штук | их характерные тычки: чем именно они себя выдают |
| сотни | статистика редкости отдельного значения |

Самое ценное достигается уже на первом уровне. Ждать сотен замеров, чтобы
начать пользоваться корпусом, не нужно.

## Согласие и границы

Замеры с чужих устройств (друзья) снимаются с их ведома — это их отпечаток,
а не абстрактные данные. Корпус остаётся внутренним: он нужен, чтобы находить
собственные изъяны, и не является ни базой для подделки конкретных людей, ни
средством их узнавания.

## Связь с Eval Lab

Корпус ложится на уже построенное как ещё один golden-набор. Метрика та же по
смыслу, что «ложный успех» в §7: профиль проходит наши собственные проверки,
но отличается от настоящих устройств по оси X. Такой профиль отчитывается об
успехе, не будучи качественным, — и увидеть это можно только сравнением с
внешней правдой, то есть корпусом.


---

## Как это устроено сейчас

```
браузер посетителя           тот же сайт                  ваш макбук
  probe.js ──маячок──▶ /corpus/collect ──scp──▶ mantia corpus-ingest
   (по клику)          nginx пишет тело в лог
```

### Три решения и их причины

**Приём на своём домене, а не на общем.** Соблазн собрать всё в одно место
велик, но тогда магазин при каждом визите стучится на посторонний сайт: лишняя
связь между проектами прямо в браузере посетителя. Адрес в зонде относительный
(`/corpus/collect`), домены не зашиты нигде. Побочно исчезает CORS.

**Запуск по первому действию человека**, а не по загрузке страницы. Дело не в
разрешениях — всё, что зонд снимает, доступно и без жеста. Дело в выборке: в
журналах сетки от 10 до 25 процентов запросов делают сканеры, и они грузят
страницу. Корпус, разбавленный чужими серверами, врёт о распределениях именно
там, где должен быть точен. Замерено: без действия спустя шесть секунд ноль
замеров, после клика — полный.

**Принимает уже стоящий nginx.** Замер — это одна строка тела запроса, и nginx
умеет записать её сам. Своя служба была бы третьей вещью, которую надо ставить,
обновлять и держать живой, ради одной директивы. Схлопывание одинаковых машин
и подсчёт происходят при разборе, на своей машине.

### Что хранится и чего не хранится

Замер схлопывается в «такое сочетание значений встретилось N раз». Записи о
посетителе не появляется вовсе: ни IP (в логе его нет — формат содержит только
сутки и тело), ни куки, ни точного времени, ни меток медиаустройств. Это не
осторожность формулировок, а требование задачи: корпусу нужны распределения, и
строка про отдельного посетителя ему бесполезна.

`combo_hash` не включает сайт и популяцию — одна машина, зашедшая и в магазин,
и на проверяльщик, остаётся **одним** устройством. А метка популяции при этом
**накапливается** (`checker,shop`): машина, ходящая в оба места, — вероятный
антидетект, и потерять этот факт значило бы испортить ровно ту разницу между
множествами, ради которой всё затевалось.

### Где что стоит

| сайт | популяция | приёмник | состояние |
|---|---|---|---|
| cansphere.com | shop | nginx | **работает** |
| legitdress.com | shop | nginx | **работает** |
| rareamour.com | shop | nginx | **работает** |
| emirates.kyiv.ua | shop | nginx | **работает** |
| spoof.expert | **checker** | nginx | **работает** |
| exif.expert | shop | route в Next.js | **не раскатан** |

`exif.expert` живёт на Windows-VPS с туннелем Cloudflare — nginx там нет,
поэтому ему написан приёмник в самом приложении
(`src/app/corpus/collect/route.ts`, исходник — `corpus/deploy/nextjs-route.ts`).
Файлы в проекте лежат, перенести на ту машину не удалось: доступа не было.

Резервные копии конфигов на каждом сервере: `/root/*.bak-corpus`.

### Забрать и разобрать

```bash
for h in cansphere legitdress rareamour spoofexpert; do
  scp $h:/var/log/nginx/corpus.log /tmp/$h.log && mantia corpus-ingest /tmp/$h.log
done
scp -i ~/.ssh/emirates root@89.127.196.235:/var/log/nginx/corpus.log /tmp/emirates.log
mantia corpus-ingest /tmp/emirates.log
mantia corpus-summary
```

Повторный разбор безопасен: замеры схлопываются по хешу, вырастет только
счётчик. **Источник правды — логи, база из них выводится**, поэтому схему
корпуса можно менять свободно: пересбор стоит одной команды.

### Что уже собрано

Первые сутки, 6–7 августа 2026:

| | устройств |
|---|---|
| всего | 31 |
| свои (макбук + заглушка) | 3 |
| **чужих** | **28** |
| мобильных | 21 |
| **десктопного Chrome** | **8** |

По площадкам: проверяльщик — 15 устройств, магазины — 13, и 3 машины замечены
и там, и там.

**Восемь чужих устройств не выглядят домашними машинами.** Windows с
`Microsoft Basic Render Driver` (2 ядра, 4 ГБ), **мак с `Google SwiftShader`** —
такого железа не существует вовсе, Linux со SwiftShader, браузер без внятного
UA на 800×600. Первые три пришли на проверяльщик — ровно там, где и ожидались
антидетекты. Это и есть та разница между двумя множествами, ради которой
корпус собирается.

**Первая связь, годная как проверка своих же профилей.** Из 20 устройств на
Safari/iOS `deviceMemory` не сообщает **ни одно**; из 10 на Chrome сообщают
девять. Профиль, заявляющий Safari и отдающий `deviceMemory`, невозможен —
хотя по отдельности оба значения безупречны.

**И урок про метод, который стоит помнить.** Правило «`deviceMemory` больше 8
не бывает» казалось очевидным и сработало на нашем же макбуке. В данных
встречаются 4, 8 и **16** — все степени двойки, все от настоящих машин.
Правило было неверным. Ради этого корпус и нужен: он опровергает правдоподобные
рассуждения замером, а не спором.

**Чего пока не хватает.** Пригодных эталонов десктопного Chrome — три (из
восьми две наши машины и три виртуалки). Для распределений нужно 30–60, для
статистики редкости — сотни. Темп: около месяца до сотни при нынешнем трафике.
Это же — главное, чего не хватает, чтобы начинать форк (см. `handover.md`, §7).

### Три ловушки, все молчаливые

Каждая проверена замером, каждая стоила времени:

* **`application/json` в маячке.** Этот тип не входит в список безопасных для
  CORS, требует предварительного `OPTIONS`, которого `sendBeacon` делать не
  умеет — браузер отбрасывает отправку целиком, и в консоли пусто. Зонд шлёт
  `text/plain`, сервер разбирает тело как JSON. **Не «чинить» обратно.**
* **Пустой лог при `return 204`.** Без `proxy_pass` nginx не читает тело
  запроса, и `$request_body` пуст: файл создаётся и остаётся нулевым. Пустышка
  на `127.0.0.1:9977` существует ровно ради этого.
* **Удалённый лог.** Стереть файл после перезагрузки nginx — значит заставить
  его писать в удалённый inode: снаружи «ничего не пишется», хотя пишется.
  Сначала удалить, потом перезагрузить.
