Business Entity & UCC Lien Search — KYB Company Lookup

Design notes for an Actor that resolves a US company from official state business registries and joins each entity to the UCC lien filings against it — entity search and lien search, connected, in one call.

Design notes for business-entity-lien-search, an Apify Actor that verifies a US company against official state business registries and, in the same call, joins each resolved entity to the UCC lien filings recorded against it. It’s built for the AI agents doing know-your-business (KYB), lending, and diligence work.

What this is

Give it a company name (optionally scoped to a list of states) and it returns one normalized record per matched entity: legal name, state registration ID, entity type, registration status (Good Standing, Active, Forfeited, Dissolved, and the rest, verbatim from the state), formation date, jurisdiction of formation, registered agent, principal/mailing address, and officers where the state publishes them. Then — the part nobody else does — each entity is joined to its Uniform Commercial Code filings as debtor: filing number, type, status, filed and lapse dates, the secured party (the lender), and the collateral description.

So one call answers the whole diligence question, not half of it: is this company real, in good standing, and does it already have liens against its assets?

Why I built it this way

The join is the product

The market already has business-entity search. What it doesn’t have is entity search connected to liens. Registries and UCC/secured-transaction filings live in different systems; analysts bridge them by hand — find the entity, switch to the UCC portal, search by name, eyeball whether it’s the same legal entity. That manual bridge is exactly the expensive, error-prone step, and it’s the one an agent most wants automated.

So the Actor resolves the entity from the state business register, then searches that state’s UCC index by the entity’s legal name and attaches the matching filings to the entity record. Entity search Actors stop at the first question. Standalone UCC scrapers answer the second but never link it to a verified entity. This does both and connects them.

Matching debtor to entity has to be precise

A loose name match would be worse than nothing — attach “Tesla Imaging LLC”’s lien to “Tesla Electric Company LLC” and you’ve manufactured a false risk signal. So the join compares the core of the legal name: uppercase, strip punctuation, and remove only unambiguous legal-form suffixes (LLC, Inc., Corp., LP, Ltd.). Descriptive words that are genuinely part of a name — Company, Co, Trust, Foundation — are kept, because stripping them would collapse distinct entities together (“Ford Motor Company” is not “Ford Motor”). The server-side search casts a slightly wider net with a contains query; the exact-core comparison then admits only the filings whose debtor really is this entity. A lien shows up on a record only when it belongs there.

One schema across states is the moat

A scraper for a single state’s registry is a commodity that breaks when that state redesigns its portal. The defensible thing is the unification: many states, identical output, one call. The architecture is a registry of per-state adapters behind a single search(name) -> [EntityRecord] contract, and it’s config-driven wherever the source allows. Most covered states publish their business register as clean open data (Socrata) — so a state is a single config row: a dataset id plus a column map onto the unified record. Two states expose a clean public JSON search API instead, and get a small bespoke adapter behind the same contract. Adding a state never changes the call you make or the shape you parse.

The UCC side is config-driven too, in two dialects. Some states publish a single denormalized lien dataset where each row already carries debtor, secured party, status, and dates — one query, one config row. Others publish a relational UCC: a debtor table, a filing/status table, a collateral table, and a secured-party table, all joined by a filing id. That’s a second config shape (the four dataset ids plus their keys), and the adapter walks the join. Same contract, richer source.

Honest coverage beats an over-claimed map

I validated every source live before building on it, and the discipline mattered. The clean, reliable set shipped first: entity search for Colorado, Connecticut, North Dakota, New York, Oregon, Pennsylvania, and Texas, with the entity→lien join live for Colorado and Connecticut — the two states that publish both a business register and a debtor-searchable UCC index as clean open data. Every run also writes a COVERAGE record so a caller can read the live coverage rather than trust a stale doc.

