Enrichment and grounding¶
Enrichment turns a candidate into a listing. The central rule: a claim survives only if it is quoted verbatim from a page we downloaded.
1. Fetch¶
Pages are fetched through AAA's SSRF-hardened SafeFetcher (public hosts only, https preferred, size caps, redirect re-checks) and stored in
SQLite with a content hash: homepage, README and repo metadata, and likely /pricing, /features, /integrations, docs pages discovered from homepage links.
2. Extract¶
GenerateTestRepair asks the model for JSON in which every claim has source_url and quote. Page text is wrapped as untrusted data,
and lines matching injection patterns are stripped before the model sees them.
{ "features": [ { "name": "Draft replies", "description": "Draft replies for human approval",
"source_url": "https://zorbo.ai/", "quote": "Draft replies for human approval" } ] }
3. Verify (the anti-invention core)¶
enrich/grounding.py checks each claim:
| Check | Rejects |
|---|---|
source_url is a page we fetched |
citing pages we never saw |
quote appears in that page (case/whitespace/markdown-normalised, ≥12 chars) |
fabricated or stitched quotes |
| value is supported by its quote (most words, all numbers) | a real quote with an embellished claim |
| integrations and prices appear exactly in the quote | invented partners or prices |
| plan name precedes its price within 400 chars on the page | wrong plan/price pairing |
Rejected items produce precise feedback, the model retries (up to 3 times), and anything still unsupported is dropped. Salvage parses leniently, field by field, so one malformed item never discards the rest.
4. Fill gaps¶
If pricing is missing or there are fewer than three features, a bounded ToolLoop (6 turns) may site_search and read_page, but only on the
product's own website or GitHub repo. The allowed host comes from our database via trusted run metadata, never from the model. It only stores pages;
facts still pass step 3.
5. Confirm prices¶
For every extracted tier, a three-sample SelfConsistency vote asks the plan's price. A tier is kept only with ≥ 2/3 agreement.
Unconfirmed prices are dropped and noted in publish_log.
6. Overview¶
The only generated prose. A generator sees only the numbered, validated facts and must end every sentence with citations like [f3].
A second model (GeneratorCritic) fact-checks it, then plain code verifies that each sentence cites existing facts and that every number
appears in the cited facts. If anything fails, the fallback is the verbatim description fact. Nothing is ever shown that was not checked.
7. Media: videos, screenshots, images¶
Collected by code, never by the model, in this order of preference:
- Videos. YouTube/Vimeo/Loom embeds and links found on the product's own pages and README, plus
.mp4/.webmfiles. If fewer than two are found, a SearXNG video search proposes more, and each result is proved relevant through YouTube's oEmbed endpoint: the product name must appear in the video title or channel name (names made only of generic words such as "AI Agent" never match). - Screenshots we capture ourselves. A headless browser (Playwright; bundled Chromium, falling back to system Chrome) renders the product's homepage, pricing and features pages at 1440×900 and saves JPEGs. Cookie and chat overlays are hidden by CSS, and blank captures (< 20 KB) are discarded. For a repo-only candidate the GitHub page is captured. Every request the browser makes (page, redirects, scripts, images) is vetted with the same SSRF rules as the fetcher, so a hostile page cannot use the browser to reach private addresses.
- Page images.
<img>tags and README images, with logos, icons, badges, sponsors, testimonials, star-history charts, QR codes and comparison/social cards filtered out. Next.js image-proxy variants collapse to one asset, and each remaining image is checked with aHEADrequest (real image, minimum size). Theog:imagesocial card is the last resort.
Screenshots are written to data/media/<candidate>/ and copied next to the exported JSON as out/media/<slug>/…; videos and images are stored as references
with their source page. Backfill existing listings, with no LLM calls, using botshub media.
8. Publish gate¶
Deterministic. A listing publishes only with a name, a website or repo, an evidence-backed overview, at least three evidenced features, a category and quality ≥ 0.6. Otherwise it is held with reasons. A held or failed re-run never replaces a published listing.
There is no human in the loop
Auto-publish is a design choice, and quote verification makes invented facts very unlikely, but it cannot catch a page that is itself wrong,
or a true-but-misleading quote. Audit a random sample and keep botshub unpublish <slug> handy.