Skip to main content

Minimal LinkedIn 1st/2nd-degree lead-gen CLI with triage TUI and browser dashboard

Project description

leads

Minimal LinkedIn lead-gen CLI: find 1st- and 2nd-degree connections at one or more companies, triage them, export.

Use a burner LinkedIn account. This violates LinkedIn's ToS. Don't run it on an account you care about.

Install

cd leads
uv pip install -e .          # or: pip install -e .

Installs a leads CLI command. Requires Python 3.11+.

The one-shot

Log into LinkedIn (burner!) in any supported browser, then:

leads find "demtech.ai"

That's it. On first run it autodetects your cookies. Then it resolves the company, scrapes 1st+2nd-degree connections, and prints a table.

Batch mode:

leads find "demtech.ai" "stripe" "linear" --yes

Multiple positional names = multiple scrapes in one invocation. --yes skips the disambiguation prompt.

Other useful flags:

Flag Effect
--triage Open the interactive triage TUI after scraping
--out f.csv Write to a file instead of printing
--format jsonl Pair with --out for JSONL output
--auto-split On the LinkedIn ~1000-result cap, auto-split regions
--quiet Summary line only
--degree 2 Only 2nd-degrees (default: both)

The 8 verbs

leads auth import [--browser X]   # autodetect cookies (one-time, usually unneeded)
leads auth login                   # manual cookie paste fallback
leads company <name>               # interactively resolve a name → URN, cache it
leads scrape --company-urn ...     # power-user discovery (URN-driven, scriptable)
leads find <names...>              # one-shot: resolve → scrape → render
leads show [filters]               # universal viewer (terminal table / CSV / JSONL)
leads triage [filters]             # interactive TUI for flagging interesting leads
leads web [filters]                # browser dashboard (mirrors triage TUI)
leads status                       # accounts, jobs, companies, today's budget

Filters (work everywhere)

-d, --distance 1 [-d 2]            # repeatable; 1st-degree, 2nd-degree
--interesting / --not-interesting  # the ⭐ flag
--location "bengaluru"             # substring match
--at "demtech"                     # filter to leads found at this company
--where "raw SQL"                  # power-user escape hatch

Output (on leads show)

leads show                                       # Rich table in terminal (default)
leads show --format csv --out leads.csv          # write CSV (full columns)
leads show --format jsonl --out leads.jsonl      # write JSONL
leads show --format csv --columns triage --out triage.csv
                                                 # CSV with the triage subset
leads show --compact                             # drop the LinkedIn URL column

Triage TUI

leads triage                                     # interactive, default
leads triage --at "demtech" --not-interesting    # pre-filter what shows up
leads triage --export triage.csv                 # spreadsheet round-trip
leads triage --import triage.csv                 # bring back the marked-up CSV

Keys: j/k or arrows to navigate · space toggle ⭐ · n edit note · o open LinkedIn URL · / filter · esc clear filter · q save+quit.

Web dashboard

A minimal browser UI with the same actions as the triage TUI — toggle ⭐, edit notes inline, multi-select + bulk delete, filter, export CSV.

leads web                                 # binds 127.0.0.1:8765, opens browser
leads web --port 9000                     # different port
leads web --no-open                       # don't auto-open
leads web --at "demtech" --interesting    # initial filters (still editable in-page)

The server binds to 127.0.0.1 only and rejects any non-loopback Host header (DNS-rebinding defense). No authentication — anything that can reach loopback on your machine is treated as you.

Filters live in the URL query string, so refresh works and you can bookmark a scoped view. The Export CSV link reflects the current filter and respects the filter as you change it.

Shortcuts: / focus search · Esc clear search / clear selection · Enter save inline edit · Cmd/Ctrl+A select-all visible.

Scrape (URN-based, scripty)

leads scrape --company-urn urn:li:fsd_company:98873360 --degree 1,2
leads scrape --company-urn A --company-urn B --auto-split --quiet

Re-running on the same company auto-resumes from the last next_offset if a previous job paused.

Cookies

Auto-import works across chrome, chromium, firefox, safari, edge, brave, opera, vivaldi, arc, librewolf, zen.

Caveats:

  • Chrome on macOS triggers a Keychain prompt — enter your Mac password.
  • Safari needs Full Disk Access for your terminal (System Settings → Privacy & Security).
  • Browser may need to be closed if its cookie DB is locked.

If auto-import fails, manual paste from DevTools → Cookies → https://www.linkedin.com:

  • li_at — long alphanumeric
  • JSESSIONID"ajax:1234...", keep the literal quotes
leads auth login

Stored in ~/.leads/config.toml (chmod 600). DB lives in ~/.leads/leads.db.

