Skip to main content

B2B visitor intelligence — ICP scoring from IP, behavior, company URL, LinkedIn, and client-similarity signals. No LLM required.

Project description

visitor-intel

Know if the visitor on your B2B website matches your ICP — from their IP address and behavioral signals. No LLM required. No API key for basic usage. Sub-millisecond per visitor.

import asyncio
from visitor_intel import (
    fetch_company_intel, classify_ai_source,
    enrich_company_url, company_signals_to_score,
    score_linkedin,
    load_client_profiles, compute_client_similarity,
    compute_icp_score, score_to_fit_tier, derive_channel,
)

async def main():
    # Load your existing client profiles once at startup
    client_profiles = await load_client_profiles([
        "https://stripe.com", "https://notion.so", "https://figma.com",
    ])

    # Signals from the visitor's browser
    company      = await fetch_company_intel("203.0.113.42")
    ai_referrer  = classify_ai_source("https://chatgpt.com/c/abc123")

    # Optional: visitor shared their company URL or LinkedIn via a form/chat
    company_signals  = await enrich_company_url("https://acmecorp.com")
    url_score        = company_signals_to_score(company_signals)
    li_score         = score_linkedin("https://linkedin.com/in/jane-doe", job_title="VP of Sales")
    similarity_score = compute_client_similarity(company_signals, client_profiles)

    score = compute_icp_score(
        company_identified=company["company_identified"],
        utm_source="linkedin",
        ai_referrer=ai_referrer,
        page_type="pricing",
        time_on_page_sec=90,
        scroll_depth_pct=80,
        cta_clicked=True,
        pages_visited=3,
        is_returning=False,
        company_url_score=url_score,
        linkedin_score=li_score,
        client_similarity_score=similarity_score,
    )
    print(score_to_fit_tier(score))  # "icp_fit"
    print(derive_channel("linkedin", None, None))  # "LinkedIn Ads"

asyncio.run(main())

Why

Every B2B website has visitors that never convert. Most of them are unqualified — but you can't tell which until it's too late or until you pay Clearbit $500/mo.

This library gives you:

  • Company identification from IP — is this a real business, or a residential/ISP connection?
  • ISP/cloud filtering — 40+ ISP and cloud provider patterns stripped automatically
  • Company website enrichment — pricing page, hiring signals, SaaS stack — from their URL, zero scraping APIs
  • LinkedIn scoring — person vs. company URL, seniority detection, no scraping
  • Client-similarity ICP scoring — define your ICP by your existing clients, not generic rules
  • AI referral detection — did this visitor come from ChatGPT, Perplexity, Claude?
  • Rule-based composite scoring — 9-signal score, 0–100, zero LLM cost
  • Channel normalization — UTM + referrer → clean display name

Install

pip install visitor-intel

Requires Python 3.9+. The only dependency is httpx.


Usage

1. Identify the company from IP

import asyncio
from visitor_intel import fetch_company_intel

async def main():
    result = await fetch_company_intel("203.0.113.42")
    print(result)
    # {
    #   "company_identified": True,
    #   "company_name": "Acme Corp",
    #   "company_domain": "",       # populated if you pass ipinfo_token
    #   "city": "San Francisco",
    #   "country": "US",
    # }

asyncio.run(main())

With IPinfo (higher accuracy):

result = await fetch_company_intel("203.0.113.42", ipinfo_token="your_token_here")
# company_domain is now populated, company.type == "business" check applied

Free tier: ip-api.com, 1000 requests/day, no sign-up. Upgrade path: pass your IPinfo token for business-type filtering and domain resolution.


2. Detect AI referrals

from visitor_intel import classify_ai_source

source = classify_ai_source("https://chatgpt.com/c/abc123")
# "chatgpt"

source = classify_ai_source("https://perplexity.ai/search?q=b2b+sales+tools")
# "perplexity"

source = classify_ai_source("https://google.com")
# None

Supported: ChatGPT, Perplexity, Claude, Copilot, Gemini, You.com, Phind.


3. Enrich from company URL

When a visitor shares their company website (via a form or chat), fetch ICP signals from it — no API key or scraping service required.

