---
name: ph_check_profiles
description: >-
  Resolves Product Hunt profiles for leads (Telegram @ first, then search/users by name),
  scrapes profile signals, compares to seed data (especially LinkedIn), and assigns
  LLM match_confidence for auto-accept vs manual review. Use when matching people to PH,
  verifying PH profile identity, enrichment after URL discovery, or confidence scoring
  for PH leads.
---

# Product Hunt profile discovery and verification (`ph_check_profiles`)

## Purpose

End-to-end workflow: **find** a Product Hunt profile for a person, **prove** it is the same individual as the seed row, assign **`match_confidence`**, then optionally enrich. “URL found” ≠ “correct person” unless verification passes.

## Preconditions (input hygiene)

Before any HTTP/Firecrawl spend:

- Skip if **`ph_profile_url`** already populated (resume-safe).
- Skip obvious non-person rows (e.g. email in name field) — document in `notes` / `match_rule`.
- **Name search:** if only one name token (no surname), **do not** run `search/users` by name; still run **handle** probe when a Telegram username exists.

## Stage A — Handle from Telegram (username-first)

1. Normalize handle: strip leading `@`, lowercase ASCII slug rules.
2. Probe `https://www.producthunt.com/@<handle>` with **Firecrawl scrape** (`markdown`) first — plain HTTP often returns Cloudflare **403**.
3. Classify from markdown: **yes** (e.g. `/@slug/followers` link for same slug), **no** (404 markers), **unknown**, **error**. Invalid slug → `skipped_invalid`.

## Stage B — Name fallback (`search/users` only)

When Stage A is `no` / `unknown` / `error` **and** first + last name exist:

1. Build **`https://www.producthunt.com/search/users?q=<first>+<last>`** (URL-encoded). **Never** use the main PH product search for people.
2. **Firecrawl scrape** with wait for results (e.g. `--wait-for` ~4000 ms or equivalent). Retry once with adjusted wait; then **Parallel.ai** URL extract on the same search URL if Firecrawl fails.
3. Parse candidates from markdown via stable anchors: `.../@<username>/followers` and `.../@<username>/following`.
4. **Matching:** normalize strings (lowercase, strip punctuation, collapse spaces). Candidate valid only if normalized display name contains **both** normalized first and last tokens.
5. **Tie-break:** among valid candidates, maximize **`followers + following`**, then followers, then following. If none valid → `not_found` (do not guess).

## Stage C — Merge decision (when both handle and name ran)

Emit one of: `confirmed` | `handle_only` | `name_only` | `mismatch` | `neither` (define consistently with the project CSV).

## Stage D — Validate profile + LLM `match_confidence`

**Goal:** Confirm identity using public PH signals vs seed CRM/export.

### D1 — Re-scrape profile (signals)

Use **Firecrawl scrape** (`markdown`) on the resolved profile URL (`https://www.producthunt.com/@username`, or project-canonical `/users/{username}` if cleaner). Extract at minimum:

- **Tagline** (headline under display name)
- **About / description**
- **Rating / points** on profile (kitten or kitty points per visible scope)
- **External links:** LinkedIn, website, X/Twitter, others present in markdown
- Optional: badges, maker history snippet for disambiguation

Fallback order if Firecrawl unusable: **Parallel.ai** → **agent-browser**. Log `firecrawl_method` / failure.

### D2 — Compare to seed

- Normalize **LinkedIn** URLs (strip tracking, compare `in/<slug>`).
- **Strong negative:** seed LinkedIn slug **A** and PH profile shows slug **B** → treat as **almost certainly wrong match** unless the user documents an exception.

### D3 — LLM confidence

Run an **LLM** (or rules + LLM) to set:

- **`match_confidence`** — numeric **0–1** or enum **`high` / `medium` / `low`** (agree schema with user once).
- **`confidence_signals`** — short audit text: which fields agreed, contradicted, or were missing.

Policy:

- **High** — auto-accept only when rules and signals align (no LinkedIn conflict).
- **Medium / low** or **LinkedIn conflict** — **queue for manual review**; do not mark production-verified silently.

### D4 — Credits

Append every validation scrape to the **same Firecrawl credit ledger** as discovery. Reuse Stage D markdown for Stage E enrichment when possible to avoid duplicate scrapes of the same profile URL.

## Stage E — Enrichment (optional, after D)

Map profile markdown into columns: `ph_about`, kitten points per scope (month/year/all_time as shown—empty scope is normal), links, badges, counters. Do **not** populate legacy generic `ph_points` (removed by PH).

## Recommended output columns (extend as needed)

- `entry_id`, `record_id`, `full_name_raw`, `query`, `query_url`, `scrape_status`
- `ph_profile_url`, `ph_username`, `matched_display`
- `ph_followers_count`, `ph_following_count`, `followers_plus_following`, `match_rule`, `notes`
- `match_confidence`, `confidence_signals`
- `seed_linkedin_url`, `ph_linkedin_url_profile`, `linkedin_mismatch` (boolean)

## Operational rules

- **One output row per input row** unless user asks otherwise.
- Checkpoint every **25–50** rows; resume by `entry_id` / processed flag.
- Validate parsers on **1–3** rows before scaling (regex too strict → zero candidates).
- Track Firecrawl credits before/after each segment; stop at user-agreed cap and persist progress.
