RU | EN
📐

Методология

Как мы собираем 600+ вакансий из 27 Telegram-каналов, склеиваем дубли, размечаем и проверяем то, что не должно было пройти автоматически.

📥 Откуда берём вакансии

27 Telegram-каналов с вакансиями для CPA-индустрии. Никакого «партнёрства» с этими каналами у нас нет — это публичные источники, которые мы парсим раз в час.

Список собирался руками с 2018 года. Мы наблюдаем за CPA-сообществом давно и каждый новый канал добавляем только после ручного отсева. Простое правило: за 30 дней должно выйти 5+ настоящих вакансий с конкретной компанией, ролью и контактами. Если канал на 80% состоит из репостов курсов, рекламы криптокошельков или объявлений «куплю акк ФБ» — мимо.

Текущий список (категория HR):

@adhunt_cpa_job, @aff_hunter, @aff_job, @arbitrage_work, @arbitrazh_vakansii_rabota, @be01team, @cpa_traffic_hr, @gamblingservices, @headshotagency, @hr_affiliate, @hr_arbitraz, @hr_b00st, @hrcpa, @hrcpagram, @job4aff, @job_cpa, @kashjob, @mediabuyers_lenkep, @opento_igaming, @partnerkin_job, @pro_vacancy, @r2bwork, @recruitingtc, @talents, @traff_job, @works_affiliate, @works_cpa

Среди них есть и официальные HR-аккаунты компаний (Parimatch Talents, LENKEP, Партнеркин), и независимые HR-агентства (CPA Hunter, Headshot, Adhunt), и агрегаторы (CPA WORK, Affiliate Work). Каждая карточка вакансии на сайте показывает первоисточник — какой канал и когда её опубликовал.

🔧 Как устроен парсер

Сидеть с одного аккаунта на 27 каналах — путь к бану. Telegram считает это подозрительной активностью. Вместо этого мы используем пул из 15 аккаунтов через библиотеку Telethon (Python). Они работают по очереди (round-robin): один забирает посты с пяти каналов, второй с других пяти, и так далее.

Каждые 60 минут парсер обходит ленту, забирает новые посты, прогоняет их через регулярки и записывает в таблицу careers.jobs. Если канал ничего нового не выложил — пропускаем. Если выложил спам или объявление о продаже акка — отсеиваем ещё на этапе фильтра.

Парсер не публикует сам, не отвечает в личные сообщения, не лайкает посты. Только чтение публичных каналов — то же самое, что делает любой подписчик, открывая свой Telegram утром.

🔄 Дедупликация — почему одна вакансия не появляется 14 раз

CPA-агентства любят рассылать вакансию веером: своим в @hrcpa, потом в @aff_job, потом в @works_cpa, потом ещё в трёх HR-чатах для подстраховки. Один и тот же текст всплывает буквально 5–15 раз. На сайте это смотрелось бы как мусор.

Мы считаем canonical hash от нормализованного содержимого: убираем эмодзи, нижний регистр, пробелы и пунктуацию, оставляем только осмысленные слова заголовка, компании и описания. Если хеш совпадает с уже существующей записью — это дубль, увеличиваем у неё source_count и добавляем источник в список «где ещё опубликовано».

Реальный пример с прошлой недели. Канал @aff_job 14 апреля написал «Ищем Media Buyer FB Gambling, бюджет от $50K, удалёнка». Канал @works_cpa 15 апреля выкладывает буква-в-букву тот же текст с другим эмодзи в заголовке. Канал @hrcpa 16 апреля добавляет «UPD: горящая, нужен сегодня». Хеш-функция убирает эмодзи и UPD-приписку, видит идентичный остаток — склеивает в одну запись с source_count=3.

Сейчас у нас в базе есть вакансия с source_count=24 (одна и та же позиция Media Buyer всплыла в 24 каналах за месяц), несколько с source_count=15. Без дедупа лента вакансий была бы примерно в 2–3 раза длиннее и состояла на 60% из повторов.

🏷 Классификация — что мы извлекаем из текста

У сырого поста длина варьируется от 200 знаков («Нужен байер, +$») до 5000 («Полное описание роли, KPI, оффер, перформанс-кварталки, наша история на 2 экрана»). Чтобы фильтр работал, мы вытаскиваем структуру:

  • Вертикаль — iGaming, Crypto, Nutra, Finance, E-commerce, Agency. Хранится массивом в vertical_tags: одна вакансия может покрывать iGaming + Crypto одновременно.
  • Роль — Affiliate Manager, Media Buyer, BizDev, Developer, Designer, HR Recruiter, Sales. В role_tags.
  • Сениорити — junior / middle / senior / lead. Один уровень.
  • Формат — удалёнка / офис / гибрид.
  • Локация — нормализуем «Кипр, Лимассол» и «Limassol Cyprus» в одну запись.
  • Зарплата — пытаемся вытащить вилку, валюту и период (час/день/месяц/год). Если в тексте «$3000–5000 net» — фиксируем 3000–5000 USD/month.
  • Скиллы — список инструментов из external_data->skills (FB Ads, Keitaro, Bemob, Voluum, Binom, и т.д.).

Всё это извлекается двумя путями. Первый — regex-парсер: дёшево, быстро, ошибается редко на стандартных конструкциях («ЗП от 3000$» → 3000 USD). Второй — fallback на LLM, когда regex выдаёт низкую уверенность или вообще ничего не нашёл (плотный текст без структуры, длинный prose).

После извлечения каждая вакансия получает quality_score от 0 до 100. Чем больше полей заполнено (зарплата, локация, контакты, описание свыше 200 знаков, нормализованная компания), тем выше скор. На главной мы поднимаем выше те, у кого quality_score ≥ 70.

🤖 AI-модерация — где останавливается regex и подключается Claude

