Skill: jernejcicorbin-hub/lead-enrichment-skill (version 1.0.0)

--- name: lead-enrichment description: > Enrich a company or person lead from public-web signals before outreach. Produces an ICP-fit verdict, key facts, red flags, and optional structured output. version: 1.0.0 metadata: openclaw: requires: env: - PRISMFY_API_KEY bins: - curl - jq primaryEnv: PRISMFY_API_KEY emoji: "🎯" homepage: https://prismfy.io --- # Lead Enrichment Use this skill when you already have a company, domain, or person lead and need a sourced qualification verdict before outreach. Why it helps: - confirms who the lead actually is, - gathers public fit evidence, - surfaces red flags before outreach, - prefers ambiguity over fake certainty. Best for: - lead qualification, - pre-outreach verification, - company/person enrichment after a lead is already found. Need a key? - Get free search access and your API key at: https://prismfy.io ## Setup 1. Install the skill: ```bash openclaw skills install lead-enrichment ``` 2. Add your Prismfy API key: ```bash export PRISMFY_API_KEY="ss_live_your_key_here" ``` To keep it after restart: ```bash echo 'export PRISMFY_API_KEY="ss_live_your_key_here"' >> ~/.bashrc source ~/.bashrc ``` Preflight: - `PRISMFY_API_KEY` is set - `curl` and `jq` are installed Advanced setup: - if quota lives on a different endpoint than search, set `PRISMFY_API_ROOT` or `PRISMFY_ME_URL` 3. Verify API access: ```bash cd ~/.openclaw/workspace/skills/lead-enrichment bash lead-enrich.sh --quota ``` 4. Quick smoke test: ```bash cd ~/.openclaw/workspace/skills/lead-enrichment bash lead-enrich.sh --company "Vercel" --query-family identity ``` 5. Export a JSON report: ```bash cd ~/.openclaw/workspace/skills/lead-enrichment bash lead-enrich.sh --company "Vercel" --query-family all --out lead_enrichment_report.json ``` You should see a short text result and no auth error. Optional automation: ```bash # Run from this skill directory: # ~/.openclaw/workspace/skills/lead-enrichment cp -r hooks/lead-enrichment ~/.openclaw/hooks/ find ~/.openclaw/hooks/lead-enrichment -maxdepth 1 -type f | sort openclaw hooks enable lead-enrichment openclaw hooks list ``` Quick example: Input: ```text Enrich this lead: person = "Guillermo Rauch", company = "Vercel", ICP = developer tooling, active growth-stage B2B. ``` Expected chat output: ```text Likely fit. 1. Identity: canonical domain = vercel.com 2. Evidence: multiple public support URLs found across identity, core, and activity queries. 3. Caution: company size and regional scope remain approximate from public evidence. 4. Source: https://vercel.com/ ``` For this Prismfy-based skill, Prismfy is the primary search mechanism for lead enrichment. Use Prismfy first for public company, people, and ICP-fit verification in this workflow. ## When to Use Use this skill when: - a company or person lead already exists, - the user wants enrichment, verification, or fit checking, - the task is pre-outreach, routing, qualification, or list cleanup. ## When NOT to Use Avoid using this skill when: - no lead candidate is provided, - the task is pure contact extraction with no qualification goal, - the task is pure market validation rather than lead qualification. ## Inputs - `--company` (optional) - `--domain` (optional) - `--person` (optional) - `--person-company` (optional) - `--role` (optional) - `--geo` (optional) - `--icp` (optional) - `--query-family` (optional: `identity|core|fit|activity|disqualifier|contact|all`) - at least one of `--company`, `--domain`, or `--person` is required ## Outputs Primary chat output: - short verdict in plain language, - fit verdict: `fit | likely_fit | weak_fit | not_fit | ambiguous`, - key signals, - disqualifiers or ambiguity, - missing evidence. Optional artifact output: - JSON when the user asks for a file, export, or machine-readable report. - Use `--out <file>` to write the report to disk. If JSON artifact is produced, required fields are: - `timestamp_utc` - `skill_version` - `entity_type` - `identity_status` - `preliminary_fit_verdict` - `summary` - `signals[]` - `disqualifiers[]` - `ambiguities[]` - `source_urls[]` - `run_failure_code` (nullable) ## Execution 1. Resolve identity first: - canonical company or person, - main domain if available, - role/company match if person-focused. 2. Run Prismfy query families in this order: - `identity` - `core` - `fit` - `activity` - `disqualifier` - `contact` only if clearly useful 3. Use the helper's preliminary evidence summary to classify conservatively: - `fit` - `likely_fit` - `weak_fit` - `not_fit` - `ambiguous` 4. Reply in chat with concise enrichment summary first. 5. Emit structured JSON only when requested, or when `--out` is used downstream. Command examples: ```bash # Check quota / connectivity bash lead-enrich.sh --quota # Company identity bash lead-enrich.sh --company "Vercel" --query-family identity # Company ICP fit bash lead-enrich.sh --company "PostHog" --icp "product-led B2B SaaS" --query-family fit # Person enrichment bash lead-enrich.sh --person "Guillermo Rauch" --company "Vercel" --role "founder" --query-family all ``` Execution contract: - preferred mode: balanced - source cap: up to 5 URLs per query - default chat mode: concise, human-readable, no raw JSON dump - if evidence is weak, prefer `weak_fit` or `ambiguous` over false certainty - `fit` requires resolved identity, multiple supporting URLs, and no material falsification evidence - strongest verdicts (`fit`, `likely_fit`) should come from `--query-family all`, not a partial family - the helper's verdict is preliminary and based on normalized public-web evidence, not hidden-data certainty - scoring is still keyword-based preliminary scoring, so human review is recommended for high-value outreach ## Query families ### `identity` - official site - about page - company exact name - person + company exact match ### `core` - product pages - docs/help center - pricing/customers/use-case pages - role/title traces ### `fit` - industry keywords - use-case keywords - segment / ICP keywords - stack or integration clues - support queries and falsification queries are both required - works without `--icp`, but `--icp` materially improves confidence ### `activity` - blog/newsroom/changelog - hiring/careers - recent announcements ### `disqualifier` - agency / consultant / freelancer signals - dead site / inactive / shutdown signals - wrong geo / wrong company / former role signals ### `contact` - contact/about/team/author/press pages - use only as a secondary family after fit qualification is already useful - can run from `--company`, `--domain`, or `--person` with company context ## Fit rubric - `fit`: resolved identity, multiple supporting URLs across families, and no material contradiction - `likely_fit`: good support with some gaps - `weak_fit`: partial relevance but weak or incomplete support - `not_fit`: meaningful disqualifiers or direct mismatch - `ambiguous`: identity or role/company match cannot be resolved confidently, or failures prevent confidence ## Failure Handling Use these failure codes: - `PRISMFY_UNAVAILABLE` - `PRISMFY_INVALID_RESPONSE` - `IDENTITY_UNCLEAR` - `NO_PUBLIC_EVIDENCE` - `RATE_LIMIT_OR_TIMEOUT` Handling guidance: - If Prismfy is unavailable, say so explicitly and downgrade certainty. - If identity is unclear, mark `ambiguous` rather than guessing. - If evidence is sparse, prefer `weak_fit`. - Never invent role, company, size, or contact details. ## Response Style - In normal chat, do not lead with JSON. - Lead with the verdict. - Then list only the most useful fit signals, red flags, and unknowns. - Prefer compact wording such as: - `1. Match: ...` - `2. Red flag: ...` - `3. Unknown: ...` - Mention a report filename only if `--out` created one. ## Safety - Do not expose API keys. - Do not fabricate people, roles, headcount, or emails. - Do not claim a contact method exists unless supported by public evidence. - Do not represent inferred data as confirmed fact. ## Minimal Example Input: - `--company`: `PostHog` - `--person`: `James Hawkins` - `--role`: `founder` - `--icp`: `developer-facing B2B SaaS` Default chat output: - `Likely fit.` - `1. Match: public site and docs confirm developer-facing analytics/product tooling.` - `2. Match: public leadership traces support founder identity.` - `3. Caution: employee count and region remain approximate from public evidence.` - `4. Source: https://posthog.com/`

