boardwatch
A self-hosted job radar that reads the fine print. Point it at the companies you care about. boardwatch watches their official ATS job boards, catches new postings early, and ranks them against your profile with an explainable score. It also reads each posting for the hard eligibility requirements that quietly rule people out (visa sponsorship, security clearance, a required degree, years of experience, a location) and flags the ones you could not actually apply to, each backed by the exact sentence it read as evidence. Nothing is guessed, nothing phones home, and it all runs on your own machine.
Status: pre-release, under active development. No auto-apply, ever. No telemetry. No accounts, no API keys. Your data stays in a local SQLite file.
$ boardwatch top
# Title Company Score Eligible Why
12 Senior Backend Engineer Stripe 0.86 no flags covers 9/11 skills · title · 1d
7 Software Engineer, Platform Linear 0.81 check covers 7/10 skills · title · 3d
33 Backend Engineer (Payments) Ramp 0.74 no flags covers 6/9 skills · 2d
5 Full-Stack Engineer Supabase 0.68 no flags covers 5/8 skills · title · 6d
18 Infrastructure Engineer OpenAI 0.61 check covers 4/9 skills · 4d
2 postings hidden as ineligible (run with --include-ineligible to see them).
(Illustrative output. # is the posting id; pass it to boardwatch show <id> for the
full posting, a per-component score breakdown, and the eligibility audit with quotes.
"no flags" means no catalogued disqualifier was found, not that you are cleared to
apply; "check" means the posting was ambiguous.)
Why boardwatch?
Job boards optimize for their advertisers, not for you. LinkedIn/Indeed bury fresh roles under sponsored noise and stale reposts; paid trackers put a subscription (and their servers, and your search history) between you and postings that are already public.
boardwatch takes the direct route. Greenhouse, Lever, and Ashby each expose a public, keyless JSON endpoint for every board they host, the same data the company's own careers page renders. boardwatch polls those endpoints politely, on your schedule, and tells you what's new since last time.
| boardwatch | LinkedIn/Indeed | Paid trackers | |
|---|---|---|---|
| Source of truth | company's own ATS | aggregated + sponsored | aggregated |
| Freshness | as fast as you poll | ranking-dependent | vendor-dependent |
| Reads eligibility | audit + quoted proof | no | no |
| Your data | local SQLite, yours | the product | on their servers |
| Cost | free (self-hosted) | free-ish (ad-driven) | subscription |
| Auto-apply / spam | never | no comment | sometimes |
Honest limits. boardwatch only covers companies hosted on Greenhouse, Lever, or Ashby (a large slice of tech, but not everyone, no Workday/Taleo/etc. yet). It reads exactly what those APIs expose. It is pre-release: expect rough edges, and read Responsible use before pointing it at boards you don't own.
Quickstart (≈2 minutes to your first shortlist)
pipx (recommended)
pipx install boardwatch # isolated, on your PATH
boardwatch init # pick a starter set of companies + paste your profile
boardwatch scan # poll the watched boards (polite, conditional GETs)
boardwatch top # ranked shortlist
boardwatch init is interactive. Pick [1] Starter set to watch a curated group of
well-known boards in one keystroke, [2] Search registry to pick from the bundled
catalog, or [3] Paste any provider:slug or board URL. Then paste your résumé text
and a few target/exclude titles and locations. A cold init → scan → top lands your
first ranked shortlist in well under ten minutes.
Docker
docker run --rm -v boardwatch-data:/data \
ghcr.io/mit112/boardwatch:latest --data-dir /data init
docker run --rm -v boardwatch-data:/data \
ghcr.io/mit112/boardwatch:latest --data-dir /data scan
docker run --rm -v boardwatch-data:/data \
ghcr.io/mit112/boardwatch:latest --data-dir /data top
From source
git clone https://github.com/mit112/boardwatch && cd boardwatch
uv sync # https://docs.astral.sh/uv/
uv run boardwatch init
How it works
init ──▶ scan ──▶ top ──▶ show <id>
│ │ │ │
companies fetch rank on full posting +
+ profile boards demand score breakdown
init: one-time setup. Choose companies (starter set / registry search / paste), then your profile (résumé text, target titles, excludes, locations, remote-only).scan: fetches every watched board through a polite fetcher (per-host pacing, retries with backoff, conditionalIf-None-Match/If-Modified-Sinceso unchanged boards cost a304), then applies each board transactionally. Prints a one-line summary.top [N]: ranks open postings against your profile right now (weights are read live), newest-and-most-relevant first, with a one-line "why".show <id>: the full posting plus a per-component score table (skill coverage, title match, recency, location fit) and the eligibility audit with quoted evidence.eligibility:facts/policyto tell boardwatch your situation,runto evaluate open postings,summaryfor a funnel of what the catalog matched. See Eligibility audit.companies:add/remove/search/list/import/exportyour watched boards.boardwatch companies add https://boards.greenhouse.io/acmejust works.doctor: per-board connectivity and freshness, plus a local DB integrity check.config show/config set: tune politeness and ranking weights (see below).
Ranking, briefly
Each posting gets a 0–1 score: a weighted blend of skill coverage, title match
(fuzzy), recency (exponential decay), and location fit. Undefined components
renormalize away, so a sparse profile still ranks sensibly. Nothing is precomputed:
change a weight and the next top reflects it. show <id> prints the exact arithmetic.
Eligibility audit
Ranking tells you how well a posting fits. The eligibility audit tells you whether you could apply at all, and shows its work.
You describe your situation once, in the catalog's own vocabulary:
boardwatch eligibility facts set work_authorization.status citizen
boardwatch eligibility facts set highest_degree bachelor
boardwatch eligibility policy set visa_sponsorship blocker # treat this family as disqualifying
Then boardwatch eligibility run reads each open posting for catalogued requirements
(visa sponsorship, security clearance, degree, years of experience, location, and more),
resolves them against your facts, and stores a verdict. boardwatch show <id> prints it
with the receipts:
Eligibility: ineligible
unmet · required: work authorization / sponsorship
quote: "This role is not able to sponsor employment visas now or in the future."
Three properties are deliberate:
- Evidence-linked. Every requirement carries the exact sentence it was read from, sliced from the posting version that was evaluated. Nothing is paraphrased or invented.
- Deterministic. The same posting and the same facts always produce the same verdict. There is no language model in the loop and nothing to hallucinate: the vocabulary and rules are a versioned catalog, and a verdict is invalidated and recomputed only when your profile or the catalog changes.
- Honest. A clean posting reads as "no flags", never as a guarantee. "no flags" means only that no catalogued disqualifier was found, not that you are cleared to apply, and ambiguous wording reads as "check" rather than a false all-clear.
boardwatch eligibility summary shows the funnel (how many postings were evaluated, the
verdict split, and what fired per family) so you can watch the catalog working before you
trust a hidden count. By default top hides postings that are ruled out and reports the
count; top --include-ineligible shows them.
What changed since you last looked
boardwatch scan records every appearance, disappearance and body revision in an
append-only ledger. digest reads the ledger from wherever you left off:
boardwatch digest # new, reopened, updated, and a closed count
boardwatch digest --peek # the same view without consuming it
boardwatch top --new # rank only the postings first seen since your last digest
The cursor is an event id, not a timestamp, so a clock change or a missed day cannot skip or repeat a window.
Your funnel
boardwatch never applies for you. It records what you did, so the state stays yours:
boardwatch track add 42 # start tracking a posting
boardwatch track status 1 applied # move it, with an immutable ledger entry
boardwatch track status 1 interviewing --note "phone screen booked"
boardwatch track list --status applied
boardwatch track log 1 # the full history for one application
Take your data with you
boardwatch export --format jsonl --out postings.jsonl
boardwatch export --format csv
Every row carries the posting, your funnel state, and the eligibility verdict together with the profile and rules hashes that identify the evaluation it was computed under. This is a flat snapshot, not a full audit trail; it does not support independent recomputation of verdicts.
Configuration
boardwatch config show prints every key, its value, and its default;
boardwatch config set <key> <value> changes it (validated at set time and load time).
| Key | Range | Default | Effect |
|---|---|---|---|
per_host_delay_seconds |
≥ 0.25 | 1.0 | politeness between requests to one host |
retry_attempts |
1–10 | 3 | retries on transient failures |
scan_workers |
1–8 | 4 | concurrent boards per scan |
weights.skill_coverage |
0–1 | 0.50 | ranking weight |
weights.title_match |
0–1 | 0.25 | ranking weight |
weights.recency |
0–1 | 0.15 | ranking weight |
weights.location_fit |
0–1 | 0.10 | ranking weight |
See docs/configuration.md for details.
Schedule scans
Run boardwatch init once interactively before scheduling scans. The scheduler must run as the same user that ran init, so it reads the same local profile and database. Start with a daily scan; the default politeness settings are designed for that cadence.
Each example below appends the scan summary to a log. Replace /absolute/path/to/boardwatch with the output of command -v boardwatch, then run the command once manually before enabling its timer.
cron (Linux or macOS)
Create a log directory, then add a daily job with crontab -e:
$ mkdir -p "$HOME/.local/state/boardwatch"
# Run every day at 08:00 local time.
0 8 * * * /absolute/path/to/boardwatch scan >> "$HOME/.local/state/boardwatch/scan.log" 2>&1
Cron has a deliberately small environment. If you set BOARDWATCH_DATA_DIR or BOARDWATCH_CONFIG_DIR when running boardwatch normally, define the same values above the job in the crontab.
launchd (macOS)
Save this as ~/Library/LaunchAgents/com.boardwatch.scan.plist, replacing the boardwatch path and the home-directory placeholder. The standard output and error paths must use absolute paths.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.boardwatch.scan</string>
<key>ProgramArguments</key>
<array>
<string>/absolute/path/to/boardwatch</string>
<string>scan</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>8</integer>
<key>Minute</key><integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string><home>/Library/Logs/boardwatch-scan.log</string>
<key>StandardErrorPath</key>
<string><home>/Library/Logs/boardwatch-scan.log</string>
</dict>
</plist>
Load it and confirm its status:
$ launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.boardwatch.scan.plist
$ launchctl print "gui/$(id -u)/com.boardwatch.scan"
After editing the file, reload it with launchctl bootout "gui/$(id -u)/com.boardwatch.scan" followed by the bootstrap command above.
systemd user timer (Linux)
Create ~/.config/systemd/user/boardwatch-scan.service:
[Unit]
Description=Scan watched job boards with boardwatch
[Service]
Type=oneshot
ExecStart=/absolute/path/to/boardwatch scan
Then create ~/.config/systemd/user/boardwatch-scan.timer:
[Unit]
Description=Run boardwatch scan every day
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer and inspect the most recent run:
$ systemctl --user daemon-reload
$ systemctl --user enable --now boardwatch-scan.timer
$ systemctl --user list-timers boardwatch-scan.timer
$ journalctl --user -u boardwatch-scan.service --since today
Persistent=true runs a missed daily scan after the next login. To keep user timers running after logout, enable lingering for the account with loginctl enable-linger "$USER".
Supported boards
| Provider | Public endpoint boardwatch reads | Auth |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/<slug>/jobs |
none |
| Lever | api.lever.co/v0/postings/<slug> |
none |
| Ashby | Ashby public job-board posting API | none |
boardwatch ships a bundled registry of verified public boards (35+ companies, with a
curated starter set), so init works offline out of the box. You can watch any board
these providers host, not just the registry, with companies add. The registry is
community-maintainable by PR; see
src/boardwatch/registry/README.md.
Responsible use & legality
boardwatch reads the same public, keyless endpoints that power each company's own careers page: it does not scrape rendered HTML, log in, or bypass any access control. That is deliberately the least-invasive way to get this data. Still, these are third-party services, and using them responsibly is on you:
- Keep the politeness defaults. The defaults (≥1 request/sec per host, conditional
GETs, bounded retries, a descriptive User-Agent) are intentionally gentle. Don't crank
scan_workersup orper_host_delay_secondsdown to hammer a board. - It's for personal job-search use, not bulk data resale or redistribution of posting content. boardwatch stores postings locally for your review.
- Provider terms & rate limits can change and may restrict automated access. You are responsible for complying with each provider's Terms of Service. If a provider asks you to stop, stop.
- No warranty. These are undocumented-stability public endpoints; they can change or break without notice.
If you're unsure whether your use is appropriate, err toward watching fewer boards, less often. A job seeker checking a dozen companies once a day is the intended shape.
Privacy & data
- Local-first. Its primary store is one SQLite database in your platform data directory;
the opt-in LLM tier also caches raw responses there as plain files on disk (override with
--data-dir). No server, no account, no cloud. - No telemetry. boardwatch phones home to nobody.
- One optional secret. The default path authenticates to nothing. The opt-in LLM tier
reads
BOARDWATCH_LLM_API_KEYfrom the environment only, and sends only public job description text, never your profile. See SECURITY.md.
Roadmap
- PyPI + GHCR published releases (
pipx install boardwatch,docker run …) - Notifications on new matches (desktop / webhook)
-
digestandtop --newchange detection (only what changed since last run) - More ATS providers (community-driven)
- Data-portability export (
--format jsonl|csv)
Have a company on a board boardwatch doesn't reach yet, or an ATS you want supported? Open an issue.
Contributing
Contributions welcome: code, registry entries, or bug reports. See
CONTRIBUTING.md for dev setup (uv sync, make check) and
the registry guide for adding a company board.
All changes land via PR against a branch-protected main.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file boardwatch-0.1.0.tar.gz.
File metadata
- Download URL: boardwatch-0.1.0.tar.gz
- Upload date:
- Size: 411.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4fab95a466d752592903c815c9a81a66f6bcd78bc41b4d40496e40f85b5d19c4
|
|
| MD5 |
0d9e2d110b658d0a3ee8519e4010b2ac
|
|
| BLAKE2b-256 |
105a63294d9a9cdda4170532cda125a5f873b14a091fad9c823702573cebedcc
|
Provenance
The following attestation bundles were made for boardwatch-0.1.0.tar.gz:
Publisher:
release.yml on mit112/boardwatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boardwatch-0.1.0.tar.gz -
Subject digest:
4fab95a466d752592903c815c9a81a66f6bcd78bc41b4d40496e40f85b5d19c4 - Sigstore transparency entry: 2313548502
- Sigstore integration time:
-
Permalink:
mit112/boardwatch@a65baf2c6a0be08d27e69cd88b43dadb8c488293 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mit112
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a65baf2c6a0be08d27e69cd88b43dadb8c488293 -
Trigger Event:
push
-
Statement type:
File details
Details for the file boardwatch-0.1.0-py3-none-any.whl.
File metadata
- Download URL: boardwatch-0.1.0-py3-none-any.whl
- Upload date:
- Size: 178.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bdb54d3972f159719bacc212d552ac154f308bace74623f0c2404f183be72d6
|
|
| MD5 |
dba8e62055204f46165e4ce2e4e77a67
|
|
| BLAKE2b-256 |
4c462083217e3f10e9ec0d50c8d14228a1cd0738c7759aee3016b2dc2527013e
|
Provenance
The following attestation bundles were made for boardwatch-0.1.0-py3-none-any.whl:
Publisher:
release.yml on mit112/boardwatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boardwatch-0.1.0-py3-none-any.whl -
Subject digest:
6bdb54d3972f159719bacc212d552ac154f308bace74623f0c2404f183be72d6 - Sigstore transparency entry: 2313548513
- Sigstore integration time:
-
Permalink:
mit112/boardwatch@a65baf2c6a0be08d27e69cd88b43dadb8c488293 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mit112
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a65baf2c6a0be08d27e69cd88b43dadb8c488293 -
Trigger Event:
push
-
Statement type: