<?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: Ricardo Batista</title>
    <description>The latest articles on DEV Community by Ricardo Batista (@rbatista19).</description>
    <link>https://dev.to/rbatista19</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%2F1418786%2F23772d20-e7df-4f2f-a42e-3b638a582674.png</url>
      <title>DEV Community: Ricardo Batista</title>
      <link>https://dev.to/rbatista19</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rbatista19"/>
    <language>en</language>
    <item>
      <title>What does it take to run a headless browser farm for ChatGPT at scale? The engineering build</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Fri, 09 Oct 2026 20:10:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/what-does-it-take-to-run-a-headless-browser-farm-for-chatgpt-at-scale-the-engineering-build-22ho</link>
      <guid>https://dev.to/rbatista19/what-does-it-take-to-run-a-headless-browser-farm-for-chatgpt-at-scale-the-engineering-build-22ho</guid>
      <description>&lt;p&gt;A headless browser farm for ChatGPT at scale takes five components in every browser and three systems around the browsers. Each browser needs a stealth-patched Chromium, a sticky residential proxy session, a network interceptor on the conversation endpoint, a completion detector and a citation expander. Around them, the farm needs a pool of warm sessions, routing per region and monitoring per field.&lt;/p&gt;

&lt;p&gt;The build is the smaller cost. One published estimate from cloro puts a self-run ChatGPT scraper at roughly $4,800 to $14,100 a month at 500,000 to 1 million requests, with engineer time as the largest line. The alternatives are rented browser infrastructure such as Bright Data or Browserbase, where the parser stays yours, and a managed endpoint such as cloro, which bills 5 credits for each ChatGPT web-search answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The response is a Server-Sent Events stream, so the scraper reads the network and not the DOM.&lt;/li&gt;
&lt;li&gt;The failures that cost the most return a success status with part of the data missing.&lt;/li&gt;
&lt;li&gt;Playwright wins when answer data is your product. A paid API wins when the data is an input.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are the components of a headless browser farm for ChatGPT?
&lt;/h2&gt;

&lt;p&gt;Each browser in the farm runs the same five parts. Each part answers one specific failure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stealth-patched Playwright Chromium.&lt;/strong&gt; A WebDriver-controlled browser exposes &lt;code&gt;navigator.webdriver&lt;/code&gt; as &lt;code&gt;true&lt;/code&gt;, and detection scripts read that flag on the first navigation. Apply &lt;code&gt;playwright-stealth&lt;/code&gt; or equivalent canvas, WebGL and audioContext patches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A residential proxy with a sticky session.&lt;/strong&gt; Datacenter IPs get a CAPTCHA on the first prompt. Cloudflare sets the &lt;code&gt;cf_clearance&lt;/code&gt; cookie after a visitor solves a challenge and skips further challenges while the cookie is valid. The session must keep that cookie across the round trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A response interceptor scoped to &lt;code&gt;backend-api/f/conversation&lt;/code&gt;.&lt;/strong&gt; ChatGPT makes other API calls during the page lifecycle. Only this URL carries the stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A completion detector that looks for &lt;code&gt;[DONE]&lt;/code&gt;.&lt;/strong&gt; DOM checks for "is the response complete" return false positives during streaming. The &lt;code&gt;[DONE]&lt;/code&gt; sentinel in the stream body is the reliable signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A citation expander.&lt;/strong&gt; Citations are not in the stream. The scraper clicks &lt;code&gt;button.group/footnote&lt;/code&gt; after the response completes and parses the flyout at &lt;code&gt;[data-testid="screen-threadFlyOut"]&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interceptor is a Playwright &lt;code&gt;response&lt;/code&gt; handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_page_interceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Set up network request interception.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Capture conversation API responses
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;backend-api/f/conversation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured_responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The captured body is raw SSE text. The parser splits it on &lt;code&gt;data:&lt;/code&gt; lines, drops the sentinel and keeps the valid JSON objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_raw_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parse ChatGPT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s Server-Sent Events stream.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;json_objects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Split by lines that start with "data: "
&lt;/span&gt;    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Skip empty lines and non-data lines
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# Remove "data: " prefix
&lt;/span&gt;        &lt;span class="n"&gt;json_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Skip special markers like [DONE]
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;json_str&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# Try to parse as JSON
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Only include if it's a dictionary (object), not string or other types
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;json_objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Skip invalid JSON
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json_objects&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://cloro.dev/blog/scrape-chatgpt/" rel="noopener noreferrer"&gt;full scraper class&lt;/a&gt; that these two excerpts come from launches Chromium with &lt;code&gt;headless=False&lt;/code&gt;. That choice is deliberate. Headless defaults are the first signal that bot detection reads, so a production farm runs full browser environments with consistent canvas, font and TLS fingerprints.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks when you build an LLM scraper in-house?
&lt;/h2&gt;

&lt;p&gt;Five failure modes hit a ChatGPT scraper in production. We run about 1,000 engine answers a day across six engines for cloro's own tracking corpus, and these are the failures that operation meets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Why a retry does not fix it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response format shift&lt;/td&gt;
&lt;td&gt;The answer arrives as HTML fragments. The model name and search queries are absent, and the source list is shorter.&lt;/td&gt;
&lt;td&gt;OpenAI serves a share of logged-out traffic in a mobile-web format and selects the format per request.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model rotation&lt;/td&gt;
&lt;td&gt;Fields that depend on the model, such as fan-out queries under &lt;code&gt;gpt-5-3&lt;/code&gt;, appear on some answers only.&lt;/td&gt;
&lt;td&gt;ChatGPT selects the serving model per request. No parameter controls it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional login wall&lt;/td&gt;
&lt;td&gt;Anonymous sessions fail for one geography.&lt;/td&gt;
&lt;td&gt;The provider changed its access rules for that region.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-bot escalation&lt;/td&gt;
&lt;td&gt;Rate limits, session churn and fingerprint checks start at volume.&lt;/td&gt;
&lt;td&gt;A browser that works at 10 answers a day meets them at 10,000.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent quality drift&lt;/td&gt;
&lt;td&gt;The scrape succeeds and the source list is missing half its entries.&lt;/td&gt;
&lt;td&gt;A DOM class changed. No request failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first and last rows cost the most. A parser built against one format reports the other as a partial success, and a partial success sends no alert.&lt;/p&gt;

&lt;p&gt;The targets also move. The &lt;code&gt;backend-api/f/conversation&lt;/code&gt; URL changed twice in 2025. The shopping-card event moved from a flat &lt;code&gt;products&lt;/code&gt; array to a nested &lt;code&gt;offers&lt;/code&gt; block. OpenAI's build pipeline generates CSS class names that change between deploys, so a scraper that relies on class selectors breaks roughly weekly. ARIA labels, role attributes and text content last longer than class names, and both still break.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep the farm from getting blocked at scale?
&lt;/h2&gt;

&lt;p&gt;The farm must look and behave like the logged-out users that ChatGPT already serves. Do these four things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep a pool of established sessions. Engines challenge new sessions harder than warm ones, and one cleared session serves many requests before rotation.&lt;/li&gt;
&lt;li&gt;Match the exit region to the request. An answer requested for Germany must exit in Germany. Traffic concentrated in one region meets volume ceilings.&lt;/li&gt;
&lt;li&gt;Space the retries and add jitter. Immediate identical retries are a bot signature.&lt;/li&gt;
&lt;li&gt;Treat a degraded format as a block. It costs the engine less to serve a reduced response to suspected automation than to refuse it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The IP type sets the ceiling. In testing published by proxies.sx, real mobile IPs lasted 50 to 100 or more queries, and datacenter IPs were blocked within the first few requests. That figure is the vendor's and was not reproduced for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you know the farm still returns good data?
&lt;/h2&gt;

&lt;p&gt;Status codes do not show the two failures that cost the most, so monitor the fields. Track these rates per day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answers with an empty model name&lt;/li&gt;
&lt;li&gt;answers with a short source list&lt;/li&gt;
&lt;li&gt;answers with no fan-out queries where you expect them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A change in any of the three is a format change or a model rotation in your sample.&lt;/p&gt;

&lt;p&gt;One invariant helps with ChatGPT. We measured grounding on roughly 2,500 prompts per engine, and ChatGPT grounded 98.4% of its answers in live sources at 14.1 sources each. A re-measurement on 2026-08-19 gave 78.9% and 12.7 sources. An empty citation list from a ChatGPT scrape is more often a parse failure than an ungrounded answer, so assert a source list that is not empty in your tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playwright or a paid API for scraping AI chat answers?
&lt;/h2&gt;

&lt;p&gt;Playwright is the correct choice when answer data is your product, your volume pays for a dedicated team and you need control that a vendor cannot give. It is free and open source, every layer is yours to tune and there is no vendor lock-in. A team with no budget and strong engineers can also run it at low volume. The roundup linked below budgets 8 to 15 engineer hours a month for a Playwright stack at 1,000 queries a day.&lt;/p&gt;

&lt;p&gt;A paid API is the correct choice when answer data is an input to another product. Under roughly 1,000 answers a day, a provider costs less as soon as you price your own time. Two kinds of paid API exist, and they are not the same purchase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser and proxy infrastructure&lt;/strong&gt; (Bright Data, Oxylabs, Browserbase, Browserless). The access layer is solved. The SSE parser, the citation expander and the selectors stay with you. Bright Data prices its hosted Browser API from $5 per GB of traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A managed answer endpoint&lt;/strong&gt; (cloro is one). The provider owns all five components and returns parsed JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The managed call looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Your prompt
&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compare the top 3 programming languages for web development in 2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# API request to cloro
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.cloro.dev/v1/monitor/chatgpt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rawResponse&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;searchQueries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ChatGPT web-search answer costs 5 credits there as an async request. A synchronous request, as in the sample above, adds 2 credits. 5,000 prompts a day is about 750,000 credits a month, which fits in the Growth plan at $500 a month for 1,350,000 credits. That is $1.85 per 1,000 answers. Failed requests are not billed. The trade is control: you cannot tune a session pool that you do not run.&lt;/p&gt;

&lt;p&gt;Neither option fits if you want the model's output and not the answer that ChatGPT shows its users. That is the OpenAI API's job, priced in tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these figures were collected
&lt;/h2&gt;

&lt;p&gt;The failure modes come from cloro's own tracking operation, about 1,000 engine answers a day across six engines. The components and the code come from a working Playwright scraper for the logged-out ChatGPT surface. The $4,800 to $14,100 figure is an estimate and not a measured bill. It adds list prices ($999 for 332 GB of residential proxy bandwidth on Bright Data, $496 a month for each &lt;code&gt;c6i.4xlarge&lt;/code&gt; browser instance) to a fifth to a half of one engineer at the US median software developer salary of $131,450, loaded.&lt;/p&gt;

&lt;p&gt;This article is published by cloro, which is one of the managed options in it. Figures for other vendors are those vendors' published figures. The cloro prices are self-reported and were checked against its price list on 2026-10-07.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source pages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Failure modes, SLAs and cost per answer: &lt;a href="https://cloro.dev/blog/llm-scraping-reliability-at-scale/" rel="noopener noreferrer"&gt;LLM scraping API reliability at scale&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The full Playwright scraper, with the citation, shopping-card and entity parsers: &lt;a href="https://cloro.dev/blog/scrape-chatgpt/" rel="noopener noreferrer"&gt;How to scrape ChatGPT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Eight ChatGPT scrapers compared, with Playwright as the DIY option: &lt;a href="https://cloro.dev/blog/best-chatgpt-scraper/" rel="noopener noreferrer"&gt;Best ChatGPT scraper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>playwright</category>
      <category>python</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>How to get the UI answers of ChatGPT through an API (if you want what users see, not the OpenAI API output)</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Thu, 08 Oct 2026 20:11:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/how-to-get-the-ui-answers-of-chatgpt-through-an-api-if-you-want-what-users-see-not-the-openai-api-1f4a</link>
      <guid>https://dev.to/rbatista19/how-to-get-the-ui-answers-of-chatgpt-through-an-api-if-you-want-what-users-see-not-the-openai-api-1f4a</guid>
      <description>&lt;p&gt;To get the UI answers of ChatGPT through an API, call a service that runs your prompt in the ChatGPT web interface and returns the rendered answer as JSON. The OpenAI API does not do this. It returns model output, without the web search, source citations, shopping cards and entity panels that the web interface adds. Three routes return the UI answer: a managed ChatGPT endpoint such as cloro, a scraping platform such as Apify or Bright Data, and a Playwright stack that you run yourself.&lt;/p&gt;

