<?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: MarkoMetrics</title>
    <description>The latest articles on DEV Community by MarkoMetrics (@markometrics).</description>
    <link>https://dev.to/markometrics</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3790131%2F9e068250-c276-4d1b-ae2d-adff0930d878.webp</url>
      <title>DEV Community: MarkoMetrics</title>
      <link>https://dev.to/markometrics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/markometrics"/>
    <language>en</language>
    <item>
      <title>Free SEO &amp; AI Search Audit Chrome Extension | MarkoMetrics</title>
      <dc:creator>MarkoMetrics</dc:creator>
      <pubDate>Sat, 10 Oct 2026 17:24:19 +0000</pubDate>
      <link>https://dev.to/markometrics/free-seo-ai-search-audit-chrome-extension-markometrics-2lj6</link>
      <guid>https://dev.to/markometrics/free-seo-ai-search-audit-chrome-extension-markometrics-2lj6</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR:
&lt;/h2&gt;

&lt;p&gt;The MarkoMetrics Chrome extension lets you audit any web page for on-page SEO, technical SEO, content, and GEO/AEO. The scan takes a few seconds, requires no account, and gives you an overall score with ranked findings. You can also generate fixes using your own OpenAI, Anthropic, or Google Gemini API key. Scanning is free and unlimited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the MarkoMetrics SEO extension?
&lt;/h2&gt;

&lt;p&gt;The MarkoMetrics Chrome extension is a browser-based SEO self-audit tool.&lt;/p&gt;

&lt;p&gt;Open any web page, click the MarkoMetrics icon, and the extension opens a side panel with an overall score, four category scores, and a list of things that need attention.&lt;/p&gt;

&lt;p&gt;Each finding includes a plain-language recommendation. For findings that can be fixed automatically, you can use AI to generate the fix.&lt;/p&gt;

&lt;p&gt;The important part is that you do not have to submit a URL and wait for an audit report. The page is checked directly in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built a free SEO self-audit tool
&lt;/h2&gt;

&lt;p&gt;Most SEO work does not start with a massive audit.&lt;/p&gt;

&lt;p&gt;It starts with a page.&lt;/p&gt;

&lt;p&gt;You want to know whether the title is right, whether the headings make sense, whether important technical elements are missing, and whether the page has enough information for search engines and AI systems to understand it.&lt;/p&gt;

&lt;p&gt;That is where a quick self-audit is useful.&lt;/p&gt;

&lt;p&gt;Traditional SEO platforms can give you much more data, but they can also take time to configure and require a paid subscription. They also tend to focus heavily on traditional search.&lt;/p&gt;

&lt;p&gt;We wanted a simpler workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open a page → scan it → see what is wrong → fix the important issues.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension also checks GEO and AEO signals, so you can look at whether a page is prepared for AI search and answer engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the MarkoMetrics extension check?
&lt;/h2&gt;

&lt;p&gt;The extension checks four areas.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. On-page SEO
&lt;/h3&gt;

&lt;p&gt;The on-page audit checks common elements that affect how a page is understood by search engines, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title tag&lt;/li&gt;
&lt;li&gt;Meta description&lt;/li&gt;
&lt;li&gt;H1 and heading structure&lt;/li&gt;
&lt;li&gt;Image alt text&lt;/li&gt;
&lt;li&gt;Canonical URL&lt;/li&gt;
&lt;li&gt;Internal links&lt;/li&gt;
&lt;li&gt;URL structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of manually checking these elements one by one, you get the findings in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Technical SEO
&lt;/h3&gt;

&lt;p&gt;The technical audit checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta robots&lt;/li&gt;
&lt;li&gt;Viewport&lt;/li&gt;
&lt;li&gt;HTTPS&lt;/li&gt;
&lt;li&gt;Structured data, including JSON-LD and microdata&lt;/li&gt;
&lt;li&gt;Render-blocking scripts&lt;/li&gt;
&lt;li&gt;robots.txt&lt;/li&gt;
&lt;li&gt;XML sitemap&lt;/li&gt;
&lt;li&gt;Broken internal links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension can read the site's publicly available robots.txt and sitemap.xml files to perform these checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Content
&lt;/h3&gt;

&lt;p&gt;The content section looks at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Word count&lt;/li&gt;
&lt;li&gt;Readability&lt;/li&gt;
&lt;li&gt;Keyword density&lt;/li&gt;
&lt;li&gt;Content freshness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks can help identify pages that are too thin, difficult to read, overly focused on a keyword, or simply outdated.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GEO and AEO
&lt;/h3&gt;

&lt;p&gt;This is the part that is often missing from traditional SEO checklists.&lt;/p&gt;

