Install
openclaw skills install @rafalzawadzki/apple-ads-optimizerOptimizes the user's Apple Ads (Search Ads) campaigns through their connected Apple Ads account - raises bids on keywords that convert, lowers or pauses wasteful ones, adds converting search terms and negatives - every change a dry run with a diff the user approves.
openclaw skills install @rafalzawadzki/apple-ads-optimizerA weekly pass over Apple Ads campaigns with explicit rules: move budget toward keywords that
install under the target cost per acquisition (CPA), away from keywords and search terms that
spend without installs, and promote proven search terms to exact keywords. Every change goes
through propose_action (dry run, free) and is applied only after the user approves it.
apple_ads, Apple Ads Platform API). An API user
with access to several ad accounts connects one ad_account_id per connection; pick the
connection whose account runs the app's campaigns.aso-keywords-metadata-rewrite) or Google Ads.campaign (optional): a campaign name or id. Default: every campaign with status ENABLED;
ask which ones when there are more than five.target_cpa (optional): the most the user wants to pay per install, in the campaign currency.
Ask for it if missing; if they don't know, suggest the median CPA of the converting keywords
from step 3 and get a yes before using it. Also ask for the daily budget they are comfortable with.Window: the last 14 full days (end = yesterday, start = end − 13 days, UTC).
Limits per run: at most 25 bid changes, 15 new keywords and 25 negatives per campaign.
Costs in AppGoat credits. Without the MCP, use the REST equivalents in reference/api.md.
list_connections {provider: "apple_ads"} - 0. Keep status: active; mode: read_only
can only dry-run (say so). None: send the user to https://dash.appgoat.ai/connections and stop.read_connection {id, what: "ads", view: "campaigns"} - 1. Campaign ids, status, app
(adam id), countries, daily budget. Pick the campaigns.read_connection {id, what: "ads", view: "ad_groups", campaign_id} - 1 (ad group ids, default bids).read_connection {id, what: "ads", view: "report", level: "keywords", campaign_id, start, end} - 1:
per keyword impressions, taps, installs, spend, avg CPT, avg CPA, keyword id, ad group id, bid.read_connection {id, what: "ads", view: "report", level: "searchterms", campaign_id, start, end} - 1:
the search terms that triggered the ads, with the keyword that matched them.read_connection {id, what: "ads", view: "negatives", campaign_id} - 1 (existing negatives).read_connection {view: "keywords", campaign_id, ad_group_id} - 1 each.keyword_research {terms: ≤ 25 terms, store:
"apple", country: } - 1 per term. Popularity 5–100 shows whether a term
has real search volume; include: ["top_apps"] shows which apps win it (relevance check).playbook: "apple-ads-optimizer" - 0:
propose_action {type: "ads.keyword.bid", connection, params: {campaign_id, ad_group_id,
keywords: [{id, bid}] or [{id, status: "PAUSED"}], max_increase: 0.3}} (status is
ENABLED or PAUSED; ENABLED resumes a paused keyword)propose_action {type: "ads.keyword.add", connection, params: {campaign_id, ad_group_id,
keywords: [{text, match_type: "EXACT", bid}]}}propose_action {type: "ads.negative.add", connection, params: {campaign_id, ad_group_id,
keywords: [{text, match_type: "EXACT"}]}}
The diffs show current → new bid per keyword; warnings flag bid jumps above max_increase and
negatives that would block an enabled keyword.apply_action {id} for each approved action - 1 per applied action. pending_approval →
give the user the approve_url; "needs approval by a person" → hand over the link, don't retry;
"account changed" (someone edited bids meanwhile) → show the new diff, re-run step 3 for that
ad group rather than forcing. reject_action {id, reason} the declined ones - 0.Budget: ~5–8 credits per campaign to read + 1 per applied action (dry runs are free).
Let T = target CPA. Only act on rows with enough data: taps ≥ 10 or spend ≥ T.
status: "PAUSED") when installs = 0 and spend ≥ 2 × T. Lower by 30% instead when
installs = 0 and spend is between T and 2 × T, or when the keyword is the app's brand name.get_action history or ask).# Apple Ads optimizer: <campaign> (<country>) - <start> → <end>, target CPA <T> <cur>
Spend <s> · installs <i> · CPA <c> · <k> keywords, <n> search terms with data
| Change | Ad group | Item | Now | Proposed | Why (14 d) | Action |
|---|---|---|---|---|---|---|
| Raise bid | Exact | habit tracker [EXACT] | 1.00 | 1.30 (+30%) | 14 installs, CPA 1.80 | act_… |
| Pause | Broad | free planner [BROAD] | 0.90 | paused | 0 installs, spend 11.20 | act_… |
| Add keyword | Exact | streak app [EXACT] | – | 0.85 | search term: 4 installs, CPA 2.10 | act_… |
| Negative | Broad | planner printable [EXACT] | – | negative | 22 taps, 0 installs | act_… |
Guardrail warnings: <none | list>.
Reply "apply all", "only bids", or name rows to skip.
Run report: what was applied, what waits for approval (links), and "Re-run in 7 days; the bid changes need about a week of data." Credits used: .
Weekly proposals that a person approves in the dashboard. The run calls apply_action once per
action, which submits it for approval (or applies it when the org has a Studio auto-apply rule for
the type); nothing changes in Apple Ads until someone approves it at https://dash.appgoat.ai/approvals.
GitHub Action (.github/workflows/appgoat-apple-ads.yml, secrets ANTHROPIC_API_KEY, APPGOAT_API_KEY):
name: AppGoat Apple Ads optimizer
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
ads:
runs-on: ubuntu-latest
steps:
- name: Install AppGoat playbooks and MCP config
env:
APPGOAT_API_KEY: ${{ secrets.APPGOAT_API_KEY }}
run: |
git clone --depth 1 https://github.com/appgoat/skills /tmp/appgoat-skills
mkdir -p .claude/skills
cp -r /tmp/appgoat-skills/skills/* .claude/skills/
# Hosted AppGoat MCP server; the key goes into a file, not the command line.
jq -n --arg key "$APPGOAT_API_KEY" \
'{mcpServers: {appgoat: {type: "http", url: "https://api.appgoat.ai/mcp", headers: {"x-api-key": $key}}}}' \
> "$RUNNER_TEMP/appgoat-mcp.json"
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Use the apple-ads-optimizer skill for all enabled campaigns with target CPA <amount> <currency>
and daily budget <amount>. This is an unattended run: instead of asking in step 7, call apply_action
once for every proposed action without guardrail warnings (this submits it for approval in the
dashboard) and reject_action the others with the reason. Never call apply_action with force.
claude_args: |
--mcp-config ${{ runner.temp }}/appgoat-mcp.json
--allowedTools mcp__appgoat__list_connections,mcp__appgoat__read_connection,mcp__appgoat__keyword_research,mcp__appgoat__propose_action,mcp__appgoat__apply_action,mcp__appgoat__reject_action,mcp__appgoat__get_action
Claude Code routine:
/schedule mondays 08:00 - Run the apple-ads-optimizer playbook for my enabled Apple Ads campaigns, target CPA <amount>. Submit the proposed changes for approval (apply_action once each) and send me the summary table and the approvals link.