From a77d99b0f88fceed88f81310cd06941c8abe42b3 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 11 Sep 2026 19:34:58 +0000 Subject: [PATCH] Fix v3 KYC lookup with validated registry cell reads --- typescript/business-lookup/README.md | 73 +++++-------- typescript/business-lookup/index.ts | 151 ++++++++++++++++----------- 2 files changed, 115 insertions(+), 109 deletions(-) diff --git a/typescript/business-lookup/README.md b/typescript/business-lookup/README.md index 08c01a77..781e0cd3 100644 --- a/typescript/business-lookup/README.md +++ b/typescript/business-lookup/README.md @@ -1,62 +1,41 @@ -# Stagehand + Browserbase: Business Lookup with Agent +# Stagehand + Browserbase: Business Lookup -## AT A GLANCE +Look up a business in San Francisco's Registered Business Lookup and read its published registration details using Stagehand V3 browser locators. -- Goal: Automate business registry searches using an autonomous AI agent with computer-use capabilities. -- Uses Stagehand Agent in CUA mode to navigate complex UI elements, apply filters, and extract structured business data. -- Demonstrates extraction with Zod schema validation for consistent data retrieval. -- Docs → https://docs.stagehand.dev/basics/agent +## How it works -## GLOSSARY +1. Open the registry in a Browserbase session. +2. Find the business in the DBA Name filter, select its checkbox, and apply the filter. +3. Wait for the filtered row and require exactly one matching location. +4. Read the labeled grid cells and require the requested DBA name, a business account number, and a location ID. +5. Scroll the registry's virtualized columns and read the dates, neighborhood, license description, and self-reported NAICS code directly from their cells. +6. Print the record as JSON and close the session. -- agent: create an autonomous AI agent that can execute complex multi-step tasks - Docs → https://docs.stagehand.dev/basics/agent#what-is-agent -- extract: extract structured data from web pages using natural language instructions - Docs → https://docs.stagehand.dev/basics/extract +Filtering and record retrieval use the registry's DOM controls and labeled grid cells. The template makes no model calls; `BROWSERBASE_API_KEY` is the only credential required. -## QUICKSTART +## Running the template -1. npm install -2. cp .env.example .env -3. Add required API keys/IDs to .env -4. npm start +Use a TypeScript environment with Stagehand V3 (`@browserbasehq/stagehand@3.6.0`), `dotenv`, and a TypeScript runner such as `tsx`. Provide `BROWSERBASE_API_KEY` through your environment or secret manager, then run `index.ts` with your TypeScript runner. -## EXPECTED OUTPUT +Set `businessName` near the top of `index.ts` to the business's DBA name. The default is `Jalebi Street`. -- Initializes Stagehand session with Browserbase -- Displays live session link for monitoring -- Navigates to SF Business Registry search page -- Agent searches for business using DBA Name filter -- Agent completes search and opens business details -- Extracts structured business information (DBA Name, Account Number, NAICS Code, etc.) -- Outputs extracted data as JSON -- Closes session cleanly +In Playground, choose **Search business registries for KYC** and run the template. Playground supplies the session and Browserbase credentials. -## COMMON PITFALLS +## Expected output -- Dependency install errors: ensure npm install completed -- Missing credentials: verify .env contains BROWSERBASE_API_KEY and GOOGLE_API_KEY -- Google API access: ensure you have access to Google's gemini-2.5-computer-use-preview-10-2025 model -- Agent failures: check that the business name exists in the registry and that maxSteps is sufficient for complex searches -- Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in +The JSON record includes the DBA and ownership names, business account number, location ID, street address, business start/end dates, neighborhood, license code description, and self-reported NAICS code. -## USE CASES +Blank registry cells are returned as `null`. For example, a business can have a license description but no self-reported NAICS code. The template reports those fields separately and does not infer an industry code from the license description. -• Business verification: Automate registration status checks, license validation, and compliance verification for multiple businesses. -• Data enrichment: Collect structured business metadata (NAICS codes, addresses, ownership) for research or CRM updates. -• Due diligence: Streamline background checks by autonomously searching and extracting business registration details from public registries. +## Troubleshooting -## NEXT STEPS +- If the business is not available in the DBA filter or the results do not update, the template times out with a description of the missing control or result. +- If several locations share the DBA name, narrow the registry filters before extracting one record. This template deliberately rejects multiple matches. +- If the row does not match the requested DBA name or lacks an account number or location ID, the template reports an error. A missing column also causes an error; only rendered blank cells become `null`. +- Browser and lookup failures include their error message. Check that your Browserbase credentials are valid and the registry is available. -• Parameterize search: Accept business names as command-line arguments or from a CSV file for batch processing. -• Expand extraction: Add support for additional fields like tax status, licenses, or historical registration changes. -• Multi-registry support: Extend agent to search across multiple city or state business registries with routing logic. +## Resources -## HELPFUL RESOURCES - -📚 Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction -🎮 Browserbase: https://www.browserbase.com -💡 Try it out: https://www.browserbase.com/playground -🔧 Templates: https://www.browserbase.com/templates -📧 Need help? support@browserbase.com -💬 Discord: http://stagehand.dev/discord +- [Stagehand V3 documentation](https://docs.stagehand.dev/v3/first-steps/introduction) +- [San Francisco Registered Business Lookup](https://data.sf.gov/stories/s/Registered-Business-Lookup/k6sk-2y6w/) +- [Browserbase Playground](https://www.browserbase.com/playground) diff --git a/typescript/business-lookup/index.ts b/typescript/business-lookup/index.ts index 89d25602..e2564cf3 100644 --- a/typescript/business-lookup/index.ts +++ b/typescript/business-lookup/index.ts @@ -1,90 +1,117 @@ -// Business Lookup with Agent - See README.md for full documentation +// Business Lookup with Stagehand - See README.md for full documentation import "dotenv/config"; import { Stagehand } from "@browserbasehq/stagehand"; -import { z } from "zod"; -// Business search variables const businessName = "Jalebi Street"; async function main() { - // Initialize Stagehand with Browserbase for cloud-based browser automation. const stagehand = new Stagehand({ env: "BROWSERBASE", verbose: 1, - model: "openai/gpt-4.1", }); try { - // Initialize browser session to start automation. await stagehand.init(); - console.log("Stagehand initialized successfully!"); - console.log( - `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, - ); - const page = stagehand.context.pages()[0]; + await page.goto("https://data.sf.gov/stories/s/Registered-Business-Lookup/k6sk-2y6w/"); - // Navigate to SF Business Registry search page. - console.log(`Navigating to SF Business Registry...`); - await page.goto("https://data.sfgov.org/stories/s/Registered-Business-Lookup/k6sk-2y6w/"); - - // Create agent with computer use capabilities for autonomous business search. - const agent = stagehand.agent({ - cua: true, // Enable Computer Use Agent mode - model: { - modelName: "google/gemini-2.5-computer-use-preview-10-2025", - apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, - }, - systemPrompt: - "You are a helpful assistant that can use a web browser to search for business information.", - }); + async function waitFor( + check: () => Promise, + description: string, + ): Promise { + const deadline = Date.now() + 20000; + while (Date.now() < deadline) { + if (await check()) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${description}`); + } - console.log(`Searching for business: ${businessName}`); - const result = await agent.execute({ - instruction: `Find and look up the business "${businessName}" in the SF Business Registry. Use the DBA Name filter to search for "${businessName}", apply the filter, and click on the business row to view detailed information. Scroll towards the right to see the NAICS code.`, - maxSteps: 30, - }); + // Click the checkbox itself: clicking its adjacent text does not select it. + const filter = '[role="button"][aria-label="Filter: DBA Name - Select..."]'; + await waitFor(() => page.locator(filter).count(), "the DBA Name filter"); + await page.locator(filter).click(); + await waitFor( + () => page.locator("#search-text-filter-input").count(), + "the business-name search input", + ); + await page.locator("#search-text-filter-input").fill(businessName); + const checkbox = `forge-checkbox[aria-label=${JSON.stringify(businessName)}]`; + await waitFor(() => page.locator(checkbox).count(), `the ${businessName} option`); + await page.locator(checkbox).click(); + await waitFor(() => page.locator(checkbox).isChecked(), "the selected business checkbox"); + await page.locator('[data-testid="apply-filter-button"]').click(); - if (!result.success) { - throw new Error("Agent failed to complete the search"); + // Wait for the results grid to replace the unfiltered first page. + await waitFor( + () => + page.evaluate((name: string) => { + const cells = [...document.querySelectorAll('[role="gridcell"][col-id="dba_name"]')]; + return cells.length > 0 && cells.every((cell) => cell.textContent!.trim() === name); + }, businessName), + `results matching ${businessName}`, + ); + if ((await page.locator('[role="gridcell"][col-id="dba_name"]').count()) !== 1) { + throw new Error( + `Multiple locations found for ${businessName}; narrow the registry filters before extracting one record.`, + ); + } + // Read the labeled grid cells directly; missing cells must fail rather + // than being confused with an empty value published by the registry. + async function readCell(column: string): Promise { + const text = await page.locator(`[role="gridcell"][col-id="${column}"]`).innerText(); + return text.trim() || null; + } + const businessInfo = { + dbaName: await readCell("dba_name"), + ownershipName: await readCell("ownership_name"), + businessAccountNumber: await readCell("certificate_number"), + locationId: await readCell("ttxid"), + streetAddress: await readCell("full_business_address"), + }; + if ( + businessInfo.dbaName !== businessName || + !businessInfo.businessAccountNumber || + !businessInfo.locationId + ) { + throw new Error("The filtered registry row is missing the requested business identity."); } - // Extract comprehensive business information after agent completes the search. - console.log("Extracting business information..."); - const businessInfo = await stagehand.extract( - "Extract all visible business information including DBA Name, Ownership Name, Business Account Number, Location Id, Street Address, Business Start Date, Business End Date, Neighborhood, NAICS Code, and NAICS Code Description", - z.object({ - dbaName: z.string(), - ownershipName: z.string().optional(), - businessAccountNumber: z.string(), - locationId: z.string().optional(), - streetAddress: z.string().optional(), - businessStartDate: z.string().optional(), - businessEndDate: z.string().optional(), - neighborhood: z.string().optional(), - naicsCode: z.string(), - naicsCodeDescription: z.string().optional(), - }), - { page }, + // The registry virtualizes its columns; scroll to render the remaining fields. + await page.evaluate(() => { + const scrollbar = document.querySelector(".ag-body-horizontal-scroll-viewport")!; + scrollbar.scrollLeft = scrollbar.scrollWidth; + }); + await waitFor( + () => page.locator('[role="gridcell"][col-id="self_reported_naics_code"]').count(), + "the Self-Reported NAICS Code column", ); - + // Preserve blank published values instead of inferring from nearby cells. + const details = { + businessStartDate: await readCell("dba_start_date"), + businessEndDate: await readCell("dba_end_date"), + neighborhood: await readCell("neighborhoods_analysis_boundaries"), + licenseCodeDescription: await readCell("lic_code_description"), + naicsCode: await readCell("self_reported_naics_code"), + }; console.log("Business Information:"); - console.log(JSON.stringify(businessInfo, null, 2)); + console.log(JSON.stringify({ ...businessInfo, ...details }, null, 2)); + if (!details.naicsCode) + console.log("The registry does not list a Self-Reported NAICS Code for this business."); } catch (error) { - console.error("Error during business lookup:", error); + const errorMessage = + error !== null && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ? error.message + : String(error); + console.error("Error during business lookup:", errorMessage); + throw error; } finally { - // Always close session to release resources and clean up. await stagehand.close(); - console.log("Session closed successfully"); } } -main().catch((err) => { - console.error("Error in business lookup:", err); - console.error("Common issues:"); - console.error(" - Check .env file has BROWSERBASE_API_KEY"); - console.error(" - Verify GOOGLE_API_KEY is set for the agent"); - console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); - process.exit(1); -}); +await main();