Automating LinkedIn Job Applications with AI and Selenium
I was spending 2-3 hours daily on LinkedIn. Scrolling, reading descriptions, deciding if a job matched my skills, researching salary, writing cover letters. Repeat for 30 jobs. Most weren't even relevant.
So I built a pipeline to do it for me.
The Pipeline
The system works in stages. Each stage filters and enriches the data so the next stage has less noise to deal with.
LinkedIn Scrape → Filter → AI Match Score → Salary Estimate → Cover Letter → Apply
500+ ~80 ~25 ~25 ~25 ~25
From 500 scraped listings, about 25 survive to the application stage. That's a 95% noise reduction rate.
Scraping LinkedIn Without Getting Banned
LinkedIn hates scrapers. They detect automation through mouse patterns, scroll speed, and session anomalies. My approach:
# Selenium with undetected-chromedriver
from undetected_chromedriver import Chrome
driver = Chrome(headless=False) # headless gets detected faster
driver.get("https://linkedin.com/jobs")
# Human-like delays between actions
import random
time.sleep(random.uniform(2.1, 4.7))
The key decisions that kept me undetected:
- Never run headless — LinkedIn's fingerprinting catches it
- Random delays between 2-5 seconds for every action
- Session rotation every 50 job views
- Exponential backoff when rate limited
The Selector Problem
LinkedIn changes their DOM class names with almost every deploy. .jobs-search-results__list-item today might be .scaffold-layout__list-detail tomorrow.
I built a fallback system:
SELECTORS = {
"job_card": [
"[data-job-id]", # Most stable — data attribute
"[aria-label*='job']", # ARIA labels change less often
".job-card-container", # Class name — least stable
]
}
def find_element(driver, key):
for selector in SELECTORS[key]:
try:
return driver.find_element(By.CSS_SELECTOR, selector)
except NoSuchElementException:
continue
raise ElementNotFound(f"All selectors failed for {key}")
Data attributes and ARIA labels are more stable than class names because they serve functional purposes that LinkedIn's engineers don't change casually.
AI Match Scoring
Each job gets scored against your resume using GPT-4o-mini. Not GPT-4 — too slow and expensive for 80+ evaluations per run.
The prompt is specific:
score_prompt = f"""
Score this job against the candidate's resume.
Return a JSON object with:
- match_score (0-100)
- matching_skills (list)
- missing_skills (list)
- deal_breakers (list)
- recommendation (apply/skip/maybe)
Resume: {resume_text}
Job: {job_description}
"""
GPT-4o-mini returns structured JSON reliably at ~0.3 seconds per job. The total cost for scoring 80 jobs is about $0.02.
4-Layer Salary Estimation
This is the part I'm most proud of. Salary data from any single source is unreliable. So I cross-validate across four:
- Coresignal — salary data from public profiles
- LinkedIn Salary Insights — scraped from the job listing if available
- GPT-4 estimation — based on role, company, location, seniority
- Algorithmic model — regression on similar roles in the same geography
The final estimate is a weighted average where source reliability determines the weight. If 3 out of 4 sources agree within 15%, confidence is high.
Cover Letter Generation
Template cover letters don't work. Neither does asking GPT to "write a cover letter" — it sounds like every other AI-generated letter.
The trick: give the LLM your resume AND the job description AND a constraint — reference a specific project that matches the role.
The Result
500+ jobs scanned daily. 25 high-match applications with tailored cover letters. What used to take 3 hours now takes 10 minutes of review.
I build AI automation tools. See my projects at 4ugusta.dev or hire me through 4UGUSTA Systems.