A Python scraper that searches multiple US car listing sites for used vehicles matching your criteria and sends Telegram alerts for new listings. Built for reliability: it handles bot detection, session management, cross-source deduplication, and automatic token refresh with minimal manual intervention.
Works for any make and model. Set your filters once, run it on a schedule, and get notified when something new hits the market.
US only. Distance filtering uses US ZIP codes via the
pgeocodelibrary, and all supported listing sites are US-based.
USA Car Search is for people who want to monitor used-car listings across several US marketplaces without manually checking each site every day. It is especially useful when you are waiting for a specific make, model, trim, year range, mileage range, color, or price range and want Telegram alerts when new matches appear.
- 7 sources: CarGurus, Cars.com, Craigslist, AutoTrader, Facebook Marketplace, eBay Motors, auto.dev
- Telegram alerts: sends a formatted summary of new listings with price, mileage, color, location, distance, and deal rating
- Cross-source deduplication: same car listed on multiple sites appears only once, matched by VIN or a year/mileage/price fingerprint
- Deal ratings: Great Deal / Good Deal / Fair Deal / High Priced / Overpriced shown for every listing; computed from median price when the source doesn't provide its own rating
- Persistent seen list: tracks listings between runs; only new ones trigger alerts; sold/removed listings are counted and reported
- Distance filtering: haversine distance from your ZIP, computed via
pgeocode; listings outside radius are skipped - Color and trim filtering: configurable; default targets black/grey/charcoal
- Year and mileage filtering: applied at scrape time for all sources
- eBay OAuth with auto-refresh: access token auto-refreshed on every run; stays valid for about 18 months without manual intervention
- AutoTrader Chrome CDP: connects to a real Chrome instance to bypass bot detection; auto-relaunches Chrome if connection drops; falls back to headless Playwright if CDP unavailable
- Craigslist multi-region: searches multiple metro areas in parallel, each with its own browser context to avoid threading errors
- CarGurus city/state/distance resolution: when the API doesn't return a state or distance, falls back to ZIP lookup then city-name search across all US ZIP codes within radius
- Cars.com ZIP resolution: resolves raw ZIP codes to City, ST format for cleaner output
- Facebook session persistence: saves and reuses a logged-in session; prompts to re-auth when expired
- Enable/disable per source: any source can be toggled off via env var without touching code
Docker is the fastest way to try the project without managing Python browser dependencies manually.
cp .env.example .env
# Edit .env with your vehicle, ZIP, Telegram bot, source URLs, and API keys.
docker compose run --rm car-search --notifyUse --all on the first run if you want every current listing treated as new:
docker compose run --rm car-search --all --notifyYou can also build and run the image directly:
docker build -t usa-car-search .
docker run --rm --env-file .env -v "$PWD/data:/data" usa-car-search --notifyPrerequisites:
- Python 3.9+
- WSL2, Linux, or Docker
- Chromium installed by Playwright
Install dependencies from the project metadata:
python3 -m pip install --upgrade pip
python3 -m pip install -e .
playwright install chromium
usa-car-search --notifyOr install directly from requirements.txt:
python3 -m pip install -r requirements.txt
playwright install chromiumOptional: load config from a .env file with python-dotenv, which is included in the default dependencies.
1. Copy and fill in your config:
cp .env.example .env
# Edit .env with your vehicle, ZIP, Telegram bot, source URLs, etc.2. Uncomment the dotenv loader at the top of usa-car-search.py:
from dotenv import load_dotenv
load_dotenv()3. Run it:
usa-car-search # print results to stdout
usa-car-search --notify # also send Telegram alert
usa-car-search --all # treat all listings as new (good for first run)The legacy script path still works:
python3 usa-car-search.py --notify- US only: ZIP-code distance calculations and supported sources are US-focused.
- Scraping can break: listing sites change markup, APIs, bot checks, and login flows without notice.
- AutoTrader works best with Chrome CDP: headless fallback exists, but CDP produces better results.
- Facebook requires a saved login session: run
fb-auth-setup.pyand keep session files private. - Credentials stay local: do not commit
.env, token files, cookies,fb-session.json, or logs containing private data.
Listing sources
-> source-specific scraping/API adapters
-> year, mileage, price, color, trim, and distance filters
-> VIN and fingerprint deduplication
-> seen-list diff
-> deal rating
-> stdout and optional Telegram alerts
- Scrape: all enabled sources run in sequence, each applying year/mileage/color/distance filters before returning results
- Deduplicate: results are merged across sources; same car matched by VIN first, then by (year, mileage, price) fingerprint with price bucketed to the nearest $500 to account for dealer fee differences
- Diff against seen list:
seen.jsonstores listing IDs from prior runs; anything not in the file is flagged as new - Rate deals: each listing gets a deal rating based on price vs. median across all results; sources like CarGurus and Cars.com provide their own ratings which are used directly
- Output: results printed to stdout in a readable format; if
--notifyis passed, new listings are also sent to Telegram - Update seen list:
seen.jsonis updated with the current run's listings; disappeared listings are noted
| Source | Method | Bot Resistance | Notes |
|---|---|---|---|
| CarGurus | Browser scrape | Medium | Paste your search URL; supports 2 URLs, for example two trims |
| Cars.com | Browser scrape | Medium | Paste your search URL |
| Craigslist | Browser scrape | Low | Multi-region, keyword search, parallel |
| AutoTrader | Browser scrape | High | Chrome CDP recommended; auto-relaunches on disconnect |
| Facebook Marketplace | Browser scrape | High | Requires saved login session |
| eBay Motors | REST API + scrape | Low | OAuth recommended; auto-refreshes token |
| auto.dev | REST API | Low | Free API key required |
All settings are environment variables. Set them in .env or export them in your shell. See .env.example for a full template.
SEARCH_MAKE=Honda
SEARCH_MODEL=Civic
SEARCH_KEYWORDS=honda civic # used for free-text search (Craigslist, Facebook, eBay)SEARCH_ZIP=90210
SEARCH_RADIUS=150 # miles from ZIP
MIN_YEAR=2020
MAX_YEAR=2023
MAX_MILES=50000
MIN_PRICE=15000
MAX_PRICE=35000Set ALLOWED_COLORS to a comma-separated list. Matching is case-insensitive and substring-based. Leave it blank to accept every color.
ALLOWED_COLORS=black,gray,grey,charcoal,dark,obsidian,magnetic,graphite
# Accept any color:
ALLOWED_COLORS=Set ALLOWED_TRIMS to a comma-separated list. Leave it blank to accept every trim.
ALLOWED_TRIMS=limited,sti
# Accept every trim:
ALLOWED_TRIMS=ENABLE_CARGURUS=true
ENABLE_CRAIGSLIST=true
ENABLE_CARSDOTCOM=true
ENABLE_AUTOTRADER=true
ENABLE_FACEBOOK=true
ENABLE_EBAY=true
ENABLE_AUTODEV=trueSet CL_REGIONS to a comma-separated list of subdomain slugs. Find them at craigslist.org/about/sites.
CL_REGIONS=newyork,boston,philadelphiaThese sites use internal make/model codes that vary by vehicle. The easiest approach:
- Go to the site
- Set all your filters: make, model, year range, mileage, color, ZIP, radius
- Copy the URL from your browser address bar
- Paste it into
.env
CARGURUS_URL=https://www.cargurus.com/Cars/l-Used-Honda-Civic-d2188?zip=90210&distance=100&...
AUTOTRADER_URL=https://www.autotrader.com/cars-for-sale/used-cars/honda/civic/?zip=90210&...
CARSDOTCOM_URL=https://www.cars.com/shopping/results/?makes[]=honda&models[]=honda-civic&...You can set CARGURUS_URL_2 for a second CarGurus search, for example a different trim or body style.
Facebook requires a real logged-in session. Run the auth setup script once:
python3 fb-auth-setup.pyA browser window opens. Log into Facebook, then press Enter. Your session is saved to fb-session.json. Re-run when it expires, typically every few weeks.
FB_CITY=newyork # city slug from facebook.com/marketplace/<city>/search
FB_SESSION_FILE=./fb-session.jsonThe scraper uses eBay's OAuth 2.0 API and automatically refreshes the access token before each run. Initial setup takes about 5 minutes.
One-time setup:
- Sign up at developer.ebay.com and create a Production app
- Under your app -> User Tokens, note your RuName (Redirect URL name)
- Set your credentials in
.env:EBAY_CLIENT_ID=YourApp-PRD-xxxxxxxxxxxx EBAY_CLIENT_SECRET=PRD-xxxxxxxxxxxx EBAY_RUNAME=YourRuName-here
- Run the setup script:
It will print an authorization URL. Open it in your browser, sign in with your eBay account, then paste the redirect URL back when prompted.
python3 ebay-oauth-setup.py
- Tokens are saved to
ebay-token.txtandebay-refresh-token.txtand auto-refreshed from then on.
The refresh token lasts about 18 months. Re-run ebay-oauth-setup.py once it expires.
If no token is configured, the scraper falls back to page scraping eBay, which is less reliable.
Get a free API key at auto.dev. Set AUTODEV_API_KEY.
AutoTrader aggressively blocks headless browsers. Connecting to a real Chrome instance via Chrome DevTools Protocol (CDP) bypasses detection and produces much better results.
Windows/WSL2 setup:
- Create
launch-chrome-debug.bat:
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--remote-debugging-port=9222 ^
--remote-debugging-address=0.0.0.0 ^
--remote-allow-origins=* ^
--user-data-dir="C:\Temp\chrome-debug" ^
--no-first-run ^
about:blank-
Run this bat file before running the scraper. You can add it to Windows Task Scheduler to launch at login automatically.
-
Find your Windows host IP from WSL2:
ip route show default | awk '{print $3}'- Set it in
.env:
CHROME_CDP_HOST=172.x.x.x
CHROME_CDP_PORT=9222- If Chrome is unreachable from WSL2, add a Windows port forwarding rule. Run as admin in PowerShell:
$wsl = (wsl hostname -I).Trim()
netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=$wslAuto-relaunch: If the CDP connection drops mid-run (ECONNRESET), the scraper will automatically kill and relaunch Chrome, then retry once before giving up.
Fallback: If CDP is not configured or Chrome is unreachable, AutoTrader falls back to headless Playwright automatically. Results may be limited due to bot detection.
- Create a bot with @BotFather, copy the token
- Add the bot to your chat or channel
- Get your chat ID. For groups/channels, use @userinfobot or the Telegram API
- Set in
.env:
TG_BOT_TOKEN=123456:ABC-...
TG_CHAT_ID=-1001234567890
TG_TOPIC_ID=42 # optional: for Telegram forum/topic threadsRun with --notify to send alerts:
usa-car-search --notifyEach new listing is sent as a separate message with price, mileage, color, location, distance away, deal rating, and a direct link.
Add to crontab on Linux/WSL2:
crontab -e0 17 * * * cd /path/to/usa-car-search && usa-car-search --notify >> search.log 2>&1This runs at 5 PM every day and sends Telegram alerts for any new listings found since the last run.
Distance is computed as haversine miles from SEARCH_ZIP. For sources that return a ZIP code, distance is derived directly. For CarGurus listings that only return a city name, the scraper:
- Looks up the city across all US ZIP codes in the
pgeocodedatabase - Finds the closest match within
SEARCH_RADIUS - Uses that match's state abbreviation and distance
This means "Avon" correctly resolves to "Avon, NY - 27 mi away" rather than just "Avon".
- Set
SEARCH_MAKE,SEARCH_MODEL, andSEARCH_KEYWORDS - Go to CarGurus, AutoTrader, and Cars.com, search with your filters, then copy and paste the URLs into
.env - Set Craigslist regions near you (
CL_REGIONS) - Set
ALLOWED_COLORSandALLOWED_TRIMSin.envif you want those filters - Run with
--allon first run to see all current listings, then switch to normal runs
Run the lightweight core tests locally:
python3 -m unittest discover -s testsThe CI workflow runs these tests across Python 3.9, 3.10, 3.11, and 3.12, then verifies the Docker image builds.
Pushing a tag such as v1.3.1 runs the release workflow and publishes a Docker image to GitHub Container Registry:
git tag v1.3.1
git push origin v1.3.1Bug reports, source requests, and pull requests are welcome. See CONTRIBUTING.md for reporting guidelines, local checks, and source-integration expectations.
For GitHub-side polish such as topics and social preview artwork, see docs/repo-discoverability.md.
- Fix: AutoTrader crash after run:
headless=Falsewas crashing the Playwright browser in headless environments (WSL2 without a display), killing the entire script after AutoTrader completed. Scraper now auto-detects display availability and falls back toheadless=Truewhen no display is present - Fix: AutoTrader "Cannot switch to a different thread": Playwright sync API is thread-locked; the daemon thread approach was fundamentally broken. AutoTrader now runs in a dedicated subprocess (
_at_worker.py) so Playwright runs on a proper main thread with full CDP support - Fix: AutoTrader CDP host: worker subprocess now correctly reads
CHROME_CDP_HOSTfrom env (previously hardcoded to wrong IP) - New:
_at_worker.py: standalone AutoTrader worker script; spawned as a subprocess by the main scraper; outputs raw listings JSON to stdout for the parent to filter
- Fix: eBay OAuth: removed
buy.item.summaryscope that was causing HTTP 400 errors during token refresh; onlyapi_scopeis required and granted by default - Fix: eBay OAuth setup script (
ebay-oauth-setup.py) updated to match; bad scope removed there too - Fix: CarGurus city/state/distance: when API returns only a city name with no state or distance, scraper now searches all US ZIP codes for that city name and resolves to the closest match within radius, for example "Avon" -> "Avon, NY - 27 mi away"
- Fix: Cross-source deduplication: VIN-matched listings now also add their fingerprint to the seen set, preventing the same car from appearing twice when one source has a VIN and another doesn't
- Fix: Cross-source price deduplication: price bucketed to nearest $500 before fingerprinting to handle dealer fee differences between sources, for example auto.dev adds about $200 in fees
- Fix: Cars.com location: raw ZIP codes now resolved to "City, ST" format via pgeocode lookup
- Fix: AutoTrader Chrome CDP: scraper now auto-kills and relaunches Chrome when a stale CDP session is detected (ECONNRESET), then retries once before falling back
- Fix: Craigslist threading: each parallel worker now spawns its own Playwright browser/context instead of sharing the parent context across threads, eliminating greenlet/threading errors
- Initial public release
- CarGurus, Cars.com, Craigslist, AutoTrader, Facebook Marketplace, eBay Motors, auto.dev sources
- eBay OAuth 2.0 with auto-refresh
- Telegram notifications with deal ratings
- Cross-source deduplication by VIN and fingerprint
- pgeocode distance filtering
seen.jsonpersistence between runs--notify,--allflags.env-based configurationebay-oauth-setup.pyhelper script
