DEV Community

earnnova-dev
earnnova-dev

Posted on

I built a self-hosted gig watcher so I stop refreshing job boards — it only pings me on new matches

Freelancers lose real money to latency. The best remote gigs get filled in the first few hours — the "Senior Backend Engineer, $120k, fully remote" posting is usually taken before you finish your second coffee. So we all do the same embarrassing thing: keep 6 job boards open and refresh every 15–20 minutes, mostly seeing the same 300 stale listings.

I got tired of it, so I built GigWatch — a small, self-hosted watcher that monitors live job/gig feeds, filters them against your skills, remembers what it has already shown you, and pings you (console, email, or Slack) only when a new matching gig actually appears. No dashboard to babysit. No account to sign up for. Just a process on a machine you already run.

Install is one line:

pip install gigwatch-nova
Enter fullscreen mode Exit fullscreen mode

It's Python 3.9+, self-hosted, and pulls from free, keyless job endpoints (Remotive, We Work Remotely, RemoteOK, Jobicy, Hacker News, plus any RSS feed you point it at). No API keys.

The whole idea in four steps

GigWatch does one loop, on a timer you control (default 15 min):

  1. Fetch — pull live listings from each configured source.
  2. Score — match each listing against your skills/title/keywords and score the fit (0–10).
  3. Dedup — compare against a local state file of everything it has already shown you. This is the part that makes it not-spammy.
  4. Alert — only fire (console line, email, or Slack) for new matches above your score floor.

The dedup step is the whole point. A job board shows you the same 300 listings forever; GigWatch shows you a listing exactly once, and only if it matches your profile.

The config you actually write

Everything is a single JSON file. A realistic one looks like this:

{
  "sources": [
    { "type": "remotive" },
    { "type": "wwr" },
    { "type": "remoteok" },
    { "type": "jobicy" },
    { "type": "hn" },
    { "type": "rss", "url": "https://www.python.org/jobs/feed/rss/", "limit": 100 }
  ],
  "profile": {
    "title": "Senior Software Engineer",
    "skills": ["python", "backend", "api"],
    "location": "remote",
    "notes": "senior, $150k+"
  },
  "filters": {
    "keywords": ["python", "backend", "api", "django", "fastapi"],
    "require_all_keywords": false,
    "min_score": 1.0,
    "exclude_keywords": ["intern", "junior"]
  },
  "alerts": {
    "console": true,
    "email": {
      "to": "${GIGWATCH_EMAIL_TO}",
      "smtp_host": "smtp.example.com",
      "smtp_port": 587,
      "use_tls": true,
      "username": "${GIGWATCH_SMTP_USER}",
      "password": "${GIGWATCH_SMTP_PASS}"
    },
    "slack": { "webhook_url": "${GIGWATCH_SLACK_WEBHOOK}" },
    "max_per_alert": 25
  },
  "state_file": "gigwatch-state.json",
  "poll_interval": 900
}
Enter fullscreen mode Exit fullscreen mode

A few things I'd tune before you trust it:

  • exclude_keywords is your friend. "intern", "junior", "contract-only" — whatever you will never want, put it here.
  • min_score is the noise dial. Start at 1.0, bump it up if you're still getting too much.
  • require_all_keywords — set it to true if you only want listings that hit every one of your keywords (much quieter, fewer false positives).
  • Secrets go in env vars (${GIGWATCH_SMTP_PASS}), not in the JSON. That keeps your config file safe to commit.

Why it's not another job board

The free endpoints that power it are keyless and public — they're the same feeds a job aggregator would use. That's a strength (nothing to register, nothing to break auth on) and a known limitation, so let me be honest about what it covers and what it doesn't:

  • ✅ Remotive, We Work Remotely, RemoteOK, Jobicy, Hacker News, and any RSS feed you add.
  • ✅ Any board that exposes a JSON or RSS feed you can point it at.
  • ❌ Upwork, LinkedIn, and other gated platforms that require a logged-in account. Those need authenticated scraping, which is a whole separate (and fragile) project. GigWatch is built for the open feed world on purpose.

If most of your gigs come from a gated platform, the open-feed watcher is still useful as a secondary channel — it catches the listings the gated boards re-post, which is often a meaningful share.

The CI version: let a schedule post matches as an Issue

If you'd rather not run a daemon, I ship a GitHub Action. It fetches live jobs filtered by skill and returns them as Markdown, so a scheduled workflow can open a fresh Issue (or PR comment) with the day's new matches. No API keys — it's powered by the same free feed:

name: gig-watch
on:
  schedule:
    - cron: "0 7 * * *"   # 07:00 UTC daily
  workflow_dispatch:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Scan feeds for matching gigs
        uses: earnnova-dev/gigwatch@main
        id: scan
        with:
          skills: python,backend,api
          limit: 20
          min_score: 1.0

      - name: Open an issue with the matches
        if: steps.scan.outputs.job_count > 0
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `New matching gigs (${context.run_id})`,
              body: `${{ steps.scan.outputs.markdown }}`
            })
Enter fullscreen mode Exit fullscreen mode

Point it at your own repo, and every morning you get a clean, searchable Issue of the day's new matching gigs. The state file makes the action idempotent, so it only ever posts new stuff.

Honest limitations

  • It's a watcher, not a scraper-with-everything. You host it; it does not phone home.
  • Feed coverage is limited to what's publicly keyless. Gated platforms are out of scope by design.
  • It scores by keyword overlap, not by an LLM. Good enough to filter noise; not a replacement for actually reading the listing.

Install

pip install gigwatch-nova
Enter fullscreen mode Exit fullscreen mode

Requires Python 3.9+. Drop in a config.json like the one above, point state_file somewhere writable, and run it under cron or your scheduler of choice. If the whole "watch the open feeds, dedup against what I've seen, ping me on new matches" thing matches your workflow, it's a few minutes to stand up — and your browser tabs can finally close.

Top comments (0)