Version: 1.0.0 | Status: Live | Use: Internal only | Deployment: clientell.fly.dev
Sales, partnerships, and research workflows require knowing who a company's customers are — but that information is scattered across case study pages, logo grids, testimonials, press releases, and partner directories. Compiling it manually takes hours. ClienTell automates that process: enter a URL, get back a structured list of client/customer mentions in under a minute.
Primary use cases:
- Pre-call competitive research — who's already a customer of a vendor or competitor?
- Partnership prospecting — mapping a potential partner's existing customer ecosystem
- Market research — identifying the customer profile of players in a given category
| Feature | Description |
|---|---|
| URL input | Accept a seed URL. Auto-prepends https:// and www. to bare domains. |
| Crawl modes | Homepage only — single page. Smart scan — targeted multi-page crawl (see §05). Deep scan — full recursive crawl up to 50 pages. |
| Client/customer extraction | Identify company names from logo alt text, testimonial bylines, case study headers, and inline body text. Normalize, deduplicate, and score confidence. Explicitly excludes partner/integration companies. |
| HTML section prioritization | High-signal HTML sections (matching client, customer, testimonial, logo-grid etc. class/id attributes) are extracted and front-loaded in the 20k character token budget before sending to Claude. |
| Results table & CSV export | Sortable, filterable table: company name, confidence, vertical, source type, context snippet, source URL. One-click CSV export. |
| Shareable results | Every completed scan has a permanent URL at /scan/:id with a "Copy link" button. The page replays the stored results (and live-polls if the scan is still running). |
| Confidence scoring | High — dedicated customer/case study page. Medium — testimonial or logo grid. Low — passing mention. |
| Industry verticals | Claude infers industry verticals per company. Multi-value, filterable. |
| Warnings | Amber banner when: page content was truncated before Claude, Claude hit the output token limit, or Smart scan hit the page cap before visiting all qualifying pages. |
| Methodology page | /methodology — explains how crawling and scoring works, what's returned and what's excluded. |
| Rate limiting | Per-IP limit of 5 crawls/hour (express-rate-limit) plus a server-wide cap of 3 concurrent crawls. There is no login — access is controlled by rate limits, not a password. |
| Cancel crawl | Mid-crawl cancel button interrupts between page visits and closes browser cleanly. |
| Crash recovery | Crawls left at running by a server restart/crash are automatically marked failed on startup, so the UI shows an error instead of polling forever. |
Single Fly.io app. Express serves both the API and the built React frontend as static files.
| Layer | Technology | Notes |
|---|---|---|
| Frontend | React 18 + Vite 6 | Built and served as static files by Express |
| Backend | Node.js (ESM, v20) + Express 4 | Port 8080 |
| Crawler | HTTP-first + Playwright fallback | fetch() for server-rendered pages; Playwright Chromium headless only for JS SPAs |
| Extraction | Claude API (claude-sonnet-4-6) |
Parallel API calls via Promise.allSettled |
| Database | Postgres 16 | Crawls and results |
| Rate limiting | express-rate-limit |
5 crawls/IP/hour; 3 concurrent server-wide |
Smart scan (default, 20 pages) — two-phase flat crawl:
- Discover — fetches the homepage, extracts same-origin
hreflinks, and plain-fetchessitemap.xmlfor additional candidates. - Score — every candidate URL is scored against
SMART_SCAN_PATTERNS. Only URLs scoring ≥ 5 are queued. - Visit — qualifying URLs are visited in score-descending order, in parallel batches of 5, up to the max pages cap. No further link discovery happens — the queue is fixed after phase 1.
SMART_SCAN_PATTERNS — page scoring:
| Score | Page types | Example URL patterns |
|---|---|---|
| 10 | Customers, Clients, Case studies, Work, Portfolio, Success stories | /customers, /case-studies, /our-work, /portfolio |
| 8 | Testimonials, Reviews, Logo walls, Trusted-by | /testimonials, /reviews, /trusted-by |
| 6 | Industries, Solutions, Spotlights, About/Clients | /industries, /solutions, /about/customers |
| ≤ 4 | Partners, Integrations, Marketplace, Press, News | /partners, /integrations, /press — never fetched |
Deep scan — recursively discovers all links from every page, scored and prioritized, up to 50 pages.
Homepage only — crawls the seed URL only.
Per-page fetch strategy:
- Try plain
fetch()first — fast path (~200–400ms). Works for the majority of B2B sites with server-rendered HTML. - If the response is an empty JS SPA shell (<200 chars of visible text after stripping tags), fall back to Playwright Chromium.
- Playwright is lazy-initialized — no browser is launched at all if all pages are served via HTTP.
Additional crawler behaviours:
- Robots.txt compliance (checked before any page visit)
- Rate limiting: 150ms between requests
- Playwright pages: scroll to bottom (500ms wait) to trigger lazy-loaded carousels and logo grids
- Cancel token:
DELETE /api/crawl/:idinterrupts between page visits, closes browser cleanly
- All pages extracted concurrently via
Promise.allSettled(not sequential) - Claude API for NER — extracts company names with surrounding context (
max_tokens: 8096) - Image alt text and filename parsing for logo grids (e.g.
Logo_HomeDepot.svg→Home Depot) - HTML section prioritization — high-signal sections front-loaded before the 20k char truncation
- Deduplication: highest-confidence mention wins per company name
- Claude explicitly instructed not to include technology partners, integration vendors, or API marketplace listings — only customers/end-users
- JSON resilience: partial output recovered if Claude hits the output token limit mid-response
ClienTell has no login or password gate. Access is bounded by rate limits instead:
POST /api/crawlis wrapped inexpress-rate-limit— 5 crawls per IP per hour (returns429past the limit)- A server-wide in-memory cap of 3 concurrent crawls (
MAX_CONCURRENT) — the 4th concurrent request gets a429"Server is busy" app.set("trust proxy", 1)so per-IP limiting works correctly behind Fly.io's TLS-terminating proxy
- Every scan persists to Postgres and is addressable at
/scan/:id ScanView(React) fetchesGET /api/crawl/:id: renders stored results if complete, surfaces an error for failed/cancelled scans, or live-polls if the scan is still running- "Copy link" button copies the current
/scan/:idURL to the clipboard
crawls (id, url, mode, max_pages, status, created_at, completed_at, warnings TEXT[], error TEXT)
results (id, crawl_id, company_name, source_url, page_type, source_type, context, confidence, created_at, verticals TEXT[])A vestigial
"session"table is still created bymigrate.js(left over from a removed auth layer) but is no longer used.
ClienTell/
├── Dockerfile # mcr.microsoft.com/playwright:v1.59.1-jammy base
├── fly.toml # performance-1x, 2gb, region iad
├── package.json
├── vite.config.js
├── .env # DATABASE_URL, ANTHROPIC_API_KEY, PORT, NODE_ENV
├── .env.example # template for the above
├── scripts/
│ ├── inspect-element.js # dev helper — inspect a page's HTML
│ ├── test-crawler.js # standalone crawler test
│ └── test-extractor.js # standalone extractor test
├── server/
│ ├── index.js # Express entry point, trust proxy, static serve, stale-crawl cleanup on boot
│ ├── routes/
│ │ └── crawl.js # POST/GET/DELETE /api/crawl — rate limit + concurrency cap
│ ├── services/
│ │ ├── crawler.js # HTTP-first fetch, Playwright fallback, smart scan batching
│ │ └── extractor.js # Claude extraction, parallel API calls, dedup, confidence
│ └── db/
│ ├── migrate.js # Creates/updates schema (crawls, results)
│ └── queries.js # saveCrawl, saveResults, getCrawl, markFailed, markCancelled, failStaleRunningCrawls
└── client/src/
├── App.jsx
├── main.jsx # Routing — App, /methodology, /scan/:id
├── index.css
├── components/
│ ├── CrawlForm.jsx # URL input, mode toggle, max pages, spider animation
│ ├── ResultsTable.jsx # Sortable/filterable table, warnings, CSV export
│ ├── ScanView.jsx # /scan/:id shareable results view
│ ├── Methodology.jsx # /methodology page
│ ├── Logo.jsx # SVG wordmark
│ └── WebCorner.jsx # Decorative corner webs
└── lib/api.js # startCrawl, pollCrawl, cancelCrawl
# Start Postgres (if not running)
brew services start postgresql@16
# Copy the env template and fill in values
cp .env.example .env
# Run DB migration (first time or after schema changes)
npm run db:migrate
# Run both Express (8080) and Vite dev server (5173) together
npm run devOpen http://localhost:5173. No password is required.
npm run dev runs dev:server (node --watch --env-file=.env server/index.js) and dev:client (vite) concurrently. To run the production server alone: npm start.
Known environment quirks:
- The
--env-fileflag in the npm scripts requires Node 20+. If a tool can't load it, export manually:export $(grep -v '^#' .env | xargs) - SSL: if you hit a local cert issue,
NODE_TLS_REJECT_UNAUTHORIZED=0in.envworks around it - Port 8080 lingering:
lsof -ti:8080 | xargs kill -9
App is live at clientell.fly.dev. Machine: performance-1x, 2GB RAM, region iad. Auto stop/start enabled with min_machines_running = 0.
Required secrets (all set):
| Secret | Description |
|---|---|
DATABASE_URL |
Injected automatically by fly postgres attach |
ANTHROPIC_API_KEY |
Claude API key |
To redeploy after changes:
fly deployMigration runs automatically on every deploy via fly.toml release_command.
- Public pages only — no login bypass or authenticated scraping
- No app-level auth — access is rate-limited (5/IP/hour, 3 concurrent), not gated by a password
- Robots.txt is respected; disallowed pages are skipped
- HTTP-first fetch has no cookie jar — pages requiring a cookie consent click before content loads may return less data (Playwright fallback handles some of these)
- Logo extraction is best-effort; alt text quality varies by site
- Smart scan relies on URL patterns — sites with non-standard structures may be mis-scored
discoverLinks()uses a regex forhref="..."— single-quoted hrefs supplemented by sitemap.xml
| Metric | Target |
|---|---|
| Extraction precision | ≥ 85% of extracted companies are genuine clients of the crawled site |
| Recall rate | ≥ 80% of publicly listed customers surfaced vs. manual audit |
| Time to results | Smart scan (20 pages) completes in under 2 minutes; typical under 45 seconds |
| Team adoption | At least one non-builder team member runs a crawl within two weeks of launch |