Are you the author? Sign in to claim
Self-hosted Python job-search pipeline for H-1B & visa holders. You run it on your own laptop — no server, no sign-up, n
💡 In plain words: 📊 Your Google Sheet is your job tracker. Beacon is the automated assistant that keeps it full, running entirely on your own laptop.
🎯 Tell it what you want. Enter your target job titles, and matching roles from 5 major job sites land in your Google Sheet automatically.
🔍 No more dead-end descriptions. Beacon scans every posting and instantly flags which companies actually sponsor work visas (H-1B/OPT) before you waste time reading.
🌱 Onboard companies instantly. Want to track a specific company? Just type the name in a new row and tag it SEED, and Beacon sets up the automated tracking for it.
🧠 On-demand AI. AI only runs when you ask. Drop your resume into your local resumes/ folder, flag a row with Go Score, and Beacon scores how well that job matches it. You pay literal pennies straight to Anthropic or Gemini (whichever you set up) on your own account, not to us.
🔒 100% Private, and free and open-source (FOSS). The software runs locally on your machine, and every line of it is public on GitHub, so nothing is hidden. No external servers, no third-party databases, and absolutely nobody watching your data.
![]()
(Company/job data above is from a live personal account. The Decision/My Decision columns shown are unset defaults, not personal choices. More screenshots, including the database schema and a sample source job board, are in docs/screenshots/.)
Tags: h1b visa-sponsorship job-search international-students opt stem-opt immigration career-tools claude-ai automation
If this saves you time, a ⭐ helps other visa holders find it too.
👥 For H-1B holders, F-1 OPT/STEM-OPT students, and anyone whose job search has to filter for visa sponsorship. Especially if you're tired of dead-end postings, don't want to pay for AI you don't need, or want your search tracked in your own private Sheet.
🎯 Why: every visa holder should be able to find jobs that fit and apply to companies that actually sponsor, without wasting time or paying for a subscription. It's zero dollars if you don't use the AI, and pennies if you do (see Savings). Free, permanently, is the goal.
Fair question. LinkedIn Premium doesn't solve the one thing that costs visa holders the most time:
| LinkedIn Premium | Beacon | |
|---|---|---|
| Detects visa sponsorship | ❌ Not a feature: you read every posting yourself | ✅ Auto-classified, restrictions caught automatically |
| Cost | ~$30–$40/month, forever | Free software. Real lifetime AI cost so far: $8.88 (see below) |
| Filter logic | Black-box "match" score | Every rule lives in a table you can read and edit |
| Privacy & tracking | Public "Open to Work" status; only sees LinkedIn's own Apply flow | Fully private, your own Sheet: tracks any job however you actually applied |
| LinkedIn account risk | None, it's their own product | None here either: Beacon never scrapes LinkedIn or touches your account, it only pulls from Adzuna, Greenhouse, Lever, Ashby, and SmartRecruiters |
🏆 The short version: it replaces the one feature visa holders need most, "will this company even consider me," at a fraction of a month's subscription.
Real numbers from this project's own history, not an estimate. Software is free. Any AI usage is paid directly to Anthropic or Gemini (whichever you configure), not to this project:
| Metric | Value |
|---|---|
| Total postings ever ingested | 137,318 |
| Actually reached your Sheet, worth your time (2.4%) | 3,355 |
| Caught as visa-restricted before you wasted time on them | 577 |
| Companies tracked | 2,674 |
| Typical AI spend for a real user | Under $1/month ($0 if you never type Go Score) |
📌 For reference, this project's own real lifetime spend so far, across all 137,318 postings above and several weeks of daily use, is $8.88 total.
💡 Stays cheap because: company lookups only use free data sources, left blank instead of falling back to a paid one; resume-fit scoring only runs when you flag a job; and visa checks are mostly free text-matching, with AI stepping in only for the small, genuinely unclear remainder.
What that restriction language actually looks like (real phrasing caught in live postings, usually buried a few paragraphs in, not in the title):
"We are unable to sponsor H-1B, F-1 OPT, and STEM OPT extension at this time." "This role is not open to visa sponsorship." "This position requires a government security clearance; you must be a US citizen for consideration."
🌐 5 job sources: Adzuna plus direct polling of 4 ATS platforms
🛡️ 3-tier visa detection: free checks first, AI only when genuinely ambiguous
🌱 Self-onboarding: type a company name into a SEED row, done
🎛️ Live-editable filters: keywords, seniority, location, all in a table, no code
🧠 On-demand fit scoring: AI runs only when you flag Go Score
📋 Google Sheets as the UI: no dashboard, no login, nothing new to learn
AI makes exactly two decisions in this pipeline. Everything else is free, deterministic code:
🛡️ Visa check: does this posting rule out sponsorship? Automatic on every job; free pattern-matching first, AI only for the genuinely unclear remainder.
🎯 Fit score: does this job match your resume? Only when you type Go Score into that row, never automatic, and never every job already sitting in your Sheet.
🏛️ A third signal sits alongside these two, but isn't AI at all: real DOL government filing data on which companies have sponsored before, a "Likely work visa sponsor" flag, not a guarantee.
🔀 Use Claude (default) or Gemini. Add a GEMINI_API_KEY to .env.
(Full mechanics, including the pipeline diagram, exactly what the AI sees, and every My Decision state, plus how to run the DOL LCA check, are in job-search-app-technical-spec.md and docs/ARCHITECTURE.md.)
flowchart TD
SCHED["Scheduler (single locked process, 3x/day + startup catch-up)"] --> SEED["Seed-via-Sheet: onboard new companies"]
SEED --> ING[Ingestion: Adzuna + Greenhouse/Lever/Ashby/SmartRecruiters]
ING --> DB[(SQLite: jobs, companies)]
DB --> FILTER[Filter Engine: keyword + location + seniority]
FILTER -->|passes| SHEET["Beacon Sheet: row added immediately"]
FILTER -->|fails| JOBLOG["Job Log Sheet: reason recorded"]
SHEET --> VISA[Visa Scanner]
VISA -->|regex, free| FLAG[Visa Flag set]
VISA -->|keyword pre-check, free| FLAG
VISA -->|genuinely ambiguous only| HAIKU[Claude Haiku]
HAIKU --> FLAG
FLAG -->|restricted| JOBLOG
SHEET -->|user flags 'Go Score'| FITSCORE[Claude Sonnet: fit score]
FITSCORE -->|below threshold| JOBLOG
FITSCORE -->|above threshold| SHEET
SHEET -.->|native Google rule| EMAIL[📧 Email notification]
SHEET -->|Decision column| POLLER[Approval Poller, every 30 min]
Full diagram with every field/table: docs/ARCHITECTURE.md
| Layer | Technology |
|---|---|
| Language | Python 3.14 |
| Scheduling | APScheduler (BlockingScheduler, single-instance file lock) |
| Database | SQLite, WAL mode |
| Tracking / UI / Notification | Google Sheets via gspread (service account auth) |
| LLM | Anthropic Claude (default): Haiku 4.5 (visa classification), Sonnet 5 (fit scoring), or Gemini: Flash-Lite + Pro, via LLM_PROVIDER=gemini |
| Job sources | Adzuna API, Greenhouse, Lever, Ashby, SmartRecruiters (all public/free) |
| Company data | Financial Modeling Prep + StartupHub.ai + TinyFish Search (all free tier) |
| Location resolution | US Census county/place reference data (bundled, public domain) |
| HTTP | requests, with retry/backoff on rate limits and transient errors |
🐍 Python 3.14 (or 3.12 if 3.14 wheels are unavailable for a dependency)
🔍 A free Adzuna developer account
🤖 A free Anthropic API key, or a free Gemini API key if you'd rather set LLM_PROVIDER=gemini
🏢 A free Financial Modeling Prep key and StartupHub.ai key
➕ Optional: a free TinyFish key, a third, best-effort industry source for companies the two above miss
📊 A Google Cloud service account (not OAuth) shared as an Editor on two Google Sheets you create yourself: one for active matches ("Beacon"), one for excluded jobs ("Job Log")
Don't have these yet? See the Appendix for step-by-step instructions for every single one.
git clone https://github.com/algoshank-pat/beacon.git
cd beacon
python bootstrap.py
bootstrap.py creates the venv, installs dependencies, copies .env and seed_companies.yaml from their templates, and runs the database migrations and default filter setup: everything that doesn't need your own credentials yet.
The rest genuinely needs your own accounts, so no script can do it for you:
.venv\Scripts\activate (Windows) or source .venv/bin/activate (macOS/Linux).env; see the Appendix for how to get each oneGOOGLE_SHEETS_CREDENTIALS_PATH points toresumes/base_resume.docx (or .md/.txt). Fit-scoring needs this file to work at all; without it, typing Go Score fails with a clear error instead of silently doing nothingseed_companies.yaml: replace the example companies with your own targets, or leave it empty (nothing breaks; Adzuna's broad keyword search runs regardless and is often the majority of what you'll see; direct company tracking is a bonus layer on top, not a requirement)python -m app.cli seed-companies --file seed_companies.yamlpython -m app.cli pipeline for one manual end-to-end runseed_filters.yaml (already loaded by bootstrap.py) and seed_companies.yaml are both live-editable after setup too. Re-run seed-filters/seed-companies or edit the DB tables directly, and the next scheduled run picks it up. You can also add a company anytime later straight from the Sheet, see below.
SEED and Company to the name you want tracked; leave every other column blankpython -m app.cli seed-via-sheetSEED row itself is deleted automatically on the run after that. It was never a real posting, just a one-time onboarding requestEvery filter criterion seed_filters.yaml actually supports today:
| Criterion | What it does |
|---|---|
role_keyword_include | Job-title keywords you're targeting (e.g. "Solutions Architect") |
tech_keyword_include | Skill/domain keywords (e.g. "Kafka", "iPaaS"); either list matching is enough to pass |
title_exclude | Title-only blocklist (e.g. "Intern", "Director") |
seniority | Only these seniority levels (e.g. mid, senior, staff) |
remote_type | Only these work-location types (e.g. remote, hybrid) |
location_include | Only postings resolved to one of these US states (2-letter code, e.g. TX, CA) or Remote-USA, matched against the clean location_state field (see app.location_state), not raw location text. A posting location_state couldn't resolve is passed through rather than filtered out, since a missing signal isn't evidence of a mismatch |
industries_include | Only certain industries: a real hard filter, empty by default so it's a no-op until you populate it |
posted_within_days | Drop anything older than this many days |
company_priority_min | Only companies at or above this priority tier (S/A/B/C) |
employee_count_min / employee_count_max | Only companies with a headcount in this range |
founded_after_year | Only companies founded after this year (a rough startup-recency proxy) |
require_us_location | Drop anything that doesn't look US-based |
require_visa_sponsorship | Evict postings the Visa Scanner classifies restricted |
require_h1b_track_record | Only companies with a confirmed historical H-1B track record (depends on the not-yet-built historical-data feature on the Roadmap) |
⚠️ Not supported yet, genuinely missing today: filtering by salary range, and filtering by funding stage/company type (a real "startups only" filter, as opposed to the founded-year/headcount proxies above). Both are now on the Roadmap.
🌍 This is not architecturally US-only, even though it ships US-focused by default. Visa-sponsorship job hunting is a real problem everywhere, not just the US. Adzuna alone already covers the UK, Canada, India, Germany, Australia, and more. Today, though, the shipped defaults are US-only in practice: require_us_location: true is on by default in seed_filters.yaml, and the Adzuna integration (app/sources/adzuna.py) supports a country parameter but the actual pipeline call never passes it, so it's silently locked to country="us". Making country a real, configurable filter_settings value (instead of a hardcoded default a few layers down) is a small, concrete fix, now on the Roadmap.
| Download/install time | The repo itself is ~8MB (seconds to clone). bootstrap.py installs 10 lightweight dependencies, no ML/data-science stack, typically under a minute on a normal connection |
| Disk space | Starts near-empty; grows with how much you ingest. This project's own database reached ~490MB after several weeks of continuous 3x/day polling against 150+ tracked companies plus broad discovery. Budget a few hundred MB to a couple GB for long-term personal use |
| Memory | The scheduler process itself runs at roughly 20-30MB RAM in practice (measured on this project's live process). It's I/O-bound (waiting on API calls), not compute-heavy |
| CPU | Negligible. Brief bursts during each poll, idle the rest of the time |
| Platform | Every CLI command (migrate, ingest, filter, pipeline, etc.) is plain cross-platform Python and runs fine on Windows, macOS, or Linux. app/scheduler.py (the persistent "run continuously" process) now also runs on all three: its single-instance lock picks msvcrt on Windows or fcntl.flock on macOS/Linux automatically (sys.platform check, no new dependency). Built directly against fcntl's documented locking semantics, not yet run live on a real Mac/Linux machine (this project's own daily use is Windows). If you hit anything running it there, it's worth a bug report |
python -m app.scheduler
Runs the main pipeline 3x/day (default 8am/1pm/6pm), fit-scoring and company enrichment on their own offset schedules, and an approval poller every 30 minutes, all inside one locked, crash-recovering process. See RUNBOOK.md for keeping it running across restarts on Windows.
beacon/
├── app/
│ ├── sources/ # Adzuna, Greenhouse, Lever, Ashby, SmartRecruiters pollers
│ ├── data/ # Bundled US Census reference data (public domain)
│ ├── migrations/ # Numbered SQL schema migrations
│ ├── sheets.py # Beacon sheet reads/writes
│ ├── job_log.py # Job Log sheet reads/writes
│ ├── filter_engine.py # Keyword/seniority/location filtering
│ ├── visa_scan.py # Three-tier visa classification
│ ├── fit_scoring.py # Resume-vs-JD scoring (Sonnet)
│ ├── enrichment.py # Free-only company data enrichment
│ ├── seed_via_sheet.py # Type-a-name-to-onboard-a-company
│ ├── scheduler.py # APScheduler process entrypoint
│ └── cli.py # Manual command-line entrypoints
├── tests/ # pytest suite (fakes for Sheets/HTTP, no live calls)
├── docs/
│ ├── ARCHITECTURE.md # Full system diagram + data flow
│ └── screenshots/ # README images
├── seed_companies.example.yaml # Safe template, copy to seed_companies.yaml
├── seed_filters.yaml # Default filter keyword/threshold seed data
├── .env.example # Safe credential template
├── bootstrap.py # One-shot venv/install/config-template setup
└── RUNBOOK.md # Day-to-day operating guide
Never committed (see .gitignore): .env, service_account.json, job_search.db*, resumes/, scheduler.log.
🔑 No hardcoded credentials: every secret is loaded from .env or the service-account JSON path, both gitignored
📄 Templates only: .env.example and seed_companies.example.yaml contain placeholder values, nothing real
🚫 Never committed: the live SQLite database (real scraped postings and personal application decisions) and the real service-account key
✅ See PUBLISHING_GUIDE.md for the full secret-audit checklist this repo was published against
🎯 Can you get me a job, or tell me which companies sponsor visas?
No. See the Disclaimer below. There's no personally curated company list anywhere in this project, and I'm not a recruiter or immigration attorney. Beacon's core signal reads a specific job posting's own text and classifies what that posting says, nothing more. It also optionally shows a "likely sponsors" flag from real DOL government filing data (see How AI Fits In), a positive historical indicator, not a guarantee about any specific role.
📄 Can I use more than one resume?
Not yet, a real, known limitation. Fit-scoring always reads a single file (
resumes/base_resume.md, falling back to.txtthen.docx, first match wins), with no way to select a different one per job or per role type. If you want to score against different resumes for different kinds of roles, today's only workaround is manually swapping the file between runs.
🚫 Does this scrape LinkedIn?
No. LinkedIn's Terms of Service explicitly prohibit automated scraping, and Beacon deliberately doesn't touch it: every source here (Adzuna, Greenhouse, Lever, Ashby, SmartRecruiters) is a public, documented API meant to be queried programmatically.
🔒 Is this a website or a service I sign up for, and is my data private?
Neither, and yes. No server, no account, no sign-up. You clone the code and run it entirely on your own machine, using your own accounts for every external service it talks to. Nothing is sent anywhere except the API calls you configure yourself (Google Sheets, Anthropic or Gemini, Adzuna, the ATS platforms, FMP/StartupHub/TinyFish), and none of those see anything beyond the single request you're making in that moment. Nobody, including whoever wrote this code, sees your search activity, your resume, or your decisions.
💰 How much does it actually cost to run?
The software itself is free, there's no fee to download or use it. You bring your own Anthropic or Gemini API key and pay that provider directly, only for what you actually use, at their normal rate. See Savings: this project's own real usage (on Anthropic), across 137,000+ jobs processed over several weeks, totals $8.88, that's what actual usage costs at this scale, not a fee anyone charges you.
🐍 Do I need to know Python to use this?
You need to be comfortable running a few CLI commands and editing a
.envfile (see Setup). Day-to-day use afterward is entirely in Google Sheets.
💼 Can I use this for a non-tech job search?
Yes. Nothing in the filter/keyword design is tech-specific. The example keyword lists target Solutions Architect/Presales-style roles because that's what this was originally built for, but every keyword, title exclusion, and threshold lives in an editable table, not code.
📋 Why Google Sheets instead of a real dashboard?
Because you already know how to use it, it's free, it's already got notifications/mobile access/sharing solved, and it means zero UI code to build or maintain.
🌍 Make country a real, configurable setting instead of a hardcoded default a few layers into app/sources/adzuna.py. Adzuna already covers the UK, Canada, India, Germany, Australia, and more, and this problem is not US-specific. Today's country="us" default and require_us_location: true default are just unconfigured defaults, not an architectural limit
⚡ Batch Google Sheets writes (append_rows + in-memory duplicate-check) instead of one API call per job. The current per-job cost is what makes a large backlog slow
🔄 Automatic re-validation of jobs already on the sheet against later filter-criteria changes (today, only newly-ingested jobs are checked against the current rules)
📝 Resume/cover-letter generation handoff to Claude Desktop on Approve (designed, not yet built)
💵 Salary-range filtering: salary is extracted and shown today, but nothing actually filters on it yet
🚀 A real "startups only" filter: founded_after_year and employee_count_max are rough proxies today; companies.funding_stage/company_type are already enriched (series_a, private, etc.) but never wired into filtering at all
🏛️ Actually download and feed in more than one fiscal year's LCA file. lca-enrich already accepts and merges multiple files (see the Real Historical Sponsorship Data section above), but only one quarter (FY2026 Q2) has been run against the live table so far
⏰ Automate the quarterly LCA re-check on a schedule once there's a reliable way past DOL's bot protection (today it's a manual download + python -m app.cli lca-enrich, by design; see the Real Historical Sponsorship Data section above)
🔍 Resume gap analysis and tailored-resume generation against your master resume, going beyond today's numeric fit score to actually explain what's missing and draft a tailored version for a specific posting
🖥️ Both of the above need a real UX, not a spreadsheet cell. A Sheets cell is a fine place for a visa flag or a 0-100 fit score; it's a bad place for a multi-paragraph gap analysis or a full tailored resume. These are natural candidates for the Claude Desktop handoff (already designed, not yet built) or some other dedicated output surface, not another Sheet column
🤖 A second paid provider (Gemini) is now supported via LLM_PROVIDER=gemini, but that's not the $0 goal itself, just proof the abstraction works with more than one provider. The real remaining step toward the Goal is open-weight models run locally on your own laptop (e.g. via Ollama): today's design already keeps AI usage to cents by calling it only when free checks can't decide, and a local model swapped in for that same narrow, well-defined classification step removes even that small cost, as long as it can match Claude/Gemini's reliability on the same task first
This entire pipeline, architecture, every source integration, the visa classification design, the Sheets automation, the test suite, and every bug fix along the way, was built through an extended pair-programming process with Claude Code, Anthropic's agentic CLI. The full build narrative, including real bugs found and fixed live against production data, is documented in RUNBOOK.md and docs/ARCHITECTURE.md.
Step-by-step for every credential .env.example asks for. No screenshots (UIs change and go stale; these steps don't).
ANTHROPIC_API_KEY)sk-ant-); you only get to see it once.env as ANTHROPIC_API_KEYGEMINI_API_KEY), alternative to AnthropicSame two jobs (visa classification, fit scoring), same forced-JSON-schema behavior, different provider billed.
.env as GEMINI_API_KEY. That's it, no LLM_PROVIDER line needed. If it's the only one of the two keys set, Gemini is picked automatically; LLM_PROVIDER=gemini is only needed to force Gemini when both keys are set.ADZUNA_APP_ID / ADZUNA_APP_KEY).envGOOGLE_SHEETS_CREDENTIALS_PATH, GOOGLE_SHEET_ID, GOOGLE_JOB_LOG_SHEET_ID)This is the most involved one, but it's a one-time setup:
.json fileservice_account.json) and point GOOGLE_SHEETS_CREDENTIALS_PATH at itclient_email field: it looks like something@your-project.iam.gserviceaccount.comclient_email address, giving it Editor access, exactly like sharing with a person. This is the step people most often miss, and without it every write will fail with a permissions error/d/ and /edit) into .env as GOOGLE_SHEET_ID and GOOGLE_JOB_LOG_SHEET_IDThis is what makes Beacon feel "live" instead of a spreadsheet you have to remember to check: Google's own native notification rule, not anything this app builds or maintains. Set it up once, only on the Beacon sheet (the Job Log is deliberately left off, see Why two separate Google Sheets, so excluded jobs don't also email you):
Because the app writes as the service account's own distinct identity (not you), Google's rule fires on every automated write same as it would for a human edit, no extra integration code needed. If you don't want emails at all, just skip this section; nothing else in Beacon depends on it.
FMP_API_KEY).envSTARTUPHUB_API_KEY).envTINYFISH_API_KEY), optionalA third, best-effort industry source, only queried for companies FMP and StartupHub both already checked and left blank. Free: TinyFish's Search API doesn't use credits, unlike their Agent/Browser APIs.
.env as TINYFISH_API_KEYenrich-companies runs fine without it, it just won't attempt this third passThis is the source behind the DOL LCA Match/Last Sponsored columns (see How AI Fits In above for what those fields mean and, just as important, what they don't guarantee). Unlike everything else in this Appendix, there's no account and no key. It's public U.S. Department of Labor data, but it does need a bit more manual care to keep current.
Prerequisites
Downloading a file
.xlsx (it's large, budget a few minutes)lca_data/ folder in the project root (already covered by .gitignore; these files are large and shouldn't be committed)Feeding it into Beacon
python -m app.cli lca-enrich lca_data/FY2026_Q2.xlsx lca_data/FY2025_Q4.xlsx
Pass every file you want considered in the same command. It parses each one, merges them (keeping the most recent filing date per employer across all files given), matches every tracked company by name, and updates DOL LCA Match/Last Sponsored on both Sheets plus the underlying companies columns.
🔄 Keeping it current: there's no "training" here, just a snapshot you refresh by hand 📅 DOL adds a new quarter roughly every 3 months. Since each new quarter's file is cumulative for its fiscal year, re-running with just that newest file already picks up every new filing from that fiscal year, so you don't need to re-download older quarters of the same year.
✅ It's safe to run lca-enrich with just one new file at a time as quarters come out. A matched company's stored date is only ever updated forward. If the date already in the database for a company is more recent than what the current run's file(s) show for it, the existing date is left alone rather than being overwritten backward in time (so accidentally re-feeding an older fiscal year's file after a newer one is already loaded can't roll anything back).
💾 Still worth keeping every file you've downloaded in lca_data/ rather than deleting them. There's no persisted history of which files have already been applied, so if you ever need to rebuild the database from scratch, you'll want the full set on hand again.
MIT. See LICENSE.
[!CAUTION]
- Not legal or immigration advice. I'm not a recruiter, employer, or immigration attorney, and nothing here guarantees any company will actually sponsor you.
- The "Likely work visa sponsor" signal is not a promise. It's a positive historical indicator from public government filing data. A company can have a strong filing history and still not sponsor for a specific role.
Beacon classifies what one specific job posting's own text says about sponsorship, nothing more. It doesn't offer you a job, and doesn't maintain any personally curated list of "companies that sponsor visas": there is no such list. The one exception is the DOL LCA match, which is real public government filing data, not a curated opinion, and even that is a "likely sponsors" positive indicator, never a guarantee, since a company's past filing says nothing about whether this specific posting will sponsor. Every classification comes from analyzing that posting's text at the moment it was ingested, the same way you'd read it yourself, just automated. For any real decision about your immigration status, talk to a qualified immigration attorney, not this tool or its output.
Built for anyone tired of finding out on page 3 of a job description that they were never going to be considered.
⚠️ Experimentelle Skill-Sammlung für deutsches Recht (Arbeits-, Gesellschafts-, Insolvenz-, Datenschutz-, Prozessrecht u
Manage multiple Claude Code agents from TUI or Web with tmux and git worktrees
Project management using GitHub Issues + Git worktrees for parallel agent execution
Core skills library for Claude Code with 20+ battle-tested skills including TDD, debugging, and brainstorming