<?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: AnswerLine</title>
    <description>The latest articles on DEV Community by AnswerLine (answerline).</description>
    <link>https://dev.to/answerline</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%2F14818%2F9740d4c4-4c00-4418-8ba5-fd5278cd082a.png</url>
      <title>DEV Community: AnswerLine</title>
      <link>https://dev.to/answerline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/answerline"/>
    <language>en</language>
    <item>
      <title>Ask ChatGPT from inside Cursor with one MCP line</title>
      <dc:creator>Richard Nemeth</dc:creator>
      <pubDate>Tue, 29 Sep 2026 15:24:29 +0000</pubDate>
      <link>https://dev.to/answerline/ask-chatgpt-from-inside-cursor-with-one-mcp-line-155</link>
      <guid>https://dev.to/answerline/ask-chatgpt-from-inside-cursor-with-one-mcp-line-155</guid>
      <description>&lt;p&gt;You can ask ChatGPT and Gemini, and search Google News, from inside Cursor or Claude Code by adding one remote MCP server: &lt;code&gt;https://mcp.answerline.dev/mcp&lt;/code&gt;, authenticated with your API key as a bearer token. Each engine then appears as a tool the editor's agent can call mid-session, and every call returns the same structured JSON as the REST API (answer text, &lt;code&gt;sources[]&lt;/code&gt; and citation pills).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the MCP server is
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) server is a hosted endpoint that exposes this API's monitor routes as tools an MCP client can list and call. It speaks Streamable HTTP over &lt;code&gt;POST&lt;/code&gt;, and supports protocol revisions 2024-11-05 through 2025-11-25 as well as 2026-07-28 (&lt;a href="https://dev.to/docs/mcp"&gt;MCP guide&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There are two endpoint forms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://mcp.answerline.dev/mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Authorization: Bearer sk_...&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;The client can send headers. Cursor and Claude Code both can.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://mcp.answerline.dev/&amp;lt;API_KEY&amp;gt;/mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key in the path&lt;/td&gt;
&lt;td&gt;The client only accepts a URL. The URL itself is then a secret.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefer the header form whenever the client supports it. A URL with a key in it ends up in logs, screenshots and shell history more easily than a header value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key in the dashboard and export it in the shell you start Claude Code from: &lt;code&gt;export API_KEY=sk_...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http answerline &lt;span class="se"&gt;\&lt;/span&gt;
  https://mcp.answerline.dev/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start a session and ask for something that needs a tool, for example "check my AnswerLine credits". The agent should call &lt;code&gt;credits&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two details from the Claude Code MCP documentation (&lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;code.claude.com/docs/en/mcp&lt;/a&gt;, checked 2026-09-17):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope decides where the key is stored. The default &lt;code&gt;local&lt;/code&gt; scope and the &lt;code&gt;user&lt;/code&gt; scope write to &lt;code&gt;~/.claude.json&lt;/code&gt;, which is private to you. The &lt;code&gt;project&lt;/code&gt;scope writes &lt;code&gt;.mcp.json&lt;/code&gt; in the project root, which is meant to be shared through version control. Because your shell expands &lt;code&gt;$API_KEY&lt;/code&gt; before &lt;code&gt;claude mcp add&lt;/code&gt; runs, the literal key is what gets stored. Never use &lt;code&gt;--scope project&lt;/code&gt; with a literal key.&lt;/li&gt;
&lt;li&gt;Shared configs can reference variables. &lt;code&gt;.mcp.json&lt;/code&gt; supports &lt;code&gt;${VAR}&lt;/code&gt; expansion in &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;headers&lt;/code&gt;, so a committed project file can hold &lt;code&gt;"Authorization": "Bearer ${ANSWERLINE_API_KEY}"&lt;/code&gt; while each developer sets the variable locally. The same page documents &lt;code&gt;MCP_TOOL_TIMEOUT&lt;/code&gt; for the tool execution timeout, which matters for long engine calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A project-scoped file that is safe to commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"answerline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.answerline.dev/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${ANSWERLINE_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cursor setup
&lt;/h2&gt;