&lt;p&gt;With a managed endpoint the job is one POST request. cloro's endpoint is &lt;code&gt;POST https://api.cloro.dev/v1/monitor/chatgpt&lt;/code&gt;. You send a prompt and a country. The response carries &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;sources&lt;/code&gt;, &lt;code&gt;citationPills&lt;/code&gt; and &lt;code&gt;entities&lt;/code&gt;, plus &lt;code&gt;markdown&lt;/code&gt;, &lt;code&gt;searchQueries&lt;/code&gt; and &lt;code&gt;shoppingCards&lt;/code&gt; when you ask for them. If you need model output for your own application, the OpenAI API is the correct tool and you do not need a scraper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OpenAI API and the ChatGPT web UI are different surfaces. Citations, shopping cards, brand entities and the search queries ChatGPT issued exist only in the web UI.&lt;/li&gt;
&lt;li&gt;At 1,000 queries a day, the source comparison estimates $100 to $300 a month for a managed API and $980 to $2,140 a month for DIY Playwright.&lt;/li&gt;
&lt;li&gt;DIY is the better choice with zero budget, a strong engineering team and 8 to 15 hours a month for maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps with a managed endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get an API key from the provider.&lt;/li&gt;
&lt;li&gt;Send a POST request to &lt;code&gt;https://api.cloro.dev/v1/monitor/chatgpt&lt;/code&gt; with &lt;code&gt;prompt&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;include&lt;/code&gt; flags for the optional fields that you need.&lt;/li&gt;
&lt;li&gt;Set the client timeout to 300 seconds or more.&lt;/li&gt;
&lt;li&gt;Read the parsed fields from &lt;code&gt;result&lt;/code&gt; in the JSON response.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why does the OpenAI API return a different answer than the ChatGPT UI?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;chat.completions&lt;/code&gt; API and the &lt;code&gt;chatgpt.com&lt;/code&gt; web interface use different stacks. The API answers from a base model. The web interface does more work before the answer reaches the screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It breaks the prompt into sub-queries (query fan-out).&lt;/li&gt;
&lt;li&gt;It runs a web search against OpenAI's search index.&lt;/li&gt;
&lt;li&gt;It adds citations from the search results.&lt;/li&gt;
&lt;li&gt;For shopping prompts, it adds a product card with merchant offers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two measurements show the size of the difference. Across roughly 2,500 prompts, we found that &lt;a href="https://cloro.dev/blog/ai-grounding-by-engine/" rel="noopener noreferrer"&gt;ChatGPT grounded 98.4% of its answers in live sources, at 14.1 sources each&lt;/a&gt;. A second measurement on 2026-08-19 gave 78.9% and 12.7 sources. And &lt;a href="https://surferseo.com/blog/llm-scraped-ai-answers-vs-api-results/" rel="noopener noreferrer"&gt;Surfer's analysis&lt;/a&gt; reports only about 20% overlap between API responses and the answers that users see in the ChatGPT UI. That figure is Surfer's and was not reproduced here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is there an API that returns what ChatGPT actually shows real users?
&lt;/h2&gt;

&lt;p&gt;Yes. A ChatGPT scraper API runs the prompt in the consumer product and returns the answer that a user sees. OpenAI does not offer one. Its developer API (&lt;code&gt;platform.openai.com&lt;/code&gt;) returns model completions, with no source pills, shopping cards or ads.&lt;/p&gt;

&lt;p&gt;Four data types exist only in the web UI: sources and citations, shopping cards with merchant offers, brand entities, and the sub-queries that ChatGPT issued before it answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I get ChatGPT responses programmatically without using the OpenAI API?
&lt;/h2&gt;

&lt;p&gt;There are four routes. The cost column is an estimate at 1,000 queries a day from &lt;a href="https://cloro.dev/blog/best-chatgpt-scraper/" rel="noopener noreferrer"&gt;cloro's comparison of 8 tools&lt;/a&gt;, with proxies, CAPTCHA solving and engineer time included.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;What you still build&lt;/th&gt;
&lt;th&gt;Estimated cost per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed ChatGPT endpoint&lt;/td&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;Nothing. Fields arrive parsed.&lt;/td&gt;
&lt;td&gt;$100 to $300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scraping platform&lt;/td&gt;
&lt;td&gt;Apify actors, Bright Data Scraping Browser&lt;/td&gt;
&lt;td&gt;Cookie refresh (Apify) or the parsing layer (Bright Data)&lt;/td&gt;
&lt;td&gt;$280 to $900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser infrastructure or anti-bot API&lt;/td&gt;
&lt;td&gt;Browserbase, Browserless, ScrapingBee, ZenRows&lt;/td&gt;
&lt;td&gt;Stream assembly, citation parsing, selectors&lt;/td&gt;
&lt;td&gt;$449 to $1,040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY framework&lt;/td&gt;
&lt;td&gt;Playwright with stealth plugins&lt;/td&gt;
&lt;td&gt;All of it&lt;/td&gt;
&lt;td&gt;$980 to $2,140&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The DIY route needs five components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stealth-patched Playwright Chromium. Vanilla Playwright is detected on the first navigation.&lt;/li&gt;
&lt;li&gt;A residential proxy with a sticky session. Datacenter IPs get a CAPTCHA on the first prompt.&lt;/li&gt;
&lt;li&gt;A network interceptor for &lt;code&gt;backend-api/f/conversation&lt;/code&gt;. Only that URL carries the answer stream.&lt;/li&gt;
&lt;li&gt;A completion check that waits for the &lt;code&gt;[DONE]&lt;/code&gt; line in the stream.&lt;/li&gt;
&lt;li&gt;A step that clicks the sources button and reads the flyout after the answer completes. Citations are not in the stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ChatGPT sends the answer as Server-Sent Events. The first function a DIY scraper needs is the parser for that stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_raw_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parse ChatGPT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s Server-Sent Events stream.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;json_objects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Split by lines that start with "data: "
&lt;/span&gt;    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Skip empty lines and non-data lines
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# Remove "data: " prefix
&lt;/span&gt;        &lt;span class="n"&gt;json_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Skip special markers like [DONE]
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;json_str&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# Try to parse as JSON
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Only include if it's a dictionary (object), not string or other types
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;json_objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Skip invalid JSON
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json_objects&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete Playwright class, the citation extractor and the shopping-card parser are in the &lt;a href="https://cloro.dev/blog/scrape-chatgpt/" rel="noopener noreferrer"&gt;full DIY guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you call an API for the ChatGPT UI answer?
&lt;/h2&gt;