&lt;p&gt;GEO, or Generative Engine Optimization, and AEO, or Answer Engine Optimization, focus on making content easier for AI search and answer engines to read, understand, and potentially cite.&lt;/p&gt;

&lt;p&gt;The extension checks for signals such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FAQ schema&lt;/li&gt;
&lt;li&gt;Question-format headings&lt;/li&gt;
&lt;li&gt;Direct-answer paragraphs&lt;/li&gt;
&lt;li&gt;llms.txt&lt;/li&gt;
&lt;li&gt;AI crawler access&lt;/li&gt;
&lt;li&gt;E-E-A-T signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a competitor keeps appearing in AI-generated answers while your page does not, these checks give you another set of things to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI fixes work
&lt;/h2&gt;

&lt;p&gt;Finding an SEO problem is useful. Getting the actual fix written is better.&lt;/p&gt;

&lt;p&gt;For supported findings, the extension can generate things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rewritten title tags&lt;/li&gt;
&lt;li&gt;Meta descriptions&lt;/li&gt;
&lt;li&gt;FAQ schema&lt;/li&gt;
&lt;li&gt;Direct-answer paragraphs&lt;/li&gt;
&lt;li&gt;Other content fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI features use your own API key.&lt;/p&gt;

&lt;p&gt;You can connect an API key from &lt;strong&gt;OpenAI, Anthropic, or Google Gemini&lt;/strong&gt; in the extension settings. When you generate a fix, the request goes directly from your browser to the provider you selected.&lt;/p&gt;

&lt;p&gt;MarkoMetrics does not add a markup to your AI usage.&lt;/p&gt;

&lt;p&gt;This is a bring-your-own-key setup. You use your own provider account and pay that provider for the API usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to your data?
&lt;/h2&gt;

&lt;p&gt;The extension does not send your scans through a MarkoMetrics backend.&lt;/p&gt;

&lt;p&gt;Your API key and scan results are stored locally in your browser.&lt;/p&gt;

&lt;p&gt;When you run a scan, the page is analyzed in your browser. Page content is only sent outside your device when you click &lt;strong&gt;Generate fix&lt;/strong&gt;. In that case, the relevant text is sent directly to the AI provider configured in your extension.&lt;/p&gt;

&lt;p&gt;The extension does not use a MarkoMetrics server as a middleman for those requests.&lt;/p&gt;

&lt;p&gt;The privacy promise inside the extension is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stored only on this device. Sent directly to the provider when you generate a fix, never to MarkoMetrics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How to install the MarkoMetrics extension
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install the free MarkoMetrics SEO Extension → &lt;a href="https://chromewebstore.google.com/detail/markometrics-plugin-ai-se/feclhjgliegkjdcpefnlkpdgaceafgld" rel="noopener noreferrer"&gt;tinyurl.com/seo-plugin&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After installing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add to Chrome&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the installation.&lt;/li&gt;
&lt;li&gt;Pin the MarkoMetrics icon to your browser toolbar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extension works with Chrome and other Chromium-based browsers, including Edge, Brave, and Arc.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run your first SEO audit
&lt;/h2&gt;

&lt;p&gt;You can audit a page in a few clicks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page you want to check.&lt;/li&gt;
&lt;li&gt;Click the MarkoMetrics extension icon.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Scan&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Review the overall score and the four category scores.&lt;/li&gt;
&lt;li&gt;Open a category to see its findings.&lt;/li&gt;
&lt;li&gt;Fix the issues manually, or add your own AI provider key and use &lt;strong&gt;Generate fix&lt;/strong&gt; on supported findings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Findings are grouped by severity, including &lt;strong&gt;critical, warning, and info&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Checks that already pass are collapsed into a count, so the issues that need work stay visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read the audit results
&lt;/h2&gt;

&lt;p&gt;Do not focus only on the overall score.&lt;/p&gt;

&lt;p&gt;The overall score is calculated from the four categories, which means a page can perform well on traditional SEO checks and still have weaknesses in its AI-search readiness.&lt;/p&gt;

&lt;p&gt;Look at the individual category scores and then open the findings.&lt;/p&gt;

&lt;p&gt;The findings are meant to be specific. Instead of simply telling you to "improve your meta description," the extension shows the current value, explains the issue, and gives you a recommendation.&lt;/p&gt;

&lt;p&gt;You can also export the audit as a &lt;strong&gt;PDF&lt;/strong&gt; or &lt;strong&gt;JSON&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The PDF is useful when you need to share an audit with a client or teammate. The JSON export contains the findings, fix types, and raw page data, making it useful for handing audit data to another tool or AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is the extension for?
&lt;/h2&gt;

