Oink scrapes 25+ sources, scores every posting against your resume with Claude, and pushes only the real matches to your Telegram — each with a hiring contact and one-tap tracking.
Try the live bot · Quick start · How it works · Self-host · API access
- Open @job_search_everyday_bot
- Send
/startand upload your CV (PDF) - Matching jobs will start arriving
| Path | You get | Cost |
|---|---|---|
| 🐷 Hosted bot (recommend to try) | /start in Telegram, upload CV, done — zero setup, sources maintained for you |
Free to try |
| 🏢 oinkjobsearch.com | A managed private instance: uptime, upgrades, and support handled for you | Free to try |
| 🔌 Apify actors | The scrapers behind this bot as clean JSON APIs — proxies and anti-bot handled | Pay per result, from ~1$ per month + your Claude subscription |
| 🛠️ Self-host this repo | Full control: your keys, your data, your prompts — see Setup | Free, only your Claude subscription |
| You are | Your sources |
|---|---|
| Software engineer (frontend, backend, full-stack, DevOps/SRE) | LinkedIn, HN "Who is Hiring", Welcome to the Jungle, Built In, Wellfound (startups/YC), EU tech boards (JustJoin.it, NoFluffJobs, Tecnoempleo, InfoJobs) |
| ML / AI engineer, data scientist | aijobs.net (curated AI/ML/MLOps board) on top of all general tech sources |
| Researcher / academic (PhD, postdoc, faculty) | EURAXESS, jobs.ac.uk, AcademicPositions, Ikerbasque, university doctoral boards |
| Humanitarian / international development | ReliefWeb, ImpactPool, DevEx, UN/INGO portals |
| Remote-first, any of the above | Dedicated remote boards plus EU-wide vacancies via EURES |
Not for: recruiters sourcing candidates, bulk scraping, or hosting a public multi-tenant service — the scrapers are deliberately low-volume and some sources' TOS restrict use to personal job search.
git clone https://github.com/Exdenta/OinkAIJobSearch.git && cd OinkAIJobSearch && claude '/setup'Then type /setup. Claude Code
walks you through the rest — dependencies, a free Telegram bot token from
@BotFather, .env, scheduling — and smoke-tests your bot at the end.
No Claude Code? Follow the manual Setup below.
- Python 3.10+ and the deps in
requirements.txt - A Telegram bot token — free, from @BotFather
- Claude Code CLI — the
claudebinary on PATH, authenticated (a Claude subscription is enough; no API key required). All AI steps — scoring, profile builds, hiring-contact lookup, market research — run throughclaude -p. Without it the bot still scrapes but can't score or personalize.
1. Install dependencies
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
# or: pip install --break-system-packages -r requirements.txt2. Put your bot token in .env
TELEGRAM_BOT_TOKEN=123456789:AAE-your-bot-token-hereNo TELEGRAM_CHAT_ID needed — users register themselves via /start.
Optional: set APIFY_TOKEN to unblock AcademicPositions via the
Apify actors.
3. Start the bot (long-running process)
python skill/job-search/scripts/bot.pyLeave this running — it handles /start, resume uploads, and button
presses. For production, wrap it in systemd / nohup / Docker:
nohup python skill/job-search/scripts/bot.py > bot.log 2>&1 &4. Onboard yourself — in Telegram, send /start to your bot and upload
your CV (PDF). The bot saves it to state/users/<chat_id>/resume.pdf,
extracts text, and you're in.
5. Smoke-test the digest
python skill/job-search/scripts/search_jobs.py --dry-runPrints what would be posted without sending or recording anything.
6. Schedule searches — two modes, pick one:
Continuous mode (recommended) — bot.py runs the search loop itself
bot.py runs the search loop every couple of hours for a single chat.
Quality is gated by the per-user buffer (P1) and pagination by the
source-page cursors (P2), so the loop never re-fetches the same source page
within 6h and only flushes ≥4-scored matches. Enable with two vars in .env
(NOT export — a shell export doesn't survive a restart or a new terminal,
.env does):
OINK_CONTINUOUS_MODE=1
OINK_CONTINUOUS_CHAT_ID=433775883 # your Telegram chat_idpython skill/job-search/scripts/bot.pyWith these set, remove any cron entry first (crontab -e) — otherwise the
same search runs twice. The interval is tunable in defaults.py
(continuous_interval_seconds, default 7200).
Also set OPERATOR_CHAT_ID to your own chat_id in .env so sustained
failures (e.g. the poller getting stuck) reach you instead of sitting
silently in bot.log. But the operator's chat_id is excluded from continuous
auto-search by default — right for the hosted multi-tenant Oink bot, wrong
for this repo's self-host mode (single-user only, so the same chat_id IS
both operator and the one real user). When OPERATOR_CHAT_ID equals
OINK_CONTINUOUS_CHAT_ID, also set OINK_CONTINUOUS_INCLUDE_OPERATOR=1 in
.env, or continuous mode will silently do nothing for that chat.
Continuous mode is single-user — only OINK_CONTINUOUS_CHAT_ID is driven
by the in-process loop. Other users still use on-demand /jobs taps.
Cron mode (legacy) — point any scheduler at search_jobs.py
0 8 * * * cd /path/to/FindJobs && /usr/bin/python3 skill/job-search/scripts/search_jobs.py >> bot.log 2>&1launchd (macOS): drop a .plist in ~/Library/LaunchAgents/ that runs
python skill/job-search/scripts/search_jobs.py on a
StartCalendarInterval.
systemd timer (Linux): pair a findjobs.service
(ExecStart=python skill/job-search/scripts/search_jobs.py) with a
findjobs.timer (OnCalendar=*-*-* 08:00:00).
The long-running bot.py process is separate from the cron-fired digest —
keep it up so /start, uploads, and button presses keep working between
digests.
| Command | Action |
|---|---|
/start |
Onboarding wizard + register your chat_id |
/help |
Full command reference |
/jobs |
Run a search now, don't wait for the daily digest |
/prefs |
Send/update free-text preferences (triggers an Opus profile rebuild) |
/clearprefs |
Wipe stored free-text preferences |
/minscore |
Set the minimum match-score filter |
/myprofile |
Show the current AI-built profile summary |
/rebuildprofile |
Force-rebuild the profile from resume + free-text |
/applied |
List every role you've marked applied |
/marketresearch |
Deep market scan for your role + location — delivered as a .docx report (~25–40 min) |
/cleardata |
Scoped deletion menu (resume / history / tailored / profile / research / everything) |
/privacy |
In-chat privacy summary + link to the full policy |
Button behavior: ✅ Applied records the application — the job never reappears. 🚫 Not applied hides it (tracked separately so you can audit). ✍️ Tailor my resume compares your resume's skills against the posting and sends back a Markdown note as a file attachment — it only rearranges emphasis, it never invents experience.
flowchart TB
scheduler["⏰ cron / launchd / systemd<br>(daily)"] --> search["search_jobs.py<br>scrape → LLM score → dedupe<br>send digest + buttons"]
search --> tg["💬 Telegram chats<br>(per registered user)"]
tg -- "button press" --> poll["getUpdates long-poll<br>(callback_query events)"]
poll --> bot["bot.py (long-running)<br>/start · CV upload<br>Applied / Skip / ✍️ Tailor"]
search <--> db[("SQLite state/jobs.db<br>users · jobs · applications<br>sent_messages")]
bot <--> db
Per-user files live under state/users/<chat_id>/ — resume.pdf,
resume.txt, tailored/<job_id>.md, research/.
Directory layout
FindJobs/
├── README.md
├── requirements.txt
├── .env ← TELEGRAM_BOT_TOKEN, OPERATOR_CONTACT,
│ APIFY_TOKEN (optional) (gitignored)
├── .env.example
├── deploy/ ← systemd units, Caddy config, VPS bootstrap scripts
├── docs/
│ ├── PRIVACY.md ← privacy policy published with the bot
│ ├── per-user-profile-plan.md
│ └── telegram_listing.md
├── state/ ← runtime data (gitignored)
│ ├── jobs.db ← SQLite — auto-created
│ └── users/<chat_id>/ ← per-user resumes, tailored notes, research
├── tools/
│ ├── get_chat_id.py ← helper (mostly obsolete now; use /start)
│ ├── demo_ui_to_user.py ← one-shot UI demo sender
│ ├── capture_sticker_ids.py
│ └── fetch_fat_roll_pig.py
└── skill/
└── job-search/
├── SKILL.md ← skill definition Claude reads
├── scripts/
│ ├── search_jobs.py ← scheduled digest orchestrator
│ ├── bot.py ← long-running Telegram bot
│ ├── onboarding.py ← /start wizard + resume intake
│ ├── db.py ← SQLite layer
│ ├── dedupe.py ← Job dataclass + per-user dedupe
│ ├── telegram_client.py
│ ├── resume_tailor.py ← skill-matching + markdown note
│ ├── fit_analyzer.py ← per-job fit scoring
│ ├── pig_stickers.py ← sticker cache + sender
│ ├── profile_builder.py ← Opus profile rebuild
│ ├── market_research.py ← /marketresearch orchestrator (10 Opus workers + manager)
│ ├── market_research_render.py ← DOCX renderer for ResearchRun
│ ├── safety_check.py ← prompt-injection gate for user input
│ ├── prompts/ ← fit_analysis.txt, market_research_*.txt, profile_builder.txt
│ ├── sources/ ← 25+ adapters, one file per board
│ └── tools/
│ └── reset_user.py ← per-user history wipe
└── references/
└── source_notes.md
Hiring-contact lookup — how the 👤 line works
For every card about to ship, a Claude agent (WebSearch + WebFetch,
hiring_contact.py) hunts for the one person who most plausibly owns the
opening: the recruiter named on the posting, the talent-acquisition partner
covering that function and region, the hiring manager, or a founder at a
tiny startup. The name links to their public profile (LinkedIn /in/…
preferred) and the italic line says why this person was picked.
Verdicts are cached per posting in the hiring_contacts table, lookups run
only for jobs that survived every send gate, and any failure just ships the
card without the block.
Knobs: HIRING_CONTACT_OFF=1 disables the pass; HIRING_CONTACT_TIMEOUT_S
(default 180), HIRING_CONTACT_WORKERS (default 3),
HIRING_CONTACT_MODEL (default sonnet) tune it.
/marketresearch — deep market research pipeline
Requires: resume uploaded + profile built (/prefs).
The bot asks for a target location (send . to reuse the location from
your profile, or type a market like Berlin, Germany / Remote EU). A
per-user lock ensures only one run at a time. Behind the scenes:
- 10 Opus sub-agents run in parallel, each with WebSearch + WebFetch and a narrow topic: current demand & volume · 24-month historical context · industry trends · your resume skills vs. the market · 12–18-month projections · salary in your home market · salary in neighboring markets · company landscape · interview & hiring bar · recommended upskilling plan.
- A manager agent synthesizes the ten JSON outputs, dedups sources, and renumbers citations globally.
market_research_render.pyrenders a polished.docx— cover page, auto-populating Word TOC, numbered references, clickable URLs, skill + salary tables.- The bot sends the DOCX plus a short Telegram summary. Each run is logged
to
research_runswith status / elapsed_ms / worker counts / input hashes / docx path.
Failure policy: ≥5 worker failures → report aborted; 1–4 → partial report
with a notice listing failed topics; manager crash on an otherwise OK run
demotes it to partial. Saved runs live under
state/users/<chat_id>/research/ and are wiped by /cleardata → 🔬 Research.
Prompt-injection hardening: every sub-agent prompt wraps the candidate's
inputs in opaque-data blocks with an instruction-ignore preamble; the
user's location input passes through the same
safety_check.check_user_input gate as /prefs before it reaches any
Claude call.
Data model (SQLite)
- users — chat_id, resume_path, resume_text, prefs_free_text (raw
/prefsinput), user_profile (Opus-built JSON profile) - jobs — every posting ever seen (stable job_id = sha1 of source+url)
- applications — (chat_id, job_id) → status ∈ {applied, skipped, interested}
- sent_messages — (chat_id, message_id) → job_id, so callbacks can resolve
- profile_builds — audit log (status, error, elapsed_ms, input hashes) for every Opus profile build
- research_runs — audit log (status, elapsed_ms, workers_ok/failed,
docx_path, input hashes) for every
/marketresearchrun - hiring_contacts — per-job cache of "who to write to" lookups (status ∈ {found, not_found} + the contact dict); transport errors are never cached so they retry on the next send
Deletion: to reset a user, DELETE FROM users WHERE chat_id=? and drop
their state/users/<chat_id>/ folder. To wipe job history, delete
state/jobs.db.
- Add a source: drop
my_source.pyintoskill/job-search/scripts/sources/, exposefetch(filters) -> list[Job], register insearch_jobs.py:SOURCES, add a toggle indefaults.py:DEFAULTS["sources"]. - Change button behavior: edit
telegram_client.py::job_keyboardand the callback dispatcher inbot.py::handle_callback. - Upgrade resume tailoring to a full LLM rewrite: replace
resume_tailor.py::build_tailor_notewith an API call.
Every scraper behind Oink is also published as a standalone Apify actor — call any source as a clean JSON API from your own pipeline, no bot required. These are the same actors this bot runs on in production every day, and they handle the hard stuff: residential proxies, DataDome/Cloudflare bypass, pagination, dedupe, and delta mode so you pay only for postings you haven't seen. No subscription — pay per result.
Use a bundle when you want broad coverage, a source Actor when you know the board you need, or an AI discovery Actor when relevant postings may live anywhere on the open web. Every row links to a Claude/Codex skill that explains fit, inputs, cost caps, API integration, output fields, and failure modes.
| Actor | Agent skill | What it does |
|---|---|---|
| ai-job-search-agent | Claude / Codex | Owner-funded AI discovery: describe a candidate and receive deduplicated, availability-checked, scored jobs from the open web. |
| web-search-scraper | Claude / Codex | BYOK open-web job discovery using Anthropic, Mistral, or OpenAI, with ranked and availability-checked results. |
| all-jobs-scraper | Claude / Codex | Merges and deduplicates normalized jobs from 19 public job boards behind one endpoint. |
| web-dev-bundle | Claude / Codex | Aggregates 10 web-developer sources with normalized output and optional BYOK candidate matching. |
| ml-ai-dev-bundle | Claude / Codex | Combines AI, machine-learning, MLOps, and data-science jobs from eight sources. |
| american-jobs-bundle | Claude / Codex | Combines six US-focused job sources into one deduplicated dataset. |
| europe-jobs-bundle | Claude / Codex | Aggregates 14 European sources spanning general, technical, and academic vacancies. |
| researcher-bundle | Claude / Codex | Merges 12 sources for PhD, postdoc, faculty, research, policy, UN, and NGO roles. |
| company-careers-bundle | Claude / Codex | Turns company names into live jobs by discovering Greenhouse, Lever, Ashby, Workable, SmartRecruiters, and Workday boards. |
| remote-boards-scraper | Claude / Codex | Combines RemoteOK, Remotive, WeWorkRemotely, and Himalayas for remote-job feeds and alerts. |
| Actor | Agent skill | What it does |
|---|---|---|
| linkedin-scraper | Claude / Codex | Searches public LinkedIn Jobs without login or cookies, with detail enrichment and scheduled delta mode. |
| linkedin-enrich-translate-normalize-scraper | Claude / Codex | Maps public LinkedIn cards, detail pages, and JobPosting JSON-LD into strict nomad-agent-job-v1, with optional English display-field translation and null-only BYOK enrichment. |
| hackernews-scraper | Claude / Codex | Extracts Hacker News Who is hiring, Who wants to be hired, and Freelancer threads, or performs site-wide HN search. |
| builtin-scraper | Claude / Codex | Collects US technology jobs from Built In, including salary, skills, detail enrichment, and delta state. |
| ai-jobs-net-scraper | Claude / Codex | Retrieves curated AI, ML, MLOps, and data-science vacancies from aijobs.net. |
| wellfound-scraper | Claude / Codex | Searches Wellfound startup jobs with remote, role, salary, freshness, and new-job filters. |
| ycombinator-was-scraper | Claude / Codex | Retrieves Y Combinator Work at a Startup jobs with salary, equity, visa, role, batch, and remote fields. |
| greenhouse-jobs-scraper | Claude / Codex | Discovers or monitors company Greenhouse boards, with salary and application-question extraction. |
| lever-jobs-scraper | Claude / Codex | Retrieves live postings from company Lever boards with filters, delta monitoring, and optional BYOK enrichment. |
| ashby-jobs-scraper | Claude / Codex | Discovers or directly queries public Ashby boards and returns normalized postings. |
| workable-jobs-scraper | Claude / Codex | Searches Workable's public index or specific company boards, with delta mode and optional BYOK enrichment. |
| wttj-scraper | Claude / Codex | Searches Welcome to the Jungle for French and European vacancies, salary data, and recurring alerts. |
| justjoinit-scraper | Claude / Codex | Retrieves Polish and Central/Eastern European technology jobs with salary and detail data. |
| nofluffjobs-scraper | Claude / Codex | Retrieves NoFluffJobs technology and business roles across Poland and Central/Eastern Europe. |
| infojobs-scraper | Claude / Codex | Searches InfoJobs Spain with province, remote-work, contract, schedule, salary, and alert filters. |
| tecnoempleo-scraper | Claude / Codex | Retrieves Spanish IT and technology vacancies with salary, location, detail, and delta fields. |
| Actor | Agent skill | What it does |
|---|---|---|
| eures-scraper | Claude / Codex | Searches the official EURES API for vacancies across Europe, including country and freshness filters. |
| euraxess-scraper | Claude / Codex | Retrieves EURAXESS PhD, postdoc, fellowship, faculty, and researcher-mobility vacancies. |
| euraxess-enrich-translate-normalize-scraper | Claude / Codex | Normalizes EURAXESS research vacancies into nomad-agent-job-v1, including deterministic requirements, locations, funding, and a versioned source extension, with optional translation and enrichment. |
| academicpositions-scraper | Claude / Codex | Finds postdoc, PhD, faculty, and research jobs from AcademicPositions. |
| jobs-ac-uk-scraper | Claude / Codex | Retrieves UK academic, postdoc, research, PhD, and university professional-services vacancies. |
| ikerbasque-scraper | Claude / Codex | Monitors Ikerbasque research calls in Spain's Basque Country, with optional deadline extraction. |
| math-ku-phd-scraper | Claude / Codex | Retrieves University of Copenhagen PhD and research-fellowship openings. |
| ub-doctoral-scraper | Claude / Codex | Retrieves currently open Universitat de Barcelona academic vacancies. |
| reliefweb-scraper | Claude / Codex | Searches current ReliefWeb humanitarian, NGO, aid-worker, and UN-sector vacancies. |
| impactpool-scraper | Claude / Codex | Retrieves UN, NGO, development-bank, EU-institution, and international-development vacancies from Impactpool. |
| devex-jobs-scraper | Claude / Codex | Discovers international-development, NGO, humanitarian, donor, and UN jobs or checks whether known Devex URLs remain live. |
| devex-scraper | Claude / Codex | Searches indexed Devex development, NGO, humanitarian, consultancy, donor, and UN roles, with optional BYOK processing. |
| un-careers-scraper | Claude / Codex | Retrieves current United Nations Secretariat vacancies from careers.un.org. |
| unjobs-scraper | Claude / Codex | Retrieves UN, NGO, and international-organisation vacancies from unjobs.org by feed, organisation, or duty station. |
For several of these sources (EURAXESS, EURES, Impactpool, unjobs.org, jobs.ac.uk, AcademicPositions) these are the only maintained scrapers on Apify. Full catalog — 50+ actors covering jobs, search, app intelligence and open data: apify.com/nomad-agent.
Note
AcademicPositions is blocked in the self-hosted scraper (Cloudflare — see
skill/job-search/references/source_notes.md); set APIFY_TOKEN in
.env and it's pulled through the actor above instead. That's also the
easiest way to support this project: source fetches through the actors
are what fund its maintenance.
.envcontains your bot token — gitignored, don't commit it.- LinkedIn and Indeed adapters scrape public search endpoints; both
services disallow automated access in their TOS. Use low volume and
accept the risk. Read
skill/job-search/references/source_notes.md. - Resume PDFs live on disk in
state/users/<chat_id>/resume.pdf. If multiple people use this instance, secure the filesystem accordingly. - If the bot token leaks: @BotFather →
/revoke, paste the new token into.env, restartbot.py.
PolyForm Noncommercial 1.0.0 — free to use, modify, and share for any noncommercial purpose (personal job search, research, education, nonprofits). Commercial use is not permitted.
Required Notice: Copyright Lex Sherman
Found a match through Oink? ⭐ this repo — or try the live bot and tell a friend.
