{
  "schema": "leumas.docs.page/1",
  "id": "how-to:business-finder",
  "slug": "how-to/business-finder",
  "kind": "pages",
  "bucket": "how-to",
  "title": "Business Finder (@leumas/business-finder)",
  "name": "Business Finder",
  "eyebrow": null,
  "chip": "@leumas/business-finder",
  "summary": "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...",
  "keywords": [
    "business-finder",
    "leumas-business-site-finder",
    "deep-scan",
    "node-safe",
    "draft-imperium-site flagship flow",
    "leumas business finder",
    "how to business finder",
    "openstreetmap"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# Business Finder (@leumas/business-finder)\n\nLeumas Prospecting: find local businesses with weak/missing web presence (OpenStreetMap discovery +\nwebsite analysis + deep-scan), score the **Imperium opportunity** (esp. a site shipping no schema.org\nJSON-LD), and draft each lead a starter Imperium site. Migrated from the legacy\n`leumas-business-site-finder` tool; the node-safe SDK powers the adapter + router, the React app\npowers the Studio/desktop/Web dashboard.\n\n## How agents call it\n\n| Surface | How |\n|---|---|\n| CLI (headless, no server/auth) | `node ops/tools/run-tool/run-tool.mjs business-finder <op> '<jsonArgs>'` |\n| MCP client / chatbot / Leviathan | adapter `business-finder` → tool/functioncall (`discover`, `analyzeSite`, `scoreLead`, `deepScan`, `assembleImperiumConfig`) |\n| Automation rule / grid cell | adapter ops as cells + action `business-finder.draft-imperium-site` (lead → drafted config) |\n| HTTP | `/api/business-finder/*` (JSON, `requireAuth`) |\n| Studio | **Prospecting** domain (`/admin/prospecting`) |\n| Code | `runSearchPipeline`, `scoreLead`, `assembleImperiumConfig` … from `@leumas/business-finder/sdk` |\n\n## Run it from the CLI\n\n```sh\nnode ops/tools/run-tool/run-tool.mjs business-finder discover '{\"query\":\"plumber\",\"city\":\"Chicago\",\"state\":\"IL\",\"maxResults\":8}'\n```\n\n`discover` hits OpenStreetMap live — set `OSM_CONTACT_EMAIL` for the polite Nominatim email. Ops:\n`discover` (area → ranked leads, best = weakest site), `analyzeSite` (`{url}` → status/score +\nmissing-schema signal), `scoreLead` (`{lead}` → Imperium-opportunity score), `deepScan` (`{url|lead}`\n→ services/USPs/credentials for a starter brief), `assembleImperiumConfig` (`{lead,mode}` →\n`{mode,data,seo}` draft). See `--describe business-finder`.\n\n## /api/business-finder routes\n\nStateful orchestration + persistence over the SDK. Runs persist in the dynamic `bf_runs` collection,\nper-user settings in `bf_settings`; everything is owner-scoped (`doc.owner === req.user.id || admin`).\n\n- `POST /search` — discover + analyze + score, persist a run. `GET /runs`, `GET /runs/:id`.\n- `POST /analyze/run/:id` — re-analyze every lead in a run.\n- `GET /leads/:id`, `PATCH /leads/:id` (manual review), `POST /leads/:id/deep-scan`.\n- `POST /leads/:id/draft-site` — **draft (never publish)** a starter Imperium config and store it on\n  the lead. `GET`/`PATCH /settings`. `POST /export/:runId` (`csv`|`json`). `GET /health`.\n\n## draft-imperium-site (the flagship flow)\n\n`POST /leads/:id/draft-site` (and the identical shared action `business-finder.draft-imperium-site`,\nso it's a chainable grid cell) composes existing adapters through the registry — **no cross-pack\nstatic import**:\n\n- `landing-copy` → AI hero / benefits / CTA copy blocks.\n- `seo` → `jsonLd` (schema.org **Organization**) — the structured data the current site lacked.\n- `assembleImperiumConfig` (pure) stitches them into `{ id, appName, mode, data, seo, source }`.\n\nPublishing stays a manual step in the **Hosting** domain; this only drafts. Each adapter call is\ntry/caught, so a missing LLM degrades to a deterministic draft rather than failing.\n\n## Studio Prospecting domain\n\n`/admin/prospecting` → `ProspectingSurface` mounts `@leumas/business-finder/app` (nav id\n`prospecting`, icon [target], surface `ProspectingSurface`). Same React app runs on the leumas-os desktop\nand the Web library.\n\n## DRY reuse (do NOT duplicate)\n\n- **Discovery** = OpenStreetMap (Nominatim + Overpass) in the SDK provider registry — no paid places\n  API, no key. `mock` provider for offline demos.\n- **Scraping/SSRF** = the shared `_shared/net.js` guard (same one `scraping` + `url-screenshot` use);\n  URL-fetching ops refuse private/localhost. `analyzeSite`/`deepScan` fetch with cheerio, not a\n  browser.\n- **AI copy** = the `landing-copy` adapter; **structured data** = the `seo` adapter — both via the\n  injected registry, never a raw provider fetch.\n- **Persistence** = the dynamic CRUD connector (`api.db`), not bespoke storage.\n- **Render/publish** = `@leumas/ui` DynamicPage + `@leumas/hosting` — the finder only produces the\n  config.\n\n## Gating\n\nAdapter ops are **free/ungated** (registry-projected). The router is `requireAuth` on everything;\nPassNode `feature:business-finder.run` meters the network/LLM lanes (search / analyze / deep-scan /\ndraft-site) — a no-op until a `passnode_rules` doc for that resource id exists. `run-tool` bypasses\nauth (no HTTP layer). No membership tier is required.\n",
  "source": {
    "path": "shared/services/knowledge/build-knowledge/business-finder.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 4718,
    "hash": "8c53ea424092150d54dbbc8b9b9ae5854ad6ff17"
  },
  "urls": {
    "html": "/p/how-to/business-finder",
    "json": "/docs/how-to/business-finder.json",
    "md": "/docs/how-to/business-finder.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
