Skip to main content
Glama
fairquery-contact-enricher

fairquery-contact-enricher

Official

Website Contact & Company Enricher

Turns company domains into one row each (public business emails, phones, social profiles, company name and address) for $3 per 1,000 domains. You pay $0.003 per domain that returns a result row, and unreachable, blocked or invalid domains are free. Example: enriching 500 domains costs at most 500 × $0.003 = $1.50, plus Apify's $0.00005 Actor-start fee.

Fairquery Apify Actor that turns a list of domains into one clean row per company: business emails (validated for MX, disposable domains and typos), phone numbers (E.164), social profiles, company name, description and postal address, taken from the homepage and its contact / about / imprint pages. Plain HTTP, no browser, robots.txt respected, one request at a time per site.

When to use this Actor (LLM / tool selection)

Use it when an agent or sales-ops workflow needs to:

  • Add a contact email, phone and LinkedIn company page to a list of company websites (CRM enrichment, lead lists, vendor onboarding)

  • Find the generic inbox (info@, sales@, support@, press@) of a company and check that its domain actually receives mail

  • Get the legal company name and address from structured data or the imprint page (EU sites)

  • Collect a company's official social accounts

Do not use it to find personal emails of individuals or to scrape people's profiles. By default it returns role / generic mailboxes only (emailScope: "role"); personal LinkedIn profiles (/in/…) are never returned.

Related MCP server: Brand Intelligence MCP

