TLDR: Before filing an entity name, I built a two-step verification pipeline — TX open-data API signal + USPTO Playwright knockout — but the most important engineering decision wasn't the scraping. It was the output contract: signals, not legal clearance.
The Problem
I'm building an entity-name-clearance tool for my business partner, a lawyer who does nonprofit formations and entity filings in Texas (and beyond).
Her workflow right now: check if a name is taken, then file.
The problem is doing that first step fast, consistently, without her clicking through three different government websites for every client.
So I needed to chain two checks before anything hits a Secretary of State form:
- Is the name already taken in Texas?
- Does a federal trademark conflict exist?
What I Assumed Would Work (and Didn't)
I figured there'd be a clean USPTO API. There isn't.
The tmsearch API paths 404. TSDR needs a key. The SPA at tmsearch.uspto.gov is the real interface — and form.submit() does NOT trigger it. Clicking the button doesn't either. The only thing that works is dispatching a real Enter keystroke sequence (keydown / keypress / keyup) on the searchbar. That's what a human does, so that's what you have to replicate.
I used Playwright (a headless browser automation library) to drive it, which let this run headless and portable — no browser open on my machine required.
The TX Side Is a Different Gotcha
Texas has no free fuzzy name search. The official ruling comes from SOSDirect, which is a login-gated, pay-per-search system. That's the actual authoritative source.
What is free: the TX Open Data Socrata dataset of active franchise taxpayers. It's a proxy. It's useful. But it is not the official availability determination.
I encoded that as a hard caveat in the tool output — not buried in a README, actually surfaced in the response.
The Normalization That Made the Signal Real
Raw trademark searches are noise-heavy.
Searching a coined name with its "Holdings LLC" suffix returned 30,463 results. After stripping the entity suffix, articles, and punctuation down to the normalized distinctive name, it dropped to 1,473. Still a lot — but now you're looking at the real signal.
The knockout logic: CONFLICT only on an exact normalized match against a LIVE mark in the page-1 results. The broad count is context, not verdict. A number like 23,925 isn't a conflict — it's noise from a common word.
The Phase-0 Gate (On Purpose)
The live SOSDirect adapter isn't wired yet. And the code throws if you try to run it in real mode.
That was a deliberate design call. The dry-run path runs mock data. The real path refuses to fake an answer until there's a proper authenticated adapter with actual SOSDirect integration.
It would've been easy to let it silently return "no results" from the proxy source and dress it up as availability. I didn't want that. my business partner is a lawyer relaying this to clients — a false negative from a proxy source dressed as an official determination is worse than no answer at all.
Why This Matters to Me
When you chain scraped sources and proxy APIs for a high-stakes decision, you don't get to call the output "verified."
"No match" ≠ available. These are signals, not legal clearance.
The real job wasn't the Playwright scraping or the normalization math — it was the output contract. Being honest about what the chain can and can't prove is the entire point. A confident wrong answer is the worst thing you can ship into a legal workflow.
P.S. If you're building anything that touches government data for a professional — name a signal a signal. Let the authoritative source stay authoritative, even if that means gating behind a paywall and a "not yet" in the code.