RU | EN
📐

Methodology

How we collect 3 071+ jobs from 10 sources (50 Telegram channels + Greenhouse / Lever / Ashby / Workable / RemoteOK / Himalayas / Remotive / WeWorkRemotely / Jobicy / Habr Career / Partnerkin / Arbihunter), merge duplicates, tag them, and double-check anything that shouldn't have passed automation.

📥 Where the jobs come from

We collect from 10 independent sources. The biggest one is Telegram — 50 public channels (of those, 23 are HR channels of the CPA industry). On top of that we read four ATS platforms (Greenhouse, Lever, Ashby, Workable), six remote-job boards (RemoteOK, We Work Remotely, Himalayas, Remotive, Jobicy, Habr Career), and two CPA-specific aggregators (Partnerkin, Arbihunter). We have no partnership with any of them — these are public sources crawled once an hour.

Telegram (the core). The channel list has been built by hand since 2018. We have watched the CPA community for a long time and only add a new channel after manual screening. Simple rule: in 30 days the channel must publish at least 5 real job ads with a specific company, role and contacts. If a channel is 80% reposts of paid courses, crypto-wallet ads or «buying FB account» posts — pass. Today the active HR list looks like this:

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

Plus another 30 channels in categories arbitrage, marketing, IT, general and geo-relocation (digital_jobster, dnative_job, hrcpa, hrcpagram, job_cpa, getitrussia, gjobs, dubai_work24, and so on). An AI classifier reads every message and decides whether it is actually a job — that lets us tolerate noisier sources without flooding the catalog.

ATS / job boards. For non-Telegram sources we don't scrape HTML — we use official feeds:

  • Greenhouse, Lever, Ashby, Workable — open ATS endpoints. We watch a curated list of affiliate / iGaming / crypto / nutra employers and pick up new openings automatically.
  • RemoteOK, We Work Remotely, Himalayas, Remotive, Jobicy — remote job boards. We filter to our niche (affiliate marketing, growth, performance, paid acquisition, crypto, iGaming).
  • Habr Career — Russian-speaking tech jobs; we keep only affiliate / arbitrage / SEO / growth roles.
  • Partnerkin, Arbihunter — dedicated CPA-industry job aggregators.

Affiliate-niche filter. ATS sources naturally contain a lot of unrelated jobs (engineers, lawyers, designers without affiliate context). Every incoming posting passes through a niche filter — a 40+ pattern Russian/English whitelist plus a blacklist. In the last sweep we dropped ~8,000 vacancies that have nothing to do with our audience and kept the ~1,700 that actually matter to affiliates.

Two-way RU↔EN translation. Telegram channels publish mostly in Russian, ATS platforms — mostly in English. We translate each job into the second language via an LLM call (the same call also extracts the structured fields), so that /en/ and the default Russian sections cover the same catalog.

Our outbound network. After moderation each fresh job is also routed into our own network of 13 city Telegram channels (Moscow, Saint Petersburg, Dubai, Limassol, Tbilisi, Almaty, Yerevan, Minsk, Belgrade, Istanbul, Tashkent, Berlin, and a global feed). Geography of the role decides which channel gets the post — this is publishing, not parsing, but it closes the loop: source → catalog → city channel. Every job card on the site shows the primary source — which channel or platform first posted it and when.

🔧 How the parser works

Sitting on 50 channels from a single account is a fast track to a ban — Telegram treats that as suspicious activity. Instead we run a pool of 15 accounts through the Telethon library (Python). They take turns round-robin: one account pulls posts from five channels, the next pulls from another five, and so on. ATS and job-board sources are handled by separate workers — each platform has its own scraper that talks to the platform's public feed (HTML or JSON) and does not touch Telegram at all.

Every 60 minutes the parser walks the feed, picks up new posts, runs them through a set of regular expressions and writes the results into the careers.jobs table. If a channel posted nothing new — we skip it. If it posted spam or an «account for sale» ad — we filter that out at the very first stage. For ATS sources we additionally run an affiliate-niche filter (whitelist + blacklist) so that an Anthropic backend engineer never ends up in our catalog.

