<?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: cloro</title>
    <description>The latest articles on DEV Community by cloro (cloro).</description>
    <link>https://dev.to/cloro</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%2Forganization%2Fprofile_image%2F16116%2Fbadbf7ff-c47d-453b-8741-3e21ab99d351.png</url>
      <title>DEV Community: cloro</title>
      <link>https://dev.to/cloro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloro"/>
    <language>en</language>
    <item>
      <title>Which web search API has an MCP server I can plug into Claude or Cursor?</title>
      <dc:creator>Ricardo Batista</dc:creator>
      <pubDate>Sat, 14 Nov 2026 17:13:06 +0000</pubDate>
      <link>https://dev.to/cloro/which-web-search-api-has-an-mcp-server-i-can-plug-into-claude-or-cursor-3p0i</link>
      <guid>https://dev.to/cloro/which-web-search-api-has-an-mcp-server-i-can-plug-into-claude-or-cursor-3p0i</guid>
      <description>&lt;p&gt;Seven web search APIs ship an official MCP server you can add to Claude, Claude Code or Cursor: Brave, Exa, Tavily, Firecrawl, Perplexity, Parallel and cloro. Pick by what the agent needs back. Exa, Tavily and Firecrawl return page text for a model to read. Brave returns results from its own index. Perplexity can return a written answer. cloro returns Google's own results page as JSON, with the AI Overview and People Also Ask. Parallel's server is free with no key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosted or local:&lt;/strong&gt; six of the seven run hosted, so you add one URL. Brave's runs on your machine through &lt;code&gt;npx&lt;/code&gt; or Docker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key or no key:&lt;/strong&gt; Parallel needs none. Exa and Firecrawl start without one. Brave and Perplexity need a key. The others take an OAuth sign-in or an API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy:&lt;/strong&gt; in a benchmark of six web search APIs on multi-hop questions, the two Google SERP APIs answered most questions from one search (80.2% and 77.4%). Parallel answered 65.3%, Exa 64.0%, Brave 53.9% and Tavily 48.4%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which web search APIs ship an MCP server?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Searches&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;th&gt;Key needed&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brave Search MCP&lt;/td&gt;
&lt;td&gt;Brave's own index&lt;/td&gt;
&lt;td&gt;Results as JSON&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;BRAVE_API_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Local, &lt;code&gt;npx&lt;/code&gt; or Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa MCP&lt;/td&gt;
&lt;td&gt;Exa's neural index&lt;/td&gt;
&lt;td&gt;Results plus page text as markdown&lt;/td&gt;
&lt;td&gt;No, to start&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://mcp.exa.ai/mcp&lt;/code&gt;, or &lt;code&gt;npx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily MCP&lt;/td&gt;
&lt;td&gt;Tavily's retrieval index&lt;/td&gt;
&lt;td&gt;Results plus extracted page content&lt;/td&gt;
&lt;td&gt;No, with the keyless header&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://mcp.tavily.com/mcp/&lt;/code&gt;, or &lt;code&gt;npx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl MCP&lt;/td&gt;
&lt;td&gt;Its own search, plus crawl and scrape&lt;/td&gt;
&lt;td&gt;Markdown or structured extraction&lt;/td&gt;
&lt;td&gt;No, capped per IP per day&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://mcp.firecrawl.dev/v2/mcp&lt;/code&gt;, or self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity MCP&lt;/td&gt;
&lt;td&gt;Perplexity's Search API and models&lt;/td&gt;
&lt;td&gt;Ranked results, or an answer with citations&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://api.perplexity.ai/mcp&lt;/code&gt;, or &lt;code&gt;npx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel Search MCP&lt;/td&gt;
&lt;td&gt;Parallel's Search API&lt;/td&gt;
&lt;td&gt;Results plus fetched pages&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://search.parallel.ai/mcp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;Google Search, plus Google AI Mode, ChatGPT, Gemini and Bing AI Mode&lt;/td&gt;
&lt;td&gt;Google's results page as JSON: organic positions, AI Overview and its sources, People Also Ask, ads&lt;/td&gt;
&lt;td&gt;OAuth in Claude, API key elsewhere&lt;/td&gt;
&lt;td&gt;Hosted at &lt;code&gt;https://mcp.cloro.dev/mcp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SerpApi, DataForSEO and Oxylabs also run official MCP servers over their Google APIs. Open-source servers such as &lt;code&gt;web-search-mcp&lt;/code&gt; and &lt;code&gt;duckduckgo-mcp-server&lt;/code&gt; scrape Bing, Brave or DuckDuckGo from your own machine with no key.&lt;/p&gt;