from visitor_intel import enrich_company_url, company_signals_to_score

signals = await enrich_company_url("https://acmecorp.com")
# {
#   "title": "Acme Corp — B2B Workflow Automation",
#   "description": "The platform for modern revenue teams.",
#   "has_pricing_page": True,
#   "has_careers_page": True,
#   "saas_tools_detected": ["hubspot", "intercom", "stripe"],
#   "domain": "acmecorp.com",
# }

score = company_signals_to_score(signals)  # 0–25

Points from company_signals_to_score:

Signal Points
Pricing page found +10
Careers page found (growing team) +5
SaaS tools detected in stack +5
Company description present +5

4. Score a LinkedIn URL

When a visitor shares their LinkedIn profile (via a form or chat), extract intent and seniority signals without scraping.

from visitor_intel import parse_linkedin_url, score_linkedin

# Parse the URL type and handle
parsed = parse_linkedin_url("https://linkedin.com/in/jane-doe")
# {"valid": True, "type": "person", "handle": "jane-doe"}

parsed = parse_linkedin_url("https://linkedin.com/company/acmecorp")
# {"valid": True, "type": "company", "handle": "acmecorp"}

# Score it (0–20)
score = score_linkedin("https://linkedin.com/in/jane-doe", job_title="VP of Sales")
# 20 — decision-maker title detected

score = score_linkedin("https://linkedin.com/in/jane-doe")
# 10 — personal URL, no title info

score = score_linkedin("https://linkedin.com/company/acmecorp")
# 5 — company page URL

score = score_linkedin(None)
# 0 — no URL

Decision-maker titles detected: CEO, CTO, CMO, COO, CFO, CPO, Founder, Co-Founder, VP, Vice President, Director, Head of, President, Owner, Partner, Managing Director, GM.


5. Client-similarity scoring

Instead of generic scoring rules, define your ICP by the companies you've already won. The library fingerprints each client (tech stack, industry, size tier, B2B orientation) and scores new visitors by similarity.

from visitor_intel import (
    load_client_profiles, compute_client_similarity,
    enrich_company_url,
)

# --- At startup (call once, cache the result) ---
client_profiles = await load_client_profiles([
    "https://stripe.com",
    "https://notion.so",
    "https://figma.com",
])

# --- Per visitor (when you have their company URL) ---
visitor_signals = await enrich_company_url("https://newvisitor.com")
similarity_score = compute_client_similarity(visitor_signals, client_profiles)
# Returns 0–30, pass as client_similarity_score= to compute_icp_score()

Similarity weights:

Dimension Weight
Tech stack overlap (Jaccard) 40%
Industry tag overlap (Jaccard) 35%
Company size tier match 15%
B2B / B2C orientation match 10%

Industries detected: SaaS, fintech, healthcare, e-commerce, marketing, HR, security, legal, logistics, edtech.

You can also inspect a single company's profile:

from visitor_intel import profile_company, CompanyProfile

profile = await profile_company("https://acmecorp.com")
# CompanyProfile(
#   domain="acmecorp.com",
#   has_pricing_page=True,
#   has_careers_page=True,
#   saas_tools=frozenset({"hubspot", "stripe"}),
#   industry_tags=frozenset({"saas", "marketing"}),
#   size_tier="smb",
#   is_b2b=True,
# )

6. Score the visitor

from visitor_intel import compute_icp_score, score_to_fit_tier

score = compute_icp_score(
    company_identified=True,          # from fetch_company_intel()
    utm_source="linkedin",            # from URL params
    ai_referrer="chatgpt",            # from classify_ai_source()
    page_type="pricing",              # which page they landed on
    time_on_page_sec=90,              # seconds on page
    scroll_depth_pct=80,              # % of page scrolled
    cta_clicked=True,                 # clicked your primary CTA
    pages_visited=3,                  # pages this session
    is_returning=True,                # repeat visitor
    company_url_score=url_score,      # from company_signals_to_score()
    linkedin_score=li_score,          # from score_linkedin()
    client_similarity_score=sim_score,# from compute_client_similarity()
)