&lt;p&gt;The extension is useful if you regularly work on websites and need quick page-level checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-house marketers&lt;/strong&gt; can use it before publishing landing pages or blog posts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO freelancers and consultants&lt;/strong&gt; can run a quick audit before discussing a page with a client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Founders and small teams&lt;/strong&gt; can use it when they do not have a dedicated SEO specialist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO teams working on AI visibility&lt;/strong&gt; can use the GEO and AEO checks to find gaps that traditional page audits may not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the MarkoMetrics SEO extension free?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;The single-page scan is free and unlimited. There is no account requirement and no scan limit.&lt;/p&gt;

&lt;p&gt;AI-generated fixes are also available through the extension without a MarkoMetrics subscription. You provide your own OpenAI, Anthropic, or Google Gemini API key, and your provider charges you for the API usage.&lt;/p&gt;

&lt;p&gt;Future paid features may include saved audit history, multi-domain tracking, and team features. The free scanning functionality is not being turned into a trial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the MarkoMetrics extension really free?
&lt;/h3&gt;

&lt;p&gt;Yes. Page scanning is free and unlimited, with no account required.&lt;/p&gt;

&lt;p&gt;If you generate AI fixes, your selected AI provider may charge you for its API usage. MarkoMetrics does not add a markup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an account?
&lt;/h3&gt;

&lt;p&gt;No. Install the extension and scan a page.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a GEO or AEO audit?
&lt;/h3&gt;

&lt;p&gt;It checks signals related to how well a page can be read, understood, and potentially cited by AI search and answer engines.&lt;/p&gt;

&lt;p&gt;The extension checks things such as FAQ schema, question-based headings, direct-answer paragraphs, llms.txt, AI crawler access, and E-E-A-T signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which AI providers are supported?
&lt;/h3&gt;

&lt;p&gt;The extension supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI&lt;/li&gt;
&lt;li&gt;Anthropic&lt;/li&gt;
&lt;li&gt;Google Gemini&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You provide your own API key for the provider you want to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can MarkoMetrics see my API key?
&lt;/h3&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Your key and scan results are stored locally in your browser. When you generate a fix, the request goes directly to your selected AI provider over HTTPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does an AI-generated fix cost?
&lt;/h3&gt;

&lt;p&gt;Your AI provider determines the API cost.&lt;/p&gt;

&lt;p&gt;For small fixes, the cost is typically a fraction of a cent, depending on the provider and request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which browsers are supported?
&lt;/h3&gt;

&lt;p&gt;The extension works in Chrome and Chromium-based browsers such as Edge, Brave, and Arc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the free SEO self-audit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/markometrics-plugin-ai-se/feclhjgliegkjdcpefnlkpdgaceafgld" rel="noopener noreferrer"&gt;Install the MarkoMetrics SEO Extension → tinyurl.com/seo-plugin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it on a landing page, blog post, product page, or another page you want to improve.&lt;/p&gt;

&lt;p&gt;You will get the audit first. If you want AI to help write the fixes, connect your own API key and generate them directly from the findings.&lt;/p&gt;

&lt;p&gt;If you need help beyond a single-page audit, MarkoMetrics also works on technical SEO, content strategy, GEO/AEO, and growth marketing.&lt;/p&gt;

&lt;p&gt;--&lt;br&gt;
Explore &lt;a href="https://markometrics.com/services" rel="noopener noreferrer"&gt;MarkoMetrics Marketing Service&lt;/a&gt; &lt;/p&gt;

</description>
      <category>claude</category>
      <category>marketing</category>
      <category>chatgpt</category>
      <category>gemini</category>
    </item>
    <item>
      <title>How We Automated Google Business Profile Data Audits Across Major B2B SaaS Markets</title>
      <dc:creator>MarkoMetrics</dc:creator>
      <pubDate>Mon, 14 Sep 2026 09:32:23 +0000</pubDate>
      <link>https://dev.to/markometrics/how-we-automated-google-business-profile-data-audits-across-major-b2b-saas-markets-280d</link>
      <guid>https://dev.to/markometrics/how-we-automated-google-business-profile-data-audits-across-major-b2b-saas-markets-280d</guid>
      <description>&lt;p&gt;If you manage local or multi-location SEO for a B2B SaaS company operating across several markets, you already know the pain: Google Business Profiles (GBP) drift out of sync fast. Categories get changed by well-meaning support staff, NAP (name/address/phone) data goes stale after office moves, hours don't get updated for holidays in different regions, and nobody notices until rankings quietly slip.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://markometrics.com" rel="noopener noreferrer"&gt;MarkoMetrics&lt;/a&gt; we manage GBP listings across multiple markets (UK, Singapore, Malaysia, Indonesia, Philippines) for SaaS clients expanding into Southeast Asia. Manually checking each profile every week doesn't scale. So we built a lightweight audit script using the Google Business Profile API to flag inconsistencies automatically.&lt;/p&gt;

