@radsoft/markbase 2026.8.2 → 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.
- package/README.md +252 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,152 +1,330 @@
|
|
|
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);
|
|
72
138
|
}
|
|
73
139
|
```
|
|
74
140
|
|
|
75
|
-
|
|
141
|
+
### In a Next.js route handler
|
|
76
142
|
|
|
77
143
|
```ts
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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"
|
|
95
|
-
|
|
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
|
-
|
|
208
|
+
---
|
|
99
209
|
|
|
100
|
-
|
|
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 {
|
|
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
|
-
//
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
// {
|
|
126
|
-
//
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
144
|
-
|
|
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
|
|
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