# Trademark Class Overlap Review (`zinin/trademark-class-overlap-review`) Actor

Group observed USPTO wordmarks and review class overlap across distinct serial records. Preserve reported status and date conflicts in source-linked assessments without a clearance verdict.

- **URL**: https://apify.com/zinin/trademark-class-overlap-review.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $17.50 / 1,000 mark assessments

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Trademark Class Overlap Review

Turn a batch of USPTO trademark-search rows into a per-wordmark review worksheet: identity-clean registration groups, exact class intersection/union across records, and every status or field disagreement kept visible — never a clearance verdict.

A USPTO trademark search can return the same wordmark under many different `serial_number` records — separate applications, separate registrations, separate classes filed at different times. This Actor turns that scattered result set into one row per distinct wordmark, with the class coverage, status agreement, and record-level conflicts already reconciled and disclosed, so a reviewer opens one worksheet row instead of re-reading every raw search hit by hand.

![Trademark Class Overlap Review — buyer input flows through USPTO wordmark evidence to a reviewed class-coverage worksheet](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/b5505823e5d52dba40d5daeb471d406383093f34/value-add-2-20260923/trademark-class-overlap-review/readme-hero.webp)

### What you get

Point this Actor at a dataset of USPTO trademark-search rows — the shape produced by `dev00/uspto-trademark-api` — and it delivers one **mark assessment** row per distinct normalized wordmark, after two passes of honesty checks most naive dedup scripts skip:

- **Global identity reconciliation, before any grouping.** Every accepted row's `serial_number` is collected across the whole batch first. If the same `serial_number` was ever observed with two different wordmarks, that is reported as one free `serial_identity_conflict` diagnostic listing every wordmark seen for it, and the serial is excluded from **every** wordmark group — it can never land in two billable groups, and the conflict is never silently resolved by picking one value.
- **Exact wordmark grouping**, applied only after identity reconciliation: Unicode NFKC normalization, `toLowerCase()` case-folding, whitespace collapse — a disclosed, documented text comparison, never phonetic similarity, confusion, or a legal-clearance judgment. `"acme"` and `"akme"` are always separate groups.
- **Class intersection and union** across the group's distinct, identity-clean records, parsed only from documented explicit numeric tokens (1–45) in the source `classes` string — every invalid or out-of-range token counted **exactly**, never truncated by any display cap, and a record contributing zero parsed classes is counted separately rather than silently treated as zero classes registered.
- **Status disagreements** across the group's distinct records (one registration LIVE, another DEAD is normal and disclosed, not an error) and **record conflicts** — cases where duplicate rows for the *same* `serial_number` disagreed on status, filing date, registration date, or classes, with an exact total and an exact display-omitted count.
- **Confidence that reflects real class-evidence gaps.** A group can only reach `confidence: 'high'` when it has neither an invalid class token nor a record with zero parsed classes anywhere in it — a group whose intersection was quietly built from only the successfully-parsed slice of a messy `classes` string never reads as complete, high-confidence coverage.

You do not get an availability, infringement, confusion-likelihood, or legal-clearance opinion, a phonetic or fuzzy-similarity match, or a claim that a registration is currently valid beyond what the batch literally contained. See **Evidence and boundaries** for the full list.

### Who uses it

- **Trademark paralegals and IP counsel** doing a first-pass triage of a USPTO search-report export before a clearance opinion — this Actor collapses duplicate search hits into one row per mark with the class coverage already computed, so the attorney's own review starts from a worksheet, not a raw hit list.
- **Brand and IP portfolio managers** auditing their own company's registered marks across classes, using `hasStatusDisagreement` and `recordConflicts` to catch stale or inconsistent portfolio-tracking data before a renewal cycle.
- **M\&A and licensing due-diligence teams** reviewing a target's or licensor's trademark estate, where the class-intersection view answers "which classes does every registration of this mark actually cover" in one pass across a batch of search results.
- **Competitive-intelligence and brand-monitoring researchers** tracking a competitor's filing activity across classes over time, using the record-level evidence (filing/registration dates, per-record classes) as a citation trail back to the individual USPTO serial numbers.
- **Data teams** building a downstream trademark-portfolio dataset who want a pure, deterministic, non-LLM transform they can re-run identically against a refreshed USPTO pull and diff against the previous run's output.

This Actor is not a substitute for a formal USPTO clearance search, a likelihood-of-confusion legal opinion, or an attorney's own review of the primary record — it is a reconciliation and triage layer in front of that review.

### How to run

