Overview
The Shopping Agent is a real-time menu intelligence system that combines web scraping, database matching, and AI personalization into a single coherent pipeline. When a user taps Shop Now from a dispensary page, the system extracts the dispensary’s live menu, matches every product against the 16,000+ strain database, scores results against the user’s personal preference profile, and delivers ranked recommendations — all within 15–45 seconds. This page documents the complete technical architecture for developers working on the Shopping Agent pipeline.System Architecture
Stage 1: Cache Check
Before any scraping begins, the task queries themenu_scans Supabase table for an unexpired entry matching the dispensary domain.
UNIQUE (website_domain, categories_hash). If the menu composition has changed (new categories), a fresh scan overwrites the old entry.
Stage 2: Menu Extraction (Firecrawl Tool-Based Architecture)
Menu extraction uses a tool-based architecture built on Firecrawl. Instead of a single extraction method, the system provides five independent tools that the pipeline orchestrator chains with cascading fallback. Each tool is independently callable and testable.The Five Tools
All tools are exported from
@tiwih/trigger and live in packages/trigger/src/lib/firecrawl-agent.ts.
Tool 1: Extract (Primary — Multi-Page)
The Extract tool uses Firecrawl’sextract() API with wildcard URL patterns to crawl an entire menu section and extract structured product data across multiple pages. This is the primary extraction method because most dispensary menus span multiple URLs (e.g., /shop/flower, /shop/edibles, /shop/vapes).
Tool 2: Scrape+JSON (Single-Page)
For menus on a single URL, the Scrape+JSON tool combines scraping and extraction in one call — Firecrawl renders the page, then uses its built-in LLM to fill the schema.Tool 3: Scrape+AI (Fallback)
When structured extraction returns 0 products (e.g., heavily JavaScript-rendered SPAs, age-gated sites), the pipeline falls back to scraping raw markdown and having Claude extract products from the text.Tool 4: Sitemap Discovery
The Sitemap tool uses Firecrawl’smap() to discover all URLs on a domain, then filters for menu-related paths. This is used as a fallback when the initial scan returns 0 products — it discovers additional menu URLs to retry extraction.
Orchestrator: Cascading Fallback
ThescanDispensaryMenu() orchestrator chains these tools with automatic fallback:
method field ("extract", "scrape-json", or "scrape-ai") indicating which tool succeeded.
Extraction Performance (Real Tests)
Why Plain JSON Schema, Not Zod
The Firecrawl JS SDK detects Zod schemas viaisZodSchema() and attempts conversion through tryZodV4Conversion. With Zod v4 (used in this project), this conversion silently fails, causing Extract to return 0 products. Using a plain JSON schema object (DISPENSARY_EXTRACT_SCHEMA) bypasses the SDK’s Zod detection entirely.
Progress Reporting During Extraction
The extraction stage emits progress metadata so the mobile UI can show a live status like “Scanning menu… found 47 products so far.”Testing the Tools
Two test scripts are available inpackages/trigger/:
Stage 3: Strain Matching
Every extracted product name is run through a three-tier matching algorithm against thestrains_v2 Supabase table. Matches are attempted in order; the first successful match wins.
Tier 1: Exact Match
name_canonical is a pre-computed lowercase, stripped version of the strain name stored at ingestion time. This handles the most common case: the dispensary uses the standard strain name.
Confidence: high
Tier 2: Slug Match
slugify() converts a string to URL-safe format (lowercase, hyphens, no special characters). Catches cases like “OG Kush” → og-kush matching a database entry with slug og-kush.
Confidence: high
Tier 3: Trigram Similarity (pg_trgm)
medium if similarity > 0.6, low if 0.4–0.6.
Unmatched Products
Products that pass through all three tiers without a match are added to thediscoveries array. These represent real strains available locally that are not yet in the High IQ database.
Parallelism
Matching runs concurrently for all extracted products usingPromise.all() batched in groups of 20 to avoid overwhelming the Supabase connection pool.
Stage 4: AI Personalization
Once all products are matched, Claude Sonnet generates personalized recommendations. The personalization step has two parts: deterministic tag assignment and AI recommendation generation.Deterministic Tag Assignment
Tags are assigned by comparing the matched strain IDs against the user’s profile data passed in the request. This is pure logic — no AI involved.isSimilarToFavorite() uses the High IQ strain similarity scores (pre-computed and stored in Supabase) to find products with similar terpene and effect profiles to the user’s favorites.
AI Recommendation Generation
After tags are assigned, the full product list (with tags and strain data) is passed to Claude Sonnet. The AI selects the top 3–5 picks and writes a plain-English reason for each.generateObject() call uses AI SDK 7 with schema for structured output, ensuring the recommendations are always valid JSON.
Stage 5: Cache Save
Results are upserted into themenu_scans table with a 4-hour expiry.
categories_hash is an MD5 of the sorted category list. If the dispensary adds a new product category (e.g., starts selling topicals), the hash changes, triggering a fresh scan on the next request.
Stage 6: Complete
The task returns the full output payload, which Trigger.dev delivers to the mobile app via WebSocket. TheuseRealtimeTaskTrigger hook in the app receives the completed run and triggers a state update to show the results screen.
De-duplication
The Hono API endpoint checks for active Trigger.dev runs before triggering a new one.Mobile App Integration
Screens
Real-Time Hook
The mobile app usesuseRealtimeTaskTrigger from @trigger.dev/react-hooks to subscribe to run progress and output without polling.
Discovery Queue: Queueing Unmatched Strains for Research
When a user taps “Add to Research Queue” in theDiscoveryScreen, the app submits the unmatched strain names directly to the Hono API — no Convex middleman involved.
Why Direct API, Not Convex
Convex is the source of truth for user-owned data: orders, stash, favorites, and dispensaries. Strain research is a platform-level concern — the data ends up in Supabase (strains_v2) and benefits all users, not just the submitter. Routing it through Convex would violate the data layer boundary and add unnecessary latency.
Data Flow
Source Type
The/queue-batch endpoint accepts a source field that identifies how the strain was discovered. The shopping_discovery value was added alongside the existing order_upload and manual values specifically for this flow.
Implementation Details
useRef guard prevents the user from double-submitting if they tap the button quickly while the request is in flight. Unlike useState, a ref update does not trigger a re-render, so the button can remain visually enabled for the next valid submission without a flash of disabled state.
The Trigger.dev strain research pipeline processes queued strains asynchronously. Full strain profiles (genetics, terpenes, effects, images) are typically available within a few hours of submission.
Configuration
Environment Variables
The following environment variables are required inapps/api/.env and must be set in the Vercel project settings for production.
Trigger.dev Package Setup
Theshopping-menu-scan task lives in the @tiwih/trigger package at packages/trigger/src/tasks/shopping-menu-scan.ts. It is deployed to the Trigger.dev cloud alongside the other pipeline tasks.
Adjustable Limits
Performance Characteristics
The Supabase
strains_v2 table has a GIN index on name_canonical for trigram searches: CREATE INDEX idx_strains_name_trgm ON strains_v2 USING GIN (name_canonical gin_trgm_ops). Without this index, trigram matching on 16,000 rows would be too slow for the pipeline.Observability
All pipeline stages emit structured logs via@tiwih/logger with the shopping category. Trigger.dev’s dashboard shows per-run stage durations, metadata snapshots, and task output — making it straightforward to identify where time is spent on any given scan.
- Trigger.dev Dashboard: cloud.trigger.dev/projects/v3/proj_kmfmmftspsichqetwiom
- Task: Filter by
shopping-menu-scanin the Runs tab - Logs: Each stage logs product counts, match rates, and timing at INFO level
Related Architecture Docs
- AI Features Guide — Overview of all AI systems in High IQ
- Streaming (SSE) — Report streaming architecture for comparison
- Data Sources — How strain data is sourced and maintained
- Strain Scoring — How strains are ranked and scored in the database