&lt;p&gt;This request comes from the public &lt;a href="https://github.com/cloro-dev/chatgpt-scraper" rel="noopener noreferrer"&gt;chatgpt-scraper README&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;best project management software for remote teams&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;searchQueries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.cloro.dev/v1/monitor/chatgpt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call with cURL, this time with shopping cards:&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://api.cloro.dev/v1/monitor/chatgpt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&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;'{"prompt": "best running shoes for flat feet", "country": "US", "include": {"shopping": true}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt&lt;/code&gt; (required)&lt;/td&gt;
&lt;td&gt;The query or question, 1 to 10,000 characters&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;country&lt;/code&gt; (required)&lt;/td&gt;
&lt;td&gt;Country code for localized results (&lt;code&gt;US&lt;/code&gt;, &lt;code&gt;GB&lt;/code&gt;, &lt;code&gt;DE&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;US state code&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.markdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the answer as Markdown&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return a URL to the full HTML, which expires after 24 hours&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.rawResponse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the unparsed upstream payload&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.searchQueries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the query fan-out terms&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.shopping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return shopping cards and inline products&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include.ads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return sponsored blocks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disableWebSearch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the answer ChatGPT gives without a forced web search&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The response:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"For remote teams, the strongest options are..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"citationPills"&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;"citationPillId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"Asana"&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://asana.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asana.com"&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;"searchQueries"&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="s2"&gt;"best project management software 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asana vs monday remote teams"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"For remote teams, the strongest options are **Asana**..."&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field in &lt;code&gt;result&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;text&lt;/code&gt;, &lt;code&gt;markdown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The answer as plain text and as Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Each cited URL with &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;citationPills&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The inline citation chips. Entries share a &lt;code&gt;citationPillId&lt;/code&gt; when one pill cites several sources.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The products, brands and concepts that the answer names, each with a type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;searchQueries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The search terms that ChatGPT ran&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shoppingCards&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Product carousels. Each one groups &lt;code&gt;products[]&lt;/code&gt; with price and merchant offers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inlineProducts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Products in the answer text, separate from the carousels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sponsored blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business and location entries with rating, reviews and address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rawResponse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The unparsed upstream payload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details affect a production client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeout.&lt;/strong&gt; The README recommends 300 seconds or more, because failed attempts are retried on the server before the response returns. A typical response takes 30 to 45 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search queries.&lt;/strong&gt; The README states that &lt;code&gt;searchQueries&lt;/code&gt; comes back empty on ChatGPT's default mobile-web interface. The &lt;code&gt;legacy: true&lt;/code&gt; option asks for the interface that returns it, on a best-effort basis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batches.&lt;/strong&gt; For thousands of prompts a day, use &lt;code&gt;POST /v1/async/task&lt;/code&gt; with &lt;code&gt;taskType: "CHATGPT"&lt;/code&gt; and receive the results by webhook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The price list has two ChatGPT rows. "ChatGPT (web search)" is the base request at 5 credits. "ChatGPT (full response)" is 7 credits: the base request plus one flat charge of 2 credits for any of &lt;code&gt;include.rawResponse&lt;/code&gt;, &lt;code&gt;include.searchQueries&lt;/code&gt;, &lt;code&gt;include.ads&lt;/code&gt; and &lt;code&gt;include.shopping&lt;/code&gt;. These are async prices. A synchronous request to &lt;code&gt;/v1/monitor/chatgpt&lt;/code&gt; adds 2 credits, so the two sample requests above cost 9 credits each. The Hobby plan is $100 a month for 250,000 credits. A panel of 1,000 prompts a day uses about 210,000 credits a month at 7 credits, or about 150,000 at 5 credits. The Free plan is 500 credits a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should I use instead of building on unofficial ChatGPT API wrappers?
&lt;/h2&gt;

&lt;p&gt;Use a route that someone maintains against ChatGPT's changes, or budget for that maintenance yourself. Code that depends on ChatGPT's private web endpoint breaks when the endpoint changes. The &lt;code&gt;backend-api/f/conversation&lt;/code&gt; path changed twice in 2025. The source guide also records two changes to the stream chunk shape in one year, and the shopping-card event moved from a flat &lt;code&gt;products&lt;/code&gt; array to a nested &lt;code&gt;offers&lt;/code&gt; block. OpenAI's CSS class names also change between deploys, so class selectors break roughly weekly.&lt;/p&gt;

&lt;p&gt;API routers such as LiteLLM and OpenRouter are a different category. They wrap the official API, so they return the API answer and not the UI answer.&lt;/p&gt;

&lt;p&gt;The contract matters too. In a &lt;a href="https://community.openai.com/t/is-it-against-tos-to-scrape-a-conversation-with-chatgpt/74678" rel="noopener noreferrer"&gt;2023 thread on OpenAI's developer forum&lt;/a&gt;, users report that the Terms of Use bar data extraction by scraping other than through the API. That risk is highest for an account that accepted the terms. Read the current &lt;a href="https://openai.com/policies/terms-of-use/" rel="noopener noreferrer"&gt;OpenAI Terms of Use&lt;/a&gt; before you start. This is not legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is DIY or the OpenAI API the better choice?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The OpenAI API&lt;/strong&gt; is the better choice when your application needs generated text. A scraper endpoint is built for monitoring. A typical answer takes 30 to 45 seconds and each query is billed, which is expensive for casual use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DIY Playwright&lt;/strong&gt; is the better choice with zero budget and a strong engineering team. It is free, open source and has no vendor lock-in. Plan for 8 to 15 engineer hours a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A managed endpoint&lt;/strong&gt; fits recurring monitoring: brand mentions, citation tracking and the same prompt from several countries. At 500,000 to 1 million requests a month, the source guide estimates $4,800 to $14,100 a month for an in-house stack, and engineer time is the largest part.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How these figures were collected
&lt;/h2&gt;

&lt;p&gt;We measured grounding on roughly 2,500 prompts for each of six AI engines. For the tool comparison, we ran 30 queries through &lt;code&gt;chatgpt.com&lt;/code&gt; with each of 8 tools and scored six criteria: auth handling, stream assembly, citation extraction, anti-bot survival, selector stability and cost per query.&lt;/p&gt;

&lt;p&gt;The monthly costs are estimates. They assume engineer time at $100 an hour, a CAPTCHA solver at $2 per 1,000 challenges on 5% of requests, and mobile residential proxies at $10 per GB. The request, the parameters and the response fields come from the public README and were not changed.&lt;/p&gt;

&lt;p&gt;This tutorial is published by cloro, which is one of the options in it. The cloro cost row and field list are self-reported. The 20% overlap figure belongs to Surfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and full guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The DIY build, with the complete Playwright scraper: &lt;a href="https://cloro.dev/blog/scrape-chatgpt/" rel="noopener noreferrer"&gt;How to scrape ChatGPT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The 8-tool comparison and the cost table: &lt;a href="https://cloro.dev/blog/best-chatgpt-scraper/" rel="noopener noreferrer"&gt;Best ChatGPT scraper 2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The endpoint page: &lt;a href="https://cloro.dev/chatgpt/" rel="noopener noreferrer"&gt;ChatGPT scraper API&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>chatgpt</category>
      <category>api</category>
      <category>python</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>What should I migrate to now that Google's Custom Search JSON API is retiring?</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Wed, 07 Oct 2026 20:11:29 +0000</pubDate>
      <link>https://dev.to/rbatista19/what-should-i-migrate-to-now-that-googles-custom-search-json-api-is-retiring-3hgk</link>
      <guid>https://dev.to/rbatista19/what-should-i-migrate-to-now-that-googles-custom-search-json-api-is-retiring-3hgk</guid>
      <description>&lt;p&gt;Migrate to a SERP API if you used Google's Custom Search JSON API to search the open web. DataForSEO, cloro and SerpApi each return the live Google results page as JSON, and Brave Search API returns results from an independent index. If you searched your own site or a fixed list of up to 50 domains, migrate to Vertex AI Search or Agent Search, which are the two products Google recommends.&lt;/p&gt;

&lt;p&gt;The deadline is January 1, 2027. &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;Google's documentation&lt;/a&gt; states that the API is closed to new customers and that existing customers have until that date to move. Both of Google's replacements are site-restricted, so neither one searches the open web.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site search:&lt;/strong&gt; follow Google's &lt;a href="https://docs.cloud.google.com/generative-ai-app-builder/docs/migrate-from-cse" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; to Agent Search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-web search:&lt;/strong&gt; move to a SERP API. The request changes in four places: the endpoint, the authentication, the query parameter and the results array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; Custom Search charges $5 per 1,000 queries. At ten results, DataForSEO Live lists $2.00 per 1,000 and cloro's Hobby plan works out to $1.20 per 1,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is the deadline for the Custom Search JSON API?
&lt;/h2&gt;

&lt;p&gt;The Custom Search JSON API stops being available to existing customers on January 1, 2027. It is already closed to new customers. Until the deadline, the published terms apply: 100 free queries a day, $5 per 1,000 queries above that, and a limit of 10,000 queries a day.&lt;/p&gt;

&lt;p&gt;The change also covers Programmable Search Engine. According to &lt;a href="https://brave.com/learn/google-api-shutdown/" rel="noopener noreferrer"&gt;Brave's summary of the announcement&lt;/a&gt;, each engine created since January 20, 2026 must use the "Sites to search" setting, which has a limit of 50 domains. An existing engine set to "Search the entire web" keeps that setting until January 1, 2027.&lt;/p&gt;

&lt;p&gt;A new API key fails today. A developer &lt;a href="https://www.reddit.com/r/googlecloud/comments/1qilhrg/comment/o0tod1e/" rel="noopener noreferrer"&gt;reported on r/googlecloud in January 2026&lt;/a&gt; that a new key returned 403 Forbidden, although the engine ID worked in the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Google recommend as a replacement?
&lt;/h2&gt;

&lt;p&gt;Google names two products, and both are site-restricted.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;API overview&lt;/a&gt; recommends Vertex AI Search for search across up to 50 domains. For anything larger it refers you to a full web search solution, and you must contact Google to get it.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.cloud.google.com/generative-ai-app-builder/docs/migrate-from-cse" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; recommends Agent Search, which it describes as Google-quality site-restricted search. You deploy it as a widget or through an API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration guide describes these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable Agent Search in the Google Cloud Console.&lt;/li&gt;
&lt;li&gt;Create an app configured for site search with AI mode.&lt;/li&gt;
&lt;li&gt;Turn on Enterprise edition features. The guide makes them mandatory for website search.&lt;/li&gt;
&lt;li&gt;Create a data store and populate it with website content.&lt;/li&gt;
&lt;li&gt;Specify the URL patterns to index.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Authentication changes too. Custom Search used an API key. Agent Search primarily expects OAuth 2.0, and its &lt;code&gt;searchLite&lt;/code&gt; method still accepts an API key. The guide also notes that summaries and domain verification can carry additional indexing costs.&lt;/p&gt;

&lt;p&gt;Google plans a paid open-web service for large customers. An email from Google's Programmable Search Engine team, &lt;a href="https://news.ycombinator.com/item?id=46730437" rel="noopener noreferrer"&gt;posted on Hacker News&lt;/a&gt;, describes an enterprise Web Search Service at $15 per 1,000 requests with a minimum monthly fee of $30,000. The email says more details are due later in 2026. That minimum equals 2,000,000 requests a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which replacement fits your workload?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you used Custom Search for&lt;/th&gt;
&lt;th&gt;Migrate to&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search on your own site, or on a fixed list of up to 50 domains&lt;/td&gt;
&lt;td&gt;Vertex AI Search or Agent Search&lt;/td&gt;
&lt;td&gt;Google built both for site-restricted search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google results from the open web, billed pay as you go&lt;/td&gt;
&lt;td&gt;DataForSEO (Live)&lt;/td&gt;
&lt;td&gt;$2.00 per 1,000 queries with no monthly minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google results with the AI Overview in the same call&lt;/td&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;The AI Overview and its cited sources are in the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google plus other engines&lt;/td&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;It covers more than 80 engines, with Bing, Baidu and Yandex among them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bing results&lt;/td&gt;
&lt;td&gt;SerpApi or DataForSEO&lt;/td&gt;
&lt;td&gt;Both return Bing results directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A non-Google index&lt;/td&gt;
&lt;td&gt;Brave Search API&lt;/td&gt;
&lt;td&gt;It runs an independent index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context for an LLM agent, where rank position is not needed&lt;/td&gt;
&lt;td&gt;A retrieval API such as Tavily or Exa&lt;/td&gt;
&lt;td&gt;These return relevance-ranked content and not the Google results page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two limits apply to cloro. Its plans are monthly, so a workload with low or irregular volume can cost less on a pay-as-you-go provider. It does not return Bing results.&lt;/p&gt;

&lt;p&gt;A SERP API has no &lt;code&gt;cx&lt;/code&gt; parameter and no site restriction. If the site restriction was the reason you used Custom Search, the first row is your case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does each open-web replacement cost per 1,000 queries?
&lt;/h2&gt;

&lt;p&gt;Each rate is for 1,000 queries at ten results, which is the shape Custom Search returned. Each rate is the vendor's published real-time list price.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Rate per 1,000 queries&lt;/th&gt;
&lt;th&gt;Index&lt;/th&gt;
&lt;th&gt;AI Overview&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom Search JSON API (until 2027)&lt;/td&gt;
&lt;td&gt;$5.00, with a limit of 10,000 a day&lt;/td&gt;
&lt;td&gt;Programmable Search Engine&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataForSEO (Live)&lt;/td&gt;
&lt;td&gt;$2.00, or $4.00 with AI Overview&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Yes, behind a flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloro (Hobby plan)&lt;/td&gt;
&lt;td&gt;$1.20, or $2.00 with AI Overview&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi (Big Data plan)&lt;/td&gt;
&lt;td&gt;$9.17&lt;/td&gt;
&lt;td&gt;Google plus more than 80 engines&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brave Search API&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;Independent index&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScraperAPI&lt;/td&gt;
&lt;td&gt;About $12.00 through its structured Google endpoint&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Web Search Service (planned)&lt;/td&gt;
&lt;td&gt;$15.00, with a $30,000 monthly minimum&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The plan sets the rate on the two subscription vendors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cloro Google call costs 3 credits, or 5 credits with the AI Overview. These are the prices of an async request. A synchronous request to &lt;code&gt;/v1/monitor/google&lt;/code&gt; adds 2 credits.&lt;/li&gt;
&lt;li&gt;The Hobby plan is $100 a month for 250,000 credits. The two calls cost $1.20 and $2.00 per 1,000.&lt;/li&gt;
&lt;li&gt;The Growth plan is $500 a month for 1,350,000 credits. The same calls cost $1.11 and $1.85 per 1,000.&lt;/li&gt;
&lt;li&gt;The Lite plan is $30 a month for 37,500 credits. The same calls cost $2.40 and $4.00 per 1,000.&lt;/li&gt;
&lt;li&gt;SerpApi's Big Data plan is $275 a month for 30,000 searches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The daily limit also goes away. Custom Search stops at 10,000 queries a day at any price, and the per-call vendors have no equivalent limit.&lt;/p&gt;

&lt;p&gt;The results page has changed since most Custom Search integrations were written. In cloro's &lt;a href="https://cloro.dev/research/serp-feature-census/" rel="noopener noreferrer"&gt;SERP feature census&lt;/a&gt;, we scored 65,945 results and found an AI Overview on 87.2% of US results and People Also Ask on 90.6%. Custom Search returned neither block. A port that keeps only the ten organic links omits both.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do Custom Search JSON fields map to a SERP API response?
&lt;/h2&gt;

&lt;p&gt;The Custom Search request and response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://www.googleapis.com/customsearch/v1
  ?key=&amp;lt;key&amp;gt;&amp;amp;cx=&amp;lt;engine id&amp;gt;&amp;amp;q=serp+api
  Results: items[] → { title, link, snippet, displayLink }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same query on a SERP API, with cloro's Google endpoint as the worked example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.cloro.dev/v1/monitor/google
  Authorization: Bearer &amp;lt;key&amp;gt;
  { "query": "serp api", "country": "US",
    "include": { "aioverview": { "markdown": true } } }
  Results: result.organicResults[] → { position, title, link, snippet, displayedLink }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Custom Search&lt;/th&gt;
&lt;th&gt;SERP API (worked example)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;key&lt;/code&gt; query parameter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Authorization: Bearer&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cx&lt;/code&gt; (search engine ID)&lt;/td&gt;
&lt;td&gt;Not needed. There is no engine to configure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;items[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result.organicResults[]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;link&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snippet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;snippet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;displayLink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;displayedLink&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;position&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;result.aioverview&lt;/code&gt;, with cited sources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of the four result fields keep their names. Other vendors use different paths, so check the array name in the vendor's documentation before you write the parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you migrate from Custom Search to a SERP API?
&lt;/h2&gt;

&lt;p&gt;Steps 3 to 8 use the field names from the worked example above.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find out which half of Custom Search you used. If your engine was restricted to your own sites, follow Google's migration guide and stop here.&lt;/li&gt;
&lt;li&gt;Select a vendor from the workload table.&lt;/li&gt;
&lt;li&gt;Replace the &lt;code&gt;GET https://www.googleapis.com/customsearch/v1&lt;/code&gt; request with a &lt;code&gt;POST&lt;/code&gt; to the vendor's endpoint.&lt;/li&gt;
&lt;li&gt;Move the API key from the &lt;code&gt;key&lt;/code&gt; query parameter to the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;cx&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;Rename &lt;code&gt;q&lt;/code&gt; to &lt;code&gt;query&lt;/code&gt; and send it in the JSON body.&lt;/li&gt;
&lt;li&gt;Read the results from &lt;code&gt;result.organicResults[]&lt;/code&gt; and not from &lt;code&gt;items[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rename &lt;code&gt;displayLink&lt;/code&gt; to &lt;code&gt;displayedLink&lt;/code&gt; in your parser. Keep &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt; and &lt;code&gt;snippet&lt;/code&gt; as they are.&lt;/li&gt;
&lt;li&gt;Decide whether to store &lt;code&gt;position&lt;/code&gt; and &lt;code&gt;result.aioverview&lt;/code&gt;. Custom Search returned neither.&lt;/li&gt;
&lt;li&gt;Send a real query set to the new API on its free tier. The free tier in the worked example is 500 credits a month, and SerpApi's is 250 searches a month.&lt;/li&gt;
&lt;li&gt;Compare the new results with your current Custom Search results for the same queries.&lt;/li&gt;
&lt;li&gt;Run both APIs in parallel for one month before January 1, 2027.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Should you wait for a first-party replacement from Google?
&lt;/h2&gt;

&lt;p&gt;No like-for-like replacement arrived the last time a first-party search API was retired. Microsoft retired the Bing Search API v7 on August 11, 2025, and the endpoints now return HTTP 410 Gone. The official replacement was Grounding with Bing Search in Azure AI Agents, which is an agent product and not a SERP API. Teams that waited for a like-for-like successor then migrated under time pressure.&lt;/p&gt;

&lt;p&gt;Google has given more notice. The first-party path again leads to a product with a different shape: site search for most customers, and a $30,000 monthly minimum for the open web.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these figures were collected
&lt;/h2&gt;

&lt;p&gt;The dates, limits and migration steps come from Google's Custom Search JSON API overview and Google's migration guide, as read on September 2, 2026. The Programmable Search Engine change is from Brave's summary. The Web Search Service price is from an email posted on Hacker News and Google has not published it on a pricing page. Each vendor rate is that vendor's published list price for its real-time mode, and prices in this category change often.&lt;/p&gt;

&lt;p&gt;This guide is published by cloro, which is one of the providers in it. Its own figures are self-reported. The census figures come from 65,945 results that we scored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full guide
&lt;/h2&gt;

&lt;p&gt;The full shutdown guide with the cost chart is here: &lt;a href="https://cloro.dev/blog/google-custom-search-api-shutdown/" rel="noopener noreferrer"&gt;Google Custom Search API shutdown: what to use instead&lt;/a&gt;. The comparison of 9 SERP APIs at depth 10 and depth 100 is here: &lt;a href="https://cloro.dev/blog/best-serp-apis/" rel="noopener noreferrer"&gt;Best SERP APIs 2026&lt;/a&gt;. The Bing retirement is covered here: &lt;a href="https://cloro.dev/blog/bing-search-api-key/" rel="noopener noreferrer"&gt;Bing Search API key in 2026&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>google</category>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to benchmark a SERP API: six axes and a cost model that survives n=100</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Fri, 21 Aug 2026 06:04:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/how-to-benchmark-a-serp-api-six-axes-and-a-cost-model-that-survives-n100-3gao</link>
      <guid>https://dev.to/rbatista19/how-to-benchmark-a-serp-api-six-axes-and-a-cost-model-that-survives-n100-3gao</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Score six axes separately and never average them. A provider can be fast and shallow, or excellent on head terms and blind on local packs.&lt;/li&gt;
&lt;li&gt;Compute cost at your own depth. DataForSEO is half cloro's price at n=10 and level with it at n=100 with AI Overview, and SerpApi's gap widens tenfold.&lt;/li&gt;
&lt;li&gt;Score a missing block and an absent block differently, or your trend lines encode the parser's blind spots as real changes in Google.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most SERP API comparisons publish one number: a price per 1,000 calls, or a median latency off a handful of queries. Both are true and both describe the easy case.&lt;/p&gt;

&lt;p&gt;A SERP API is a parser sitting on scraping infrastructure. It can be fast and shallow. It can be cheap at ten results and ruinous at a hundred. It can be excellent on commercial head terms and blind on local packs. Average those into one score and you have hidden every failure mode that will actually bite you.&lt;/p&gt;

&lt;p&gt;Run it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the query set. Fifty queries, evenly split across the four classes. Freeze the list, because you will re-run it against every provider and again in three months.&lt;/li&gt;
&lt;li&gt;Fix the request shape. Same country, language, depth and AIO flag everywhere. Any provider-specific parameter is a variable you can no longer attribute.&lt;/li&gt;
&lt;li&gt;Capture ground truth by hand for ten of them, so a parser miss is distinguishable from an absent block.&lt;/li&gt;
&lt;li&gt;Score the six axes as six columns. Do not average.&lt;/li&gt;
&lt;li&gt;Compute cost at your depth using each vendor's own unit, at both n=10 and the depth you actually need.&lt;/li&gt;
&lt;li&gt;Measure latency under concurrent load, reporting p50 and p95. A sequential median tells you nothing about queue behaviour.&lt;/li&gt;
&lt;li&gt;Ask the two questions the harness cannot answer: how schema changes are versioned, and where the data comes from.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Six axes, scored separately
&lt;/h2&gt;

&lt;p&gt;Never collapse them into a single number. What you weight depends on your workload: a rank tracker running nightly batches cares about cost at depth, an agent doing &lt;a href="https://cloro.dev/blog/ai-grounding-by-engine/" rel="noopener noreferrer"&gt;grounding&lt;/a&gt; cares about latency, a GEO platform cares almost entirely about &lt;a href="https://cloro.dev/ai-overview/" rel="noopener noreferrer"&gt;AI Overview&lt;/a&gt; field fidelity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;What you are actually testing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI Overview fidelity&lt;/td&gt;
&lt;td&gt;Not "does it return AIO" but in what shape. Cited sources with URLs and positions, a text-only summary, or raw HTML you parse yourself. Only the first survives a &lt;a href="https://cloro.dev/blog/how_to_scrape_google_ai_overview/" rel="noopener noreferrer"&gt;Google DOM change&lt;/a&gt; without work on your side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-engine coverage&lt;/td&gt;
&lt;td&gt;Whether each surface is a real endpoint or a wrapper around the same Google call. Coverage claims are cheap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Envelope completeness&lt;/td&gt;
&lt;td&gt;Ads, &lt;a href="https://cloro.dev/blog/people_also_ask_seo/" rel="noopener noreferrer"&gt;People Also Ask&lt;/a&gt;, related searches, knowledge panel, shopping. Ten of the twelve we tested return the full envelope, so this rarely separates the field. It does catch the ones quietly returning organic only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geolocation precision&lt;/td&gt;
&lt;td&gt;Country, city, or UULE-precise. This is where a local SEO tracker gets burned late, because a national average and a metro result diverge on exactly the queries a client checks by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;True cost per 1,000&lt;/td&gt;
&lt;td&gt;At your depth and feature set, not the advertised base rate. Its own section below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency p50 and p95&lt;/td&gt;
&lt;td&gt;The p95 is the one that matters. Dispatch faster than the queue clears and the slowest 5% balloon past the median. A 2-second median hides a 15-second tail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fifty queries, four classes
&lt;/h2&gt;

&lt;p&gt;Same request shape through every provider. Commercial head terms, which stress the envelope. AI-Overview-eligible informational queries, which separate real parsing from a text summary. Local and maps-heavy, which stress geolocation. Long tail, where thin SERPs show up as ambiguous nulls rather than errors.&lt;/p&gt;

&lt;p&gt;The classes matter more than the count. A provider that looks identical to its competitors on head terms can be missing a third of AI Overviews on informational ones, and an aggregate success rate will never show you that.&lt;/p&gt;

&lt;p&gt;One thing worth stealing: &lt;strong&gt;score a missing block and an absent block differently&lt;/strong&gt;. When a provider returns nothing for an AI Overview, you cannot tell whether the SERP had no Overview or the parser missed it. Capture ground truth by hand for ten queries so you can tell the two apart. Skip that and your trend lines encode the parser's blind spots as real changes in Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost model, where most evaluations go wrong
&lt;/h2&gt;

&lt;p&gt;Three things decide your bill, and the headline rate is none of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depth.&lt;/strong&gt; Google removed &lt;code&gt;&amp;amp;num=100&lt;/code&gt; on September 11, 2025, so everyone paginates at 10 results a page. &lt;a href="https://locomotive.agency/blog/google-removes-num100-parameter-what-this-means-for-your-website/" rel="noopener noreferrer"&gt;Locomotive Agency&lt;/a&gt; put the infrastructure cost increase at roughly 10x at top-100 depth. If you need 100 results you are buying ten pages, and how a vendor bills those pages is now the dominant term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Overview enrichment.&lt;/strong&gt; Usually a surcharge, not an included field. DataForSEO gates it behind &lt;a href="https://nextgrowth.ai/dataforseo-api-guide/" rel="noopener noreferrer"&gt;&lt;code&gt;load_async_ai_overview&lt;/code&gt;&lt;/a&gt; at $0.0006 per keyword, roughly doubling the first-page price. Others include it. Credit-pool vendors charge extra credits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Billing unit.&lt;/strong&gt; Per-call, per-successful-result, and per-credit are not comparable until you do the arithmetic at your own depth. A credit pool turns a fixed cost into a variable one. Per-successful-result billing turns your failure rate into a price.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;n=10&lt;/th&gt;
&lt;th&gt;n=100 + AIO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://cloro.dev/serp-api/" rel="noopener noreferrer"&gt;cloro&lt;/a&gt; (Hobby, $0.40/1k credits)&lt;/td&gt;
&lt;td&gt;3 credits at n=10, 23 at n=100 with AIO&lt;/td&gt;
&lt;td&gt;$1.20/1k&lt;/td&gt;
&lt;td&gt;$9.20/1k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataForSEO (Standard queue)&lt;/td&gt;
&lt;td&gt;per call, +$2/1k for AIO on page one&lt;/td&gt;
&lt;td&gt;$0.60/1k&lt;/td&gt;
&lt;td&gt;$9.20/1k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi (Big Data tier)&lt;/td&gt;
&lt;td&gt;per search, one search = one 10-result page (&lt;a href="https://serpapi.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;$9.17/1k&lt;/td&gt;
&lt;td&gt;$91.70/1k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ranking inverts between those two columns. DataForSEO is half cloro's price at n=10 and level with it at n=100 with AIO. SerpApi's gap widens by an order of magnitude, because each batch of ten bills as its own search. Run this before you shortlist. The provider that wins your evaluation at n=10 can be the most expensive thing you run in production, and the &lt;a href="https://cloro.dev/blog/cheapest-serp-api/" rel="noopener noreferrer"&gt;whole category is priced this way here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things the harness cannot tell you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Whether the schema will hold.&lt;/strong&gt; A benchmark captures one moment. It says nothing about whether the vendor renames a field next quarter and breaks your parsers. You cannot measure this from outside, so ask: how are schema changes versioned, and what is the deprecation window? No answer means a maintenance cost you have not priced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the data comes from.&lt;/strong&gt; Google sued SerpApi on December 19, 2025, alleging circumvention of anti-bot protections under DMCA 1201 (&lt;a href="https://blog.google/technology/safety-security/serpapi-lawsuit/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;, &lt;a href="https://searchengineland.com/google-sues-serpapi-466541" rel="noopener noreferrer"&gt;Search Engine Land&lt;/a&gt; has the detail). SerpApi &lt;a href="https://www.vktr.com/ai-platforms/google-sues-serpapi-over-data-scraping/" rel="noopener noreferrer"&gt;moved to dismiss in February 2026&lt;/a&gt;, arguing it returns only what a signed-out user sees. Unresolved as of August 2026. No court has ruled that scraping public results is unlawful and the suit targets circumvention rather than scraping, but provenance is still a procurement question: official feed, scraped results pages, or the vendor's own index. &lt;a href="https://cloro.dev/blog/website-scraping-legal/" rel="noopener noreferrer"&gt;Where the legal line actually sits&lt;/a&gt; is worth reading before you pick on this axis.&lt;/p&gt;

&lt;p&gt;Both official routes are closing anyway. &lt;a href="https://cloro.dev/blog/bing-search-api-key/" rel="noopener noreferrer"&gt;Bing's search APIs retired&lt;/a&gt; in August 2025, and Google's Custom Search JSON API is closed to new customers with a January 1, 2027 shutdown per &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;Google's own docs&lt;/a&gt;. &lt;a href="https://cloro.dev/blog/google-search-api/" rel="noopener noreferrer"&gt;What is actually being retired and what is not&lt;/a&gt; is worth checking, because most write-ups get it wrong.&lt;/p&gt;

&lt;p&gt;If you want the filled-in version, 12 providers scored on all six axes with the cost table at four depths, it is in &lt;a href="https://cloro.dev/blog/best_serp_apis/" rel="noopener noreferrer"&gt;Best SERP APIs 2026&lt;/a&gt;. Check vendor pricing pages yourself before committing. The category has repriced twice since &lt;code&gt;num=100&lt;/code&gt; and will again.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webscraping</category>
      <category>benchmarking</category>
      <category>seo</category>
    </item>
    <item>
      <title>LLM visibility tracking tools: 15 compared, and when to build instead</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:02:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/llm-visibility-tracking-tools-15-compared-and-when-to-build-instead-2cf</link>
      <guid>https://dev.to/rbatista19/llm-visibility-tracking-tools-15-compared-and-when-to-build-instead-2cf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM observability and LLM visibility are unrelated products sold under overlapping search terms. This is about the second one.&lt;/li&gt;
&lt;li&gt;Citation overlap between engines on the same prompt runs under 30%, so a pooled six-engine average can hide being invisible on the engine your category actually uses.&lt;/li&gt;
&lt;li&gt;Build-versus-buy flips around 50,000 queries a month or 10 tracked clients. Below 5,000 a dashboard wins on time-to-value alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, a disambiguation that wastes a lot of people's time. &lt;strong&gt;LLM observability&lt;/strong&gt; is telemetry for models your application calls: tokens, latency, traces, cost. Datadog, LangSmith, Helicone. &lt;strong&gt;LLM visibility&lt;/strong&gt; is what models say about your brand: whether ChatGPT names you, which sources it cites, how you move against competitors. Same search terms, unrelated products. This is about the second one.&lt;/p&gt;

&lt;p&gt;Search Console will not show you any of it, because the surface that decides whether ChatGPT names your brand is the answer generated before anyone clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do LLM visibility tracking tools actually measure?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mention rate&lt;/strong&gt; is the share of sampled answers naming your brand at all. Everything else is a cut of this number. A platform that shows a composite "visibility score" without the mention count underneath is asking you to trust a black box, and that score is not comparable to anyone else's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share of voice&lt;/strong&gt; is your mention rate against the competitors named on the same prompts. It only means something if you control the competitor list, so check whether you define it or the platform infers it from your category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Citation rate and position&lt;/strong&gt; is whether the answer links your domain and where you sit in the source list. Mention and citation move independently. Your brand can be named in an answer that cites someone else entirely, and a tool that collapses the two hides the more actionable half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentiment and accuracy&lt;/strong&gt; is whether the answer describes your product correctly. Newest of the four, least standardised across vendors, so it deserves the most scrutiny in a demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-engine, not pooled.&lt;/strong&gt; In our own cross-engine monitoring, citation overlap between engines on the same prompt runs &lt;strong&gt;under 30%&lt;/strong&gt;. A single average across six engines can hide being invisible on the one that matters to your category.&lt;/p&gt;

&lt;p&gt;Two numbers for scale before the tools. &lt;a href="https://ahrefs.com/blog/ai-overviews-reduce-clicks-update/" rel="noopener noreferrer"&gt;Ahrefs&lt;/a&gt; measured position-1 organic CTR falling roughly 58% on queries that trigger an AI Overview, and per a &lt;a href="https://www.globenewswire.com/news-release/2026/04/07/3269307/0/en/Only-14-of-Marketers-Track-AI-Search-Citations-Even-as-89-of-Brands-are-Already-Appearing-in-them.html" rel="noopener noreferrer"&gt;Globe Newswire industry report&lt;/a&gt; only 14% of marketers track AI citations while 89% of brands already appear in AI answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Notable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;$599/mo&lt;/td&gt;
&lt;td&gt;B2B SaaS doing GEO&lt;/td&gt;
&lt;td&gt;Prompt intelligence, Action Center recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profound&lt;/td&gt;
&lt;td&gt;$5,000/mo&lt;/td&gt;
&lt;td&gt;Enterprise compliance&lt;/td&gt;
&lt;td&gt;10+ engines, sentiment, ISO-certified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI&lt;/td&gt;
&lt;td&gt;$2,000/mo&lt;/td&gt;
&lt;td&gt;Competitor depth&lt;/td&gt;
&lt;td&gt;Real-time citations, clickstream correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brandlight&lt;/td&gt;
&lt;td&gt;$2,000/mo&lt;/td&gt;
&lt;td&gt;Accuracy alerting&lt;/td&gt;
&lt;td&gt;Brand accuracy monitoring, health scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DemandSphere&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Layering AI onto an existing search programme&lt;/td&gt;
&lt;td&gt;AI citations on the same footing as SERP share of voice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightwatch&lt;/td&gt;
&lt;td&gt;Contact&lt;/td&gt;
&lt;td&gt;Agencies already rank tracking&lt;/td&gt;
&lt;td&gt;Citation Intelligence, unlimited seats, white-label on all plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SE Ranking&lt;/td&gt;
&lt;td&gt;$119/mo&lt;/td&gt;
&lt;td&gt;SMBs on a budget&lt;/td&gt;
&lt;td&gt;AIO and AI Mode trackers, competitor citation gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semrush AI Toolkit&lt;/td&gt;
&lt;td&gt;$99/mo add-on&lt;/td&gt;
&lt;td&gt;Existing Semrush users&lt;/td&gt;
&lt;td&gt;25 prompts, up to 9 competitors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AthenaHQ&lt;/td&gt;
&lt;td&gt;$199/mo&lt;/td&gt;
&lt;td&gt;Growing SaaS and agencies&lt;/td&gt;
&lt;td&gt;Clean dashboard, share of voice, weekly trends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OtterlyAI&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;Monitoring plus optimisation&lt;/td&gt;
&lt;td&gt;ChatGPT Ads and Shopping tracking, GEO audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scrunch&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;AI-native reporting&lt;/td&gt;
&lt;td&gt;Answer-first reporting, competitor benchmarking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evertune&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Prompt-level analytics&lt;/td&gt;
&lt;td&gt;Share of voice broken down per prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aiclicks.io&lt;/td&gt;
&lt;td&gt;$39/mo promo&lt;/td&gt;
&lt;td&gt;Tracking plus content in one place&lt;/td&gt;
&lt;td&gt;Built-in writer, GSC integration, prompt database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoritas Visibility Explorer&lt;/td&gt;
&lt;td&gt;$99/mo&lt;/td&gt;
&lt;td&gt;Multi-client agencies&lt;/td&gt;
&lt;td&gt;Daily difference reports, 30+ markets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ahrefs Brand Radar&lt;/td&gt;
&lt;td&gt;$129/mo&lt;/td&gt;
&lt;td&gt;Existing Ahrefs users&lt;/td&gt;
&lt;td&gt;243M+ monthly prompts, joined to Ahrefs SEO data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Metric sets cluster by product focus rather than price. Evertune and AthenaHQ are built around share of voice. Peec AI and Nightwatch lead on citation-level detail. Profound and Brandlight are the two treating sentiment and brand accuracy as first-class rather than as add-ons. DemandSphere is the one that puts AI citations on the same foundation as the search-visibility number you already report to the business.&lt;/p&gt;

&lt;p&gt;The gap that shows up in testing is engine coverage versus claimed engine coverage. Most cover ChatGPT, Perplexity, Gemini and AI Overview. &lt;a href="https://cloro.dev/copilot/" rel="noopener noreferrer"&gt;Copilot&lt;/a&gt; and AI Mode are spottier, and depth varies even where the logo appears on the pricing page. Ask whether each engine is queried through an official API or browser automation, because API-only coverage structurally misses AI Overview and Perplexity's web-search surface, which are the citation-heavy ones.&lt;/p&gt;

&lt;p&gt;Worth knowing before you read anyone's citation-gap report: per &lt;a href="https://otterly.ai/blog/the-ai-citations-report-2026/" rel="noopener noreferrer"&gt;Otterly's analysis of over a million citations&lt;/a&gt;, 73% of sites carry technical barriers (robots.txt blocks, CDN rules, JS-only content) that stop AI crawlers reaching the page at all. Some "citation gap" findings are crawler-access findings wearing a different hat.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we tested
&lt;/h3&gt;

&lt;p&gt;One B2B SaaS brand and one consumer-product brand, 25 commercial-investigation queries each, four weeks, spanning comparison, definitional, recommendation and how-to intents. We captured ground truth by hand across ChatGPT with web search, Perplexity, the Gemini app, Copilot, AI Overviews and AI Mode, then compared what each tool reported for the same queries on the same days. Scored on engine coverage, citation fidelity, update frequency, reporting depth, pricing fairness, and methodology transparency.&lt;/p&gt;

&lt;p&gt;This roundup is published by cloro, a data provider rather than a dashboard, so none of the 15 tools above is a competitor. cloro appears once below, in the infrastructure section, and is not scored against them.&lt;/p&gt;

&lt;p&gt;A roundup is editorial judgement on one date. The &lt;a href="https://cloro.dev/ai-visibility/ai-visibility-platforms/" rel="noopener noreferrer"&gt;AI visibility platforms leaderboard&lt;/a&gt; is the standing measurement beside it, re-scored weekly and free to cite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API question, which most comparisons get wrong
&lt;/h2&gt;

&lt;p&gt;Vendors use "API" for three different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled export.&lt;/strong&gt; CSV drops, Sheets, sometimes a Looker Studio connector. Fine for reporting, useless for a product feature. This is the most common thing sold as integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read API over their stored data.&lt;/strong&gt; Query the vendor's results for your tracked prompts. Good for pulling their numbers into your BI stack, still bounded by the prompts, engines and cadence your plan allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw data API.&lt;/strong&gt; Submit any prompt to any surface, get the parsed response. Different product category, not a dashboard feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things a comparison table cannot hold. API availability moves between pricing tiers often enough that any published list is stale within a quarter. And an API on a dashboard product is normally rate-limited against the prompt allowance you already bought, so "has an API" does not mean "can back a product feature."&lt;/p&gt;

&lt;h2&gt;
  
  
  When building your own starts to make sense
&lt;/h2&gt;

&lt;p&gt;Three bands, from watching teams do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Under ~5,000 queries a month.&lt;/strong&gt; Buy a dashboard. Anything you build will take longer to stand up than the value it returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over ~50,000 queries a month, or 10+ tracked clients.&lt;/strong&gt; Per-seat dashboard pricing crosses per-call API pricing, and an in-house build pays back in two to four months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In between.&lt;/strong&gt; It depends entirely on whether you already have BI and ops infrastructure to host the output. If the answer is a CSV landing in a warehouse, you are paying for a UI layer and throwing it away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The builds themselves are small. Three patterns cover most of it: nightly scheduler into Postgres into Metabase with Slack alerts; per-client query lists with per-tenant keys and white-label reports; or a daily batch into BigQuery joined against GA4, Search Console and CRM data. Each is one to two weeks of engineering on top of a managed data layer.&lt;/p&gt;

&lt;p&gt;What is not small is the parsing layer. Scraping it yourself runs $500-2,000 a month in proxies and browser infrastructure plus two to three months of build, and the hard part is not the scraping. It is that a citation parser working cleanly in January can miss 30% of them by April after a UI change, silently. Official model APIs avoid that and cannot help either, because they do not expose citation data the way a live search surface does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloro.dev/use-cases/ai-visibility-tracking/" rel="noopener noreferrer"&gt;cloro&lt;/a&gt; is one such layer: a single endpoint across ChatGPT, Perplexity, Gemini, Copilot, AI Overview, AI Mode, Google Search and Google News, returning parsed source URLs with position and label, plus &lt;a href="https://cloro.dev/blog/query-fan-out/" rel="noopener noreferrer"&gt;query fan-out&lt;/a&gt; terms, as JSON rather than screenshots. Credits are deducted only on successful extraction. Hobby is $100/month for 250,000 credits ($0.40 per 1,000), scaling to $0.34 at the top tier, with 500 free credits to test and &lt;a href="https://cloro.dev/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt; rather than an onboarding call. Async is the right call for nightly batches; sync costs two extra credits and returns inline.&lt;/p&gt;

&lt;p&gt;It is not a dashboard, and if what you want is a dashboard then one of the 15 above is the better purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to pressure-test in a demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engine coverage method.&lt;/strong&gt; Official API or browser automation, per engine. Vagueness here is the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citation format.&lt;/strong&gt; Structured queryable URLs, or screenshots. This decides whether you can compute anything yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query control.&lt;/strong&gt; Can you import prompts from Search Console and define your own, or does it generate them from your domain?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost at your real cadence.&lt;/strong&gt; Affordable at 50 queries a week is a different product at 500. Most teams find they need weekly rather than daily, which changes the maths a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the ChatGPT surface specifically rather than all six, &lt;a href="https://cloro.dev/blog/chatgpt-visibility-tracker/" rel="noopener noreferrer"&gt;monitoring ChatGPT mentions&lt;/a&gt; is covered end to end separately.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>api</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Best ChatGPT scraper in 2026: what breaks, and what 8 tools cost</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:57:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/best-chatgpt-scraper-in-2026-what-breaks-and-what-8-tools-cost-4im6</link>
      <guid>https://dev.to/rbatista19/best-chatgpt-scraper-in-2026-what-breaks-and-what-8-tools-cost-4im6</guid>
      <description>&lt;p&gt;The best ChatGPT scraper for brand monitoring is a managed API that returns parsed citations, and cloro is the only one of the eight below that ships that parsing rather than handing back HTML. For general-purpose scraping at volume, Bright Data's Scraping Browser is the stronger pick. For zero budget with engineering capacity, it is Playwright and a proxy bill. Which of those three you are depends on the table further down, not on the verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The official OpenAI API cannot answer brand-monitoring questions, because citations, shopping cards and the search-versus-memory decision only exist in the web UI.&lt;/li&gt;
&lt;li&gt;At 1,000 queries a day, DIY Playwright runs $980-2,140 a month once proxies, CAPTCHA and 8-15 engineer hours are counted, against $100-300 for a managed API.&lt;/li&gt;
&lt;li&gt;Selector maintenance is the hidden line item. OpenAI's dynamic CSS class names change roughly weekly, and the failure is silent rather than loud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two ChatGPTs. The API developers build on, and the web interface &lt;a href="https://almcorp.com/blog/chatgpt-900-million-weekly-active-users/" rel="noopener noreferrer"&gt;900 million weekly users&lt;/a&gt; actually see. Only the second one browses the web, cites sources, renders &lt;a href="https://cloro.dev/blog/chatgpt-shopping/" rel="noopener noreferrer"&gt;shopping cards&lt;/a&gt;, and decides per-query whether to search or answer from memory.&lt;/p&gt;

&lt;p&gt;If you want to know whether ChatGPT recommends your product or names your competitor instead, the API structurally cannot tell you. You have to read the rendered UI, which means scraping one of the better-defended properties on the public web.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes chatgpt.com hard to scrape?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cloudflare fingerprints your TLS handshake
&lt;/h3&gt;

&lt;p&gt;Per &lt;a href="https://scrapfly.io/blog/posts/how-to-bypass-cloudflare-anti-scraping" rel="noopener noreferrer"&gt;Scrapfly's analysis&lt;/a&gt;, the 2026 stack starts with JA4 TLS fingerprinting. Default &lt;code&gt;requests&lt;/code&gt; or &lt;code&gt;urllib&lt;/code&gt; fail on the handshake, before any of your logic runs. Then behavioral profiling of timing and movement, JavaScript challenges a static client cannot execute, and Turnstile CAPTCHAs on anything that smells automated.&lt;/p&gt;

&lt;p&gt;Datacenter IPs are the fastest way to get nowhere. &lt;a href="https://www.proxies.sx/blog/scrape-chatgpt-search-results-mobile-proxies" rel="noopener noreferrer"&gt;proxies.sx testing&lt;/a&gt; has real mobile IPs surviving 50-100+ queries while datacenter ranges get blocked in the first few requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The response streams
&lt;/h3&gt;

&lt;p&gt;ChatGPT does not return HTML. It streams tokens over Server-Sent Events, so you keep the connection alive, parse &lt;code&gt;event: message&lt;/code&gt; frames as they land, assemble the answer, and detect the end signal. Static HTTP clients see nothing. Headless browsers handle it transparently and charge you in compute and latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Class names change between deploys
&lt;/h3&gt;

&lt;p&gt;OpenAI's React build emits dynamic classes like &lt;code&gt;._a4b3f&lt;/code&gt;. Anything selecting on them breaks roughly weekly. Semantic selectors (ARIA labels, roles, text content) survive longer, and you still want fallback chains. This is the line item every DIY estimate underprices.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Login, 2FA, and session persistence
&lt;/h3&gt;

&lt;p&gt;Most useful workflows need an account, most accounts have 2FA, and the login flow sits behind the same Cloudflare checks. Three realistic paths: export cookies after a manual login and refresh on a schedule, pay someone to hold the session, or run headful Playwright with stealth plugins and accept the breakage.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Proxy economics
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://databay.com/blog/how-much-do-residential-proxies-cost" rel="noopener noreferrer"&gt;Databay's 2026 breakdown&lt;/a&gt; puts residential proxies at $3-15/GB, with the mobile IPs ChatGPT tolerates at the top of that range. At 1,000 queries a day you burn roughly 3-8 GB a month, plus CAPTCHA credits at $1-3 per 1,000 challenges. That line item alone often exceeds a managed subscription. There is a fuller treatment of &lt;a href="https://cloro.dev/blog/proxies-for-serp-scraping/" rel="noopener noreferrer"&gt;proxy selection for SERP work&lt;/a&gt; elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it solves&lt;/th&gt;
&lt;th&gt;What you still write&lt;/th&gt;
&lt;th&gt;Starting price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cloro.dev/chatgpt/" rel="noopener noreferrer"&gt;cloro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Access, SSE assembly, &lt;a href="https://cloro.dev/chatgpt/sources/" rel="noopener noreferrer"&gt;citation parsing&lt;/a&gt;, fan-out, auth&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;$100/mo, 500 free credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify&lt;/td&gt;
&lt;td&gt;Access and compute, via marketplace actors&lt;/td&gt;
&lt;td&gt;Cookie refresh; parsing varies by actor&lt;/td&gt;
&lt;td&gt;$49/mo + compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bright Data&lt;/td&gt;
&lt;td&gt;Access at any scale, 72M+ IPs, CAPTCHA&lt;/td&gt;
&lt;td&gt;SSE assembly and all parsing&lt;/td&gt;
&lt;td&gt;~$1.50/1k at volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browserbase&lt;/td&gt;
&lt;td&gt;Persistent sessions, stealth, agent ergonomics&lt;/td&gt;
&lt;td&gt;SSE assembly, parsing, selectors&lt;/td&gt;
&lt;td&gt;$50/mo, free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browserless&lt;/td&gt;
&lt;td&gt;Headless Chrome, self-hostable&lt;/td&gt;
&lt;td&gt;SSE assembly, parsing, selectors, some evasion&lt;/td&gt;
&lt;td&gt;$50/mo, free Docker image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScrapingBee&lt;/td&gt;
&lt;td&gt;Cloudflare bypass with a clean API&lt;/td&gt;
&lt;td&gt;SSE assembly, parsing, selectors&lt;/td&gt;
&lt;td&gt;$49/mo, 100k credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZenRows&lt;/td&gt;
&lt;td&gt;Cloudflare bypass, cheaper tier&lt;/td&gt;
&lt;td&gt;SSE assembly, parsing, selectors&lt;/td&gt;
&lt;td&gt;$69/mo, 250k credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Nothing. It is the toolkit&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;td&gt;Free, plus your time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This roundup is published by cloro, which is one of the eight tools in it. The prices and capabilities attributed to the other seven come from their own documentation, linked where they are not obvious.&lt;/p&gt;

&lt;p&gt;The split that matters is the third column. Every tool except the managed APIs leaves you owning SSE assembly and selector maintenance, which is the part that fails weekly rather than the part that fails loudly.&lt;/p&gt;

&lt;p&gt;A few notes the table cannot hold. Apify's actors are community-maintained more often than not, so reliability tracks whoever still cares about that actor. Bright Data's Scraping Browser is genuinely hard for Cloudflare to block and overkill below serious volume. Browserbase is built for agents and its session persistence solves the auth problem cleanly, which is a real advantage here. ScrapingBee's team &lt;a href="https://www.scrapingbee.com/blog/best-chatgpt-scraper-tools/" rel="noopener noreferrer"&gt;published their own ChatGPT scraper roundup&lt;/a&gt;, which is a decent signal they understand the target.&lt;/p&gt;

&lt;p&gt;Out of scope on purpose: the official OpenAI API (different surface), wrappers like LiteLLM and OpenRouter (they wrap the API, not the UI), Cloudflare-bypass libraries like Camoufox and FlareSolverr (they solve the WAF and leave you the rest), and answer APIs like Tavily and Exa (synthesized answers, not the rendered UI).&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs at 1,000 queries a day
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Subscription&lt;/th&gt;
&lt;th&gt;Proxies/credits&lt;/th&gt;
&lt;th&gt;CAPTCHA&lt;/th&gt;
&lt;th&gt;Eng hrs/mo&lt;/th&gt;
&lt;th&gt;Total $/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;$30-300&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$30-300&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify (official actor)&lt;/td&gt;
&lt;td&gt;$49&lt;/td&gt;
&lt;td&gt;$30-80&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$280-410&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScrapingBee&lt;/td&gt;
&lt;td&gt;$49-249&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$449-849&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZenRows&lt;/td&gt;
&lt;td&gt;$69-249&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$469-849&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browserless&lt;/td&gt;
&lt;td&gt;$50-100&lt;/td&gt;
&lt;td&gt;$100-300&lt;/td&gt;
&lt;td&gt;$30-90&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$580-890&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bright Data&lt;/td&gt;
&lt;td&gt;pay-as-you-go&lt;/td&gt;
&lt;td&gt;$200-500&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$600-900&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browserbase&lt;/td&gt;
&lt;td&gt;$50-200&lt;/td&gt;
&lt;td&gt;$50-150&lt;/td&gt;
&lt;td&gt;$30-90&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$730-1,040&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright (DIY)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$150-450&lt;/td&gt;
&lt;td&gt;$30-90&lt;/td&gt;
&lt;td&gt;8-15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$980-2,140&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Assumptions, because a cost table without them is decoration: engineer time at $100/hour fully loaded, CAPTCHA solver at $2 per 1,000 challenges firing on 5% of requests, mobile residential proxies at $10/GB, requests averaging 4-8 KB.&lt;/p&gt;

&lt;p&gt;Two things fall out. The DIY gap widens with volume, because proxy bandwidth scales linearly while per-call rates flatten or improve with tiers. And the browser-infrastructure tier sits in an awkward middle: cheaper than Bright Data, more expensive than a managed API once you price the parsing work.&lt;/p&gt;

&lt;p&gt;Change the engineer-hours assumption and the ranking moves. If your team already runs Playwright infrastructure and selector patches cost you an hour a month rather than ten, DIY wins outright. That is a real case, not a strawman.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already on Apify, ChatGPT is one target of several.&lt;/strong&gt; Use the official actor rather than a community one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume is the binding constraint.&lt;/strong&gt; Bright Data, and budget for writing the parsing layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building an agent that needs a live session.&lt;/strong&gt; Browserbase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want Cloudflare handled and nothing else.&lt;/strong&gt; ScrapingBee, or ZenRows if the budget is tight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero budget, real engineering capacity.&lt;/strong&gt; Playwright with stealth plugins and mobile proxies. Plan for 8-15 hours a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want parsed citations and fan-out with no maintenance.&lt;/strong&gt; &lt;a href="https://cloro.dev/chatgpt/" rel="noopener noreferrer"&gt;cloro&lt;/a&gt; is built for that case and is a monitoring tool rather than a general scraper. If you need arbitrary browser automation, buy browser infrastructure instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One legal note, since it comes up in every thread: scraping your own session or publicly visible content is generally fine, while bypassing authentication or pulling other people's data is not. &lt;a href="https://cloro.dev/blog/website-scraping-legal/" rel="noopener noreferrer"&gt;Where the line sits&lt;/a&gt; is covered properly elsewhere.&lt;/p&gt;

&lt;p&gt;If you are doing this for brand monitoring rather than data collection, the &lt;a href="https://cloro.dev/blog/llm_visibility_tracking_tools/" rel="noopener noreferrer"&gt;tools built for that job&lt;/a&gt; are a different category and probably the better starting point.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>webscraping</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>How to scrape Google AI Mode: citation pills + place cards</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Fri, 14 Aug 2026 05:49:00 +0000</pubDate>
      <link>https://dev.to/rbatista19/how-to-scrape-google-ai-mode-citation-pills-place-cards-2c7</link>
      <guid>https://dev.to/rbatista19/how-to-scrape-google-ai-mode-citation-pills-place-cards-2c7</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI Mode and AI Overview cite the same URLs only 13.7% of the time, so scraping one tells you nothing reliable about the other.&lt;/li&gt;
&lt;li&gt;Source URLs live in HTML comments keyed by UUID, not in anchor tags, so a scraper reading &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; returns an empty citation list rather than an error.&lt;/li&gt;
&lt;li&gt;Strip &lt;code&gt;#:~:text&lt;/code&gt; scroll-to-text fragments before counting, or one article reads as several distinct URLs and inflates your citation numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google AI Mode does not put its sources in anchor tags. It renders each one as a button, and hides the actual URL in an HTML comment keyed by UUID. A scraper that reads &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; finds nothing. That one design choice is most of the work in this post.&lt;/p&gt;

&lt;p&gt;The rest is that nothing is in the initial HTML at all. The shell loads, then the answer streams in over a separate call to &lt;code&gt;/async/folwr&lt;/code&gt;. Parse on page load and you get an empty container.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the URL with &lt;code&gt;udm=50&lt;/code&gt; and &lt;code&gt;aep=11&lt;/code&gt; to force the interface.&lt;/li&gt;
&lt;li&gt;Register a network interceptor on &lt;code&gt;https://www.google.com/async/folwr&lt;/code&gt; before navigating.&lt;/li&gt;
&lt;li&gt;Navigate, and treat a non-200 as a CAPTCHA rather than a dead page.&lt;/li&gt;
&lt;li&gt;Poll until the intercepted response lands, with a hard timeout.&lt;/li&gt;
&lt;li&gt;Read the answer text off &lt;code&gt;data-session-thread-id&lt;/code&gt;, not off a snapshot.&lt;/li&gt;
&lt;li&gt;Collect the citation buttons, then rejoin them to their HTML-comment metadata by UUID.&lt;/li&gt;
&lt;li&gt;Rewrite the pills into anchors before converting anything to markdown.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 6 and 7 are where the work is. The rest is plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with this surface specifically
&lt;/h2&gt;

&lt;p&gt;AI Mode is not AI Overview with a different skin. Per &lt;a href="https://almcorp.com/blog/google-ai-mode-cites-itself-organic-links-seo-2026/" rel="noopener noreferrer"&gt;ALM Corp's analysis of 1.3 million AI Mode citations&lt;/a&gt;, &lt;strong&gt;the two cite the same URLs only 13.7% of the time&lt;/strong&gt; on semantically similar queries, and google.com self-cites in &lt;strong&gt;17.42%&lt;/strong&gt; of AI Mode answers, more than YouTube, Facebook, Reddit, Amazon, Indeed and Zillow combined. Scraping one tells you nothing reliable about the other.&lt;/p&gt;

&lt;p&gt;It is also where the comparison queries go. &lt;a href="https://almcorp.com/blog/google-ai-overviews-organic-ctr-2026/" rel="noopener noreferrer"&gt;Seer Interactive's 2026 trigger data&lt;/a&gt; puts AI surfaces on 95.4% of comparison queries and 85.9% of question-format ones, and AI Mode passed 75 million daily active users by late 2025 per &lt;a href="https://blog.google/company-news/inside-google/message-ceo/alphabet-earnings-q3-2025/" rel="noopener noreferrer"&gt;Alphabet's Q3 2025 earnings call&lt;/a&gt;. The &lt;a href="https://cloro.dev/blog/query-fan-out/" rel="noopener noreferrer"&gt;query fan-out&lt;/a&gt; behind it, where Google splits your question into subtopics and runs many searches at once, is why one answer stitches together so many cited pages.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Citation style&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI Mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;udm=50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pills, metadata in HTML comments&lt;/td&gt;
&lt;td&gt;High: async streaming, drifting selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cloro.dev/blog/how_to_scrape_google_ai_overview/" rel="noopener noreferrer"&gt;AI Overview&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Organic query, varies by class&lt;/td&gt;
&lt;td&gt;Inline source list above the SERP&lt;/td&gt;
&lt;td&gt;Medium: single fetch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cloro.dev/blog/scrape-gemini/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;gemini.google.com&lt;/td&gt;
&lt;td&gt;Grounded sources panel&lt;/td&gt;
&lt;td&gt;High: separate auth domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cloro.dev/blog/scrape-chatgpt/" rel="noopener noreferrer"&gt;ChatGPT Search&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;chatgpt.com with search on&lt;/td&gt;
&lt;td&gt;Numbered citations&lt;/td&gt;
&lt;td&gt;Highest: SSE plus Cloudflare&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Force the interface
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;udm=50&lt;/code&gt; is the switch. &lt;code&gt;aep=11&lt;/code&gt; goes with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;search_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_url_with_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.google.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;udm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# AI Mode
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;google_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;google_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wait for the stream, then parse
&lt;/h2&gt;

&lt;p&gt;We drive &lt;a href="https://playwright.dev/python/docs/network" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; and capture the async call, then poll until it lands. Sixty seconds, then fail loudly. A hard timeout beats a silent partial parse, because a half-streamed answer looks like a real answer with fewer citations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;page_interceptor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PlaywrightInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_not_block_resources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page_interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_capture_urls&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.google.com/async/folwr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page_interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup_page_interceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                      &lt;span class="c1"&gt;# 500ms intervals, 60s cap
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured_responses&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never received AI Mode response after 60 seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer text hangs off &lt;code&gt;data-session-thread-id&lt;/code&gt;. Target that and read its parent, because AI Mode rewrites its own container several times while streaming and a &lt;a href="https://playwright.dev/python/docs/locators" rel="noopener noreferrer"&gt;locator&lt;/a&gt; survives that where a snapshot does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;thread_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-session-thread-id]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;thread_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text_content&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The citation pills
&lt;/h2&gt;

&lt;p&gt;Here is the shape you are working against. A UUID ties the visible button to a comment block carrying the real URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--Sv6Kpe[["uuid-12345",["label","description"],["https://example.com","source2"]]]--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-icl-uuid=&lt;/span&gt;&lt;span class="s"&gt;"uuid-12345"&lt;/span&gt; &lt;span class="na"&gt;data-amic=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;[1]&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: find the buttons, read their UUIDs, regex the comments back out of the page HTML, and filter Google's own domains out of the URL list. One pill can carry several sources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_aimode_citation_pills&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LinkData&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;citation_pills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LinkData&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;pill_locators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;button[data-icl-uuid][data-amic=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unescape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pill_locators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;pill_button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pill_locators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pill_button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_visible&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pill_button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-icl-uuid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!--Sv6Kpe\[\[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.*?]]--&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;current_pill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LinkData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;desc_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s*,\s*\[\s*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s*,\s*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;(https://[^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;
                            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstatic.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypted-tbn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt;
                &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#:~:text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;u003d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;u0026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;current_pill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LinkData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_pill&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_pill&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;desc_match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;desc_match&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_pill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;citation_pills&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_pill&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;citation_pills&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;#:~:text&lt;/code&gt; strip matters more than it looks. Google appends scroll-to-text fragments, so the same article comes back as several distinct URLs and your citation counts inflate quietly.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cloro.dev/ai-mode/sources/" rel="noopener noreferrer"&gt;sources panel&lt;/a&gt; needs its own selector per layout. Web-results pages expose it one way, the dialog another. Playwright's &lt;a href="https://playwright.dev/python/docs/actionability" rel="noopener noreferrer"&gt;actionability checks&lt;/a&gt; handle the waiting, so log and continue on timeout rather than killing the run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sources_selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[data-container-id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rhs-col&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;] [role=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dialog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;] a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_web_results_page&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a.ZbQNgf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Converting to markdown without losing the sources
&lt;/h2&gt;

&lt;p&gt;Hand the raw HTML to &lt;a href="https://pypi.org/project/html2text/" rel="noopener noreferrer"&gt;html2text&lt;/a&gt; and every pill becomes a dead button. Rewrite the pills into real anchors first, using the UUID map you already built, then convert.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-icl-uuid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-amic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}):&lt;/span&gt;
    &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-icl-uuid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;link_data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;citation_pills&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])):&lt;/span&gt;
        &lt;span class="n"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;link_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html2text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTML2Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignore_links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body_width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What breaks at scale