path: _meta.json
content: {
  "name": "lead-enrichment",
  "displayName": "Lead Enrichment",
  "version": "1.0.0",
  "description": "Enrich a company or person lead from public-web signals using Prismfy-powered search.",
  "author": "Prismfy",
  "entry": "SKILL.md",
  "tags": [
    "leadgen",
    "sales",
    "research",
    "enrichment",
    "prismfy"
  ],
  "runtime": {
    "requiresEnv": ["PRISMFY_API_KEY"],
    "requiresBins": ["curl", "jq"]
  }
}


path: lead-enrich.sh
content: #!/usr/bin/env bash
set -euo pipefail

API_URL="${PRISMFY_API_URL:-https://api.prismfy.io/v1/search}"
API_URL="${API_URL%/}"
API_ROOT="${PRISMFY_API_ROOT:-}"
API_ROOT="${API_ROOT%/}"
ME_URL="${PRISMFY_ME_URL:-}"
ME_URL="${ME_URL%/}"
SKILL_VERSION="1.0.0"

die() { echo "ERROR: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }

need curl
need jq

company=""
domain=""
person=""
person_company=""
role=""
geo=""
icp=""
query_family="all"
quota=0
raw=0
engine=""
engines=""
out_file=""

usage() {
  cat <<'EOF'
Lead Enrichment helper

Usage:
  lead-enrich.sh --company "Company" [--query-family identity|core|fit|activity|disqualifier|contact|all]
  lead-enrich.sh --person "Person" [--company "Company"] [--role "Title"] [--query-family all]
  lead-enrich.sh --domain example.com [--query-family core]
  lead-enrich.sh --quota

Options:
  --company NAME
  --domain DOMAIN
  --person NAME
  --person-company NAME
  --role TITLE
  --geo GEO
  --icp TEXT
  --query-family NAME
  --engine NAME
  --engines CSV
  --out FILE
  --raw
  --quota

Notes:
  - This helper produces a conservative preliminary enrichment verdict.
  - It normalizes public-web evidence and favors ambiguity over false certainty.
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --company) shift; company="${1:-}" ;;
    --domain) shift; domain="${1:-}" ;;
    --person) shift; person="${1:-}" ;;
    --person-company) shift; person_company="${1:-}" ;;
    --role) shift; role="${1:-}" ;;
    --geo) shift; geo="${1:-}" ;;
    --icp) shift; icp="${1:-}" ;;
    --query-family) shift; query_family="${1:-all}" ;;
    --engine) shift; engine="${1:-}" ;;
    --engines) shift; engines="${1:-}" ;;
    --out) shift; out_file="${1:-}" ;;
    --raw) raw=1 ;;
    --quota) quota=1 ;;
    --help|-h) usage; exit 0 ;;
    *) die "unknown arg: $1" ;;
  esac
  shift
