GigWatch
A self-hosted freelance-gig watcher. GigWatch monitors live job/gig feeds, filters them against your skills, remembers what it has already shown you, and alerts you (console, email, or Slack) only when a new matching gig appears.
Stop refreshing Upwork/Remotive/LinkedIn every 20 minutes. Point GigWatch at the feeds you care about, tell it what you do, and let it ping you when something relevant lands.
$ gigwatch scan
scanned 17 job(s) from 1 source(s); 4 match filter; 2 new
GigWatch: 2 new matching gig(s)
1. Senior Python Backend Engineer
company: Acme Digital
salary: $120k-$150k
where: Remote (Worldwide)
matched: python, backend
score: 9.0
https://remotive.com/remote-jobs/...
Why this exists
Freelancers lose real money to latency — the best gigs get filled in the first hours. Existing monitors (Distill, PageCrawl, Apify's Upwork monitor) are hosted SaaS that scrape your sessions and cost monthly. GigWatch is:
- Self-hosted & private — your skills, your feeds, your machine. No account, no session scraping, no data leaving your box.
- Free & open (MIT) — the core is a small, readable Python CLI.
- Portable — run it on a laptop, a $5 VPS, or in a cron job.
Features
- Multiple feed sources — Remotive, We Work Remotely, RemoteOK, and Hacker News "Who is Hiring?" (built-in, no auth), any RSS/Atom feed, or any JSON endpoint returning a list of job objects.
- AI job ranking —
gigwatch rankscores every match 0-100 against a profile you write (role, skills, location, notes) and explains why. Uses an LLM whenOPENAI_API_KEYis set (any OpenAI-compatible endpoint); otherwise a deterministic, dependency-free heuristic. Either way you get a ranked shortlist, not a raw dump. - Output formats —
scan,list, andranktake--format text|markdown|json|html. The HTML output is a single self-contained page (inline CSS, no external assets) you can save, email, or publish to GitHub Pages as a live demo. - Batched digests —
gigwatch digestcatches every new match the moment it appears but delivers one consolidated alert per period (default: daily) instead of a ping per scan. The mode that powers a hosted offering: runwatchon a short interval to keep the buffer fresh,digeston a long interval to flush it. - Self-contained hosted server —
gigwatch serveboots a live, shareable dashboard (auto-refreshing HTML) plus machine-readable/api/jobs(JSON) and/feed(RSS) endpoints on one port, with a background refresh loop and a/healthprobe. Stdlib-only, one command — this is the engine behind the hosted "$29/mo" tier and a demo you can point anyone at. - Skill-based filtering — keyword matching (any/all), category and location filters, exclude-list, and a relevance score (title hits weigh more than body hits).
- Dedupe by state — a local JSON state file tracks what you've already seen, so you're only ever alerted on new matches. State auto-prunes.
- Alerts — console (default), SMTP email, or Slack (webhook or chat API). Secrets come from environment variables, never the config file.
- Zero dependencies — pure Python standard library. If you can run
python3, you can run GigWatch.
Quick start
No install required — it's stdlib-only:
# 1. Get the code
git clone https://github.com/earnnova-dev/gigwatch
cd gigwatch
# 2. Create a starter config (or copy config.example.json to config.json)
python3 -m gigwatch init
# 3. Edit config.json: put YOUR skills in filters.keywords
# e.g. ["python", "backend", "api", "django"]
# 4. See what would match right now (dry run, no state touched)
python3 -m gigwatch list
# 5. Do a real scan: alerts on new matches, remembers them
python3 -m gigwatch scan
Or install it as a proper command:
pip install . # or: pipx install .
gigwatch init && gigwatch scan
Run it continuously
# Loop forever, re-scanning every poll_interval seconds (default 15 min):
gigwatch watch
# Or use cron on a VPS (once an hour):
0 * * * * cd /opt/gigwatch && /usr/bin/python3 -m gigwatch scan
Batched digests (one email per day)
watch pings you the instant a match lands — great for a personal job hunt,
noisy for a recruiter or a job board scanning every 15 minutes. digest
catches every new match as it appears but delivers one consolidated alert
per period:
# Catch: keep the buffer fresh (e.g. every 15 min)
*/15 * * * * cd /opt/gigwatch && /usr/bin/python3 -m gigwatch watch --interval 900
# Deliver: one email per day with everything new since the last digest
0 9 * * * cd /opt/gigwatch && /usr/bin/python3 -m gigwatch digest --period 86400
The buffer lives in gigwatch-digest.json (override with --buffer). Use
--force to flush immediately, or --period 3600 for hourly digests.
Run it as a live hosted service
serve turns GigWatch into a small web service with a single command. It
starts a background refresh loop (fetch → filter → cache) and serves the
current matches on one port:
# Local (default, 127.0.0.1:8765), refreshing every 15 min:
gigwatch serve
# Expose it on a VPS and gate the API/feed with a bearer token:
gigwatch serve --host 0.0.0.0 --port 8080 --refresh 900 --token "s3cret"
Endpoints:
| Path | What it returns |
|---|---|
/ |
A live HTML dashboard (auto-refreshes in the browser). Public by design — it's the shareable link. |
/api/jobs |
The current matches as JSON (for integrations / scrapers). |
/feed |
The current matches as an RSS 2.0 feed (subscribe in any feed reader). |
/health |
Liveness probe: version, uptime, last refresh, match count, source errors. |
The dashboard is public so you can share it as a link; pass --token to
require Authorization: Bearer <token> on /api/jobs and /feed while
leaving the dashboard and /health open. It is a read-only window onto
the latest matches — it does not send email/Slack and does not touch the
seen-state file, so a hosted instance never double-delivers alerts. Put it
behind a reverse proxy (nginx/Caddy) with TLS for a real deployment.
Rank matches by fit
rank fetches and filters like scan, then scores every match 0-100 against
a profile and explains the score. It uses an LLM when OPENAI_API_KEY is set
(any OpenAI-compatible endpoint; the model is auto-discovered), otherwise a
deterministic heuristic — so it works with no key at all.
# Use the profile from config.json:
gigwatch rank
# Or pass a profile on the command line:
gigwatch rank --title "Senior Python Engineer" \
--skills "python,backend,api" \
--location remote --notes "senior, \$150k+"
# Force the offline heuristic (no LLM call):
gigwatch rank --no-ai --format markdown
Alerts
Console is on by default. To also get email/Slack, fill in the alerts
section of config.json and set the env vars it references:
export GIGWATCH_EMAIL_TO=you@yourdomain.com
export GIGWATCH_SMTP_USER=...
export GIGWATCH_SMTP_PASS=...
export GIGWATCH_SLACK_WEBHOOK=https://hooks.slack.com/services/...
The ${VAR} placeholders in the config are expanded from the environment at
load time, so secrets never live in the file (and the file is safe to commit
if you want).
Configuration
See config.example.json for a fully annotated
example. The top-level keys:
| Key | Meaning |
|---|---|
sources |
List of feeds to watch. {"type":"remotive"}, {"type":"wwr"}, {"type":"remoteok"}, {"type":"hn"}, {"type":"rss","url":...}, or {"type":"json","url":...}. |
profile |
Optional candidate profile for rank: {"title","skills":[...],"location","notes"}. |
filters.keywords |
Your skills. A job matches if it contains any of these (or all, with require_all_keywords). |
filters.categories / filters.locations |
Optional extra filters (empty = match anything). |
filters.exclude_keywords |
Words that disqualify a job (e.g. "intern", "junior"). |
filters.min_score |
Relevance floor. Title hits = 3 pts, body hits = 1 pt per keyword. |
alerts |
console, email, slack (see above) and max_per_alert. |
state_file |
Where seen-jobs are remembered (default gigwatch-state.json). |
poll_interval |
Seconds between scans in watch mode (default 900). |
Adding your own JSON source
Any endpoint that returns a JSON array of objects works. Each object should
have at least title and url; company/company_name, category,
location/country, salary, tags, and description are picked up if
present. Wrap in {"jobs":[...]}, {"data":[...]}, {"results":[...]}, or
{"items":[...]} and it still works.
Commands
| Command | What it does |
|---|---|
gigwatch init |
Write a starter config.json. |
gigwatch list |
Dry run — fetch + filter + print matches. Does not touch state. |
gigwatch scan |
Fetch, filter, alert on new matches, and record them as seen. |
gigwatch rank |
Fetch + filter, then rank matches 0-100 for your profile (AI or heuristic). |
gigwatch watch |
Loop scan every poll_interval seconds. |
gigwatch serve |
Self-contained hosted instance: live dashboard + JSON + RSS + health on one port. |
gigwatch reset |
Clear the seen-state (next scan alerts on everything that matches). |
Useful flags: --config PATH (default config.json), -v/--verbose,
--max-age-days N (state pruning; 0 keeps everything), and
--format text|markdown|json on scan/list/rank. For rank, also
--profile FILE (a JSON profile), --skills a,b,c, --title, --location,
--notes, and --no-ai (force the deterministic heuristic engine).
How it works
sources ──fetch──> [Job, Job, ...]
│
filter_jobs() (keywords / category / location / score)
│
[ScoredJob, ...]
│
compare against state file
│
┌──────┴──────┐
new jobs already seen
│ │
alert (console/ drop silently
email/slack)
│
mark seen in state file ──> gigwatch-state.json
The state file is plain JSON ({job_id: first_seen_utc}) so you can inspect
it, back it up, or move it between machines.
Roadmap / ideas
More built-in sources (Hacker News "Who is hiring")— done in 0.2.0.AI ranking: summarize each match and rank by fit to a profile— done in 0.2.0 (gigwatch rank).- LinkedIn via RSS, Upwork via a user-supplied export.
- Draft proposals / cover letters per match.
A tiny hosted tier (the natural monetization path — see below).— done in 0.4.0:gigwatch serveis a self-contained hosted instance (live dashboard + JSON + RSS + health on one port, stdlib-only).
Monetization
The core is free and MIT-licensed. The paid tier is a hosted GigWatch:
we run gigwatch serve on our infra behind TLS, you get a live shareable
dashboard, a JSON/RSS feed, and email/Slack alerts (via digest) without
running anything. Three tiers:
- Free (self-hosted) — the full CLI, MIT, stdlib-only.
- $29/mo (hosted) — we run your instance, you get the live dashboard + feed + daily digest alerts, no infra to manage.
- $99 (custom setup) — one-time: we configure a dedicated instance for your niche (sources, filters, profile, alerts) and hand it over.
License
MIT — see LICENSE.
Contact
For questions, hosting-tier interest, or support, open an issue — that's the fastest route. Email works too: novagw[at]uberip[dot]com.
Contributing
Issues and PRs welcome. The code is deliberately small and stdlib-only;
keep it that way. Run pytest (unit) and pytest -m live (hits the real
Remotive API) before sending a PR.
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 gigwatch_nova-0.4.0.tar.gz.
File metadata
- Download URL: gigwatch_nova-0.4.0.tar.gz
- Upload date:
- Size: 48.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
962ab7a4cd7e5b97671831b7a239a3968375aa3d9523bb85ce6082e1a9f822cd
|
|
| MD5 |
1272b256729b841b90f1d90f2b4d68cf
|
|
| BLAKE2b-256 |
3614d99cb69e11bcdef8cd1527064531d8413e9ec33d0d0c03e5437c90cac66e
|
File details
Details for the file gigwatch_nova-0.4.0-py3-none-any.whl.
File metadata
- Download URL: gigwatch_nova-0.4.0-py3-none-any.whl
- Upload date:
- Size: 39.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62b7a68d247046b5957d442c260e5ad89c79a40dd428803602cb3202bb177001
|
|
| MD5 |
d6cffacb0f147cd2aea07c810dfe0607
|
|
| BLAKE2b-256 |
2f788a6af7be89908e36b2402de722644ad32714b84e17d54c540d378373d3b9
|