&lt;/h2&gt;

&lt;p&gt;A non-200 is usually a &lt;a href="https://cloro.dev/blog/solve-captchas/" rel="noopener noreferrer"&gt;CAPTCHA&lt;/a&gt;, not a dead page, so solve before you give up. One blocked request should not tank a batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_http_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;solve_captcha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_interceptor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (probably captcha)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real running cost is selector drift. AI Mode's DOM changes without notice, and a scraper that worked last month returns empty rather than throwing. Alert on success rate, not on exceptions. A sudden drop is a layout change, and you will find it weeks late if you are only watching for errors. You will also want a &lt;a href="https://cloro.dev/blog/proxies-for-serp-scraping/" rel="noopener noreferrer"&gt;proxy pool&lt;/a&gt; and fingerprint rotation before this runs at any volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or skip it
&lt;/h2&gt;

&lt;p&gt;cloro maintains this pipeline as a managed endpoint, which is the disclosure to weigh this section against. One POST, parsed text and sources back, &lt;a href="https://cloro.dev/blog/serp-api-reliability-at-scale/" rel="noopener noreferrer"&gt;P50 under 8 seconds&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.cloro.dev/v1/monitor/aimode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer sk_live_your_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What do you know about Tesla&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s latest updates?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; citations, fan-out: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;searchQueries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built in-house, this runs $5,000 to $10,000 a month once you count engineering time, browser instances and proxies. Below a few thousand queries a month that maths does not favour us, and you should build it. Above that it does, and &lt;a href="https://cloro.dev/ai-mode/" rel="noopener noreferrer"&gt;cloro's AI Mode endpoint&lt;/a&gt; absorbs the selector drift.&lt;/p&gt;