Never paste cookies into chat or shared docs. They're bearer tokens equivalent to your password.

Pacing & budgets

The tool deliberately scrapes slowly to avoid getting your burner account flagged. Between every API call it waits 5–12 seconds (randomized), and every ~20 calls it takes a longer 45–90 second pause. So scraping 1000 leads takes ~30–60 minutes, not seconds. This is on purpose — fast scraping gets you banned.

There are two budgets:

  • Per-invocation: 50 search calls. One leads find run won't use more than 50 search API calls.
  • Per-day: 150 search calls. Across all runs in a day. Tracked in the api_calls table.

When you run leads find "a" "b" "c", those three companies share the same 50-call budget — it doesn't reset between them.

What happens when the budget runs out?

The scrape pauses mid-company. It doesn't lose progress — everything fetched so far is already in the DB. The job row is marked paused and remembers the exact offset.

Just re-run the same command later (e.g. tomorrow when the daily budget resets):

leads find "demtech.ai"   # picks up from where it left off, no flag needed

You'll see a dim line like Resuming job #7 for demtech.ai at offset 150.

What if LinkedIn locks the account?

If you get a 429 / challenge / 401 response, the account is marked locked and all commands abort. Wait a few hours, then re-run — most lockouts clear on their own. If not, run leads auth login to paste fresh cookies and try again.

Checking what state you're in

leads status

Shows your daily budget used, recent jobs (with their status and offset), and which companies have been scraped.

What if a company has more than ~1000 connections?

LinkedIn's search_people endpoint won't return more than about 1000 results per query, regardless of how many actually exist. When the tool hits this cap, it asks:

stripe hit the ~1000-result cap.
Auto-split across 5 regions (United States, India, UK, Germany, Singapore)? [y/N]
  • Press y → the tool re-scrapes the same company once per region, getting up to ~1000 leads from each. Total: up to ~5000 leads instead of 1000. Costs up to 5× the search budget.
  • Press n → keep the 1000 you got and move on.

To skip the prompt and always split, pass --auto-split:

leads find "stripe" --auto-split

This is also useful in --quiet mode (no prompts), since otherwise the tool just prints a warning and moves on without splitting.

Project layout

leads/
├── pyproject.toml
├── schema.sql
├── leads/
│   ├── cli.py            # Typer app (entry: `leads`)
│   ├── config.py         # ~/.leads/config.toml
│   ├── cookie_import.py  # cross-browser cookie autodetect
│   ├── db.py             # sqlite3 + schema init + migrations
│   ├── linkedin.py       # tomquirk lib wrapper + paced calls
│   ├── budget.py         # api_calls, daily/invocation caps
│   ├── filters.py        # structured filter flags → SQL WHERE
│   ├── regions.py        # default 5 regions for auto-split
│   ├── companies.py      # interactive disambiguation + cache
│   ├── scrape.py         # multi-company, auto-resume, auto-split
│   ├── find.py           # the one-shot orchestrator
│   ├── show.py           # universal viewer (terminal/CSV/JSONL)
│   ├── triage.py         # CSV import/export
│   ├── triage_tui.py     # Textual TUI for interactive triage
│   └── web/              # Bottle + htmx browser dashboard
│       ├── server.py
│       ├── templates/index.tpl
│       └── static/        # htmx.min.js, app.css, app.js
└── tests/test_db.py

Tests

pytest

Schema round-trip, triage CSV round-trip, filter compilation, show CSV export.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

leadhunter-0.1.0.tar.gz (51.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

leadhunter-0.1.0-py3-none-any.whl (57.3 kB view details)

Uploaded Python 3

File details

Details for the file leadhunter-0.1.0.tar.gz.

File metadata

  • Download URL: leadhunter-0.1.0.tar.gz
  • Upload date:
  • Size: 51.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.12

File hashes

Hashes for leadhunter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ff2c4db74bba61ad553eac727b67c818c828ffdcc17da8d57659b00809fa6b95
MD5 343248bbc74097e018aa6fe9c3093cb9
BLAKE2b-256 12eac676daf29ef0b8779a3c696899066e99f0114956cdd1bd0c7339fe7863cf

See more details on using hashes here.

File details

Details for the file leadhunter-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: leadhunter-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 57.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.12

File hashes

Hashes for leadhunter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0bf987a0bb954d1ed77dd195f4065f20d75b2279651eece158bf1bde6ecc0387
MD5 c3f54371d783620fd6b7848a01c5182c
BLAKE2b-256 ed3a1a9098844f8dd4147f40b7e1684f0dabf7837663b77c985b346154879ccc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page