done

if [[ "$quota" -eq 1 ]]; then
  [[ -n "${PRISMFY_API_KEY:-}" ]] || die "PRISMFY_API_KEY is not set"
  if [[ -z "$ME_URL" ]]; then
    if [[ -n "$API_ROOT" ]]; then
      ME_URL="${API_ROOT}/user/me"
    elif [[ "$API_URL" == */search ]]; then
      ME_URL="${API_URL%/search}/user/me"
    else
      die "cannot derive quota endpoint from PRISMFY_API_URL; set PRISMFY_API_ROOT or PRISMFY_ME_URL"
    fi
  fi
  curl -fsS -H "Authorization: Bearer $PRISMFY_API_KEY" "$ME_URL"
  exit 0
fi

[[ -n "${PRISMFY_API_KEY:-}" ]] || die "PRISMFY_API_KEY is not set"
[[ -n "$company" || -n "$domain" || -n "$person" ]] || { usage; die "provide --company, --domain, or --person"; }

tmp_queries="$(mktemp)"
tmp_runs="$(mktemp)"
tmp_report="$(mktemp)"
trap 'rm -f "$tmp_queries" "$tmp_runs" "$tmp_report"' EXIT
printf '' > "$tmp_queries"
echo "[]" > "$tmp_runs"

target_company="${company:-$person_company}"
target_domain="$domain"

add_query() {
  local family="$1"
  local hypothesis="$2"
  local text="$3"
  [[ -n "$text" ]] || return 0
  jq -cn --arg family "$family" --arg hypothesis "$hypothesis" --arg text "$text" \
    '{family:$family,hypothesis:$hypothesis,query:$text}' >> "$tmp_queries"
  printf '\n' >> "$tmp_queries"
}

build_identity_queries() {
  if [[ -n "$company" ]]; then
    add_query identity support "\"$company\" official site"
    add_query identity support "\"$company\" about"
    add_query identity support "\"$company\" company"
  fi
  if [[ -n "$domain" ]]; then
    add_query identity support "\"$domain\" company"
    add_query identity support "site:$domain about"
    add_query identity support "site:$domain company"
  fi
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query identity support "\"$person\" \"$co\""
      add_query identity support "\"$person\" \"$co\" ${role:-role}"
    else
      add_query identity support "\"$person\" role"
      add_query identity support "\"$person\" bio"
    fi
  fi
}

build_core_queries() {
  if [[ -n "$domain" ]]; then
    add_query core support "site:$domain about"
    add_query core support "site:$domain product"
    add_query core support "site:$domain solutions"
    add_query core support "site:$domain pricing"
    add_query core support "site:$domain docs OR help"
  elif [[ -n "$company" ]]; then
    add_query core support "\"$company\" product"
    add_query core support "\"$company\" pricing"
    add_query core support "\"$company\" customers"
    add_query core support "\"$company\" docs"
  fi
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query core support "\"$person\" \"$co\" title"
      add_query core support "\"$person\" \"$co\" team"
      add_query core support "\"$person\" \"$co\" leadership"
    else
      add_query core support "\"$person\" title"
      add_query core support "\"$person\" bio"
      add_query core support "\"$person\" speaker"
    fi
  fi
}

