<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CRJ</title>
    <description>The latest articles on DEV Community by CRJ (@crj_787403881c99662c653e8).</description>
    <link>https://dev.to/crj_787403881c99662c653e8</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4110602%2F1f71a147-d8e0-4a25-b475-df5a3ae8241e.png</url>
      <title>DEV Community: CRJ</title>
      <link>https://dev.to/crj_787403881c99662c653e8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/crj_787403881c99662c653e8"/>
    <language>en</language>
    <item>
      <title>How to migrate an online store's product catalog in minutes (not weeks)</title>
      <dc:creator>CRJ</dc:creator>
      <pubDate>Sat, 05 Sep 2026 22:08:02 +0000</pubDate>
      <link>https://dev.to/crj_787403881c99662c653e8/how-to-migrate-an-online-stores-product-catalog-in-minutes-not-weeks-5bil</link>
      <guid>https://dev.to/crj_787403881c99662c653e8/how-to-migrate-an-online-stores-product-catalog-in-minutes-not-weeks-5bil</guid>
      <description>&lt;p&gt;Migrating a store to a new platform is mostly one boring, expensive problem: &lt;strong&gt;getting the product catalog out of the old site&lt;/strong&gt;. The checkout, the theme, the domain — those are afternoons. The catalog is weeks.&lt;/p&gt;

&lt;p&gt;Here is how to do it in minutes instead, and why the three obvious approaches usually don't get you there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why catalog migration eats weeks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Retyping by hand
&lt;/h3&gt;

&lt;p&gt;The default. Someone opens the old store in one tab and the new admin in another, and copies 600 products across. At a realistic 3-4 minutes per product — name, description, price, SKU, category, download the photo, re-upload it — that's about 35 hours of work. It is also the version that silently loses data: whoever gets tired at product 400 starts skipping the specs table.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The CSV export that "already exists"
&lt;/h3&gt;

&lt;p&gt;Every platform has an export button, so this feels solved. It isn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The export gives you &lt;em&gt;their&lt;/em&gt; column names, not the new platform's.&lt;/li&gt;
&lt;li&gt;Photos come as URLs pointing at the old CDN, which dies with the old plan.&lt;/li&gt;
&lt;li&gt;Variants, bundles and specs get flattened into one unparseable text blob.&lt;/li&gt;
&lt;li&gt;Legacy or custom-built stores (a huge share of small B2B sites) have no export at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You end up writing a transform script anyway — and you can only start once you have someone's admin credentials, which for an agency migrating a client is often the long pole.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A scraper with CSS selectors
&lt;/h3&gt;

&lt;p&gt;The engineer's answer, and it works — for exactly one site. You open DevTools, find that the price lives in &lt;code&gt;.product-info__price &amp;gt; span&lt;/code&gt;, write the selector, and it holds until the store's theme updates. Multiply by every client you migrate and you are maintaining a per-site template library. Selector-based scrapers don't generalize; that's their whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI approach: no selectors at all
&lt;/h2&gt;

&lt;p&gt;Instead of teaching a script &lt;em&gt;where&lt;/em&gt; the price is, you let a model read the page and tell you &lt;em&gt;what's on it&lt;/em&gt;. The crawler is generic — follow links, find category pages, follow pagination, collect candidate product URLs — and the extraction is a prompt.&lt;/p&gt;

&lt;p&gt;The non-obvious part is not extraction. It's &lt;strong&gt;rejection&lt;/strong&gt;. E-commerce pages lie: a category page carries a name, a price and a photo, exactly like a product page. So the single most important instruction in the prompt is that the model must answer &lt;code&gt;null&lt;/code&gt; when the page is a listing, not a product. Teaching the AI to say "this isn't a product" mattered more than teaching it to extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step, with curl
&lt;/h2&gt;

&lt;p&gt;I packaged the engine as an API. Subscribe on RapidAPI (free tier, no card), grab your key, and it's two calls: submit a job, poll it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Submit the store URL&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://ai-product-catalog-extractor.p.rapidapi.com/v1/extract'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-key: YOUR_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-host: ai-product-catalog-extractor.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://the-old-store.com/",
    "limites": { "max_paginas": 200 }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a &lt;code&gt;202&lt;/code&gt; back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f2c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"poll"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/v1/jobs/9f2c..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Poll until it's done&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://ai-product-catalog-extractor.p.rapidapi.com/v1/jobs/9f2c...'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-key: YOUR_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-host: ai-product-catalog-extractor.p.rapidapi.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While it runs you get live progress — &lt;code&gt;fase&lt;/code&gt; (phase), &lt;code&gt;paginas_visitadas&lt;/code&gt;, &lt;code&gt;itens_extraidos&lt;/code&gt; — so you can render a progress bar instead of a spinner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Read the catalog&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"itens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"nome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rebite Repuxo Alumínio 4,8 x 12mm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"marca"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"categoria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rebites"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RA-4812"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"preco"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;42.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"descricao"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rebite de repuxo em alumínio com haste de aço..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"foto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://old-store.com/img/ra-4812.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://old-store.com/produtos/rebite-repuxo-4812"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_candidatos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"truncado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tokens_entrada"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;113559&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"chamadas_ia"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;About those Portuguese field names.&lt;/strong&gt; The engine was born inside a Brazilian ERP, so the default preset ships &lt;code&gt;nome&lt;/code&gt;, &lt;code&gt;preco&lt;/code&gt;, &lt;code&gt;foto&lt;/code&gt;, &lt;code&gt;descricao&lt;/code&gt;. That's cosmetic, not structural — you can send your own schema and get whatever keys your target platform wants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tipo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"texto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tipo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tipo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foto"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same engine, keys ready to &lt;code&gt;POST&lt;/code&gt; straight into Shopify or WooCommerce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real numbers
