@radsoft/markbase 2026.8.2 → 2026.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +254 -75
  2. package/dist/client.js +22 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,152 +1,331 @@
1
1
  # @radsoft/markbase
2
2
 
3
- Official Node client for the [Markbase](https://markbase.co) USPTO trademark API.
4
- Search, screen, and look up 14M+ US trademarks with full TypeScript types.
5
-
6
- Covers the **USPTO only**. If you need EUIPO, WIPO, or UKIPO, a multi-office
7
- platform is a better fit.
3
+ Node client for the [Markbase](https://markbase.co) USPTO trademark API — search,
4
+ screen, and look up **14M+ US trademarks**. Fully typed, zero dependencies.
8
5
 
9
6
  ```bash
10
7
  npm i @radsoft/markbase
11
8
  ```
12
9
 
13
- ## Quick start
10
+ Covers the **USPTO only**. If you need EUIPO, WIPO, or UKIPO, a multi-office
11
+ platform is a better fit — we'd rather say so than waste your time.
12
+
13
+ ---
14
+
15
+ ## 30 seconds
14
16
 
15
- No key needed to try it — the keyless tier allows 1,000 requests a month.
16
- [Get a free key](https://markbase.co/login) for 5,000.
17
+ No key needed to try it. The keyless tier allows 1,000 requests a month;
18
+ [a free account](https://markbase.co/login) raises that to 5,000.
17
19
 
18
20
  ```ts
19
21
  import { Markbase } from "@radsoft/markbase";
20
22
 
21
- const mb = new Markbase({ apiKey: process.env.MARKBASE_API_KEY });
23
+ const mb = new Markbase(); // reads MARKBASE_API_KEY if set
22
24
 
23
- // "can we call it this?"
24
- const result = await mb.clearance("Acme", { trademarkClass: 9 });
25
- if (result.highest_risk === "high") {
26
- console.log("Conflicts:", result.matches.map((m) => m.word_mark));
27
- }
25
+ const { highest_risk, matches } = await mb.clearance("nikee", {
26
+ trademarkClass: 25, // clothing
27
+ });
28
+
29
+ console.log(highest_risk); // "high"
30
+ console.log(matches[0].word_mark); // "NIKE"
31
+ console.log(matches[0].owner_name);// "Nike, Inc."
32
+ console.log(matches[0].similarity);// 0.85
28
33
  ```
29
34
 
30
- The key is read from `MARKBASE_API_KEY` if you don't pass one. Pass `apiKey: null`
31
- to force the keyless tier even when that variable is set.
35
+ ---
36
+
37
+ ## The one thing to get right: classes
38
+
39
+ Trademark rights are scoped to **goods and services**, not to names. DELTA
40
+ coexists as an airline, a faucet maker, and dental insurance — same word, three
41
+ owners, no conflict.
32
42
 
33
- ## Screening a name
43
+ So screening without a class gives you noise:
34
44
 
35
- Trademark rights are scoped to goods and services — DELTA coexists as an airline,
36
- a faucet maker, and dental insurance. **Always pass a class**, or you get noise
37
- instead of blockers.
45
+ ```ts
46
+ await mb.clearance("Delta"); // ~everything, mostly irrelevant
47
+ await mb.clearance("Delta", { trademarkClass: 9 }); // only what could block you
48
+ ```
49
+
50
+ There are 45 classes. Fetch them to build a picker:
38
51
 
39
52
  ```ts
40
- const { highest_risk, matches } = await mb.clearance("nikee", { trademarkClass: 25 });
41
- // highest_risk: "high"
42
- // matches[0]: { word_mark: "NIKE", owner_name: "Nike, Inc.", similarity: 0.85, risk: "high" }
53
+ const classes = await mb.classes();
54
+ // [{ code: "009", title: "Computers & electronics" }, ...]
43
55
  ```
44
56
 
45
- `mb.classes()` returns all 45 Nice classes if you need to pick one.
57
+ Common ones: **9** software · **25** clothing · **35** advertising & business ·
58
+ **41** education & entertainment · **42** science & technology services (where
59
+ most SaaS sits).
46
60
 
47
- ## Searching
61
+ ---
48
62
 
49
- Typo-tolerant — `gogle` finds Google's marks.
63
+ ## Recipes
64
+
65
+ ### Screen names from a generator
66
+
67
+ The usual shape: generate candidates, drop the ones that are obviously taken.
50
68
 
51
69
  ```ts
52
- const res = await mb.search({ q: "gogle", limit: 10 });
70
+ import { Markbase } from "@radsoft/markbase";
53
71
 
54
- // filters
55
- await mb.search({
56
- q: "acme",
57
- trademarkClass: 9, // 9 and "009" both work
58
- statusCode: "800", // registered
59
- ownerCountry: "US",
60
- filingDateFrom: "20200101",
72
+ const mb = new Markbase();
73
+
74
+ async function viable(names: string[], trademarkClass: number) {
75
+ const checks = await Promise.all(
76
+ names.map(async (name) => {
77
+ const { highest_risk } = await mb.clearance(name, { trademarkClass });
78
+ return { name, risk: highest_risk };
79
+ })
80
+ );
81
+ return checks.filter((c) => c.risk === "low" || c.risk === "none");
82
+ }
83
+
84
+ await viable(["Zeplo", "Nikee", "Flowmint"], 25);
85
+ // [{ name: "Zeplo", risk: "low" }, { name: "Flowmint", risk: "low" }]
86
+ // "Nikee" is dropped — high risk against NIKE in class 25
87
+ ```
88
+
89
+ ### Flag conflicts at checkout
90
+
91
+ For a registrar or a "claim your brand" flow — warn before they pay.
92
+
93
+ ```ts
94
+ const { highest_risk, matches } = await mb.clearance(brand, {
95
+ trademarkClass: 42,
96
+ limit: 3,
61
97
  });
98
+
99
+ if (highest_risk === "high") {
100
+ return {
101
+ warn: `“${matches[0].word_mark}” is already registered by ${matches[0].owner_name}.`,
102
+ conflicts: matches,
103
+ };
104
+ }
62
105
  ```
63
106
 
64
- `total_hits` is capped at 1,000 by the search engine — narrow with filters when
65
- you need an exact count, or use `aggregate()`.
107
+ ### Watch a competitor's portfolio
108
+
109
+ ```ts
110
+ const { hits } = await mb.byOwner("Nike, Inc.", { limit: 50 });
66
111
 
67
- Walk large result sets without managing pages:
112
+ const live = hits.filter((m) => statusOf(m.status_code) === "registered");
113
+ console.log(`${live.length} live in this page`);
114
+
115
+ // note: total_hits saturates at 1000 for large portfolios — page through with
116
+ // the `page` option, or use aggregate() if you need a true count
117
+ ```
118
+
119
+ ### Analyse a whole class
120
+
121
+ `total_hits` caps at 1,000, so use `aggregate()` when you want real numbers.
122
+
123
+ ```ts
124
+ const { buckets } = await mb.aggregate("class");
125
+ // [{ key: "009", count: 1839467 }, { key: "035", count: 1437003 }, ...]
126
+
127
+ const byCountry = await mb.aggregate("country");
128
+ const byYear = await mb.aggregate("year");
129
+ ```
130
+
131
+ ### Walk a large result set
132
+
133
+ `paginate()` handles the page arithmetic and stops when results run out.
68
134
 
69
135
  ```ts
70
136
  for await (const mark of mb.paginate({ q: "coffee", limit: 100 }, 5)) {
71
- console.log(mark.word_mark);
137
+ console.log(mark.serial_number, mark.word_mark);
138
+ }
139
+ ```
140
+
141
+ ### In a Next.js route handler
142
+
143
+ ```ts
144
+ // app/api/check/route.ts
145
+ import { Markbase } from "@radsoft/markbase";
146
+
147
+ const mb = new Markbase({ apiKey: process.env.MARKBASE_API_KEY });
148
+
149
+ export async function POST(req: Request) {
150
+ const { name, trademarkClass } = await req.json();
151
+ const result = await mb.clearance(name, { trademarkClass });
152
+ return Response.json(result);
72
153
  }
73
154
  ```
74
155
 
75
- ## Everything else
156
+ Keep the key server-side. It's a secret; don't ship it to the browser.
157
+
158
+ ---
159
+
160
+ ## Reference
161
+
162
+ | Method | Returns |
163
+ |---|---|
164
+ | `search(opts \| string)` | `SearchResult` — typo-tolerant full-text search |
165
+ | `clearance(mark, opts?)` | `ClearanceResult` — ranked conflicts with a risk level |
166
+ | `suggest(q, limit?)` | `Suggestion[]` — autocomplete, tuned for as-you-type |
167
+ | `getTrademark(serial)` | `Trademark` — one full record |
168
+ | `getTimeline(serial)` | prosecution history |
169
+ | `byOwner(name, opts?)` | `OwnerResult` — an owner's portfolio |
170
+ | `getBatch(serials)` | `BatchResult` — up to 50 at once (**Scale plan**) |
171
+ | `aggregate(field)` | `AggregateResult` — counts by class/status/country/drawing/year |
172
+ | `classes()` | `NiceClass[]` — all 45 |
173
+ | `info()` | `IndexInfo` — index size and freshness |
174
+ | `paginate(opts, maxPages?)` | `AsyncGenerator<Trademark>` |
175
+
176
+ ### Search options
76
177
 
77
178
  ```ts
78
- await mb.suggest("nik", 5); // autocomplete, tuned for as-you-type
79
- await mb.getTrademark("72414177"); // one full record
80
- await mb.getTimeline("72414177"); // prosecution history
81
- await mb.byOwner("Nike, Inc."); // an owner's whole portfolio
82
- await mb.aggregate("class"); // distribution counts
83
- await mb.info(); // index size and freshness
84
- await mb.getBatch([...]); // up to 50 at once — Scale plan only
179
+ await mb.search({
180
+ q: "acme", // typo-tolerant: "gogle" finds Google
181
+ trademarkClass: 9, // 9, "009", or [9, 42] all work
182
+ statusCode: "800", // 800 = registered
183
+ ownerCountry: "US",
184
+ ownerState: "CA",
185
+ filingDateFrom: "20200101", // YYYYMMDD
186
+ filingDateTo: "20251231",
187
+ sort: "filing_date:desc",
188
+ page: 1,
189
+ limit: 50,
190
+ facets: true, // include distribution counts
191
+ });
85
192
  ```
86
193
 
87
- ## Helpers
194
+ ### Helpers
88
195
 
89
196
  USPTO records are raw. These make them presentable:
90
197
 
91
198
  ```ts
92
- import { statusOf, parseDate } from "@radsoft/markbase";
93
-
94
- statusOf("800"); // "registered" (600s are dead, 710s cancelled, 900+ expired)
95
- parseDate("19720131"); // Date — USPTO stores YYYYMMDD strings
199
+ import { statusOf, parseDate, padClass } from "@radsoft/markbase";
200
+
201
+ statusOf("800"); // "registered"
202
+ statusOf("630"); // "pending" — a new application, not a dead one
203
+ statusOf("601"); // "dead"
204
+ statusOf("710"); // "cancelled"
205
+ parseDate("19720131"); // Date(1972-01-31) — the register stores YYYYMMDD
206
+ padClass(9); // "009"
96
207
  ```
97
208
 
98
- ## Errors and limits
209
+ ---
210
+
211
+ ## Errors
99
212
 
100
- Typed errors, so you can branch on what actually went wrong:
213
+ Typed, so you can branch on what actually went wrong rather than parsing strings.
101
214
 
102
215
  ```ts
103
- import { RateLimitError, PlanError, AuthError, NotFoundError } from "@radsoft/markbase";
216
+ import {
217
+ MarkbaseError, AuthError, PlanError, RateLimitError, NotFoundError,
218
+ } from "@radsoft/markbase";
104
219
 
105
220
  try {
106
221
  await mb.getBatch(serials);
107
222
  } catch (err) {
108
223
  if (err instanceof PlanError) {
109
- // batch is Scale-only
224
+ // endpoint isn't on this plan — batch is Scale-only
110
225
  } else if (err instanceof RateLimitError && err.kind === "quota") {
111
- // monthly allowance gone — err.resetsAt tells you when it returns.
112
- // retrying sooner won't help, which is why the client doesn't.
226
+ // month's allowance is gone; err.resetsAt says when it returns
227
+ } else if (err instanceof AuthError) {
228
+ // bad or missing key
229
+ } else if (err instanceof NotFoundError) {
230
+ // no such serial number
113
231
  }
114
232
  }
115
233
  ```
116
234
 
117
- Per-minute limits and 5xx are retried automatically with backoff (`maxRetries`,
118
- default 2). Monthly quota exhaustion is not — waiting seconds can't fix a
119
- month-long window.
235
+ | Error | When |
236
+ |---|---|
237
+ | `AuthError` | 401 — key missing or invalid |
238
+ | `PlanError` | 403 — endpoint not on your plan |
239
+ | `NotFoundError` | 404 — no such record |
240
+ | `RateLimitError` | 429 — `kind` is `"rate"` or `"quota"` |
241
+ | `MarkbaseError` | anything else, including network failures (`status: 0`) |
242
+
243
+ ---
120
244
 
121
- Current limits are on the client after any call:
245
+ ## Rate limits and quota
246
+
247
+ Per-minute limits and 5xx are **retried automatically** with backoff
248
+ (`maxRetries`, default 2).
249
+
250
+ Monthly quota exhaustion is **not retried** — waiting a few seconds can't fix a
251
+ month-long window, so it throws immediately with `resetsAt` rather than burning
252
+ your retries on a call that can't succeed.
253
+
254
+ Live limits sit on the client after any call:
122
255
 
123
256
  ```ts
257
+ await mb.search({ q: "nike" });
258
+
124
259
  mb.rateLimit;
125
- // { plan: "launch", rateLimit: 120, rateRemaining: 119,
126
- // quotaLimit: 50000, quotaRemaining: 49873, quotaResetsAt: Date }
260
+ // {
261
+ // plan: "launch",
262
+ // rateLimit: 120, rateRemaining: 119,
263
+ // quotaLimit: 50000, quotaRemaining: 49873,
264
+ // quotaResetsAt: Date
265
+ // }
127
266
  ```
128
267
 
268
+ Useful for backing off before you hit the wall:
269
+
270
+ ```ts
271
+ if (mb.rateLimit && mb.rateLimit.quotaRemaining !== null && mb.rateLimit.quotaRemaining < 100) {
272
+ console.warn("Nearly out of quota this month");
273
+ }
274
+ ```
275
+
276
+ ---
277
+
129
278
  ## Options
130
279
 
131
- | Option | Default | |
132
- |---|---|---|
133
- | `apiKey` | `MARKBASE_API_KEY` | `null` forces keyless |
134
- | `baseUrl` | `https://api.markbase.co` | |
135
- | `timeout` | `30000` | ms before abort |
136
- | `maxRetries` | `2` | rate limits and 5xx |
137
- | `fetch` | global `fetch` | inject your own |
280
+ ```ts
281
+ new Markbase({
282
+ apiKey: process.env.MARKBASE_API_KEY, // null forces the keyless tier
283
+ baseUrl: "https://api.markbase.co",
284
+ timeout: 30_000, // ms before abort
285
+ maxRetries: 2, // rate limits and 5xx
286
+ fetch: customFetch, // inject your own
287
+ });
288
+
289
+ new Markbase("mb_your_key"); // shorthand
290
+ ```
291
+
292
+ `apiKey` falls back to `MARKBASE_API_KEY`. Pass `null` to force keyless even
293
+ when that variable is set.
294
+
295
+ ---
296
+
297
+ ## TypeScript
298
+
299
+ Ships with declarations — no `@types` package needed.
300
+
301
+ ```ts
302
+ import type {
303
+ Trademark, SearchResult, ClearanceResult, ClearanceMatch,
304
+ MarkStatus, RiskLevel, NiceClass, RateLimitInfo,
305
+ } from "@radsoft/markbase";
306
+
307
+ function render(m: ClearanceMatch): string {
308
+ return `${m.word_mark} — ${m.risk} risk (${m.similarity})`;
309
+ }
310
+ ```
311
+
312
+ ---
138
313
 
139
314
  ## Not legal advice
140
315
 
141
- Clearance returns a first-pass screening signal over the federal register. It
316
+ Clearance returns a **first-pass screening signal** over the federal register. It
142
317
  does not cover state trademarks, common-law rights, or the legal analysis of
143
- likelihood of confusion. Treat a clean result as "worth pursuing", not
144
- "cleared", and talk to a trademark attorney before adopting a name.
318
+ likelihood of confusion — no API does.
319
+
320
+ Treat a clean result as *worth pursuing*, not *cleared*, and put a trademark
321
+ attorney at the end of the funnel for any name you actually intend to adopt.
322
+
323
+ ---
145
324
 
146
325
  ## Also available
147
326
 
148
- - **[@radsoft/markbase-mcp](https://www.npmjs.com/package/@radsoft/markbase-mcp)** — MCP server, so an AI assistant can screen names directly
149
- - **[API docs](https://api.markbase.co/docs)** · **[OpenAPI spec](https://api.markbase.co/openapi.json)**
327
+ - **[@radsoft/markbase-mcp](https://www.npmjs.com/package/@radsoft/markbase-mcp)** — MCP server, so Claude and other assistants can screen names directly
328
+ - **[API docs](https://api.markbase.co/docs)** · **[OpenAPI spec](https://api.markbase.co/openapi.json)** · **[Trademark classes](https://markbase.co/trademark-classes)**
150
329
 
151
330
  ## License
152
331
 
package/dist/client.js CHANGED
@@ -4,24 +4,30 @@ export function padClass(v) {
4
4
  const s = String(v);
5
5
  return /^\d+$/.test(s) ? s.padStart(3, "0") : s;
6
6
  }
7
+ // uspto status codes don't fall into tidy ranges — 630 is a brand new
8
+ // application and 602 an abandoned one. groups follow uspto.report's live/dead
9
+ // table, plus 610 (terminated after sanctions) and 781-783 (newer cancellations)
10
+ // checked against tsdr. anything not listed is unknown rather than a guess.
11
+ const STATUS_GROUPS = {
12
+ pending: "410 413 616 620 630 631 638 640-661 663-668 672 680-682 686 688-690 692-694 " +
13
+ "718-722 724 725 730-734 740 744-746 748 752 753 756 757 760 762-766 771-775 " +
14
+ "777 779 794 801-804 806-825 969 973",
15
+ registered: "624 625 700-708 717 739 778 780 790 800",
16
+ dead: "402 411 412 416 600-610 612 614 618 632 901",
17
+ cancelled: "400 401 403-406 414 415 417 626 709-715 781-783",
18
+ expired: "900",
19
+ };
20
+ const STATUS_BY_CODE = new Map();
21
+ for (const [status, list] of Object.entries(STATUS_GROUPS)) {
22
+ for (const part of list.split(" ")) {
23
+ const [from, to = from] = part.split("-").map(Number);
24
+ for (let n = from; n <= to; n++)
25
+ STATUS_BY_CODE.set(n, status);
26
+ }
27
+ }
7
28
  /** Turn a numeric USPTO status code into something you can show a user. */
8
29
  export function statusOf(code) {
9
- if (!code)
10
- return "unknown";
11
- const n = parseInt(code, 10);
12
- if (Number.isNaN(n))
13
- return "unknown";
14
- if (n >= 600 && n < 700)
15
- return "dead";
16
- if (n >= 710 && n < 720)
17
- return "cancelled";
18
- if (n >= 900)
19
- return "expired";
20
- if (n >= 800 || n === 700 || (n >= 730 && n < 740))
21
- return "registered";
22
- if (n < 600)
23
- return "pending";
24
- return "registered";
30
+ return STATUS_BY_CODE.get(parseInt(code ?? "", 10)) ?? "unknown";
25
31
  }
26
32
  /** USPTO dates are YYYYMMDD strings; this gives you a Date or null. */
27
33
  export function parseDate(d) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radsoft/markbase",
3
- "version": "2026.8.2",
3
+ "version": "2026.10.1",
4
4
  "description": "Official Node client for the Markbase USPTO trademark API — search, screen, and look up 14M+ US trademarks.",
5
5
  "license": "MIT",
6
6
  "type": "module",