# Brand Name IP Screener (`protocol/brand-name-ip-screener`) Actor

Screens brand names vs curated famous-mark seed datasets (not full registry) for trademark-collision risk across FR/US/EU/WO/UK — visual + phonetic similarity scoped to Nice classes, with a domain co-check. Clear/caution/high-risk per jurisdiction. Informational only — not legal advice.

- **URL**: https://apify.com/protocol/brand-name-ip-screener.md
- **Developed by:** [Protocol](https://apify.com/protocol) (community)
- **Categories:** AI, Developer tools, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1,000.00 / 1,000 name screened in one jurisdictions

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Brand Name IP Screener

Generate brand-name candidates from a positioning brief, or screen names you already have, for
trademark-collision risk — with **France (INPI) and the EU (EUIPO)** covered first, plus the
US (USPTO), UK (UKIPO), and international (WIPO) — then get a domain-availability check and a
collision-risk verdict in one run — `clear` (no collision found among the marks screened),
`caution`, or `high-risk`. Each jurisdiction is screened against a **curated famous-mark seed
dataset, not full registry coverage** — this is a pre-filing risk triage, not a substitute for a
full-registry clearance search. Unlike a domain checker (which only
asks "is the URL free?"), this asks the harder question a trademark examiner asks: **is this name
confusingly similar — including phonetically (sounds-alike) — to an existing registered mark in my
class of goods/services?**

- **Generate + screen + rank in one run** — not just an LLM prompt, not just a trademark search.
- **5 jurisdictions:** France (INPI) and the EU (EUIPO) first, then US (USPTO), UK (UKIPO), and
  International (WIPO) — each screened against a curated dataset of well-known, real registered marks.
- **Visual + phonetic similarity**, the way examiners assess likelihood of confusion — not just
  spelling. `Teslaa` is correctly flagged against `Tesla` even though it isn't a substring match.
- **Domain co-check** — probes `.com`, `.io`, `.ai`, `.dev` via RDAP in the same run.
- **Agent-ready:** flat CSV-friendly dataset plus a structured `OUTPUT` record per name, each
  carrying a mandatory "not legal advice" disclaimer.

> **Informational screening only — not legal advice.** Registry-free ≠ trademark-free ≠
> right-to-use. Clearance requires a qualified trademark attorney. This disclaimer ships on every
> result.

***

### Quick start

1. [Create a free Apify account](https://console.apify.com/sign-up) (or sign in).
2. Open this Actor's page and go to the **Input** tab.
3. Either add at least one name under `names` (**screen mode**), or fill in `positioning`
   (**generate mode**) — exactly one of the two is required.
4. Click **Start** and wait a few seconds.
5. Open the **Storage** tab to view the per-jurisdiction dataset, or read the `OUTPUT` key-value
   record for the full structured verdict per name.

***

### What this Actor does

**Screen mode** (`names` set):

1. **Normalizes** each name (lowercase, diacritics folded, legal suffixes stripped) to its
   distinctive core.
2. **Screens** it against every selected jurisdiction's curated mark dataset, scoring visual
   similarity (Damerau-Levenshtein) and phonetic similarity (Double Metaphone, English + French)
   in parallel and fusing the two into a 0–100 `ipRiskScore`.
3. **Scopes to Nice classes** — your goods/services classes 1–45, either supplied directly or
   inferred from a free-text `industry` field.
4. **Co-checks domain availability** via RDAP across 5 TLDs (optional, on by default).
5. Writes a flat dataset (one row per name × jurisdiction) plus a structured `OUTPUT` record per
   name with an overall `brandReadiness` verdict and a human-readable recommendation.

**Generate mode** (`positioning` set instead of `names`) does all of the above, plus runs it as a
closed loop:

1. **Generates** candidate names from your positioning brief via an LLM (DeepSeek primary,
   GPT-4o-mini fallback via OpenRouter).
2. **Filters** them deterministically — profanity, banned words, duplicates, and your
   `mustInclude`/`avoid` constraints (enforced after generation, not just asked of the model) —
   entirely locally, zero network calls for this stage.
3. **Screens** every survivor through the exact same pipeline as screen mode above.
4. **Ranks** them by a composite score: naming quality × (1 − ipRisk) × domain availability — so
   the names at the top of the list are good *and* show low collision risk in the screened seed *and* are actually available.

***

### Why use it

- **Catch what a spelling check misses.** Phonetic matching flags sounds-alike collisions
  (`Brandcraft` vs `Brandkraft`) that a substring or Levenshtein-only check would pass clean.
- **Don't waste a clearance search on a taken domain.** The RDAP co-check runs in the same pass.
- **Skip the false sense of security.** Jurisdictions with no enabled data source report
  `not_screened`, and every `clear` carries a `marksScreened` count so a weak `clear` can't pass
  for a strong one — this Actor never returns a false `clear`.
- **Generate names that are actually usable**, not just names that sound good — generate mode
  ranks by ownability, not just LLM creativity.

***

### How this compares

This Actor screens brand names against a **curated seed of well-known, real registered marks** per
jurisdiction (FR/US/EU/WO/UK), scores visual + phonetic similarity scoped to your Nice classes,
co-checks domain availability via RDAP, and returns a `clear` / `caution` / `high-risk` verdict in
seconds. It is **informational screening, not legal advice** — read every verdict as "no collision
found among the marks screened," never as "trademark-free" or "safe to launch."

| Option | What you get | Cost / speed | Honest limits |
| --- | --- | --- | --- |
| **This Actor** | Automated, scored screening across 5 jurisdictions, visual + phonetic, Nice-class-scoped, plus RDAP domain co-check, in one run. | Seconds; ~$1/name×jurisdiction screened (generate mode is **free**). | Curated famous-mark seed, **not the full registry**. "No collision found" means among the marks screened — a real register may hold a conflicting mark outside the sample. Informational only; not a clearance opinion. |
| **$150–$300 paralegal / trademark-attorney clearance** | An authoritative, jurisdictionally complete clearance opinion from a qualified attorney who searches the full register, weighs likelihood of confusion in light of case law, and advises on filing strategy. | $150–$300+; hours to days. | Slower and more expensive. Authoritative where this Actor is probabilistic — the right step before you file or invest. |
| **Manual searching of EUIPO eSearch Plus / TMview / USPTO / INPI** | Free, direct access to the live registries. | Free; slow — minutes-to-hours per name per jurisdiction, and you do all the scoring yourself. | Un-scored: you eyeball similarity and easily miss visual/phonetic near-misses (`Teslaa` vs `Tesla`, `Brandkraft` vs `Brandcraft`). No Nice-class scoping unless you filter by hand. No domain co-check. Easy to conclude "no match found" when a sounds-alike collision is one tab over. |
| **Doing nothing (blind launch)** | Nothing. | $0 upfront. | Highest downstream risk: a cease-and-desist, opposition, or rebrand after you've already invested in marketing, inventory, or filings. |

#### When to pick what

- **Use this Actor first** to triage a long list cheaply — drop obvious collisions, rank what's left,
  and find out which names are even worth paying a human to clear. A `high-risk` verdict here is a
  strong signal to drop or rename; a `clear` is "no collision found in the curated seed," not a
  green light.
- **Engage a qualified trademark attorney** before you file, invest, or launch — especially for any
  name this Actor scored `caution` or `high-risk`, and for any name you can't afford to rebrand
  later. The attorney's clearance is the authoritative step this Actor deliberately does not
  replace (see the `attorneyReview` flag on every result).
- **Manual registry searching** is free and worth doing as a second opinion on a shortlisted name,
  but it doesn't replace scored similarity (you'll miss phonetic near-misses) and it doesn't scale
  past a name or two.
- **Doing nothing** is the only option that costs nothing upfront and exposes you to the full cost
  of a collision discovered after launch.

> **Informational screening only — not legal advice.** "Low collision risk found in the curated
> seed" and "no collision found among the marks screened" are probabilistic observations from a
> famous-mark sample, not a guarantee of right-to-use. Clearance requires a qualified trademark
> attorney.

***

### Complete the funnel

A name that screens with low collision risk still needs handles. Once this Actor ranks your
candidates, check whether the matching usernames are free across GitHub, X, Instagram, Threads,
TikTok, Twitch, and YouTube with the sibling [**AI Username Generator & Availability
Checker**](https://apify.com/Protocol/username-generator-checker) Actor
(`Protocol/username-generator-checker`) — the next step in the same naming workflow. Generate
there, or validate a name you already have, and finish the
name → trademark screen → handle + domain check loop in two paid runs.

> Same "not legal advice" boundary: handle availability is a separate signal from trademark
> right-to-use. Both still require a qualified attorney's clearance before you file.

***

### Pricing

This Actor is billed on **pay-per-event (PPE)** — you pay for value milestones, not compute time.
There is no rental fee and no subscription; you only pay for the billable events below.

| Event                                | When it fires                                                                                                                                                                                                                 | Price |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| `screening_report_produced`          | **Screen mode** — one name fully screened across all your selected jurisdictions, with the structured `OUTPUT` verdict written. Billed **per name × jurisdiction** that returns `screened` (i.e. a real collision check ran). | $1.00 |
| `candidate_generated_and_rescreened` | **Generate mode** — one candidate that survived the deterministic filter (profanity / banned-word / duplicate) and was screened through the full IP + RDAP pipeline.                                                          | **$0.00** |

**What you never pay for:**

- **Generate mode — it is free.** Name generation and the full IP + RDAP screening of every
  generated candidate cost nothing, in any number of jurisdictions (see below).
- Jurisdiction rows that report `not_screened` (no enabled data source) — never billed, never a false `clear`.
- Candidates in generate mode that the Tier-1 filter rejected — they're listed under `OUTPUT.filtered` at no charge.
- Compute time, retries, or LLM fallback attempts that don't produce a result.

**Worked examples** (launch pricing, set in the Apify Console; 14 days' notice is given before any change, max once per month):

- Screen 3 names across `["FR", "US", "EU"]` → 3 × 3 = 9 screened rows → **$9.00**.
- Generate mode, `count: 10`, 2 candidates filtered out → 8 candidates generated + rescreened → **$0.00**.
- Screen 1 name across 5 jurisdictions where `UK` has no enabled source → 4 screened rows + 1 `not_screened` → billed for 4 → **$4.00**.

> **Generate mode is free.** `candidate_generated_and_rescreened` is priced at **$0.00**, no
> matter how many candidates you generate or how many jurisdictions you screen them across.
> Generate-mode runs supply their own LLM key (or provider subscription) via `llmApiKey`, so this
> Actor carries no inference cost to pass on. The event still fires at $0 so generate-mode usage
> stays visible in your run's charge breakdown. Pricing may change in a future version
> (14 days' notice, as above).

> Clearance-grade screening at a fraction of a $200+ paralegal search. Pricing is indicative at
> launch and finalized in the Console before publication.

> **Set "Maximum cost per run" high enough for the whole batch.** Screen mode costs
> `$1.00 × names × jurisdictions`, so 2 names × 5 jurisdictions is **$10.00**. If the run's cost
> cap is reached mid-batch, the Actor stops screening the remaining names and reports them as
> `not_screened` with reason `budget_exhausted` — it never silently returns a `clear` it did not
> verify, and the unscreened names are never charged. That is the safe failure mode, but it does
> mean a cap set below the batch total gives you a partial report. Either raise the cap (or leave
> it unlimited) or split the batch.

***

### Supported jurisdictions

| Jurisdiction | Registry             | Coverage                                                                                                                                                                                                                        |
| ------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FR`         | INPI (France)        | Curated seed dataset of 163 well-known, real registered marks (software/SaaS, e-commerce, finance, food/beverage, fashion, cosmetics, automotive, media, telecom, transport, pharma, furniture, education, real estate, travel) |
| `US`         | USPTO                | Curated seed dataset of 203 well-known, real registered marks (tech, fintech, retail, food/beverage, telecom, pharma, automotive, media, hospitality, education)                                                                                                         |
| `EU`         | EUIPO                | Curated seed dataset of 201 well-known, real registered marks (EU-headquartered companies + global brands' EU Trade Mark registrations across tech, automotive, luxury/fashion, food/beverage, finance, pharma, retail, travel, media)                                                                                                                                  |
| `WO`         | WIPO (Madrid System) | Curated seed dataset of 321 well-known marks, mostly Asia-headquartered multinationals (electronics, automotive, retail, internet services, finance, industrial)                                                                                        |
| `UK`         | UKIPO                | Curated seed dataset of 300 well-known, real registered marks (banking/finance, retail/fashion, telecom/media, travel/hospitality, tech, energy/industrial, pharma)                                                                                                      |

> **"Curated seed dataset" means famous-mark collision screening, not full registry coverage.**
> Each registry holds millions of marks; this Actor ships a hand-picked sample of well-known ones
> per jurisdiction, real and currently registered. Full bulk ingestion (INPI ~3.1 GB on
> data.gouv.fr; USPTO bulk XML/TSV since TSDR is status-by-number only; EUIPO eSearch Plus OAuth2;
> WIPO Global Brand Database; UKIPO bulk data / TMview) is a larger, separate undertaking. A
> jurisdiction in `jurisdictions` with no enabled source reports `screeningStatus: "not_screened"`
> — never a false `clear`.

***

### Input

Exactly one of `names` / `positioning` is required.

| Field               | Type             | Default  | Description                                                                                                                          |
| ------------------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `names`             | `string[]`       | —        | Proposed names to screen (≤20). Leave empty and set `positioning` instead to generate names first.                                   |
| `jurisdictions`     | `string[]`       | `["FR"]` | Any of `FR`, `US`, `EU`, `WO`, `UK`.                                                                                                 |
| `niceClasses`       | `string[]`       | `[]`     | Your goods/services classes 1–45. Recommended — scoping removes noise. Leave empty (with `industry` set) to auto-infer.              |
| `industry`          | `string`         | —        | Free-text sector, e.g. `"fintech"`. Drives Nice-class inference (when `niceClasses` is empty) and the human-readable recommendation. |
| `language`          | `"en"` | `"fr"` | `"en"`   | Drives the phonetic encoder.                                                                                                         |
| `riskThreshold`     | `integer`        | `60`     | Minimum similarity (0–100) for a mark to be listed in the output.                                                                    |
| `enableDomainCheck` | `boolean`        | `true`   | Runs the RDAP co-check across `.com`/`.io`/`.ai`/`.dev`.                                                                       |

#### Generation-mode fields (only used when `positioning` is set)

| Field                   | Type       | Default        | Description                                                                                                           |
| ----------------------- | ---------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
| `positioning`           | `string`   | —              | Free-text brand positioning / value proposition. Setting this (with `names` empty) switches the run to generate mode. |
| `tone`                  | enum       | `Professional` | One of `Professional`, `Playful`, `Premium`, `Technical`, `Friendly`.                                                 |
| `techniques`            | `string[]` | `[]`           | Naming techniques to bias toward, e.g. `["compound", "invented"]`. Empty = let the model choose.                      |
| `mustInclude` / `avoid` | `string[]` | `[]`           | Fragments every generated name must contain at least one of / must never contain. Enforced deterministically after generation (not just asked of the model); rejects are free. |
| `count`                 | `integer`  | `10`           | How many names to generate before filtering (hard cap 20).                                                            |
| `maxLength`             | `integer`  | `18`           | Max generated-name length, excluding spaces (hard cap 30).                                                            |
| `bannedWords`           | `string[]` | `[]`           | Extra words to reject beyond the built-in profanity list.                                                             |

##### Bring-your-own LLM (v1.8.0)

Generate mode needs an LLM key. There are two ways to supply one — **per-run
user overrides win, the operator env var is the fallback:**

| Field         | Type     | Default          | Description                                                                                                                                                                                                  |
| ------------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `llmProvider` | enum     | `openai`         | `openai` = any OpenAI-compatible endpoint (OpenRouter, OpenAI, Groq, Ollama, LM Studio). `anthropic` = native Claude Messages API.                                                                           |
| `llmApiKey`   | `string` | —                | Your own provider API key (marked **secret** — never logged). Overrides the operator's `OPENROUTER_API_KEY`.                                                                                                 |
| `llmBaseUrl`  | `string` | provider default | Provider endpoint, HTTPS (plain HTTP only for localhost). Overrides `OPENROUTER_BASE_URL`.                                                                                                                   |
| `llmModel`    | `string` | operator default | Single model id used for both primary and fallback. Examples: `anthropic/claude-3.5-sonnet` (OpenRouter), `llama-3.3-70b-versatile` (Groq), `gpt-4o-mini` (OpenAI), `claude-haiku-4-5-20251001` (Anthropic). |

If you supply `llmApiKey`, you pay your own LLM provider directly. If you leave
these blank, the run uses the operator-configured `OPENROUTER_API_KEY` (the Actor
owner's) — **but only on the default OpenRouter endpoint with the default
`openai` provider.** Setting `llmBaseUrl` to any non-default endpoint, or
`llmProvider` to `anthropic`, **requires your own `llmApiKey`**: the operator's
key is never transmitted to a custom endpoint or a non-default provider (key
custody). A run that omits it fails fast with `ERR_MISSING_API_KEY` — it never
silently falls back to the operator key. Screen-only runs (`names` set) never
read any key.

**Provider examples** (set as input, or as the operator env vars):

```jsonc
// Groq (fast + cheap) — OpenAI-compatible
{ "llmProvider": "openai", "llmApiKey": "gsk_…", "llmBaseUrl": "https://api.groq.com/openai/v1/chat/completions", "llmModel": "llama-3.3-70b-versatile" }

// OpenAI direct
{ "llmProvider": "openai", "llmApiKey": "sk-…", "llmBaseUrl": "https://api.openai.com/v1/chat/completions", "llmModel": "gpt-4o-mini" }

// Anthropic (native Claude API) — no OpenRouter middleman
{ "llmProvider": "anthropic", "llmApiKey": "sk-ant-…", "llmModel": "claude-haiku-4-5-20251001" }

// OpenRouter (operator default) — leave llm* blank, or set your own OpenRouter key
{ "llmProvider": "openai", "llmApiKey": "sk-or-…", "llmModel": "anthropic/claude-3.5-sonnet" }
```

> If neither a user `llmApiKey` nor the operator `OPENROUTER_API_KEY` is set,
> generate mode fails fast with `ERR_MISSING_API_KEY` — it never silently falls
> back to a weaker model.

#### Example input — screen mode

```json
{
    "names": ["Teslaa", "Acme"],
    "jurisdictions": ["FR", "US", "EU"],
    "niceClasses": ["9", "12", "42"]
}
```

#### Example input — generate mode

```json
{
    "positioning": "a calmer way to manage freelance invoices",
    "industry": "fintech",
    "tone": "Professional",
    "count": 10
}
```

***

### Output

The Actor writes to two places:

1. **Dataset** — one row per **name × selected jurisdiction**.
2. **Key-value store `OUTPUT`** — `{ schemaVersion, runId, mode, records[], filtered[]? }`. This
   is the record AI agents should read first; `mode` is `"screen"` or `"generate"`, and `filtered`
   is only present in generate mode.

#### Dataset fields

| Field                               | Meaning                                                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `name` / `normalizedName`           | The screened name, and its normalized comparison form.                                                    |
| `jurisdiction`                      | `FR` / `US` / `EU` / `WO` / `UK`.                                                                         |
| `source`                            | Registry label, e.g. `"INPI"`.                                                                            |
| `screeningStatus`                   | `"screened"` or `"not_screened"` — never a false `clear`.                                                 |
| `risk`                              | `"clear"` / `"caution"` / `"high-risk"` / `"not_screened"`.                                               |
| `ipRiskScore`                       | 0–100 collision risk for this jurisdiction; higher = more likely to conflict.                             |
| `topSimilarMark` / `topSimilarity`  | The closest colliding mark and its 0–100 similarity score, or `null`.                                     |
| `similarMarksJson`                  | JSON-encoded array of all similar marks with their visual/phonetic breakdown.                             |
| `niceClasses` / `niceClassesSource` | Classes the screening was scoped to, and whether they were `user`-supplied or `inferred` from `industry`. |
| `marksScreened`                     | How many in-scope marks were actually scored for this row (coverage honesty). A `clear` over 3 marks ≠ a `clear` over 150. 0 when `not_screened`. |
| `notScreenedReason`                 | Typed reason when `screeningStatus` is `not_screened` (`no_source` / `name_not_latin_screenable` / `name_has_no_distinctive_core` / `budget_exhausted` / `source_error` / `confusable_script`); `null` when screened. |
| `runId`                             | Apify run ID for traceability.                                                                            |
| `disclaimer`                        | The mandatory "not legal advice" disclaimer.                                                              |

#### Structured `OUTPUT` record fields

Each item in `records[]` carries: `schemaVersion`, `name`, `normalizedName`, `brandReadiness`
(worst risk band across all screened jurisdictions), `ipRiskScore`, `recommendation`
(human-readable), `jurisdictions[]` (per-jurisdiction verdict + similar marks, each with
`marksScreened` — the in-scope mark count behind that verdict), `niceClassesSource`,
`matchedKeywords` (the industry keywords that matched when classes were inferred; empty when
user-supplied), `domainAvailability` (`{ tlds, results[], summary: { free, taken, error } }`),
`attorneyReview` (`{ recommended, message }` — informational escalation only, see
[FAQ](#faq)), `seedAsOf` (the YYYY-MM the curated famous-mark seeds were last
verified — the seeds are point-in-time, not a live registry), and `disclaimer`.

In **generate mode**, each record additionally carries a `generation` block (`technique`,
`brandFit`, `readability`, `memorability`, `compositeScore`), and `records[]` is sorted by
`compositeScore` descending. Candidates rejected by the Tier-1 filter (profanity / banned word /
duplicate / `missing_required_fragment` / `forbidden_fragment`) never reach the screen — they're
listed separately under `OUTPUT.filtered` as `{ name, normalizedName, invalidReason }`.

#### Example output — screen mode

Real run: `{ "names": ["Teslaa"], "jurisdictions": ["FR"] }` correctly flags a phonetic collision
with the real seed mark `Tesla` (visual similarity alone would score this lower; phonetics catch
it):

```json
{
    "schemaVersion": "1.6.0",
    "name": "Teslaa",
    "normalizedName": "teslaa",
    "brandReadiness": "high-risk",
    "ipRiskScore": 92,
    "recommendation": "High collision risk: \"Teslaa\" is phonetically close to \"Tesla\" (INPI, classes 12/9). Get trademark clearance before filing or investing in the name.",
    "jurisdictions": [
        {
            "jurisdiction": "FR",
            "source": "INPI",
            "screeningStatus": "screened",
            "risk": "high-risk",
            "ipRiskScore": 92,
            "marksScreened": 163,
            "similarMarks": [
                {
                    "mark": {
                        "mark": "Tesla",
                        "jurisdiction": "WO",
                        "niceClasses": [12, 9],
                        "status": "registered",
                        "owner": "Tesla, Inc."
                    },
                    "score": 92,
                    "similarity": { "visual": 83, "phonetic": 100 },
                    "signal": "phonetic"
                }
            ]
        }
    ],
    "niceClassesSource": "inferred",
    "matchedKeywords": ["automotive"],
    "domainAvailability": {
        "tlds": ["com", "io", "ai", "dev"],
        "results": [
            { "domain": "teslaa.com", "tld": "com", "status": "taken" },
            { "domain": "teslaa.io", "tld": "io", "status": "available" },
            { "domain": "teslaa.ai", "tld": "ai", "status": "available" },
            { "domain": "teslaa.dev", "tld": "dev", "status": "available" }
        ],
        "summary": { "free": 3, "taken": 1, "error": 0 }
    },
    "attorneyReview": {
        "recommended": true,
        "message": "High collision risk detected. We recommend engaging a qualified trademark attorney for a full clearance opinion before filing or investing further in this name. This screening is informational only and does not constitute legal advice."
    },
    "disclaimer": "Informational screening only — not legal advice. Registry-free ≠ trademark-free ≠ right-to-use. Clearance requires a qualified trademark attorney."
}
```

#### Example output — generate mode

Real run: `{ "positioning": "a calmer way to manage freelance invoices", "count": 2 }`. Note
`brewforge` outranks `alphavault` despite a lower `brandFit` — its higher `readability` and
`memorability` win out in the composite:

```json
{
    "schemaVersion": "1.6.0",
    "runId": "abc123",
    "mode": "generate",
    "records": [
        {
            "name": "brewforge",
            "normalizedName": "brewforge",
            "brandReadiness": "caution",
            "ipRiskScore": 42,
            "generation": {
                "technique": "compound",
                "brandFit": 90,
                "readability": 87,
                "memorability": 85,
                "compositeScore": 51
            }
        },
        {
            "name": "alphavault",
            "normalizedName": "alphavault",
            "brandReadiness": "caution",
            "ipRiskScore": 43,
            "generation": {
                "technique": "compound",
                "brandFit": 85,
                "readability": 88,
                "memorability": 80,
                "compositeScore": 48
            }
        }
    ],
    "filtered": [
        { "name": "AlphaVault", "normalizedName": "alphavault", "invalidReason": "duplicate" },
        { "name": "shitstorm", "normalizedName": "shitstorm", "invalidReason": "profanity" }
    ]
}
```

*(`jurisdictions`, `domainAvailability`, `niceClassesSource`, `attorneyReview`, and `disclaimer`
are present on every record exactly as in screen mode — omitted above for brevity.)*

***

### How it works

- **Visual similarity** — Damerau-Levenshtein edit distance on the normalized form (handles
  transpositions, not just insert/delete/substitute).
- **Phonetic similarity** — Double Metaphone for English; a French pre-processing pass (silent
  finals, nasal vowels, `ph`/`ch`/`qu` digraphs) feeding the same encoder for French. Each name
  gets a `[primary, secondary]` code pair; the max-of-pairs comparison means a partial match in
  either code still scores above zero.
- **Fusion** — visual and phonetic scores are combined into one `ipRiskScore`, scoped to the
  requested Nice classes, with banding into `clear` (no collision found in the screened seed), `caution`, or `high-risk`.
- **Domain check** — RDAP (the IETF successor to WHOIS) queried directly per TLD, bounded
  concurrency, with a timeout; network failures degrade to `error` entries and never throw.
- **Generation** — an OpenRouter-routed LLM (DeepSeek primary, GPT-4o-mini fallback, one retry on
  malformed JSON before falling back, schema-validated output) proposes names against your
  positioning brief; everything downstream runs through the same deterministic filter and
  screening pipeline as screen mode.

***

### Usage via API

Run the Actor programmatically with the Apify API client. Use
`Protocol/brand-name-ip-screener` as this Actor's ID, and supply your
[Apify API token](https://console.apify.com/account/integrations).

#### Python

```bash
pip install apify-client
```

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_API_TOKEN>")

run_input = {
    "names": ["Teslaa", "Acme"],
    "jurisdictions": ["FR", "US", "EU"],
    "industry": "fintech",
}

run = client.actor("Protocol/brand-name-ip-screener").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["name"], item["jurisdiction"], item["risk"], item["ipRiskScore"])

output = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")
print(output["value"])
```

#### JavaScript / TypeScript

```bash
npm install apify-client
```

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_API_TOKEN>' });

const run = await client.actor('Protocol/brand-name-ip-screener').call({
    positioning: 'a calmer way to manage freelance invoices',
    industry: 'fintech',
    count: 10,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.table(items.map((i) => ({ name: i.name, jurisdiction: i.jurisdiction, risk: i.risk })));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/Protocol~brand-name-ip-screener/run-sync-get-dataset-items?token=<YOUR_APIFY_API_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{ "names": ["Brandcraft"], "jurisdictions": ["FR"] }'
```

***

### Use with AI agents (MCP)

The Actor is built to be driven by AI agents. `INPUT_SCHEMA.json` exposes typed fields with enum
dropdowns and defaults, so an MCP client (e.g. Claude Desktop via the
[Apify MCP Server](https://docs.apify.com/platform/integrations/mcp)) can call it with minimal
prompting. Agents should read the structured `OUTPUT` record — it carries `brandReadiness`,
`recommendation`, and `attorneyReview` per name, so the agent doesn't have to reduce the flat
dataset itself. Invalid input returns a structured error code rather than a silent failure (see
[Run-level error codes](#run-level-error-codes)).

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=Protocol/brand-name-ip-screener",
                "--header",
                "Authorization: Bearer <YOUR_APIFY_API_TOKEN>"
            ]
        }
    }
}
```

#### Tool-call example (screen mode)

Once the `apify` MCP server above is connected, an agent invokes the screener as a tool call. The
tool name exposed by the Apify MCP Server is the Actor identifier (`Protocol/brand-name-ip-screener`);
the argument is the same input shape as [Example input — screen mode](#example-input--screen-mode).

```jsonc
// Agent → MCP tool call
{
  "tool": "Protocol/brand-name-ip-screener",
  "input": {
    "names": ["Teslaa", "Acme"],
    "jurisdictions": ["FR", "US", "EU"],
    "niceClasses": ["9", "12", "42"],
    "enableDomainCheck": true
  }
}
```

The tool returns the flat per-name × jurisdiction dataset rows plus a reference to the `OUTPUT`
key-value record. Agents should read `OUTPUT` first — it carries the per-name `brandReadiness`,
`recommendation`, and `attorneyReview` verdicts so the agent does not have to reduce the flat
dataset itself. See [Branching on the OUTPUT record](#branching-on-the-output-record-agent-guidance)
for the decision fields, and [Run-level error codes](#run-level-error-codes) for the coded
failure shape. Invalid input returns a coded error (e.g. `ERR_INVALID_INPUT`) rather than a silent
failure — branch on the `ERR_*` prefix, not on free text.

#### Branching on the OUTPUT record (agent guidance)

When driving this Actor from a tool-calling agent, branch on these fields rather than free-text
parsing:

- **`brandReadiness`** — the overall verdict across all screened jurisdictions: `clear` |
  `caution` | `high-risk` | `not_screened`. Treat `not_screened` as "no signal", **not** as
  "safe" — it means no registry source ran for this name.
- **`jurisdictions[].screeningStatus`** — `screened` vs `not_screened` per jurisdiction. Only
  `screened` rows were actually compared against marks; a `not_screened` row carries a `reason`
  (`No data source enabled…`, `name_not_latin_screenable`, `name_has_no_distinctive_core`, or `budget_exhausted`).
- **`jurisdictions[].marksScreened`** — the denominator behind each verdict: how many in-scope
  marks were actually scored. A `clear` over 3 marks is **not** the same confidence as a `clear`
  over 150. Weight a `clear` by `marksScreened` before reporting low collision risk — a `clear` is "no collision found among the marks screened", not a promise of clearance.
- **`niceClassesSource`** + **`matchedKeywords`** — whether the Nice-class scope was `user`-supplied
  or `inferred` from `industry`, and which industry keywords matched. If `inferred` and
  `matchedKeywords` is empty, the screen ran across **all classes** (noisier) — consider asking
  the user for explicit `niceClasses` before relying on a `clear` as a low-collision signal.
- **`attorneyReview.recommended`** — a structured boolean for high-risk escalation messaging
  (informational only; no referral, see [FAQ](#faq)).
- **`domainAvailability.summary`** — `{ free, taken, error }` across `.com/.io/.ai/.dev`.

**Billing per event:** every `screened` jurisdiction in screen mode bills `screening_report_produced`
($1.00). Generate mode is **free** — every filter-survivor emits
`candidate_generated_and_rescreened` at $0.00. `not_screened` rows and filter rejects are free too.
If cost matters, scope with `niceClasses` and limit `jurisdictions`.

***

### Run-level error codes

Every run failure exits with a coded `Actor.fail` message prefixed by an `ERR_*` code, so an MCP
agent can branch deterministically rather than parsing free text. The table below enumerates every
code defined in `src/lib/errors.ts` plus the runtime catch-all emitted by `src/main.ts`.

| Code                          | Meaning                                                                                                                  | When it fires                                                                                                                                                             | What an agent consumer should do                                                                                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR_INVALID_INPUT`           | Input failed schema validation; `names` and `positioning` were both empty OR both set; or a `niceClasses` entry was not an integer in 1–45. | At input validation, before any network/billing work. Raised by `src/input/validate.ts` (`InvalidInputError`).                                                            | **Surface to user + retry.** Do not retry the same payload. Ask the user to fix the flagged field(s), set exactly one of `names` / `positioning`, or correct `niceClasses`. No charge has been made.        |
| `ERR_MISSING_API_KEY`         | Generate mode was requested but no LLM key is configured — neither a user `llmApiKey` nor the operator `OPENROUTER_API_KEY`. Also raised when a custom `llmBaseUrl` or non-default `llmProvider` is set without its own `llmApiKey` (the operator key is never sent to a custom endpoint — key custody). | At LLM router init, generate mode only. Raised by `src/generation/router.ts` (`MissingApiKeyError`). Screen-only runs never hit this.                                      | **Surface to user + retry.** Ask the user for a `llmApiKey` (bring-your-own), or to switch back to the default `openai` provider + default endpoint so the operator key can be used. |
| `ERR_LLM_PIPELINE_FAILED`     | Both the primary and fallback LLM models failed or timed out (generate mode only).                                       | After both generation attempts exhaust retries / timeout / malformed-JSON recovery in `src/generation/router.ts` (`LlmPipelineError`).                                     | **Retry once with backoff**, then surface to user. If it persists, suggest a different `llmModel` / provider, or fall back to screen mode by supplying `names` directly.              |
| `ERR_REGISTRY_SOURCE`         | A registry mark source threw (network / parse / auth failure) for a jurisdiction.                                        | Re-thrown by `src/registry/seedAdapter.ts` (`RegistrySourceError`) when a `MarkSource` rejects; caught per-jurisdiction in `src/main.ts`, which surfaces that jurisdiction as `not_screened` with reason `source_error` and logs it. The run continues. | **Partial result.** Treat the affected jurisdiction as `not_screened` (no verdict), not as `clear`. Retry the run; if it persists for one jurisdiction only, screen without it or report via the Issues tab. |
| `ERR_OUTPUT_SCHEMA_VIOLATION` | An internal output record did not match the declared dataset / `OUTPUT` schema.                                          | Raised by `src/output/datasetValidator.ts` (`OutputSchemaViolationError`) right before `Actor.pushData`, after AJV-validating every flat dataset row against the published `.actor/dataset_schema.json` (B4). The run fails the moment a row breaks the public contract.   | **Stop + report.** This is an actor-side defect, not a user-input issue. Do not retry the same input; surface the run id to the user and direct them to the Actor's Issues tab.       |
| `ERR_UNEXPECTED`              | Any non-coded throw after `Actor.init` — an unanticipated runtime failure (network blip, adapter crash, etc.).           | Catch-all in `src/main.ts`: any `Error` without an `ERR_*` `code` is wrapped in a typed `UnexpectedError` (`src/lib/errors.ts`, exported from `ERROR_CODES.UNEXPECTED` since B4) before `Actor.fail`, then rethrown so the platform records the non-zero exit. | **Retry once.** If it persists, surface to the user and report via the Actor's Issues tab with the run id. Never assume data was produced — re-check the dataset / `OUTPUT` on retry.  |

> **Informational screening only — not legal advice.** None of these codes is a trademark verdict.
> A `clear` / `caution` / `high-risk` result is carried in `OUTPUT.records[].brandReadiness`, not
> in the error channel. See [FAQ](#faq) for the legal boundary.

***

### FAQ

**Why does the same name sometimes get a different risk score per jurisdiction?**
Each jurisdiction is screened against its own curated mark dataset, so a name can collide with a
real mark in one jurisdiction's sample and not another's. This is expected, not a bug.

**What does `marksScreened` tell me, and why does a `clear` come with a number?**
`marksScreened` is the count of in-scope marks actually scored for a verdict (after the Nice-class
filter). A `clear` over 3 marks is a much weaker signal than a `clear` over 150, so every verdict
carries its denominator. Read `clear` as "no collision found in the screened seed," not as
"trademark-free" — a real registry may hold a conflicting mark that isn't in the curated sample.

**What does `attorneyReview.recommended: true` actually get me?**
Nothing beyond the message itself — it's a flag, not a referral. There is no attorney network,
fee, or law-firm relationship behind it; it's the same "get clearance" advice as the
`recommendation` field, surfaced as a structured boolean an agent can branch on.

**Why is `niceClassesSource` sometimes `"inferred"` even though I set `industry`?**
`niceClassesSource` is `"inferred"` whenever `niceClasses` was left empty — the engine then derives
classes from a curated keyword table keyed on `industry` (e.g. `"fintech"` → classes 9, 36, 42).
Set `niceClasses` explicitly if you want `"user"` provenance and tighter scoping.

**Will the same `positioning` always generate the same names?**
No. LLM generation is non-deterministic — the same input can produce different candidates across
runs. The trademark/domain verdicts for a given generated name are deterministic.

**Can a domain marked `available` be taken later?**
Yes. RDAP results are a point-in-time snapshot. Re-confirm before relying on it.

**Is my input data sent anywhere?**
In generate mode, `positioning`/`industry`/`tone`/etc. are sent to a third-party LLM provider (via
OpenRouter) to generate candidates. Screen mode sends nothing externally except the RDAP domain
probes (no credentials, no PII).

***

### Known limitations

| Limitation                             | What to expect                                                                                                                                       |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Curated seed data, not full registries | Famous-mark collision screening only — a `clear` verdict means **no collision found in the curated seed**, not "trademark-free". A name can still conflict with a real mark not in the sample. Weight a `clear` by the per-jurisdiction `marksScreened` count. |
| Non-deterministic generation           | The same `positioning` may produce different candidates on different runs.                                                                           |
| Point-in-time domain availability      | A TLD marked `available` may be claimed before you register it.                                                                                      |
| Max 20 names/candidates per run        | Hard cap; larger batches require multiple runs.                                                                                                      |
| Non-Latin names are not screenable     | A name that normalizes to an empty core is surfaced as `not_screened` — reason `name_not_latin_screenable` when its letters are all non-Latin (CJK / Cyrillic), or `name_has_no_distinctive_core` when it carries no script at all (punctuation / emoji only). Never scored as a false `clear`, never billed. |
| Lookalike-script names are not screenable | A name mixing Latin with a lookalike script (Cyrillic, Greek, Armenian, Cherokee, fullwidth or mathematical letterforms — e.g. `Gоogle` with a Cyrillic `o`, or `Ｇoogle` with a fullwidth `G`) is surfaced as `not_screened` with reason `confusable_script`. Normalizing it would silently screen a *different* name, so it is never scored and never billed. |
| No social-handle check                 | Domain availability is checked; GitHub/X/Twitch/etc. handle availability is not (see the sibling AI Username Generator Actor).                       |

***

### Data & privacy

- **Generate mode only:** `positioning`, `industry`, `tone`, `techniques`, `mustInclude`, and
  `avoid` are sent to a third-party LLM provider (via OpenRouter) to generate candidates. Do not
  include personal data you don't want shared with an LLM provider.
- **Domain checks are unauthenticated** — no credentials are sent to any registry, and no raw
  external response bodies are stored in your dataset or logs.
- **Data retention:** datasets are retained per your Apify account settings. Delete the run to
  erase its outputs.

***

### Develop

```bash
pnpm install
pnpm test         # unit tests (no network — RDAP/LLM sources are injected)
pnpm type-check
pnpm lint
```

#### RDAP environment variables (optional)

Copy `.env.example` to `.env` and uncomment to override:

| variable           | default | effect                                   |
| ------------------ | ------- | ---------------------------------------- |
| `RDAP_ENABLED`     | `true`  | set to `false` to skip the network calls |
| `RDAP_TIMEOUT_MS`  | `5000`  | per-request HTTP timeout                 |
| `RDAP_CONCURRENCY` | `4`     | max simultaneous RDAP probes             |

#### Generation environment variables

Operator-side, only read in generate mode (`positioning` set) **and only when the
run does not supply its own `llmApiKey`** (see [Bring-your-own LLM](#bring-your-own-llm-v180)).
On Apify, set these as **secret environment variables** (Console → Settings →
Environment variables) — they are injected at run time and never baked into the
build image or exposed to users.

| variable              | default                                | effect                                                                                              |
| --------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `OPENROUTER_API_KEY`  | —                                      | **required** for generate mode when no user `llmApiKey` is supplied; the operator's LLM key.        |
| `OPENROUTER_BASE_URL` | OpenRouter's chat-completions endpoint | must be HTTPS (plain HTTP allowed for localhost only). Ignored for `llmProvider: "anthropic"` runs. |
| `LLM_PRIMARY_MODEL`   | `deepseek/deepseek-chat`               | primary generation model. A user-supplied `llmModel` overrides both primary and fallback.           |
| `LLM_FALLBACK_MODEL`  | `openai/gpt-4o-mini`                   | fallback model on primary failure/timeout/malformed JSON.                                           |
| `LLM_TIMEOUT_MS`      | `8000`                                 | per-request timeout for both models.                                                                |

***

### Contributing

Source: **[github.com/m-raphael/brand-name-ip-screener](https://github.com/m-raphael/brand-name-ip-screener)**.
Contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, the test-first
workflow, the schema-coordination rules, and the hard invariants ("never a false `clear`", the UPL
boundary, no referral/fee logic in this Actor). The repo runs CI (type-check, lint, network-free
tests, build) on every pull request.

Seed-data provenance, the third-party-trademark notice, and coverage limits are documented in
[DATA-SOURCES.md](./DATA-SOURCES.md) — read that before relying on a verdict, and use it to file a
correction if a seed entry is wrong.

### Security

Found a security issue — especially anything touching **key custody** (the operator
`OPENROUTER_API_KEY` must never reach a user-supplied endpoint)? Do **not** open a public issue;
see [SECURITY.md](./SECURITY.md) for the private reporting channel and the bug classes that matter
most here.

***

### Support

Found a bug or want a jurisdiction added? Open an issue on the Actor's **Issues** tab in the
Apify Console — that is the primary support channel — or on
[GitHub](https://github.com/m-raphael/brand-name-ip-screener/issues). Security reports go through
[SECURITY.md](./SECURITY.md), not public issues.

# Actor input Schema

## `names` (type: `array`):

One or more proposed names to screen (e.g. \["Brandcraft", "Vaultly"]). Each is screened against every selected jurisdiction. Hard cap 20 per run; each name must be 1–100 characters and non-blank. Duplicate names (same normalized core, e.g. "Brandcraft" and "BRANDCRAFT") are screened and billed once. Leave empty and fill in `positioning` instead to GENERATE new names first (v1.2.0 closed loop) — exactly one of `names` / `positioning` is required. Billing (screen mode): $1.00 per name × jurisdiction that returns `screened`; a name that normalizes to an empty core (fully non-Latin input) is surfaced as `not_screened` and is free. Cost note: screen mode bills $1.00 per name × jurisdiction, so a large batch needs the run's “Maximum cost per run” set high enough (or left unlimited) — if the cap is reached mid-batch the remaining names are reported not\_screened with reason budget\_exhausted, and are never charged.

## `positioning` (type: `string`):

Free-text brand positioning brief, e.g. "a calmer way to manage freelance invoices". When set (and `names` is left empty), the run GENERATES candidate names via an LLM, deterministically filters them (profanity/banned-words/dupes + mustInclude/avoid), screens the survivors through the same IP + domain pipeline as `names`, then ranks by a composite score. Billing (generate mode): FREE — generate mode costs $0.00. Every filter-survivor emits `candidate_generated_and_rescreened` at $0.00, and rejected candidates are listed under OUTPUT.filtered, also at no charge.

## `tone` (type: `string`):

Tone the generated names should aim for. Only used when `positioning` is set; ignored in screen mode, so it changes no screen-mode output. In generate mode it biases the candidate names the LLM produces but does not change which candidates survive filtering — generate mode is free ($0.00 per filter-survivor) regardless of tone.

## `techniques` (type: `array`):

Optional. Naming techniques the LLM should favor, e.g. "compound", "affix", "invented", "metaphor". Leave empty to let the model choose a mix. Only used when `positioning` is set; ignored in screen mode. In generate mode it biases the style of candidate names produced but does not change filter logic — generate mode is free ($0.00 per filter-survivor) regardless of techniques chosen.

## `mustInclude` (type: `array`):

Optional. Every generated name must contain at least one of these fragments (normalized, case- and punctuation-insensitive). Deterministically enforced AFTER generation (v1.4.0): a candidate missing every required fragment is rejected at the filter (invalidReason `missing_required_fragment`, listed under OUTPUT.filtered) and is never screened or billed. Only used when `positioning` is set.

## `avoid` (type: `array`):

Optional. Generated names must never contain any of these fragments (normalized, case- and punctuation-insensitive). Deterministically enforced AFTER generation (v1.4.0): a candidate containing a forbidden fragment is rejected at the filter (invalidReason `forbidden_fragment`, listed under OUTPUT.filtered) and is never screened or billed. Only used when `positioning` is set.

## `count` (type: `integer`):

Optional. How many candidate names to generate before filtering/screening. Capped at 20. Only used when `positioning` is set. Billing: generate mode is free — each filter-survivor emits a $0.00 event, so raising `count` costs nothing.

## `maxLength` (type: `integer`):

Optional. Maximum character length (excluding spaces) for generated names. Only used when `positioning` is set; ignored in screen mode. In generate mode, candidates exceeding this length are rejected at the filter (listed under `OUTPUT.filtered`) and never screened or billed; survivors emit a $0.00 event each — generate mode is free, so tighter limits change coverage, not cost.

## `bannedWords` (type: `array`):

Optional. Extra words to reject beyond the built-in profanity list, e.g. a competitor's name. Only used when `positioning` is set; ignored in screen mode. In generate mode, a candidate containing a banned word is rejected at the filter (listed under `OUTPUT.filtered`) and never screened or billed; survivors emit a $0.00 event each — generate mode is free, so adding entries here changes coverage, not cost.

## `llmProvider` (type: `string`):

Optional. Wire protocol for the generation model. "openai" = OpenAI-compatible (OpenRouter, OpenAI, Groq, Ollama, LM Studio). "anthropic" = native Claude Messages API. Defaults to "openai". Only used when `positioning` is set; ignored in screen mode, so it changes no screen-mode output. In generate mode it selects the API protocol used to call the model and does not affect PPE billing — generate mode is free ($0.00 per filter-survivor) regardless of provider; your own LLM provider costs are separate from the Actor charge.

## `llmApiKey` (type: `string`):

Your own API key for the chosen provider. When set, it overrides the operator-configured key (OPENROUTER\_API\_KEY env), so you pay your own LLM provider directly. Marked secret — never logged. REQUIRED for generate mode whenever the operator has NOT configured a key, AND whenever you set `llmBaseUrl` to a non-default endpoint OR set `llmProvider` to anything other than the default "openai" — the operator's key is never sent to a custom endpoint or a non-default provider (key-custody). Example providers: OpenRouter (https://openrouter.ai/keys), Groq (https://console.groq.com/keys), OpenAI (https://platform.openai.com/api-keys), Anthropic (https://console.anthropic.com/settings/keys). Effect: enables generate mode when the operator has no key, and unlocks custom endpoints/non-default providers. Billing: this key pays your own LLM provider and does not change the Actor's PPE charge — generate mode is free ($0.00 per filter-survivor); screen mode is unaffected and bills its usual `$1.00` per name × jurisdiction screened.

## `llmBaseUrl` (type: `string`):

Provider chat-completions / messages endpoint. Overrides OPENROUTER\_BASE\_URL. Must be HTTPS (plain HTTP allowed for localhost only, e.g. a local Ollama/LM Studio). Leave empty for the provider default. Setting this to a non-default endpoint REQUIRES your own `llmApiKey` — the operator's key is never sent to a custom endpoint. Examples — OpenRouter: https://openrouter.ai/api/v1/chat/completions · OpenAI: https://api.openai.com/v1/chat/completions · Groq: https://api.groq.com/openai/v1/chat/completions · Anthropic: https://api.anthropic.com/v1/messages. Effect: routes generation calls to a chosen endpoint (generate mode only); ignored in screen mode. Billing: does not affect the Actor's PPE charge — generate mode is free ($0.00 per filter-survivor) regardless of endpoint; your own endpoint's LLM costs are separate.

## `llmModel` (type: `string`):

Optional. Single model id to use for generation (overrides the operator's primary/fallback pair). Examples — OpenRouter: anthropic/claude-3.5-sonnet · Groq: llama-3.3-70b-versatile · OpenAI: gpt-4o-mini · Anthropic: claude-haiku-4-5-20251001. Leave empty to use the operator-configured models. Effect: changes which model generates candidate names (generate mode only); ignored in screen mode. Billing: does not affect the Actor's PPE charge — generate mode is free ($0.00 per filter-survivor) regardless of model; the model's own provider costs are separate.

## `jurisdictions` (type: `array`):

Which trademark registries to screen against. "FR" = INPI (France), "US" = USPTO, "EU" = EUIPO, "WO" = WIPO (international/Madrid), and "UK" = UKIPO are all screened against curated seed datasets of well-known registered marks — famous-mark collision screening, not full registry coverage. Defaults to \["FR"]. Billing (screen mode): each selected jurisdiction that returns `screened` bills $1.00 per name; a jurisdiction with no enabled source returns `not_screened` and is free. Each verdict carries `marksScreened` (how many in-scope marks were compared) so a weak `clear` can't pass for a strong one.

## `niceClasses` (type: `array`):

Optional but strongly recommended. The Nice classification number(s) for your goods/services (1–45). Trademark conflicts only matter within similar classes, so scoping here removes noise from the output. Leave empty to screen across ALL classes (noisier, more false positives). When left empty AND `industry` is set, v1.1.0 infers classes from a curated keyword table and tags the run `niceClassesSource: inferred` (see output). Billing: scoping does not change the per-jurisdiction charge — screen mode still bills `$1.00` per name × jurisdiction that returns `screened`, and generate mode is free ($0.00), regardless of how many Nice classes are selected; `not_screened` rows stay free.

## `industry` (type: `string`):

Optional free-text industry (e.g. "fintech", "coffee roastery"). Effect on output: used only for the human-readable recommendation, and (when `niceClasses` is empty) to infer Nice classes via a curated keyword table, tagging the run `niceClassesSource: inferred`. Prefer `niceClasses` for the actual scoping. Billing: no direct effect — screen mode still bills `$1.00` per name × jurisdiction screened and generate mode is free ($0.00); industry never adds or removes a charge on its own.

## `language` (type: `string`):

Language whose phonetic rules drive the sounds-alike comparison. Effect on output: "fr" applies French phonetics for the INPI path; "en" applies English. Defaults to "en". Changing it can alter which marks score above `riskThreshold` and therefore which similar marks are listed. Billing: no effect — screen mode still bills `$1.00` per name × jurisdiction screened and generate mode is free ($0.00), regardless of the phonetic language selected.

## `riskThreshold` (type: `integer`):

Optional. Only similar marks scoring at or above this 0–100 similarity are listed in the output. Defaults to 60. Lower it to see more borderline matches; raise it to list fewer. Effect on output: filters which similar marks appear per verdict (the `marksScreened` count and per-jurisdiction `screened`/`not_screened` status are unaffected). Billing: no effect — screen mode still bills `$1.00` per name × jurisdiction that returns `screened` and generate mode is free ($0.00), regardless of how many marks the threshold surfaces.

## `enableDomainCheck` (type: `boolean`):

Optional. When true (default), the run probes .com / .io / .ai / .dev via RDAP and surfaces availability + a free/taken/error summary in the OUTPUT record. Set to false to skip the network calls entirely (overrides RDAP\_ENABLED env). Effect on output: toggles the domain-availability block in the output; disabling it removes the RDAP summary but leaves trademark verdicts unchanged. Billing: no effect — the RDAP co-check is never charged; screen mode still bills `$1.00` per name × jurisdiction screened and generate mode is free ($0.00), regardless of this flag.

## Actor input object example

```json
{
  "names": [
    "Brandcraft"
  ],
  "tone": "Professional",
  "techniques": [],
  "mustInclude": [],
  "avoid": [],
  "count": 10,
  "maxLength": 18,
  "bannedWords": [],
  "llmProvider": "openai",
  "llmBaseUrl": "",
  "llmModel": "",
  "jurisdictions": [
    "FR"
  ],
  "niceClasses": [],
  "language": "en",
  "riskThreshold": 60,
  "enableDomainCheck": true
}
```

# Actor output Schema

## `report` (type: `string`):

The structured OUTPUT record: { schemaVersion, runId, mode, records\[], filtered\[]? }. Each record carries brandReadiness (clear / caution / high-risk / not\_screened), ipRiskScore, a human-readable recommendation, per-jurisdiction verdicts with their marksScreened denominator, domainAvailability, attorneyReview, seedAsOf, and the mandatory legal disclaimer. In generate mode records\[] is sorted by compositeScore and filtered\[] lists the candidates rejected before screening. Branch on these fields rather than parsing free text.

## `screeningRows` (type: `string`):

Flat dataset rows matching .actor/dataset\_schema.json — name, jurisdiction, risk band, ipRiskScore, closest colliding mark, marksScreened, notScreenedReason, and the disclaimer. Exports cleanly to CSV/Excel. A row with screeningStatus 'not\_screened' was never compared against any mark: treat it as no signal, never as low risk.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "names": [
        "Brandcraft"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("protocol/brand-name-ip-screener").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "names": ["Brandcraft"] }

# Run the Actor and wait for it to finish
run = client.actor("protocol/brand-name-ip-screener").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "names": [
    "Brandcraft"
  ]
}' |
apify call protocol/brand-name-ip-screener --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,protocol/brand-name-ip-screener"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/qDfm7pIxjZmc3yKPX/builds/SktPFRK9RqVaaj3hp/openapi.json