&lt;/h2&gt;

&lt;p&gt;A cold run against a Brazilian industrial-tools store, no configuration, no selectors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;160 pages crawled → 126 candidates → 50 clean products in 72 seconds.&lt;/strong&gt; Zero duplicates, zero category pages leaking into the results, ~113k input tokens (a few cents of model cost).&lt;/p&gt;

&lt;p&gt;Compare that with 35 hours of retyping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPA storefronts don't work yet.&lt;/strong&gt; If the products only exist after JavaScript runs (VTEX, headless Shopify, client-side Next.js), you'll get zero. Static HTML only, for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One page = one product&lt;/strong&gt; is assumed. Stores where SKUs exist only inside a filterable listing, with no individual URL, return nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt is respected by default.&lt;/strong&gt; There's an owner-authorization flag for when it's your own store.&lt;/li&gt;
&lt;li&gt;Crawling is rate-limited per host on purpose, so a large catalog takes minutes, not seconds. Pointing at the listing page instead of the homepage speeds things up a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it on your own migration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API listing (free tier, 50 calls/month): &lt;a href="https://rapidapi.com/felipelacerda/api/ai-product-catalog-extractor" rel="noopener noreferrer"&gt;AI Product Catalog Extractor on RapidAPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live demo of a real extraction: &lt;a href="http://72.60.244.148:3024/" rel="noopener noreferrer"&gt;72.60.244.148:3024&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How it was built, and the three cleanup guards that came from production bugs: &lt;a href="https://dev.to/crj_787403881c99662c653e8/i-built-an-ai-api-that-turns-any-online-store-url-into-a-structured-product-catalog-5775"&gt;I built an AI API that turns any online store URL into a structured product catalog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you point it at a store and it breaks, tell me which one — the edge cases are the roadmap.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>api</category>
    </item>
    <item>
      <title>I built an AI API that turns any online store URL into a structured product catalog</title>
      <dc:creator>CRJ</dc:creator>
      <pubDate>Sat, 05 Sep 2026 04:03:08 +0000</pubDate>
      <link>https://dev.to/crj_787403881c99662c653e8/i-built-an-ai-api-that-turns-any-online-store-url-into-a-structured-product-catalog-5775</link>
      <guid>https://dev.to/crj_787403881c99662c653e8/i-built-an-ai-api-that-turns-any-online-store-url-into-a-structured-product-catalog-5775</guid>
      <description>&lt;p&gt;I watched a store owner retype 600 products by hand while migrating platforms. It took her team two weeks. That pain became this API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You POST one URL — the homepage of any online store. The API crawls it the way a person would (categories, pagination, product pages) and returns the entire catalog as structured JSON: name, brand, category, price, SKU, photos, and an AI-written description for every product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://.../v1/extract &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://any-store.com"}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"job_id": "...", "poll": "/v1/jobs/&amp;lt;id&amp;gt;"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No CSS selectors to configure. No per-site templates to maintain. The LLM reads each page and understands it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part was not the crawling
&lt;/h2&gt;

&lt;p&gt;Everyone assumes the crawler is the hard part. It isn't — &lt;code&gt;fetch&lt;/code&gt; plus a BFS queue gets you there in an afternoon.&lt;/p&gt;

&lt;p&gt;The hard part is that &lt;strong&gt;e-commerce pages lie to you&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A category page looks exactly like a product page ("Steel Drill 4-12mm — from $29").&lt;/li&gt;
&lt;li&gt;Listing cards carry a name, a price and a photo — everything a product has.&lt;/li&gt;
&lt;li&gt;The same product appears at &lt;code&gt;/drills/steel-drill&lt;/code&gt; and &lt;code&gt;/promo/steel-drill&lt;/code&gt; with different descriptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We shipped three "cleanup guards" that only exist because each one fixed a real production incident: a listing-card detector (if a page links to N similar slugs, it's a category — return null), a dedup pass with slug-digit tie-breaking, and a parent-category pruner. The prompt itself instructs the model to answer &lt;em&gt;null&lt;/em&gt; when the page isn't a single product — teaching the AI to say "this isn't a product" mattered more than teaching it to extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from a real run
&lt;/h2&gt;

&lt;p&gt;A Brazilian industrial-tools store, cold start: &lt;strong&gt;160 pages crawled → 126 candidates → 50 clean products in 72 seconds&lt;/strong&gt;, ~113k input tokens. Zero duplicates, zero category pages leaking into the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript-rendered storefronts (SPA) aren't supported yet — that's the next tier.&lt;/li&gt;
&lt;li&gt;One page = one product is assumed; stores where SKUs only exist inside filterable listings won't extract.&lt;/li&gt;
&lt;li&gt;It respects robots.txt by default (there's an owner-override flag).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;There's a free tier (50 calls/month, no card): &lt;a href="https://rapidapi.com/felipelacerda/api/ai-product-catalog-extractor" rel="noopener noreferrer"&gt;AI Product Catalog Extractor on RapidAPI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Live demo of a real extraction (50 products, with photos): &lt;a href="http://72.60.244.148:3024/robo/" rel="noopener noreferrer"&gt;demo page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd genuinely love to hear about stores that break it — edge cases are the roadmap.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