&lt;p&gt;Google has no official MCP server that returns the Google.com results page. Its &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services" rel="noopener noreferrer"&gt;managed MCP servers&lt;/a&gt; search Workspace content and data you load into Agent Search, and its Agent Platform server can ground a Gemini answer in Google Search. None of them returns organic positions or the AI Overview for a public query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;The servers differ most in what they hand back to the model, so pick by the job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your agent needs&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean page text to read and summarize, for RAG&lt;/td&gt;
&lt;td&gt;Exa, Tavily or Firecrawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An independent index that is not Google&lt;/td&gt;
&lt;td&gt;Brave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A finished answer with citations&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search at no cost and with no key&lt;/td&gt;
&lt;td&gt;Parallel, or an open-source server you run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What Google shows for a query in a country: rankings, the AI Overview, People Also Ask&lt;/td&gt;
&lt;td&gt;A server on a Google SERP API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A correct answer from one search on a hard question&lt;/td&gt;
&lt;td&gt;A Google SERP API, the leaders on multi-hop questions below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of these differences only show up once the agent runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output shape.&lt;/strong&gt; Exa, Tavily and Firecrawl put page text into the context, which is what a model needs to answer from, and it costs tokens on every call. Brave and the Google SERP servers return structured results, so the agent can answer "is this domain in the top 5?" by reading a field, not by parsing prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Index.&lt;/strong&gt; Only a server on a Google SERP API sees Google's answer layer. When 500 queries ran through Exa and through Google's live results, about a third of Exa's top 10 URLs were also in Google's top 10. That is by design: Exa retrieves by meaning. It is the wrong source for a rank check and a good one for finding pages a keyword search misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tiers change.&lt;/strong&gt; Brave has had no free plan since February 2026: every plan gets $5 of credit a month, enough for 1,000 searches, and sign-up needs a card. Exa gives $10 of credit a month, and Tavily and Firecrawl give 1,000 credits a month. None of the keyless servers publishes its limit, so expect throttling once an agent searches in a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you add a web search MCP server to Claude Code?
&lt;/h2&gt;

&lt;p&gt;A hosted server is one command. This adds Exa, which starts without a key. Replace the name and URL with the server you picked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add --transport http exa https://mcp.exa.ai/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open a session and run &lt;code&gt;/mcp&lt;/code&gt;. A server that uses OAuth asks you to sign in there. A server that needs a key takes it in a header or in the URL, as its docs describe.&lt;/p&gt;

&lt;p&gt;A local server runs as a command. For Brave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add brave -e &lt;span class="nv"&gt;BRAVE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key -- npx -y @brave/brave-search-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each server you add puts its tool definitions into the context before you ask anything. Run &lt;code&gt;/context&lt;/code&gt; to see what they cost, and keep the one or two you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you add one to Cursor?
&lt;/h2&gt;

&lt;p&gt;Cursor reads MCP servers from its &lt;code&gt;mcp.json&lt;/code&gt;. A hosted server is one entry with its URL:&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="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"exa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://mcp.exa.ai/mcp"&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;p&gt;Restart Cursor and the tools appear. Some servers take the API key in the URL path, which works in clients that cannot send custom headers. Check each server's docs for the exact URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which web search API is most accurate from one search?
&lt;/h2&gt;