build_fit_queries() {
  local target="${company:-$domain}"
  if [[ -n "$target" ]]; then
    [[ -n "$icp" ]] && add_query fit support "\"$target\" \"$icp\""
    [[ -n "$domain" && -n "$icp" ]] && add_query fit support "site:$domain \"$icp\""
    add_query fit support "\"$target\" customers"
    add_query fit support "\"$target\" integrations"
    add_query fit support "\"$target\" use cases"
    add_query fit support "\"$target\" b2b saas"
    add_query fit support "\"$target\" developer"
    add_query fit falsify "\"$target\" agency"
    add_query fit falsify "\"$target\" consultant"
    add_query fit falsify "\"$target\" freelancer"
    add_query fit falsify "\"$target\" consumer"
    add_query fit falsify "\"$target\" student"
    add_query fit falsify "\"$target\" hobby"
  fi
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query fit support "\"$person\" \"$co\" ${role:-role}"
      [[ -n "$icp" ]] && add_query fit support "\"$person\" \"$co\" \"$icp\""
      add_query fit falsify "\"$person\" former \"$co\""
      add_query fit falsify "\"$person\" advisor \"$co\""
      add_query fit falsify "\"$person\" intern \"$co\""
      add_query fit falsify "\"$person\" student"
    else
      add_query fit support "\"$person\" ${role:-role}"
      [[ -n "$icp" ]] && add_query fit support "\"$person\" \"$icp\""
      add_query fit falsify "\"$person\" student"
      add_query fit falsify "\"$person\" intern"
    fi
  fi
}

build_activity_queries() {
  local target="${company:-$domain}"
  [[ -n "$domain" ]] && {
    add_query activity support "site:$domain blog"
    add_query activity support "site:$domain news"
    add_query activity support "site:$domain changelog"
    add_query activity support "site:$domain careers"
  }
  [[ -n "$target" ]] && {
    add_query activity support "\"$target\" hiring"
    add_query activity support "\"$target\" announcement"
    add_query activity support "\"$target\" funding"
    add_query activity support "\"$target\" recent"
  }
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query activity support "\"$person\" \"$co\" interview"
      add_query activity support "\"$person\" \"$co\" webinar"
    else
      add_query activity support "\"$person\" interview"
      add_query activity support "\"$person\" webinar"
    fi
  fi
}

build_disqualifier_queries() {
  local target="${company:-$domain}"
  [[ -n "$target" ]] && {
    add_query disqualifier falsify "\"$target\" shutdown"
    add_query disqualifier falsify "\"$target\" inactive"
    add_query disqualifier falsify "\"$target\" acquired"
  }
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query disqualifier falsify "\"$person\" former \"$co\""
      add_query disqualifier falsify "\"$person\" ex-\"$co\""
    fi
    add_query disqualifier falsify "\"$person\" student"
    add_query disqualifier falsify "\"$person\" intern"
  fi
}

build_contact_queries() {
  if [[ -n "$domain" ]]; then
    add_query contact support "site:$domain contact"
    add_query contact support "site:$domain team"
    add_query contact support "site:$domain about"
    add_query contact support "site:$domain press"
  elif [[ -n "$company" ]]; then
    add_query contact support "\"$company\" contact"
    add_query contact support "\"$company\" team"
    add_query contact support "\"$company\" about"
    add_query contact support "\"$company\" press"
  fi
  if [[ -n "$person" ]]; then
    local co="${person_company:-$company}"
    if [[ -n "$co" ]]; then
      add_query contact support "\"$person\" \"$co\" contact"
      add_query contact support "\"$person\" \"$co\" team"
      add_query contact support "\"$person\" \"$co\" author"
    else
      add_query contact support "\"$person\" contact"
      add_query contact support "\"$person\" author"
      add_query contact support "\"$person\" email"
    fi
  fi
}

case "$query_family" in
  identity) build_identity_queries ;;
  core) build_core_queries ;;
  fit) build_fit_queries ;;
  activity) build_activity_queries ;;
  disqualifier) build_disqualifier_queries ;;
  contact) build_contact_queries ;;
  all)
    build_identity_queries
    build_core_queries
    build_fit_queries
    build_activity_queries
    build_disqualifier_queries
    ;;
  *) die "unsupported query family: $query_family" ;;
esac

mapfile -t query_lines < <(jq -cs 'unique_by(.query) | .[]' "$tmp_queries")
[[ "${#query_lines[@]}" -gt 0 ]] || die "no queries generated"

