cd /news/ai-agents/job-search-in-claude-code-half-of-us… · home › topics › ai-agents › article
[ARTICLE · art-158660] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Job search in Claude Code: half of US backend postings were copies

A developer ran a job search from Claude Code on 27 October 2026 against a database of LinkedIn postings, using three Apify Actors connected through Apify's MCP server, and found that roughly half of US backend postings were duplicates. The test persona was a London backend developer with five years of Python, and the search used a free preview mode that returns posting counts and breakdowns by country and title instead of rows.

by read16 min views4 publishedOct 27, 2026

The job-search threads on this site read like the same ledger. Seventy-two applications, twenty-three rejections, two offers in one senior engineer's two-month search. Seventy-three applications and sixty-one silences in another. A rejection two minutes after pressing submit. Postings that do not feel real, or have sat open for months.

The advice under those posts is usually "apply more" or "tailor more". Both make the queue longer. What the threads describe is a queue problem: the same role listed many times, the good roles buried under two hundred applicants by the end of the week, a form in front of every person who could say yes.

On 27 October 2026 I ran a developer job search from Claude Code over a database of LinkedIn job postings, to see how much of that queue you can step around. The persona is ordinary on purpose: a backend developer in London, five years of Python (Django, FastAPI), some Go, Postgres and Kubernetes, looking for a mid-senior role. The tools are three Apify Actors over the same jobs database: LinkedIn Jobs Database API for the search, Companies Hiring Now for the employers, and Advanced LinkedIn Jobs Scraper when you want the same rows with the field names of the best-known scraper. No LinkedIn login, no cookies, no browser.

Every number below comes from that day's runs. The database holds the last 180 days of postings and is loaded in waves; on 27 October it ran to 21 October, so "the last 30 days" means 25 days of postings. Rows shown here carry no names of people and no names of employers.

Apify runs an MCP server at https://mcp.apify.com; the tools parameter pins the Actors a session may reach. In Claude Code:

claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=b2bsearch/linkedin-job-search-api,b2bsearch/hiring-companies-finder,b2bsearch/linkedin-jobs-scraper"

Any other MCP client (Claude Desktop, Cursor, VS Code, ChatGPT) takes the same URL, with OAuth on first connect or an Authorization: Bearer <APIFY_TOKEN> header:

{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=b2bsearch/linkedin-job-search-api,b2bsearch/hiring-companies-finder,b2bsearch/linkedin-jobs-scraper"
    }
  }
}

The Actors appear as tools named b2bsearch--linkedin-job-search-api, b2bsearch--hiring-companies-finder and b2bsearch--linkedin-jobs-scraper (the slash becomes a double hyphen). The server adds get-actor-run and get-dataset-items next to them: a tool call returns the run and its datasetId, and the rows come from get-dataset-items. Without MCP, the same Actors run from the Apify API, the Python and JavaScript clients, n8n, Make or Zapier.

Two of the steps below were run by Claude Code itself, from a plain-English request, in sessions I recorded; the inputs quoted there are the ones it built. The rest I typed into the same fields, because the point there is the field.

The search Actor has a preview mode that returns the count and a breakdown instead of rows, and it is not billed. One preview per country for backend titles (backend engineer, backend developer, software engineer backend), last 30 days:

{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "previewOnly": true
}
Backend postings, 25 days to 21 Oct US UK Germany Netherlands India
Postings 2,244 316 266 97 573
Distinct roles (employer + title) 1,126 266 216 92 463
Salary stated 14 % 15 % 12 % 19 % 5 %
Recruiter named on the posting 7 % 22 % 11 % 31 % 17 %
Under 25 applicants 43 % 35 % 40 % 35 % 20 %
Over 200 applicants 18 % 15 % 15 % 20 % 46 %

Three things in that table change what you do next. Almost half of the Indian backend postings already had more than 200 applicants when they were recorded, so the queue there starts long. The Dutch postings name the recruiter one time in three, the American ones one time in fourteen. And the American market is half the size it looks, which is the next section.

