Skip to content

Repository files navigation

🔭 Beacon: The Job Search Assistant for Work Visa Holders

💡 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.

📸 See it in action

Beacon sheet with job postings, visa flags, and decision columns

(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/.)

Python Anthropic APScheduler gspread SQLite License Tag

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.

🎯 Who this is for, and why

👥 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.

💸 Why not just pay for LinkedIn Premium?

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.

💰 Time and Cost Savings

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."


✨ Key Features

🌐 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


🧠 How AI Fits In

AI makes exactly two decisions in this pipeline. Everything else is free, deterministic code:

🛡️ Visa check: does this posting rule out sponsorship? Free pattern-matching runs automatically on every job. The genuinely unclear remainder lands in a "Needs Review" state and stops there; AI only runs on one of those if you flag it "Request Check" on the sheet, never automatically.

🎯 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.)


🏗️ Architecture

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 -->|ambiguous, free tiers only| NEEDSREVIEW["Needs Review (stops here)"]
    NEEDSREVIEW -->|user flags 'Request Check'| 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]
Loading

Full diagram with every field/table: docs/ARCHITECTURE.md


🛠️ Tech Stack

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

⚙️ Setup

Prerequisites

🐍 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.

Installation

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.

Finish setup

The rest genuinely needs your own accounts, so no script can do it for you:

  1. Activate the venv: .venv\Scripts\activate (Windows) or source .venv/bin/activate (macOS/Linux)
  2. Fill in your API keys and Sheet IDs in .env; see the Appendix for how to get each one
  3. Place your Google service-account JSON key at the path GOOGLE_SHEETS_CREDENTIALS_PATH points to
  4. Place your resume at resumes/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 nothing
  5. Edit seed_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)
  6. Run python -m app.cli seed-companies --file seed_companies.yaml
  7. Run python -m app.cli pipeline for one manual end-to-end run

seed_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.

Adding a company later, straight from the Sheet, no file editing

  1. On the Beacon sheet, add a new row
  2. Set that row's Job ID to SEED and Company to the name you want tracked; leave every other column blank
  3. Wait for the next scheduled run, or trigger it yourself right away: python -m app.cli seed-via-sheet
  4. It guesses and verifies a real job-board slug across Greenhouse, Lever, Ashby, and SmartRecruiters, and writes the outcome into that row's Title cell. On success, the company is added and its real postings often start flowing in on that same run; on failure, the Title cell explains why
  5. Either way, the SEED row itself is deleted automatically on the run after that. It was never a real posting, just a one-time onboarding request

Every 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.

Resource requirements & platform support

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

Running continuously

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.


📁 Project Structure

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.


🔐 Security

🔑 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


❓ FAQ

