@radsoft/markbase 2026.8.1 → 2026.8.3

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 (2) hide show
  1. package/README.md +252 -74
  2. package/package.json +7 -3
package/README.md CHANGED
@@ -1,152 +1,330 @@
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);
72
138
  }
73
139
  ```
74
140
 
75
- ## Everything else
141
+ ### In a Next.js route handler
76
142
 
77
143
  ```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
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);
153
+ }
85
154
  ```
86
155
 
87
- ## Helpers
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
177
+
178
+ ```ts
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
+ });
192
+ ```
193
+
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";
199
+ import { statusOf, parseDate, padClass } from "@radsoft/markbase";
93
200
 
94
- statusOf("800"); // "registered" (600s are dead, 710s cancelled, 900+ expired)
95
- parseDate("19720131"); // Date — USPTO stores YYYYMMDD strings
201
+ statusOf("800"); // "registered"
202
+ statusOf("601"); // "dead"
203
+ statusOf("710"); // "cancelled"
204
+ parseDate("19720131"); // Date(1972-01-31) — the register stores YYYYMMDD
205
+ padClass(9); // "009"
96
206
  ```
97
207
 
98
- ## Errors and limits
208
+ ---
99
209
 
100
- Typed errors, so you can branch on what actually went wrong:
210
+ ## Errors
211
+
212
+ Typed, so you can branch on what actually went wrong rather than parsing strings.
101
213
 
102
214
  ```ts
103
- import { RateLimitError, PlanError, AuthError, NotFoundError } from "@radsoft/markbase";
215
+ import {
216
+ MarkbaseError, AuthError, PlanError, RateLimitError, NotFoundError,
217
+ } from "@radsoft/markbase";
104
218
 
105
219
  try {
106
220
  await mb.getBatch(serials);
107
221
  } catch (err) {
108
222
  if (err instanceof PlanError) {
109
- // batch is Scale-only
223
+ // endpoint isn't on this plan — batch is Scale-only
110
224
  } 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.
225
+ // month's allowance is gone; err.resetsAt says when it returns
226
+ } else if (err instanceof AuthError) {
227
+ // bad or missing key
228
+ } else if (err instanceof NotFoundError) {
229
+ // no such serial number
113
230
  }
114
231
  }
115
232
  ```
116
233
 
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.
234
+ | Error | When |
235
+ |---|---|
236
+ | `AuthError` | 401 — key missing or invalid |
237
+ | `PlanError` | 403 — endpoint not on your plan |
238
+ | `NotFoundError` | 404 — no such record |
239
+ | `RateLimitError` | 429 — `kind` is `"rate"` or `"quota"` |
240
+ | `MarkbaseError` | anything else, including network failures (`status: 0`) |
241
+
242
+ ---
120
243
 
121
- Current limits are on the client after any call:
244
+ ## Rate limits and quota
245
+
246
+ Per-minute limits and 5xx are **retried automatically** with backoff
247
+ (`maxRetries`, default 2).
248
+
249
+ Monthly quota exhaustion is **not retried** — waiting a few seconds can't fix a
250
+ month-long window, so it throws immediately with `resetsAt` rather than burning
251
+ your retries on a call that can't succeed.
252
+
253
+ Live limits sit on the client after any call:
122
254
 
123
255
  ```ts
256
+ await mb.search({ q: "nike" });
257
+
124
258
  mb.rateLimit;
125
- // { plan: "launch", rateLimit: 120, rateRemaining: 119,
126
- // quotaLimit: 50000, quotaRemaining: 49873, quotaResetsAt: Date }
259
+ // {
260
+ // plan: "launch",
261
+ // rateLimit: 120, rateRemaining: 119,
262
+ // quotaLimit: 50000, quotaRemaining: 49873,
263
+ // quotaResetsAt: Date
264
+ // }
265
+ ```
266
+
267
+ Useful for backing off before you hit the wall:
268
+
269
+ ```ts
270
+ if (mb.rateLimit && mb.rateLimit.quotaRemaining !== null && mb.rateLimit.quotaRemaining < 100) {
271
+ console.warn("Nearly out of quota this month");
272
+ }
127
273
  ```
128
274
 
275
+ ---
276
+
129
277
  ## Options
130
278
 
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 |
279
+ ```ts
280
+ new Markbase({
281
+ apiKey: process.env.MARKBASE_API_KEY, // null forces the keyless tier
282
+ baseUrl: "https://api.markbase.co",
283
+ timeout: 30_000, // ms before abort
284
+ maxRetries: 2, // rate limits and 5xx
285
+ fetch: customFetch, // inject your own
286
+ });
287
+
288
+ new Markbase("mb_your_key"); // shorthand
289
+ ```
290
+
291
+ `apiKey` falls back to `MARKBASE_API_KEY`. Pass `null` to force keyless even
292
+ when that variable is set.
293
+
294
+ ---
295
+
296
+ ## TypeScript
297
+
298
+ Ships with declarations — no `@types` package needed.
299
+
300
+ ```ts
301
+ import type {
302
+ Trademark, SearchResult, ClearanceResult, ClearanceMatch,
303
+ MarkStatus, RiskLevel, NiceClass, RateLimitInfo,
304
+ } from "@radsoft/markbase";
305
+
306
+ function render(m: ClearanceMatch): string {
307
+ return `${m.word_mark} — ${m.risk} risk (${m.similarity})`;
308
+ }
309
+ ```
310
+
311
+ ---
138
312
 
139
313
  ## Not legal advice
140
314
 
141
- Clearance returns a first-pass screening signal over the federal register. It
315
+ Clearance returns a **first-pass screening signal** over the federal register. It
142
316
  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.
317
+ likelihood of confusion — no API does.
318
+
319
+ Treat a clean result as *worth pursuing*, not *cleared*, and put a trademark
320
+ attorney at the end of the funnel for any name you actually intend to adopt.
321
+
322
+ ---
145
323
 
146
324
  ## Also available
147
325
 
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)**
326
+ - **[@radsoft/markbase-mcp](https://www.npmjs.com/package/@radsoft/markbase-mcp)** — MCP server, so Claude and other assistants can screen names directly
327
+ - **[API docs](https://api.markbase.co/docs)** · **[OpenAPI spec](https://api.markbase.co/openapi.json)** · **[Trademark classes](https://markbase.co/trademark-classes)**
150
328
 
151
329
  ## License
152
330
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radsoft/markbase",
3
- "version": "2026.8.1",
3
+ "version": "2026.8.3",
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",
@@ -12,7 +12,9 @@
12
12
  "default": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": ["dist"],
15
+ "files": [
16
+ "dist"
17
+ ],
16
18
  "keywords": [
17
19
  "trademark",
18
20
  "uspto",
@@ -29,7 +31,9 @@
29
31
  "url": "git+https://github.com/Rad-Soft/marked-api.git",
30
32
  "directory": "sdk"
31
33
  },
32
- "publishConfig": { "access": "public" },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
33
37
  "scripts": {
34
38
  "build": "tsc",
35
39
  "dev": "tsc --watch",