Finds entry-level postings at employers with real H-1B filing history, and drops the ones that disqualify you outright.
It joins two sources that cannot go stale on someone else's schedule:
- DOL OFLC LCA disclosure data. Quarterly, official, a legal filing requirement. Free bulk download, no API key.
- Public ATS job board APIs (Greenhouse, Lever, Ashby). Served straight from the employer, no aggregator in between.
The core workflow is:
- Load the latest LCA disclosure data.
- Discover employers with supported public job boards.
- Pull their current postings.
- Filter and rank the results.
- Optionally generate a personalized report, sync it to Google Sheets, and send email notifications through GitHub Actions.
Clone the repository and create a virtual environment.
git clone https://github.com/pruhnav/sponsorscan.git
cd sponsorscan
python -m venv .venv
Set-ExecutionPolicy -Scope Process Bypass
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txtThe execution-policy change applies only to the current PowerShell window. When you open a new terminal later, return to the repository and activate the environment again:
cd "C:\path\to\sponsorscan"
Set-ExecutionPolicy -Scope Process Bypass
.\.venv\Scripts\Activate.ps1git clone https://github.com/pruhnav/sponsorscan.git
cd sponsorscan
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txtGo to https://www.dol.gov/agencies/eta/foreign-labor/performance and download
the most recent LCA Programs (H-1B, H-1B1, E-3) disclosure file. It is an
.xlsx file and is typically 100-400 MB.
The exact filename changes each quarter. Replace the example filename below with the file you downloaded.
python sponsorscan.py load-lca "$HOME\Downloads\LCA_Disclosure_Data_FY2026_Q2.xlsx" --replaceIf the filename contains spaces, keep the path inside quotation marks.
python sponsorscan.py load-lca ~/Downloads/LCA_Disclosure_Data_FY2026_Q2.xlsx --replaceThis takes a few minutes. It builds a local SQLite database of employers with certified, denied, and withdrawn counts, the roles they filed for, and the states in which they filed.
Re-run this step when the DOL publishes a new quarterly disclosure file.
python sponsorscan.py discoverThis is what keeps the tool from being a hand-typed watchlist. It reads the
employers you just loaded, keeps the ones nationwide that filed for tech and
data roles, guesses job-board slugs from their legal names, probes all three
supported providers, and writes every confirmed board into companies.yaml.
The first run can take a while because it may perform thousands of HTTP probes.
Results are cached in the database as they land, so Ctrl+C is safe and a later
run resumes from the cache.
| Flag | Effect |
|---|---|
--states CA,NY,WA |
Restrict discovery to selected states; blank is nationwide |
--roles "machine learning,data scien" |
Use custom title keywords; blank uses the default tech/data set |
--min-certified 15 |
Require heavier sponsorship history; default is 5 |
--max-certified 2000 |
Skip employers above the specified number of certified LCAs; 0 removes the cap |
--no-merge |
Overwrite companies.yaml instead of adding to it |
Only three major ATS providers are currently covered. Workday, Taleo, iCIMS, and SmartRecruiters are not, which is why some large enterprises, universities, and hospitals will be missing.
python sponsorscan.py fetch-jobs --replaceThis reads the boards listed in companies.yaml, fetches current postings, and
stores them in sponsorscan.db.
Use --replace when you want the jobs table to represent the latest full fetch.
Run the same command again whenever you want refreshed postings.
python sponsorscan.py report --out matches.csvThe report defaults to locations anywhere in the United States. Pass
--locations to narrow it.
| Flag | Effect |
|---|---|
--locations "san francisco,santa clara,remote" |
Match comma-separated location substrings |
--any-location |
Keep postings that miss the location filter instead of dropping them |
--sponsors-only |
Drop employers with no certified LCAs on record |
--include-senior |
Stop filtering Senior, Staff, Principal, and Director titles |
--top 40 |
Control how many rows print in the terminal; the CSV still contains every match |
The generated CSV is written in the current folder. It can be opened directly in Excel or imported into Google Sheets.
| Signal | Points |
|---|---|
| Employer has certified LCAs | +40 |
| 25 or more certified | +10 |
| Half or more filings at prevailing wage Level III/IV | +10 |
| A quarter or more filings denied or withdrawn | -10 |
| Has filed for a similar job title | +15 |
| Entry-level title | +20 |
| Location match | +15 |
| Posting explicitly states sponsorship | +10 |
Anything hitting a disqualifier is dropped before scoring, not penalized. Disqualifiers in the standard report include citizenship requirements, security clearance, ITAR/export-control restrictions, and sponsorship-negation language.
The standard report command ranks jobs primarily from employer sponsorship
history, title, and location. A personalized report can add stricter candidate
eligibility checks and resume-weighted ranking.
A personalized setup may account for:
- target role families;
- skills demonstrated on the candidate's resume;
- maximum acceptable experience requirements;
- degree requirements;
- seniority indicators;
- work authorization;
- company preferences;
- posting age;
- previously seen jobs.
The personalized report script is run after fetch-jobs and reads from the same
sponsorscan.db database:
python sponsor_daily_report.py `
--hours 48 `
--out matches_48h.csv `
--new-out new_jobs_48h.csvPowerShell uses the backtick character for line continuation. The same command can be entered on one line:
python sponsor_daily_report.py --hours 48 --out matches_48h.csv --new-out new_jobs_48h.csvOn macOS or Linux:
python sponsor_daily_report.py \
--hours 48 \
--out matches_48h.csv \
--new-out new_jobs_48h.csvThe report keeps a local state file so new_jobs_48h.csv contains jobs that were
not present in the previous run. Keep each user's state file separate when
running reports for more than one person.
Work-authorization filtering should be configured separately from resume scoring.
An OPT-oriented configuration should normally continue to consider jobs that say they do not provide future visa sponsorship, because the candidate may already have temporary employment authorization. It should still reject jobs that explicitly:
- exclude OPT, STEM OPT, or CPT candidates;
- require permanent or unrestricted work authorization;
- require U.S. citizenship or permanent residence;
- require an active security clearance;
- impose an incompatible ITAR or export-control restriction.
Whether a role is suitable for a STEM OPT extension also depends on the employer and the candidate's individual circumstances. Always verify the posting and the employer before applying.
A citizen-oriented configuration can keep postings that require U.S. citizenship or U.S.-person status. Citizenship does not automatically mean that the candidate holds an active security clearance, so clearance-required roles should remain separately configurable.
Resume skills, target roles, seniority, experience limits, and degree filters should still be personalized for the individual candidate.
A Google Sheets integration can replace the contents of two worksheet tabs after each report:
All Matches 48hNew Jobs 48h
A typical uploader reads:
GOOGLE_SERVICE_ACCOUNT_JSON
GOOGLE_SPREADSHEET_ID
from environment variables or GitHub Actions secrets.
The Google service account must be given Editor access to the destination spreadsheet. Do not commit the service-account JSON file or its contents to the repository.
A workflow step can call the uploader after report generation:
- name: Update Google Sheet
env:
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
GOOGLE_SPREADSHEET_ID: ${{ secrets.GOOGLE_SPREADSHEET_ID }}
run: python update_google_sheet.pyIf the uploader uses the Google Sheets API, add its required client libraries to
requirements.txt.
Email notifications can be sent only when new_jobs_48h.csv contains at least
one job. A Gmail-based sender typically reads:
GMAIL_ADDRESS
GMAIL_APP_PASSWORD
NOTIFICATION_EMAIL
from environment variables or GitHub Actions secrets.
GMAIL_ADDRESS must be a real Gmail account used to send the message. It is not
the Google Sheets service-account address. GMAIL_APP_PASSWORD must belong to
the same Gmail account.
A workflow step can call the sender after the Google Sheets update:
- name: Email new job matches
env:
GMAIL_ADDRESS: ${{ secrets.GMAIL_ADDRESS }}
GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}
NOTIFICATION_EMAIL: ${{ secrets.NOTIFICATION_EMAIL }}
run: python send_job_email.pyDo not commit email passwords, app passwords, recipient addresses, or other private configuration to the repository.
The complete process can run on a schedule through GitHub Actions:
- Check out the repository.
- Install Python dependencies.
- Restore or build
sponsorscan.db. - Fetch current postings.
- Generate the personalized report.
- Update Google Sheets.
- Send an email when new matches exist.
- Upload the CSV files as workflow artifacts.
- Commit the updated state file when it changes.
A scheduled workflow can also include workflow_dispatch so it can be tested
manually from the Actions tab.
Store credentials under:
Repository Settings
-> Secrets and variables
-> Actions
Never place secret values directly in the workflow YAML.
python selftest.pyThe same command works on Windows, macOS, and Linux while the virtual environment is active.
The network code was originally written in a sandbox where dol.gov and the ATS
endpoints were unavailable. The parsing, matching, filtering, and ranking logic
is covered by selftest.py. If a provider changes its JSON structure, the
corresponding fetch_* function is the first place to inspect.
An LCA filing is not a job offer or a guarantee. It means the employer filed for someone at some point in the disclosure period. It is evidence of prior willingness to sponsor, nothing more.
Employer name matching is imperfect. DOL sees legal entity names, while job
boards show brand names. Instacart, for example, files as Maplebear Inc. Fuzzy
matching at cutoff 90 handles many differences, but set name: explicitly in
companies.yaml when you know the legal name and check the signals column for
fuzzy-match notes.
discover guesses slugs, and a guess can land on the wrong company. A probe
only proves that the board exists, not that it belongs to the employer whose
name produced the guess. Single-token guesses on multi-word names are
deliberately avoided, but collisions can still occur. If a company's postings
look unrelated to the company, remove its line from companies.yaml.
No employer is excluded by name. An earlier version filtered out anything whose name matched "consulting" or "staffing." That dropped Palantir Technologies Inc. as a false positive while keeping firms such as Tata Consultancy, Infosys, Accenture, and Cognizant. The available filing data is a better signal than a company-name heuristic.
Two CSV columns are especially useful:
lca_senior_wage_share— the share of filings at prevailing wage Level III/IV.lca_denied_withdrawn_rate— the share of filings that were denied or withdrawn.
Judge employers using those signals rather than company name alone.
Expect a low discovery hit rate. Most employers will not match any slug guess because they use a different provider or because their board URL does not resemble their legal name. A few hundred confirmed boards out of a few thousand candidates is still substantially broader than a hand-maintained list.
OPT candidates do not necessarily need immediate sponsorship. A candidate's current employment authorization and a company's willingness to sponsor later are separate questions. The eligibility filter matters for present applications; LCA history is evidence that may matter for a later transition.
Cap-exempt employers are underrepresented. Universities, university-affiliated nonprofits, and national laboratories often use ATS providers that SponsorScan does not yet support. Those employers may require a separate fetcher.
The following files and values should never be committed:
sponsorscan.db;- service-account JSON files;
- Gmail app passwords;
- personal resume data;
- private profile files;
- generated state files containing personal tracking history;
- local CSV reports, unless intentionally published.
Add local and personalized files to .gitignore before enabling automation.