<?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: Free Mind</title>
    <description>The latest articles on DEV Community by Free Mind (@free_mind_090cbd19c788f98).</description>
    <link>https://dev.to/free_mind_090cbd19c788f98</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052796%2F3037bfda-fa58-450c-b6e0-945473a0b9bd.jpg</url>
      <title>DEV Community: Free Mind</title>
      <link>https://dev.to/free_mind_090cbd19c788f98</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/free_mind_090cbd19c788f98"/>
    <language>en</language>
    <item>
      <title>Building a Multi-Agent Marketing Stack: Wiring GA4, Search Console, and Meta Ads into One System</title>
      <dc:creator>Free Mind</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:30:33 +0000</pubDate>
      <link>https://dev.to/free_mind_090cbd19c788f98/building-a-multi-agent-marketing-stack-wiring-ga4-search-console-and-meta-ads-into-one-system-2jm8</link>
      <guid>https://dev.to/free_mind_090cbd19c788f98/building-a-multi-agent-marketing-stack-wiring-ga4-search-console-and-meta-ads-into-one-system-2jm8</guid>
      <description>&lt;h1&gt;
  
  
  Building a Multi-Agent Marketing Stack: Wiring GA4, Search Console, and Meta Ads into One System
&lt;/h1&gt;

&lt;p&gt;Most "AI marketing automation" content online is either a no-code tool pitch or a vague thinkpiece. This is neither — it's a breakdown of the actual architecture pattern we landed on after building out a multi-channel marketing agent system: separate specialized agents per channel (SEO, paid social, content), coordinated by a thin orchestration layer, all backed by real APIs instead of scraped dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a single "marketing AI"
&lt;/h2&gt;

&lt;p&gt;The obvious first instinct is one big agent with access to every tool: GA4, Search Console, Meta Ads, CMS, everything. In practice this falls apart fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential blast radius&lt;/strong&gt; — one compromised prompt or bug can touch every connected system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context pollution&lt;/strong&gt; — SEO keyword data and ad creative performance data don't belong in the same reasoning context; the agent starts making decisions with irrelevant signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No clear ownership&lt;/strong&gt; — when something breaks, "the marketing agent" isn't a debuggable unit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix that worked for us: &lt;strong&gt;one agent per channel, each with narrowly scoped credentials and a fixed API surface&lt;/strong&gt;, plus a coordination layer above them that only sees summarized state, not raw API access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern: scoped service accounts per channel
&lt;/h2&gt;

&lt;p&gt;For the SEO agent, GA4 and Search Console access is via a single service account with read-only Analytics + Search Console scopes:&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;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BetaAnalyticsDataClient&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;service_account&lt;/span&gt;&lt;span class="p"&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_service_account_file&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/service-account.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;scopes&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;https://www.googleapis.com/auth/analytics.readonly&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="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BetaAnalyticsDataClient&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Meta Ads agent gets its own token, scoped to a single ad account ID — never a full Business Manager token. This is a deliberate constraint we enforce at the prompt/instruction layer too: even if the underlying token &lt;em&gt;could&lt;/em&gt; see other ad accounts, the agent is told exactly one account ID is in scope and to refuse to act on anything else. Defense in depth beats trusting the token scope alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern: normalize responses before they hit the agent's context
&lt;/h2&gt;

&lt;p&gt;GA4 and Search Console return data in almost-but-not-quite-compatible shapes. GA4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;=&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;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mets&lt;/span&gt; &lt;span class="o"&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="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search Console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;rows&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="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ctr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ctr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# float, e.g. 0.0248
&lt;/span&gt;    &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you feed raw API responses straight into an LLM's context across multiple sources, it will misattribute fields between them — CTR from one dimension_values array gets confused with position from another. We normalize every source into the same flat dict shape (&lt;code&gt;{dimension, metric, value}&lt;/code&gt;) &lt;em&gt;before&lt;/em&gt; it ever reaches a prompt. Boring code, but it's the difference between reliable output and an agent that quietly hallucinates a number that looks plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern: coordination without shared context
&lt;/h2&gt;

&lt;p&gt;The orchestration layer (our "marketing manager" agent) doesn't get API access at all. It only receives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Structured summaries each channel agent produces (KPIs, top opportunities, current spend/budget state)&lt;/li&gt;
&lt;li&gt;A fixed weekly cadence / budget framework it can reference&lt;/li&gt;
&lt;li&gt;Explicit routing rules for which channel owns which type of request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means the coordinator can't accidentally query GA4 directly and burn a rate limit, and it can't leak Meta Ads token access into an SEO conversation. Every cross-channel decision is made on &lt;em&gt;summaries&lt;/em&gt;, not raw data — which also keeps the coordinator's context window small enough to reason well over a full week of multi-channel state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credential hygiene that actually held up
&lt;/h2&gt;

&lt;p&gt;A few boring rules that saved us from leaking secrets, repeated per agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; per agent directory, never a shared root &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; scoped per agent (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;__pycache__/&lt;/code&gt;, &lt;code&gt;*.pyc&lt;/code&gt;, cache dirs) rather than trusting a root-level ignore file to catch everything&lt;/li&gt;
&lt;li&gt;Service accounts get the &lt;em&gt;minimum&lt;/em&gt; API scope available (read-only where possible), never a general-purpose OAuth token&lt;/li&gt;
&lt;li&gt;Every new channel integration gets verified with a throwaway authenticated call (&lt;code&gt;GET /users/me&lt;/code&gt; equivalent) before anything else — cheap, catches key-format mistakes (stray spaces in &lt;code&gt;.env&lt;/code&gt; keys are a surprisingly common bug) before they cost you a debugging session downstream&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're building anything beyond a single-tool AI agent, the channel-isolation pattern is worth the extra setup cost. It trades a bit of upfront boilerplate (more credential files, more normalization code) for a system where failures are contained, debuggable, and don't cascade — which matters a lot more once you have four or five channels running instead of one.&lt;/p&gt;




&lt;p&gt;Built and maintained by &lt;a href="https://www.freemindconsult.com/" rel="noopener noreferrer"&gt;Free Mind Consultancy&lt;/a&gt; — we build AI-driven marketing automation (SEO, paid social, content) for authors, founders, and publishers. Read more about our &lt;a href="https://www.freemindconsult.com/services/business-automation-india.html" rel="noopener noreferrer"&gt;business automation work&lt;/a&gt;.&lt;/p&gt;

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