&lt;p&gt;Six web search APIs ran on the 824 multi-hop questions of the FRAMES set in November 2026. Each question needs facts from two or more pages. Each API got the question as its query, one search and 10 results. One model answered from those results alone, and a second model graded each answer against the dataset's answer key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Multi-hop questions answered from one search&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;80.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;77.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel&lt;/td&gt;
&lt;td&gt;65.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;64.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brave&lt;/td&gt;
&lt;td&gt;53.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;48.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lead of the top API over SerpApi holds in a paired test on the same questions (p = 0.022). The AI Overview does most of the work: with the same results and the AI Overview removed, accuracy on 100 SimpleQA Verified questions fell from 99% to 86%. On short factual questions the order changes: Parallel (97.8%), Exa (97.3%) and the top Google API (96.9%) are within the margin of error of each other. The full method, the per-question-type results and the raw data are in the &lt;a href="https://cloro.dev/blog/web-search-api-benchmark/" rel="noopener noreferrer"&gt;web search API benchmark&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does each search cost over MCP?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Price per 1,000 searches&lt;/th&gt;
&lt;th&gt;Free allowance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brave&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$5 of credit a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity Search API&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;$7&lt;/td&gt;
&lt;td&gt;$10 of credit a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;About $8 pay as you go ($0.008 a credit)&lt;/td&gt;
&lt;td&gt;1,000 credits a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl&lt;/td&gt;
&lt;td&gt;About $7.60 on Hobby, billed monthly&lt;/td&gt;
&lt;td&gt;1,000 credits a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel&lt;/td&gt;
&lt;td&gt;Free over MCP&lt;/td&gt;
&lt;td&gt;No key needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloro&lt;/td&gt;
&lt;td&gt;$2.00 on Hobby; $2.80 with the AI Overview&lt;/td&gt;
&lt;td&gt;500 credits a month, about 100 searches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Google SERP row assumes a synchronous MCP call: a Google search is 3 credits plus a 2-credit sync surcharge, so 5 credits, and Hobby is $100 a month for 250,000 credits. For thousands of scheduled searches, the async API skips the surcharge.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this was checked
&lt;/h2&gt;

&lt;p&gt;This comparison is published by cloro, which is one of the servers in it. The endpoints, tool names, free allowances and prices of the other servers come from each vendor's own docs, pricing page or GitHub repository, read on October 24, 2026. The Tavily price and the Brave package were checked again on November 14, 2026. The accuracy figures come from the benchmark linked above, which publishes its questions and scoring.&lt;/p&gt;

&lt;p&gt;The full comparison, with the free open-source servers and a sample of what a Google results call returns, is in &lt;a href="https://cloro.dev/blog/google-search-mcp/" rel="noopener noreferrer"&gt;Best Web Search MCP Servers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>claude</category>
      <category>cursor</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/cloro/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/cloro/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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&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="kt"&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="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Any&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="s2"&gt;"""Parse ChatGPT's Server-Sent Events stream."""&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="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="s2"&gt;"\n"&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="k"&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="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="s2"&gt;"data: "&lt;/span&gt;&lt;span class="p"&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="p"&gt;]&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="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="s2"&gt;"[DONE]"&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="nb"&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="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&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="nc"&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="k"&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="s1"&gt;'prompt'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'best project management software for remote teams'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'include'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'markdown'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'searchQueries'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&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;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="s1"&gt;'https://api.cloro.dev/v1/monitor/chatgpt'&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="s1"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Bearer YOUR_API_KEY'&lt;/span&gt;&lt;span class="p"&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="k"&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;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;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 -X POST https://api.cloro.dev/v1/monitor/chatgpt \
  -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &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="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"citationPills"&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="nl"&gt;"citationPillId"&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="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Asana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"asana.com"&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="nl"&gt;"searchQueries"&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"asana vs monday remote teams"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"For remote teams, the strongest options are **Asana**..."&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;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;

