Skip to content

Paid search copy agent

This page explains how a paid search copy agent reads campaign structure, keywords, and landing pages in the ad platform — read-only, under an interactive grant — and generates reviewable copy drafts checked against a versioned claims policy. After reading it, you will understand which modules this scenario needs, why bids and budgets must stay outside the delegated scope, and why data like claims rules belongs in a policy store rather than Memory.

Use case

Growth teams need to create ad headlines and descriptions from keyword groups, competitor ads, landing pages, and brand rules. Campaign structure and keywords in the ad platform are required inputs, but the same account also controls bids, budgets, and serving status — handing it to a copy-generating script hands over control of the entire ad spend.

Typical triggers:

  • A new batch of keyword groups goes live, and multiple headline and description variants are needed before launch.
  • A landing page redesign leaves existing ad copy out of sync with the page, requiring a batch rewrite.
  • Competitor ad wording shifts, and copy direction must be updated against public SERPs.

Engineering challenges

  • High variant volume, high compliance cost: dozens of keyword groups × multiple headline and description variants, each checked against forbidden claims and against what the landing page can actually support. Manual review always misses some, and an out-of-bounds claim that goes live is a compliance and platform-penalty risk.
  • Copy provenance is hard to trace: which keyword group, which page version, and which rule each variant was based on — scattered spreadsheets cannot support pre-launch review or later audits.
  • Borrowed ad-account access is too broad: an ad account inherently carries bid, budget, and serving controls, while the copy task only needs to read structure and keywords and produce drafts. One slip directly affects real ad spend.

Module composition

ModuleRoleNotes
GenAuthCoreRead-only delegation, revocation, and the audit chain for the ad platform; bid, budget, and serving actions stay outside the grant.
Web AgentCoreControlled sessions sign in to the ad platform (Profiles can reuse login state) to read campaign structure read-only, extract landing page content, and check public SERPs and competitor ads with WebSearch.
GUMemNot usedForbidden claims and brand rules are versioned policy — keep them in your policy store and inject them per version; performance history is business state and belongs in your analytics store. Neither is Memory. This scenario has no personal user preference worth persisting across tasks.

Paid search copy agent architecture

Permission and delegation boundaries

The Agent holds no inherent permissions. The effective authority for each copy task is the intersection of three sets: what the user actually holds ∩ what was explicitly delegated for this task ∩ what the enterprise has approved. Applied here:

  • The delegated scope covers only "read the selected account's campaign structure, keyword lists, and landing pages, and create copy drafts" — no changes to bids, budgets, serving status, or targeting.
  • Delegation credentials are short-lived; minute-level validity is recommended for a single copy task, with re-delegation after expiry.
  • The user or an administrator can revoke the grant at any time; new read or draft requests fail immediately after revocation.
  • Out-of-scope attempts (for example, touching a budget or serving switch) are rejected and recorded — the audit chain covers all attempts, not just successful actions.

Note: the SDK example requests product-level scopes (such as webagent.do_anything:read). Fine-grained boundaries — domain lists, page ranges, action whitelists — are enforced by the GenAuth Agent Profile or your policy layer, not by the task prompt; that configuration is not shown on this page. See Delegate token and attenuation for the full semantics.

Workflow

Paid search copy agent workflow

  1. The user selects an ad account, keyword groups, and a target landing page.

  2. GenAuth starts an interactive delegation; after the user confirms in Qoni Console, your server-side callback exchanges it for a least-privilege credential.

  3. Your app loads the current version of forbidden claims, brand rules, and compliance limits from the policy store and injects them into the task.

  4. Web Agent signs in to the ad platform and reads the existing campaign structure and keyword lists, read-only.

    Checkpoint: This step permits reads only; any interface action toward bids, budgets, or serving status should be rejected and recorded.

  5. Web Agent extracts landing page content and checks public SERPs and competitor ads through WebSearch for reference.

  6. The Agent generates headlines, descriptions, and variants per keyword group, checking every variant against forbidden claims and annotating the rule it was checked against.

  7. The Agent returns copy drafts, variant comparisons, and review notes with an audit id attached; after app-side validation they enter the review queue.

    Checkpoint: No variant should contain forbidden claims or promises the landing page cannot support; non-compliant variants should be removed with the reason stated.

Example code

The example below wires this scenario into your backend with the official Qoni SDK (@qoniai/qoni): interactive delegation (with a full callback) → load the claims policy from your policy store → one doAnything.run() for read-only data pulls and variant generation → parse and validate the variants.

