Урок 1.1. Ручная настройка трекера без ИИ
То же самое, что в первом уроке, но руками: команды, конфиги, скрипт на лендинг, колонки и схема статусов — без агентов и автоматики.
Это приложение к первому уроку. Там всё ставит ИИ-агент по одному промту, а здесь то же самое руками: какие команды выполнять, какой скрипт и куда вставлять на лендинге, где прописывается ключ партнёрки, как завести колонки в трекере.
Порядок важен: сначала сервер и домены, потом лендинги, потом постбеки, в конце колонки.
1. Установка трекера
Сервер: Ubuntu 24.04 или 26.04, минимум 2 ядра, 4 ГБ памяти, 50 ГБ диска. Сервер должен быть чистым — без nginx, php и mysql, иначе установщик подерётся за порты.
wget https://data.binom.org/binom_v2_install.sh -O /root/binom_v2_install.sh
bash /root/binom_v2_install.sh install --email=ВАШ_EMAIL
Частая засада. На свежем VPS в фоне работает автообновление и держит блокировку apt. Установка падает с ошибкой про Could not get lock. Лечится так:
ps aux | grep apt # смотрим, что висит
systemctl stop unattended-upgrades
dpkg --configure -a
# и запускаем установку заново
Ставится минут десять, поднимается девять контейнеров. После установки доступы лежат в файле /root/binom/.binom — заберите их сразу, файл самоудаляется через 2 часа.
2. Домены
Все A-записи ведут прямо на IP сервера. Если домены на Cloudflare — статус записи «DNS only», серое облако. С оранжевым сертификат трекер обычно всё равно получит, но прокси встанет между вами и кликами: постбеки от партнёрки могут не дойти, вместо редиректа посетитель иногда увидит проверочную страницу, а IP в отчётах будет не его. Поэтому серое.
dig +short A ваш-домен # должен вернуть IP сервера, а не адрес Cloudflare
Домен 1 — админка
docker exec binom_backend php bin/console app:managed:domain:init --host=ДОМЕН_АДМИНКИ
После этого вход в панель — по адресу https://ДОМЕН_АДМИНКИ/index. Сертификат выпустится сам при первом заходе.
Домен 2 — ссылки кампаний
Добавляется в самом трекере: Domains справа сверху, вставляете домен, сохраняете. Проверка: откройте https://ДОМЕН_КАМПАНИЙ/click — в ответ придёт Bad Request. Это правильно, значит эндпоинт живой.
Домен 3 — лендинги
Трекер не хостит произвольные PHP-лендинги, поэтому поднимаем рядом лёгкий хостинг:
mkdir -p /var/www/landers
docker run -d --name landers-php --restart unless-stopped \
-p 127.0.0.1:8090:80 -v /var/www/landers:/var/www/html php:8.2-apache
Дальше создаём маршрут для домена. Файл /var/lib/docker/volumes/binom_proxy_config/_data/domains/ДОМЕН_ЛЕНДОВ.caddy:
http://ДОМЕН_ЛЕНДОВ,
https://ДОМЕН_ЛЕНДОВ {
rewrite * /ДОМЕН_ЛЕНДОВ{path}
reverse_proxy 127.0.0.1:8090
log {
level ERROR
output file /var/log/ДОМЕН_ЛЕНДОВ.error.log
}
}
docker exec binom_proxy caddy validate --config /etc/caddy/Caddyfile
docker exec binom_proxy caddy reload --config /etc/caddy/Caddyfile
Файлы лендингов кладём в /var/www/landers/ДОМЕН_ЛЕНДОВ/путь/, открываются они как https://ДОМЕН_ЛЕНДОВ/путь/. После каждой заливки:
chown -R www-data:www-data /var/www/landers
И обязательно выключите показ ошибок PHP, чтобы пользователь не видел пути и куски кода: добавьте display_errors = Off в конфиг php внутри контейнера landers-php.
3. Скрипт на лендинг
Этот скрипт собирает вовлечённость и ботов — те самые колонки, по которым мы чистим трафик в четвёртом уроке. Вставляется перед закрывающим тегом </body> на каждом лендинге.
Что поменять: ВАШ-ДОМЕН-ТРЕКЕРА на домен кампаний, ВАШ-ДОМЕН-ЛЕНДОВ на домен, где лежат лендинги.
<script>
(function () {
var TRACKER_BASE = 'https://ВАШ-ДОМЕН-ТРЕКЕРА/click';
var qs = new URLSearchParams(window.location.search);
var clickid = qs.get('subacc') || qs.get('click_id') || qs.get('clickid') || '';
// заполнить скрытое поле формы click_id (если есть) значением из URL
var _ci = document.getElementById('click_id');
if (_ci && clickid) _ci.value = clickid;
var fired = { time15: false, time30: false, scroll25: false, scroll50: false, scroll75: false, scroll90: false };
function fireEvent(num, value) {
if (!clickid) return;
var url = TRACKER_BASE + '?upd_clickid=' + encodeURIComponent(clickid) + '&event' + num + '=' + value;
fetch(url, { method: 'GET', mode: 'no-cors', cache: 'no-cache' }).catch(function () {});
}
setTimeout(function () { if (!fired.time15) { fired.time15 = true; fireEvent(6, 1); } }, 15000);
setTimeout(function () { if (!fired.time30) { fired.time30 = true; fireEvent(7, 1); } }, 30000);
window.addEventListener('scroll', function () {
var top = window.scrollY || document.documentElement.scrollTop;
var full = document.documentElement.scrollHeight - window.innerHeight;
if (full <= 0) return;
var pct = Math.round((top / full) * 100);
[25, 50, 75, 90].forEach(function (t) {
var key = 'scroll' + t;
if (pct >= t && !fired[key]) { fired[key] = true; fireEvent(8, t); }
});
}, { passive: true });
// Binom засчитывает LP click на первом запросе; дальше редирект на заглушку оффера, его отказ игнорируем
function fireLpClick() {
if (!clickid || fired.lp) return;
fired.lp = true;
fetch(TRACKER_BASE + '?lp=1&bcid=' + encodeURIComponent(clickid),
{ method: 'GET', mode: 'no-cors', keepalive: true }).catch(function () {});
}
var ORDER_ZONE = '#promo-main-wrapper-aptboxes,[class*="wheel"],[class*="door"],[class*="gift"],[class*="prize"],[class*="spin"],[class*="roulette"],[class*="scratch"]';
function inOrderZone(el) {
if (!el || !el.closest) return false;
if (el.closest(ORDER_ZONE)) return true;
var form = el.closest('form');
return !!(form && form.querySelector('[name="phone"]'));
}
document.addEventListener('click', function (e) {
if (inOrderZone(e.target)) fireLpClick();
var img = e.target.closest && e.target.closest('img');
if (img && img.naturalWidth > 1 && !fired.img) { fired.img = true; fireEvent(9, 1); }
}, true);
document.addEventListener('focusin', function (e) {
if (inOrderZone(e.target)) fireLpClick();
}, true);
// event10 — простой чекер: на мобильном трафике заход без touch = подозрительно (десктоп/эмулятор)
var _isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
if (!_isTouch) fireEvent(10, 1);
// event11 — BotD (headless/Selenium/Puppeteer/Playwright), самохост, телеметрия выключена
if (clickid) {
try {
var _b = document.createElement('script');
_b.src = 'https://ВАШ-ДОМЕН-ЛЕНДОВ/botd.min.js';
_b.async = true;
_b.onload = function () {
if (window.BotD && window.BotD.load) {
window.BotD.load({ monitoring: false })
.then(function (a) { return a.detect(); })
.then(function (r) { if (r && r.bot) fireEvent(11, 1); })
.catch(function () {});
}
};
document.head.appendChild(_b);
} catch (e) {}
}
})();
</script>
Что делает каждый кусок
- event6 и event7 — посетитель пробыл на странице 15 и 30 секунд.
- event8 — глубина скролла, отсечки 25, 50, 75 и 90 процентов.
- event9 — клик по картинке, то есть живое взаимодействие с контентом.
- event10 — у устройства нет тача. На мобильном трафике это повод присмотреться, но сам по себе не бот: так же выглядят десктоп и некоторые браузеры.
- event11 — визит, который библиотека BotD пометила как подозрительный по отпечатку браузера. Это оценка, а не приговор.
- lp=1 — клик в зону заказа: форма, колесо, двери, коробки. Это LP click в отчёте.
Важно про номера событий. События с 1 по 5 и с 25 по 30 не трогайте: 25–30 заняты схемой статусов, о ней ниже.
BotD кладём на свой домен
Библиотека бесплатная, 12 КБ. Скачиваем и кладём в корень домена лендингов, чтобы не ходить на чужой CDN:
curl -sL https://openfpcdn.io/botd/v2/umd.min.js \
-o /var/www/landers/ДОМЕН_ЛЕНДОВ/botd.min.js
Если на лендинге есть форма
Добавьте в форму скрытое поле — скрипт сам подставит в него click id из ссылки:
<input type="hidden" name="subacc" id="click_id" value="">
4. Ключ партнёрки и отправка заявок
Тут два случая. Если вы льёте на готовый пакет лендинга из AdCombo — в нём уже есть своя отправка, нужно только добавить отметку лида в трекер. Если лендинг свой, с формой — отправку в партнёрку пишем сами.
Где взять ключ
API-ключ лежит в кабинете AdCombo в настройках профиля. Он один на аккаунт и нужен для отправки заявок с вашего лендинга.
Свой лендинг: конфиг
Файл api/config.php рядом с лендингом:
<?php
$apiKey = 'ВАШ_API_КЛЮЧ';
$apiCreateOrderUrl = 'https://api.adcombo.com/api/v2/order/create/';
$offer_id = 40792; // ID оффера из партнёрки
$country_code = 'CD'; // страна оффера
$price = 50; // цена оффера
$binomClickUrl = 'https://ВАШ-ДОМЕН-ТРЕКЕРА/click';
Свой лендинг: отправка заявки
Ключевые места файла api/send_lead.php:
// 1. click id — из формы, а если его там нет, из адреса страницы
$click_id = @$_POST['click_id'] ?: @$_POST['clickid'] ?: @$_POST['subacc'];
// 2. IP посетителя — ОБЯЗАТЕЛЬНО из заголовков
$ip = '';
foreach (array('HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP') as $k) {
if (!empty($_SERVER[$k])) { $ip = trim(explode(',', $_SERVER[$k])[0]); break; }
}
if (!$ip) $ip = $_SERVER['REMOTE_ADDR'];
// 3. отправка в партнёрку
$params = array('api_key'=>$apiKey, 'name'=>@$_POST['name'], 'phone'=>@$_POST['phone'],
'offer_id'=>$offer_id, 'country_code'=>$country_code, 'price'=>$price,
'ip'=>$ip, 'subacc'=>$click_id);
$res = @file_get_contents($apiCreateOrderUrl . '?' . http_build_query($params));
// 4. отметка лида в трекере
if ($click_id) {
@file_get_contents($binomClickUrl . '?' . http_build_query(
array('cnv_id'=>$click_id, 'payout'=>0, 'cnv_status'=>'lead')));
}
Про IP — это не мелочь. Лендинги стоят за прокси, и в REMOTE_ADDR приезжает внутренний адрес вроде 172.17.0.1. Партнёрка на такой отвечает ошибкой Wrong IP и заявка не создаётся вообще. Именно поэтому IP берётся из заголовков.
Готовый пакет из партнёрки
В пакетах AdCombo отправка уже написана и IP берётся правильно. Нужно только добавить отметку лида в трекер. В файле order.php находим строку, где партнёрка ответила успехом, и сразу после неё вставляем:
if ($data['code'] == 'ok') {
// click id: из запроса, а если его нет — из адреса страницы, с которой пришли
$binom_click_id = '';
foreach (array('subacc','click_id','clickid') as $key) {
if (!empty($_REQUEST[$key])) { $binom_click_id = $_REQUEST[$key]; break; }
}
if (!$binom_click_id && isset($_SERVER['HTTP_REFERER'])) {
$ref = parse_url($_SERVER['HTTP_REFERER']);
if (isset($ref['query'])) {
parse_str($ref['query'], $rp);
foreach (array('subacc','click_id','clickid') as $key) {
if (!empty($rp[$key])) { $binom_click_id = $rp[$key]; break; }
}
}
}
if ($binom_click_id) {
@file_get_contents('https://ВАШ-ДОМЕН-ТРЕКЕРА/click?' . http_build_query(array(
'cnv_id' => $binom_click_id, 'payout' => 0, 'cnv_status' => 'lead')));
}
5. Постбеки
Подробно кто кому что шлёт — во втором уроке, здесь коротко, что куда вставить.
Партнёрка → трекер
В кабинете AdCombo, во все три поля постбека:
https://ВАШ-ДОМЕН-ТРЕКЕРА/click?cnv_id={subacc}&payout={revenue}&cnv_status={status}
Здесь {subacc}, {revenue} и {status} — макросы партнёрки. {status} обязательно оставить макросом, иначе развалится разбивка по статусам.
Трекер → сетка
В карточке источника трафика. Одной ссылки здесь мало: заявка и подтверждённый заказ — разные события, и сетке их надо отдавать по отдельности. Иначе она запомнит полную выплату уже на холде, а про отказ не узнает никогда.
Поле Postback URL — ссылка лидовой цели, с нулём вместо выплаты:
MGID: https://a.mgid.com/postback/ВАШ_CLIENT_ID?c={externalid}&e=Lead&r=0
Adskeeper: https://a.adskeeper.com/postback/ВАШ_CLIENT_ID?c={externalid}&e=new&r=0
Режим S2S — Send all. При «Send only first» апрув не уйдёт: первое событие уже потрачено на холде.
Галка Status-postback relation — и три пары «статус → ссылка»:
hold → https://a.mgid.com/postback/ВАШ_CLIENT_ID?c={externalid}&e=Lead&r=0
lead → https://a.mgid.com/postback/ВАШ_CLIENT_ID?c={externalid}&e=Lead&r=0
confirmed → https://a.mgid.com/postback/ВАШ_CLIENT_ID?c={externalid}&e=Approve&r={payout}

Выплату несёт только цель апрува. {externalid} без подчёркивания, в {payout} не потеряйте закрывающую скобку, имя в e= должно совпадать с целью в кабинете сетки. Как завести вторую цель — во втором уроке.
6. Колонки в трекере
Создавайте колонки через интерфейс: Settings → Stats → Columns → Add custom column. Тогда все внутренние связи создадутся сами.
Leads (общие) — встроенная, просто включите в списке колонок
H. Leads event_25
A. Leads event_27
R. Leads event_29
Time 15s, % event_6 / clicks * 100
Time 30s, % event_7 / clicks * 100
Scroll % event_8 / clicks
No-touch % event_10 / clicks * 100
Bots % event_11 / clicks * 100
Approve % event_27 / (event_25 + event_27 + event_29) * 100
Ловушка с процентами. Формат «%» в трекере только дописывает знак процента, он не умножает на сто. Поэтому умножение прописано прямо в формуле.
Если создаёте колонки напрямую в базе — учтите, что колонка живёт в двух таблицах: в одной формула, в другой список страниц, где её показывать. Вставите только в первую — колонка не появится, а редактор выдаст ошибку ViewOnPage not found. Через интерфейс этой проблемы нет.
7. Схема статусов
Это то, что превращает постбеки партнёрки в колонки H/A/R Leads. Настраивается в Settings → Postback processing → схема E-commerce, события с 25 по 30.
- APPLY FOR: All campaigns. Без этого схема просто не работает.
- Блок lead/hold — впишите статусы, которыми партнёрка сообщает о новой заявке. Действие: +1 к event_25.
- Условие на блок lead/hold: предыдущий статус конверсии пустой. Без этого условия каждый повторный постбек снова добавляет холд-лид, и цифра задваивается.
- Блок confirmed (если предыдущий статус был lead или hold) — минус event_25, плюс event_27, плюс выплата.
- Блоки cancelled и trash — откат: плюс event_29.
- Удалите во всех блоках действие Set 0 to conversion — тогда встроенная колонка Leads покажет все лиды, а не только апрувы. Действие Set 0 to payout оставьте: доход должен считаться только по подтверждённым.
Если правите схему не через интерфейс, а напрямую в базе — после правок перезапустите контейнер binom_traffic_distribution, он кэширует правила. При сохранении через интерфейс перезапуск не нужен.
8. Проверка перед запуском
- Все девять контейнеров трекера в статусе Up.
- Панель открывается по адресу админки, сертификат валиден.
- Адрес /click на домене кампаний отвечает Bad Request на пустой запрос.
- Тестовая страница на домене лендингов открывается по https.
- Открыли лендинг со ссылкой с click id, подождали 15 секунд, полистали — в отчёте появились события.
- Оставили тестовую заявку: в партнёрке появился заказ, в трекере строка со статусом lead.
- Через несколько минут от партнёрки пришёл статус hold — значит постбек работает.
Если что-то не заводится, не сидите сутки: у трекера есть поддержка, а у партнёрки — ваш менеджер. Обычно проблема в одном символе: перепутанный макрос, лишняя оранжевая тучка в Cloudflare или забытый chown.