One trap before it: the title is not always in English. In Germany, softwareentwickler alone matched 470 postings in the same 25 days, more than every title with the English word backend in it (336). Search the words the market writes, in its language.

Of the 2,244 American backend postings, one employer posted 609: twenty titles, fifteen of them listed 30 times or more, once per city (Atlanta, Dallas, Miami, Phoenix and on). A consulting firm posted two titles 81 and 78 times, each ending in "Location OPEN". Grouped on employer and title, the 2,244 postings are 1,126 roles. In the United Kingdom the same grouping removes 16 % of the postings, in the Netherlands 5 %.

Each row carries jobId, companyId and title, so the grouping is one line in a spreadsheet or one sentence to Claude: "keep one row per companyId and title, the newest". Two more kinds of noise are worth naming:

removeAgency: true drops staffing firms, aggregators and employers that post far more roles than they have people. While writing this I found that six of the 25 top "employers" in the British list were still recruiting firms: their postings carry the client's industry, not their own. Since build 0.1.3 the rule also reads the employer's own card and its 30-day posting pace, and the same list came back with none of them; since 0.1.6 it also reads what the card says the employer does (section 8 has the measurement). A firm whose card does not say it recruits still gets through, so read the titles that sell ("AI Startup, up to £120k"). A backend title says nothing about Django or Go. The text does, and the search Actor can filter on it: put the role in titleSearch and the technology in descriptionSearch.