The parser does not publish, does not reply in private chats, does not like posts. It only reads public channels and public ATS endpoints — exactly what any subscriber does when they open Telegram or a careers page in the morning.

🔄 Deduplication — why one job does not show up 14 times

CPA agencies love to fan a job out: first to their friends in @hrcpa, then to @aff_job, then to @works_cpa, then to three more HR chats just in case. The same text floats up 5 to 15 times. Add to that the fact that many employers also list the same role on Greenhouse, Lever or RemoteOK — and one «Media Buyer, FB Gambling» job can have a dozen origin points across Telegram + ATS. On the site that would look like trash.

We compute a canonical hash from normalized content: strip emoji, lowercase everything, drop whitespace and punctuation, and keep only the meaningful words in the title, company name and description. If the hash matches an existing record — it is a duplicate. We bump the existing record's source_count and add the new source to its «also published in» list. Cross-source matching is the same — a Telegram post and a Greenhouse entry from the same company collapse into one card.

A real example from last week. Channel @aff_job posted «Looking for a Media Buyer, FB Gambling, budget from $50K, remote». Channel @works_cpa publishes the very same text letter-for-letter with a different emoji in the title. Channel @hrcpa adds «UPD: urgent, need someone today». The hash function strips the emoji and the UPD prefix, sees the identical remainder, and merges them into one record with source_count=3.

Today the catalog holds about 3,100 archived duplicates that never showed up as separate cards thanks to dedup. Without it the feed would be roughly 2–3 times longer and the bulk of it would be repeats.

🏷 Classification — what we pull out of the text

A raw post can be anywhere from 200 characters («Need a buyer, +$») to 5,000 («Full role description, KPIs, offer, quarterly performance bonuses, our company story over two screens»). For the filter to work, we extract a clean structure:

  • Vertical — iGaming, Crypto, Nutra, Finance, E-commerce, Agency. Stored as an array in vertical_tags: a single job can cover iGaming + Crypto at the same time.
  • Role — Affiliate Manager, Media Buyer, BizDev, Developer, Designer, HR Recruiter, Sales. Stored in role_tags.
  • Seniority — junior / middle / senior / lead. One level per job.
  • Format — remote / office / hybrid.
  • Location — we normalize «Cyprus, Limassol» and «Limassol Cyprus» into a single record.
  • Salary — we try to extract the range, currency and period (hour / day / month / year). If the text says «$3000–5000 net» we record 3000–5000 USD/month.
  • Skills — the list of tools, stored in external_data->skills (FB Ads, Keitaro, Bemob, Voluum, Binom, etc).

All of this is extracted via two paths. The first is a regex parser: cheap, fast, rarely wrong on standard patterns («Salary from $3000» → 3000 USD). The second is a fallback to an LLM, used when regex returns low confidence or finds nothing at all (dense unstructured prose).

After extraction each job gets a quality_score between 0 and 100. The more fields are filled in (salary, location, contacts, description over 200 characters, normalized company name), the higher the score. On the homepage we surface those with quality_score ≥ 70.

🤖 AI moderation — where regex stops and Claude takes over

Regex handles a clean format well. It does not handle masked spam, hidden course promos and résumé posts from job seekers. So everything where the parser is uncertain goes into a queue for the LLM (we use Claude Sonnet from Anthropic).

The prompt is strict: the model must return JSON with one of three decisions — publish, reject, review. Plus a short justification (for logs and manual audit). No creative free-form replies.

A case from March. Channel @aff_job on March 12 posted: «Looking for a team lead on a casino traffic team, salary $25K, remote, no experience required». The parser flagged low confidence — no company, no specifics, no reasonable experience requirements. Claude Sonnet returned reject with the rationale: «the phrasing 'no experience required for $25K with no duties listed' is a typical pattern for harvesting contacts or laundering a candidate database; the rate does not match the market for a junior role». The decision was applied automatically and the job never entered the public feed.

