Free Mind

Building a Multi-Agent Marketing Stack: Wiring GA4, Search Console, and Meta Ads into One System

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.

The problem with a single "marketing AI"

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:

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

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

Pattern: scoped service accounts per channel

For the SEO agent, GA4 and Search Console access is via a single service account with read-only Analytics + Search Console scopes:

from google.oauth2 import service_account
from google.analytics.data_v1beta import BetaAnalyticsDataClient

creds = service_account.Credentials.from_service_account_file(
    "credentials/service-account.json",
    scopes=["https://www.googleapis.com/auth/analytics.readonly"],
)
client = BetaAnalyticsDataClient(credentials=creds)

Enter fullscreen mode Exit fullscreen mode

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 could 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.

Pattern: normalize responses before they hit the agent's context

GA4 and Search Console return data in almost-but-not-quite-compatible shapes. GA4:

for row in response.rows:
    dims = [d.value for d in row.dimension_values]
    mets = [m.value for m in row.metric_values]

Enter fullscreen mode Exit fullscreen mode

Search Console:

for row in response.get("rows", []):
    keys = row["keys"]
    clicks = row["clicks"]
    ctr = row["ctr"]        # float, e.g. 0.0248
    position = row["position"]

Enter fullscreen mode Exit fullscreen mode

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 ({dimension, metric, value}) before 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.

Pattern: coordination without shared context

The orchestration layer (our "marketing manager" agent) doesn't get API access at all. It only receives:

  1. Structured summaries each channel agent produces (KPIs, top opportunities, current spend/budget state)
  2. A fixed weekly cadence / budget framework it can reference
  3. Explicit routing rules for which channel owns which type of request

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 summaries, not raw data — which also keeps the coordinator's context window small enough to reason well over a full week of multi-channel state.

Credential hygiene that actually held up

A few boring rules that saved us from leaking secrets, repeated per agent:

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

Takeaway

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.


Built and maintained by Free Mind Consultancy — we build AI-driven marketing automation (SEO, paid social, content) for authors, founders, and publishers. Read more about our business automation work.