{
  "titleSearch": ["backend"],
  "descriptionSearch": ["django", "fastapi"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "previewOnly": true
}
Postings with "backend" in the title, 25 days All Text mentions
UK 327 34 Django or FastAPI, 24 golang
Netherlands 103 39 Kubernetes
Germany 336 27 Django or FastAPI

Thirty-four roles is a week of good applications, not a month of bad ones. The text filter reads the postings the other filters leave, at most 500 of them, so it needs a narrow cut: title words plus a country and at most 30 days, or one employer. "Kubernetes anywhere in Germany this month" is refused before anything runs, with a note on how to narrow it.

For a fit judgement that is more than one keyword, there is a better split of labour, and the agent session in section 8 used it: let the database apply the filters it can apply exactly (title, country, size, agencies), pull 40 rows with the posting text, and let Claude read the text.

The applicant count is recorded with the posting, in buckets of 25. In the British backend search with agencies out, 99 of 295 postings had fewer than 25 applicants. Of the 33 posted on the last two days in the database, 26 did.

{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "datePostedAfter": "2026-10-20",
  "maxApplicants": "25",
  "removeAgency": true
}

That is the timing problem from the threads above in numbers: a fresh posting is a short queue, and the queue grows within days. The loop that follows from it is a daily run with datePostedAfter set to the dataAsOf date of the previous run (every row carries it in _input), deduplicated on jobId.

One honest limit. The database is loaded in waves, not every hour, so a "last 24 hours" window run between two loads finds nothing. On 27 October it did exactly that for Germany. Since build 0.1.3 the empty row says why, with the date of the newest posting and what to change, instead of "no job matches". Anchor the loop on dataAsOf, not on the clock.

Two filters decide where a cover letter is worth writing. salaryMin keeps postings that state a salary at or above a yearly amount in a currency; recruiterOnly keeps postings that name the recruiter or hiring manager, whose name, headline and profile link then ride the row.

UK backend, agencies out, 25 days Postings Employers Also name a recruiter Also state a salary
All 295 170 63 42
Salary stated, at least £80,000 34 27 18 34
Recruiter named 63 47 63 21

Postings that name a recruiter state a salary one time in three; the rest, about one time in eleven. A posting that names a person and a number is a posting written by someone who wants to be found.

Companies Hiring Now turns the same search into employers, most matching postings first, each with its hiring pace: open postings, and postings in the last 7, 30 and 90 days.

{
  "jobTitles": ["backend", "\"software engineer\""],
  "locations": ["United Kingdom"],
  "postedLimit": "month",
  "maxCompanies": 25
}

The search Actor puts the same pace on every job row with employerHiring: true (employerOpenJobs, employerJobs7d, employerJobs30d, employerJobs90d, employerTopTitles30d). Two postings from the British pull show why it matters. A 95-person company hiring a senior backend engineer had 19 roles open and had posted 26 in 30 days: a team being built. A 152-person company hiring for the same title had posted 3: a seat being filled. Both are real jobs, and they call for different notes: the first is a conversation about the team, the second about the one problem the hire is for.

The same columns help with the postings that do not feel real. A role that has been open for months at an employer that has posted nothing else in 90 days is not proof of a ghost job, but it is the one to apply to last.

The most useful part of a hiring signal is the person behind it. With decisionMakers: true every employer in the result is followed by its leaders as rows of their own (_type: decision_maker), ranked founder and CEO first, each with title, headline, profile link and the job they belong to (forJobId, forJobTitle). For a backend developer the right people are the engineering leaders of small companies, where the CTO still reads their own messages:

{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "removeAgency": true,
  "organizationEmployeesGte": 11,
  "organizationEmployeesLte": 500,
  "employerHiring": true,
  "decisionMakers": true,
  "decisionMakerRoles": ["founder", "cxo", "vp", "director"],
  "decisionMakerTitleKeywords": ["cto", "engineering", "technology", "technical"],
  "decisionMakersPerCompany": 2,
  "descriptionType": "text",
  "limit": 20
}

Twenty postings at 16 employers came back with nine engineering leaders: three CTOs, two co-founders who are the CTO, a VP of Engineering, two Directors of Engineering and a Director of Technical Product. Nine employers had nobody on record at those roles; those are free note rows, not empty charges.

The first time I ran it, the leaders were a Sales Director, a Claims Director and a Director of Casino. Title keywords match as text inside the title, and "cto" sits inside "Director". Since build 0.1.4, C-level acronyms (cto, coo, cfo and the rest) match as whole words, so cto finds "Co-Founder & CTO" and not "Director".

The decision-maker rows on the job listings carry the profile link, not a mailbox. If you want an address, Decision Maker Email Finder by Domain takes the employer's companyDomain from the job row and returns the leaders with the email on record.

Finding the role is half the work; the other half is a short note to a person, and then the next one. Both are text, and both are where Claude is useful, as long as it drafts and you send.

I gave a fresh Claude Code session, connected only to the Apify tools above, a plain request: my stack (Python with Django and FastAPI, some Go, Postgres, Kubernetes), London, a mid-senior backend role at a company of 11 to 500 people. Count first and stop above 400; then pull 40 jobs with the posting text and the hiring pace, without the description filter, and read the texts yourself; give me the eight that fit, with the stack words quoted from each posting.

The session took 11 turns and three minutes. The pull it built:

{
  "titleSearch": ["backend", "back end", "back-end"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "removeAgency": true,
  "organizationSizeFilter": ["11-50", "51-200", "201-500"],
  "limit": 40,
  "descriptionType": "text",
  "employerHiring": true
}

The preview counted 370 postings. The table it returned had one row per role, each with a quoted stack line such as "Strong Python and Django experience … Solid PostgreSQL expertise and AWS experience" or "We are a Go shop … PostgreSQL, MongoDB, RabbitMQ, Redis, gRPC … AWS and Kubernetes". Six of the eight had fewer than 25 applicants. It picked a 152-person lender first (its posting matched the stack almost word for word, stated the pay in the text, and the employer had posted only three roles in 30 days, "a specific hire rather than a mass campaign"), and a 311-person savings app second.

A second session got the dataset from section 7 (twenty postings, nine engineering leaders) and this brief: pick the three postings that fit the stack and have a decision-maker row; for each, at most 90 words, one detail from the posting, one from the hiring pace if it helps, one sentence of matching experience using only what I said, one ask, no flattery; placeholders instead of real names; under each draft, the fields used. One of the three, as written:

Hi {first_name},

I saw the Senior Backend Engineer role at {company}: backend services for the whole lending lifecycle, from credit decisioning to collections, with integrations into credit bureaus and KYC/AML providers. The posting asks for Python, Django and PostgreSQL. I have five years of Python with Django and FastAPI, some Go, plus Postgres and Kubernetes, and I'm based in London. Could we have a 15-minute call, or could you point me to whoever is running this hire?

That is the whole method: one fact from the posting, one true sentence about you, one small ask, sent to a person instead of a form.

The session also did three things I had not asked for, and they are the reason to keep a human on the send button. It left the hiring pace out of two drafts because the numbers said nothing about engineering (one employer's 60 postings in 30 days were mostly operations roles, which it read from employerTopTitles30d). It flagged a leader whose title said VP of Engineering while their headline named a different company, and another whose record the Actor marks as last refreshed before 2025 (_note), with "confirm the role before you write". And it pointed out that one role asked for six years against my five.

Once the pieces work by hand, the routine is short:

datePostedAfter set to the previous run's dataAsOf, deduplicate on jobId and on companyId + title, and append the new rows to a tracker (a CSV in the repo, a Google Sheet, a Notion database). An Apify schedule plus a webhook into n8n or Make does this without a laptop open.status: "expired" and expiredAfter set to last week, and close the rows that match. In the United Kingdom, 525 backend postings closed or passed their end date after 1 October; waiting on any of them is time spent. The first session got a looser brief (filter by the stack, then pull 15). It put the stack into descriptionSearch next to an employer-size bound, and the pull crawled: every page read posting texts, and the size bound skipped postings and asked for more pages. Nine minutes in, with 14 rows delivered and the turns used up, I stopped it. Two things in that session were worth keeping. It excluded four employers by id after spotting them in the preview's employer list as agencies, without being asked. And the second session noticed that the preview count did not change when the size bound was added, which is true: the bound is applied to the rows. Since build 0.1.5 the preview says so in its note.

Reading 40 rows, the second session also set 16 aside as recruitment agencies that the agency filter had let through: firms with an empty industry whose own description reads "specialist search firm" or "recruitment consultancy". Since build 0.1.6 the filter reads that description too. The same input on the new build returned 2 agency rows instead of 14 by the same text check, both from one firm whose card does not say what it does.

The prompt pattern that worked: count first with a stop rule, let the database apply what it applies exactly, let the model read the text, ask for the fields used, and never let it send.

backend AND remote or backend AND hybrid in the UK found 24 postings, a sample of the ones that say it in the title. To help someone find a job: run b2bsearch/linkedin-job-search-api with previewOnly: true and titleSearch (role words, boolean allowed, one line per variant, include the local language's titles), locationSearch (country names or codes, US states, cities), timeRange (30d for a market view), removeAgency: true; read _total and the market rows (dimension, value, count). Then run it without previewOnly, with limit set to what the person will read (20 to 50), descriptionType: "text", employerHiring: true, and organizationEmployeesGte/ Lte for company size; the size bound applies to rows, not to the preview count. Use descriptionSearch only with a title and a country and at most 30 days; otherwise read the description column yourself. Deduplicate on companyId + title. Rank by fit from the text, then applicantRange (<25 applicants first), postedAt, employerJobs30d. For the people to write to, add decisionMakers: true with decisionMakerRoles and decisionMakerTitleKeywords; C-level acronyms match as whole words. Rows with _status other than found are free notes and explain themselves in _error or _note. For a daily feed, set datePostedAfter to the previous run's _input.dataAsOf; for closed roles, run status: "expired" with expiredAfter. Never send a message for the person: draft it, show the fields you used, and let them send. Agent setup: apify.com/agents.md, Apify MCP server docs.

This is the first post of a series on developer job search with Claude Code. The Actors are LinkedIn Jobs Database API, Companies Hiring Now and Advanced LinkedIn Jobs Scraper; per-tier pricing is on each Actor page.

── more in #ai-agents 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/job-search-in-claude…] indexed:0 read:16min 2026-10-27 · —