ts
import { Qoni, QoniScopes } from '@qoniai/qoni'

const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
})

export async function startCopyTask(userId: string, taskId: string) {
  // 1. Interactive delegation: sign-in is involved, so the user confirms in Qoni Console
  const { data: authorization } = await qoni.delegateToken({
    mode: 'interactive',
    agent: 'paid-search-copy',
    scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
    redirectUri: 'https://app.example.com/qoni/callback',
    state: taskId,
    user: { id: userId },
    expiresIn: 900, // minute-level validity for a single copy task
  })
  redirectUserTo(authorization.authorizationUrl)
}

// GET /qoni/callback — after the user approves, exchange for the grant
// server-side and continue the task
export async function handleQoniCallback(request: Request) {
  const query = new URL(request.url).searchParams
  const { data: grant } = await qoni.completeDelegateToken({
    grantId: query.get('grantId')!,
    code: query.get('code')!,
    state: query.get('state')!,
  })
  const task = await loadCopyTask(query.get('state')!) // your task store
  return runCopyTask(grant, task)
}

async function runCopyTask(
  grant: { token: string; auditId: string; grantedScopes: string[] },
  task: { adAccountId: string; landingPageUrl: string },
) {
  // 2. Before the run: load the current claims policy from YOUR policy store
  //    (this is versioned policy, not Memory)
  const policy = await loadClaimsPolicy() // e.g. { version: '2026-08', forbiddenClaims: [...], voice: ... }

  // 3. One call runs the pull and drafting: read structure, extract pages, draft variants
  const run = await qoni.doAnything.run({
    token: grant.token,
    prompt: `
      Sign in to the ad platform and read — read-only — the campaign
      structure and keyword lists for account ${task.adAccountId}. Extract
      landing page ${task.landingPageUrl}; check public SERPs for competitor
      ads. Generate headline and description variants per keyword group.
      Return variants as a JSON array of
      { keywordGroup, headline, description, ruleId } objects.
      Drop variants with forbidden claims or promises the landing page
      cannot support, and note why. Do not change bids, budgets, serving
      status, or targeting. Do not submit ads.

      Claims policy (version ${policy.version}):
      ${JSON.stringify(policy)}
    `,
    capture: { screenshots: true },
  })

  const result = await run.wait({
    // Login walls / CAPTCHAs / risk-control pages: forward to the user
    onInteraction: (interaction) => notifyUserActionRequired(interaction),
  })

  // 4. Parse and validate the output contract on the app side:
  //    variants without a keyword group or rule id never reach the review queue
  const variants = parseVariants(result.output).filter(
    (v) => v.keywordGroup && v.ruleId,
  )

  return {
    variants,
    artifacts: result.artifacts, // step screenshots, archived with the drafts
    policyVersion: policy.version,
    audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
  }
}

The output structure is a contract set by the task prompt: here it is an array of { keywordGroup, headline, description, ruleId }, parsed and validated by parseVariants on the app side, and any entry missing keywordGroup or ruleId is dropped. The SDK itself returns the generic RunResult (runId, status, output, artifacts, and so on).

Data and memory boundaries

This scenario touches four kinds of data; GUMem is not used here:

  • Versioned rules: forbidden claims, brand rules, compliance limits — managed by version in your policy store, with every variant referencing a rule id and version.
  • Business state: copy variants, review outcomes, per-keyword-group performance history — archived to your campaign records and analytics store for review and traceability.
  • Audit records: the delegation and behavior chain formed by grantId and auditId — maintained by GenAuth.
  • User Memory (optional): only long-term personal preferences a user or reviewer has explicitly confirmed belong in GUMem; the inputs here are team-level rules and business data, so this scenario neither recalls nor writes back by default.

Failure handling

SituationRecommended handling
Ad platform login state expiresSuspend the task, notify the user to sign in again, and resume from the checkpoint.
Landing page extraction fails or content mismatches keywordsTreat it as a failure and replay the session recording; never write copy from guessed page content.
Bid or budget change request outside the delegated scopeReject and record it; the attempted action remains visible in the audit chain.
A variant lacks a keyword group or rule idApp-side validation drops the variant and the review notes record how many were dropped and the policy version applied.

Production notes

Do not submit ads or change budgets automatically. All variants should go to a draft or review queue. Cap read frequency against the ad platform to avoid triggering risk controls; product claims in copy must be supported by the landing page or authorized materials — unsupported claims do not enter drafts. When the claims policy changes, publish a new version in the policy store so stale rules never constrain new variants.

Next steps