# Business Finder (@leumas/business-finder)

Leumas Prospecting: find local businesses with weak/missing web presence (OpenStreetMap discovery + website analysis + deep-scan), score the Imperium opportunity (esp. a site shipping no schema.org...


Leumas Prospecting: find local businesses with weak/missing web presence (OpenStreetMap discovery +
website analysis + deep-scan), score the **Imperium opportunity** (esp. a site shipping no schema.org
JSON-LD), and draft each lead a starter Imperium site. Migrated from the legacy
`leumas-business-site-finder` tool; the node-safe SDK powers the adapter + router, the React app
powers the Studio/desktop/Web dashboard.

## How agents call it

| Surface | How |
|---|---|
| CLI (headless, no server/auth) | `node ops/tools/run-tool/run-tool.mjs business-finder <op> '<jsonArgs>'` |
| MCP client / chatbot / Leviathan | adapter `business-finder` → tool/functioncall (`discover`, `analyzeSite`, `scoreLead`, `deepScan`, `assembleImperiumConfig`) |
| Automation rule / grid cell | adapter ops as cells + action `business-finder.draft-imperium-site` (lead → drafted config) |
| HTTP | `/api/business-finder/*` (JSON, `requireAuth`) |
| Studio | **Prospecting** domain (`/admin/prospecting`) |
| Code | `runSearchPipeline`, `scoreLead`, `assembleImperiumConfig` … from `@leumas/business-finder/sdk` |

## Run it from the CLI

```sh
node ops/tools/run-tool/run-tool.mjs business-finder discover '{"query":"plumber","city":"Chicago","state":"IL","maxResults":8}'
```

`discover` hits OpenStreetMap live — set `OSM_CONTACT_EMAIL` for the polite Nominatim email. Ops:
`discover` (area → ranked leads, best = weakest site), `analyzeSite` (`{url}` → status/score +
missing-schema signal), `scoreLead` (`{lead}` → Imperium-opportunity score), `deepScan` (`{url|lead}`
→ services/USPs/credentials for a starter brief), `assembleImperiumConfig` (`{lead,mode}` →
`{mode,data,seo}` draft). See `--describe business-finder`.

## /api/business-finder routes

Stateful orchestration + persistence over the SDK. Runs persist in the dynamic `bf_runs` collection,
per-user settings in `bf_settings`; everything is owner-scoped (`doc.owner === req.user.id || admin`).

- `POST /search` — discover + analyze + score, persist a run. `GET /runs`, `GET /runs/:id`.
- `POST /analyze/run/:id` — re-analyze every lead in a run.
- `GET /leads/:id`, `PATCH /leads/:id` (manual review), `POST /leads/:id/deep-scan`.
- `POST /leads/:id/draft-site` — **draft (never publish)** a starter Imperium config and store it on
  the lead. `GET`/`PATCH /settings`. `POST /export/:runId` (`csv`|`json`). `GET /health`.

## draft-imperium-site (the flagship flow)

`POST /leads/:id/draft-site` (and the identical shared action `business-finder.draft-imperium-site`,
so it's a chainable grid cell) composes existing adapters through the registry — **no cross-pack
static import**:

- `landing-copy` → AI hero / benefits / CTA copy blocks.
- `seo` → `jsonLd` (schema.org **Organization**) — the structured data the current site lacked.
- `assembleImperiumConfig` (pure) stitches them into `{ id, appName, mode, data, seo, source }`.

Publishing stays a manual step in the **Hosting** domain; this only drafts. Each adapter call is
try/caught, so a missing LLM degrades to a deterministic draft rather than failing.

## Studio Prospecting domain

`/admin/prospecting` → `ProspectingSurface` mounts `@leumas/business-finder/app` (nav id
`prospecting`, icon [target], surface `ProspectingSurface`). Same React app runs on the leumas-os desktop
and the Web library.

## DRY reuse (do NOT duplicate)

- **Discovery** = OpenStreetMap (Nominatim + Overpass) in the SDK provider registry — no paid places
  API, no key. `mock` provider for offline demos.
- **Scraping/SSRF** = the shared `_shared/net.js` guard (same one `scraping` + `url-screenshot` use);
  URL-fetching ops refuse private/localhost. `analyzeSite`/`deepScan` fetch with cheerio, not a
  browser.
- **AI copy** = the `landing-copy` adapter; **structured data** = the `seo` adapter — both via the
  injected registry, never a raw provider fetch.
- **Persistence** = the dynamic CRUD connector (`api.db`), not bespoke storage.
- **Render/publish** = `@leumas/ui` DynamicPage + `@leumas/hosting` — the finder only produces the
  config.

## Gating

Adapter ops are **free/ungated** (registry-projected). The router is `requireAuth` on everything;
PassNode `feature:business-finder.run` meters the network/LLM lanes (search / analyze / deep-scan /
draft-site) — a no-op until a `passnode_rules` doc for that resource id exists. `run-tool` bypasses
auth (no HTTP layer). No membership tier is required.


---
Source: shared/services/knowledge/build-knowledge/business-finder.md
Canonical: https://docs.leumas.tech/p/how-to/business-finder