&lt;p&gt;If you want the dashboard rather than the pipeline, &lt;a href="https://cloro.dev/blog/llm_visibility_tracking_tools/" rel="noopener noreferrer"&gt;the tools that track this surface&lt;/a&gt; are compared separately.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>dataextraction</category>
      <category>aimode</category>
    </item>
    <item>
      <title>SERP API reliability at scale: uptime, latency, concurrency</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Wed, 12 Aug 2026 05:46:10 +0000</pubDate>
      <link>https://dev.to/rbatista19/serp-api-reliability-at-scale-uptime-latency-concurrency-op0</link>
      <guid>https://dev.to/rbatista19/serp-api-reliability-at-scale-uptime-latency-concurrency-op0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two of the four headline reliability numbers in this category are not uptime at all: DataForSEO publishes 99.95% with no credit terms, and Bright Data's 99.99% is a success rate from a benchmark with a disclosed vested interest.&lt;/li&gt;
&lt;li&gt;At 10,000 requests a day the P95 decides your architecture, not the median. Bright Data measures 2.61s P50 against 4.92s P95, and a 2-second median can hide a 15-second tail once queuing starts.&lt;/li&gt;
&lt;li&gt;Google's &lt;code&gt;&amp;amp;num=100&lt;/code&gt; removal plus DataForSEO's depth-based pricing took the Live tier from $2.00 to $17.50 per 1,000 at 100 results with AI Overview, a ninefold jump from a change you did not make.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below a few hundred requests a day, every SERP API looks fine. The failure modes only show up somewhere past 10,000: queue latency, rate-limit rejections, concurrency ceilings. By then you have built on top of one.&lt;/p&gt;