Just as important is what I didn’t ship. Delaware — the country’s top incorporation state — has a free name search, but it returns only the name and file number (status, type, and agent sit behind a paid detail lookup), it’s an inconsistent handshake, and the page explicitly prohibits data mining. So Delaware is a documented miss, not a target: I don’t cross paywalls or fight a source that tells me not to scrape it. Florida’s live search is bot-walled, but the state offers a free public bulk-data download — that’s a genuine future addition (an index build), not a live query. Several large states (Ohio, Washington, Nevada, Wyoming, Georgia) sit behind Cloudflare Turnstile, Incapsula, or F5 anti-bot walls; they’re mapped as a later gateway-tier tier, not pretended-covered now. A smaller set that actually works beats a bigger one that 500s on a real input.

Not all absences are the same

When a field comes back null, the record says why. Texas’s Comptroller search returns the entity name and taxpayer ID but not status or type — those live on a separate detail page — so a Texas record carries a field_notes entry marking those fields not_in_source with the explanation. A state we cover for entities but not yet for liens returns the entity with liens_checked: false and a note saying the lien join isn’t integrated there yet — distinct from a lien-covered state that genuinely found no liens (liens_checked: true, ucc_summary: {total: 0, active: 0}, a confident “clean”). An agent can tell “structurally unavailable” from “we don’t have it yet” from “checked, none found,” and act on each differently.

Billing that can’t surprise you

Charging is per completed entity record, pushed before it’s billed, and never charged on a no_match, not_covered, or failed result. A broad name like “Smith” is bounded two ways: a per-company record cap, and the run’s max-charge ceiling, with every charge clamped to the caller’s remaining budget in code. You can’t get a runaway bill from a common name.

How to use it

Verify a company and pull its liens in Colorado and Connecticut:

curl -X POST "https://api.apify.com/v2/acts/shelvick~business-entity-lien-search/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companyName": "Tesla", "states": ["CO", "CT"], "includeLiens": true}'

From the Apify Python SDK, verifying a batch of counterparties at once:

from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("shelvick/business-entity-lien-search").call(
    run_input={"companyNames": ["Acme Holdings", "Beta Logistics LLC"], "includeLiens": True}
)
for rec in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(rec["legal_name"], rec["state"], rec["entity_status"], rec["ucc_summary"])

Agents can call it as a tool through the Apify MCP server (mcp.apify.com), where the input schema and per-field descriptions are advertised to the model. Set matchMode to exact when you know the legal name, contains (the default) to discover every entity sharing a name, and includeLiens: false for a faster entity-only check.

How it compares to standalone registry tools

This Actor Standalone entity-search tools Single-state UCC scrapers
Entity search by name Multi-state, one schema Usually, coverage varies No
UCC / lien filings Joined to each entity No Yes, but unlinked
One call for entity + liens Yes No No
Machine-readable status + coverage notes Yes Rarely Rarely
Precise debtor→entity name match Yes N/A N/A

The bet is that multi-jurisdiction assembly behind one schema, plus the entity-to-lien join, is worth more to a diligence workflow than any single registry scraped in isolation.

Pricing model

Pay-per-event, billed only on success: one charge per completed entity record, after it’s pushed to the dataset. The charge covers the registry search, normalization, and the lien join where available; misses and failures are free. The current per-record rate and any subscriber discounts are on the Apify Store Pricing tab, which is authoritative.

Open questions / future work

  • More UCC-join states. The join lives where a state publishes both a register and a debtor-searchable UCC index as clean data (Colorado, Connecticut today). Minnesota’s “Business & Lien System” is the obvious next candidate — it bundles both — and North Dakota’s registry API hints at a lien endpoint worth a second look.
  • The formation hubs. Delaware, Wyoming, and Nevada are where a huge share of US entities actually form, and all three are gated (paywall or anti-bot). The compliant paths — licensed bulk data, or a stealth gateway tier for the walled portals — are mapped but deliberately deferred.
  • Florida via bulk index. Florida’s free public data download is the strongest near-term coverage add; it’s an index-build project rather than a live query, following the same pattern I use for other too-big-to-query-live sources.
  • Officers and agents where they live in a second dataset. A few states keep principals in a separate companion table; the same best-effort join pattern that fills other gaps applies, once enough states queue behind it to justify the mechanism.