A personal tool for my internship search. It collects openings from 22 sources, ranks them against my profile and tracks my applications. It runs locally, on my laptop.
Role
Personal project
Stack
Python, Playwright, FastAPI, SQLite
Runs on
Locally, on macOS
Interfaces
Desktop app and web dashboard
Career Copilot, demo
The web dashboard on demo data: companies, openings and people are made up.
22connected sources: 4 portals, 9 ATS and 9 job boards
250automated tests
16,386lines of code
The problem
Too many sources, the same openings.
Internships are scattered across job portals, applicant tracking systems and company careers pages. Checking them by hand every day was slow, and the same role showed up several times on different sources.
Portals
Automated browser or feeds
LinkedIn
Indeed
Glassdoor
Welcome to the Jungle
ATS
Companies' public APIs
Greenhouse
Lever
Ashby
Workable
SmartRecruiters
Teamtailor
Recruitee
Personio
Workday
Job board
APIs and RSS feeds
Himalayas
RemoteOK
AI Jobs
NoDesk
Music Business
Work With Indies
TopStartups
JobFluent
theInterna
What I built
Find, score, track.
Find
Searches portals, ATS boards and careers pages from one interface and keeps everything in a local archive, without duplicates.
Network Radar, demo data
Score
Each opening gets a relevance score, a CV match score and an AI verdict with its reasoning.
Opening detail, demo data
Track
A Kanban keeps status, dates and follow-ups for every application; Network Radar links companies to people worth contacting.
Applications Kanban, demo data
How a score is made
Perfect Fit, line by line.
Each opening adds up relevance, CV match and the AI verdict with fixed weights, plus two bonuses. If the AI judges it off-profile it loses 55 points: a well-written title is not enough.
Founder's Associate Intern
Velarion, Berlin. Target company, AI verdict: maybe
86
050100
+31.7Relevance 69 × 0.46
+28.9CV Match 76 × 0.38
+7.6AI 63 × 0.12 (maybe)
+18Target company 10, internship 8
Business Development Intern
Mirelune, Stockholm. Target company, AI verdict: not a fit
9
050100
+26.7Relevance 58 × 0.46
+19CV Match 50 × 0.38
+18Target company 10, internship 8
−55AI verdict: not a fit
Precision
Fewer openings, the right ones.
Reviewing the filters I found two mistakes: some titles counted as internships because one word contained another, and the same opening was saved once per source. Terms are now matched as whole words, and duplicates are recognised by title, company and city.
TitleBeforeNow
International Sales ExecutiveInternshipExcluded
Internal Audit SpecialistInternshipExcluded
Backstage Operations SpecialistInternshipExcluded
Marketing InternInternshipInternship
LinkedInGrowth Marketing Intern (m/f/d) Velarion GmbH, Berlin
Company ATSGrowth Marketing Intern Velarion, Berlin, Germany
→
ArchiveOne opening The second source is skipped before the AI analysis.
How it works
From sources to pipeline.
SourcesPortals, ATS boards and job boards.
CollectionA time limit per source: if one is blocked, the others carry on.
FiltersWhole words, chosen cities, one row per opening.
ScoresRelevance, CV match and AI verdict.
ArchiveLocal SQLite, desktop app and web dashboard.
Technical choices
Six concrete decisions.
Public ATS APIs where they exist: Greenhouse, Lever, Ashby, Workable and SmartRecruiters. The automated browser is used only for sources without one.
Consistent browser-like requests. One reused session per source and retries only on 429 and server errors: a 403 or 404 does not improve by retrying.
Bounded run times. 10 minutes for API sources, 45 for browser sources. When time runs out, the openings already read are kept.
Updates without losing anything. When an opening is read again, status, notes and follow-ups stay as they were.
Nothing is sent without my approval. The app prepares drafts and materials; I send applications and messages myself.
250 automated tests on temporary databases, so real data is never touched.
What I'd do next
Keep only the sources that work.
I'd measure which sources actually lead to interviews and use that to weight the ranking.