Regex отлично справляется с понятным форматом. Не справляется с замаскированным спамом, скрытым промо курсов и резюме-постами от соискателей. Поэтому всё, что у парсера в зоне неуверенности, едет в очередь к LLM (мы используем Claude Sonnet, провайдер Anthropic).

Промпт строгий: модель должна вернуть JSON с одним из трёх решений — publish, reject, review. Плюс короткое обоснование (для логов и ручного аудита). Никаких креативных свободных ответов.

Кейс из марта. Канал @aff_job 12 марта выложил пост: «Ищем тимлида в команду по сливу казино, ЗП $25K, удалёнка, без опыта». Парсер дал низкую уверенность — нет компании, нет конкретики, нет адекватных требований к опыту. Claude Sonnet отдал reject с обоснованием: «формулировка „без опыта на $25K без описания обязанностей" — типичный паттерн вытягивания контактов либо отмыва базы кандидатов; ставка не соответствует рынку для junior-уровня». Решение применилось автоматически, вакансия не попала в публичную ленту.

За март LLM-модерация отбросила примерно 7% постов из тех, что прошли regex-фильтр. Среди них: переодетые рекламы курсов («курс по медиабаингу за $99, после прохождения трудоустроим»), попытки собрать резюме («пришли свой опыт нам в личку, мы перешлём работодателю»), и явные пирамидки.

Решения с уверенностью ≥75% (publish) или ≥80% (reject) применяются автоматически. Всё ниже — в ручную модерацию. Ручная очередь смотрится раз в день, обычно там 5–15 кейсов.

⏰ Жизненный цикл — почему вакансии исчезают через 30 дней

Каждая вакансия живёт 30 дней с последнего появления в источнике. Не появилась — закрываем автоматически, переводим в status=expired. Старая страница остаётся доступной по прямой ссылке, но получает noindex,follow и пропадает из поиска.

Этот срок выбран не от потолка. Мы смотрели типовое время жизни вакансии в iGaming/CPA: компании держат активный поиск в среднем 21–28 дней, потом либо находят кого-то, либо пересматривают требования. 30 дней — комфортный буфер, чтобы не закрыть живую позицию слишком рано.

Реальный пример. Вакансия «Media Buyer FB Gambling CTEAM» опубликована 1 февраля. За следующие 30 дней её переопубликовали в наших источниках 19 раз — значит компания всё ещё ищет. Мы держим её в active с source_count=19. Если бы 30 дней подряд ни один источник её не упомянул — она бы сама ушла в архив.

Сейчас в базе: 563 активных вакансии и около 2 800 архивных за всю историю с конца 2024 года. Архив остаётся для ретроспективы — посмотреть, какие компании что искали, какие зарплаты были в индустрии. Но в основной ленте показываем только живое.

⚖️ Что мы НЕ делаем

У job-каталогов есть много способов заработать поверх трафика. Мы все эти способы отвергаем:

  • Не берём комиссию с работодателей за публикацию или приоритет в выдаче. Все вакансии равны, ранжирование строго по quality_score и свежести.
  • Не берём процент с найма. Мы не HR-агентство, мы каталог. Никаких «$2K за placement» с компании.
  • Не берём оплату с кандидатов. Регистрация, отклики, любая активность — бесплатна. Никогда не введём pay-wall за просмотр контактов.
  • Не публикуем «серые» вакансии — фарм аккаунтов, мульты, обнал, дроп-схемы — даже если такая вакансия есть в исходном канале.
  • Не показываем Google AdSense, баннерную рекламу третьих сторон в карточках вакансий, не подмешиваем туда нативку.
  • Не делаем спам-рассылки кандидатам в Telegram. Если у нас есть твой контакт — он только для уведомлений по подпискам, на которые ты сам подписался.
  • Не передаём контакты компаний третьим сторонам. Всё, что есть в публичной карточке, и так публично в источнике.

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

👥 Кто за этим стоит

Affiliate.Careers — часть независимого проекта сообщества арбитражников. Никаких внешних инвесторов, никаких рекламных контрактов с конкретными CPA-сетями.

Команда работает по ролям: разработка платформы (Laravel + Postgres + Telethon-парсер), модерация спорных кейсов, ведение списка каналов-источников, поддержка пользователей. Всё небольшое, не корпоративное — это сознательно. Чем меньше компания, тем меньше соблазнов продавать «приоритетные размещения» или принимать «партнёрские предложения» от сомнительных рекрутеров.

Контакт для коррекций, удаления вакансий, жалоб на спам — внизу страницы.

🛠 Если работодатель просит удалить вакансию

Бывает: компания закрыла позицию, не хочет светиться, передумала. Алгоритм у нас такой:

  1. Проверяем, что обращение исходит от человека, имеющего отношение к компании. Самый простой способ — связь через тот же Telegram-канал, откуда мы взяли вакансию (если канал @hrcpa просит удалить — значит это они её и публиковали). Альтернатива — письмо с email-адреса домена компании.
  2. Если связь подтверждена — удаляем вручную в течение 24 часов. Полностью, не «скрываем», а удаляем запись из careers.jobs и страница начинает отдавать 410 Gone.
  3. Если связь не подтверждается — отвечаем, какие именно подтверждения нужны. Не удаляем по обращениям из левых аккаунтов — иначе схема превращается в «у конкурента нашлась вакансия — пишу, чтобы убрали».

Удалённая вакансия не возвращается даже если её заново опубликуют в источниках. У записи фиксируется removed_by_request=true, и парсер игнорирует canonical_hash на повторном появлении.

📩 Нашли проблему?

Кривое описание, опечатка, ушедшая вакансия которая всё ещё активна, или вообще «это не вакансия, это спам»? Напиши на страницу контактов — мы реагируем в течение часа в рабочее время.