tier = score_to_fit_tier(score)
# "icp_fit"   → score ≥ 70
# "potential" → score ≥ 50
# "low_fit"   → score < 50

All signals are optional. Missing/None values score zero for that signal.


7. Normalize the channel

from visitor_intel import derive_channel

derive_channel("linkedin", None, None)
# "LinkedIn Ads"

derive_channel(None, "chatgpt", None)
# "ChatGPT"

derive_channel(None, None, "https://google.com/search?q=visitor+intelligence")
# "Organic"

derive_channel(None, None, None)
# "Direct"

Score breakdown

Signal Points Condition
Company identified +20 Business IP detected
Traffic source +15 LinkedIn / Google Ads
+10 Organic / SEO
+8 Other UTM
+5 Direct (no UTM)
AI referral +10 ChatGPT / Perplexity / Claude / etc.
Page type +20 Pricing
+15 Product / Features
+10 Docs / Technical
+5 Other
Time on page +10 ≥60 seconds
+5 ≥30 seconds
Scroll depth +10 ≥70% scrolled
CTA clicked +15 Primary CTA clicked
Pages visited +15 3+ pages this session
+10 2 pages this session
Returning visitor +10 Known repeat visitor
Company URL +10 Pricing page found on their site
+5 Careers page found (growing team)
+5 B2B SaaS tools detected in their stack
+5 Company description present
LinkedIn +20 Personal URL + decision-maker title
+10 Personal URL (any title)
+5 Company page URL
Client similarity +0–30 Weighted fingerprint match against your known clients

Maximum: 100 (capped). Company URL, LinkedIn, and client similarity signals are optional — pass 0 if not available.


Running the example

git clone https://github.com/Wing-e7/visitor-intel
cd visitor-intel
pip install -e ".[dev]"
python examples/quickstart.py

Running tests

pytest tests/ -v

No network calls required — all tests are synchronous unit tests against the scoring and normalization logic.


Architecture

visitor-intel/
  visitor_intel/
    ip_enrichment.py      # fetch_ip_intel, fetch_company_intel, classify_ai_source
    company_enrichment.py # enrich_company_url, company_signals_to_score
    linkedin.py           # parse_linkedin_url, score_linkedin
    icp_profile.py        # CompanyProfile, profile_company, load_client_profiles, compute_client_similarity
    scoring.py            # compute_icp_score, score_to_fit_tier
    channel.py            # derive_channel
  examples/
    quickstart.py
  tests/
    test_scoring.py
    test_company_enrichment.py
    test_linkedin.py
    test_icp_profile.py

No framework. No ORM. No config files. Plain async Python + httpx.


What it doesn't do

  • No LLM calls (zero marginal cost per visitor)
  • No enrichment beyond what IP + behavioral signals give you
  • No storage (bring your own DB)
  • No real-time webhook delivery (wire it into your own stack)

For the full experience — AI-powered visitor scoring, voice conversation, and CRM handoff — see Percepto AI.


Contributing

PRs welcome. Open an issue first for anything beyond a small fix.


License

MIT © Percepto AI

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

visitor_intel-0.1.0.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

visitor_intel-0.1.0-py3-none-any.whl (17.7 kB view details)

Uploaded Python 3

File details

Details for the file visitor_intel-0.1.0.tar.gz.

File metadata

  • Download URL: visitor_intel-0.1.0.tar.gz
  • Upload date:
  • Size: 20.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for visitor_intel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 096f6c4a7b8406e7ce498f9da53cd35c778f31832a62c62bb3f4a1dfbce05c20
MD5 a28c8856a1b976b67fabfebbad6bc1cd
BLAKE2b-256 1d14dd93a9013cf6e11d41ad2c13cafc04a2d6b7b0e842de61e72c3bc0b47663

See more details on using hashes here.

File details

Details for the file visitor_intel-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: visitor_intel-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 17.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for visitor_intel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 91b48c91fa9b4efba37f7abf265e51ca7fb382055b60f18cd5816e53e06759f7
MD5 e44c02c9aa7bfc5c5818e7283d6630bd
BLAKE2b-256 5b76a932e676506ca858b2cd7fcabdba8fc6453a1991799fc4b23dbc7f7c12a8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page