&lt;p&gt;Cursor reads MCP servers from &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in the project root or &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; for all projects, supports stdio, SSE and Streamable HTTP transports, and takes remote servers as a &lt;code&gt;url&lt;/code&gt; with optional &lt;code&gt;headers&lt;/code&gt; (&lt;a href="https://cursor.com/docs/context/mcp" rel="noopener noreferrer"&gt;cursor.com/docs/context/mcp&lt;/a&gt;, checked 2026-09-17).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick the file. Use &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; if you want the tools everywhere; use &lt;code&gt;.cursor/mcp.json&lt;/code&gt; if the project needs them and your teammates should get the entry too.&lt;/li&gt;
&lt;li&gt;Add the server, referencing an environment variable for the key. Cursor resolves &lt;code&gt;${env:NAME}&lt;/code&gt; in &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;headers&lt;/code&gt; (same source):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"answerline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.answerline.dev/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${env:ANSWERLINE_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;ANSWERLINE_API_KEY&lt;/code&gt; in the environment Cursor is started from; a variable exported in a terminal opened later is not visible to an editor that is already running.&lt;/li&gt;
&lt;li&gt;Confirm the server is enabled in Cursor's MCP settings and that the tools are listed. If the server shows an error, Cursor's Output panel has an "MCP Logs" channel with connection and authentication errors (same source).&lt;/li&gt;
&lt;li&gt;Expect an approval prompt. Cursor asks for approval before running MCP tools by default (same source). Leave that on while you learn what the calls cost; each approved engine call spends credits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other editors and agents configured with an &lt;code&gt;mcpServers  JSON file that supports remote servers take the same&lt;/code&gt;url&lt;code&gt;and&lt;/code&gt;headers` shape; check your client's own documentation for variable interpolation before relying on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the key and endpoint
&lt;/h2&gt;

&lt;p&gt;If the editor shows no tools, test the endpoint directly. First the REST API, which isolates key problems:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
curl -s https://api.answerline.dev/v1/credits \&lt;br&gt;
  -H "Authorization: Bearer $API_KEY"&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A valid key returns &lt;code&gt;remaining&lt;/code&gt;, &lt;code&gt;perCycle&lt;/code&gt; and &lt;code&gt;cycleResetsAt&lt;/code&gt;. A &lt;code&gt;401&lt;/code&gt; with &lt;code&gt;MISSING_API_KEY&lt;/code&gt;, &lt;code&gt;INVALID_API_KEY_FORMAT&lt;/code&gt; or &lt;code&gt;INVALID_OR_EXPIRED_API_KEY&lt;/code&gt; means the key, not the MCP setup, is the problem (&lt;a href="https://dev.to/docs/authentication"&gt;authentication&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Then ask the MCP server for its tool list with a plain JSON-RPC request:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
curl -s https://mcp.answerline.dev/mcp \&lt;br&gt;
  -H "Authorization: Bearer $API_KEY" \&lt;br&gt;
  -H "Content-Type: application/json" \&lt;br&gt;
  -H "Accept: application/json" \&lt;br&gt;
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The response lists the tools with their input schemas. &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; on the endpoint answer &lt;code&gt;405&lt;/code&gt;, so a browser visit to the URL tells you nothing; use &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools and what each call costs
&lt;/h2&gt;

&lt;p&gt;Tool names follow the route: &lt;code&gt;/v1/monitor/google/news&lt;/code&gt; becomes &lt;code&gt;google_news&lt;/code&gt;. Each engine tool's input schema is that endpoint's request body, so the agent passes &lt;code&gt;prompt&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt; to &lt;code&gt;chatgpt&lt;/code&gt;, or &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt; to &lt;code&gt;google_news&lt;/code&gt;, plus any options the endpoint accepts.&lt;/p&gt;

&lt;p&gt;Every engine tool call is a synchronous API call. It costs the engine's base price plus the 2-credit synchronous surcharge:&lt;/p&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;Route&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Cost per tool call&lt;/th&gt;
&lt;th&gt;Common add-ons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chatgpt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/chatgpt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5 + 2 = 7&lt;/td&gt;
&lt;td&gt;+2 once for &lt;code&gt;include.rawResponse&lt;/code&gt; or &lt;code&gt;shopping&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/gemini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4 + 2 = 6&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/copilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5 + 2 = 7&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aimode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/aimode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4 + 2 = 6&lt;/td&gt;
&lt;td&gt;+1 per expanded product cluster, up to 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3 + 2 = 5&lt;/td&gt;
&lt;td&gt;+2 per extra page; +2 once for the AI Overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google_news&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/monitor/google/news&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 + 2 = 4&lt;/td&gt;
&lt;td&gt;+2 per extra page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;credits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /v1/credits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;perplexity&lt;/code&gt; and &lt;code&gt;grok&lt;/code&gt; tools are paused for now and coming back soon, and are not listed until then.&lt;/p&gt;

&lt;p&gt;Worked examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT with &lt;code&gt;include.shopping: true&lt;/code&gt;: 5 + 2 (feature group) + 2 (sync) = 9 credits.&lt;/li&gt;
&lt;li&gt;Google News, &lt;code&gt;pages: 2&lt;/code&gt;: 2 + 2 (one extra page) + 2 (sync) = 6 credits.&lt;/li&gt;
&lt;li&gt;The Free plan's 500 one-time credits work on every engine tool: 71 plain &lt;code&gt;chatgpt&lt;/code&gt; or &lt;code&gt;copilot&lt;/code&gt; calls at 7, 83 &lt;code&gt;gemini&lt;/code&gt; or &lt;code&gt;aimode&lt;/code&gt; calls at 6, 100 &lt;code&gt;google&lt;/code&gt; calls at 5, or 125 &lt;code&gt;google_news&lt;/code&gt; calls at 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failed calls are charged nothing, and the charged amount of each call is in the result's &lt;code&gt;_meta&lt;/code&gt; as &lt;code&gt;x-credits-charged&lt;/code&gt;, next to &lt;code&gt;x-credits-remaining&lt;/code&gt;. See &lt;a href="https://dev.to/docs/credits"&gt;credits&lt;/a&gt; for the full rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slots and timeouts
&lt;/h2&gt;

&lt;p&gt;Two limits shape how an editor agent should use these tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency slots
&lt;/h3&gt;

&lt;p&gt;A synchronous call holds one of your plan's concurrency slots for its whole run. The Free plan has 1 slot, Lite 10, Hobby 20, Starter 50, Growth 75 and Business 100 (&lt;a href="https://dev.to/docs/rate-limits"&gt;rate limits&lt;/a&gt;). An agent that fires three tool calls in parallel on the Free plan will see the extra calls come back with &lt;code&gt;CONCURRENT_LIMIT_EXCEEDED&lt;/code&gt;. Tell the agent to run engine calls one at a time unless you know your plan has room.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duration
&lt;/h3&gt;

&lt;p&gt;A synchronous call can take up to five minutes. The server streams keep-alive comments to clients that accept &lt;code&gt;text/event-stream&lt;/code&gt;, but the client's own tool timeout still applies. In Claude Code, &lt;code&gt;MCP_TOOL_TIMEOUT&lt;/code&gt; controls it; in other clients, look for a tool or request timeout setting and set it above five minutes where you can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the key out of the repository
&lt;/h2&gt;

&lt;p&gt;The key spends credits on your account, so treat every place it can land as sensitive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never commit a config file with a literal key. Reference an environment variable (&lt;code&gt;${env:NAME}&lt;/code&gt; in Cursor, &lt;code&gt;${VAR}&lt;/code&gt; in Claude Code's &lt;code&gt;.mcp.json&lt;/code&gt;) and commit only that.&lt;/li&gt;
&lt;li&gt;Treat the key-in-path URL as a password. &lt;code&gt;https://mcp.answerline.dev/&amp;lt;API_KEY&amp;gt;/mcp&lt;/code&gt; should never appear in a README, an issue, a chat message or a screen recording.&lt;/li&gt;
&lt;li&gt;Use a separate key for editor work. If a laptop config leaks, you can revoke that key alone; a revoked key stops working within a minute.&lt;/li&gt;
&lt;li&gt;Git-ignore any project config that holds a literal key, such as a &lt;code&gt;.cursor/mcp.json&lt;/code&gt; you have not yet switched to a variable.&lt;/li&gt;
&lt;li&gt;Keep approvals on. Cursor's default approval prompt is also a spending check.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Errors the agent should handle
&lt;/h2&gt;

&lt;p&gt;API errors do not break the MCP session. The server returns a normal tool result with &lt;code&gt;isError: true&lt;/code&gt; and the API's error body as text, and puts the request id in &lt;code&gt;_meta&lt;/code&gt; under &lt;code&gt;x-request-id&lt;/code&gt;. The model can read the &lt;code&gt;code&lt;/code&gt; and act:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What the agent should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONCURRENT_LIMIT_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All slots were busy&lt;/td&gt;
&lt;td&gt;Wait for its other call to finish, then retry once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INSUFFICIENT_CREDITS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance too low for the call&lt;/td&gt;
&lt;td&gt;Stop and tell you; do not retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RATE_LIMIT_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Over the per-second request limit&lt;/td&gt;
&lt;td&gt;Retry after a short pause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;400&lt;/code&gt; "Request validation failed" with &lt;code&gt;details[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A field was wrong, for example a &lt;code&gt;prompt&lt;/code&gt; over 10,000 characters&lt;/td&gt;
&lt;td&gt;Read &lt;code&gt;details[].field&lt;/code&gt; and fix the arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EXTERNAL_SERVICE_ERROR&lt;/code&gt; (502)&lt;/td&gt;
&lt;td&gt;The engine request failed&lt;/td&gt;
&lt;td&gt;Report it; the call was not charged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A standing instruction for the agent: "Call engine tools sequentially. If a result has isError, read the code; stop on INSUFFICIENT_CREDITS and report the x-request-id for anything else you cannot fix."&lt;/p&gt;

&lt;h2&gt;
  
  
  Example session
&lt;/h2&gt;

&lt;p&gt;"Before we ship this pricing page, ask ChatGPT and Gemini 'best invoicing tool for freelancers' in the US, list every domain each one cites, and tell me which of our competitors appear."&lt;/p&gt;

&lt;p&gt;The agent makes two calls, &lt;code&gt;chatgpt&lt;/code&gt; (7 credits) and &lt;code&gt;gemini&lt;/code&gt; (6 credits), 13 in total. It reads &lt;code&gt;sources[].url&lt;/code&gt; from both results, reduces them to domains, and compares the two sets. It can also match brand names in each answer's &lt;code&gt;text&lt;/code&gt; to see which brands were named even where no source is cited. Don't ask it for ChatGPT's underlying searches: &lt;code&gt;include.searchQueries&lt;/code&gt; is free, but &lt;code&gt;searchQueries[]&lt;/code&gt; comes back empty on the ChatGPT answers served today (&lt;a href="https://dev.to/engines/chatgpt/search-queries"&gt;search queries&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Because the JSON lands in the editor's context, the agent can then write a monitoring script against the field names it has just seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to leave the editor
&lt;/h2&gt;

&lt;p&gt;Editor tools are for questions you ask now. Recurring work, such as the same 200 prompts every Monday, belongs in &lt;a href="https://dev.to/docs/async"&gt;async tasks and batches&lt;/a&gt;: they skip the 2-credit surcharge (200 ChatGPT prompts cost 200 × 5 = 1,000 credits as tasks versus 200 × 7 = 1,400 as tool calls), queue instead of failing when slots are busy, and can deliver results by webhook (&lt;a href="https://dev.to/blog/sync-async-webhooks"&gt;sync, async and webhooks&lt;/a&gt;). For agent architecture beyond the editor, see &lt;a href="https://dev.to/blog/mcp-server-agents"&gt;giving an AI agent live answer data&lt;/a&gt; and the &lt;a href="https://dev.to/blog/langchain-agent-live-answers"&gt;LangChain tools&lt;/a&gt;; for tracking competitors over time, see &lt;a href="https://dev.to/blog/competitor-ai-answer-monitoring"&gt;competitor AI answer monitoring&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full tool reference: &lt;a href="https://dev.to/docs/mcp"&gt;/docs/mcp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>cursor</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The queries ChatGPT runs before it answers yours</title>
      <dc:creator>Richard Nemeth</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:35:17 +0000</pubDate>
      <link>https://dev.to/answerline/the-queries-chatgpt-runs-before-it-answers-yours-53mf</link>
      <guid>https://dev.to/answerline/the-queries-chatgpt-runs-before-it-answers-yours-53mf</guid>
      <description>&lt;p&gt;When ChatGPT answers with web search, it does not have to search your prompt word for word. It can run several searches, narrower, rephrased or with added context, and its cited sources come from what those searches return. That set of searches is the query fan-out. ChatGPT does not show it to users, and on the ChatGPT answers this API serves today the &lt;code&gt;searchQueries&lt;/code&gt; field comes back empty. What you can see is the result of the fan-out: the pages cited, in order, and the answer text. This post covers how to reason about the fan-out from those.&lt;/p&gt;

&lt;p&gt;Google uses the same term for AI Mode: its announcement describes AI Mode breaking a question into subtopics and running many searches at once with a "query fan-out technique" (&lt;a href="https://blog.google/products/search/google-search-ai-mode-update/" rel="noopener noreferrer"&gt;Google, May 20, 2025&lt;/a&gt;, checked 2026-09-17).&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query fan-out&lt;/strong&gt;: the searches an AI assistant runs to answer one prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out query&lt;/strong&gt;: one search in that set. The API field for them is &lt;code&gt;result.searchQueries&lt;/code&gt;, empty on the ChatGPT answers served today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source frequency&lt;/strong&gt;: the number of answers in a prompt set that cite a given domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inferred search&lt;/strong&gt;: a search you work out from the cited pages and the answer text. It is a hypothesis, not returned data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
&lt;/p&gt;
One prompt, several searches, one cited answer
&lt;br&gt;
A single request carrying one prompt fans out into several web searches, which ChatGPT runs but does not return today. The pages those searches return feed one answer, which comes back as result.text together with sources and citationPills, each entry carrying a url and a label.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
POST /v1/monitor/chatgpt&lt;br&gt;
one prompt&lt;br&gt;
one request&lt;br&gt;
search 1&lt;br&gt;
search 2&lt;br&gt;
search 3&lt;br&gt;
search 4&lt;br&gt;
result.text&lt;br&gt;
sources[]&lt;br&gt;
url, label&lt;br&gt;
citationPills[]&lt;br&gt;
url, domain&lt;br&gt;
searches ChatGPT ran: not returned today&lt;br&gt;
&lt;br&gt;


&lt;p&gt;The searches in the middle column are what the answer was actually built from, so they, not the prompt's own wording, are the queries a page has to win. For ChatGPT you can't see them today; the right-hand column is what you can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the fan-out matters
&lt;/h2&gt;

&lt;p&gt;A prompt such as "iPhone 17 or Galaxy S26?" is one question to the user and several retrieval problems to the assistant: specs, camera comparisons, battery tests, prices, maybe reviews from a specific year. The pages that win those narrower searches are the ones the answer can cite. Three consequences for anyone working on AI visibility:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your target keyword is not the retrieval query.&lt;/strong&gt; A page optimized for the prompt's wording can miss the searches actually run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out queries can repeat across prompts.&lt;/strong&gt; When different prompts in one category trigger overlapping searches, one page that serves that search is relevant to many answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fan-out leaves traces.&lt;/strong&gt; ChatGPT does not return its searches, but the cited sources are what those searches found. Which domains get cited, and in what order, is the measurable side of the fan-out.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the API returns
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iPhone 17 or Galaxy S26?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send it to &lt;code&gt;POST /v1/monitor/chatgpt&lt;/code&gt; or as the payload of a &lt;code&gt;CHATGPT&lt;/code&gt; task. The result carries &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;sources[]&lt;/code&gt; in the order ChatGPT shows them, and &lt;code&gt;citationPills[]&lt;/code&gt;. The contract also has &lt;code&gt;result.searchQueries&lt;/code&gt; for the fan-out, requested with &lt;code&gt;include.searchQueries: true&lt;/code&gt; at no extra cost, but it comes back empty on the ChatGPT answers served today.&lt;/p&gt;

&lt;p&gt;Two request details decide whether you get useful data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web search is forced by default.&lt;/strong&gt; Leave &lt;code&gt;disableWebSearch&lt;/code&gt; unset. With &lt;code&gt;true&lt;/code&gt;, you get the answer ChatGPT gives on its own, which searches only when it decides to, so &lt;code&gt;sources&lt;/code&gt; and &lt;code&gt;citationPills&lt;/code&gt; are often empty. That mode is useful for a different question (what does the model say without retrieval?), not for studying retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market matters.&lt;/strong&gt; &lt;code&gt;country&lt;/code&gt; is required. ChatGPT answers from 213 countries; US-state targeting is coming soon. Run each market as its own series and don't pool markets before you have compared them.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.answerline.dev/v1/monitor/chatgpt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANSWERLINE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "prompt": "best CRM for a 10-person B2B sales team",
    "country": "US"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Aggregate cited sources across a prompt set
&lt;/h2&gt;

&lt;p&gt;One answer's sources say little; across 50 or 200 prompts they give a distribution. Count each cited domain once per answer that cited 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;source_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cited domains with the number of answers that cited them, most frequent first.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nf"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;results&lt;/code&gt; are &lt;code&gt;result&lt;/code&gt; objects: from synchronous responses, or &lt;code&gt;response.result&lt;/code&gt; of completed tasks.&lt;/p&gt;

&lt;p&gt;Counting per answer, not per occurrence, keeps one answer that cites a domain several times from inflating it. Also report coverage: the share of answers that cited any sources at all. If only 40% did, your counts describe that 40%, and you should say so next to the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason about the searches you can't see
&lt;/h2&gt;

&lt;p&gt;Without the searches, work backwards from what was cited and what was said:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the cited pages.&lt;/strong&gt; Each source's &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; say what the page is about, and so what the search that found it must have matched. A page about camera tests for two phones was found by a camera search, not by the prompt as typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the answer text for subtopics.&lt;/strong&gt; An answer that covers specs, price, battery and reviews in turn most likely searched for each. Each subtopic is a candidate query for your content plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group prompts by shared sources.&lt;/strong&gt; Prompts whose answers cite the same domains probably rely on overlapping searches, and one page can serve all of them.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prompts_by_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;rows are (prompt_id, result) pairs. Maps each cited domain to the prompts whose answers cite it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&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;prompt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is inference, not returned data: a cited page tells you a search found it, not which search. Label inferred searches as such in reports. &lt;a href="https://answerline.dev/blog/how-ai-engines-choose-citations" rel="noopener noreferrer"&gt;How AI engines choose citations&lt;/a&gt; covers the source side in more depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn it into work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rank topics&lt;/strong&gt; by how many answers cite pages on them across the prompt set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map each topic to a page.&lt;/strong&gt; Note the page on your site that should answer it, or that none does. A missing page for a frequently cited topic is the clearest content gap you will find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the organic side.&lt;/strong&gt; Inferred searches are still searches, so run the top ones through a Google Search request and see who ranks. A domain that ranks organically and is cited in answers holds both positions; one that ranks but is never cited suggests the page doesn't answer the narrower question well. &lt;a href="https://answerline.dev/blog/structured-serp-data-api" rel="noopener noreferrer"&gt;Structured SERP data&lt;/a&gt; shows the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split by market.&lt;/strong&gt; Run the set with each &lt;code&gt;country&lt;/code&gt; you sell in; fan-out can differ by market, and so do the cited domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the set fixed.&lt;/strong&gt; Re-run the same prompts on a schedule so changes in the cited sources are visible, and version the prompt set when you change it. &lt;a href="https://answerline.dev/blog/prompt-set-design" rel="noopener noreferrer"&gt;Prompt set design&lt;/a&gt; covers versioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed new prompts back.&lt;/strong&gt; Subtopics that recur across answers are good candidates for new monitoring prompts, because they reflect how the engine decomposes your category.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A report that people act on
&lt;/h2&gt;

&lt;p&gt;A report content teams can act on has four columns per topic:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic (inferred from cited pages)&lt;/th&gt;
&lt;th&gt;Answers citing it&lt;/th&gt;
&lt;th&gt;Top cited domains&lt;/th&gt;
&lt;th&gt;Our page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;best crm small business&lt;/td&gt;
&lt;td&gt;34 of 150&lt;/td&gt;
&lt;td&gt;review site A, vendor B, publisher C&lt;/td&gt;
&lt;td&gt;/crm-for-small-teams (not cited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crm pricing comparison&lt;/td&gt;
&lt;td&gt;21 of 150&lt;/td&gt;
&lt;td&gt;vendor B, publisher D&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crm with email automation&lt;/td&gt;
&lt;td&gt;12 of 150&lt;/td&gt;
&lt;td&gt;vendor E, our domain&lt;/td&gt;
&lt;td&gt;/features/email (cited 5 times)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How to read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High frequency, no page&lt;/strong&gt;: a content gap. Write the page that answers the narrower question directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High frequency, page exists, not cited&lt;/strong&gt;: the page is not what the engine retrieves or uses for that topic. Compare it with the cited pages: scope, freshness, structure, and whether it answers the exact question the topic implies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page cited&lt;/strong&gt;: protect it. Keep it current and watch the topic in the next runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a fifth column with a change indicator against the previous run of the same prompt set version, and the report doubles as a trend view. Keep the denominator ("of 150") visible, and state that topics are inferred from cited pages, not from returned searches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fan-out on other engines
&lt;/h2&gt;

&lt;p&gt;Some engines return their searches, with different field names:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;How to get it&lt;/th&gt;
&lt;th&gt;Base credits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result.searchQueries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;include.searchQueries: true&lt;/code&gt; (free); empty on the answers served today&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result.searchQueries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coming; not returned yet&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result.searchQueries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returned in the response&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result.search_model_queries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returned in the response&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Grok and Perplexity are paused for now and coming back soon. Perplexity also returns &lt;code&gt;related_queries&lt;/code&gt;, its suggested follow-up searches. Those are suggestions for the user, not searches it ran, so keep them in a separate table. The contract also defines &lt;code&gt;product.generatedProductQuery&lt;/code&gt; on ChatGPT inline products, a product-level cousin of the fan-out, but &lt;code&gt;inlineProducts&lt;/code&gt; comes back empty on the answers served today; see &lt;a href="https://answerline.dev/blog/chatgpt-shopping-cards" rel="noopener noreferrer"&gt;ChatGPT shopping cards&lt;/a&gt; for the product data you get.&lt;/p&gt;

&lt;p&gt;Where an engine returns searches, count each normalized search once per answer that ran it, and group near-duplicates ("best crm small business 2026", "best CRM for small businesses") before counting. Normalize every engine's queries the same way so you can see which query groups every engine runs and which are engine-specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it at scale
&lt;/h2&gt;

&lt;p&gt;For a prompt set, send async tasks in batches (up to 500 per request) with a webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"taskType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHATGPT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"idempotencyKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fanout-v3-2026-w38-crm-012-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"best CRM for a 10-person B2B sales team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"webhook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.example/hooks/answerline"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the prompt set version in the idempotency key, as above, so a re-run under a new version never collides with an old one. &lt;a href="https://answerline.dev/blog/sync-async-webhooks" rel="noopener noreferrer"&gt;Sync, async and webhooks&lt;/a&gt; explains the flow.&lt;/p&gt;

&lt;p&gt;Cost, from the current credit table (&lt;a href="https://answerline.dev/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt; has credit prices): 150 prompts Ă— 1 market Ă— weekly (about 4.3 runs a month) Ă— 5 credits for a ChatGPT task â‰ˆ 3,200 credits a month. &lt;code&gt;include.searchQueries&lt;/code&gt; is free, so turning it on does not change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reading an empty &lt;code&gt;searchQueries&lt;/code&gt; as "no search".&lt;/strong&gt; On ChatGPT today it means the searches were not exposed, not that none ran; check &lt;code&gt;sources&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counting occurrences instead of answers.&lt;/strong&gt; One answer citing a domain several times should count once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating inferred searches as data.&lt;/strong&gt; A search worked out from cited pages is a hypothesis; label it that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disabling web search by accident.&lt;/strong&gt; &lt;code&gt;disableWebSearch: true&lt;/code&gt; changes the question you are measuring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pooling markets early.&lt;/strong&gt; Compare markets before merging them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judging one run.&lt;/strong&gt; Answers and their sources vary between runs; trend over repeated runs. &lt;a href="https://answerline.dev/blog/ai-answer-volatility" rel="noopener noreferrer"&gt;AI answer volatility&lt;/a&gt; covers sampling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparing across prompt set versions.&lt;/strong&gt; A reworded prompt produces a different fan-out. Compare runs only within one version, and when you promote a new version, run old and new side by side once so the change in counts can be attributed to the wording rather than to the engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See the full ChatGPT field list on the &lt;a href="https://answerline.dev/engines/chatgpt" rel="noopener noreferrer"&gt;ChatGPT engine page&lt;/a&gt;, or start with the &lt;a href="https://answerline.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you want ChatGPT's answer and cited sources as JSON instead of reading browser traffic: &lt;code&gt;POST /v1/monitor/chatgpt&lt;/code&gt; returns them in one call. Free tier, no card: &lt;a href="https://answerline.dev" rel="noopener noreferrer"&gt;https://answerline.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>api</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Is there a Google Search API? What exists in 2026</title>
      <dc:creator>Richard Nemeth</dc:creator>
      <pubDate>Sun, 27 Sep 2026 20:55:18 +0000</pubDate>
      <link>https://dev.to/answerline/is-there-a-google-search-api-what-exists-in-2026-3n58</link>
      <guid>https://dev.to/answerline/is-there-a-google-search-api-what-exists-in-2026-3n58</guid>
      <description>&lt;p&gt;No. As of September 2026 Google does not offer new customers an API that returns its web search results. The Custom Search JSON API, the closest thing Google had, is closed to new customers, and existing customers must move off it by January 1, 2027. What Google does offer are search products for your own sites and data, and a way to let Gemini models search the web and answer. For Google's actual results page as data, teams use a third-party SERP API or build a scraper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Search API&lt;/strong&gt; (as people search for it): a programmatic way to send a query and get Google's web results back as data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SERP&lt;/strong&gt;: the search engine results page, including organic results and modules such as People Also Ask, ads, local packs and AI Overviews. See &lt;a href="https://answerline.dev/blog/what-is-a-serp" rel="noopener noreferrer"&gt;what is a SERP&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SERP API&lt;/strong&gt;: a service that runs searches on Google and returns the SERP as structured JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding&lt;/strong&gt;: giving a language model search results as context so its answer cites current sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Google offers
&lt;/h2&gt;

&lt;p&gt;Everything in this section was checked on Google's own pages on 2026-09-17.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Search JSON API (closing)
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;overview page&lt;/a&gt; states that the Custom Search JSON API is closed to new customers and that existing customers have until January 1, 2027 to transition. It returns JSON results for a query against a Programmable Search Engine you configure. For existing customers it lists 100 free queries per day, then $5 per 1,000 queries, up to 10,000 queries per day. The &lt;a href="https://developers.google.com/custom-search/v1/reference/rest/v1/cse/list" rel="noopener noreferrer"&gt;cse.list reference&lt;/a&gt; limits each request to 10 results and a query to 100 results in total.&lt;/p&gt;

&lt;p&gt;Even for customers who have it, it was never Google.com as data. Google's &lt;a href="https://support.google.com/programmable-search/answer/70392?hl=en" rel="noopener noreferrer"&gt;help page&lt;/a&gt; says an engine configured to search the entire web is limited to a subset of the Google Web Search corpus, emphasizes results from your own sites, and lacks features such as Oneboxes, real-time results and universal search. Migration options are covered in &lt;a href="https://answerline.dev/blog/google-custom-search-api-alternatives" rel="noopener noreferrer"&gt;Google Custom Search JSON API alternatives&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Programmable Search Engine
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developers.google.com/custom-search/docs/overview" rel="noopener noreferrer"&gt;Programmable Search Engine&lt;/a&gt; lets you create a search engine for your website, blog or a collection of websites. Since January 20, 2026, per the &lt;a href="https://support.google.com/programmable-search/answer/12397162?hl=en" rel="noopener noreferrer"&gt;help page&lt;/a&gt;, new engines must use "Sites to search", covering up to 50 designated domains; existing engines set to "Search the entire web" can keep that setting until January 1, 2027. It is a site search product, not a web search API.&lt;/p&gt;

&lt;p&gt;The related Custom Search Site Restricted JSON API &lt;a href="https://developers.google.com/custom-search/v1/site_restricted_api" rel="noopener noreferrer"&gt;ceased to serve traffic on January 8, 2025&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Search (formerly Vertex AI Search)
&lt;/h3&gt;

&lt;p&gt;Google's Custom Search overview names Vertex AI Search as an alternative for searching up to 50 domains, and asks customers needing full web search to contact Google. Google Cloud's &lt;a href="https://docs.cloud.google.com/generative-ai-app-builder/docs/introduction" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; now calls the product Agent Search (formerly Vertex AI Search): enterprise search and recommendations over your public websites and data stores with your own data. It is the right tool for searching content you own. It is not described as a way to query Google's public web results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grounding with Google Search (Gemini API)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/google-search" rel="noopener noreferrer"&gt;Grounding with Google Search&lt;/a&gt; connects a Gemini model to real-time web content. The model decides what to search, and the response contains the synthesized answer with citations and the queries the model ran; the documentation also refers to usage requirements for displaying search suggestions. The &lt;a href="https://ai.google.dev/gemini-api/docs/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; lists, for Gemini 3.x models, 5,000 free search requests per month shared across those models, then $14 per 1,000 requests, with each search query the model executes billed; Gemini 2.5 models are billed per grounded prompt.&lt;/p&gt;

&lt;p&gt;This is useful when your end goal is an answer for a user. It does not give you positions, a list of ten results, or the modules on the page, so it cannot answer "where do we rank" or "what does the AI Overview say".&lt;/p&gt;

&lt;h2&gt;
  
  
  SERP APIs
&lt;/h2&gt;

&lt;p&gt;A SERP API runs the query on Google for a chosen market, language, location and device, parses the page, and returns JSON. Pricing models differ (monthly plans, pay-as-you-go per 1,000, credits), and so do the fields each one extracts. Examples with prices from their own pages, checked 2026-09-17:&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;Published price&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;Free: 250 searches/month; Starter $25/month for 1,000; Developer $75/month for 5,000&lt;/td&gt;
&lt;td&gt;&lt;a href="https://serpapi.com/pricing" rel="noopener noreferrer"&gt;serpapi.com/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataForSEO (Google Organic)&lt;/td&gt;
&lt;td&gt;Standard queue $0.6 per 1K SERPs; Priority $1.2 per 1K; Live $2 per 1K&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dataforseo.com/pricing/serp/google-organic-serp-api" rel="noopener noreferrer"&gt;dataforseo.com pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serper&lt;/td&gt;
&lt;td&gt;2,500 free queries, no credit card&lt;/td&gt;
&lt;td&gt;&lt;a href="https://serper.dev/" rel="noopener noreferrer"&gt;serper.dev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Broader comparisons: &lt;a href="https://answerline.dev/blog/best-serp-apis" rel="noopener noreferrer"&gt;best SERP APIs&lt;/a&gt;, &lt;a href="https://answerline.dev/blog/cheapest-serp-api" rel="noopener noreferrer"&gt;cheapest SERP API&lt;/a&gt;, &lt;a href="https://answerline.dev/blog/best-google-scraper" rel="noopener noreferrer"&gt;best Google scraper&lt;/a&gt;, and &lt;a href="https://answerline.dev/compare/serpapi" rel="noopener noreferrer"&gt;SerpApi compared&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this API returns
&lt;/h3&gt;

&lt;p&gt;A request to this API's &lt;code&gt;POST /v1/monitor/google&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.answerline.dev/v1/monitor/google &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANSWERLINE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "query": "crm for small business", "country": "US", "pages": 2, "include": { "aioverview": { "markdown": true } } }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returns &lt;code&gt;result.organicResults[]&lt;/code&gt; (&lt;code&gt;position&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt;, &lt;code&gt;displayedLink&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;sitelinks&lt;/code&gt;), &lt;code&gt;peopleAlsoAsk[]&lt;/code&gt;, &lt;code&gt;relatedSearches[]&lt;/code&gt;, &lt;code&gt;ads[]&lt;/code&gt;, and, when Google shows them, &lt;code&gt;localResults[]&lt;/code&gt;, &lt;code&gt;shoppingCards[]&lt;/code&gt;, &lt;code&gt;knowledgeGraph&lt;/code&gt;, &lt;code&gt;peopleAreSaying[]&lt;/code&gt; and the &lt;code&gt;aioverview&lt;/code&gt; with &lt;code&gt;sources[]&lt;/code&gt; and &lt;code&gt;citationPills[]&lt;/code&gt;. Targeting fields are &lt;code&gt;country&lt;/code&gt; or &lt;code&gt;gl&lt;/code&gt;, &lt;code&gt;hl&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt; or &lt;code&gt;uule&lt;/code&gt;, &lt;code&gt;device&lt;/code&gt; and &lt;code&gt;pages&lt;/code&gt; (1 to 10); alternatively, pass a full Google search URL in &lt;code&gt;url&lt;/code&gt;. See &lt;a href="https://answerline.dev/blog/google-search-parameters" rel="noopener noreferrer"&gt;Google search parameters&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Python, with &lt;code&gt;requests&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.answerline.dev/v1/monitor/google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ANSWERLINE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm for small business&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organicResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript, with &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.answerline.dev/v1/monitor/google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANSWERLINE_API_KEY&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm for small business&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organicResults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cost: a Google Search async task is 3 credits, each page after the first 2 more, &lt;code&gt;include.aioverview&lt;/code&gt; or &lt;code&gt;include.paaAioverview&lt;/code&gt; 2 more once, and a synchronous call like the ones above adds 2. The curl request above (2 pages with AI Overview, synchronous) is 3 + 2 + 2 + 2 = 9 credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building your own scraper
&lt;/h2&gt;

&lt;p&gt;Building in-house means you own every part of the pipeline: request infrastructure, market and location targeting, parsers for each SERP module, change detection, retries and storage. The ongoing costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layouts change.&lt;/strong&gt; Modules are added, renamed and restructured; every change is a silent parser break until you detect it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameters change.&lt;/strong&gt; In September 2025, &lt;a href="https://www.seroundtable.com/google-search-drops-100-results-parameter-40097.html" rel="noopener noreferrer"&gt;Search Engine Roundtable&lt;/a&gt; reported that the &lt;code&gt;&amp;amp;num=100&lt;/code&gt; parameter stopped working and that this broke most Google rank trackers. A scraper built on 100 results per request has to switch to paging when that happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terms and legal exposure.&lt;/strong&gt; Review Google's terms of service and your jurisdiction's rules with counsel before collecting data at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational load.&lt;/strong&gt; Queues, rate control, monitoring and on-call for a system that is not your product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting around bot detection is out of scope here and is not a sound basis for a business. For a structured build-versus-buy analysis, see &lt;a href="https://answerline.dev/compare/build-vs-buy" rel="noopener noreferrer"&gt;build vs buy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Custom Search JSON API&lt;/th&gt;
&lt;th&gt;Programmable Search / Agent Search&lt;/th&gt;
&lt;th&gt;Grounding with Google Search&lt;/th&gt;
&lt;th&gt;SERP API&lt;/th&gt;
&lt;th&gt;Own scraper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Available to new customers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data source&lt;/td&gt;
&lt;td&gt;Subset of Google's corpus via a configured engine&lt;/td&gt;
&lt;td&gt;Your sites, up to 50 domains, your data&lt;/td&gt;
&lt;td&gt;Google Search, used by the model&lt;/td&gt;
&lt;td&gt;Google's results page&lt;/td&gt;
&lt;td&gt;Google's results page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Up to 10 links per request&lt;/td&gt;
&lt;td&gt;Search results over your content&lt;/td&gt;
&lt;td&gt;Generated answer with citations&lt;/td&gt;
&lt;td&gt;Structured SERP JSON&lt;/td&gt;
&lt;td&gt;Whatever you parse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positions and SERP modules&lt;/td&gt;
&lt;td&gt;No modules&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;If you build them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Overview content&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Depends on provider&lt;/td&gt;
&lt;td&gt;If you build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance on your side&lt;/td&gt;
&lt;td&gt;Low, until January 1, 2027&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Evaluating a SERP API
&lt;/h2&gt;

&lt;p&gt;Provider pages look alike. Run the same test on each candidate before you commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build a query set that reflects your use.&lt;/strong&gt; 50 to 200 queries from your own workload across your markets, languages and devices, including local-intent queries if you track locations and a few queries where you know an AI Overview appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check field coverage, not just organic results.&lt;/strong&gt; For each response, record whether People Also Ask, ads (with their block position), local results, the knowledge graph and the AI Overview with its sources are present when Google shows them. A missing module is data you cannot report on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check targeting.&lt;/strong&gt; Confirm the provider can set country, interface language, city-level location and device independently. City targeting by canonical location name or &lt;code&gt;uule&lt;/code&gt; matters for local work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check depth.&lt;/strong&gt; Since results come in pages of about ten, ask how the provider handles depth (pages per request, pricing per page) and whether results carry page and position numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check failure semantics.&lt;/strong&gt; Are failed requests charged? Is there an async mode with webhooks, batch submission and idempotency keys, so retries do not create duplicate charges? See &lt;a href="https://answerline.dev/blog/idempotency-keys-explained" rel="noopener noreferrer"&gt;idempotency keys&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price the whole workload.&lt;/strong&gt; Multiply queries by markets, devices, pages and frequency, add optional features such as the AI Overview, and compare the monthly total rather than the headline price per 1,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the contract.&lt;/strong&gt; An OpenAPI document lets you generate clients and validate responses; undocumented fields tend to change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For this API, the fields, limits and error shapes are in the &lt;a href="https://answerline.dev/docs/reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search box for your own site or docs:&lt;/strong&gt; Programmable Search Engine or Agent Search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chatbot or agent that needs current facts with citations:&lt;/strong&gt; Grounding with Google Search, or a SERP API if you want to choose which results reach the model. See &lt;a href="https://answerline.dev/blog/best-search-api-for-ai-agents" rel="noopener noreferrer"&gt;search APIs for AI agents&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rank tracking, SERP feature monitoring, AI Overview tracking:&lt;/strong&gt; a SERP API. See &lt;a href="https://answerline.dev/blog/google-rank-tracking-api" rel="noopener noreferrer"&gt;Google rank tracking API&lt;/a&gt;, &lt;a href="https://answerline.dev/blog/google-ai-overview-api" rel="noopener noreferrer"&gt;Google AI Overview API&lt;/a&gt; and &lt;a href="https://answerline.dev/blog/serp-features-change-tracking" rel="noopener noreferrer"&gt;SERP feature change tracking&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword research from live SERPs:&lt;/strong&gt; a SERP API that returns People Also Ask and related searches; see &lt;a href="https://answerline.dev/blog/people-also-ask-keyword-research" rel="noopener noreferrer"&gt;People Also Ask keyword research&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search data is your core product and you have a team for it:&lt;/strong&gt; consider building, knowing the maintenance and legal work involved.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treating Custom Search results as Google rankings. Google itself says the corpus and features differ.&lt;/li&gt;
&lt;li&gt;Using grounding to measure visibility. You see the model's answer, not the results page.&lt;/li&gt;
&lt;li&gt;Planning around undocumented URL parameters. &lt;code&gt;num=100&lt;/code&gt; stopped working without notice; anything Google does not document can change.&lt;/li&gt;
&lt;li&gt;Comparing providers on price alone. Check which fields each returns for your queries: an API that omits the AI Overview or local pack is cheaper because it gives you less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related: &lt;a href="https://answerline.dev/blog/google-search-operators" rel="noopener noreferrer"&gt;Google search operators&lt;/a&gt; and &lt;a href="https://answerline.dev/blog/bing-search-api-alternatives" rel="noopener noreferrer"&gt;Bing Search API alternatives&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Try a request from the &lt;a href="https://answerline.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>google</category>
      <category>api</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to get Google News results as structured JSON</title>
      <dc:creator>Richard Nemeth</dc:creator>
      <pubDate>Sun, 27 Sep 2026 16:20:07 +0000</pubDate>
      <link>https://dev.to/answerline/how-to-get-google-news-results-as-structured-json-2pj</link>
      <guid>https://dev.to/answerline/how-to-get-google-news-results-as-structured-json-2pj</guid>
      <description>&lt;p&gt;The &lt;a href="https://answerline.dev/engines/google-news" rel="noopener noreferrer"&gt;Google News API&lt;/a&gt; takes a query and returns the articles Google News shows for it, in order, as data: headline, publisher, link, snippet, date and thumbnail, for a chosen country and language. Below: the request and response, date normalization, storage and deduplication, and three patterns (brand alerts, coverage share, adverse media screening).&lt;/p&gt;

&lt;p&gt;If you are deciding between RSS feeds and structured results, read &lt;a href="https://answerline.dev/blog/google-news-rss" rel="noopener noreferrer"&gt;Google News RSS feeds: what they give you and where they stop&lt;/a&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;News result&lt;/strong&gt;: one article on the Google News results page for a query, returned as an item of &lt;code&gt;result.newsResults[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position&lt;/strong&gt;: the 1-indexed order of the article in the news results, returned with the &lt;code&gt;page&lt;/code&gt; it appeared on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: the publisher name as Google displays it, such as a newspaper or a trade site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage share&lt;/strong&gt;: the fraction of news results for a query set that belong to a given publisher or mention a given brand.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.answerline.dev/v1/monitor/google/news &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANSWERLINE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "query": "acme corp recall",
    "country": "DE",
    "hl": "de",
    "pages": 2
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string, 1-10,000 chars&lt;/td&gt;
&lt;td&gt;Required. The News search.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;ISO 3166-1 alpha-2. Send &lt;code&gt;country&lt;/code&gt; or &lt;code&gt;gl&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Google's name for the result geography; same codes, either case. Different values in &lt;code&gt;country&lt;/code&gt; and &lt;code&gt;gl&lt;/code&gt; are a 400.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Interface language (&lt;code&gt;en&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;pt-BR&lt;/code&gt;). Overrides the language derived from the country.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer 1-10&lt;/td&gt;
&lt;td&gt;Default 1.&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;boolean&lt;/td&gt;
&lt;td&gt;Not available on Google News: a request that sets it is refused with a 400.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;News targeting is by country and language. Supported countries are listed by &lt;code&gt;GET /v1/countries?model=google&lt;/code&gt;. Don't route News through the Google Search endpoint's &lt;code&gt;url&lt;/code&gt; shape: a Google URL carrying &lt;code&gt;tbm=nws&lt;/code&gt; is rejected there, because News has its own endpoint and pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"newsResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Corp expands recall to second product line"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://news.example.de/acme-recall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The company said on Tuesday that..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Nachrichten"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 hours ago"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"thumbnail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://news.example.de/images/acme.jpg"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Field by field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;position&lt;/code&gt;&lt;/strong&gt;: 1-indexed rank in the news results. With &lt;code&gt;pages&lt;/code&gt; above 1, &lt;code&gt;page&lt;/code&gt; tells you which page the article came from; store both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;&lt;/strong&gt;: the headline and the text excerpt shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;link&lt;/code&gt;&lt;/strong&gt;: the article URL. If Google served it as a redirect, &lt;code&gt;redirectLink&lt;/code&gt; carries Google's redirect next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;source&lt;/code&gt;&lt;/strong&gt;: the publisher's display name. It is a name, not a domain; derive the domain from &lt;code&gt;link&lt;/code&gt; when you need a stable key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;date&lt;/code&gt;&lt;/strong&gt;: the date text as displayed, for example &lt;code&gt;"2 hours ago"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;thumbnail&lt;/code&gt;&lt;/strong&gt;: the image URL when one is shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;html[]&lt;/code&gt;&lt;/strong&gt;: not returned for Google News; &lt;code&gt;include.html&lt;/code&gt; is refused on this endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Normalizing dates
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;date&lt;/code&gt; is display text, such as &lt;code&gt;"2 hours ago"&lt;/code&gt;, so do not assume one format. Store three things: the raw string, the capture time, and a parsed estimate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;UNITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hour&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;published_estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parse English relative dates like &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3 hours ago&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. Returns None for formats it doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t know.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;m&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(\d+)\s+(minute|hour|day|week)s?\s+ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&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="nf"&gt;lower&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;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;captured_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;UNITS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&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;2&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&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="n"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;published_estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3 hours ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Display text can depend on the interface language you request with &lt;code&gt;hl&lt;/code&gt;, so a parser written for English may not match other languages. Rather than parse every format, keep &lt;code&gt;None&lt;/code&gt; for the unknown ones and fall back to the first time you saw the article: for alerting, "first seen" is usually the timestamp that matters anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing and deduplicating articles
&lt;/h2&gt;

&lt;p&gt;The same article comes back on every run while it stays in the results. Split the data into what an article is and where it appeared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;news_articles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;url_key&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- normalized link&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;first_seen&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;                &lt;span class="c1"&gt;-- estimate, nullable&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;news_observations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;run_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;country&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;url_key&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;news_articles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;position&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;    &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;       &lt;span class="nb"&gt;int&lt;/span&gt;    &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_raw&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;captured_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url_key&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;Normalize &lt;code&gt;link&lt;/code&gt; into &lt;code&gt;url_key&lt;/code&gt; by lowercasing the host, dropping &lt;code&gt;www.&lt;/code&gt;, removing the fragment and common tracking parameters (&lt;code&gt;utm_*&lt;/code&gt;), and trimming a trailing slash. Do not strip all query parameters: some publishers identify articles by them.&lt;/p&gt;

&lt;p&gt;With this split, "new article" is an insert into &lt;code&gt;news_articles&lt;/code&gt;, and "article moved up" is a comparison of &lt;code&gt;position&lt;/code&gt; between two observations of the same &lt;code&gt;url_key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing queries that return what you mean
&lt;/h2&gt;

&lt;p&gt;The query is the largest source of noise in news monitoring. Quotes for exact matches and &lt;code&gt;-&lt;/code&gt; for exclusions are both documented in Google's &lt;a href="https://support.google.com/websearch/answer/2466433" rel="noopener noreferrer"&gt;Refine web searches&lt;/a&gt; help page (checked 2026-09-17). A few rules keep result sets reviewable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quote multi-word names.&lt;/strong&gt; &lt;code&gt;"acme corp"&lt;/code&gt; asks for the exact phrase; without quotes the two words are searched as separate terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude known collisions.&lt;/strong&gt; If your brand shares a name with a place, a film or a sports team, add &lt;code&gt;-&lt;/code&gt; terms for the collision (&lt;code&gt;"acme" -cartoon&lt;/code&gt;) and review what the exclusion removes on the first few runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One intent per query.&lt;/strong&gt; Keep "brand news", "brand plus product", and "brand plus risk terms" as separate queries so each result set answers one question and can have its own schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the market's language.&lt;/strong&gt; Send local-language names and terms with the matching &lt;code&gt;hl&lt;/code&gt; for each market; a translated English query can miss how local outlets write about you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the query list.&lt;/strong&gt; Store a version with every observation, so a change in results can be separated from a change in the query.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pattern 1: brand and competitor alerts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write one query per entity and variant: the brand name in quotes, the brand plus key product names, and the executives' names if they are public figures.&lt;/li&gt;
&lt;li&gt;Run each query per market as an async task on a schedule.&lt;/li&gt;
&lt;li&gt;On each webhook, insert new articles; alert on inserts whose &lt;code&gt;title&lt;/code&gt; or &lt;code&gt;snippet&lt;/code&gt; matches the entity.&lt;/li&gt;
&lt;li&gt;Suppress repeats by &lt;code&gt;url_key&lt;/code&gt;, and group alerts by &lt;code&gt;source&lt;/code&gt; so one story syndicated across many outlets arrives as one message with a list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quoted phrases and operators such as &lt;code&gt;-&lt;/code&gt; go inside &lt;code&gt;query&lt;/code&gt; exactly as you would type them into Google. &lt;a href="https://answerline.dev/blog/google-search-operators" rel="noopener noreferrer"&gt;Google search operators&lt;/a&gt; lists the ones worth using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: coverage share
&lt;/h2&gt;

&lt;p&gt;Coverage share answers "who owns the news results for our topics?". For a fixed query set, count results per publisher domain or per brand mention and divide by all results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publisher_share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;results: newsResults items from one run of a query set.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&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;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limit to the first page or the top 10 positions so a query with more results does not dominate. Track the share weekly: a PR team sees whether coverage is concentrated in a few outlets, and a publisher sees which competitors win the topics it covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: adverse media screening
&lt;/h2&gt;

&lt;p&gt;Compliance and risk teams screen names against negative news. The shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each entity, combine the name with risk terms in separate queries (&lt;code&gt;"Jane Example" fraud&lt;/code&gt;, &lt;code&gt;"Jane Example" lawsuit&lt;/code&gt;), because a long &lt;code&gt;OR&lt;/code&gt; chain returns a mixed set that is harder to review.&lt;/li&gt;
&lt;li&gt;Run in each country where the entity operates, with the local &lt;code&gt;hl&lt;/code&gt;, since local-language coverage often appears there first.&lt;/li&gt;
&lt;li&gt;Store every observation, not only matches: an auditor may ask what the screen returned on a given date.&lt;/li&gt;
&lt;li&gt;Route matches to a reviewer; do not auto-decide on a headline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://answerline.dev/use-cases/adverse-media-screening" rel="noopener noreferrer"&gt;adverse media screening use case&lt;/a&gt; describes the workflow end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling
&lt;/h2&gt;

&lt;p&gt;News moves quickly, but not every query needs the same frequency. A practical split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query type&lt;/th&gt;
&lt;th&gt;Suggested interval&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active incident or launch&lt;/td&gt;
&lt;td&gt;15-60 minutes&lt;/td&gt;
&lt;td&gt;Pickup speed matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand and executive names&lt;/td&gt;
&lt;td&gt;Every few hours&lt;/td&gt;
&lt;td&gt;Catch stories the same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Topic and competitor coverage&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Share changes slowly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screening of third parties&lt;/td&gt;
&lt;td&gt;Daily or weekly&lt;/td&gt;
&lt;td&gt;Driven by policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Send each run as one batch of async tasks (up to 500 per request) with a webhook, so nothing polls. Use an &lt;code&gt;idempotencyKey&lt;/code&gt; built from the run slot, query id and market, so a retried scheduler run creates no duplicates. See &lt;a href="https://answerline.dev/blog/sync-async-webhooks" rel="noopener noreferrer"&gt;sync, async and webhooks&lt;/a&gt; and &lt;a href="https://answerline.dev/docs/async" rel="noopener noreferrer"&gt;async tasks&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.answerline.dev/v1/async/task/batch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANSWERLINE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[
    { "taskType": "GOOGLE_NEWS", "idempotencyKey": "news-2026-09-15T10-acme-de",
      "payload": { "query": "\"acme corp\"", "country": "DE", "hl": "de" },
      "webhook": { "url": "https://your-app.example/hooks/news" } },
    { "taskType": "GOOGLE_NEWS", "idempotencyKey": "news-2026-09-15T10-acme-us",
      "payload": { "query": "\"acme corp\"", "country": "US" },
      "webhook": { "url": "https://your-app.example/hooks/news" } }
  ]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost in credits
&lt;/h2&gt;

&lt;p&gt;From the current credit table (see &lt;a href="https://answerline.dev/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt; for credit prices):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google News task, 1 page&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google News task, 3 pages&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google News synchronous call, 1 page&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A budget is queries × markets × runs per month × credits per run. 50 queries in 2 markets, hourly (about 720 runs a month), one page as async tasks: 50 × 2 × 720 × 2 = 144,000 credits. The same set every 6 hours is 24,000. Frequency is the largest lever, so match it to the table above rather than running everything at the fastest interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treating &lt;code&gt;source&lt;/code&gt; as a key.&lt;/strong&gt; Publisher display names change and collide. Key on the normalized link domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing dates in one language.&lt;/strong&gt; The date text follows &lt;code&gt;hl&lt;/code&gt;. Keep the raw string and a first-seen timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting on every run.&lt;/strong&gt; Alert on new &lt;code&gt;url_key&lt;/code&gt; inserts, not on every observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using long OR chains.&lt;/strong&gt; Split risk terms into separate queries so each result set is reviewable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Field reference: &lt;a href="https://answerline.dev/engines/google-news" rel="noopener noreferrer"&gt;Google News engine page&lt;/a&gt;. More patterns: &lt;a href="https://answerline.dev/use-cases/news-monitoring" rel="noopener noreferrer"&gt;news monitoring&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>news</category>
    </item>
    <item>
      <title>What in-house AI visibility tracking costs to run</title>
      <dc:creator>Richard Nemeth</dc:creator>
      <pubDate>Sun, 27 Sep 2026 16:16:05 +0000</pubDate>
      <link>https://dev.to/answerline/what-in-house-ai-visibility-tracking-costs-to-run-3h17</link>
      <guid>https://dev.to/answerline/what-in-house-ai-visibility-tracking-costs-to-run-3h17</guid>
      <description>&lt;p&gt;The cost of collecting AI answers in-house is rarely the API bill you avoid. It is the engineering time to reach production quality on each engine, plus the maintenance that follows for as long as you run it. Building is the right call when the collection layer is itself your product or you need data no provider returns; the &lt;a href="https://answerline.dev/compare/build-vs-buy" rel="noopener noreferrer"&gt;build vs buy&lt;/a&gt; comparison covers that decision, and the framework below fills in the numbers behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Collection layer&lt;/strong&gt;: everything between "we have a prompt" and "we have a structured record of what the engine answered": execution, parsing, retries and storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis layer&lt;/strong&gt;: what you build on top: share of voice, citation tracking, competitor reports, alerts. This is where customers compare products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt;: one prompt, on one engine, in one market, collected once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What building involves
&lt;/h2&gt;

&lt;p&gt;Consumer AI products such as ChatGPT, Perplexity, Gemini, Copilot, Grok and Google AI Mode show answers that include web search, citations, product cards and local results. Those surfaces live in the consumer applications, not in the model providers' developer APIs. Each of the components below exists once per engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Collection runs
&lt;/h3&gt;

&lt;p&gt;You run automated collection against each product and keep throughput inside what each product allows. That is infrastructure to provision, scale, patch and monitor, sized for your peak run.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Parsing that keeps up with interface changes
&lt;/h3&gt;

&lt;p&gt;Each engine renders answers differently: inline citations, grouped sources, shopping cards, maps, follow-up queries. Each needs its own parser, a fixture set, and tests. When an interface changes, the failure can be silent: the run succeeds and a field such as &lt;code&gt;sources&lt;/code&gt; comes back empty. Catching that takes data-quality monitoring per field, per engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Market coverage
&lt;/h3&gt;

&lt;p&gt;Answers differ by country, and for local intent by region. Reporting per market requires collecting from each market and validating that the stored answers reflect it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Queueing, retries and storage
&lt;/h3&gt;

&lt;p&gt;At volume, partial failure is the normal case. You need a queue with priorities, retries that cannot create duplicates, per-run deadlines, result storage and a way to push results to downstream systems. That is a distributed-systems project in its own right.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Terms, compliance and review
&lt;/h3&gt;

&lt;p&gt;Each consumer product has its own terms of service. Collecting from them at volume is a decision for your legal and compliance review, per engine, before engineering starts, and again when terms change.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. On-call
&lt;/h3&gt;

&lt;p&gt;Interfaces change without notice and on no schedule. Somebody owns the pager for each engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cost framework you can fill in
&lt;/h2&gt;

&lt;p&gt;Team cost, engine count and quality bar vary too much for universal numbers. Fill in this table with your own.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Your number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Engines you need&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Engineer-months to reach production quality on one engine&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;Engineer-months of maintenance per engine, per month&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Loaded cost of one engineer-month&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;Monthly infrastructure: compute, storage, monitoring&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;One-off review and compliance cost&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;Months you plan to run it&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build, cost over T months = &lt;code&gt;E × B × C + E × M × C × T + I × T + R&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Buy, cost over T months = the monthly price of the plan that covers your credits (below) × T&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adjust for three effects the formulas miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build is paid before value. &lt;code&gt;E × B&lt;/code&gt; months pass before the first reliable dataset; for a product that competes on time to market, that delay has its own cost.&lt;/li&gt;
&lt;li&gt;Maintenance scales with engines, not volume. A fifth engine adds a fifth parser and a fifth pager rotation, even if you run few prompts on it.&lt;/li&gt;
&lt;li&gt;Opportunity cost. Every engineer-month in the collection layer is one not spent on the analysis layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Computing the buy side
&lt;/h2&gt;

&lt;p&gt;Buying is priced in credits per request. As async tasks, the base price per request is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Credits per request&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Mode&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Search&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google News&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Perplexity and Grok are paused for now and coming back soon; requests to them are refused and not billed. Synchronous calls add 2 credits each. Some options add to the price: on ChatGPT, +2 once if &lt;code&gt;include.rawResponse&lt;/code&gt; or &lt;code&gt;shopping&lt;/code&gt; is on (&lt;code&gt;searchQueries&lt;/code&gt; and &lt;code&gt;ads&lt;/code&gt; are free); on AI Mode, +1 per expanded product cluster, up to 6; on Google Search, +2 per extra page and +2 once for the AI Overview; on Google News, +2 per extra page. Credits are reserved at the maximum and charged at the actual cost on success; failed requests are charged nothing, and queued tasks you clear are never charged. The details are in &lt;a href="https://answerline.dev/docs/credits" rel="noopener noreferrer"&gt;credits&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worked example 1: a large program
&lt;/h3&gt;

&lt;p&gt;20,000 prompts on ChatGPT and Gemini in the US, plus 2,000 Google News queries in 10 markets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs per sweep: 20,000 × 2 + 2,000 × 10 = 60,000.&lt;/li&gt;
&lt;li&gt;Credits for the assistants: 20,000 × (5 + 4) = 180,000.&lt;/li&gt;
&lt;li&gt;Credits for Google News: 2,000 × 10 × 2 = 40,000.&lt;/li&gt;
&lt;li&gt;Credits per full sweep, async, no add-ons: 180,000 + 40,000 = 220,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A weekly sweep (4 a month) is 880,000 credits, inside the Growth plan's 1,350,000. A daily sweep is 6,600,000 a month and needs an Enterprise plan. Plan prices and a volume estimator are on &lt;a href="https://answerline.dev/pricing" rel="noopener noreferrer"&gt;the pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worked example 2: a focused program
&lt;/h3&gt;

&lt;p&gt;200 prompts on ChatGPT and Gemini in the US, plus 50 Google News queries in 3 markets, weekly (4 runs a month):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credits per prompt: 5 + 4 = 9.&lt;/li&gt;
&lt;li&gt;Per sweep: 200 × 9 + 50 × 3 × 2 = 1,800 + 300 = 2,100 credits.&lt;/li&gt;
&lt;li&gt;Per month: 2,100 × 4 = 8,400 credits, inside the Lite plan's 37,500.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unused monthly credits do not roll over, so size the plan to your steady cadence, not your peak. The &lt;a href="https://answerline.dev/blog/ai-monitoring-cost-planning" rel="noopener noreferrer"&gt;cost planning guide&lt;/a&gt; covers sampling and cadence choices that change these numbers, and &lt;a href="https://answerline.dev/blog/sampling-vs-census-ai-monitoring" rel="noopener noreferrer"&gt;sampling vs census&lt;/a&gt; covers when you can run fewer prompts without losing the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the buy side returns
&lt;/h2&gt;

&lt;p&gt;Compare the output as well as the price. Each engine returns structured JSON under &lt;code&gt;result&lt;/code&gt;, with fields that differ by engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All AI engines: &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;sources[]&lt;/code&gt;, and &lt;code&gt;markdown&lt;/code&gt; with &lt;code&gt;include.markdown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;ChatGPT: &lt;code&gt;citationPills[]&lt;/code&gt;, &lt;code&gt;entities[]&lt;/code&gt;, &lt;code&gt;shoppingCards[]&lt;/code&gt; (with &lt;code&gt;include.shopping&lt;/code&gt;), &lt;code&gt;map[]&lt;/code&gt;. The &lt;code&gt;searchQueries[]&lt;/code&gt; (free with &lt;code&gt;include.searchQueries&lt;/code&gt;), &lt;code&gt;inlineProducts[]&lt;/code&gt; (with &lt;code&gt;include.shopping&lt;/code&gt;) and &lt;code&gt;ads[]&lt;/code&gt; (with &lt;code&gt;include.ads&lt;/code&gt;) fields exist but come back empty on the answers served today.&lt;/li&gt;
&lt;li&gt;Gemini: &lt;code&gt;citationPills[]&lt;/code&gt;, &lt;code&gt;places[]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For volume, tasks go through &lt;code&gt;POST /v1/async/task/batch&lt;/code&gt; (up to 500 per request) with idempotency keys and webhooks. Retries, deadlines and queueing are part of the API contract rather than code you maintain; see &lt;a href="https://answerline.dev/docs/async" rel="noopener noreferrer"&gt;async tasks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you still build when you buy
&lt;/h2&gt;

&lt;p&gt;Buying the collection layer moves engineering to the parts that differ between products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt set and schedule. Which prompts, engines and markets, and how often. &lt;a href="https://answerline.dev/blog/prompt-set-design" rel="noopener noreferrer"&gt;Prompt set design&lt;/a&gt; and &lt;a href="https://answerline.dev/blog/ai-monitoring-cadence" rel="noopener noreferrer"&gt;monitoring cadence&lt;/a&gt; cover the choices.&lt;/li&gt;
&lt;li&gt;Submission and retries. Batching, idempotency keys derived from each task's meaning, and a retry policy that never duplicates work; see &lt;a href="https://answerline.dev/blog/idempotency-keys-explained" rel="noopener noreferrer"&gt;idempotency keys&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A webhook receiver. Signature verification over the raw body, deduplication by &lt;code&gt;task.id&lt;/code&gt;, and storage; see &lt;a href="https://answerline.dev/blog/verify-webhook-signatures" rel="noopener noreferrer"&gt;verifying webhook signatures&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Normalization. Field names differ by engine (&lt;code&gt;shopping_cards&lt;/code&gt; on Perplexity, &lt;code&gt;shoppingCards&lt;/code&gt; on ChatGPT; citation ids 0-based on ChatGPT and 1-based elsewhere), so map them into your own schema once.&lt;/li&gt;
&lt;li&gt;The analysis layer. Mentions, citations, share of voice and reports, which is what customers pay for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SDKs and workflow integrations are listed under &lt;a href="https://answerline.dev/docs/integrations" rel="noopener noreferrer"&gt;integrations&lt;/a&gt;; for AI agents, the &lt;a href="https://answerline.dev/integrations/mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; exposes each engine as a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision criteria
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Lean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Answers feed an analytics, SEO or visibility product&lt;/td&gt;
&lt;td&gt;Buy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need several engines and several markets&lt;/td&gt;
&lt;td&gt;Buy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first reliable dataset matters&lt;/td&gt;
&lt;td&gt;Buy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collection itself is the product you sell&lt;/td&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need a field no provider returns&lt;/td&gt;
&lt;td&gt;Build that part, scoped to one engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One engine, low volume, and spare engineering capacity&lt;/td&gt;
&lt;td&gt;Either; compare with the framework&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A hybrid also works: buy the engines and markets a provider covers, and build narrowly where you need something specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate in a week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick your ten hardest prompts: the ones with shopping, local or citation-heavy answers that break parsers.&lt;/li&gt;
&lt;li&gt;Run them on every engine you need. The free tier (500 credits, once, no card) works on every live engine, so you can evaluate the assistants before paying: 10 prompts as synchronous calls on ChatGPT and Gemini cost 10 × (9 + 2 × 2) = 130 credits. As async tasks, 10 × 9 = 90.&lt;/li&gt;
&lt;li&gt;Compare field by field against what your pipeline or manual checks show: citations present, ordering, product cards, places.&lt;/li&gt;
&lt;li&gt;Check the failure path: send a malformed request and a burst of requests, and confirm your code handles 422, 429 and batch item errors as described in &lt;a href="https://answerline.dev/blog/api-errors-and-retries" rel="noopener noreferrer"&gt;the errors post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Price your volume with the credit math above and your cadence.&lt;/li&gt;
&lt;li&gt;Fill in the build table with your own team's estimates and compare.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pitfalls in the comparison
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Comparing one engine built to several engines bought. Price the scope you need.&lt;/li&gt;
&lt;li&gt;Ignoring maintenance. First-version cost is the smaller term over any horizon longer than a few months.&lt;/li&gt;
&lt;li&gt;Treating developer model APIs as equivalent. A completion from a model API does not include the consumer app's web search, citations or product cards; see &lt;a href="https://answerline.dev/compare/provider-apis" rel="noopener noreferrer"&gt;provider APIs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Pricing at peak cadence. Credits do not roll over; plan for the steady state.&lt;/li&gt;
&lt;li&gt;Skipping the terms review. It belongs at the start of a build plan, not after launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table version of this comparison is at &lt;a href="https://answerline.dev/compare/build-vs-buy" rel="noopener noreferrer"&gt;build vs buy&lt;/a&gt;; to run your first prompts, start with the &lt;a href="https://answerline.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
  </channel>
</rss>
