{"slug": "show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup", "title": "Show HN: Locus – added bulk CSV search, real SMTP checks, and lead dedup", "summary": "Locus, an open-source lead-generation tool, added bulk CSV search, real SMTP email verification, and cross-session lead deduplication, according to its Show HN announcement. The self-hosted pipeline searches Google Maps listings by location and category, then runs each business through an LLM enrichment step using Gemini, OpenAI, Anthropic, or OpenRouter to find a verified contact email. Locus ships as a Docker Compose stack (app, worker, Postgres, Redis) with no hosted version, no account, and no fees, and its author notes automated collection of Google Maps data violates Google's Terms of Service.", "body_md": "**Search a location and category, pull real business listings, enrich them\nwith AI-verified emails. Free, open-source, bring your own keys, no account\nwith us, no fees.**\n\nLocus runs a pipeline: search a place and a category, pull the matching businesses, then run each one through an AI enrichment step that tries to find a verified email. Results land in a table you can filter, sort, export (CSV, Excel, JSON), or push to a webhook.\n\nBuilt with Next.js (App Router), Prisma and Postgres, NextAuth, BullMQ and Redis for the background worker, and a bring-your-own-key model for the search and AI providers.\n\n- **Search and extraction** : a location, a category, and a radius pull real\nGoogle Maps business listings, with live progress over WebSocket.\n- **Bulk search from a CSV** : upload a file of query/location pairs (an\noptional radius per row) and queue up to 50 searches at once instead of\nrunning them one at a time.\n- **AI enrichment** : each business gets run through an LLM step that tries to\nfind and verify a real contact email from its website, alongside phone\nnumbers pulled from the listing itself. Choose from Gemini, OpenAI,\nAnthropic, or OpenRouter.\n- **Real SMTP verification** : on top of the AI's answer, a genuine mailbox\ncheck (MX lookup, then an SMTP handshake, nothing ever gets sent) confirms\nan email before it's marked verified, not just an AI guess.\n- **Optional Hunter.io lookup** : add a Hunter.io key and it's checked before\nthe website-and-AI step runs, skipping straight to a known email when\nHunter already has one on file for the domain.\n- **Cross-session matching** : the same business found in two different\nsearches gets recognized and merged, not duplicated, backfilling any\nmissing phone or email instead of creating a second row.\n- **History** : every past search is kept with its full config snapshot.\nRe-run one, open a past search's results straight from the list, or\nselect two sessions to compare their yield side by side.\n- **Export and integrations** : CSV, Excel, or JSON export, plus outbound\nwebhooks and CRM connectors (HubSpot, Salesforce).\n- **Bring your own key, for everything** : search provider, AI provider,\ndatabase, Redis, the server itself. No hosted version, no vendor lock-in,\nnothing running that you don't control.\n- **Built for self-hosting** : a real Docker Compose stack (app, worker,\nPostgres, Redis) that's actually been cold-started and verified end to\nend, not just written and assumed to work.\n\nLocus pulls data from Google Maps and third-party search APIs. Automated collection of Google Maps data is against Google's Terms of Service. You are responsible for how you run this, which providers and proxies you use, and whether that is allowed where you operate. This project is provided for research and educational purposes with no warranty. See the licence.\n\n- Node.js 20+\n- Docker (for local Postgres and Redis) or your own instances\n- A search provider key (SerpApi, Serper.dev, or Google Places)\n- An AI provider key (Gemini, OpenAI, Anthropic, or OpenRouter) for enrichment\n\nThere's no hosted version of Locus. Running it, for local development or for real, is entirely up to you: your own Postgres, your own Redis, your own server or machine. Two ways to do that:\n\nThis builds and runs everything, the app, the worker, Postgres, and Redis, as one stack.\n\n```\ngit clone https://github.com/<you>/locus.git\ncd locus\ncp .env.example .env\n# Fill in NEXTAUTH_URL, NEXTAUTH_SECRET, ENRICHMENT_KEY_SECRET, and\n# INTERNAL_API_SECRET in .env before continuing. Docker compose refuses to\n# start without them, on purpose.\ndocker compose up --build -d\n```\n\nOpen the app at the `NEXTAUTH_URL` you set (`http://localhost:3000` for a\nsame-machine test), create an account, and add your provider keys in\nSettings. See the comments in `docker-compose.yml` and `.env.example` for\nwhat `NEXT_PUBLIC_WS_URL` needs to be once this is reachable from outside\nyour own machine (it has to be a real address a browser can reach, not a\nDocker-internal one), and put a reverse proxy (nginx, Caddy, Traefik) in\nfront for HTTPS rather than exposing the app's port directly.\n\n```\ngit clone https://github.com/<you>/locus.git\ncd locus\nnpm install\ncp .env.example .env\ndocker compose up -d postgres redis\nnpx prisma migrate deploy\nnpm run dev:all\n```\n\nPostgres comes up on port 5433, Redis on 6379, the app on 3000, the worker on 4000, all with reload on file changes.\n\n`.env.example` documents every variable. The ones you must set for a working\ninstall, Docker or not:\n\n| Variable | What it is | \n|---|---|\n| `DATABASE_URL` | Postgres connection string. The compose file uses `postgresql://locus:locus_dev@localhost:5433/locus` by default | \n| `REDIS_URL` | Redis connection string for the job queue | \n| `NEXTAUTH_SECRET` | Session signing secret. `openssl rand -base64 32` | \n| `NEXTAUTH_URL` | The app's real public URL, `http://localhost:3000` for local testing | \n| `ENRICHMENT_KEY_SECRET` | Encrypts provider keys at rest. `openssl rand -hex 32` | \n| `INTERNAL_API_SECRET` | Shared secret between the app and the worker. `openssl rand -hex 32` | \n| `FRONTEND_ORIGIN` | Origin allowed to reach the worker, `http://localhost:3000` in dev | \n\nOptional: `RESEND_API_KEY` (password-reset email, logs the link to the console\nwhen unset), `SENTRY_*`, `NINE_ROUTER_BASE_URL` (self-hosted model router),\n`TRUST_PROXY_HEADERS` (only if a reverse proxy sits in front of this app).\n\nOnce the app is running and you've created an account:\n\n1. \n**Add provider keys.** Settings > API Keys needs one business-search key\n(SerpApi, Serper.dev, or Google Places) and one AI key (Gemini, OpenAI,\nAnthropic, or OpenRouter) before anything can run. SerpApi and Serper.dev\nboth have a free way to get started, no card required. A Hunter.io key\nis optional, it has a free tier (50 lookups/month) and, when added, gets\nchecked before the website-and-AI enrichment step runs.2. \n**Run a search** , one at a time on the Dashboard, or**upload a CSV** for several at once (a`query` column and a`location` column, an\noptional`radius` column per row). Progress streams in live: businesses\nfound, phone numbers extracted, emails verified, as they happen, not\njust at the end.\n3. \n**Work the results.** The leads table supports filtering by status\n(verified, needs enrichment, pending, failed), search, sort, and bulk\nactions. A lead that failed enrichment can be retried individually or in\nbulk. The same business found again in a later search gets merged into\nits existing lead instead of duplicated. Export whenever you want a CSV,\nExcel file, or JSON, or push results to a webhook or CRM connector.\n4. \n**Come back to it later.** Every search lands in History with its full\nconfig snapshot (radius, provider, enrichment depth). Click any past\nsearch to reopen its results, re-run it with the same settings, or select\ntwo searches to compare their yield side by side.\n\n```\nnpm run dev:all        # app plus worker with reload\nnpm test               # Vitest unit and integration\nnpm run test:e2e       # Playwright, needs the app running\nnpx tsc --noEmit       # typecheck\n```\n\nSee `TESTING.md` for the test layers and conventions.\n\n- `app/` , Next.js App Router pages and API routes\n- `server/index.ts` , the Express and BullMQ worker that runs searches and\nenrichment and pushes progress over WebSocket\n- `lib/enrichment/` , provider adapters and the enrichment waterfall\n- `lib/export/` , CSV/Excel/JSON export and the webhook/connector layer\n- `prisma/schema.prisma` , the data model\n\nSee `CONTRIBUTING.md`. Security reports go through `SECURITY.md`, not public\nissues.\n\nAGPL-3.0-or-later. If you run a modified version as a network service, you have\nto make your changes available to its users. See `LICENSE`.\n\nBuilt by [Cloudz Computing](https://www.cloudzcomputing.com)", "url": "https://wpnews.pro/news/show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup", "canonical_source": "https://github.com/mabdullahb/Locus", "published_at": "2026-09-11 12:25:47+00:00", "updated_at": "2026-09-11 12:41:00.633019+00:00", "lang": "en", "topics": ["ai-tools", "ai-products", "developer-tools", "ai-agents"], "entities": ["Locus", "Google Maps", "Gemini", "OpenAI", "Anthropic", "OpenRouter", "Hunter.io", "HubSpot"], "alternates": {"html": "https://wpnews.pro/news/show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup", "markdown": "https://wpnews.pro/news/show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup.md", "text": "https://wpnews.pro/news/show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup.txt", "jsonld": "https://wpnews.pro/news/show-hn-locus-added-bulk-csv-search-real-smtp-checks-and-lead-dedup.jsonld"}}