</description>
      <category>chatgpt</category>
      <category>api</category>
      <category>python</category>
      <category>webscraping</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/cloro/how-to-benchmark-a-serp-api-six-axes-and-a-cost-model-that-survives-n100-3gao</link>
      <guid>https://dev.to/cloro/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 grounding cares about latency, a GEO platform cares almost entirely about AI Overview 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 Google DOM change 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, People Also Ask, 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;cloro (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. Where the legal line actually sits 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;. What is actually being retired and what is not 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>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/cloro/best-chatgpt-scraper-in-2026-what-breaks-and-what-8-tools-cost-4im6</link>
      <guid>https://dev.to/cloro/best-chatgpt-scraper-in-2026-what-breaks-and-what-8-tools-cost-4im6</guid>
      <description>&lt;p&gt;The best ChatGPT scraper depends on the job. For brand monitoring, use a managed API that returns parsed citations; of the eight tools below, one returns them parsed and the rest return 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 shopping cards, 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 proxy selection for SERP work 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, citation parsing, 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; cloro 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. Where the line sits 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/cloro/how-to-scrape-google-ai-mode-citation-pills-place-cards-2c7</link>
      <guid>https://dev.to/cloro/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;AI Overview&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;Gemini&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;ChatGPT Search&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="s2"&gt;"https://www.google.com/search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"udm"&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="s2"&gt;"aep"&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="s2"&gt;"q"&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="s2"&gt;"hl"&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="s2"&gt;"hl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"gl"&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="s2"&gt;"gl"&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="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="kc"&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://www.google.com/async/folwr"&lt;/span&gt;&lt;span class="p"&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="k"&gt;in&lt;/span&gt; &lt;span class="nb"&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="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="nb"&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="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="s2"&gt;"Never received AI Mode response after 60 seconds"&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="s2"&gt;"[data-session-thread-id]"&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="s2"&gt;".."&lt;/span&gt;&lt;span class="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&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="o"&gt;=&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="o"&gt;=&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="nc"&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="nc"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&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="p"&gt;]&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="nc"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&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="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;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="s1"&gt;'button[data-icl-uuid][data-amic="true"]'&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="p"&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;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&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="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;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="p"&gt;)&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="s2"&gt;"data-icl-uuid"&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="n"&gt;rf&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;!--Sv6Kpe\[\["{re.escape(uuid)}".*?]]--&amp;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="nc"&gt;List&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&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="k"&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="no"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;rf&lt;/span&gt;&lt;span class="s1"&gt;'"{re.escape(uuid)}"\s*,\s*\[\s*"[^"]+"\s*,\s*"([^"]+)"'&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="nb"&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="k"&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;r&lt;/span&gt;&lt;span class="s1"&gt;'"(https://[^"]+)"'&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="nb"&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="k"&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="k"&gt;in&lt;/span&gt;
                            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"gstatic.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"encrypted-tbn"&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="p"&gt;,&lt;/span&gt;
                &lt;span class="kc"&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="s2"&gt;"#:~:text"&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"\\u003d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"\\u0026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;amp;"&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="nb"&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="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Source {len(current_pill) + 1}"&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="kc"&gt;None&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="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 sources panel 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="s1"&gt;'[data-container-id="rhs-col"] [role="dialog"] a'&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="s2"&gt;"a.ZbQNgf"&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="s2"&gt;"html.parser"&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="k"&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="s2"&gt;"button"&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="s2"&gt;"data-icl-uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data-amic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&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;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="s2"&gt;"data-icl-uuid"&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="k"&gt;in&lt;/span&gt; &lt;span class="nb"&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="p"&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;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="s2"&gt;"a"&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="s2"&gt;"url"&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="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="s2"&gt;"label"&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="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="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="kc"&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="kt"&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="p"&gt;)&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="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 CAPTCHA, 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="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="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="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"HTTP error: {response.status} (probably captcha)"&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 proxy pool 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="s2"&gt;"https://api.cloro.dev/v1/monitor/aimode"&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="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bearer sk_live_your_api_key"&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"What do you know about Tesla's latest updates?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&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="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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{len(result['sources'])} citations, fan-out: {result['searchQueries']}"&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>
  </channel>
</rss>