&lt;p&gt;Three things decide it, and only one of them appears on a pricing page.&lt;/p&gt;

&lt;p&gt;This comparison is published by cloro, which is one of the four providers in it. Every figure attributed to another vendor is sourced below; the cloro rows are self-reported and should be read as such.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a SERP API uptime SLA actually commit to?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Uptime SLA&lt;/th&gt;
&lt;th&gt;Median response&lt;/th&gt;
&lt;th&gt;Rate limits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;99.9% with service credits&lt;/td&gt;
&lt;td&gt;Sub-second (async queue)&lt;/td&gt;
&lt;td&gt;Parallel lanes, no hard cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;None published&lt;/td&gt;
&lt;td&gt;&lt;a href="https://scrape.do/blog/serpapi-alternatives/" rel="noopener noreferrer"&gt;1.96s&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Hard monthly cap, request queuing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataForSEO&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://nextgrowth.ai/dataforseo-api-guide/" rel="noopener noreferrer"&gt;99.95%&lt;/a&gt;, no credit terms stated&lt;/td&gt;
&lt;td&gt;Live fastest; ~5-min Standard queue&lt;/td&gt;
&lt;td&gt;High default RPS, async batching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bright Data&lt;/td&gt;
&lt;td&gt;None published&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hackernoon.com/serp-benchmarks-success-rates-and-latency-at-scale" rel="noopener noreferrer"&gt;2.61s standard, 0.89s premium&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Configurable 5-20+, no documented ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those four numbers are not what they look like.&lt;/p&gt;