api_search() {
  local query="$1"
  local payload
  payload="$(jq -n \
    --arg q "$query" \
    --arg engine "$engine" \
    --arg engines "$engines" \
    --arg geo "$geo" \
    '{
      query: $q
    }
    + (if $engine != "" then {engine: $engine} else {} end)
    + (if $engines != "" then {engines: ($engines | split(","))} else {} end)
    + (if $geo != "" then {geo: $geo} else {} end)')"

  local attempt=0
  local max_attempts=2
  local response
  local err
  while (( attempt < max_attempts )); do
    attempt=$((attempt + 1))
    err="$(mktemp)"
    if response="$(curl -m 20 -fsS "$API_URL" \
      -H "Authorization: Bearer $PRISMFY_API_KEY" \
      -H "Content-Type: application/json" \
      -d "$payload" 2>"$err")"; then
      rm -f "$err"
      printf '%s' "$response"
      return 0
    fi
    local msg
    msg="$(cat "$err" 2>/dev/null || true)"
    rm -f "$err"
    if (( attempt >= max_attempts )); then
      printf '%s' "$msg"
      return 1
    fi
    sleep 1
  done
}

for line in "${query_lines[@]}"; do
  family="$(echo "$line" | jq -r '.family')"
  hypothesis="$(echo "$line" | jq -r '.hypothesis')"
  query="$(echo "$line" | jq -r '.query')"
   if response="$(api_search "$query")"; then
    if printf '%s' "$response" | jq -e 'type == "object" and ((has("results") | not) or (.results | type == "array"))' >/dev/null 2>&1; then
      trimmed_response="$(printf '%s' "$response" | jq -c 'if (.results? | type) == "array" then .results |= .[:5] else . end')"
      jq --arg family "$family" \
         --arg hypothesis "$hypothesis" \
         --arg query "$query" \
         --argjson response "$trimmed_response" \
         '. + [{
           family:$family,
           hypothesis:$hypothesis,
           query:$query,
           status:"ok",
           response:$response
         }]' "$tmp_runs" > "${tmp_runs}.new"
    else
      jq --arg family "$family" \
         --arg hypothesis "$hypothesis" \
         --arg query "$query" \
         --arg error "invalid_response_schema" \
         '. + [{
           family:$family,
           hypothesis:$hypothesis,
           query:$query,
           status:"error",
           error:$error
         }]' "$tmp_runs" > "${tmp_runs}.new"
    fi
  else
    jq --arg family "$family" \
       --arg hypothesis "$hypothesis" \
       --arg query "$query" \
       --arg error "$response" \
       '. + [{
         family:$family,
         hypothesis:$hypothesis,
         query:$query,
         status:"error",
         error:$error
       }]' "$tmp_runs" > "${tmp_runs}.new"
  fi
  mv "${tmp_runs}.new" "$tmp_runs"
done