How it works

  1. Normalises each input (https://www.Acme.com/contact, jane@acme.com, acme.com → acme.com) and removes duplicates.

  2. Reads robots.txt (RFC 9309: missing = allowed, 5xx = disallowed) and honours Disallow rules and Crawl-delay for user agent FairqueryBot.

  3. Fetches the homepage, then the best-scoring same-site pages (contact, Kontakt, contacto, about, imprint/Impressum, legal notice, team…) up to maxPagesPerDomain, waiting requestDelayMs between requests to the same site.

  4. Extracts: JSON-LD Organization/LocalBusiness (name, legalName, telephone, email, address, sameAs), mailto: and tel: links, Cloudflare-protected emails, name [at] domain [dot] com obfuscation, og:site_name, <address> blocks, social links.

  5. Validates emails with the same code as Fairquery Email Validator: syntax, DNS-over-HTTPS MX/null-MX, disposable list, free-provider and typo checks (no SMTP probing).

Input

Field

Type

Default

Description

domains

string[]

(required)

Domains, URLs or emails

maxPagesPerDomain

int

5

1–15, homepage included

emailScope

role | business | all

role

business adds named addresses on the company's own domain

validateEmails

bool

true

MX / disposable / typo

checkSpfDmarc

bool

false

SPF/DMARC for the company mail domain

defaultPhoneCountry

string

–

e.g. US; also parse national-format numbers in page text

domainConcurrency

int

5

Sites in parallel

requestDelayMs

int

1000

Delay between requests to one site

requestTimeoutSecs

int

15

Per request

domainTimeoutSecs

int

40

Hard limit per domain (all pages + DNS). At the limit you get the data collected so far (partial: true), or an uncharged DOMAIN_TIMEOUT error row if the homepage never loaded

maxDomains

int

1000

Safety cap

{ "domains": ["apify.com", "stripe.com", "https://basecamp.com/"], "maxPagesPerDomain": 4 }

Output

{
  "schema_version": "1.0.0", "type": "domain", "input": "apify.com", "domain": "apify.com", "final_url": "https://apify.com/",
  "company_name": "Apify Technologies s.r.o.",
  "description": "Cloud platform for web scraping, browser automation, AI agents, and data for AI…",
  "emails": [
    { "email": "hello@apify.com", "source_url": "https://apify.com/contact", "is_role_account": true, "on_company_domain": true,
      "is_free_provider": false, "is_disposable": false, "verdict": "valid", "reasons": ["ROLE_ACCOUNT"], "has_mx": true, "mail_provider": "Google Workspace / Gmail" }
  ],
  "primary_email": "hello@apify.com",
  "phones": [{ "e164": "+18889262289", "raw": "+1 888 926 2289", "source_url": "https://stripe.com/contact/sales", "source": "tel" }],
  "social_links": { "linkedin": ["https://linkedin.com/company/apify"], "twitter": ["https://x.com/apify"], "github": ["https://github.com/apify"] },
  "address": "Na Příkopě 959/27, 11000, Prague, CZ",
  "mail_domain": { "has_mx": true, "null_mx": false, "mail_provider": "Google Workspace / Gmail", "has_spf": null, "has_dmarc": null, "dmarc_policy": null },
  "pages_crawled": ["https://apify.com/", "https://apify.com/contact"], "pages_blocked_by_robots": 0, "robots_txt": "ok", "email_scope": "role"
}

Error rows (type: "error", never charged): INPUT_EMPTY, INVALID_DOMAIN, MAX_DOMAINS_EXCEEDED, SITE_UNREACHABLE, BLOCKED_BY_ROBOTS, ENRICH_FAILED, DOMAIN_TIMEOUT, RUN_TIMEOUT_NEAR.

RUN_SUMMARY (key-value store): pages fetched, pages blocked by robots.txt, emails found vs. excluded by scope, DNS requests, charged events.

Pricing

Event

Price

When

domain-enriched

$0.003

Each domain that returns a result row

apify-actor-start

$0.00005

Once per run, per GB of memory (Apify default event)

Error rows (SITE_UNREACHABLE, BLOCKED_BY_ROBOTS, INVALID_DOMAIN, ENRICH_FAILED, …) are never charged. A domain is charged once, however many pages are crawled (up to maxPagesPerDomain).

Worked example: 1,000 domains from a Google Maps export, of which 120 are unreachable or blocked: 880 × $0.003 = $2.64, plus $0.00005.

To cap spend, set a maximum cost per run in the run options; the Actor stops cleanly at the limit. maxDomains caps how many domains one run processes. Prices are from the Store pricing tab (October 2026).

Memory / runtime

Default 256 MB, no browser, no proxy. With the default politeness settings a domain takes about 3–8 s (homepage + 4 pages, 1 s apart), and 5 domains run in parallel, so 100 domains finish in roughly 1–3 minutes. Sites that block datacenter IPs or need JavaScript to render contact details will return fewer fields. Memory is capped at 512 MB (more does not help: the Actor is network-bound). Each domain has a hard 40 s time limit (domainTimeoutSecs), so one slow or tarpitting site can't hang the run, and every finished domain is saved and charged immediately. The default run timeout is 15 minutes (about 500–1,000 domains); shortly before the timeout the Actor stops starting new domains and adds an uncharged RUN_TIMEOUT_NEAR row listing how many were skipped, so split very large lists or raise the timeout.

MCP / Apify tools

Call from agents via Apify MCP with Actor fairquery/website-contact-company-enricher:

{ "actor": "fairquery/website-contact-company-enricher", "input": { "domains": ["acme.com"] } }

Local stdio MCP server

The mcp/ folder is a stdio MCP server (TypeScript, @modelcontextprotocol/sdk) with three tools, all with annotations and output schemas:

Tool

What it does

Cost

enrich_domains

Runs this Actor and returns the rows. With waitForFinish: false it starts the run and returns the run ID at once (for large batches)

Needs APIFY_TOKEN, billed per domain (same price either way)

get_enrichment_results

Status and paged rows of a run started with waitForFinish: false, including partial rows while it runs

Free, needs APIFY_TOKEN (reads your own run)

normalize_domains

Cleans URLs/emails into unique domains, lists invalid ones, and shows billable_count and estimated_max_cost_usd (an upper bound) before you run

Free, local, no token

cd mcp && npm install && npm run build
node scripts/smoke-test.mjs                          # initialize + tools/list + free tool, no token
APIFY_TOKEN=... node scripts/smoke-test.mjs --call   # real call
{ "mcpServers": { "fairquery-contact-enricher": { "command": "node", "args": ["/path/to/website-contact-company-enricher/mcp/dist/index.js"], "env": { "APIFY_TOKEN": "<your Apify token>" } } } }

Docker: the root Dockerfile builds the MCP server image (docker build -t fairquery-contact-enricher-mcp .); it is also the Dockerfile for Glama. The Actor image is .actor/Dockerfile. glama.json lists fairquery as maintainer.

Works well after

Troubleshooting

Symptom

Cause and fix

Run is rejected because domains is required

The input had no domains. The three sample domains in the Console form are prefill only and are not sent with API calls.

BLOCKED_BY_ROBOTS or few pages crawled

The site's robots.txt disallows FairqueryBot for those paths, and the Actor honours it. These rows are free.

SITE_UNREACHABLE / no emails on a JavaScript-heavy site

The site blocks datacenter IPs or renders contact details with JavaScript. The Actor uses plain HTTP with no proxy or browser, so such sites return fewer fields.

Named people's emails missing

By default emailScope is role (info@, sales@ …). Use business to include named addresses on the company's own domain, but only if you have a lawful basis to process them.

Run slower than expected

Politeness settings: requestDelayMs (default 1,000 ms between requests to one site) and domainConcurrency (default 5). Raise concurrency rather than lowering the delay.

FAQ

Where does the data come from?

Only from pages the company publishes on its own domain: homepage, contact, about, imprint and team pages, plus JSON-LD structured data. No third-party databases.

Does it return personal emails or LinkedIn profiles of people?

By default it returns role and generic mailboxes only, and personal LinkedIn profiles (/in/…) are never returned.

Are emails verified?

They are checked for syntax, MX/null-MX, disposable domains and typos (the same code as the Fairquery Email Validator). There is no SMTP probing, so a mailbox is never confirmed.

How much does it cost?

$0.003 per enriched domain ($3 per 1,000), plus a $0.00005 start fee. Unreachable, blocked and invalid domains are free.

How do I limit spend?

Set a maximum cost per run in the run options and/or lower maxDomains.

Is this GDPR-compliant?

The Actor only reads what businesses publish, but business contact data can still be personal data. You need a lawful basis, must honour opt-outs and must follow anti-spam law. See the disclaimer.

Fairquery family

Other Fairquery tools on the Apify Store. Each one returns structured rows with machine-readable error codes, and error rows are never charged:

All Fairquery Actors: https://apify.com/fairquery

Kits & templates

Optional guides from Fairquery for wiring this Actor into agents and automations (the Actor works fine without them):

Disclaimer

Returns only information that companies publish on their own websites. You are responsible for how you use it: business contact data can still be personal data under GDPR/UK GDPR, so have a lawful basis, honour opt-outs, and follow anti-spam law (CAN-SPAM, ePrivacy/PECR) when you contact anyone. DNS checks cannot prove a mailbox exists.

Development

Local development, tests and deployment notes are in DEVELOPMENT.md. The Apify Store README is .actor/README.md.

Available Tools

3 tools
enrich_domainsEnrich domains with public business contactsA
Read-onlyIdempotent
Inspect

WHAT: For each company domain, crawl the homepage plus up to N contact/about/imprint pages (plain HTTP, robots.txt respected, polite per-site delay) and return one row with company_name, description, business/role emails (MX, disposable and typo validated), phones (E.164), social profiles (LinkedIn company, X, Facebook, Instagram, YouTube, GitHub...), postal address and mail-domain info. WHEN TO USE: enriching a lead/CRM list of company websites with public contact data; finding the official contact email or address of a business. WHEN NOT TO USE: to clean/dedupe a messy list first or preview what will be billed, call normalize_domains (free). Not for finding personal emails of individuals, LinkedIn people search, or JavaScript-only sites that need a browser. COST: paid. Runs Apify Actor fairquery/website-contact-company-enricher with your APIFY_TOKEN; pay-per-event "domain-enriched": $0.003 per domain that returns a result row. Unreachable, blocked or invalid domains are free. 100 domains ~ $0.30. ~2-10 s per domain. RETURNS: one type="domain" row per enriched domain (fields are null or empty when not published on the crawled pages) or free type="error" rows (code, message, retryable); with waitForFinish=false, items is empty and run holds run_id for get_enrichment_results. PARAMETERS: domains: entries are split on commas, semicolons and newlines, then each is reduced to its hostname (scheme, path, port and a leading "www." removed; for an email the part after @ is used; other subdomains are kept, so "shop.acme.com" and "acme.com" are separate) and deduplicated, so you pay once per unique enriched domain; unparseable entries give free INVALID_DOMAIN rows (max 1000 domains per run; extras get a free MAX_DOMAINS_EXCEEDED row). Run normalize_domains first to preview exactly what will be billed (billable_count, estimated_max_cost_usd). waitForFinish=false returns at once with run.run_id and the run keeps going in your Apify account (same price); use it for lists over ~100 domains, which may not finish within this call's 280 s wait, then poll get_enrichment_results. maxPagesPerDomain changes run time and coverage, never the price; 1 = homepage only, 3-5 usually finds the contact/imprint page. emailScope only filters which found addresses are returned: "role" keeps generic mailboxes (info@, sales@, support@...) on any domain, "business" adds named addresses on the company's own domain, "all" also keeps third-party/personal addresses - choose the narrowest scope your lawful basis allows. validateEmails=false skips only the DNS part: disposable/typo checks still run locally, has_mx and mail_domain become null and clean addresses get verdict "unknown" instead of "valid"; checkSpfDmarc adds SPF/DMARC for the company mail domain and has no effect when validateEmails=false. defaultPhoneCountry widens phone extraction to national-format numbers in page text (e.g. "030 1234567" with "DE"); without it only tel: links, JSON-LD and +international numbers are parsed, all returned as E.164. EXAMPLE: {"domains":["stripe.com","https://basecamp.com/about"],"maxPagesPerDomain":3} -> 2 domain rows.

NameRequiredDescriptionDefault
domainsYesRequired. 1+ non-empty strings: domains, website URLs or email addresses (the domain part is used); comma/semicolon/newline lists are split; deduplicated; billed once per enriched domain. Example: ["apify.com","https://stripe.com","info@basecamp.com"]
emailScopeNoOptional, one of "role" | "business" | "all", default "role" (GDPR-friendly). "role" = generic mailboxes only (info@, sales@, support@); "business" = also named addresses on the company's own domain; "all" = every address published on the pages. Example: "business"
checkSpfDmarcNoOptional boolean, default false. true adds SPF/DMARC presence and DMARC policy (none|quarantine|reject) for the company mail domain to mail_domain; requires validateEmails=true.
waitForFinishNoOptional boolean, default true. true waits up to 280 s and returns the rows; false starts the run and returns immediately with run.run_id (poll get_enrichment_results). Same price either way. Example: false
validateEmailsNoOptional boolean, default true. true runs a DNS-over-HTTPS MX lookup per email domain (no SMTP probing); false skips DNS (disposable/typo checks still run, clean emails get verdict "unknown").
maxPagesPerDomainNoOptional integer 1-15, default 5. Pages fetched per domain: homepage plus the best contact/about/imprint pages linked from it; affects time, not price. Example: 3
defaultPhoneCountryNoOptional, no default. ISO 3166-1 alpha-2 country code, exactly 2 letters, e.g. "US", "DE". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers.

Output Schema

NameRequiredDescription
runNoOnly with waitForFinish=false: run_id, status, dataset_id, console_url
countYesNumber of rows in items
itemsYesDataset rows: type=domain or type=error

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint/idempotent/openWorld, and the description goes well beyond them: robots.txt respected, per-site politeness delay, 2-10s per domain, a 280s wait cap with the waitForFinish=false escape hatch and get_enrichment_results polling path, plus an explicit price model ($0.003 per enriched domain, free for unreachable/blocked/invalid domains, ~1000-domain cap with free MAX_DOMAINS_EXCEEDED rows). That cost and long-running-operation disclosure is exactly the kind of context an agent cannot get from structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The WHAT/WHEN/COST/RETURNS/PARAMETERS/EXAMPLE structure is strongly front-loaded with the decision-relevant content first, and the example anchors the call shape. It is long, and the PARAMETERS block restates several descriptions already present in the schema verbatim, which is the only real redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 7-parameter, paid, long-running open-world crawl, this covers everything an agent needs: cost exposure, runtime expectations, async polling behavior, null-vs-error row semantics, and the free preview path via normalize_domains. The presence of an output schema is complemented rather than duplicated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline would be 3, but the description adds real semantics: hostname reduction rules (leading www removed, subdomains kept as separate billable domains, email local part discarded), dedup/billing consequences, the unparseable-entry INVALID_DOMAIN behavior, the undocumented 1000-domain run cap, and tuning heuristics ('1 = homepage only, 3-5 usually finds the contact/imprint page'). It also clarifies cross-parameter dependencies (checkSpfDmarc inert when validateEmails=false) and the lawful-basis guidance for emailScope.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The WHAT block gives a specific verb (crawl homepage + contact/about/imprint pages) and resource (company domains), then enumerates the exact output fields returned. An agent knows precisely what this tool produces and can distinguish it from get_enrichment_results and normalize_domains at a glance.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It has explicit WHEN TO USE (lead/CRM lists, finding official contact email or address) and WHEN NOT TO USE (call normalize_domains to clean/preview first; not for personal emails, LinkedIn people search, or JS-only sites). The named alternative and the condition that selects it are both stated, leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_enrichment_resultsGet results of a background enrichment run (free)A
Read-onlyIdempotent
Inspect

WHAT: Read the status and result rows of an enrich_domains run started with waitForFinish=false, one page at a time. WHEN TO USE: after enrich_domains returned run.run_id, to poll until finished is true and page through the rows; rows already written are returned while the run is still RUNNING. WHEN NOT TO USE: to start an enrichment (enrich_domains) or to preview cost (normalize_domains). Cannot read runs of other Actors. BEHAVIOR: GET requests to the Apify API only (actor, run, dataset); never starts, aborts or re-bills a run. Typically <1 s. Poll every 10-30 s; a run works on 5 domains at a time, ~2-10 s each. COST: free; needs the APIFY_TOKEN that started the run. Reading charges no Actor event (the run was billed per enriched domain when it ran). RETURNS: run (run_id, status, finished) plus one page of the same rows enrich_domains returns; Errors: APIFY_TOKEN_MISSING, RUN_NOT_FOUND, RUN_NOT_FOR_THIS_ACTOR, APIFY_TOKEN_INVALID, APIFY_RUN_LOOKUP_FAILED / APIFY_DATASET_READ_FAILED (retryable). PARAMETERS: runId is run.run_id from enrich_domains (also in the Apify console URL). status is READY | RUNNING | SUCCEEDED | FAILED | ABORTED | TIMED-OUT; finished=true for the last four, after which total no longer changes. A FAILED or TIMED-OUT run still returns the rows it wrote. offset/limit page the rows in write order; total counts all rows written so far, so keep calling with nextOffset until it is null and finished is true. EXAMPLE: {"runId":"a1B2c3D4e5F6g7H8i"} -> {"run":{"status":"SUCCEEDED","finished":true},"count":1,"total":1,"items":[{"type":"domain","domain":"apify.com",...}]}

NameRequiredDescriptionDefault
limitNoOptional page size, integer 1-1000, default 100. Example: 250
runIdYesRequired. Apify run ID (letters and digits) from enrich_domains run.run_id. Example: "a1B2c3D4e5F6g7H8i"
offsetNoOptional integer >= 0, default 0; use nextOffset from the previous page. Example: 100

Output Schema

NameRequiredDescription
runYesrun_id, status, finished, dataset_id, started_at, finished_at, console_url
countYesRows in this page
itemsYesDataset rows: type=domain or type=error
totalYesRows written by the run so far
nextOffsetYesOffset of the next page, or null

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint/idempotentHint/non-destructive, and the description adds substantial context beyond them: GET-only calls, never starts/aborts/re-bills, sub-second typical latency, poll cadence of 10-30 s, processing throughput (5 domains at a time, ~2-10 s each), token/auth requirements, cost model, and the full error-code list including which failures are retryable.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but fully front-loaded and labeled (WHAT/WHEN/BEHAVIOR/COST/RETURNS/PARAMETERS/EXAMPLE). Every section carries distinct information for a polling tool, and the one-line JSON example compactly conveys the response shape.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite an output schema existing, the description still covers status/finished semantics, that FAILED or TIMED-OUT runs return partial rows, and the error taxonomy. Combined with annotations, schema, and output schema, an agent has everything needed to call and interpret this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds real meaning: runId comes from enrich_domains run.run_id (or the console URL), offset/limit page rows in write order, and the caller should loop with nextOffset until it is null while finished is true. That paging contract is not derivable from the schema alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Starts with a precise verb+resource+scope: 'Read the status and result rows of an enrich_domains run started with waitForFinish=false, one page at a time.' It explicitly distinguishes itself from siblings enrich_domains (starting a run) and normalize_domains (cost preview), so an agent can route without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Has dedicated WHEN TO USE and WHEN NOT TO USE sections: use after enrich_domains returns run.run_id to poll until finished and page rows; do not use to start an enrichment or preview cost, and cannot read runs of other Actors. Both the trigger condition and the exclusions are stated explicitly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

normalize_domainsNormalize and dedupe domains (free)A
Read-onlyIdempotent
Inspect

WHAT: Turn URLs, email addresses and messy strings into bare registrable hostnames (lowercase, no scheme/path/www), remove duplicates and list entries that are not valid domains. WHEN TO USE: before enrich_domains, to clean an input list and see exactly which domains would be billed. WHEN NOT TO USE: to get contacts, company names or check that a site is reachable - use enrich_domains. Does no DNS lookup. COST: free; local, no network, no APIFY_TOKEN. RETURNS: domains is exactly the list enrich_domains would bill; billable_count and estimated_max_cost_usd preview that bill; invalid entries would only get free error rows there. PARAMETERS: inputs: each element may hold one value or a newline/comma/semicolon-separated list; pieces are trimmed and empty ones dropped. Each piece is lowercased and reduced to its hostname (scheme, path, query, port and a leading "www." removed; for a bare email the part after @ is used); other subdomains are kept, so "blog.acme.com" and "acme.com" stay separate. A piece is invalid unless the host has a dot and a 2+ letter TLD (punycode xn-- hosts accepted). domains keeps first-seen order and duplicates_removed counts repeats after normalization. This is exactly the normalization enrich_domains applies: billable_count = min(domains.length, 1000 per run) and estimated_max_cost_usd = billable_count x $0.003 + the $0.00005 start fee, the most passing the same inputs to enrich_domains can cost (unreachable domains lower it). An empty array is accepted and returns empty lists. EXAMPLE: {"inputs":["https://www.Stripe.com/pricing","info@stripe.com","not a domain"]} -> {"domains":["stripe.com"],"invalid":["not a domain"],"duplicates_removed":1,"billable_count":1,"estimated_max_cost_usd":0.00305}

NameRequiredDescriptionDefault
inputsYesRequired array (may be empty) of strings: URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: ["https://www.acme.com/contact","sales@acme.com","acme.com, beta.io"]

Output Schema

NameRequiredDescription
domainsYesUnique normalized domains, input order
invalidYesEntries that could not be parsed as a domain
billable_countYesDomains one enrich_domains run would process and could bill (max 1000)
duplicates_removedYesCount of duplicate entries dropped
estimated_max_cost_usdYesUpper bound in USD for enrich_domains on the same inputs

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive/closed-world, but the description adds substantial context beyond them: free/local, no network, no APIFY_TOKEN, no DNS lookup, and the billing-preview semantics (billable_count, estimated_max_cost_usd formula, invalid entries only producing free error rows). Rich disclosure that goes well past the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-organized with labeled WHAT/WHEN/COST/RETURNS/PARAMETERS/EXAMPLE sections and the core purpose front-loaded. Slightly verbose: the RETURNS and cost-formula details partly restate the output schema, and the PARAMETERS block is long, but almost every line carries actionable information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Complete for a complex normalization tool: inputs, normalization rules, validity criteria, ordering, billing preview, an end-to-end EXAMPLE with concrete outputs, and cost model are all covered. An agent has everything needed to call it correctly and reason about downstream cost.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is already 100%, yet the PARAMETERS section adds real meaning beyond it: newline/comma/semicolon list handling, trimming, lowercasing, hostname reduction, www/scheme/path/port stripping, email @-splitting, subdomain retention, the dot+2-letter-TLD validity rule, punycode acceptance, first-seen ordering, and empty-array acceptance. This far exceeds the schema's single-sentence item description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Turn URLs, email addresses and messy strings into bare registrable hostnames') and immediately distinguishes scope from the sibling enrich_domains. An agent can tell exactly what this does and how it differs from enrichment without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit WHEN TO USE ('before enrich_domains, to clean an input list') and WHEN NOT TO USE ('to get contacts, company names or check that a site is reachable - use enrich_domains'), naming the alternative directly. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv1.1.0
    • Changedenrich_domains2 fields changed
      • addedInput schema / properties / waitForFinish
        Added value: +{
        +  "description": "Optional boolean, default true. true waits up to 280 s and returns the rows; false starts the run and returns immediately with run.run_id (poll get_enrichment_results). Same price either way. Example: false",
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / run
        Added value: +{
        +  "additionalProperties": {},
        +  "description": "Only with waitForFinish=false: run_id, status, dataset_id, console_url",
        +  "type": "object"
        +}
    • Addedget_enrichment_results
    • Changednormalize_domains3 fields changed
      • addedOutput schema / properties / billable_count
        Added value: +{
        +  "description": "Domains one enrich_domains run would process and could bill (max 1000)",
        +  "type": "number"
        +}
      • addedOutput schema / properties / estimated_max_cost_usd
        Added value: +{
        +  "description": "Upper bound in USD for enrich_domains on the same inputs",
        +  "type": "number"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "domains",
        -  "invalid",
        -  "duplicates_removed"
        -]New value: +[
        +  "domains",
        +  "invalid",
        +  "duplicates_removed",
        +  "billable_count",
        +  "estimated_max_cost_usd"
        +]
  2. 2 tool updates
    • Changedenrich_domains6 fields changed
      • changedInput schema / properties / checkSpfDmarc / description
        Previous value: -"Default false. Add SPF/DMARC presence and DMARC policy for the company mail domain."New value: +"Optional boolean, default false. true adds SPF/DMARC presence and DMARC policy (none|quarantine|reject) for the company mail domain to mail_domain; requires validateEmails=true."
      • changedInput schema / properties / defaultPhoneCountry / description
        Previous value: -"ISO 3166-1 alpha-2 code, e.g. \"US\", \"DE\". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers. No default."New value: +"Optional, no default. ISO 3166-1 alpha-2 country code, exactly 2 letters, e.g. \"US\", \"DE\". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers."
      • changedInput schema / properties / domains / description
        Previous value: -"Required. Domains, website URLs or email addresses (the domain part is used); comma/semicolon lists are split. Example: [\"apify.com\",\"https://stripe.com\",\"info@basecamp.com\"]."New value: +"Required. 1+ non-empty strings: domains, website URLs or email addresses (the domain part is used); comma/semicolon/newline lists are split; deduplicated; billed once per enriched domain. Example: [\"apify.com\",\"https://stripe.com\",\"info@basecamp.com\"]"
      • changedInput schema / properties / emailScope / description
        Previous value: -"\"role\" (default, GDPR-friendly) = generic mailboxes only (info@, sales@, support@); \"business\" = also named addresses on the company's own domain; \"all\" = every address published on the pages."New value: +"Optional, one of \"role\" | \"business\" | \"all\", default \"role\" (GDPR-friendly). \"role\" = generic mailboxes only (info@, sales@, support@); \"business\" = also named addresses on the company's own domain; \"all\" = every address published on the pages. Example: \"business\""
      • changedInput schema / properties / maxPagesPerDomain / description
        Previous value: -"Pages fetched per domain, 1-15 (default 5): homepage plus the best contact/about/imprint pages linked from it."New value: +"Optional integer 1-15, default 5. Pages fetched per domain: homepage plus the best contact/about/imprint pages linked from it; affects time, not price. Example: 3"
      • changedInput schema / properties / validateEmails / description
        Previous value: -"Default true. DNS-over-HTTPS MX lookup plus disposable and typo checks on found emails (no SMTP probing)."New value: +"Optional boolean, default true. true runs a DNS-over-HTTPS MX lookup per email domain (no SMTP probing); false skips DNS (disposable/typo checks still run, clean emails get verdict \"unknown\")."
    • Changednormalize_domains1 field changed
      • changedInput schema / properties / inputs / description
        Previous value: -"Required. URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: [\"https://www.acme.com/contact\",\"sales@acme.com\"]."New value: +"Required array (may be empty) of strings: URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: [\"https://www.acme.com/contact\",\"sales@acme.com\",\"acme.com, beta.io\"]"
  3. 2 tool updatesv0.1.0
    • First observedenrich_domains
    • First observednormalize_domains

TDQS

A4.9/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct role: normalize_domains cleans/previews input for free, enrich_domains performs the paid enrichment, and get_enrichment_results polls async runs. The descriptions explicitly state WHEN NOT TO USE, cross-referencing the other tools, so there is no realistic chance of misselection.

Naming Consistency5/5

All three names follow a consistent snake_case verb_noun pattern: normalize_domains, enrich_domains, get_enrichment_results. The convention is predictable and readable throughout.

Tool Count5/5

Three tools map exactly onto the natural workflow stages (preview, enrich, retrieve async results), with each tool earning its place and no redundancy. This is well-scoped for a narrow contact-enrichment purpose.

Completeness4/5

The surface covers the full lifecycle: free normalization/preview, paid enrichment, and async result retrieval including paging. Minor gaps exist (no run cancellation/abort or free-domain validation separate from enrichment), but agents can work around them.

Maintenance

ActivityNo data
ResponsivenessSyncing

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    Provides domain and brand intelligence for AI agents, including company enrichment, tech stack detection, and brand research from free public sources with an on-demand cache.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables enriching company domains into structured firmographics including employee band, industry, HQ, founded year, revenue estimate, logo, and description, with source provenance and data completeness scoring.
    1
    62 npm
    1
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Domain and company intelligence for AI agents. Enables vetting companies, qualifying leads, and mapping targets from free public data without API keys.
    6
    2 npm
    1
    MIT