&lt;p&gt;DataForSEO's 99.95% is higher than ours and the documentation states no credit or penalty terms. That makes it a target rather than a remedy. A published SLA without credits tells you what a provider aims for, not what you recover when they miss.&lt;/p&gt;

&lt;p&gt;Bright Data's widely quoted 99.99% is a &lt;strong&gt;success rate&lt;/strong&gt;, not uptime, and it comes from a benchmark whose author &lt;a href="https://hackernoon.com/serp-benchmarks-success-rates-and-latency-at-scale" rel="noopener noreferrer"&gt;disclosed a vested interest&lt;/a&gt;. Success rate measures request completion. Uptime measures whether the endpoint answers at all. They are different failures and only one of them wakes you up.&lt;/p&gt;

&lt;p&gt;So when you compare this column, check three things: is it uptime or success rate, is it contractual or measured, and does anything happen when they miss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency: the median is the wrong number
&lt;/h2&gt;

&lt;p&gt;Almost every published latency figure is measured sequentially, which is the easy case. What decides production behaviour is what happens when you dispatch faster than the provider's queue clears.&lt;/p&gt;

&lt;p&gt;The best public numbers come from that same 30-day benchmark of nine providers: Bright Data's standard P50 at 2.61s against a &lt;strong&gt;P95 of 4.92s&lt;/strong&gt;, in a field ranging from Serper at 2.23s to DataForSEO at 4.54s. Premium routing takes the P50 to 0.89s, and that is the vendor-adjacent figure, so discount it.&lt;/p&gt;