&lt;p&gt;Here's how it works, and the code so you can adapt it for your own stack.&lt;/p&gt;

&lt;p&gt;The problem with manual GBP audits&lt;br&gt;
Data drift: fields get edited by multiple team members with no single source of truth&lt;br&gt;
Category mismatches: businesses often end up with an overly generic primary category, which hurts relevance&lt;br&gt;
NAP inconsistency: address formatting differs across markets (postal code placement, unit numbering conventions)&lt;br&gt;
Silent errors: a wrong phone number or closed status can sit unnoticed for weeks&lt;/p&gt;

&lt;p&gt;None of these are catastrophic individually, but together they erode local ranking signals and trust — and B2B SaaS companies with multiple regional entities (or one HQ profile per country) accumulate this drift fast.&lt;/p&gt;

&lt;p&gt;What the script checks&lt;/p&gt;

&lt;p&gt;We built a Python script that pulls each profile via the API and flags:&lt;/p&gt;

&lt;p&gt;Primary/secondary category mismatches against a defined "expected categories" list&lt;br&gt;
Missing or malformed phone numbers per market (using phonenumbers for country-specific validation)&lt;br&gt;
Address field completeness and consistency against a canonical CRM record&lt;br&gt;
Business hours gaps or overlaps&lt;br&gt;
Whether the profile is verified and not suspended&lt;br&gt;
Setup&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;p&gt;A Google Cloud project with the Business Profile API enabled&lt;br&gt;
OAuth2 credentials for an account with access to your Business Profile locations&lt;br&gt;
google-api-python-client, phonenumbers, and pandas&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;google-api-python-client google-auth-oauthlib phonenumbers pandas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audit script&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;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2.credentials&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;

&lt;span class="c1"&gt;# Canonical data — your source of truth (e.g. pulled from a CRM export)
&lt;/span&gt;&lt;span class="n"&gt;CANONICAL&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;location_id_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_category&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;Software Company&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_code&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;SG&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;expected_phone&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;+65 1234 5678&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="c1"&gt;# ... add per-location records
&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;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_authorized_user_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mybusinessbusinessinformation&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;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&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;audit_location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accounts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/locations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;location_id&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;readMask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories,phoneNumbers,storefrontAddress,regularHours,openInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Category check
&lt;/span&gt;    &lt;span class="n"&gt;primary_category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location&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;categories&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="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;primaryCategory&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="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;displayName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;primary_category&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Category mismatch: got &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;primary_category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, expected &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;expected_category&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="c1"&gt;# Phone validation
&lt;/span&gt;    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location&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;phoneNumbers&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="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;primaryPhone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical&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_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid phone format: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phone&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="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumberParseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unparseable phone number: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phone&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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing phone number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verification / open status
&lt;/span&gt;    &lt;span class="n"&gt;open_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location&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;openInfo&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;open_info&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;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location status is &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;open_info&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;status&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="s"&gt;, not OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Hours check — flag empty hours as a gap
&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;location&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;regularHours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No business hours set&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="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&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;issues&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&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;def&lt;/span&gt; &lt;span class="nf"&gt;run_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_map&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;audit_location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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;loc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;canonical_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&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;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credentials.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_ACCOUNT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CANONICAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gbp_audit_results.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this weekly (cron, GitHub Actions, whatever fits your stack) and pipe the CSV into Slack or email so the team sees drift before it becomes a ranking problem.&lt;/p&gt;

&lt;p&gt;Results from running this across our client base&lt;/p&gt;

&lt;p&gt;Since automating this, we catch category and phone-format issues within a week of them occurring instead of during a quarterly manual review — which, across a multi-market SaaS footprint, used to mean weeks of degraded local visibility going unnoticed in at least one market at any given time.&lt;/p&gt;

&lt;p&gt;Extending it further&lt;/p&gt;

&lt;p&gt;A few directions worth taking this:&lt;/p&gt;

&lt;p&gt;Add a diff-tracking layer (store daily snapshots, alert only on changes rather than re-flagging static issues)&lt;br&gt;
Extend CANONICAL to pull dynamically from a CRM API instead of a hardcoded dict&lt;br&gt;
Add review-response latency tracking using the Reviews API — slow review responses are also a local ranking factor&lt;/p&gt;

&lt;p&gt;If you're running local SEO for a multi-market company and want to compare notes on GBP automation, happy to chat in the comments. We write more about local SEO/GBP work over at the &lt;a href="https://markometrics.com/blogs" rel="noopener noreferrer"&gt;MarkoMetrics blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>api</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
