# Fairquery > Fairquery builds data tools for developers and AI agents: sanctions screening (OFAC, EU, UK, UN), SEC EDGAR data, PII/secret redaction, email validation, website tech/SEO/AI-readiness checks, company contact enrichment and OSV dependency vulnerability checks. Most tools are Apify Actors with a local stdio MCP server; the Agent Trust MCP server bundles several of them. ## Posts - [How do I get SEC EDGAR company facts (XBRL financials) and filings by ticker via API?](https://fairquery.github.io/posts/sec-edgar-api-company-facts-xbrl-filings-by-ticker/): Get XBRL company facts (revenue, net income, EPS, assets) and 10-K/10-Q/8-K filings from SEC EDGAR by ticker, CIK or name: free Python against the official SEC APIs, two data traps, and an Apify Actor at $0.0015/company + $0.001/filing + $0.0002/fact. Real outputs. - [How do I screen names against OFAC, EU, UK and UN sanctions lists with an API?](https://fairquery.github.io/posts/how-to-screen-names-ofac-eu-uk-un-sanctions-api/): Screen people and companies against OFAC SDN/Consolidated, EU, UK and UN sanctions lists via API: fuzzy matching, aliases, country and DOB checks, real output, $3 per 1,000 names on Apify or RapidAPI plans. - [How do I redact PII and secrets before sending data to an LLM?](https://fairquery.github.io/posts/redact-pii-secrets-before-sending-data-to-llm/): Detect and redact emails, phones, card numbers, SSNs, IBANs and API keys before text reaches an LLM: free local MCP tools (pii_scan_text, pii_redact_text) or an API at $0.0015/doc + $0.0002/finding. Real outputs. - [How do I find a company's contact email, phone and social profiles from its domain?](https://fairquery.github.io/posts/find-company-contact-email-phone-socials-from-domain/): Turn a domain into public business contacts: role emails validated for MX, E.164 phones, social profiles, company name and address, robots.txt respected. Real output, $0.003 per domain. - [How do I check npm and PyPI dependencies for vulnerabilities in CI with OSV?](https://fairquery.github.io/posts/check-npm-pypi-dependencies-vulnerabilities-ci-osv/): Gate CI on vulnerable npm/PyPI dependencies with OSV.dev: a free querybatch gate, free MCP lookups, and a GitHub Actions example with CVSS, CVE IDs and fixed versions at $0.0005/package + $0.0015/vulnerability. Real outputs. - [How do I give my AI agent trust and safety tools via MCP?](https://fairquery.github.io/posts/give-ai-agent-trust-safety-tools-mcp-server/): Add PII/secret redaction, email checks, sanctions screening and OSV vulnerability lookups to Claude Desktop, Cursor or VS Code with one MCP server: 12 tools, 7 free. Config snippets and real outputs. - [How to validate email addresses via API: MX, disposable and typo checks](https://fairquery.github.io/posts/validate-email-addresses-api-mx-disposable-typo/): Validate email addresses without sending mail: syntax, MX and null MX, disposable, role and free-provider flags, typo suggestions, SPF/DMARC. Free options and an API at $0.00079 per email. ## Tools - [Sanctions & Watchlist Screener](https://apify.com/fairquery/sanctions-watchlist-screener): Fuzzy-screen people and company names against OFAC SDN/Consolidated, EU, UK and UN sanctions lists from the official sources (at most 6 h old, publication dates shown). Apify Actor + stdio MCP server. Source: https://github.com/fairquery/sanctions-watchlist-screener - [SEC EDGAR Company Facts & Filings](https://apify.com/fairquery/edgar-company-facts-filings): SEC EDGAR filings and XBRL financial facts by ticker, CIK or company name. Apify Actor + MCP. Source: https://github.com/fairquery/edgar-company-facts-filings - [LLM Data Guard (PII & secrets)](https://apify.com/fairquery/llm-data-guard-pii-secrets): Detect and redact PII and secrets (API keys, tokens, private keys) before text reaches an LLM. Apify Actor + stdio MCP server. Source: https://github.com/fairquery/llm-data-guard-pii-secrets - [Website Contact & Company Enricher](https://apify.com/fairquery/website-contact-company-enricher): Domain to public business contacts: role emails, phones, socials, company name, address. Respects robots.txt. Apify Actor + MCP. Source: https://github.com/fairquery/website-contact-company-enricher - [OSV Dependency Vulnerability Check](https://apify.com/fairquery/osv-dependency-vulnerability-check): Check npm/PyPI/Go/Maven dependencies and lockfiles against OSV.dev: CVEs, CVSS, fixed versions. Apify Actor + MCP. Source: https://github.com/fairquery/osv-dependency-vulnerability-check - [Website Tech, SEO & AI-Readiness](https://apify.com/fairquery/website-tech-seo-ai-readiness): Website tech stack, SEO score, robots.txt AI-bot rules, sitemap and llms.txt check. Apify Actor + MCP. Source: https://github.com/fairquery/website-tech-seo-ai-readiness - [Email Validator (MX, disposable, typo check)](https://apify.com/fairquery/email-validator-mx-disposable): Validate emails without sending mail: syntax, MX, disposable/role/free-provider flags, typo fixes, SPF/DMARC. Apify Actor + MCP. Source: https://github.com/fairquery/email-validator-mx-disposable - [Agent Trust & Data Safety MCP server](https://github.com/fairquery/fairquery-agent-trust-mcp): One MCP server with 12 tools for AI agents: PII/secret redaction, email checks, sanctions screening and OSV vulnerability lookups. 7 free local tools; paid tools use your own Apify token. Source: https://github.com/fairquery/fairquery-agent-trust-mcp - [Fairquery OSV for VS Code](https://github.com/fairquery/fairquery-vscode-osv): VS Code extension that flags vulnerable dependencies using the free OSV.dev API. Source: https://github.com/fairquery/fairquery-vscode-osv - [n8n templates](https://github.com/fairquery/n8n-templates): Free n8n workflows for Fairquery Apify Actors: OSV CI gate, signup email gate, SEO and AI-crawler monitor. Source: https://github.com/fairquery/n8n-templates ## Where to get them - [Apify Store](https://apify.com/fairquery) - [GitHub](https://github.com/fairquery) - [RapidAPI: Sanctions Screening (OFAC, EU, UK, UN)](https://rapidapi.com/fairquery/api/sanctions-screening-ofac-eu-uk-un) - [RapidAPI: Email Validator (MX, disposable and typo check)](https://rapidapi.com/Fairquery/api/email-validator-mx-disposable-and-typo-check) - [RapidAPI: Website Intelligence (tech stack, SEO and contacts)](https://rapidapi.com/Fairquery/api/website-intelligence-tech-stack-seo-and-contacts) - [npm: @fairquery/agent-trust-mcp](https://www.npmjs.com/package/@fairquery/agent-trust-mcp) - [Glama: Fairquery Agent Trust MCP](https://glama.ai/mcp/servers/fairquery/fairquery-agent-trust-mcp) - [Gumroad](https://fairquery.gumroad.com) ## About and security - [About Fairquery](https://fairquery.github.io/about/): what Fairquery is, its principles (precision, transparent pay-per-result pricing, minimal data by design, AI-agent friendly) and contact. - [Security & Privacy](https://fairquery.github.io/security/): data handling per tool and how to report a vulnerability. - [Terms of Service](https://fairquery.github.io/terms/): plain-language terms of use; platform terms (Apify, RapidAPI, Gumroad) govern billing and refunds. - [Privacy Notice](https://fairquery.github.io/privacy/): what Fairquery collects, how it is used and shared, cookies (none on the blog), retention and how to make requests. - [security.txt](https://fairquery.github.io/.well-known/security.txt) - Contact: fairquery@proton.me ## Optional - [Full text for LLMs](https://fairquery.github.io/llms-full.txt) - [Tools page](https://fairquery.github.io/tools/) - [Atom feed](https://fairquery.github.io/feed.xml) --- # How do I get SEC EDGAR company facts (XBRL financials) and filings by ticker via API? URL: https://fairquery.github.io/posts/sec-edgar-api-company-facts-xbrl-filings-by-ticker/ Published: 2026-10-17 Short answer: the SEC publishes free JSON APIs with no key: map a ticker to its 10-digit CIK with company_tickers.json, then read data.sec.gov/api/xbrl/companyfacts/CIK##########.json for XBRL financials (revenue, net income, EPS, assets) and data.sec.gov/submissions/CIK##########.json for the filing list (10-K, 10-Q, 8-K). Send a User-Agent with your contact details and stay under 10 requests per second. Watch two traps: values are repeated across filings, and concepts change over time (Apple's Revenues stops in 2018). If you want clean rows instead, the Fairquery SEC EDGAR Actor resolves tickers, CIKs or names and returns filings and the latest facts as JSON for $0.0015 per company, $0.001 per filing row and $0.0002 per fact row. ## The free SEC EDGAR APIs you need What | Endpoint | Notes | Ticker → CIK | https://www.sec.gov/files/company_tickers.json | One file with every listed ticker, its CIK and company name. Pad the CIK to 10 digits for the other endpoints. | Filings and company metadata | https://data.sec.gov/submissions/CIK##########.json | Name, SIC code, exchanges, fiscal year end and the recent filings (form, dates, accession number, primary document). | All XBRL facts of one company | https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json | Every reported value of every concept (us-gaap, dei), by unit, with period start/end, form, fiscal period and filing date. | One concept of one company | https://data.sec.gov/api/xbrl/companyconcept/CIK##########/us-gaap/.json | Smaller download when you need a single line item. | One concept across companies | https://data.sec.gov/api/xbrl/frames/us-gaap//USD/CY2025Q2.json | One value per company for a calendar period, useful for screens and comparisons. | No API key or account is needed. The SEC's fair-access rules ask every client to declare a User-Agent that identifies you (for example a name and email) and to stay at or below 10 requests per second. Requests without a proper User-Agent are often refused. ## Option 1 (free): SEC company facts in Python Standard library only. It looks up Apple's CIK and prints the three most recent revenue values: ``` import json, urllib.request UA = {"User-Agent": "Your Name you@example.com"} # SEC asks every client to identify itself def get(url): with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=30) as r: return json.load(r) # 1. Ticker -> 10-digit CIK tickers = get("https://www.sec.gov/files/company_tickers.json") cik = next(f"{t['cik_str']:010d}" for t in tickers.values() if t["ticker"] == "AAPL") # 2. All XBRL facts for the company, then the latest values of one concept facts = get(f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json") rows = facts["facts"]["us-gaap"]["RevenueFromContractWithCustomerExcludingAssessedTax"]["units"]["USD"] rows = sorted(rows, key=lambda f: (f["end"], f["filed"]), reverse=True) for f in rows[:3]: print(f["start"], f["end"], f["val"], f["form"], f["fp"], f["filed"], f.get("frame", "")) ``` Real output (October 17, 2026, about 0.4 s for both requests). Columns: period start, period end, value in USD, form, fiscal period, filing date, frame: ``` 2025-09-28 2026-06-27 364357000000 10-Q Q3 2026-07-31 2026-03-29 2026-06-27 109417000000 10-Q Q3 2026-07-31 CY2026Q2 2025-09-28 2026-03-28 254940000000 10-Q Q2 2026-05-01 ``` The first row is the nine-month year-to-date figure, the second is the single quarter (April to June 2026), and the third is the previous quarter's year-to-date value. To build a clean quarterly or annual series you have to tell those apart, and handle the two traps below. ## Trap 1: the same value appears many times Each 10-Q and 10-K repeats earlier periods as comparatives, and companyfacts lists the value once per filing that reported it. If you sort only by filing date and take the top rows, you get last year's comparatives from the newest 10-Q instead of the current quarter. (We found exactly this in our own Actor and fixed it; see below.) Two ways out: - Sort by period end first, then by filed, and keep one value per (start, end) pair, from the latest filing so restatements win. - Or use the frame field. The SEC sets it (for example CY2026Q2) on one value per company and calendar period, so filtering to facts that have a frame gives one value per period. Year-to-date values that don't match a calendar quarter or year have no frame. ## Trap 2: the concept you ask for may have been retired Companies choose XBRL concepts, and the choices change. Apple reported revenue as Revenues until fiscal 2018; its filings since January 2019 (when the ASC 606 revenue standard took effect for it) use RevenueFromContractWithCustomerExcludingAssessedTax. Ask for Revenues and you silently get 2018 data. In practice, request both concepts (and check the period dates), or look at which concepts a company actually uses in its latest filing. The same applies to other line items, such as net income attributable to the parent versus including non-controlling interests. ## Option 2 (API or MCP): filings and facts as clean rows The Fairquery SEC EDGAR Company Facts & Filings Actor (fairquery/edgar-company-facts-filings) calls the same official endpoints, throttled to at most 5 requests per second with a declared User-Agent. It takes tickers (AAPL), CIKs (320193) or name fragments (nvidia), and emits one row per company, filing, fact, optional text excerpt or error. For facts it sorts by period end, then filing date, and returns a re-reported value only once, so maxFactsPerMetric: 2 gives you the two most recent periods. We ran it on October 17, 2026 with this input, asking for both revenue concepts on purpose: ``` { "companies": [ "AAPL" ], "mode": "both", "formTypes": [ "10-K" ], "maxFilingsPerCompany": 2, "metrics": [ "Revenues", "RevenueFromContractWithCustomerExcludingAssessedTax", "NetIncomeLoss", "EarningsPerShareDiluted" ], "maxFactsPerMetric": 2 } ``` Real output (run time about 2 seconds; 11 rows: 1 company, 2 filings, 8 facts). Abridged to the company row, the latest 10-K and the revenue facts: ``` [ { "schema_version": "1.0.0", "type": "company", "source_url": "https://data.sec.gov/submissions/CIK0000320193.json", "scraped_at": "2026-10-17T20:09:52.368Z", "query": "AAPL", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "entity_type": "operating", "sic": "3571", "sic_description": "Electronic Computers", "exchanges": [ "Nasdaq" ], "ein": "942404110", "fiscal_year_end": "0926" }, { "schema_version": "1.0.0", "type": "filing", "source_url": "https://data.sec.gov/submissions/CIK0000320193.json", "scraped_at": "2026-10-17T20:09:52.368Z", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "form": "10-K", "filing_date": "2025-10-31", "report_date": "2025-09-27", "accession_number": "0000320193-25-000079", "primary_document": "aapl-20250927.htm", "primary_doc_description": "10-K", "document_url": "https://www.sec.gov/Archives/edgar/data/320193/000032019325000079/aapl-20250927.htm", "filing_url": "https://www.sec.gov/Archives/edgar/data/320193/000032019325000079/", "is_xbrl": true, "is_inline_xbrl": true, "size": 9392337 }, { "schema_version": "1.0.0", "type": "fact", "source_url": "https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json", "scraped_at": "2026-10-17T20:09:52.659Z", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "taxonomy": "us-gaap", "concept": "RevenueFromContractWithCustomerExcludingAssessedTax", "label": "Revenue from Contract with Customer, Excluding Assessed Tax", "unit": "USD", "value": 109417000000, "end": "2026-06-27", "start": "2026-03-29", "filed": "2026-07-31", "form": "10-Q", "frame": "CY2026Q2", "fy": 2026, "fp": "Q3", "accession_number": "0000320193-26-000020" }, { "schema_version": "1.0.0", "type": "fact", "source_url": "https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json", "scraped_at": "2026-10-17T20:09:52.659Z", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "taxonomy": "us-gaap", "concept": "RevenueFromContractWithCustomerExcludingAssessedTax", "label": "Revenue from Contract with Customer, Excluding Assessed Tax", "unit": "USD", "value": 364357000000, "end": "2026-06-27", "start": "2025-09-28", "filed": "2026-07-31", "form": "10-Q", "fy": 2026, "fp": "Q3", "accession_number": "0000320193-26-000020" }, { "schema_version": "1.0.0", "type": "fact", "source_url": "https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json", "scraped_at": "2026-10-17T20:09:52.659Z", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "taxonomy": "us-gaap", "concept": "Revenues", "label": "Revenues", "unit": "USD", "value": 62900000000, "end": "2018-09-29", "start": "2018-07-01", "filed": "2018-11-05", "form": "10-K", "frame": "CY2018Q3", "fy": 2018, "fp": "FY", "accession_number": "0000320193-18-000145" } ] ``` Note the last row: Revenues for Apple ends in September 2018, while RevenueFromContractWithCustomerExcludingAssessedTax gives the quarter to June 27, 2026 ($109.4 billion) and the nine months to that date ($364.4 billion). The same run returned NetIncomeLoss (29,789,000,000 for the quarter) and diluted EPS (2.02). Every row carries source_url, the accession number and the filing date, so an agent can cite where a number came from. Run summary: 3 SEC requests, 0 errors. That run costs 1 × $0.0015 + 2 × $0.001 + 8 × $0.0002 = $0.0051, plus Apify's $0.00005 Actor-start fee. ### Name lookup and error rows Input {"companies": ["nvidia", "ZZNOTREAL"], "mode": "filings", "formTypes": ["8-K"], "maxFilingsPerCompany": 3} resolved the name to NVIDIA CORP (CIK 0001045810) and returned a machine-readable error for the unknown query. Real output, abridged to the company row, the newest 8-K and the error: ``` [ { "schema_version": "1.0.0", "type": "company", "source_url": "https://data.sec.gov/submissions/CIK0001045810.json", "scraped_at": "2026-10-17T20:10:05.758Z", "query": "nvidia", "cik": "0001045810", "ticker": "NVDA", "name": "NVIDIA CORP", "entity_type": "operating", "sic": "3674", "sic_description": "Semiconductors & Related Devices", "exchanges": [ "Nasdaq" ], "ein": "943177549", "fiscal_year_end": "0131" }, { "schema_version": "1.0.0", "type": "filing", "source_url": "https://data.sec.gov/submissions/CIK0001045810.json", "scraped_at": "2026-10-17T20:10:05.758Z", "cik": "0001045810", "ticker": "NVDA", "name": "NVIDIA CORP", "form": "8-K", "filing_date": "2026-09-03", "report_date": "2026-09-02", "accession_number": "0001045810-26-000078", "primary_document": "nvda-20260902.htm", "primary_doc_description": "8-K", "document_url": "https://www.sec.gov/Archives/edgar/data/1045810/000104581026000078/nvda-20260902.htm", "filing_url": "https://www.sec.gov/Archives/edgar/data/1045810/000104581026000078/", "is_xbrl": true, "is_inline_xbrl": true, "size": 146499 }, { "schema_version": "1.0.0", "type": "error", "source_url": "https://data.sec.gov/", "scraped_at": "2026-10-17T20:10:05.929Z", "code": "COMPANY_NOT_FOUND", "message": "Could not resolve company query: ZZNOTREAL", "retryable": false, "query": "ZZNOTREAL" } ] ``` Error rows are never charged. Other codes are INPUT_EMPTY, RESOLVE_FAILED, SUBMISSIONS_FETCH_FAILED, FACTS_FETCH_FAILED and EXCERPT_FETCH_FAILED. ### From Python, curl or an AI agent With the official apify-client package (v3; set APIFY_TOKEN): ``` import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("fairquery/edgar-company-facts-filings").call(run_input={ "companies": ["AAPL"], "mode": "both", "formTypes": ["10-K"], "maxFilingsPerCompany": 2, "metrics": ["Revenues", "RevenueFromContractWithCustomerExcludingAssessedTax", "NetIncomeLoss", "EarningsPerShareDiluted"], "maxFactsPerMetric": 2, }) for row in client.dataset(run.default_dataset_id).iterate_items(): if row["type"] == "fact": print(row["concept"], row["start"], row["end"], row["value"], row["form"], row.get("frame", "")) ``` Or one synchronous HTTP call that returns the rows directly: ``` curl -s -X POST "https://api.apify.com/v2/acts/fairquery~edgar-company-facts-filings/run-sync-get-dataset-items" \ -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \ -d '{"companies": ["nvidia"], "mode": "filings", "formTypes": ["8-K"], "maxFilingsPerCompany": 1}' ``` In our tests a one-company run took about 2 to 14 seconds end to end, mostly container start-up; the SEC requests themselves took under half a second. For agents, add the Apify MCP server scoped to this Actor (https://mcp.apify.com/?tools=fetch-actor-details,fairquery/edgar-company-facts-filings) to Claude Desktop, Cursor or VS Code, then ask for example: "Get Apple's last two 10-K filings and its latest revenue, net income and diluted EPS from SEC EDGAR." A local stdio MCP server is also in the GitHub repo. ## Input options worth knowing - mode: filings, facts or both. A company row is always emitted when the query resolves. - formTypes (default 10-K, 10-Q, 8-K; [] for all) and since (YYYY-MM-DD) filter filings; maxFilingsPerCompany caps them (default 5). - metrics: exact XBRL concept names, case-insensitive. An empty list uses a curated set of 10 us-gaap concepts that includes both revenue concepts. - includeTextExcerpt: downloads each kept filing's primary document and returns a plain-text excerpt of up to excerptMaxChars characters ($0.002 per excerpt). ## Pricing Option | Price | SEC EDGAR APIs directly (your own code) | Free | Fairquery SEC EDGAR Actor (Apify, pay per event) | $0.0015 per company resolved, $0.001 per filing row, $0.0002 per fact row, $0.002 per text excerpt, plus Apify's $0.00005 Actor-start event. Error rows are free. | Example: 100 companies with 5 filings and 16 fact rows each cost 100 × $0.0015 + 500 × $0.001 + 1,600 × $0.0002 = $0.97. ## How it compares with alternatives Option | Good for | Trade-offs | SEC APIs directly | Free, official, complete for what they cover | You handle CIK lookup, throttling, User-Agent, duplicate values, concept changes and joining filings to facts | Open-source Python libraries (for example edgartools) | Rich parsing of filings and financial statements in Python notebooks and scripts | A Python dependency to run and maintain; not a hosted API or MCP tool | Commercial SEC data APIs | Broader feature sets such as full-text search and parsers for many form types | Account and subscription plans | Fairquery SEC EDGAR Actor | One API or MCP call from agents, n8n or scripts; ticker/CIK/name lookup, filings with document URLs and de-duplicated latest facts as JSON rows, pay per row | Recent filings and companyfacts only: no realtime quotes, no full-text search, no Form 4 parsing, no frames (cross-company) queries, US SEC registrants only | ## FAQ ### Is there a free SEC EDGAR API? Yes. The SEC serves free JSON at data.sec.gov with no API key: submissions (company metadata and filings), companyfacts, companyconcept and frames (XBRL financial data). Declare a User-Agent with your contact details and stay at or below 10 requests per second. ### How do I get a company's CIK from its ticker? Download https://www.sec.gov/files/company_tickers.json, find the ticker, and left-pad cik_str to 10 digits (Apple is 0000320193). The Fairquery EDGAR Actor also accepts tickers, CIKs or company name fragments directly. ### Why is revenue missing or years old for some companies? XBRL concepts change. Apple's Revenues values end in 2018; since 2019 it reports RevenueFromContractWithCustomerExcludingAssessedTax. Request both concepts and check the period end dates. ### Why does companyfacts list the same value several times? Each 10-Q and 10-K repeats earlier periods as comparatives, and companyfacts lists the value once per filing. Sort by period end, keep one value per period from the latest filing, or filter on the frame field. The Fairquery Actor returns each period once. ### How much does the Fairquery SEC EDGAR Actor cost? $0.0015 per company resolved, $0.001 per filing row, $0.0002 per fact row and $0.002 per optional text excerpt, plus Apify's $0.00005 Actor-start event. Error rows are free. Our example run (1 company, 2 filings, 8 facts) costs $0.0051. ### Can AI agents use it? Yes. Add the Apify MCP server with tools=fairquery/edgar-company-facts-filings to Claude Desktop, Cursor or VS Code, call the Actor via the Apify API, or run the stdio MCP server from the GitHub repo. ### Is this investment advice? No. It returns public SEC EDGAR data as reported, for research and tooling, with no warranty of completeness. Check important figures against the linked filing. Not investment advice. Data comes from the U.S. Securities and Exchange Commission's EDGAR system and is returned as reported, with no warranty of completeness. Check important figures against the filing itself (every row links to it). Source and docs: github.com/fairquery/edgar-company-facts-filings (MIT) · Apify Store · all Fairquery tools --- # How do I screen names against OFAC, EU, UK and UN sanctions lists with an API? URL: https://fairquery.github.io/posts/how-to-screen-names-ofac-eu-uk-un-sanctions-api/ Published: 2026-10-17 Short answer: send each name to a screening API that downloads the official OFAC, EU, UK and UN lists, fuzzy-matches the name against every primary name and alias, and returns a score plus the listing details so a person can review it. The Fairquery Sanctions & Watchlist Screener does this in one call for $3 per 1,000 names ($0.003 per name, plus $0.007 per run) on Apify, using all five lists from the official sources, never more than 6 hours old, with each list's publication date in every result row. For per-request calls with a fixed monthly plan, the same screening is on RapidAPI (free tier: 20 requests/month). Important: screening results are leads for human review, not legal determinations. Automated fuzzy matching can return false positives and can miss true matches (transliterations, very short names, a list that fails to download). Confirm every potential match against the official publication and take legal advice before acting. Fairquery is not a certified or regulated screening provider and is not affiliated with OFAC, the European Commission, the UK FCDO/OFSI or the UN. ## Which lists are covered The Actor downloads these files from the issuing authority and keeps the parsed lists in a cache in your own Apify account for at most 6 hours (configurable from 0, meaning always download, to 12 hours). Older copies are never used: the list is downloaded again, and if that download fails the list is reported as unavailable instead of being served from a stale copy. Every result row reports each list's publication date and when it was fetched, so you can show exactly which version you screened against (sources can lag or be unavailable). lists id | List | Authority / source | OFAC_SDN | Specially Designated Nationals (SDN) List | US Treasury OFAC, sanctionslistservice.ofac.treas.gov SDN.CSV + ALT.CSV + ADD.CSV | OFAC_CONS | Consolidated (non-SDN) Sanctions List | US Treasury OFAC, CONS_PRIM / CONS_ALT / CONS_ADD.CSV | EU | EU Consolidated Financial Sanctions List | European Commission (DG FISMA), webgate.ec.europa.eu/fsd/fsf CSV | UK | UK Sanctions List (incl. OFSI asset-freeze targets) | UK FCDO / HM Treasury OFSI, sanctionslist.fcdo.gov.uk CSV | UN | UN Security Council Consolidated List | UN Security Council, scsanctions.un.org XML | In our test run on October 17, 2026 the loaded entry counts were OFAC SDN 19,457, OFAC Consolidated 481, EU 6,234, UK 6,408 and UN 1,010. These change whenever the authorities publish updates. ## How the matching works - Normalisation: diacritics, case, punctuation, &, legal suffixes (LLC, GmbH, JSC) and honorifics are removed before comparison. - Token-based fuzzy score (0–100): names are compared token by token with Jaro-Winkler, so word order (PUTIN, Vladimir), extra middle names and spelling variants (Usama/Osama, Gaddafi/Qadhafi) still match. The default threshold is minScore: 85; use 90+ for less noise or 80 for more recall. - Aliases: every a.k.a./f.k.a. and non-Latin-script name is screened; matched_on says whether the hit was the primary name or an alias. - Country and date of birth: an optional country adds country_match (and excludeNonMatchingCountry drops mismatches); an optional dateOfBirth drops listed individuals whose birth year differs by more than dobYearTolerance (default 1). ## How to screen names with curl This runs the Actor synchronously and returns the result rows in the same HTTP response. Replace $APIFY_TOKEN with your Apify API token: ``` curl -X POST \ "https://api.apify.com/v2/acts/fairquery~sanctions-watchlist-screener/run-sync-get-dataset-items" \ -H "Authorization: Bearer $APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "names": ["Rosneft", "Jane Example Smith"], "queries": [{"name": "Banco Nacional de Cuba", "id": "cust-42", "country": "CU"}], "maxMatchesPerName": 3 }' ``` ### Real output from that request We ran exactly this input on October 17, 2026. One row per name; abridged to the first two matches per name and a subset of match fields (aliases, dates of birth and list names omitted): ``` [ { "type": "screening", "query": { "name": "Rosneft", "id": null, "country": null, "date_of_birth": null, "entity_type": "any" }, "status": "potential_match", "best_score": 100, "match_count": 12, "matches": [ { "score": 100, "matched_name": "ROSNEFT", "matched_on": "alias", "list": "OFAC_CONS", "entry_id": "17022", "entity_type": "entity", "primary_name": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY", "programs": [ "UKRAINE-EO13662", "RUSSIA-EO14024" ], "countries": [ "Russia" ], "country_match": null, "source_url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=17022" }, { "score": 100, "matched_name": "ROSNEFT", "matched_on": "alias", "list": "UK", "entry_id": "RUS3064", "entity_type": "entity", "primary_name": "PJSC Rosneft Oil Company", "programs": [ "The Russia (Sanctions) (EU Exit) Regulations 2019" ], "countries": [ "Russia" ], "country_match": null, "source_url": "https://www.gov.uk/government/publications/the-uk-sanctions-list" } ], "lists_screened": [ "OFAC_SDN", "OFAC_CONS", "EU", "UK", "UN" ], "lists_unavailable": [] }, { "type": "screening", "query": { "name": "Jane Example Smith", "id": null, "country": null, "date_of_birth": null, "entity_type": "any" }, "status": "no_match", "best_score": null, "match_count": 0, "matches": [], "lists_screened": [ "OFAC_SDN", "OFAC_CONS", "EU", "UK", "UN" ], "lists_unavailable": [] }, { "type": "screening", "query": { "name": "Banco Nacional de Cuba", "id": "cust-42", "country": "CU", "date_of_birth": null, "entity_type": "any" }, "status": "potential_match", "best_score": 100, "match_count": 1, "matches": [ { "score": 100, "matched_name": "BANCO NACIONAL DE CUBA", "matched_on": "primary", "list": "OFAC_SDN", "entry_id": "306", "entity_type": "entity", "primary_name": "BANCO NACIONAL DE CUBA", "programs": [ "CUBA" ], "countries": [ "Switzerland", "Spain", "Japan", "Panama" ], "country_match": false, "source_url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=306" } ], "lists_screened": [ "OFAC_SDN", "OFAC_CONS", "EU", "UK", "UN" ], "lists_unavailable": [] } ] ``` Things to notice: Rosneft scored 100 on an alias of the listed entity on several lists (match_count 12 in total, top 3 returned). Banco Nacional de Cuba matched the OFAC SDN primary name, but country_match is false because the listed addresses are outside Cuba, which is exactly the kind of detail a reviewer needs. Jane Example Smith came back no_match. The run's RUN_SUMMARY record shows what was loaded: ``` { "names_screened": 3, "names_with_matches": 2, "potential_matches": 13, "lists": { "OFAC_CONS": { "loaded": true, "entries": 481, "ms": 2222 }, "UN": { "loaded": true, "entries": 1010, "ms": 3730 }, "UK": { "loaded": true, "entries": 6408, "ms": 5768 }, "OFAC_SDN": { "loaded": true, "entries": 19457, "ms": 6604 }, "EU": { "loaded": true, "entries": 6234, "ms": 8072 } }, "charged_events": { "name-screened": 3 } } ``` The whole run took about 11 seconds, almost all of it downloading and indexing the five lists. The Actor README documents roughly 10–40 seconds for list loading when the lists are downloaded (depending on the government servers) and about 1–5 seconds from the cache, after which screening runs at hundreds of names per second, so put all your names into one run rather than one run per name. ## Python and JavaScript ``` import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("fairquery/sanctions-watchlist-screener").call( run_input={"names": ["Acme Trading LLC", "Rosneft"], "minScore": 90} ) for row in client.dataset(run["defaultDatasetId"]).iterate_items(): if row["type"] == "screening" and row["status"] == "potential_match": print(row["query"]["name"], row["best_score"], [(m["list"], m["primary_name"]) for m in row["matches"]]) ``` ``` import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('fairquery/sanctions-watchlist-screener').call({ queries: [{ name: 'Ali Khamenei', id: 'cust-42', country: 'IR', dateOfBirth: '1939' }], }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); for (const r of items.filter((i) => i.status === 'potential_match')) { console.log(r.query.id, r.best_score, r.matches.map((m) => `${m.list}: ${m.primary_name}`)); } ``` ## One name per request: the RapidAPI endpoint If you'd rather call a plain REST endpoint with a RapidAPI key, the Sanctions Screening – OFAC EU UK UN listing exposes GET /screen?name=... (also accepts POST with a JSON body) with optional country, dateOfBirth, entityType, lists, minScore, maxMatches and excludeNonMatchingCountry. RapidAPI's playground on the listing generates the request code for your language. Plans as published on the listing: Plan | Price / month | Requests included | Overage | Rate limit | BASIC | $0 | 20 (hard limit) | none | 1 req/s | PRO | $19 | 5,000 | $0.006 | 5 req/s | ULTRA | $49 | 25,000 | $0.004 | 10 req/s | MEGA | $99 | 100,000 | $0.002 | 20 req/s | Speed: the first request after a period of inactivity can take up to about 60 s while the five official lists are downloaded and indexed (about 40 s in our tests); later requests are typically fast, about 1 s or less. Use a client timeout of at least 90 s and retry once on a timeout. ## Using it from an AI agent (MCP) - Apify MCP: connect your MCP client to mcp.apify.com and call Actor fairquery/sanctions-watchlist-screener with {"names": ["Acme Trading LLC"]}. - Agent Trust MCP server: @fairquery/agent-trust-mcp includes sanctions_screen_names ($0.003 per name, your own Apify token), the free sanctions_list_sources, and trust_screen_lead, which combines an email check and sanctions screening into a pass/review/block decision. - Single-purpose server: the GitHub repo ships a stdio MCP server with screen_names and a free list_sources. ## Pricing $0.003 per name screened (= $3 per 1,000 names) on Apify, pay per event, plus a $0.007 Actor-start event per run. A name is charged once whether it matches or not. Error rows are never charged, and if no list can be downloaded nothing is charged. The three names in the example above cost 3 × $0.003 + $0.007 = $0.016. On RapidAPI you pay the monthly plan above instead. ## How it compares with alternatives Option | Good for | Trade-offs | OFAC Sanctions List Search (official web tool) | Free manual look-ups against OFAC lists | OFAC lists only; an interactive search, not built for screening a batch through your own code | Downloading the lists yourself | Full control, no per-name fee | Five formats (CSV and XML), alias tables, name normalisation and fuzzy matching are yours to build and maintain | OpenSanctions | Much broader coverage (many more sanctions sources plus PEP and other datasets), hosted matching API | Per its published terms, bulk data is free only for non-commercial use; business use, including compliance screening, needs a paid API plan or data license | Regulated KYC/AML vendors | Audit trails, PEP/adverse media, ongoing monitoring, contractual service levels | Contract-based pricing and onboarding | Fairquery Sanctions Screener | Cheap batch or per-request pre-screening of OFAC/EU/UK/UN from official sources, API and MCP access | Five lists only (no PEP, adverse media or other national lists), no ongoing monitoring, not a certified screening service | ## FAQ ### What is the cheapest way to screen names against OFAC, EU, UK and UN sanctions lists via API? The Fairquery Sanctions & Watchlist Screener on Apify costs $0.003 per name ($3 per 1,000 names), plus $0.007 per run, pay per use, and screens against all five lists from the official sources, never more than 6 hours old, with each list's publication date in every row. On RapidAPI the same screening has a free tier of 20 requests/month and paid plans from $19/month for 5,000 requests. ### Which sanctions lists are covered? OFAC SDN, OFAC Consolidated (non-SDN), the EU Consolidated Financial Sanctions List, the UK Sanctions List (incl. OFSI asset-freeze targets) and the UN Security Council Consolidated List. PEP lists, adverse media and other national lists are not included. ### Is a potential match a legal determination? No. Results are automated fuzzy matches for human review, not legal determinations. They can include false positives and can miss true matches. Confirm every potential match against the official publication and take legal advice before acting. ### How does the fuzzy matching handle word order and spelling variants? Names are normalised (diacritics, case, punctuation, legal suffixes, honorifics) and compared token by token with Jaro-Winkler, so 'PUTIN, Vladimir', extra middle names and variants like Usama/Osama still match. Every alias is screened. The score is 0-100 with a default threshold of 85. ### How long does a screening run take? Downloading and indexing the five lists takes roughly 10-40 seconds depending on the government servers (about 11 seconds in our test run); after that, names are screened at hundreds per second. Put all names in one run. ### Can an AI agent call it? Yes: via Apify MCP with Actor fairquery/sanctions-watchlist-screener, via the sanctions_screen_names and trust_screen_lead tools in @fairquery/agent-trust-mcp, or via the stdio MCP server in the GitHub repo. Source and docs: github.com/fairquery/sanctions-watchlist-screener (MIT) · Apify Store · RapidAPI · all Fairquery tools --- # How do I redact PII and secrets before sending data to an LLM? URL: https://fairquery.github.io/posts/redact-pii-secrets-before-sending-data-to-llm/ Published: 2026-10-17 Short answer: run every prompt, document or log through a detector that finds PII (emails, phones, card numbers, SSNs, IBANs, IPs) and secrets (API keys, tokens, private keys), replace each hit with a placeholder like [EMAIL_1], send only the redacted text to the LLM, and put the real values back into the answer afterwards if you need them. You can do this for free on your own machine with the pii_scan_text and pii_redact_text tools in the @fairquery/agent-trust-mcp MCP server (text never leaves your machine), or at batch scale with the LLM Data Guard Apify Actor for $0.0015 per document + $0.0002 per finding, which adds name/address detection, URL and PDF input, and a restore mode. ## Why redact before the LLM call Once text is in a prompt it can end up in provider logs, traces, evaluation datasets, vector stores and, through retrieval, in other users' answers. OWASP lists this as LLM02: Sensitive Information Disclosure in its Top 10 for LLM applications. Redacting at the input side is the cheapest control: the model usually doesn't need the real email address or card number to summarise a support ticket, classify a log line or draft a reply. ## What gets detected Category | Types | How | PII | EMAIL, PHONE, SSN, CREDIT_CARD, IBAN, UK_NINO, IP_ADDRESS | Luhn + issuer prefix for cards, mod-97 + country length for IBANs, libphonenumber for phones, SSN area/group rules | Secrets | PRIVATE_KEY, AWS_ACCESS_KEY, AWS_SECRET_KEY, GITHUB_TOKEN, GITLAB_TOKEN, SLACK_TOKEN, SLACK_WEBHOOK, STRIPE_KEY, GOOGLE_API_KEY, OPENAI_KEY, ANTHROPIC_KEY, HUGGINGFACE_TOKEN, NPM_TOKEN, SENDGRID_KEY, TWILIO_KEY, AZURE_STORAGE_KEY, JWT, URL_CREDENTIALS, GENERIC_SECRET | Gitleaks-style rules; GENERIC_SECRET (api_key=..., password=...) uses a Shannon-entropy threshold | Opt-in | HIGH_ENTROPY_STRING (local + Actor); PERSON, LOCATION, STREET_ADDRESS (Actor only) | Entropy scan; open-source NER (compromise, English) and street-address patterns | Overlaps are resolved (secrets > cards/IBAN > SSN > email > IP > phone > address > names), so a card number is never also reported as a phone. ## Option 1 (free): redact locally with the Agent Trust MCP server Add the server to Claude Desktop (claude_desktop_config.json), Cursor (~/.cursor/mcp.json) or any MCP client. No token is needed for the free tools: ``` { "mcpServers": { "fairquery-trust": { "command": "npx", "args": ["-y", "@fairquery/agent-trust-mcp"] } } } ``` Then ask the agent to "redact this ticket before summarising it", or call the tools from your own code with the MCP SDK. This is the script we ran (Node.js 20+, npm i @modelcontextprotocol/sdk): ``` import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; const transport = new StdioClientTransport({ command: 'npx', args: ['-y', '@fairquery/agent-trust-mcp'] }); const client = new Client({ name: 'redactor', version: '1.0.0' }); await client.connect(transport); const ticket = 'Ticket #4411 from Jane Doe , phone +1 415-555-0132. ' + 'Card 4111 1111 1111 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE'; const res = await client.callTool({ name: 'pii_redact_text', arguments: { texts: [ticket], returnPlaceholderMap: true }, }); console.log(res.content[0].text); // JSON: meta.placeholder_map + items[].output_text await client.close(); ``` (The card number is the standard Visa test number and the AWS key is the example key from AWS's documentation.) ### Real output: pii_redact_text (placeholder style) ``` { "meta": { "engine": "local", "redaction_style": "placeholder", "placeholder_map": { "[EMAIL_1]": "jane.doe@example.com", "[PHONE_1]": "+1 415-555-0132", "[CREDIT_CARD_1]": "4111 1111 1111 1111", "[AWS_ACCESS_KEY_1]": "AKIAIOSFODNN7EXAMPLE" } }, "items": [ { "type": "redacted_document", "finding_count": 4, "risk_level": "critical", "output_text": "Ticket #4411 from Jane Doe <[EMAIL_1]>, phone [PHONE_1]. Card [CREDIT_CARD_1] was declined. Debug log: AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_1]" } ] } ``` Send output_text to the model; keep placeholder_map on your side (it contains the originals) to swap the real values back into the answer. With redactionStyle: "mask" the same input became: ``` Ticket #4411 from Jane Doe , phone +* ***-***-0132. Card **** **** **** 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIA**************** ``` ### Real output: pii_scan_text (detect only) Use the scan tool when you only want to know whether text is safe to send, for example to block a prompt or route it for review. Findings carry offsets and a masked preview, never the raw value: ``` { "count": 4, "total": 4, "offset": 0, "hasMore": false, "nextOffset": null, "meta": { "documents": [ { "doc_index": 0, "chars": 162, "finding_count": 4, "counts_by_type": { "EMAIL": 1, "PHONE": 1, "CREDIT_CARD": 1, "AWS_ACCESS_KEY": 1 }, "risk_level": "critical" } ], "engine": "local" }, "items": [ { "type": "EMAIL", "doc_index": 0, "category": "pii", "rule": "email", "start": 28, "end": 48, "confidence": "high", "masked": "j***@e***.com" }, { "type": "PHONE", "doc_index": 0, "category": "pii", "rule": "libphonenumber", "start": 57, "end": 72, "confidence": "high", "masked": "+* ***-***-0132" }, { "type": "CREDIT_CARD", "doc_index": 0, "category": "pii", "rule": "card-luhn", "start": 79, "end": 98, "confidence": "high", "masked": "**** **** **** 1111" }, { "type": "AWS_ACCESS_KEY", "doc_index": 0, "category": "secret", "rule": "aws-access-key-id", "start": 142, "end": 162, "confidence": "high", "masked": "AKIA****************" } ] } ``` In the local tools, risk_level is critical if any secret is found, high for card/IBAN/SSN/NI numbers, medium for other PII and none otherwise. In our test the scan returned in 14 ms and the redaction in 5 ms (server already running). Limits per call: up to 50 texts, 200,000 characters each, 500,000 in total. ## Option 2 (API, batch): LLM Data Guard Actor Use the Actor when you need to process many documents, fetch public URLs or PDFs (text layer, no OCR, up to 10 MB each), detect person names and street addresses, use consistent placeholders across a whole dataset (for RAG or fine-tuning), or restore values server-side. ``` curl -X POST \ "https://api.apify.com/v2/acts/fairquery~llm-data-guard-pii-secrets/run-sync-get-dataset-items" \ -H "Authorization: Bearer $APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "texts": ["Ticket #4411 from Jane Doe , phone +1 415-555-0132. Card 4111 1111 1111 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE"], "mode": "redact", "detectNames": true }' ``` ### Real output from that request Abridged (first two of five findings shown). With detectNames on, the Actor also caught the name Jane Doe as a low-confidence PERSON: ``` { "type": "document", "id": "text-1", "chars": 162, "mode": "redact", "finding_count": 5, "counts_by_type": { "PERSON": 1, "EMAIL": 1, "PHONE": 1, "CREDIT_CARD": 1, "AWS_ACCESS_KEY": 1 }, "risk_level": "high", "findings": [ { "type": "PERSON", "category": "pii", "rule": "ner-compromise", "confidence": "low", "start": 18, "end": 26, "length": 8, "preview": "**** ***", "replacement": "[PERSON_1]" }, { "type": "EMAIL", "category": "pii", "rule": "email", "confidence": "high", "start": 28, "end": 48, "length": 20, "preview": "j***@e***.com", "replacement": "[EMAIL_1]" }, "… 3 more (PHONE, CREDIT_CARD, AWS_ACCESS_KEY)" ], "output_text": "Ticket #4411 from [PERSON_1] <[EMAIL_1]>, phone [PHONE_1]. Card [CREDIT_CARD_1] was declined. Debug log: AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_1]" } ``` That run took 2.5 seconds end to end on Apify (mostly container start) and would cost $0.0015 + 5 × $0.0002 = $0.0025. The README documents roughly 1–5 MB of text per second for the regex detectors; NER is slower. In the Actor, risk_level is high for any secret or card/SSN/IBAN/NI number, medium for other confident PII and low for low-confidence hits only. ### Restore after the model answers ``` { "mode": "restore", "texts": ["Dear [PERSON_1], we emailed [EMAIL_1] about the declined card."], "placeholderMap": { "[PERSON_1]": "Jane Doe", "[EMAIL_1]": "jane.doe@example.com" } } ``` Get the map by setting returnPlaceholderMap: true in redact mode. Treat it like the original data, because it is. ### Data handling The Actor is designed not to log document content. It writes only result rows and a counts-only RUN_SUMMARY, deletes the run's INPUT record at the end by default (purgeInputAfterRun), and omits raw values from findings unless you set includeFindingValues. The free local tools are designed to make no network calls. ## Pricing Option | Price | pii_scan_text, pii_redact_text, pii_list_detectors (local) | Free, no account | LLM Data Guard Actor / pii_guard_documents MCP tool | $0.0015 per document + $0.0002 per finding (+ Apify's $0.00005 Actor-start event); error rows never charged | ## How it compares with alternatives Option | Good for | Trade-offs | Microsoft Presidio (open source) | Self-hosted PII detection and anonymisation with NER models and custom recognizers, many languages via spaCy/transformers models | You run and tune it yourself (Python services, models); secrets/API-key detection is not its focus | Cloud DLP / PII APIs (Google Cloud Sensitive Data Protection, Amazon Comprehend, Azure AI Language) | Broad, managed detection with many info types and languages | The text is sent to that cloud provider; pricing and setup vary by provider | Secret scanners (gitleaks, TruffleHog) | Finding leaked credentials in repositories and git history | Built for code and CI, not for redacting PII in prompts | Fairquery (local MCP tools + Actor) | PII and secrets in one pass, reversible placeholders, free local mode, MCP-native for agents | Pattern-based: English-only NER, no OCR, unusual formats can slip through | ## FAQ ### How do I remove personal data from a prompt before sending it to ChatGPT, Claude or another LLM? Run the text through a PII and secret detector, replace each hit with a placeholder such as [EMAIL_1], send the redacted text, and restore the originals in the answer if needed. Fairquery's free pii_redact_text tool in @fairquery/agent-trust-mcp does this locally; the LLM Data Guard Actor does it at batch scale via API. ### Is there a free way to redact PII locally? Yes. The pii_scan_text and pii_redact_text tools in the @fairquery/agent-trust-mcp MCP server are free, need no account and make no network calls. Limits: 50 texts per call, 200,000 characters each, 500,000 in total. ### Which secrets can it detect? Private keys, AWS access and secret keys, GitHub, GitLab, Slack, Stripe, Google, OpenAI, Anthropic, Hugging Face, npm, SendGrid, Twilio and Azure storage keys, JWTs, credentials in URLs, and generic api_key=/password= assignments above an entropy threshold. ### Can I put the original values back after the LLM answers? Yes. Placeholder redaction is reversible: request the placeholder map (returnPlaceholderMap) and swap values back yourself, or call the Actor with mode 'restore' and the map. ### How much does the LLM Data Guard API cost? $0.0015 per document plus $0.0002 per finding on Apify (pay per event); error rows are never charged. A document with 5 findings costs $0.0025. ### Does it detect names and addresses? The Actor can, opt-in (detectNames, detectAddresses), using English open-source NER and street-address patterns; these hits are marked low confidence. The free local tools do not detect names or addresses. Honest limits: pattern-based detection finds most but not all sensitive data. Non-English names, unusual formats, images and scanned PDFs can slip through, and some matches are false positives. Use it as one layer of a data-protection program, not as a compliance certification, and only process data you are allowed to process. Source and docs: github.com/fairquery/llm-data-guard-pii-secrets · Apify Store · Agent Trust MCP on GitHub · npm · all Fairquery tools --- # How do I find a company's contact email, phone and social profiles from its domain? URL: https://fairquery.github.io/posts/find-company-contact-email-phone-socials-from-domain/ Published: 2026-10-17 Short answer: fetch the company's homepage plus its contact, about and imprint pages (respecting robots.txt), extract mailto:/tel: links, JSON-LD Organization data, obfuscated emails and social links, then validate the emails' domain for MX records. The Fairquery Website Contact & Company Enricher does all of that per domain and returns one clean row (role emails, E.164 phones, socials, company name, description, address) for $0.003 per domain; unreachable, blocked or invalid domains are free. ## What you get per domain - Business emails, validated with the same code as the Fairquery Email Validator: syntax, DNS MX / null MX, disposable list, free-provider and typo checks (no SMTP probing). By default only role/generic inboxes (info@, sales@, hello@) are returned. - Phone numbers normalised to E.164, with the page they came from. - Social profiles: company LinkedIn, X/Twitter, GitHub, Facebook, Instagram, YouTube, TikTok and so on. Personal LinkedIn profiles (/in/...) are never returned. - Company name, description and postal address from JSON-LD, og:site_name,
blocks and imprint pages. - Mail domain facts: MX, null MX, mail provider (for example Google Workspace or Microsoft 365), optional SPF/DMARC. ## How it works - Normalises each input (https://www.Acme.com/contact, jane@acme.com, acme.com → acme.com) and removes duplicates. - Reads robots.txt (RFC 9309) and honours Disallow and Crawl-delay for user agent FairqueryBot. - Fetches the homepage, then the best-scoring same-site pages (contact, Kontakt, contacto, about, imprint/Impressum, legal notice, team) up to maxPagesPerDomain (default 5, max 15), one request at a time per site with requestDelayMs (default 1,000 ms) between them. - Extracts JSON-LD, mailto:/tel: links, Cloudflare-protected emails, name [at] domain [dot] com obfuscation, social links and addresses, then validates the emails. It is plain HTTP, with no headless browser and no proxy, so sites that render contact details only with JavaScript or block datacenter IPs return fewer fields. ## How to run it with curl ``` curl -X POST \ "https://api.apify.com/v2/acts/fairquery~website-contact-company-enricher/run-sync-get-dataset-items" \ -H "Authorization: Bearer $APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"domains": ["apify.com"], "maxPagesPerDomain": 4}' ``` ### Real output from that request We ran exactly this on October 17, 2026 (row shown in full, minus the timestamp and the duplicate addresses array): ``` { "type": "domain", "input": "apify.com", "domain": "apify.com", "final_url": "https://apify.com/", "company_name": "Apify Technologies s.r.o.", "description": "Thousands of tools to automate your business. Get real-time web data, track competitors, generate leads, and integrate your apps and AI agents.", "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": [], "social_links": { "github": [ "https://github.com/apify" ], "linkedin": [ "https://linkedin.com/company/apify" ], "twitter": [ "https://x.com/apify" ], "tiktok": [ "https://tiktok.com/@apifytech", "https://tiktok.com/@apifyoffice" ] }, "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", "https://apify.com/contact-sales" ], "pages_blocked_by_robots": 0, "robots_txt": "ok", "email_scope": "role" } ``` The run crawled 3 pages, took 5.1 seconds and would cost $0.003. The README documents about 3–8 seconds per domain with the default politeness settings and 5 domains in parallel, so 100 domains take roughly 1–3 minutes. No phone number was published on the crawled pages here, so phones is empty rather than guessed. ## Python: enrich a CSV of company websites ``` import csv, os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) domains = [row["website"] for row in csv.DictReader(open("companies.csv"))] run = client.actor("fairquery/website-contact-company-enricher").call( run_input={"domains": domains, "emailScope": "role", "defaultPhoneCountry": "US"} ) for r in client.dataset(run["defaultDatasetId"]).iterate_items(): if r["type"] == "domain": print(r["domain"], r["primary_email"], [p["e164"] for p in r["phones"]], r["social_links"].get("linkedin")) else: print("skipped:", r.get("code"), r.get("message")) ``` ## Input options Field | Default | Meaning | domains | (required) | Domains, URLs or emails | maxPagesPerDomain | 5 | 1–15, homepage included | emailScope | role | business adds named addresses on the company's own domain; all returns everything found | validateEmails | true | MX / disposable / typo checks | checkSpfDmarc | false | SPF/DMARC for the company mail domain | defaultPhoneCountry | – | e.g. US; also parses national-format numbers in page text | domainConcurrency / requestDelayMs / requestTimeoutSecs | 5 / 1000 / 15 | Politeness and timeouts | maxDomains | 1000 | Safety cap per run | Error rows (never charged): INPUT_EMPTY, INVALID_DOMAIN, MAX_DOMAINS_EXCEEDED, SITE_UNREACHABLE, BLOCKED_BY_ROBOTS, ENRICH_FAILED. ## From an AI agent Call Actor fairquery/website-contact-company-enricher through Apify MCP, or run the stdio MCP server from the GitHub repo. It has enrich_domains (paid, your Apify token) and a free local normalize_domains tool for cleaning a messy list first. Real output of normalize_domains for ["https://www.Apify.com/contact", "jane@stripe.com", "basecamp.com", "apify.com", "not a domain"]: ``` { "domains": ["apify.com", "stripe.com", "basecamp.com"], "invalid": ["not a domain"], "duplicates_removed": 1 } ``` ## Pricing $0.003 per domain enriched (= $3 per 1,000 domains) on Apify, pay per event, plus Apify's $0.00005 Actor-start event. Only domains that return a result row are charged; unreachable, robots-blocked and invalid domains are free. ## How it compares with alternatives Option | Good for | Trade-offs | Doing it by hand | A handful of companies | Slow; easy to miss imprint pages and obfuscated emails | Writing your own scraper | Full control | robots.txt handling, page discovery, email de-obfuscation, phone parsing and email validation to build and maintain | B2B contact databases (for example Hunter, Apollo, ZoomInfo) | Finding named people, job titles and personal work emails from large pre-built databases | Subscription pricing; data comes from their database rather than a live read of the company's site | Fairquery Contact Enricher | Live, per-domain company contact data with source URLs and validated emails, pay per domain | Only what the company publishes on its own site; role inboxes by default, no person search; JavaScript-only sites yield less | ## FAQ ### How can I get a company's contact email from its website domain? Crawl the homepage and its contact, about and imprint pages, extract mailto links, JSON-LD and obfuscated emails, and validate the domain's MX records. The Fairquery Website Contact & Company Enricher does this per domain for $0.003 and returns validated role emails with the page they were found on. ### Does it find personal email addresses of employees? No. By default it returns only role/generic inboxes such as info@, sales@ or hello@ (emailScope 'role'). 'business' adds named addresses published on the company's own domain. It never returns personal LinkedIn profiles and does not search for people. ### Does it respect robots.txt? Yes. It reads robots.txt (RFC 9309), honours Disallow and Crawl-delay for user agent FairqueryBot, makes one request at a time per site with a 1 second default delay, and reports pages_blocked_by_robots. ### How fast is it? About 3-8 seconds per domain with the default settings and 5 domains in parallel, so 100 domains take roughly 1-3 minutes. Our single-domain test run took 5.1 seconds. ### What does it cost? $0.003 per domain enriched on Apify. Unreachable, robots-blocked and invalid domains are free. ### Why are some fields empty? It only returns what the company publishes on the crawled pages. Sites that render contacts with JavaScript only, or block datacenter IPs, return fewer fields, because it uses plain HTTP without a browser or proxy. Use responsibly: business contact data can still be personal data under GDPR/UK GDPR. 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. Source and docs: github.com/fairquery/website-contact-company-enricher (MIT) · Apify Store · all Fairquery tools --- # How do I check npm and PyPI dependencies for vulnerabilities in CI with OSV? URL: https://fairquery.github.io/posts/check-npm-pypi-dependencies-vulnerabilities-ci-osv/ Published: 2026-10-17 Short answer: pin exact versions (use your lockfile or pip freeze), send them to the free OSV.dev API, and fail the build when a vulnerability at or above your severity bar is found. For a free yes/no gate you can call OSV's querybatch endpoint directly or use the free osv_query_packages tool in @fairquery/agent-trust-mcp. For CI-ready rows with CVE IDs, CVSS scores, severity filtering, alias de-duplication and a recommended upgrade version, run the Fairquery OSV Dependency Vulnerability Check Actor: $0.0005 per package checked + $0.0015 per vulnerability found. ## Why OSV OSV.dev is a free, open vulnerability database and API, an open-source project launched by Google. It aggregates advisories from the GitHub Advisory Database, the PyPA advisory database, the Go vulnerability database, RustSec and others in one schema, and covers npm, PyPI, Go, Maven, crates.io, RubyGems, NuGet, Packagist, Pub, Hex and more. It needs no API key. The catch is that a raw OSV response is a list of advisory IDs per package; severity, CVSS, CVE aliases and fixed versions are in separate detail records you must fetch and merge yourself. ## Option 1 (free): query OSV directly ``` curl -s -X POST https://api.osv.dev/v1/querybatch -d '{ "queries": [ {"package": {"ecosystem": "npm", "name": "lodash"}, "version": "4.17.20"}, {"package": {"ecosystem": "npm", "name": "express"}, "version": "4.21.2"} ]}' ``` Real response (October 17, 2026, about 0.2 s): five advisories for lodash 4.17.20, none for express 4.21.2. ``` {"results":[{"vulns":[{"id":"GHSA-29mw-wpgm-hmr9","modified":"2025-09-29T21:12:31.102523Z"},{"id":"GHSA-35jh-r3h4-6jhm","modified":"2026-10-17T02:55:38.085813Z"},{"id":"GHSA-f23m-r3pf-42rh","modified":"2026-10-17T02:56:13.084362Z"},{"id":"GHSA-r5fr-rjxr-66jc","modified":"2026-10-17T02:56:15.633068Z"},{"id":"GHSA-xxjr-mmjv-4gpg","modified":"2026-10-17T02:55:54.229635Z"}]},{}]} ``` A minimal free CI gate for an npm lockfile (v2/v3) with jq: ``` jq '{queries: [.packages | to_entries[] | select(.key | startswith("node_modules/")) | {package: {ecosystem: "npm", name: (.key | sub("^.*node_modules/"; ""))}, version: .value.version}]}' \ package-lock.json > q.json curl -s -X POST https://api.osv.dev/v1/querybatch -d @q.json > osv.json jq -e '[.results[].vulns // [] | length] | add == 0' osv.json # exit code 1 if anything is vulnerable ``` This gate fails on any advisory, including low-severity ones, because querybatch doesn't return severity. In an agent, the free osv_query_packages tool does the same lookup for up to 200 exact versions per call. Real output for lodash 4.17.20, django 3.2.0 and express 4.21.2 (abridged): ``` { "meta": { "vulnerable_count": 2, "source": "https://api.osv.dev/v1/querybatch" }, "items": [ { "ecosystem": "npm", "name": "lodash", "version": "4.17.20", "vulnerable": true, "vuln_count": 5, "vuln_ids": [ "GHSA-29mw-wpgm-hmr9", "GHSA-35jh-r3h4-6jhm", "GHSA-f23m-r3pf-42rh", "GHSA-r5fr-rjxr-66jc", "GHSA-xxjr-mmjv-4gpg" ] }, { "ecosystem": "PyPI", "name": "django", "version": "3.2.0", "vulnerable": true, "vuln_count": 66, "vuln_ids": [ "GHSA-2gwj-7jmv-h26r", "GHSA-2hrw-hx67-34x6", "GHSA-3h9f-r86x-qvjx", "… 63 more" ] }, { "ecosystem": "npm", "name": "express", "version": "4.21.2", "vulnerable": false, "vuln_count": 0, "vuln_ids": [] } ] } ``` Its sibling osv_get_vulnerability (also free) returns the full advisory, for example GHSA-35jh-r3h4-6jhm = CVE-2021-23337, "Command Injection in lodash", severity HIGH. ## Option 2 (API): severity-filtered rows from the OSV Actor The Actor parses package.json, package-lock.json (v1–v3), requirements.txt or go.mod, queries OSV, fetches every advisory, computes the CVSS v3 base score, maps GHSA/PYSEC/GO IDs to CVEs, collapses duplicate advisories (dedupeAliases), filters by minSeverity, and computes the smallest version that fixes every emitted vulnerability. We ran it on October 17, 2026 with this input (two explicit packages plus a small package.json): ``` { "packages": [ {"ecosystem": "npm", "name": "lodash", "version": "4.17.20"}, {"ecosystem": "PyPI", "name": "django", "version": "3.2.0"} ], "manifestText": "{\"dependencies\":{\"lodash\":\"4.17.20\",\"express\":\"4.21.2\"}}", "minSeverity": "HIGH" } ``` ### Real output Abridged: three package rows (lodash appeared in both inputs and was checked once) and the first of 23 vulnerability rows: ``` { "package_rows": [ { "type": "package", "ecosystem": "npm", "name": "lodash", "version": "4.17.20", "purl": "pkg:npm/lodash@4.17.20", "origin": "input", "vulnerable": true, "vuln_count": 2, "vuln_count_total": 5, "vuln_ids": [ "GHSA-r5fr-rjxr-66jc", "GHSA-35jh-r3h4-6jhm" ], "max_severity": "HIGH", "max_cvss_score": 8.1, "fixed_versions": [ "4.18.0", "4.17.21" ], "recommended_version": "4.18.0" }, { "type": "package", "ecosystem": "PyPI", "name": "django", "version": "3.2.0", "purl": "pkg:pypi/django@3.2.0", "origin": "input", "vulnerable": true, "vuln_count": 21, "vuln_count_total": 66, "vuln_ids": [ "GHSA-2gwj-7jmv-h26r", "GHSA-p64x-8rxx-wf6q", "GHSA-r3xc-prgr-mg9p", "… 18 more" ], "max_severity": "CRITICAL", "max_cvss_score": 9.8, "fixed_versions": [ "2.2.28", "3.2.13", "4.0.4", "… 47 more" ], "recommended_version": "5.2.17" }, { "type": "package", "ecosystem": "npm", "name": "express", "version": "4.21.2", "purl": "pkg:npm/express@4.21.2", "origin": "manifest", "vulnerable": false, "vuln_count": 0, "vuln_count_total": 0, "vuln_ids": [], "max_severity": "NONE", "max_cvss_score": null, "fixed_versions": [], "recommended_version": null } ], "first_vulnerability_row": { "type": "vulnerability", "id": "GHSA-r5fr-rjxr-66jc", "package_name": "lodash", "package_version": "4.17.20", "cve_ids": [ "CVE-2026-4800" ], "summary": "lodash vulnerable to Code Injection via `_.template` imports key names", "severity": "HIGH", "cvss_score": 8.1, "cvss_version": "3.1", "fixed_versions": [ "4.18.0" ], "osv_url": "https://osv.dev/vulnerability/GHSA-r5fr-rjxr-66jc" } } ``` Because a package.json holds ranges, the Actor also emitted a free warning row; feed it the lockfile instead for exact versions: ``` { "type": "error", "code": "RANGE_APPROXIMATED", "message": "package.json ranges were resolved to their lower bound (e.g. ^4.17.20 -> 4.17.20). Installed versions may differ; prefer package-lock.json.", "context": { "manifest_type": "package.json", "severity": "warning" } } ``` And the counts from the run's RUN_SUMMARY. OSV knows 66 advisories for django 3.2.0; after the HIGH filter and alias de-duplication 21 remain: ``` { "packages_checked": 3, "packages_vulnerable": 2, "vulnerabilities_emitted": 23, "vulnerabilities_filtered_out": 20, "vulnerabilities_deduplicated": 28, "severity_counts": { "NONE": 0, "UNKNOWN": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 17, "CRITICAL": 6 }, "osv_requests": 72 } ``` The run took 2.8 seconds on Apify and would cost 3 × $0.0005 + 23 × $0.0015 = $0.036. The README documents about 5–10 s for a 2-package run; time grows with the number of advisories to fetch (8 detail requests run in parallel). ## GitHub Actions: fail the PR on HIGH/CRITICAL Add your Apify token as the repository secret APIFY_TOKEN, then: ``` name: dependency-vulnerabilities on: [pull_request] jobs: osv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: OSV check (HIGH and CRITICAL only) env: APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }} run: | jq -n --rawfile m package-lock.json \ '{manifestText: $m, minSeverity: "HIGH"}' > input.json curl -sf -X POST \ "https://api.apify.com/v2/acts/fairquery~osv-dependency-vulnerability-check/run-sync-get-dataset-items" \ -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \ -d @input.json > osv.json jq -r '.[] | select(.type=="package" and .vulnerable) | "\(.name)@\(.version) -> upgrade to \(.recommended_version // "n/a") (\(.max_severity))"' osv.json n=$(jq '[.[] | select(.type=="vulnerability")] | length' osv.json) echo "$n HIGH/CRITICAL vulnerabilities" test "$n" -eq 0 ``` Run against the dataset from our test run above, the reporting and gating steps print: ``` lodash@4.17.20 -> upgrade to 4.18.0 (HIGH) django@3.2.0 -> upgrade to 5.2.17 (CRITICAL) 23 HIGH/CRITICAL vulnerabilities ``` and exit with status 1, failing the job. Notes: when you pass only a manifest, only the packages in that manifest are checked and billed; an input with neither packages nor manifestText fails immediately with an INPUT_EMPTY error row and no package or vulnerability events are charged. For Python, use pip freeze > requirements.txt first (unpinned lines are skipped with a VERSION_NOT_PINNED warning). The default safety cap is maxPackages: 1000 unique packages per run; raise it for very large lockfiles. You can also set a maximum charge per run in the Apify run options; the Actor stops cleanly at the limit. Prefer n8n? Fairquery's free n8n templates include an OSV CI gate workflow that calls this Actor. ## Pricing Option | Price | OSV.dev API directly, osv_query_packages, osv_get_vulnerability | Free | OSV Actor / osv_audit_dependencies MCP tool | $0.0005 per package row + $0.0015 per vulnerability row emitted (after the severity filter and de-duplication), + Apify's $0.00005 Actor-start event. Error and warning rows are never charged. | Cost scales with what you check: a 500-package lockfile with no HIGH+ findings costs 500 × $0.0005 = $0.25 per run. For a gate that runs on every push, consider the free lookup for the pass/fail decision and the Actor for the detailed report. ## How it compares with alternatives Option | Good for | Trade-offs | npm audit | Built into npm, free, uses the GitHub Advisory Database | npm projects only | pip-audit | Free PyPA tool for Python environments and requirements files | Python only | OSV-Scanner | Free, open-source CLI from Google that scans lockfiles and directories against OSV, many ecosystems | A binary to install and run in CI; output formatting and gating are up to you | Dependabot / GitHub security alerts | Automatic alerts and update PRs on GitHub repositories | Works on repositories hosted on GitHub; not built for checking an arbitrary package list from an agent or another CI system | Fairquery OSV Actor | One API/MCP call from CI, agents or n8n; CVSS, CVE mapping, severity filter, de-duplication, recommended version as JSON rows | Paid per package and per vulnerability; exact versions only; no reachability analysis, license checks or container image scanning | ## FAQ ### How do I fail a CI build when a dependency has a known vulnerability? Pin exact versions (lockfile or pip freeze), query OSV.dev, and exit non-zero when results contain vulnerabilities at or above your severity bar. The post shows a free jq + OSV querybatch gate and a GitHub Actions job using the Fairquery OSV Actor with minSeverity HIGH. ### Is the OSV.dev API free? Yes. OSV.dev is free and needs no API key. Its querybatch endpoint returns advisory IDs per package; severity and fix details come from separate per-advisory requests. ### What does the Fairquery OSV Actor add over the raw OSV API? Manifest parsing (package.json, package-lock.json v1-v3, requirements.txt, go.mod), computed CVSS v3 scores, CVE mapping, a minSeverity filter, alias de-duplication and a recommended upgrade version, as JSON rows for CI or agents. ### How much does it cost? $0.0005 per package checked plus $0.0015 per vulnerability row emitted after filtering and de-duplication. Error and warning rows are free. Our test (3 packages, 23 HIGH/CRITICAL vulnerabilities) would cost $0.036. ### Which ecosystems are supported? Everything OSV supports, including npm, PyPI, Go, Maven, crates.io, RubyGems, NuGet, Packagist, Pub and Hex. Exact versions are required; package.json ranges are approximated to their lower bound with a warning. ### Does no result mean my dependencies are safe? No. It means OSV has no matching advisory. No database is complete, and the check does not cover reachability, licenses or container images. Honest limits: data comes from OSV.dev and its upstream databases. No advisory database is complete, and no results does not prove a package is safe. Source and docs: github.com/fairquery/osv-dependency-vulnerability-check (MIT) · Apify Store · Fairquery OSV for VS Code · all Fairquery tools --- # How do I give my AI agent trust and safety tools via MCP? URL: https://fairquery.github.io/posts/give-ai-agent-trust-safety-tools-mcp-server/ Published: 2026-10-17 Short answer: add one MCP server to your agent's config: @fairquery/agent-trust-mcp gives Claude Desktop, Cursor, VS Code or any MCP client 12 trust-and-safety tools: PII and secret redaction, email checks, sanctions screening (OFAC, EU, UK, UN) and OSV dependency-vulnerability lookups. 7 tools are free and need no account; they run on your machine (the two OSV lookups call the free OSV.dev API directly). The 5 deeper tools are optional and bill your own Apify account per result. It is listed in the official MCP Registry as io.github.fairquery/fairquery-agent-trust-mcp and on Glama. ## Why an agent needs trust and safety tools Agents increasingly read untrusted input (tickets, emails, scraped pages, uploaded files) and act on it (sign up users, onboard suppliers, add dependencies, send data to other models). Four checks come up again and again: - Is there personal data or a secret in this text? Redact it before it goes into a prompt, a log or a vector store. - Is this email address real and not throwaway? Check syntax, typos, disposable and role addresses, MX records. - Is this person or company on a sanctions list? Pre-screen against OFAC, EU, UK and UN lists before onboarding or paying. - Is this package version vulnerable? Check against OSV.dev before installing or approving a dependency. ## The 12 tools Tool | Cost | What it does | pii_list_detectors | Free (local) | Catalog of detectors, redaction styles and pricing for every paid tool | pii_scan_text | Free (local) | Finds PII and secrets in text; findings are paged and show masked values only | pii_redact_text | Free (local) | Redacts with placeholder, mask or hash style; can return a placeholder map | pii_guard_documents | $0.0015/doc + $0.0002/finding | Adds NER names/addresses, URLs and PDFs, and restore mode | email_check_syntax | Free (local) | Syntax, normalisation, typo suggestions, disposable/role/free-provider flags | email_validate_deliverability | $0.00079/email | DNS/MX, null MX, SPF/DMARC, mail provider, verdict | sanctions_list_sources | Free (local) | Lists covered, issuing authorities, disclaimer | sanctions_screen_names | $0.003/name + $0.007/call | Fuzzy screening against OFAC SDN/Consolidated, EU, UK and UN | osv_query_packages | Free (api.osv.dev) | Vulnerable or not, plus advisory IDs, for up to 200 exact versions | osv_get_vulnerability | Free (api.osv.dev) | Full OSV advisory records | osv_audit_dependencies | $0.0005/package + $0.0015/vuln | Manifest/lockfile audit with severity, fixes and recommended version | trust_screen_lead | $0.00079/email + $0.003/name + $0.007/call | Email check and sanctions screening in parallel, merged into a pass/review/block decision per lead | Paid tools run the public Fairquery Apify Actors (llm-data-guard-pii-secrets, email-validator-mx-disposable, sanctions-watchlist-screener, osv-dependency-vulnerability-check) with your APIFY_TOKEN, so usage is billed to your Apify account at the prices above. Every tool description has WHAT / WHEN / WHEN NOT / COST / RETURNS / EXAMPLE sections so the model can pick the right one, outputs are paged (limit, offset, nextOffset, hasMore), and errors use named codes. ## How to install it Requires Node.js 20 or later. Leave out APIFY_TOKEN if you only want the free tools. ### Claude Desktop (claude_desktop_config.json) ``` { "mcpServers": { "fairquery-trust": { "command": "npx", "args": ["-y", "@fairquery/agent-trust-mcp"], "env": { "APIFY_TOKEN": "" } } } } ``` ### Cursor (~/.cursor/mcp.json or .cursor/mcp.json) ``` { "mcpServers": { "fairquery-trust": { "command": "npx", "args": ["-y", "@fairquery/agent-trust-mcp"], "env": { "APIFY_TOKEN": "" } } } } ``` ### VS Code (.vscode/mcp.json) ``` { "inputs": [{ "type": "promptString", "id": "apify-token", "description": "Apify token (optional, paid tools only)", "password": true }], "servers": { "fairquery-trust": { "type": "stdio", "command": "npx", "args": ["-y", "@fairquery/agent-trust-mcp"], "env": { "APIFY_TOKEN": "${input:apify-token}" } } } } ``` Other ways to run it: npx -y github:fairquery/fairquery-agent-trust-mcp (straight from GitHub), node dist/index.js --http from a clone for Streamable HTTP on http://127.0.0.1:3000/mcp, or the Docker image built from the repo's Dockerfile. ## Real outputs from the free tools We connected to version 1.0.3 from npm with the MCP SDK client (no APIFY_TOKEN set) on October 17, 2026. tools/list returned the 12 tools above. Some calls: ### pii_redact_text Input: Ticket #4411 from Jane Doe , phone +1 415-555-0132. Card 4111 1111 1111 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE (test card number, AWS documentation example key). Returned in 5 ms: ``` { "meta": { "engine": "local", "redaction_style": "placeholder", "placeholder_map": { "[EMAIL_1]": "jane.doe@example.com", "[PHONE_1]": "+1 415-555-0132", "[CREDIT_CARD_1]": "4111 1111 1111 1111", "[AWS_ACCESS_KEY_1]": "AKIAIOSFODNN7EXAMPLE" } }, "items": [ { "type": "redacted_document", "finding_count": 4, "risk_level": "critical", "output_text": "Ticket #4411 from Jane Doe <[EMAIL_1]>, phone [PHONE_1]. Card [CREDIT_CARD_1] was declined. Debug log: AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_1]" } ] } ``` ### email_check_syntax Input ["jane@acme.com", "bob@gmial.com", "info@mailinator.com"], abridged to the key fields. No DNS lookups are done by the free tool, hence DNS_NOT_CHECKED; use email_validate_deliverability for MX/SPF/DMARC. ``` [ { "email": "jane@acme.com", "verdict": "unknown", "reasons": [ "DNS_NOT_CHECKED" ], "did_you_mean": null, "is_disposable": false, "is_role_account": false }, { "email": "bob@gmial.com", "verdict": "risky", "reasons": [ "POSSIBLE_TYPO", "DNS_NOT_CHECKED" ], "did_you_mean": "bob@gmail.com", "is_disposable": false, "is_role_account": false }, { "email": "info@mailinator.com", "verdict": "risky", "reasons": [ "DISPOSABLE_DOMAIN", "ROLE_ACCOUNT", "DNS_NOT_CHECKED" ], "did_you_mean": null, "is_disposable": true, "is_role_account": true } ] ``` ### osv_query_packages ``` { "meta": { "vulnerable_count": 2, "source": "https://api.osv.dev/v1/querybatch" }, "items": [ { "ecosystem": "npm", "name": "lodash", "version": "4.17.20", "vulnerable": true, "vuln_count": 5, "vuln_ids": [ "GHSA-29mw-wpgm-hmr9", "GHSA-35jh-r3h4-6jhm", "GHSA-f23m-r3pf-42rh", "GHSA-r5fr-rjxr-66jc", "GHSA-xxjr-mmjv-4gpg" ] }, { "ecosystem": "PyPI", "name": "django", "version": "3.2.0", "vulnerable": true, "vuln_count": 66, "vuln_ids": [ "GHSA-2gwj-7jmv-h26r", "GHSA-2hrw-hx67-34x6", "GHSA-3h9f-r86x-qvjx", "… 63 more" ] }, { "ecosystem": "npm", "name": "express", "version": "4.21.2", "vulnerable": false, "vuln_count": 0, "vuln_ids": [] } ] } ``` ### A paid tool without a token Calling sanctions_screen_names without APIFY_TOKEN does not fail silently or spend anything; it returns a named error: ``` { "error": "APIFY_TOKEN_MISSING", "message": "This is a paid tool. Set APIFY_TOKEN to YOUR OWN Apify API token (https://console.apify.com/settings/integrations); usage is billed to your Apify account. Free tools work without a token.", "retryable": false } ``` With a token, the paid tools return the same rows as the underlying Actors; see the real runs in our guides on sanctions screening, PII redaction and OSV checks in CI. ## Example prompts - "Redact this support ticket, then summarise it." (pii_redact_text) - "Is lodash@4.17.20 vulnerable? Show the advisories." (osv_query_packages, then osv_get_vulnerability) - "Check these signup emails for typos and throwaway domains." (email_check_syntax) - "Screen these 20 new suppliers and tell me which need review." (trust_screen_lead or sanctions_screen_names, paid) ## Security and privacy The free tools never contact Apify or Fairquery; PII tools make no network calls at all, and the OSV tools call only api.osv.dev. The code contains no secrets, and your token is read only from your own environment. The source is MIT-licensed on GitHub, with a CI workflow and a SECURITY.md. ## How it compares with alternatives Option | Good for | Trade-offs | Apify MCP server (mcp.apify.com) | Calling any Apify Actor, including all Fairquery Actors, from an agent | Generic remote Actor interface; Actor runs are billed per run and there are no offline PII or email tools | Single-purpose Fairquery MCP servers (in each Actor's GitHub repo) | Exposing just one capability | Separate servers to configure for each job | Building your own tools | Full control | You maintain detectors, list parsers, OSV handling and tool descriptions yourself | Fairquery Agent Trust MCP | Four common safety checks in one server, free local tier, paid depth on demand | Pattern-based PII detection; sanctions screening is not a certified service; no SMTP mailbox probing | ## FAQ ### What is the easiest way to add PII redaction and safety checks to Claude Desktop or Cursor? Add @fairquery/agent-trust-mcp to your MCP config with command 'npx' and args ['-y', '@fairquery/agent-trust-mcp']. Its free tools work without an account; set APIFY_TOKEN only for the paid tools. ### Which tools are free? Seven: pii_list_detectors, pii_scan_text, pii_redact_text, email_check_syntax and sanctions_list_sources run locally; osv_query_packages and osv_get_vulnerability call the free OSV.dev API directly. ### What do the paid tools cost? pii_guard_documents $0.0015/document + $0.0002/finding; email_validate_deliverability $0.00079/email; sanctions_screen_names $0.003/name + $0.007/call; osv_audit_dependencies $0.0005/package + $0.0015/vulnerability; trust_screen_lead $0.00079 per lead with an email + $0.003 per lead with a name (+ $0.007 per call when any lead has a name). They are billed to your own Apify account. ### Is it in the official MCP Registry? Yes, as io.github.fairquery/fairquery-agent-trust-mcp (npm package @fairquery/agent-trust-mcp, stdio transport). It is also listed on Glama. ### Does my data leave my machine? Not with the free PII and email tools; they make no network calls. The free OSV tools call only api.osv.dev. Paid tools send their input to the corresponding Fairquery Apify Actor run in your Apify account. ### What happens if the agent calls a paid tool without a token? It gets a named APIFY_TOKEN_MISSING error and nothing is charged. Disclaimer: sanctions results come from automated fuzzy matching and are not a certified screening service or a legal determination; review every potential match yourself. Email checks do not probe SMTP mailboxes. PII detection is pattern-based and can miss data. Links: npm: @fairquery/agent-trust-mcp · GitHub · MCP Registry entry · Glama · Fairquery on Apify · all Fairquery tools --- # How to validate email addresses via API: MX, disposable and typo checks URL: https://fairquery.github.io/posts/validate-email-addresses-api-mx-disposable-typo/ Published: 2026-10-17 Short answer: to validate an email address via API without sending mail, run four checks: (1) syntax, (2) DNS: does the domain exist and publish MX records (and not a "null MX"), (3) risk flags: disposable domain, role account (info@, sales@), free webmail, and (4) typo detection on the domain (gmial.com → gmail.com). The Fairquery Email Validator does all of this in one call for $0.00079 per email on Apify, and is also available on RapidAPI. It returns a valid / risky / invalid / unknown verdict with reason codes. ## What "email validation" can and cannot tell you Email validation is about answering one practical question before you store or email an address: can this address plausibly receive mail, and is it worth keeping? There are two families of techniques: - DNS-based checks (what this post covers): look at the address and its domain. Is it well-formed? Does the domain exist? Does it publish mail servers? Is it a known throwaway domain? These checks are fast, cheap, and never touch the recipient's mailbox. - SMTP probing: connecting to the recipient's mail server and asking whether a mailbox exists without delivering a message. It can confirm more, but many providers deliberately give ambiguous answers ("catch-all" domains accept everything), and probing at volume can damage your IP reputation and conflict with provider terms. The Fairquery validator deliberately does not probe SMTP. So valid means "well-formed, deliverable domain, no risk flags", not "mailbox confirmed". That is an honest limit of every DNS-only validator, and you should design your workflow around it (for example, combine it with a double opt-in confirmation email for signups). ## The checks, one by one ### 1. Syntax The full RFC 5322 grammar allows strange things (quoted local parts, comments) that almost no real mailbox uses. A pragmatic validator accepts the RFC 5321/5322 subset people actually use and converts internationalised domains (IDN) to punycode before looking them up. Regex-only validation stops here, which is why it lets jane@acme.con through. ### 2. Domain and MX records Mail is delivered to the hosts listed in the domain's MX records. If the domain doesn't exist (DOMAIN_NOT_FOUND) or has no mail server at all (NO_MAIL_SERVER), mail will bounce. Two subtleties: - Null MX (RFC 7505): a domain can publish an MX record of . to say "I never accept mail". That is a definite invalid (NULL_MX). - Implicit MX: with no MX record, senders fall back to the domain's A/AAAA record. Mail may work, but it is unusual for real mail domains, so it is flagged IMPLICIT_MX_ONLY (risky) rather than passed silently. ### 3. Disposable, role and free-provider flags - Disposable domains (temporary inboxes) are a common source of fake signups and trial abuse. The validator uses a bundled CC0 list from the open-source disposable-email-domains project (9,800+ domains, subdomains matched). No list is complete; new throwaway domains appear constantly. - Role accounts like info@, sales@, support@ reach a team or a shared inbox, not a person. Fine for some uses, undesirable for others; set allowRoleAccounts: false to mark them risky. - Free webmail (Gmail, Outlook.com, Yahoo and so on) is normal for consumers but often a signal on B2B lead forms. Set allowFreeProviders: false for B2B lists. ### 4. Typo suggestions A surprising number of bad addresses are simple domain typos: gmial.com, hotmial.com, outlok.com. The validator compares the domain against popular providers by edit distance and returns a did_you_mean suggestion. The best place to use this is on the signup form itself ("Did you mean jane@gmail.com?"), before the bad address ever reaches your database. ### Bonus: mail provider, SPF and DMARC The response also reports the detected mail provider (for example Google Workspace or Microsoft 365) and whether the domain publishes SPF and DMARC, including the DMARC policy. That's useful context for lead qualification and for spotting domains that are set up for real business mail. ## Free options - Do it yourself. A syntax check plus a DNS MX lookup is a few lines in most languages (for example dig MX example.com, Node's dns.resolveMx, or Python's dnspython). Add the free disposable-email-domains list and you have a basic validator. You'll maintain the typo logic, role list, caching and edge cases (null MX, IDN) yourself. - Fairquery's free offline tool. The open-source repo (MIT) ships a local MCP server with a free check_email_syntax tool that runs offline without a token. ## API option: Fairquery Email Validator Inputs (all optional except emails): Field | Default | Meaning | emails | – | Addresses; entries can be comma/semicolon/newline lists or Name | checkDns | true | MX/A lookups (off = syntax only) | checkSpfDmarc | true | Report SPF/DMARC and DMARC policy | allowRoleAccounts | true | If false, role addresses become risky | allowDisposable | false | If true, disposable domains are not risky | allowFreeProviders | true | If false, free webmail becomes risky | maxEmails | 10000 | Safety cap per run | ### curl Run the Actor synchronously and get results back in one request (replace $APIFY_TOKEN with your Apify API token): ``` curl -X POST \ "https://api.apify.com/v2/acts/fairquery~email-validator-mx-disposable/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"emails": ["jane@acme.com", "info@apify.com", "bob@gmial.com"], "allowFreeProviders": false}' ``` ### JavaScript (Node.js) ``` import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('fairquery/email-validator-mx-disposable').call({ emails: ['jane@acme.com', 'bob@gmial.com'], allowRoleAccounts: false, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); for (const r of items.filter((i) => i.type === 'email')) { console.log(r.email, r.verdict, r.reasons, r.did_you_mean ?? ''); } ``` ### Python ``` import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("fairquery/email-validator-mx-disposable").call( run_input={"emails": ["jane@acme.com", "bob@gmial.com"], "allowFreeProviders": False} ) for r in client.dataset(run["defaultDatasetId"]).iterate_items(): if r["type"] == "email": print(r["email"], r["verdict"], r["reasons"], r.get("did_you_mean")) ``` Prefer RapidAPI's key-based billing and code snippets? Use the RapidAPI listing; its endpoint docs and playground show request formats for many languages. ### What comes back Each address gets one row. Abridged shape (field names are real; values depend on live DNS at check time): ``` { "type": "email", "email": "bob@gmial.com", "domain": "gmial.com", "verdict": "risky", "reasons": ["POSSIBLE_TYPO", "..."], "syntax_valid": true, "is_disposable": false, "is_role_account": false, "is_free_provider": false, "did_you_mean": "bob@gmail.com", "domain_exists": true, "has_mx": false, "null_mx": false, "implicit_mx": true, "mx_records": [], "mail_provider": null, "has_spf": false, "has_dmarc": false, "dmarc_policy": null, "source_url": "https://cloudflare-dns.com/dns-query?name=gmial.com&type=MX", "checked_at": "..." } ``` Reason codes: INVALID_SYNTAX, DOMAIN_NOT_FOUND, NULL_MX, NO_MAIL_SERVER (→ invalid); DISPOSABLE_DOMAIN, ROLE_ACCOUNT, FREE_PROVIDER, POSSIBLE_TYPO, IMPLICIT_MX_ONLY (→ risky, depending on the allow flags); DNS_NOT_CHECKED (→ unknown). If a DNS lookup fails you get an error row (DNS_LOOKUP_FAILED, with retryable), and error rows are never charged. Duplicates (case-insensitive) are skipped. ### Using it from an AI agent Agents can call the Actor through Apify MCP with {"actor": "fairquery/email-validator-mx-disposable", "input": {"emails": ["jane@acme.com"]}}, or run the stdio MCP server from the GitHub repo (validate_emails, plus the free check_email_syntax). ## Pricing $0.00079 per email checked on Apify (pay per event; error rows are free). DNS lookups are cached per domain, so lists with many addresses at the same company are efficient. RapidAPI pricing is shown on the listing. ## Practical recommendations - Signup forms: reject invalid, show did_you_mean inline, and block or challenge disposable domains. Still send a confirmation email. - B2B lead lists: set allowFreeProviders: false and allowRoleAccounts: false; review risky rows rather than deleting them blindly. - Old CRM data: re-check periodically; domains expire and companies change mail setups. - Compliance: validation is not consent. Follow CAN-SPAM, GDPR/ePrivacy and similar rules when emailing. ## FAQ ### How do I check if an email address is valid without sending an email? Check the syntax, then look up the domain's DNS: it must exist and publish MX records (and not a null MX). Add disposable-domain, role-account and typo checks. The Fairquery Email Validator does all of this via API without sending mail or probing SMTP. ### Can an API confirm that a mailbox really exists? Not reliably without SMTP probing, and even then catch-all domains accept everything. The Fairquery validator does not probe SMTP; 'valid' means well-formed, deliverable domain and no risk flags, not mailbox confirmed. ### How do I detect disposable email addresses? Match the domain (including subdomains) against a maintained disposable-domain list. Fairquery uses the CC0 disposable-email-domains list (9,800+ domains) and returns is_disposable plus a DISPOSABLE_DOMAIN reason code. ### What is a null MX record? An MX record pointing to '.' (RFC 7505) that says the domain never accepts email. Addresses at such domains are invalid. ### How much does the Fairquery Email Validator cost? $0.00079 per email checked on Apify, pay per event. Error rows are never charged. It is also listed on RapidAPI. ### Can AI agents use it? Yes. Call Actor fairquery/email-validator-mx-disposable through Apify MCP, or run the stdio MCP server from the GitHub repo, which exposes validate_emails and a free offline check_email_syntax tool. Honest limits: DNS-based validation cannot guarantee a mailbox exists or accepts mail. Use results to reduce bounces and filter obvious junk, not as proof of deliverability. Source and docs: github.com/fairquery/email-validator-mx-disposable · Apify Store · RapidAPI · all Fairquery tools