1. **Get source rows.** Run `dev00/uspto-trademark-api` (or an equivalent USPTO search producing the same row shape — `success`, `serial_number`, `trademark_wordmark`, `status`, `filing_date`, `registration_date`, `classes`, `query_phrase`) for the wordmarks you want reviewed. One row per registration/application record is expected; duplicate rows for the same `serial_number` are expected and reconciled, not an error condition.
2. **Point this Actor at that dataset.** Paste the source `datasetId` into **Source Dataset** (a `resourcePicker` field — Apify resolves it with your own run's READ permission, never an elevated grant), or supply a `runId`. An Apify Integration "Actor/task run" template can populate `payload.resource` automatically.
3. **Leave `items` as the inline example, or override it.** Source Dataset/`runId`, when set, **overrides** the inline `items` array entirely. The shipped inline example is a real, curated USPTO snapshot (see **Sources and rights**), so you can press Start immediately.
4. **Set `maxResults` and `maxInputRows` to fit your batch and budget.** `maxResults` (default 2) caps how many *paid* mark-assessment rows are delivered; free diagnostics are never capped. `maxInputRows` (default 100, ceiling 1000) caps how many source rows are read at all.
5. **Set the platform's own Max total charge run option** for a hard USD ceiling — the preflight budget check (see **Pricing**) already refuses to start a paid pass if even one result cannot be afforded, giving a clean `budget_stopped` diagnostic instead of a partially-billed run.
6. **Read the dataset.** Every delivered row carries `product`, `schemaVersion`, `found`, `status`, `partial`, `sourceCoverage`, `sourceDatasetId`/`sourceRunId`, and `processedAt` in addition to its own fields — see **Field dictionary** and **Output examples**.

There are no product-specific input fields beyond the shared source-selection fields — no `asOf`, no domain toggle. Every row in the batch is treated identically by the same class-parsing and identity rules.

### Pricing

One pricing noun, one billing event: **mark assessment** (`result-found`). Diagnostics, budget-stop notices, source-partial notices, and empty-batch notices are always free — they never carry the `result-found` event, and are never billed even when they are the only output of a run.

| Apify tier | Price per mark assessment |
|---|---|
| Free | $0.0250 |
| Bronze | $0.0225 (10% off) |
| Silver | $0.0200 (20% off) |
| Gold | $0.0175 (30% off) |
| Platinum | $0.0175 (30% off) |
| Diamond | $0.0175 (30% off) |

There is **no separate start fee or per-run flat fee** — you are charged only for delivered, useful mark-assessment rows, one `result-found` event per distinct normalized wordmark actually delivered. A run producing zero useful results (an empty batch, a fully invalid batch, or a run stopped by your own budget before any result could be afforded) has no result-found event charge; owner validation and platform resource expenses are separate.

Billing is **write-then-charge, non-transactional, and fails closed on ambiguity**: the runtime writes a result row first, then requests the charge; if the charge cannot be confirmed, it does not silently retry (a retry risking a double charge is refused), and the run is marked failed rather than a clean success it cannot verify. **An already-delivered result can, in a rare failure case, remain in the Dataset without a confirmed matching charge** — `chargedEventCounts.result-found` (see each scenario's own `settled.json` reconciliation below) is the authoritative confirmed count, not guaranteed to equal the visible useful-row count on every failure path. A **new run against the same input is new processing and may bill again** — no deduplication against a prior run's delivered wordmark groups.

Before reading any source, the runtime preflights affordability: if your charge ceiling (platform Max total charge) cannot cover even one result at your account's tier, the run stops immediately with a free `budget_stopped` diagnostic and reads no source rows — see **below-price-r2** in **Output examples**.

### Input contract

This Actor consumes the shared PPE (pay-per-event) source contract used across this batch of downstream Actors — a single JSON input object, read once at Actor start:

| Field | Type | Required | Notes |
|---|---|---|---|
| `items` | array | No | Inline source rows (max 1000 by schema; bounded to 1000 rows / 4 MiB total / 64 KiB per row by the runtime). Ships pre-filled with a real, curated USPTO snapshot (3 rows). Ignored once `datasetId`/`runId` resolves a source. |
| `datasetId` | string (`resourcePicker`, `dataset`, READ) | No | Existing Dataset ID, resolved with your own run's permission. Overrides inline `items`. Configure your integration to map `{{resource.defaultDatasetId}}` explicitly; the Actor does not populate this field itself. |
| `runId` | string | No | Actor run ID whose default Dataset is read instead, resolved with your own permission — this Actor never starts the upstream run itself. |
| `payload` | object | No | An explicitly supplied resource envelope; default empty. Malformed or conflicting same-kind IDs are rejected. Dataset selection never grants private read permission by itself. |
| `maxInputRows` | integer, 1–1000 | No (default 100) | Caps source rows read. Rows beyond this are reported via `sourcePartial`/`sourcePartialReason: 'max_input_rows_reached'`, never silently truncated. |
| `maxResults` | integer, 1–1000 | No (default 2) | Caps *paid* rows delivered. Free diagnostics are never capped. A free `max_results_reached` diagnostic states exact `delivered`/`totalUseful`. |

**Recognized row shape**: `success` (must be `true` — and is overridden by any contradictory failure evidence, see below), `serial_number` (required — accepted as a positive integer or an all-digit string), `trademark_wordmark` (required — the grouping key after normalization), `status`, `filing_date`/`registration_date` (strict `YYYY-MM-DD` event dates, never a capture timestamp), `classes` (source string; only 1–45 numeric tokens split on comma/semicolon/slash/whitespace are parsed), `query_phrase` (ignored entirely — it is the search query used to find the record, never the mark's own identity).

**Contradictory failure evidence always overrides `success: true`.** A row carrying `found: false`, `failed: true`, a failure-status word (`failed`/`error`/`timed-out`/`aborted`), or a nonempty `error`/`errors` field is rejected as a free `source_row_failed` diagnostic regardless of its own `success` flag — a self-contradicting row is never trustworthy identity evidence. A row missing a usable `serial_number` or `trademark_wordmark` is a separate free diagnostic (`missing_serial_number`/`missing_wordmark`); neither ever produces a fabricated paid result.

### Output examples

Every scenario below is the **complete, unedited JSON array** written to this Actor's Dataset by a real cloud run of this Actor's current code (build `0.1.2`, `buildId fESgb3d7NfJDjFmvR`), captured under `ops/value-add-next-20260923/evidence/actors/trademark-class-overlap-review/<scenario>/items.json`, cross-checked against each run's own `settled.json`. Nothing below is paraphrased, trimmed, or re-ordered.

#### `final-prefill-r2` — the shipped inline example

Real public "star wars" USPTO search snapshot (see **Sources and rights**): three distinct, identity-clean serial numbers, all normalizing to the same wordmark. `settled.json` confirms `chargedEventCounts.result-found: 1`, `"delivered: 1 paid, 0 free."`.

```json
[
  {
    "entityId": "wordmark:star wars",
    "found": true,
    "status": "class_group_reviewed",
    "action": "review_class_group",
    "confidence": "high",
    "normalizedWordmark": "star wars",
    "wordmark": "star wars",
    "distinctRecordCount": 3,
    "classIntersection": [
      25
    ],
    "classUnion": [
      25
    ],
    "recordsWithNoParsedClasses": 0,
    "invalidClassTokenCount": 0,
    "statusValues": [
      "LIVE"
    ],
    "hasStatusDisagreement": false,
    "evidenceOmitted": 0,
    "recordConflicts": [],
    "recordConflictCount": 0,
    "recordConflictsOmitted": 0,
    "observedAt": null,
    "evidence": [
      {
        "field": "serial_number",
        "serialNumber": "73149856",
        "status": "LIVE",
        "filingDate": "1977-11-25",
        "registrationDate": "1979-10-30",
        "classes": [
          25
        ],
        "sourceRowIndex": 0
      },
      {
        "field": "serial_number",
        "serialNumber": "73487823",
        "status": "LIVE",
        "filingDate": "1984-06-29",
        "registrationDate": "1985-11-19",
        "classes": [
          25
        ],
        "sourceRowIndex": 1
      },
      {
        "field": "serial_number",
        "serialNumber": "75306994",
        "status": "LIVE",
        "filingDate": "1997-06-11",
        "registrationDate": "2002-01-29",
        "classes": [
          25
        ],
        "sourceRowIndex": 2
      }
    ],
    "businessPartial": false,
    "sourceRowIndexes": [
      0,
      1,
      2
    ],
    "partial": false,
    "sourceCoverage": {
      "complete": true,
      "reason": null,
      "droppedOversized": 0
    },
    "product": "trademark-class-overlap-review",
    "schemaVersion": "1.0",
    "sourceDatasetId": null,
    "sourceRunId": null,
    "suppliedSourceRunId": null,
    "sourceRunIdVerified": null,
    "processedAt": "2026-09-23T08:32:25.670Z"
  }
]
```

Three genuinely distinct serial numbers — filed 1977, 1984, and 1997, all LIVE, all Class 25 — reconcile into one `distinctRecordCount: 3` group with `classIntersection: [25]` and no disagreement anywhere, which is exactly why this row reaches `confidence: 'high'`: zero invalid class tokens and zero records with no parsed classes.

#### `partial-columns-r2` — bounded source (`maxInputRows: 1`)

This scenario name is a legacy label from the shared naming convention: it means the run was **bounded by `maxInputRows: 1`, not that any column/field was deleted** — every field of the one row actually read is unchanged. `settled.json` confirms `"delivered: 1 paid, 1 free."`.

```json
[
  {
    "status": "partial_source",
    "reason": "max_input_rows_reached",
    "processedAt": "2026-09-23T08:33:42.003Z",
    "partial": true,
    "sourceCoverage": {
      "complete": false,
      "reason": "max_input_rows_reached",
      "droppedOversized": 0
    },
    "product": "trademark-class-overlap-review",
    "schemaVersion": "1.0",
    "sourceDatasetId": null,
    "sourceRunId": null,
    "suppliedSourceRunId": null,
    "sourceRunIdVerified": null,
    "found": false
  },
  {
    "entityId": "wordmark:star wars",
    "found": true,
    "status": "class_group_reviewed",
    "action": "review_class_group",
    "confidence": "high",
    "normalizedWordmark": "star wars",
    "wordmark": "star wars",
    "distinctRecordCount": 1,
    "classIntersection": [
      25
    ],
    "classUnion": [
      25
    ],
    "recordsWithNoParsedClasses": 0,
    "invalidClassTokenCount": 0,
    "statusValues": [
      "LIVE"
    ],
    "hasStatusDisagreement": false,
    "evidenceOmitted": 0,
    "recordConflicts": [],
    "recordConflictCount": 0,
    "recordConflictsOmitted": 0,
    "observedAt": null,
    "evidence": [
      {
        "field": "serial_number",
        "serialNumber": "73149856",
        "status": "LIVE",
        "filingDate": "1977-11-25",
        "registrationDate": "1979-10-30",
        "classes": [
          25
        ],
        "sourceRowIndex": 0
      }
    ],
    "businessPartial": false,
    "sourceRowIndexes": [
      0
    ],
    "partial": true,
    "sourceCoverage": {
      "complete": false,
      "reason": "max_input_rows_reached",
      "droppedOversized": 0
    },
    "product": "trademark-class-overlap-review",
    "schemaVersion": "1.0",
    "sourceDatasetId": null,
    "sourceRunId": null,
    "suppliedSourceRunId": null,
    "sourceRunIdVerified": null,
    "processedAt": "2026-09-23T08:33:42.066Z"
  }
]
```

Note `confidence: 'high'` here despite `partial: true` — the two are independent. This particular one-record group has zero invalid class tokens and zero unparsed-class records (the class evidence itself is complete), so confidence stays high; `partial: true` is driven entirely by `sourceCoverage.complete: false` propagating from the run-level `maxInputRows` bound, not by any class-parsing gap. A batch that instead triggered an invalid Nice class token or a record with zero parsed classes would additionally force `confidence` to `'medium'` (or `'low'` if a record conflict was also present) — see **Evidence and boundaries** for the exact confidence rule and **Decision routing** for how to read the two signals together.

#### `below-price-r2` — preflight budget refusal, source never read

`maxTotalChargeUsd: 0.005` set below the cost of one result. `settled.json` confirms `chargedEventCounts.result-found: 0`, `"budget_stopped: 0 paid, 1 free."` — the preflight guard refuses the source read once it determines the charge ceiling cannot cover one result.

```json
[
  {
    "product": "trademark-class-overlap-review",
    "schemaVersion": "1.0",
    "status": "budget_stopped",
    "reason": "Run charge limit cannot cover any result; no source was read.",
    "found": false,
    "processedAt": "2026-09-23T08:33:19.904Z"
  }
]
```

#### `empty-source-r2` — a batch with no usable rows

```json
[
  {
    "product": "trademark-class-overlap-review",
    "schemaVersion": "1.0",
    "status": "no_results",
    "found": false,
    "reason": "no_usable_rows_in_batch",
    "sourceCoverage": {
      "complete": true,
      "reason": null,
      "droppedOversized": 0
    },
    "processedAt": "2026-09-23T08:33:10.741Z"
  }
]
```

This is the runtime's explicit floor: a run that would otherwise write nothing (no paid result, no diagnostic) writes this one free row instead, so a buyer's Dataset reads as "the batch genuinely had nothing usable" rather than an empty, unexplained silence. Like every dataset write in this runtime, that floor write is not unconditional: if the write itself fails, the run is reported **failed** (never a false empty success) rather than silently skipping the row — see **Evidence and boundaries** for the write-then-charge honesty rules that apply to every row this Actor writes.

### Field dictionary

Every field delivered on a **found:true** row, beyond the shared runtime envelope (`product`, `schemaVersion`, `found`, `status`, `action`, `confidence`, `partial`, `businessPartial`, `evidence`, `sourceRowIndexes`, `sourceCoverage`, `sourceDatasetId`, `sourceRunId`, `suppliedSourceRunId`, `sourceRunIdVerified`, `processedAt`):

| Field | Meaning |
|---|---|
| `normalizedWordmark` / `wordmark` | The grouping key (`raw.normalize('NFKC').trim().replace(/\s+/g,' ').toLowerCase()`) and a display copy of the raw text — computed only after global `serial_number` identity reconciliation (see **Identity and grouping rules** below). |
| `distinctRecordCount` | Unique, identity-clean `serial_number` values in this group. Each is a fully distinct registration/application record, even when the wordmark text is identical. A `serial_number` observed with more than one distinct wordmark is excluded from every group entirely — see `identityConflictedSerialCount` in the run summary. |
| `classIntersection` | Sorted array of numeric classes (1–45) present on **every** record in the group that had at least one parsed class. Records with zero parsed classes are excluded from this intersection and counted separately in `recordsWithNoParsedClasses`. Empty when no record had a parsed class, or when the parsed classes share nothing in common. |
| `classUnion` | Sorted array of every distinct class parsed across any record in the group. |
| `recordsWithNoParsedClasses` | Count of distinct records whose `classes` field yielded zero valid 1–45 tokens (missing field, wrong type, or every token invalid) — an explicit gap, never treated as zero classes registered. |
| `invalidClassTokenCount` | The **exact** total count, across every record in the group, of `classes`-string tokens that failed the documented 1–45 numeric parse — every token counted, never truncated by any display cap. |
| `statusValues` / `hasStatusDisagreement` | Sorted distinct status strings across the group's distinct records, and whether more than one value was observed (e.g. one registration LIVE, another DEAD — genuinely normal, disclosed rather than hidden). |
| `recordConflicts` / `recordConflictCount` / `recordConflictsOmitted` | Bounded list (cap 10) of `{ serialNumber, field, values, sourceRowIndexes }` where the **same** `serial_number` was observed with more than one distinct value for status, filing date, registration date, or classes across duplicate rows, the exact total found, and the exact count not displayed. |
| `evidence` / `evidenceOmitted` | Bounded list (cap 12) of per-record `{ serialNumber, status, filingDate, registrationDate, classes, sourceRowIndex }`, and the exact count of this group's records not shown. |
| `observedAt` | Always `null` — this Actor's documented USPTO source rows carry only event dates (`filing_date`/`registration_date`), never a capture timestamp, so an observation time is never fabricated. |

### Evidence and boundaries

Source correspondence is checked when run metadata is accessible: a proven run/Dataset mismatch is rejected. If metadata is inaccessible but an explicit Dataset is readable, processing can continue with sourceRunIdVerified false. Only source requests use GET; SDK output and billing perform writes. Diagnostic delivery is bounded to200 rows and64KiB per row; the summary records truncation/oversized omissions. Storage failure can prevent OUTPUT/SUMMARY persistence, and ambiguous writes may leave raw rows above confirmed counters. A run cap below one result stops before source reading; a cap covering part of the batch can deliver that part.

**What this Actor deliberately does not claim**, matching the source contract in `SPEC.md` exactly:

- **Not a clearance, availability, or infringement opinion.** `confidence` and `action` describe evidence coverage and a suggested next step, never a legal verdict. This Actor never asserts a mark is available to register, clear to use, non-infringing, or currently valid beyond what the supplied batch literally contained.
- **Not similarity or confusion matching.** Grouping is exact Unicode NFKC normalization, case-folding, and whitespace collapse only — a disclosed, documented text comparison, never a phonetic, confusion, or legal-similarity judgment. Two marks that a trademark examiner might treat as confusingly similar (different spelling, same sound) are always separate groups here.
- **A record with zero parsed classes is never treated as zero classes registered.** It is counted in `recordsWithNoParsedClasses`, an explicit, disclosed gap in the evidence, distinct from a genuine finding of no overlapping classes.
- **An invalid `classes` token is never silently dropped or coerced into a nearby valid class.** It is counted exactly in `invalidClassTokenCount`, with no display cap ever truncating that count, even when the group's `classIntersection` itself is derived only from the tokens that did parse.
- **A `success: true` flag never overrides contradictory failure evidence on the same row.** `found: false`, `failed: true`, a failure-status word, or a nonempty `error`/`errors` field is checked **unconditionally**, before the `success` gate — a self-contradicting row is rejected outright, never trusted as identity evidence just because one field claimed success.
- **A conflicted `serial_number` is never split across two billable groups, and never silently assigned to one.** Identity reconciliation runs globally across the whole batch, before any grouping exists — see **Identity and grouping rules** below.
- **Confidence reflects real class-evidence gaps, precisely.** `confidence` reserves `'high'` only when both `invalidClassTokenTotal === 0` and `recordsWithNoParsedClasses === 0` across every record in the group (otherwise `'medium'`, or `'low'` if `recordConflictsTotal > 0` also holds); `partial` is also forced `true` whenever either condition holds — a group whose `classIntersection` came from only the successfully-parsed portion of a messy `classes` value (e.g. `"9 bad"`) never reads as complete, high-confidence coverage with no visible signal that part of the evidence was unusable.
- **No DOB, address, nationality, or any personal-identity field anywhere** — this Actor's entire source contract is USPTO wordmark/registration records, which carry no personal-identity fields to begin with; nothing here processes or outputs one.
- **Replay and billing honesty.** `EXISTING_RESULT`/`EXISTING_CLAIM` guard against re-processing an already-claimed run; a `chargedEventCounts.result-found` mismatch halts further billing rather than risk a duplicate charge. Delivery is write-then-charge and non-transactional, so a Dataset write can succeed while charge confirmation cannot — the run is then reported **failed**, with the honest delivered count in the message, never a false "succeeded". Summary/OUTPUT persistence is itself best-effort: a failure there also fails the run (`FINALIZATION_FAILED`); dataset rows/logs remain the source of truth.

#### Identity and grouping rules

Before any wordmark grouping exists, every accepted row's `serial_number` is collected across the **whole batch**. A `serial_number` observed with more than one distinct normalized `trademark_wordmark` is an explicit identity conflict — reported as one free `serial_identity_conflict` diagnostic listing every wordmark seen for it, and excluded from every wordmark group entirely. Only after this global pass do the remaining, identity-clean records get grouped by their exact normalized wordmark text. This ordering matters: it is what prevents a single messy serial number from contaminating two separate billable groups, or from being silently and arbitrarily assigned to just one of them.

### Decision routing

A suggested triage order for a batch of delivered rows, not an automated verdict:

1. **`confidence: high`, `recordConflictCount: 0`** — the cleanest signal: every record in the group contributed at least one parsed class, no invalid tokens anywhere, no duplicate-row disagreement. `classIntersection` is a trustworthy summary of shared class coverage across every clean record.
2. **`confidence: medium`** — at least one record had an invalid class token or contributed zero parsed classes. `classIntersection`/`classUnion` are still exact for the evidence that *did* parse, but check `invalidClassTokenCount`/`recordsWithNoParsedClasses` before treating the class picture as complete — a class genuinely registered but recorded as an unparseable token in the source is not visible in the intersection.
3. **`confidence: low` (`recordConflictCount > 0`)** — at least one `serial_number` in the group carries an unresolved field disagreement across duplicate source rows. Resolve the conflict (or re-query) before relying on that record's status, dates, or classes.
4. **`hasStatusDisagreement: true`** — normal and expected when a mark has multiple registrations with different lifecycle states; not itself a data-quality signal, but worth surfacing to a reviewer deciding which specific registration matters for their question.
5. **Free diagnostic rows** (`serial_identity_conflict`, `max_results_reached`, `budget_stopped`, `partial_source`, `no_results`) — run- or identity-level signals, not wordmark-group results. `serial_identity_conflict` in particular is worth a manual look: it means the source data itself disagreed on what a specific application/registration is called.

### Commercial playbooks

- **Pre-filing clearance triage.** Run a USPTO search across your candidate wordmark and close variants, then use this Actor's grouped output as the first-pass worksheet an attorney reviews before commissioning a full clearance search — this Actor narrows "which existing registrations share my exact normalized wordmark and what classes do they cover," leaving similarity/confusion judgment to counsel.
- **Portfolio renewal audit.** Batch your own company's full registered-mark list, and treat any `hasStatusDisagreement: true` or `recordConflictCount > 0` group as a flag to reconcile against your internal portfolio tracker before a renewal deadline — a status disagreement in the source data is often the first sign an internal record has drifted from the USPTO record of truth.
- **Licensing and franchise due diligence.** For a licensor's mark portfolio, the class-intersection view answers "which classes does every registration of this mark actually, currently cover" in one batch pass, giving a licensing team a citation-backed starting point before the formal IP due-diligence review.
- **M\&A trademark-estate review.** Batch a target company's known marks alongside your own, and use `classUnion`/`classIntersection` side by side to spot class-coverage gaps or overlaps worth a specific line item in the diligence memo.
- **Competitive filing-activity monitoring.** Re-run periodically (see **Integration recipes**) against a competitor's known brand names, diffing delivered `distinctRecordCount`/`classUnion` between runs to catch newly-filed classes since the last check — each scheduled run is new processing and bills again per the no-dedup replay policy.

### Integration recipes

**Apify Integration setup.** Map `{{resource.defaultDatasetId}}` into Source Dataset and configure the destination run’s Dataset READ grant. Optionally supply a matching resource envelope in payload; the envelope grants no access by itself.

**A bounded, explicit Apify API call** (using your own API token, never embedded in this README):

```
POST https://api.apify.com/v2/acts/zinin~trademark-class-overlap-review/runs?maxTotalChargeUsd=0.50
Authorization: Bearer <YOUR_API_TOKEN>
Content-Type: application/json

{
  "datasetId": "<upstream USPTO search Dataset ID you already control READ access to>",
  "maxInputRows": 200,
  "maxResults": 20
}
```

`datasetId` is a `resourcePicker`-backed field — the platform enforces that your API token's own READ permission on that Dataset is what is checked, never an elevated grant carried by this Actor. A `payload` envelope with no resolving `datasetId`/`runId` inside it is never sufficient permission on its own — see **Input contract**.

**A clearly-labelled webhook registration TEMPLATE** — an illustrative registration against Apify's own webhook API to be notified when a run finishes, **not a captured execution**, and it deliberately omits any real token:

```json
{
  "eventTypes": ["ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"],
  "condition": { "actorId": "<your-actor-id>" },
  "requestUrl": "https://your-own-endpoint.example.com/webhooks/trademark-class-overlap",
  "headersTemplate": "{\"Authorization\":\"Bearer <YOUR_OWN_ENDPOINT_SECRET>\"}",
  "payloadTemplate": "{\"runId\":\"{{resource.id}}\",\"datasetId\":\"{{resource.defaultDatasetId}}\",\"status\":\"{{resource.status}}\"}",
  "doNotRetry": true
}
```

`condition` is what scopes this webhook to this Actor's own runs only, never a global listener. `headersTemplate`, like `payloadTemplate`, is a JSON-encoded **string** in Apify's webhook API, not a nested object — the placeholder `Authorization` value shown here is illustrative only, never a real token, and is never placed in the `requestUrl` itself. `doNotRetry: true` is deliberate: because delivery is write-then-charge and non-transactional (see **Pricing**), an automatic webhook retry on your receiving endpoint could re-process an already-delivered run's notification as a new one. Treat a missed webhook as a signal to poll run status via the API, not a reason to auto-retry.

![Trademark Class Overlap Review — scheduled trigger through this Actor into an IP review queue or portfolio tracker](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/b5505823e5d52dba40d5daeb471d406383093f34/value-add-2-20260923/trademark-class-overlap-review/readme-workflow.webp)

**n8n / Make**: use the native Apify node (or a generic HTTP node against the API call above) to trigger a run on a schedule or the upstream search's completion webhook, then branch on each delivered row's `confidence`/`hasStatusDisagreement` field to route low-confidence or disagreeing groups into a review queue separate from clean, high-confidence rows.

**MCP / agent tooling**: plain JSON input/output with no product-specific authentication beyond the shared Dataset `resourcePicker` model, so it composes directly into an agent tool-call chain that already holds a USPTO search Dataset reference.

### Operating guide

- **Start small, then widen.** Run with a low `maxInputRows`/`maxResults` against a known batch first (the shipped input is visible before launch; running it incurs the configured useful-result charge), confirm the field shapes match your downstream system, then raise the caps for production.
- **Read `confidence` together with the two gap counts, not instead of them.** `invalidClassTokenCount` and `recordsWithNoParsedClasses` are always exact even at `confidence: 'medium'`; a nonzero value on a group you care about is worth opening the raw `classes` string in your own upstream data to see exactly what failed to parse.
- **A capped display list does not mean capped truth.** `recordConflictCount` and `evidenceOmitted` are always exact, independent of the 10/12-item display caps. A nonzero omitted count is signal enough to escalate, or partition that wordmark’s source rows into smaller batches and check omissions on each. One-wordmark filtering alone does not remove per-group caps; deduplicate overlapping partitions before combining counts.
- **`serial_identity_conflict` diagnostics are worth reading individually**, not just counting — each one names the exact conflicting wordmarks observed for one serial number, which is often enough to tell you whether the source data has a genuine data-entry problem or a legitimate historical name-change on the same registration.
- **Budget your run before you run it.** Set the platform's Max total charge to comfortably cover your intended `maxResults` at your tier; the preflight guard (see **Pricing**) fails an under-budgeted run cleanly and for free before reading any source.
- **Re-runs are not deduplicated.** For "only new registrations since last time," diff delivered `normalizedWordmark`/`distinctRecordCount` sets between runs downstream — replay safety only refuses double-billing of the *same* run's own already-confirmed results.
- **Treat every source value as untrusted text** — `status`, `wordmark`, and every string field are bounded and never interpreted as anything beyond display text on your side either.

### FAQ

**Does a high-confidence result mean this mark is clear to register?**
No. `confidence: 'high'` means the class evidence in this group is internally complete and consistent (no invalid tokens, no unparsed-class records, no record conflicts) — it says nothing about availability, infringement, or confusion with any other mark. See **Evidence and boundaries**.

**Does `classIntersection` being empty mean the mark has no registered classes?**
Not necessarily. Check `recordsWithNoParsedClasses` first — an empty intersection can mean the group's records genuinely share no common class, *or* that some records contributed no parseable class data at all. The two cases are distinguished explicitly, never conflated.

**Why did two search hits for what looks like the same mark end up in different groups?**
Grouping is exact NFKC-normalized, case-folded, whitespace-collapsed text — not phonetic or fuzzy matching. Any character-level difference (including one a human might read as "the same mark") produces a separate group. This is deliberate: see **Evidence and boundaries**.

**What does a `serial_identity_conflict` diagnostic mean?**
The same `serial_number` was observed with more than one distinct wordmark across your batch — a genuine data disagreement in the source. That serial is excluded from every wordmark group rather than guessed into one; see **Decision routing**.

**Why did my run deliver fewer paid rows than expected?**
Check the free diagnostics: `max_results_reached` states exact `delivered`/`totalUseful` (raise `maxResults` and re-run — bills again for the additional rows only); `budget_stopped` means your charge ceiling was reached; `partial_source` means the source batch was bounded.

**What happens if a `classes` string has an invalid token mixed with valid ones, like `"9 bad 25"`?**
The valid tokens (9, 25) still contribute to `classIntersection`/`classUnion`; the invalid token (`bad`) is counted in `invalidClassTokenCount`. This also forces `confidence` down from `'high'` and `partial: true` on the affected group.

**Is this Actor's result a trademark clearance opinion?**
No. It is a reconciliation and triage worksheet for a human reviewer or attorney — never a legal-clearance, availability, or infringement determination. Pair it with a formal clearance search and counsel's own review for an actual legal opinion.

### Sources and rights

Source data originates from the USPTO's public trademark records, via the `dev00/uspto-trademark-api` Actor's documented output shape. This Actor never calls the USPTO API or website directly — it only transforms rows you already collected through your own upstream source.

The inline example shipped with this Actor's input schema is a **real, curated projection** of an actual `dev00/uspto-trademark-api` run (`actorId nhSY7qeLJUsvkXQTH`, `runId 1UQYwy4rLd96omF0V`, `datasetId bV1vfVZVsUCA3PT1R`, captured 2026-09-23) — public trademark-record fields only, values unchanged, with an added `__source` object disclosing provenance. It is a **snapshot**, not a live query on every run of this Actor; re-run the upstream search yourself for current data.

USPTO trademark records are US federal public records. This README makes no revenue, accuracy, or legal-compliance guarantee beyond the documented, testable behavior of the transform, and nothing here constitutes legal, trademark-clearance, or investment advice — a formal clearance opinion requires a licensed attorney's own review of the primary USPTO record.

**Related Zinin/* Actors you may already have in your account*\* (each independently public and priced — adjacent workflow context, not a claim their rows are directly compatible input to this Actor):

- **[zinin/patent-monitor](https://apify.com/zinin/patent-monitor)** — a separate, independently-sourced patent-filing watch for the same company or inventor names; useful alongside a trademark portfolio review for a broader IP picture, no shared data model.
- **[zinin/company-registry-enricher](https://apify.com/zinin/company-registry-enricher)** — company-registry lookups to confirm the legal entity actually behind a trademark applicant/registrant name before a licensing or M\&A review.
- **[zinin/litigation-check](https://apify.com/zinin/litigation-check)** — litigation-evidence Actor you can run against the same named companies or mark owners for a broader due-diligence picture; run as its own separate pass.
- **[zinin/sec-edgar-watcher](https://apify.com/zinin/sec-edgar-watcher)** — for a US-listed licensor or licensee, a complementary SEC-filing watch; no shared input format, point it at its own source.

# Actor input Schema

## `items` (type: `array`):

Use the saved real source example, paste compatible records, or select an existing Dataset/run. Public examples omit personal addresses, birth dates and nationality.

## `payload` (type: `object`):

Optional. Expected shape: { "resource": { "id": "<run id>", "defaultDatasetId": "<dataset id>", "actId": "<actor id>" } }. Ignored fields are harmless; a malformed resource.\* value is rejected, never silently ignored.

## `datasetId` (type: `string`):

Read an existing source Dataset with the permissions granted to this run. Overrides inline items. For Integrations, map {{resource.defaultDatasetId}} to this resourcePicker field. A webhook payload alone does not grant private Dataset access.

## `runId` (type: `string`):

Optional Actor run ID to read instead of Source Dataset. Its default Dataset is resolved with your run's own permissions — no elevated access is used, and no upstream Actor is started.

## `maxInputRows` (type: `integer`):

At most 1000 input rows, 4 MiB total and 64 KiB per source row. Coverage limits are reported; aggregate evidence is bounded separately.

## `maxResults` (type: `integer`):

Maximum useful mark assessment rows delivered; free diagnostics are separate. The platform Max total charge may stop delivery earlier.

## Actor input object example

```json
{
  "items": [
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 73149856,
      "status": "LIVE",
      "filing_date": "1977-11-25",
      "registration_date": "1979-10-30",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 0,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    },
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 73487823,
      "status": "LIVE",
      "filing_date": "1984-06-29",
      "registration_date": "1985-11-19",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 1,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    },
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 75306994,
      "status": "LIVE",
      "filing_date": "1997-06-11",
      "registration_date": "2002-01-29",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 2,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    }
  ],
  "payload": {},
  "maxInputRows": 100,
  "maxResults": 2
}
```

# Actor output Schema

## `results` (type: `string`):

API URL for the default dataset items produced by this run.

## `summary` (type: `string`):

API URL for the OUTPUT record in the default key-value store.

# 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 = {
    "items": [
        {
            "success": true,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 73149856,
            "status": "LIVE",
            "filing_date": "1977-11-25",
            "registration_date": "1979-10-30",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 0,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
            }
        },
        {
            "success": true,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 73487823,
            "status": "LIVE",
            "filing_date": "1984-06-29",
            "registration_date": "1985-11-19",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 1,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
            }
        },
        {
            "success": true,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 75306994,
            "status": "LIVE",
            "filing_date": "1997-06-11",
            "registration_date": "2002-01-29",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 2,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/trademark-class-overlap-review").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 = { "items": [
        {
            "success": True,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 73149856,
            "status": "LIVE",
            "filing_date": "1977-11-25",
            "registration_date": "1979-10-30",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 0,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload.",
            },
        },
        {
            "success": True,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 73487823,
            "status": "LIVE",
            "filing_date": "1984-06-29",
            "registration_date": "1985-11-19",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 1,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload.",
            },
        },
        {
            "success": True,
            "query_phrase": "star wars",
            "trademark_wordmark": "star wars",
            "serial_number": 75306994,
            "status": "LIVE",
            "filing_date": "1997-06-11",
            "registration_date": "2002-01-29",
            "classes": "25",
            "__source": {
                "actor": "dev00/uspto-trademark-api",
                "actorId": "nhSY7qeLJUsvkXQTH",
                "runId": "1UQYwy4rLd96omF0V",
                "datasetId": "bV1vfVZVsUCA3PT1R",
                "sampleRowIndex": 2,
                "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload.",
            },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("zinin/trademark-class-overlap-review").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 '{
  "items": [
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 73149856,
      "status": "LIVE",
      "filing_date": "1977-11-25",
      "registration_date": "1979-10-30",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 0,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    },
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 73487823,
      "status": "LIVE",
      "filing_date": "1984-06-29",
      "registration_date": "1985-11-19",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 1,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    },
    {
      "success": true,
      "query_phrase": "star wars",
      "trademark_wordmark": "star wars",
      "serial_number": 75306994,
      "status": "LIVE",
      "filing_date": "1997-06-11",
      "registration_date": "2002-01-29",
      "classes": "25",
      "__source": {
        "actor": "dev00/uspto-trademark-api",
        "actorId": "nhSY7qeLJUsvkXQTH",
        "runId": "1UQYwy4rLd96omF0V",
        "datasetId": "bV1vfVZVsUCA3PT1R",
        "sampleRowIndex": 2,
        "projection": "Public business-record fields only; observed values unchanged. Added __source is provenance, not upstream payload."
      }
    }
  ]
}' |
apify call zinin/trademark-class-overlap-review --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/trademark-class-overlap-review"
        }
    }
}
```

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/aN5sDzYGCypz8WAHm/builds/0IRrdBTC4dP6kAVSL/openapi.json
