@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.
- package/README.md +254 -75
- package/dist/client.js +22 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,152 +1,331 @@
|
|
|
1
1
|
# @radsoft/markbase
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
[
|
|
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(
|
|
23
|
+
const mb = new Markbase(); // reads MARKBASE_API_KEY if set
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
43
|
+
So screening without a class gives you noise:
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
41
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
---
|
|
48
62
|
|
|
49
|
-
|
|
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
|
-
|
|
70
|
+
import { Markbase } from "@radsoft/markbase";
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
107
|
+
### Watch a competitor's portfolio
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const { hits } = await mb.byOwner("Nike, Inc.", { limit: 50 });
|
|
66
111
|
|
|
67
|
-
|
|
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
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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"
|
|
95
|
-
|
|
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
|
-
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Errors
|
|
99
212
|
|
|
100
|
-
Typed
|
|
213
|
+
Typed, so you can branch on what actually went wrong rather than parsing strings.
|
|
101
214
|
|
|
102
215
|
```ts
|
|
103
|
-
import {
|
|
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
|
-
//
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
// {
|
|
126
|
-
//
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
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