🎯 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 .txt then .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 .env file (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.


🛣️ Roadmap

🌍 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

Done for the worst offenders (salary refresh, cloud platforms refresh, company enrichment): each used to cost 2-4 Sheets API calls per job (a row lookup plus 1-3 writes), which was both a real latency cost on a large backlog and, since every write independently triggers Google's native email notification, the root cause of a live-reported notification flood. Now one row-lookup read plus one batched write per whole run, regardless of how many jobs it touches. Still open: add_job_to_beacon's append_row (one call per brand-new job passing the Filter Engine) and visa-scan/fit-scoring's per-job cell updates aren't batched yet -- lower priority since the latter two are already naturally throttled by LLM call latency between jobs, unlike the three fixed here which had no such throttle

🔄 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


🤖 Built With

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.


📎 Appendix: Getting Each API Key

Step-by-step for every credential .env.example asks for. No screenshots (UIs change and go stale; these steps don't).

Anthropic API key (ANTHROPIC_API_KEY)

  1. Go to console.anthropic.com and sign up or log in
  2. Go to Settings → API Keys
  3. Click Create Key, give it a name, and copy the value (starts with sk-ant-); you only get to see it once
  4. Add billing/credits if you haven't already (Settings → Billing). New accounts usually start with some free credit, but visa-scan and fit-scoring need an active balance to keep running past that
  5. Paste the key into .env as ANTHROPIC_API_KEY

Gemini API key (GEMINI_API_KEY), alternative to Anthropic

Same two jobs (visa classification, fit scoring), same forced-JSON-schema behavior, different provider billed.

  1. Go to aistudio.google.com and sign in with a Google account
  2. Click Get API Key → Create API key (no credit card required for the free tier)
  3. Copy the key into .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 (ADZUNA_APP_ID / ADZUNA_APP_KEY)

  1. Go to developer.adzuna.com and register for a free account
  2. Once verified, your dashboard shows your App ID and App Key directly, no extra steps
  3. Copy both into .env

Google Sheets service account (GOOGLE_SHEETS_CREDENTIALS_PATH, GOOGLE_SHEET_ID, GOOGLE_JOB_LOG_SHEET_ID)

This is the most involved one, but it's a one-time setup:

  1. Go to console.cloud.google.com and create a new project (or reuse an existing one)
  2. APIs & Services → Library: search for "Google Sheets API" and click Enable
  3. APIs & Services → Credentials → Create Credentials → Service Account: give it any name, no special roles needed for this
  4. Open the new service account → Keys tab → Add Key → Create New Key → JSON: this downloads a .json file
  5. Save that file into the project (e.g. as service_account.json) and point GOOGLE_SHEETS_CREDENTIALS_PATH at it
  6. Open the downloaded JSON and find the client_email field: it looks like something@your-project.iam.gserviceaccount.com
  7. Create two blank Google Sheets in your own Google account: one for active matches ("Beacon"), one for excluded jobs ("Job Log")
  8. Share each Sheet with that client_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
  9. Copy each Sheet's ID from its URL (the long string between /d/ and /edit) into .env as GOOGLE_SHEET_ID and GOOGLE_JOB_LOG_SHEET_ID

Turning on Google Sheet email notifications

This 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):

  1. Open the Beacon sheet in your own Google account (the one you shared with the service account, not the service account itself)
  2. Tools → Notification settings (older Sheets UI: Tools → Notification rules...)
  3. Set it to notify "Any changes are made"
  4. Choose "Email - right away" if you want a ping the moment a new job lands or a status changes, or "Email - daily digest" for one summary a day instead
  5. Save. You'll get emailed at your Google account's address any time the app (or you) writes to the sheet

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.

Financial Modeling Prep (FMP_API_KEY)

  1. Go to financialmodelingprep.com and sign up for a free account
  2. Your API key is shown directly on your dashboard
  3. Copy it into .env

StartupHub.ai (STARTUPHUB_API_KEY)

  1. Go to startuphub.ai and sign up for a free account
  2. Get your API key from your account dashboard
  3. Copy it into .env

TinyFish Search API (TINYFISH_API_KEY), optional

A 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.

  1. Go to agent.tinyfish.ai/api-keys and grab a free key
  2. Copy it into .env as TINYFISH_API_KEY
  3. Skip this entirely if you don't want it. enrich-companies runs fine without it, it just won't attempt this third pass

DOL LCA disclosure data (no signup, no API key, manual download)

This 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

  • Just a regular web browser. No signup, no key, no cost: this is public government data
  • A few hundred MB of free disk space, one fiscal year's cumulative file runs ~100–150MB (the real FY2026 Q2 file, measured live, was ~137MB / ~1M rows)
  • The download itself has to happen in your own normal browser session, not through this app. DOL's site blocks unattended/automated downloads (confirmed live: both this project's sandboxed browser tool and a plain scripted HTTP request were blocked)

Downloading a file

  1. Go to the OFLC Performance Data page
  2. Expand "Disclosure Data"
  3. Under "LCA Programs (H-1B, H-1B1, E-3)", find the fiscal year(s) you want. Each quarter's file is cumulative within its own federal fiscal year (Oct 1 – Sep 30), so for each fiscal year, download only its latest published quarter. Downloading every quarter of the same year adds nothing, since the newest one already contains all the earlier ones
  4. Click the file link to download the .xlsx (it's large, budget a few minutes)
  5. Repeat steps 3–4 for any additional fiscal years you want covered
  6. Save every file you download into a local 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.


📄 License

MIT. See LICENSE.


⚠️ Disclaimer

Caution

  1. Not legal or immigration advice. I'm not a recruiter, employer, or immigration attorney, and nothing here guarantees any company will actually sponsor you.
  2. 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.

About

Self-hosted Python job-search pipeline for H-1B & visa holders. You run it on your own laptop — no server, no sign-up, no data ever leaves your machine. Screens out visa-sponsorship dead ends automatically, AI only where it's actually needed.

Topics

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages