Skill: sebclawops/openclaw-google-ads (version 0.2.0)

--- name: openclaw-google-ads description: Shared Google Ads API skill for OpenClaw agents. Query account, campaign, ad group, keyword, search term, and performance data with local scripts and GAQL examples. Use when an agent needs Google Ads reporting, campaign audits, wasted spend checks, conversion-tracking review, or production-safe campaign analysis. Prefer read-first analysis and require explicit approval before live account changes. --- # OpenClaw Google Ads Use this skill for Google Ads API work across agents. This is a shared skill, not a Sea Cool-only skill. Keep account-specific practices in project docs or memory, not in the core skill. ## Use this skill for - campaign and account performance reporting - account health audits - wasted spend review - search term and keyword analysis - conversion tracking review - structured GAQL querying - production-safe recommendations before live changes ## Default stance Start read-only. Analyze first, recommend second, change last. Do not make live account changes without explicit approval. ## Access model Google Ads API Basic Access is suitable for real production use. If credentials are missing or invalid, stop and fix access before pretending the skill can do real work. ## Setup and references Read only what you need: - `references/api-setup.md` for credentials, auth flow, and connection testing - `references/gaql-examples.md` for query patterns - `references/audit-workflows.md` for practical account review flows - `references/optimization.md` for optimization heuristics and common mistakes - `references/browser-fallback.md` only when API access is unavailable or UI confirmation is explicitly needed ## Available scripts - `scripts/authenticate.py` - `scripts/gaql_query.py` - `scripts/get_account_summary.py` - `scripts/get_campaigns.py` Use scripts for repeatable API work. Use references for judgment. ## Safety rules - never expose tokens or credentials in files or chat - prefer read-only analysis before any operational change - require explicit approval for pausing, enabling, editing, or budget changes - treat optimization advice as heuristics, not universal truth - when customer-identifying data appears in exported reports or account notes, apply PII protection before broader model use ## Output style Lead with findings and recommended actions. Keep reports practical: - what is happening - what looks wrong - what to check next - what to change, if approved

path: README.md
content: # openclaw-google-ads

Shared Google Ads API skill for OpenClaw agents.

## Goal

Give agents a practical, production-safe way to analyze Google Ads accounts, run structured queries, audit account health, and recommend improvements without mixing in account-specific business rules.

## What this skill does well

- account and campaign reporting
- GAQL-based querying
- audit workflows
- wasted spend analysis
- conversion tracking review
- optimization guidance grounded in practical heuristics

## What this skill is not

- a Sea Cool-only skill
- a replacement for business-specific campaign strategy docs
- a license to make live account changes without approval
- a place to store credentials

## Structure

```text
openclaw-google-ads/
├── SKILL.md
├── README.md
├── requirements.txt
├── references/
│   ├── api-setup.md
│   ├── gaql-examples.md
│   ├── audit-workflows.md
│   ├── optimization.md
│   └── browser-fallback.md
└── scripts/
    ├── authenticate.py
    ├── gaql_query.py
    ├── get_account_summary.py
    └── get_campaigns.py
```

## Practical use order

1. verify credentials and connection
2. run read-only summary or GAQL queries
3. audit the account or campaign area in question
4. recommend actions
5. only make live changes with explicit approval

## Notes

- Keep account-specific learnings in the relevant agent or project docs.
- Keep the shared skill general.
- If API access is unavailable, use the browser fallback only when explicitly needed.


path: references/api-setup.md
content: # API Setup

## Access level

Google Ads API Basic Access is approved, so this skill can support real production reporting and operational workflows.

## Required credentials

- `GOOGLE_ADS_DEVELOPER_TOKEN`
- `GOOGLE_ADS_CLIENT_ID`
- `GOOGLE_ADS_CLIENT_SECRET`
- `GOOGLE_ADS_REFRESH_TOKEN`
- `GOOGLE_ADS_MANAGER_ACCOUNT_ID` optional
- `GOOGLE_ADS_CLIENT_ACCOUNT_ID` optional

## Credential handling

Do not tell users to store credentials in plaintext files.
For this OpenClaw environment, follow the current key protocol used by the workspace owner.
Keep credentials in secure storage and expose them to the runtime through approved environment injection.

## Dependencies

```bash
pip3 install -r requirements.txt
```

## Authentication flow

Run:

```bash
python3 scripts/authenticate.py
```

Use the result to obtain or refresh the Google Ads refresh token.
Do not paste credentials into tracked files.

## Test connection

```bash
python3 scripts/get_account_summary.py --account YOUR-ACCOUNT-ID
```

If this fails:
- verify all required environment variables exist
- verify the developer token is valid
- verify the refresh token is current
- verify the account ID is a 10-digit ID without dashes

## Operational note

Prefer manager-account-driven access when multiple accounts are involved.
Keep client account IDs explicit when running analysis to avoid checking the wrong account.


path: references/audit-workflows.md
content: # Audit Workflows

## 1. Account health check

Start with:
- account summary for the last 30 days
- campaign list with status and top-line metrics
- major trend changes in spend, clicks, conversions, and conversion value

Look for:
- spend up while conversions are flat or down
- conversions present but no conversion value where value should exist
- major campaigns paused unintentionally
- obvious account-level underdelivery or runaway spend

## 2. Wasted spend review

Check:
- zero-conversion campaigns with meaningful spend
- zero-conversion keywords with meaningful spend
- search terms with poor relevance
- campaigns with weak CTR and no downstream results

Questions to answer:
- what is spending without producing value?
- is the issue targeting, query quality, landing page quality, or tracking?

## 3. Conversion tracking review

Check:
- are the expected primary conversions active?
- are there recent conversions, or did tracking go dark?
- is the account mixing micro-conversions with primary conversions?
- is value tracking present and believable?

Watch for:
- inactive conversion actions
- call conversions with weak quality thresholds
- inflated reporting from counting the wrong actions
- sudden drops that smell like tracking breakage, not demand change

## 4. Campaign triage

For a weak campaign, review in order:
1. status and budget constraints
2. impressions and impression share if available
3. CTR
4. CPC and spend trend
5. conversions and CPA or ROAS
6. search term quality
7. landing page relevance and speed

## 5. Before recommending changes

Separate findings into:
- clear problem
- likely cause
- recommended next step
- change that needs explicit approval

Keep reports practical and short.


path: references/browser-fallback.md
content: # Browser Fallback

Use this only when:
- API access is unavailable
- the user explicitly wants UI confirmation
- a specific setting or workflow is easier to verify in the Google Ads interface

## Default stance

Prefer the API for reporting and repeatable analysis.
Use the browser for confirmation, screenshots, or UI-only checks.

## Basic workflow

1. Open ads.google.com in the approved browser context.
2. Confirm the correct account is selected.
3. Navigate to the relevant area:
   - campaigns
   - ad groups
   - keywords
   - search terms
   - conversions
4. Snapshot the visible data.
5. Report findings clearly.

## Cautions

- Google Ads UI is heavy and easy to misread if the wrong account or date range is selected.
- Confirm date range before drawing conclusions.
- Do not make live UI changes without explicit approval.


path: references/gaql-examples.md
content: # GAQL Examples

## Notes

- Customer IDs are 10-digit numbers without dashes.
- Cost fields are returned in micros. Divide by 1,000,000 for currency units.
- Start with read-only reporting queries before considering any operational change.

## Campaign performance, last 30 days

```sql
SELECT
    campaign.id,
    campaign.name,
    campaign.status,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions,
    metrics.conversions_value
FROM campaign
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Ad group performance

```sql
SELECT
    ad_group.id,
    ad_group.name,
    ad_group.status,
    campaign.name,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.ctr,
    metrics.average_cpc,
    metrics.conversions
FROM ad_group
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Keyword performance

```sql
SELECT
    ad_group_criterion.criterion_id,
    ad_group_criterion.keyword.text,
    ad_group_criterion.keyword.match_type,
    campaign.name,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions,
    metrics.quality_score
FROM keyword_view
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Search terms report

```sql
SELECT
    search_term_view.search_term,
    campaign.name,
    ad_group.name,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions
FROM search_term_view
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Zero-conversion keywords with meaningful spend

```sql
SELECT
    ad_group_criterion.keyword.text,
    ad_group_criterion.keyword.match_type,
    campaign.name,
    metrics.cost_micros,
    metrics.clicks,
    metrics.impressions,
    metrics.conversions
FROM keyword_view
WHERE segments.date DURING LAST_30_DAYS
  AND metrics.conversions = 0
  AND metrics.cost_micros > 5000000
ORDER BY metrics.cost_micros DESC
```

## Search terms with spend and no conversions

```sql
SELECT
    search_term_view.search_term,
    campaign.name,
    metrics.cost_micros,
    metrics.clicks,
    metrics.impressions,
    metrics.conversions
FROM search_term_view
WHERE segments.date DURING LAST_30_DAYS
  AND metrics.conversions = 0
  AND metrics.cost_micros > 5000000
ORDER BY metrics.cost_micros DESC
```

## Conversion actions overview

```sql
SELECT
    conversion_action.name,
    conversion_action.category,
    conversion_action.status,
    conversion_action.primary_for_goal,
    conversion_action.type
FROM conversion_action
ORDER BY conversion_action.name
```

## Device performance

```sql
SELECT
    segments.device,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions,
    metrics.conversions_value
FROM campaign
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Geographic performance

```sql
SELECT
    geographic_view.country_criterion_id,
    geographic_view.location_name,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions,
    metrics.conversions_value
FROM geographic_view
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
```

## Daily trend

```sql
SELECT
    segments.date,
    metrics.impressions,
    metrics.clicks,
    metrics.cost_micros,
    metrics.conversions,
    metrics.conversions_value
FROM campaign
WHERE segments.date DURING LAST_30_DAYS
ORDER BY segments.date DESC
```

## Account overview

```sql
SELECT
    customer.id,
    customer.descriptive_name,
    customer.status,
    customer.currency_code,
    customer.time_zone
FROM customer
```


path: references/optimization.md
content: # Optimization Heuristics

Use these as practical rules of thumb, not as absolute truth.

## Keyword strategy

- Review search terms regularly. Broad and phrase traffic drifts fast.
- Broad match usually needs strong bidding logic and good negatives.
- Long-tail keywords can improve efficiency, but too many low-volume fragments can stall spend.
- Theme-based ad groups are usually better than overly fragmented structures.

## Quality score and relevance

- Expected CTR, ad relevance, and landing page experience all matter.
- Keyword-to-ad-to-landing-page alignment usually beats clever copy.
- Landing page speed and clarity matter before more spend does.

## Bidding

- Aggressive target CPA can choke volume.
- Maximize Conversions without guardrails can waste budget.
- Manual CPC can be useful early, but often leaves money on the table once conversion volume is real.
- Bid changes should be evaluated against conversion lag, not same-day noise.

## Campaign structure

- Keep branded and non-branded search separate.
- Separate regions when budget control or reporting needs differ.
- Avoid mixing unlike intents in the same campaign when analysis gets muddy.
- Shared budgets can hide real demand and tradeoffs.

## Search term hygiene

- Negative keyword discipline compounds over time.
- Search terms often reveal landing page mismatch, not just keyword mismatch.
- If wasted spend clusters around a theme, fix the structure, not just the individual term.

## Conversion interpretation

- Verify which conversion actions actually matter.
- Micro-conversions can be useful, but they can also distort optimization if treated as primary value.
- Last-click views can hide assist behavior.

## Scaling

- Search volume has a ceiling.
- Higher spend often comes with worse efficiency.
- Scaling decisions should explicitly balance volume and efficiency, not pretend both always improve together.


path: requirements.txt
content: google-ads==24.1.0
google-auth-oauthlib>=0.5.0
google-auth>=2.0.0


path: scripts/authenticate.py
content: #!/usr/bin/env python3
"""
Google Ads API Authentication Script
Handles OAuth flow and stores refresh token for API access.
"""

import os
import sys

# OAuth scopes for Google Ads API
SCOPES = ['https://www.googleapis.com/auth/adwords']

def main():
    # Import here so we can catch import errors
    try:
        from google_auth_oauthlib.flow import InstalledAppFlow
    except ImportError:
        print("Error: google-auth-oauthlib not installed")
        print("Run: pip install google-auth-oauthlib")
        sys.exit(1)
    
    # Load client credentials from environment
    client_id = os.environ.get('GOOGLE_ADS_CLIENT_ID')
    client_secret = os.environ.get('GOOGLE_ADS_CLIENT_SECRET')
    
    if not client_id or not client_secret:
        print("Error: GOOGLE_ADS_CLIENT_ID and GOOGLE_ADS_CLIENT_SECRET must be set")
        print("Make sure OAuth credentials are configured")
        sys.exit(1)
    
    # Create client config dict
    client_config = {
        "installed": {
            "client_id": client_id,
            "client_secret": client_secret,
            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
            "token_uri": "https://oauth2.googleapis.com/token",
            "redirect_uris": ["http://localhost"]
        }
    }
    
    # Run OAuth flow
    flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
    
    # Use local server flow
    credentials = flow.run_local_server(port=0)
    
    # Store refresh token
    refresh_token = credentials.refresh_token
    
    print(f"\n{'='*60}")
    print(f"REFRESH [REDACTED_SECRET]")
    print(f"{'='*60}")
    print("\nDo not paste this into tracked files or plaintext workspace notes.")
    print("Store it using the approved secure credential workflow for this OpenClaw environment.")
    print("Expose it to the runtime through approved environment injection only.")

if __name__ == '__main__':
    main()


path: scripts/gaql_query.py
content: #!/usr/bin/env python3
"""
Run custom GAQL queries against Google Ads API.
"""

import sys
import argparse
from google.ads.googleads.client import GoogleAdsClient
from google.protobuf.json_format import MessageToDict


def run_query(account_id, query):
    """Execute a GAQL query and print rows as dictionaries."""

    client = GoogleAdsClient.load_from_env()
    ga_service = client.get_service("GoogleAdsService")

    search_request = client.get_type("SearchGoogleAdsRequest")
    search_request.customer_id = account_id.replace('-', '')
    search_request.query = query

    print(f"Querying account: {account_id}")
    print(f"Query: {query}")
    print("-" * 80)

    try:
        results = ga_service.search(request=search_request)

        count = 0
        for row in results:
            count += 1
            print(f"\nRow {count}:")
            row_dict = MessageToDict(row._pb, preserving_proto_field_name=True)
            for key, value in row_dict.items():
                print(f"  {key}: {value}")

        print(f"\nTotal rows: {count}")

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(description='Run GAQL query on Google Ads')
    parser.add_argument('--account', required=True, help='Account ID (with dashes)')
    parser.add_argument('--query', required=True, help='GAQL query string')

    args = parser.parse_args()
    run_query(args.account, args.query)


if __name__ == '__main__':
    main()


path: scripts/get_account_summary.py
content: #!/usr/bin/env python3
"""
Get account performance summary from Google Ads API
"""

import os
import sys
import argparse
from google.ads.googleads.client import GoogleAdsClient

def get_account_summary(manager_account_id, days=30):
    """Get performance summary for the account."""
    
    # Load config from env with proto_plus setting
    config = {
        "developer_token": os.environ.get("GOOGLE_ADS_DEVELOPER_TOKEN"),
        "client_id": os.environ.get("GOOGLE_ADS_CLIENT_ID"),
        "client_secret": os.environ.get("GOOGLE_ADS_CLIENT_SECRET"),
        "refresh_token": os.environ.get("GOOGLE_ADS_REFRESH_TOKEN"),
        "login_customer_id": os.environ.get("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "").replace("-", ""),
        "use_proto_plus": True
    }
    
    client = GoogleAdsClient.load_from_dict(config)
    ga_service = client.get_service("GoogleAdsService")
    
    query = f"""
        SELECT
            customer.id,
            customer.descriptive_name,
            metrics.impressions,
            metrics.clicks,
            metrics.cost_micros,
            metrics.conversions,
            metrics.conversions_value
        FROM customer
        WHERE segments.date DURING LAST_{days}_DAYS
    """
    
    search_request = client.get_type("SearchGoogleAdsRequest")
    search_request.customer_id = manager_account_id.replace('-', '')
    search_request.query = query
    
    print(f"Account Summary (Last {days} Days)")
    print("-" * 80)
    
    try:
        results = ga_service.search(request=search_request)
        
        total_impressions = 0
        total_clicks = 0
        total_cost = 0
        total_conversions = 0
        
        for row in results:
            customer = row.customer
            metrics = row.metrics
            
            cost = metrics.cost_micros / 1000000 if metrics.cost_micros else 0
            
            print(f"Account: {customer.descriptive_name} ({customer.id})")
            print(f"  Impressions: {metrics.impressions:,}")
            print(f"  Clicks: {metrics.clicks:,}")
            print(f"  Cost: ${cost:,.2f}")
            print(f"  Conversions: {metrics.conversions}")
            print(f"  Conv. Value: ${metrics.conversions_value:,.2f}")
            print()
            
            total_impressions += metrics.impressions
            total_clicks += metrics.clicks
            total_cost += cost
            total_conversions += metrics.conversions
        
        print("-" * 80)
        print("TOTALS:")
        print(f"  Impressions: {total_impressions:,}")
        print(f"  Clicks: {total_clicks:,}")
        print(f"  Cost: ${total_cost:,.2f}")
        print(f"  Conversions: {total_conversions}")
        
        if total_clicks > 0:
            cpc = total_cost / total_clicks
            print(f"  Avg CPC: ${cpc:.2f}")
        
        if total_impressions > 0:
            ctr = (total_clicks / total_impressions) * 100
            print(f"  CTR: {ctr:.2f}%")
            
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(description='Get Google Ads account summary')
    parser.add_argument('--account', required=True, help='Account ID (with dashes)')
    parser.add_argument('--days', type=int, default=30, help='Number of days (default: 30)')
    
    args = parser.parse_args()
    
    get_account_summary(args.account, args.days)

if __name__ == '__main__':
    main()


path: scripts/get_campaigns.py
content: #!/usr/bin/env python3
"""
Get campaigns from Google Ads API
"""

import os
import sys
import argparse
from google.ads.googleads.client import GoogleAdsClient

def get_campaigns(manager_account_id, client_account_id=None):
    """List all campaigns for the specified account."""
    
    # Initialize client
    client = GoogleAdsClient.load_from_env()
    
    # Use client account if specified, otherwise use manager
    customer_id = client_account_id or manager_account_id
    
    ga_service = client.get_service("GoogleAdsService")
    
    query = """
        SELECT
            campaign.id,
            campaign.name,
            campaign.status,
            campaign.advertising_channel_type,
            metrics.impressions,
            metrics.clicks,
            metrics.cost_micros,
            metrics.conversions
        FROM campaign
        WHERE segments.date DURING LAST_30_DAYS
        ORDER BY campaign.id
    """
    
    search_request = client.get_type("SearchGoogleAdsRequest")
    search_request.customer_id = customer_id.replace('-', '')
    search_request.query = query
    
    print(f"Campaigns for account: {customer_id}")
    print("-" * 80)
    
    try:
        results = ga_service.search(request=search_request)
        
        for row in results:
            campaign = row.campaign
            metrics = row.metrics
            
            cost = metrics.cost_micros / 1000000 if metrics.cost_micros else 0
            
            print(f"ID: {campaign.id}")
            print(f"Name: {campaign.name}")
            print(f"Status: {campaign.status.name}")
            print(f"Channel: {campaign.advertising_channel_type.name}")
            print(f"Impressions: {metrics.impressions}")
            print(f"Clicks: {metrics.clicks}")
            print(f"Cost: ${cost:.2f}")
            print(f"Conversions: {metrics.conversions}")
            print("-" * 80)
            
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(description='Get Google Ads campaigns')
    parser.add_argument('--account', required=True, help='Account ID (with dashes)')
    parser.add_argument('--client-account', help='Client account ID (for manager accounts)')
    
    args = parser.parse_args()
    
    get_campaigns(args.account, args.client_account)

if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)