In March LLM moderation rejected roughly 7% of the posts that had already passed the regex filter. Among them: disguised course ads («media buying course for $99, we will place you in a job after graduation»), résumé-harvesting attempts («send your CV to us in DM, we will forward it to the employer»), and obvious pyramid schemes.

Decisions with ≥75% confidence (publish) or ≥80% confidence (reject) are applied automatically. Anything below that goes to manual moderation. The manual queue is reviewed once a day, usually with 5–15 cases waiting.

⏰ Life cycle — why jobs disappear after 30 days

Every job lives 30 days from its last appearance in the source. If it stops appearing — we close it automatically and switch it to status=expired. The old page is still reachable by direct link, but gets noindex,follow and drops out of search.

This window is not pulled out of thin air. We looked at the typical lifespan of jobs in iGaming/CPA: companies keep an active hiring round open for 21–28 days on average, then either fill the role or revise the requirements. 30 days is a comfortable buffer that avoids closing a live position too early.

A real example. The job «Media Buyer FB Gambling CTEAM» was published on February 1. Over the next 30 days it was republished across our sources multiple times — meaning the company is still hiring. We keep it in active with a bumped source_count. If 30 days had passed without a single source mentioning it again — it would have moved itself to the archive.

Right now the database holds: 3 071 active jobs and roughly 6 072 archived across the entire history since late 2024. The archive stays for retrospective use — to see which companies were hiring for what, what salaries the industry was paying. But the main feed only shows live positions.

⚖️ What we DO NOT do

Job catalogs have many ways to monetize traffic on top of the core product. We reject all of them:

  • We do not charge employers for posting or for priority placement. All jobs are equal, ranking is strictly by quality_score and freshness.
  • We do not take a placement fee. We are not an HR agency — we are a catalog. No «$2K per placement» from the company side.
  • We do not charge candidates. Browsing, applying, any activity — free. We will never put up a paywall to view contact details.
  • We do not publish «grey-market» jobs — account farming, multi-accounts, cash-out schemes, money muling — even when such ads are present in the source channel.
  • We do not show Google AdSense or third-party banner ads inside job cards, and we do not mix in native ads.
  • We do not run spam campaigns to candidates on Telegram. If we have your contact — it is only for notifications on subscriptions you have explicitly opted into.
  • We do not pass company contact details to third parties. Whatever is in a public card was already public in the source channel.

This is not a moral stance, it is a technical choice: adding any one of these would turn the catalog into yet another junk site full of inflated salaries and fake companies within a month. There are plenty of examples of how that goes.

👥 Who runs this

Affiliate.Careers is part of an independent project built by an affiliate-marketing community. No outside investors, no advertising contracts with specific CPA networks.

The team works by role: platform engineering (Laravel + Postgres + Telethon parser), moderation of edge cases, maintaining the list of source channels, user support. Everything is small and non-corporate by design. The smaller the team, the lower the temptation to sell «priority placements» or accept «partnership offers» from dubious recruiters.

Contact for corrections, job removal, spam complaints — at the bottom of the page.

🛠 If an employer asks to remove a job

It happens: a company has closed the role, does not want to be visible, changed its mind. Our procedure is:

  1. We verify the request comes from someone connected to the company. The simplest way — a message from the same Telegram channel where we picked up the job (if @hrcpa asks for removal — they are the ones who published it). An alternative — an email from an address on the company's domain.
  2. If the connection is confirmed — we remove the job manually within 24 hours. Fully removed, not «hidden»: the record is deleted from careers.jobs and the page starts returning 410 Gone.
  3. If the connection is not confirmed — we reply with the specific proof we need. We do not remove jobs based on requests from unrelated accounts — otherwise the scheme degrades into «found a competitor's job — write to have it removed».

A removed job does not come back even if it is republished in the sources. The record is tagged removed_by_request=true, and the parser ignores the canonical_hash on any reappearance.

📩 Found a problem?

A garbled description, a typo, a job that has actually closed but is still showing as active, or just plain «this is not a job, this is spam»? Drop us a line on the contact page — we respond within an hour during working hours.