&lt;p&gt;At 10,000 daily requests the P95 is the number that matters. A 2-second median hides a 15-second tail once queuing kicks in. An overnight rank tracker can absorb that tail. Brand monitoring cannot, because a five-minute delay on an incident query defeats the point.&lt;/p&gt;

&lt;p&gt;DataForSEO's Standard queue is the clearest case: one tester measured async callbacks averaging &lt;a href="https://nextgrowth.ai/dataforseo-api-guide/" rel="noopener noreferrer"&gt;12 seconds for 100 keywords&lt;/a&gt;, stretching toward five minutes at production volume. That is fine for scheduled work and useless for anything real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency is an architecture choice, not a setting
&lt;/h2&gt;

&lt;p&gt;A 10k batch survives or fails on this, and each provider picked a different model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SerpApi&lt;/strong&gt; queues. Hit the rate limit and requests wait behind one another rather than running in parallel, so latency climbs with volume. Exhaust the monthly cap and requests fail outright until the next billing cycle. If your volume is variable you overprovision for the worst day and pay for headroom you rarely use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DataForSEO&lt;/strong&gt; batches asynchronously. Submit, wait, retrieve. It absorbs thousands of tasks without per-request rate errors, and you pay for it in freshness. No requests-per-second ceiling appears in the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bright Data&lt;/strong&gt; hands you the dial. Its own &lt;a href="https://github.com/brightdata/bright-data-serp-city-accurate-rank-tracking" rel="noopener noreferrer"&gt;rank-tracking demo&lt;/a&gt; defaults to 5 concurrent and recommends 5 to 10, documenting a 1 to 20+ range with no published ceiling. Past the recommended range you are tuning against undocumented limits and watching for rate errors yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cloro&lt;/strong&gt; runs parallel lanes behind an async job queue. You POST a batch, get a job ID back in milliseconds, and results arrive by webhook as workers finish. Request 10,000 does not wait behind the other 9,999, so adding volume widens the fan-out instead of lengthening the wait. Concurrency scales by tier rather than capping at a fixed lane count.&lt;/p&gt;

&lt;p&gt;The honest summary of that difference: SerpApi and DataForSEO both trade real-time responsiveness for predictable batch cost, in opposite directions. If your workload is nightly and cost-sensitive, DataForSEO's queue is the right trade and cloro is overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Overview coverage, where the gap is measurable
&lt;/h2&gt;

&lt;p&gt;For GEO work the question is whether you get &lt;a href="https://cloro.dev/ai-overview/" rel="noopener noreferrer"&gt;AI Overview&lt;/a&gt; data as consistent structured fields, not whether the provider returns it at all.&lt;/p&gt;

&lt;p&gt;SerpApi detected AI Overviews in &lt;a href="https://scrape.do/blog/serpapi-alternatives/" rel="noopener noreferrer"&gt;68% of qualifying queries&lt;/a&gt; in a published 25-query benchmark. Roughly a third of pages that should show an Overview returned nothing detectable, and that is the ugly kind of gap: a missing Overview reads identically to an absent one, so your trend lines encode the parser's blind spots as changes in Google.&lt;/p&gt;

&lt;p&gt;DataForSEO makes it opt-in with &lt;a href="https://nextgrowth.ai/dataforseo-api-guide/" rel="noopener noreferrer"&gt;&lt;code&gt;load_async_ai_overview&lt;/code&gt;&lt;/a&gt; at $0.0006 per keyword, which doubles the Live tier to about $4.00 per 1,000 at ten results, and &lt;a href="https://cloro.dev/comparisons/dataforseo-alternatives/" rel="noopener noreferrer"&gt;$17.50 per 1,000 at 100 results with AIO&lt;/a&gt;. Structured, but you submit and poll rather than reading it inline.&lt;/p&gt;

&lt;p&gt;cloro returns Overview text, cited sources and position in the same payload as organic results. The reason that matters is not convenience: Google changed the Overview DOM during its January 2025 JS-rendering update, and anyone parsing raw HTML rebuilt their extraction. Provider-managed fields absorb that server-side. It is the strongest argument for buying rather than building, covered at length in &lt;a href="https://cloro.dev/blog/ai-search-visibility-build-vs-buy/" rel="noopener noreferrer"&gt;this build-versus-buy breakdown&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost at volume, which moved twice in 2025
&lt;/h2&gt;

&lt;p&gt;Model this at your depth, not the advertised rate.&lt;/p&gt;

&lt;p&gt;SerpApi is subscription with hard caps and no pay-as-you-go: roughly $2,500 at 100k monthly requests, $25,000 at 1M. DataForSEO is the cheapest at raw volume, about $0.60 per 1,000 on Standard, so 300k requests a month lands near $180. Bright Data prices per call near $0.005 with a minimum commitment &lt;a href="https://thunderbit.com/blog/best-serp-api-for-fast-accurate-data" rel="noopener noreferrer"&gt;cited around $499/month&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then two changes reset the arithmetic. DataForSEO moved to depth-based pricing on September 19, 2025, and Google deprecated &lt;code&gt;&amp;amp;num=100&lt;/code&gt; on September 11, 2025, forcing pagination at 10 results a page. If you need 100 results plus AI Overview, that Live tier goes from $2.00 to $17.50 per 1,000. A ninefold jump, from a parameter change you did not make. A &lt;a href="https://cloro.dev/blog/cheapest-serp-api/" rel="noopener noreferrer"&gt;fuller pricing breakdown&lt;/a&gt; runs the same numbers at four depths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick by workload
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nightly batches, cost-sensitive:&lt;/strong&gt; DataForSEO. The queue latency is real and irrelevant to you, and $0.60 per 1,000 is hard to argue with. Watch the depth pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;City-level geo-targeting across many markets:&lt;/strong&gt; Bright Data. 195 countries with city-level targeting is the widest coverage published, and you manage concurrency yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low volume, many engines, want it managed:&lt;/strong&gt; SerpApi. Live inside a tier and the caps never bite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time monitoring past 10k/day:&lt;/strong&gt; &lt;a href="https://cloro.dev/serp-api/" rel="noopener noreferrer"&gt;cloro&lt;/a&gt; is built for this shape, and its constraint is that it is API-first. If you want a dashboard rather than endpoints, buy a dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you pick, run your own 10k-request test against it under concurrent load before you commit. Every number above, cloro's included, was measured by someone with an interest in the result.&lt;/p&gt;

</description>
      <category>api</category>
      <category>llm</category>
      <category>dataextraction</category>
      <category>ai</category>
    </item>
    <item>
      <title>🚀 Launching FidForward! 🚀</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Mon, 08 Jul 2024 16:54:15 +0000</pubDate>
      <link>https://dev.to/rbatista19/launching-fidforward-54gc</link>
      <guid>https://dev.to/rbatista19/launching-fidforward-54gc</guid>
      <description>&lt;p&gt;Today, Bernardo and I are excited to launch FidForward in private beta!&lt;/p&gt;

&lt;p&gt;We're looking for companies with 10-50 employees willing to pilot the next generation of performance management.&lt;/p&gt;

&lt;p&gt;With two pilot programs already running successfully (achieving a 30-50% improvement in eNPS), we’re eager to expand FidForward to more companies to validate these results further.&lt;/p&gt;

&lt;p&gt;If you’re a founder seeking to implement performance management or a larger company aiming to elevate your current system, please reach out to me!&lt;/p&gt;

&lt;p&gt;🔗 Learn more about FidForward at &lt;a href="https://fidforward.com" rel="noopener noreferrer"&gt;FidForward.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>node</category>
    </item>
    <item>
      <title>We were tired of bureaucracy, so we built an opensource repo for the best guides</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Sun, 23 Jun 2024 21:09:11 +0000</pubDate>
      <link>https://dev.to/rbatista19/we-were-tired-of-bureaucracy-so-we-built-an-opensource-repo-for-the-best-guides-da6</link>
      <guid>https://dev.to/rbatista19/we-were-tired-of-bureaucracy-so-we-built-an-opensource-repo-for-the-best-guides-da6</guid>
      <description>&lt;p&gt;Today we are launching &lt;a href="https://tramitit.com/" rel="noopener noreferrer"&gt;Tramitit&lt;/a&gt;, a shared database on getting all those poor-UX local procedures done in a much simpler way!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works&lt;/strong&gt;: The community provides detailed walkthroughs on every possible bureaucratic process you might go through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the community by the community&lt;/strong&gt;: The content on this website is curated, verified, and rated by the community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Welcoming providers that can help our users&lt;/strong&gt;: Although our guides are as simple as possible, users may still prefer to delegate tasks to a curated provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are in a bit a chicken &amp;amp; egg problem: for the traffic to pick up and have contributors, we need to start by adding some content - some of which we have already gone through and fixed it, other not yet there. We will get there over time, and work with providers that can also bring the quality up.&lt;/p&gt;

&lt;p&gt;You can find our &lt;a href="https://github.com/tramitit/guides" rel="noopener noreferrer"&gt;GitHub repo here&lt;/a&gt;, where you can contribute as well!&lt;br&gt;
Our &lt;a href="https://github.com/tramitit/tramitit.github.io" rel="noopener noreferrer"&gt;website is also open-source&lt;/a&gt;, so feel free to add anything.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Getting started with Open SaaS</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Sun, 21 Apr 2024 07:34:17 +0000</pubDate>
      <link>https://dev.to/rbatista19/getting-started-with-open-saas-2gnb</link>
      <guid>https://dev.to/rbatista19/getting-started-with-open-saas-2gnb</guid>
      <description>&lt;p&gt;When building &lt;a href="https://article-generation.com/" rel="noopener noreferrer"&gt;Article Generation&lt;/a&gt; and &lt;a href="https://meeting-reminders.com/" rel="noopener noreferrer"&gt;Meeting Reminders&lt;/a&gt;, I decided to get started as fast as possible. So I looked for a free boilerplate and stumbled upon &lt;a href="https://opensaas.sh/" rel="noopener noreferrer"&gt;Open SaaS&lt;/a&gt;, which used YC-backed &lt;a href="https://wasp-lang.dev/" rel="noopener noreferrer"&gt;Wasp&lt;/a&gt;. It is a full-stack React + NodeJS + Prisma that takes 8 hours to get started with.&lt;/p&gt;

&lt;p&gt;Their documentation is brilliant, and great support over Discord. I decided to list here the full walkthrough on how to set up the development environment so that it is a simple copy-paste for you.&lt;/p&gt;

&lt;p&gt;For frontend deployment, I used &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt; (for the generous free package) and the recommended &lt;a href="https://fly.io/" rel="noopener noreferrer"&gt;fly.io&lt;/a&gt; for server + database (also cheap package).&lt;/p&gt;

&lt;h2&gt;
  
  
  Run development commands
&lt;/h2&gt;

&lt;p&gt;Start development database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd app &amp;amp;&amp;amp; wasp start db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View development database in Prisma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wasp db studio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start development app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd app &amp;amp;&amp;amp; wasp start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start development blog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd blog &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run frontend deployment commands
&lt;/h2&gt;

&lt;p&gt;Locate the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd .wasp/build/web-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install &amp;amp;&amp;amp; REACT_APP_API_URL={your-server-URL} npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy preview:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;netlify deploy&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Deploy to production:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;netlify deploy --prod&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every time there is a Wasp app rebuild (with&amp;nbsp;wasp build), it will remove your&amp;nbsp;&lt;code&gt;.wasp/build/&lt;/code&gt;&amp;nbsp;directory. Until the GitHub issue is fixed, you need to update &lt;code&gt;netlify.toml&lt;/code&gt;’s publish line to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  publish = ".wasp/build/web-app/build"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run backend &amp;amp; db deployment commands
&lt;/h2&gt;

&lt;p&gt;Locate the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd .wasp/build/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flyctl deploy --remote-only --config ../../src/shared/fly-server.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time there is a Wasp app rebuild (with&amp;nbsp;wasp build), it will remove your&amp;nbsp;&lt;code&gt;.wasp/build/&lt;/code&gt;&amp;nbsp;directory. That’s why there is a copy of the file in &lt;code&gt;app/src/shared/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run blog deployment commands
&lt;/h2&gt;

&lt;p&gt;Locate the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;netlify deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy to production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;netlify deploy --prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good luck with building, hopefully it will be faster now!&lt;/p&gt;

</description>
      <category>wasp</category>
      <category>node</category>
      <category>react</category>
    </item>
  </channel>
</rss>