jq -n \
  --arg timestamp_utc "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
  --arg skill_version "$SKILL_VERSION" \
  --arg company "$company" \
  --arg domain "$domain" \
  --arg person "$person" \
  --arg current_company "${person_company:-$company}" \
  --arg role "$role" \
  --arg icp "$icp" \
  --arg query_family "$query_family" \
  --argjson runs "$(cat "$tmp_runs")" \
  '
  def host_from_url:
    (. // "")
    | sub("^https?://";"")
    | split("/")[0]
    | sub("^www\\."; "");
  def source_type($known_domain):
    if .domain == "" then "unknown"
    elif $known_domain != "" and .domain == $known_domain then "official"
    elif (.domain | test("reddit\\.com$")) then "forum"
    elif (.domain | test("github\\.com$")) then "code"
    elif (.domain | test("news|techcrunch|crunchbase|venturebeat")) then "news"
    else "web"
    end;
  def normalized_text:
    ((.title // "") + " " + (.snippet // "")) | ascii_downcase;
  def stance($known_domain; $icp):
    . as $row
    | ($row | normalized_text) as $t
    | ($icp | ascii_downcase | split(" ") | map(select(length >= 4))) as $icp_terms
    | ($icp_terms | map(select($t | contains(.))) | length) as $icp_hits
    | if $row.family == "disqualifier" or $row.hypothesis == "falsify" then
        if ($t | test("shutdown|inactive|consultant|agency|freelancer|consumer|student|hobby|former|intern|advisor|acquired"))
           and ($t | test("not shutdown|not inactive|not a consultant|not an agency|not a freelancer") | not)
        then "falsify" else "neutral" end
      elif $row.source_type == "official" and $row.family == "activity" and ($t | test("blog|news|changelog|careers|hiring|announcement|funding|release")) then
        "support"
      elif $row.source_type == "official" and $row.family == "core" and ($t | test("product|pricing|docs|customers|solutions|integrations|help")) then
        "support"
      elif $row.source_type == "official" and $row.family == "identity" and ($t | test("about|company|team|leadership|founder|ceo|cto|official")) then
        "support"
      elif $row.source_type == "official" and $row.family == "fit" and ($icp_hits > 0 or ($t | test("customers|integrations|use cases|developer|b2b|saas"))) then
        "support"
      elif $row.family == "activity" and ($t | test("blog|news|changelog|careers|hiring|announcement|funding|release")) then
        "support"
      elif $row.family == "core" and ($t | test("product|pricing|docs|customers|solutions|integrations|help")) then
        "support"
      elif $row.family == "identity" and ($t | test("about|company|team|leadership|founder|ceo|cto|official")) then
        "support"
      elif $row.family == "fit" and ($icp_hits > 0 or ($t | test("customers|integrations|use cases|developer|b2b|saas"))) then
        "support"
      else
        "neutral"
      end;
  def limited_results:
    (.response.results // [])[:5];
  def results_to_evidence($known_domain):
    [ .runs[]
      | select(.status=="ok")
      | .family as $family
      | .hypothesis as $hypothesis
      | .query as $query
      | limited_results[]
      | {
          family: $family,
          hypothesis: $hypothesis,
          query: $query,
          title: (.title // ""),
          url: (.url // ""),
          snippet: (.snippet // ""),
          domain: ((.url // "") | host_from_url)
        }
      | .source_type = (source_type($known_domain))
      | .stance = (stance($known_domain; $icp))
    ]
    | unique_by(.url)
    | map(select(.url != ""));
  def canonical_domain($provided_domain):
    if $provided_domain != "" then $provided_domain
    else
      ([ .runs[]
         | select(.status=="ok" and .family=="identity")
         | limited_results[]
         | select((((.title // "") + " " + (.snippet // "")) | ascii_downcase | test("official|about|company|team|leadership|founder|ceo|cto")))
         | (.url // "" | host_from_url)
         | select(. != "")
         | select(test("reddit\\.com$|github\\.com$|linkedin\\.com$|x\\.com$|twitter\\.com$|youtube\\.com$") | not)
       ] | group_by(.) | map(select(length >= 2)) | sort_by(length) | reverse | .[0][0]) // ""
    end;
  def count_family($evidence; $family):
    ($evidence | map(select(.family==$family and .stance=="support")) | length);
  def count_stance($evidence; $stance):
    ($evidence | map(select(.stance==$stance)) | length);
  def count_source_type($evidence; $source_type):
    ($evidence | map(select(.stance=="support" and .source_type==$source_type)) | length);
  def domains_for($evidence; $family):
    ($evidence | map(select(.family==$family and .stance=="support") | .domain) | unique | map(select(. != "")));
  {
    runs: $runs,
    timestamp_utc: $timestamp_utc,
    skill_version: $skill_version,
    company: $company,
    domain: $domain,
    person: $person,
    current_company: $current_company,
    role: $role,
    icp: $icp,
    query_family: $query_family
  } as $root
  | $root
  | (canonical_domain($domain)) as $canonical_domain
  | (results_to_evidence($canonical_domain)) as $evidence
  | (count_family($evidence; "identity")) as $identity_support_count
  | (count_family($evidence; "core")) as $core_count
  | (count_family($evidence; "fit")) as $fit_count
  | (count_family($evidence; "activity")) as $activity_count
  | ($evidence | map(select(.family=="disqualifier" and .stance=="falsify")) | length) as $disqualifier_count
  | (count_stance($evidence; "support")) as $support_count
  | (count_stance($evidence; "falsify")) as $falsify_count
  | (count_source_type($evidence; "official")) as $official_support_count
  | ($evidence | map(.domain) | unique | map(select(. != ""))) as $domains
  | (($root.runs | map(select(.status=="error")) | length)) as $error_count
  | (($root.runs | length)) as $run_count
  | ($root.runs | map(select(.status=="error") | .error) | join(" | ")) as $error_text
  | (
      if $canonical_domain != "" and $identity_support_count >= 2 then "resolved"
      elif $identity_support_count >= 1 then "partial"
      else "unclear"
      end
    ) as $identity_status
  | (
      if $identity_status == "unclear" then "ambiguous"
      elif $error_count == $run_count then "ambiguous"
      elif $query_family != "all" and ($support_count > 0 or $activity_count > 0) then "weak_fit"
      elif $disqualifier_count >= 2 and $support_count <= 1 then "not_fit"
      elif $falsify_count >= 3 and $support_count <= 1 then "not_fit"
      elif $identity_status == "resolved" and $official_support_count >= 2 and $core_count >= 1 and $fit_count >= 1 and $activity_count >= 1 and $support_count >= 5 and ($domains | length) >= 2 and $falsify_count == 0 and $error_count == 0 then "fit"
      elif $identity_status == "resolved" and $official_support_count >= 1 and $core_count >= 1 and $fit_count >= 1 and $support_count >= 4 and $falsify_count == 0 and $error_count <= 1 then "likely_fit"
      elif $support_count >= 1 or $activity_count >= 1 then "weak_fit"
      else "ambiguous"
      end
    ) as $fit_verdict
  | (
      if ($error_text | test("timed out|timeout|429|rate")) then "RATE_LIMIT_OR_TIMEOUT"
      elif ($error_text | test("invalid_json_response|invalid_response_schema")) then "PRISMFY_INVALID_RESPONSE"
      elif $error_count == $run_count then "PRISMFY_UNAVAILABLE"
      elif $identity_status == "unclear" then "IDENTITY_UNCLEAR"
      elif ($evidence | length) == 0 then "NO_PUBLIC_EVIDENCE"
      else null
      end
    ) as $run_failure_code
  | {
      timestamp_utc: $timestamp_utc,
      skill_version: $skill_version,
      entity: {
        company_name: $company,
        domain: $canonical_domain,
        person_name: $person,
        current_company: $current_company,
        role: $role,
        icp_criteria: $icp
      },
      entity_type:
        (if $person != "" and ($company != "" or $current_company != "") then "person+company"
         elif $person != "" then "person"
         else "company"
         end),
      query_family: $query_family,
      identity_status: $identity_status,
      preliminary_fit_verdict: $fit_verdict,
      summary:
        ("identity=" + $identity_status
         + ", support_urls=" + ($support_count|tostring)
         + ", falsify_urls=" + ($falsify_count|tostring)
         + ", activity_urls=" + ($activity_count|tostring)
         + ", disqualifier_urls=" + ($disqualifier_count|tostring)
         + ", domains=" + (($domains|length)|tostring)),
      signals: [
        (if $canonical_domain != "" then "canonical_domain:" + $canonical_domain else empty end),
        (if $support_count > 0 then "support_evidence:" + ($support_count|tostring) else empty end),
        (if $activity_count > 0 then "activity_evidence:" + ($activity_count|tostring) else empty end)
      ],
      disqualifiers:
        ($evidence
         | map(select(.stance=="falsify")
               | ((.title // .snippet) | tostring))
         | map(select(length > 0))
         | .[:5]),
      ambiguities: [
        (if $identity_status != "resolved" then "identity_not_fully_resolved" else empty end),
        (if $error_count > 0 and $error_count < $run_count then "partial_query_failures" else empty end),
        (if $canonical_domain == "" and $company != "" then "canonical_domain_not_confirmed" else empty end)
      ],
      source_urls: ($evidence | map(.url) | unique | .[:10]),
      evidence: ($evidence | .[:20]),
      query_runs: $root.runs,
      run_failure_code: $run_failure_code
    }' > "$tmp_report"

if [[ -n "$out_file" ]]; then
  cp "$tmp_report" "$out_file"
fi

if [[ "$raw" -eq 1 ]]; then
  cat "$tmp_report"
  exit 0
fi

jq -r '
  .preliminary_fit_verdict as $v |
  .entity as $e |
  [
    (
      if $v == "fit" then "Fit."
      elif $v == "likely_fit" then "Likely fit."
      elif $v == "weak_fit" then "Weak fit."
      elif $v == "not_fit" then "Not fit."
      else "Ambiguous."
      end
    ),
    (
      if .entity.domain != "" then
        "1. Identity: canonical domain = " + .entity.domain
      elif .identity_status != "resolved" then
        "1. Identity: not fully resolved from public evidence"
      else
        "1. Identity: resolved from public evidence"
      end
    ),
    (
      "2. Evidence: " + .summary
    ),
    (
      if (.disqualifiers | length) > 0 then
        "3. Red flag: " + .disqualifiers[0]
      elif (.ambiguities | length) > 0 then
        "3. Caution: " + .ambiguities[0]
      else
        "3. Caution: treat this as preliminary public-web enrichment, not hidden-data certainty"
      end
    ),
    (
      if (.source_urls | length) > 0 then
        "4. Source: " + .source_urls[0]
      else
        "4. Source: no strong source URL surfaced"
      end
    )
  ] | join("\n")
' "$tmp_report"


path: README.md
content: # Turn Raw Leads into Sourced Fit Verdicts

`lead-enrichment` turns a company, domain, or person into a public-web qualification verdict before outreach.

Built for outbound and qualification workflows, it checks identity, ICP match, activity, and red flags, then returns a concise fit-or-no-fit summary with source-backed evidence.

You give:
- `company`, `domain`, or `person + company`

You get:
- canonical identity
- preliminary fit verdict
- top supporting signals
- red flags and ambiguity
- source URLs
- optional JSON when you ask for export mode

Powered by Prismfy web search. Get your API key at [prismfy.io](https://prismfy.io).

## Example

Input:

```text
Enrich this lead: company = "PostHog", person = "James Hawkins", role = "founder", ICP = product-led B2B SaaS, active growth-stage team.
```

Expected result:

```text
Likely fit.

1. Identity: canonical domain = posthog.com
2. Evidence: identity resolved, product/docs/hiring traces found, multiple public support URLs.
3. Caution: employee count and regional scope remain approximate from public evidence.
```

## What it does

Given a company, domain, or person, this skill:
1. resolves identity,
2. collects public company or role facts,
3. tests support and falsification signals,
4. checks activity and recency,
5. flags disqualifiers,
6. returns a concise preliminary verdict,
7. optionally writes a structured JSON artifact.

The default behavior is chat-first. It should answer like an analyst, not dump raw JSON unless you asked for an export.
Use `--out <file>` when you want a saved JSON report.

## Setup

### 1. Install the skill

```bash
openclaw skills install lead-enrichment
```

### 2. Add your Prismfy API key

```bash
export PRISMFY_API_KEY="ss_live_your_key_here"
```

To keep it after restart:

```bash
echo 'export PRISMFY_API_KEY="ss_live_your_key_here"' >> ~/.bashrc
source ~/.bashrc
```

### 3. Preflight

- `PRISMFY_API_KEY` is set
- `curl` and `jq` are installed

### 4. Verify API access

```bash
cd ~/.openclaw/workspace/skills/lead-enrichment
bash lead-enrich.sh --quota
```

Advanced setup:
- if quota lives on a different endpoint than search, set `PRISMFY_API_ROOT` or `PRISMFY_ME_URL`

### 5. Quick smoke test

```bash
cd ~/.openclaw/workspace/skills/lead-enrichment
bash lead-enrich.sh --company "Vercel" --query-family identity
```

You should see a short text result and no auth error.

### 6. Export a JSON report

```bash
cd ~/.openclaw/workspace/skills/lead-enrichment
bash lead-enrich.sh --company "Vercel" --query-family all --out lead_enrichment_report.json
```

## Optional automation

Recommended if you want OpenClaw to remember this workflow more consistently.

```bash
# Run from this skill directory:
# ~/.openclaw/workspace/skills/lead-enrichment

cp -r hooks/lead-enrichment ~/.openclaw/hooks/
find ~/.openclaw/hooks/lead-enrichment -maxdepth 1 -type f | sort
openclaw hooks enable lead-enrichment
openclaw hooks list
```

## Included files

- `SKILL.md` — OpenClaw skill instructions
- `lead-enrich.sh` — bundled Prismfy query helper
- `hooks/lead-enrichment/` — optional bootstrap reminder hook

## Important MVP note

This skill is designed as evidence-backed enrichment, not a hidden-data enrichment vendor replacement.

It works from public-web signals found through Prismfy. The verdict is intentionally preliminary and should default to ambiguity when identity or fit evidence is weak.
Scoring is still keyword-based preliminary scoring, so human review is still recommended for high-value outreach.
Use `--query-family all` when you want the strongest fit verdict.


path: hooks/lead-enrichment/handler.js
content: /**
 * Lead Enrichment Hook for OpenClaw
 *
 * Injects a lightweight reminder during bootstrap for company/person lead enrichment.
 */

const REMINDER_CONTENT = `
## Lead Enrichment Reminder

Use lead enrichment when a task needs:
- company verification,
- person/company role confirmation,
- ICP-fit checking,
- public activity signals,
- pre-outreach qualification.

Workflow:
- preflight: confirm \`PRISMFY_API_KEY\` is available
- resolve identity first
- run Prismfy queries for identity, core facts, fit, activity, and disqualifiers
- use \`bash lead-enrich.sh --company "Vercel" --query-family all\` as the baseline command shape
- return a short preliminary fit verdict in chat
- cite sources for verdict-driving facts
- default to ambiguous when identity or fit evidence is weak

Guardrails:
- require \`PRISMFY_API_KEY\` before live search
- do not invent role, company, headcount, or contact details
- if evidence is weak, prefer weak_fit or ambiguous
- do not treat inferred facts as confirmed facts
`.trim();

const handler = async (event) => {
  if (!event || typeof event !== "object") return;
  if (event.type !== "agent" || event.action !== "bootstrap") return;
  if (!event.context || typeof event.context !== "object") return;

  if (!Array.isArray(event.context.bootstrapFiles)) event.context.bootstrapFiles = [];
  event.context.bootstrapFiles.push({
    path: "LEAD_ENRICHMENT_REMINDER.md",
    content: REMINDER_CONTENT,
    virtual: true,
  });
};

module.exports = handler;
module.exports.default = handler;


path: hooks/lead-enrichment/HOOK.md
content: ---
name: lead-enrichment
description: "Injects a lead enrichment reminder during agent bootstrap"
metadata: {"openclaw":{"emoji":"🎯","events":["agent:bootstrap"]}}
---

# Lead Enrichment Hook

Injects a lightweight reminder to use `lead-enrich.sh` with `PRISMFY_API_KEY` for public-web lead enrichment before outreach or lead routing.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)