Skip to content

Repository files navigation

Oink — the orange-hat pig greeting you in Telegram

Oink — AI Job Search in Telegram

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.

Live bot Python Powered by Claude Apify actors License

Try the live bot · Quick start · How it works · Self-host · API access


Try it first

  1. Open @job_search_everyday_bot
  2. Send /start and upload your CV (PDF)
  3. Matching jobs will start arriving

How it works

Job card in Telegram
  • Scrapes 25+ sources — LinkedIn, HN "Who is Hiring", Wellfound, EURES, ReliefWeb, remote boards, EU tech boards, academic/research boards (full list in skill/job-search/scripts/sources/).
  • Builds your profile with Claude — from your uploaded resume plus free-text preferences (/prefs), rebuilt by Opus on demand.
  • Scores every posting with an LLM — no keyword filters; a scoring prompt weighs each job against your profile, and only matches above your /minscore threshold ship.
  • Delivers each match as a Telegram card — with a hiring contact and inline buttons to track applications or generate a tailored resume note.
  • Runs deep market research on request/marketresearch fans out 10 Opus agents and returns a polished .docx report on demand, salaries, and trends for your role + location.
  • Remembers everything — applied/skipped roles never reappear; history lives in a local SQLite DB you own.

Ways to use this

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

Who it's for

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.

Quick start

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.

What you need

  • Python 3.10+ and the deps in requirements.txt
  • A Telegram bot token — free, from @BotFather
  • Claude Code CLI — the claude binary on PATH, authenticated (a Claude subscription is enough; no API key required). All AI steps — scoring, profile builds, hiring-contact lookup, market research — run through claude -p. Without it the bot still scrapes but can't score or personalize.

Setup (self-hosting)

1. Install dependencies

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
# or: pip install --break-system-packages -r requirements.txt

2. Put your bot token in .env

TELEGRAM_BOT_TOKEN=123456789:AAE-your-bot-token-here

No 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.py

Leave 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-run

Prints 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_id
python skill/job-search/scripts/bot.py

With 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>&1

launchd (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.

Bot commands

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.

Architecture

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
Loading

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:

  1. 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.
  2. A manager agent synthesizes the ten JSON outputs, dedups sources, and renumbers citations globally.
  3. market_research_render.py renders a polished .docx — cover page, auto-populating Word TOC, numbered references, clickable URLs, skill + salary tables.
  4. The bot sends the DOCX plus a short Telegram summary. Each run is logged to research_runs with 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 /prefs input), 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 /marketresearch run
  • 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.

Extending

  • Add a source: drop my_source.py into skill/job-search/scripts/sources/, expose fetch(filters) -> list[Job], register in search_jobs.py:SOURCES, add a toggle in defaults.py:DEFAULTS["sources"].
  • Change button behavior: edit telegram_client.py::job_keyboard and the callback dispatcher in bot.py::handle_callback.
  • Upgrade resume tailoring to a full LLM rewrite: replace resume_tailor.py::build_tailor_note with an API call.

The scrapers, as an API

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.

Bundles and AI-assisted discovery

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.

General, technology, startup, and ATS sources

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.

Academic, European, humanitarian, and UN sources

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.

Security notes

  • .env contains 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, restart bot.py.

License

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.

About

AI job-search agent in Telegram — scrapes 25+ job boards, scores every posting against your CV with Claude, pushes real matches to your chat. Self-host free or use the hosted bot.

Topics

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages