Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ META_BUSINESS_ID=
META_APP_ID=
META_APP_SECRET=

# Live data backend: graph (default, curl-only) | social-cli (@vishalgojha/social-cli)
# social-cli adds native --level insights, mutations, and OAuth but is slower.
META_KIT_BACKEND=graph

# Graph API (used by the graph backend for breakdowns the official CLI can't do)
GRAPH_API_VERSION=v21.0

# Kit behavior
# NOTE: values here do NOT override variables already set on the command line,
# so `META_KIT_MODE=mock ./run.sh ...` always wins over this file.
META_KIT_MODE=mock
META_KIT_OUTPUT=json
META_KIT_DEFAULT_PRESET=last_7d
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,13 @@ No Ads Manager required at any step.

## Running With OpenClaw

This kit is built for [OpenClaw](https://openclaw.ai), an open-source AI agent framework. Under the hood, reports route through `./run.sh` → `scripts/meta-kit.sh` → Meta's official Ads CLI (`meta`).
This kit is built for [OpenClaw](https://openclaw.ai), an open-source AI agent framework. Under the hood, reports route through `./run.sh` → `scripts/meta-kit.sh`.

**Live data backends** (`META_KIT_BACKEND`):
- `graph` (default) — direct Meta Marketing API via `curl`. Fast, zero extra deps. Campaign list via the official `meta` CLI.
- `social-cli` — [`@vishalgojha/social-cli`](https://www.npmjs.com/package/@vishalgojha/social-cli), the kit's original engine: native `--level` insights, mutations, OAuth, FB/IG/WhatsApp. `npm i -g @vishalgojha/social-cli`, then `META_KIT_BACKEND=social-cli`.

Both feed one shared transform, so report output is identical. The official Ads CLI's `insights get` is account-level only (no `--level`), which is why the breakdowns route through the Graph API or social-cli. See `scripts/lib/live-adapter.sh` and `scripts/lib/backend-social.sh`.

```bash
# Install OpenClaw
Expand Down
61 changes: 61 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,67 @@ META_KIT_MODE=read-only ./run.sh daily-check

The adapter writes sanitized read-only snapshots under `local/outputs/read-only/`.

### Live data backends

The non-mock data engine is selectable with `META_KIT_BACKEND`:

| Backend | Value | Needs | Notes |
|---------|-------|-------|-------|
| Graph (default) | `graph` | just `curl` | Direct Meta Marketing API. Fast (~2s), zero extra deps. Campaign list via the official `meta` CLI. |
| social-cli | `social-cli` | `npm i -g @vishalgojha/social-cli` | The kit's original engine. Native `--level` insights (async report runs), mutations, OAuth, FB/IG/WhatsApp. Slower (~50s — async jobs). |

```bash
# Default (Graph):
./run.sh daily-check

# social-cli backend:
npm i -g @vishalgojha/social-cli
social auth login --token "$ACCESS_TOKEN" --api facebook --no-open # or: social auth login (OAuth)
META_KIT_BACKEND=social-cli ./run.sh daily-check
```

Both backends produce identical report output — they feed the same internal
schema through one shared transform (`mk_assemble_insights`). The official Ads
CLI `insights get` is account-level only (no `--level`), which is why neither the
reports nor a "meta-cli backend" can do per-ad breakdowns alone; both real
backends solve that (Graph via `level=` params, social-cli via `--level`).

Override the Graph API version with `GRAPH_API_VERSION` (default `v21.0`).

**Pacing-target caveat:** the Graph backend sums active campaign + active ad-set
daily budgets (it has an account-level ad-sets edge). social-cli has no
account-level ad-sets edge, so its target reflects active *campaign* (CBO)
budgets only and under-reports accounts whose budgets live at the ad-set level.
Spend, winners, bleeders, and fatigue are identical across backends.

`.env` values do **not** override variables already set on the command line, so
`META_KIT_MODE=mock ./run.sh daily-check` always forces mock regardless of `.env`.

## Multi-brand / multi-client

Each report reads `AD_ACCOUNT_ID` (and `ACCESS_TOKEN`) at call time, so you can
run multiple brands without editing files. Two patterns:

```bash
# A) Inline per-run (same token, different account):
AD_ACCOUNT_ID=act_1111111111 ./run.sh daily-check
AD_ACCOUNT_ID=act_2222222222 ./run.sh daily-check

# B) Per-client env files (different tokens per client):
# .env.clienta.local -> ACCESS_TOKEN + AD_ACCOUNT_ID for client A
# .env.clientb.local -> ACCESS_TOKEN + AD_ACCOUNT_ID for client B
META_KIT_ENV_FILE=.env.clienta.local ./run.sh daily-check
META_KIT_ENV_FILE=.env.clientb.local ./run.sh daily-check
```

All `.env` and `.env.*.local` files are gitignored — tokens never get committed.

## Tests

```bash
./scripts/test/test_live_adapter.sh # offline unit tests for the live adapter transform
```

---

## Mutations: approval-only
Expand Down
108 changes: 108 additions & 0 deletions scripts/lib/backend-social.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env bash

# social-cli backend: routes live data through @vishalgojha/social-cli ("social").
#
# Why offer it alongside the default Graph backend: social-cli was the kit's
# ORIGINAL engine and natively supports `marketing insights --level campaign|ad
# --time-increment` (via async report runs) plus mutations (pause/resume/
# set-budget) and OAuth login. The official Ads CLI dropped --level, which is why
# the kit broke in live mode. This backend brings the richer surface back as an
# opt-in (META_KIT_BACKEND=social-cli).
#
# Trade-off: social-cli insights use async report jobs (slower, ~10s/poll) but
# are robust. The Graph backend is faster and dependency-free. Same internal
# schema either way — both feed the shared pure transform mk_assemble_insights.

# Strip social-cli's stdout preamble (banner note / progress lines) and emit JSON
# starting at the first line that begins with [ or {.
mk_social_strip() {
awk 'f || /^[[{]/ { f=1; print }'
}

mk_social_installed() {
command -v social >/dev/null 2>&1
}

# mk_social_list_json <social args...> -> clean JSON (array/object) on stdout
mk_social_list_json() {
social --no-banner "$@" --json 2>/dev/null | mk_social_strip
}

# mk_social_insights_data <account> <level> <preset> [time_increment]
# Uses --export to a temp file so progress lines never pollute the JSON.
# Returns {data: [...]} to match the bundle shape the transform expects.
mk_social_insights_data() {
local acct="$1" level="$2" preset="$3" inc="${4:-}"
local f out fields
f="$(mktemp "${TMPDIR:-/tmp}/meta-kit-soc-XXXXXX.json")"

case "$level" in
campaign) fields="campaign_id,campaign_name,spend,ctr,cpc" ;;
ad) if [[ -n "$inc" ]]; then fields="ad_id,ad_name,ctr,frequency"; else fields="ad_id,ad_name,spend,ctr,cpc,frequency"; fi ;;
*) fields="spend,ctr,cpc" ;;
esac

local args=(--no-banner marketing insights "$acct" --level "$level" --preset "$preset" --fields "$fields" --export "$f" --export-format json)
[[ -n "$inc" ]] && args+=(--time-increment "$inc")

social "${args[@]}" >/dev/null 2>&1

if [[ -s "$f" ]] && jq -e . "$f" >/dev/null 2>&1; then
out="$(jq '{data: .}' "$f")"
else
out='{"data":[]}'
fi
rm -f "$f"
printf '%s' "$out"
}

# mk_fetch_bundle_social -> the 9-key raw bundle consumed by mk_assemble_insights.
mk_fetch_bundle_social() {
local acct status camps_raw camp7 camp_today ad7 ad_daily
acct="$(mk_normalize_account "${AD_ACCOUNT_ID:-${META_AD_ACCOUNT:-}}")"

status="$(social --no-banner marketing status "$acct" --json 2>/dev/null | mk_social_strip)"
camps_raw="$(mk_social_list_json marketing campaigns "$acct")"
camp7="$(mk_social_insights_data "$acct" campaign last_7d)"
camp_today="$(mk_social_insights_data "$acct" campaign today)"
ad7="$(mk_social_insights_data "$acct" ad last_7d)"
ad_daily="$(mk_social_insights_data "$acct" ad last_7d 1)"

# Campaigns: map social's `status` to effective_status; keep daily_budget for the target.
local campaigns
campaigns="$(jq '{data: [ (.[]? // empty) | {effective_status: .status, daily_budget: (.daily_budget // "0"), id: .id, name: .name, status: .status} ]}' <<<"${camps_raw:-[]}")"

# Ad sets: social-cli only lists ad sets per-campaign (no account-level edge),
# and those calls are rate-limit-prone. Rather than ship a pacing denominator
# that swings between runs, the social backend computes daily_budget_target from
# active CAMPAIGN budgets only (left empty here). This under-reports accounts
# whose budgets live at the ad-set level (non-CBO). The graph backend, which has
# an account-level adsets edge, includes ad-set budgets. Documented in SETUP.md.
local adsets='{"data":[]}'

# Account spend derived from campaign-level sums (saves two async jobs).
local acct_7d acct_today acct_meta
acct_7d="$(jq '{data:[{spend: (([(.data[]?.spend // "0")|tonumber]|add // 0) * 100 | round / 100 | tostring), account_currency: "USD"}]}' <<<"$camp7")"
acct_today="$(jq '{data:[{spend: (([(.data[]?.spend // "0")|tonumber]|add // 0) * 100 | round / 100 | tostring)}]}' <<<"$camp_today")"
local empty='{}'
acct_meta="$(jq '{id: (.account.id // ""), currency: (.account.currency // "USD")}' <<<"${status:-$empty}")"

jq -n \
--argjson acct_meta "$acct_meta" \
--argjson acct_7d "$acct_7d" \
--argjson acct_today "$acct_today" \
--argjson camp_7d "$camp7" \
--argjson camp_today "$camp_today" \
--argjson ad_7d "$ad7" \
--argjson ad_daily "$ad_daily" \
--argjson campaigns "$campaigns" \
--argjson adsets "$adsets" \
'{acct_meta:$acct_meta, acct_7d:$acct_7d, acct_today:$acct_today, camp_7d:$camp_7d, camp_today:$camp_today, ad_7d:$ad_7d, ad_daily:$ad_daily, campaigns:$campaigns, adsets:$adsets}'
}

# Campaign list for report_campaigns under the social backend.
mk_social_campaigns_list() {
local acct
acct="$(mk_normalize_account "${AD_ACCOUNT_ID:-${META_AD_ACCOUNT:-}}")"
mk_social_list_json marketing campaigns "$acct" | mk_normalize_campaigns
}
34 changes: 32 additions & 2 deletions scripts/lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,30 @@ mk_load_env() {
fi

if [[ -f "$env_file" ]]; then
# shellcheck disable=SC1090
set -a; source "$env_file"; set +a
# Load .env WITHOUT clobbering variables already set in the environment, so a
# command-line prefix wins over the file. This is what makes the documented
# overrides work, e.g. `META_KIT_MODE=mock ./run.sh ...` even when .env pins a
# mode. (A plain `set -a; source` would let the file overwrite the prefix.)
local _line _key _val
while IFS= read -r _line || [[ -n "$_line" ]]; do
_line="${_line%%$'\r'}"
_line="${_line#"${_line%%[![:space:]]*}"}" # ltrim
[[ -z "$_line" || "$_line" == \#* ]] && continue
_line="${_line#export }"
[[ "$_line" != *=* ]] && continue
_key="${_line%%=*}"
_val="${_line#*=}"
_key="${_key//[[:space:]]/}"
[[ -z "$_key" ]] && continue
# Strip one layer of surrounding quotes from the value, if present.
if [[ "$_val" == \"*\" || "$_val" == \'*\' ]]; then
_val="${_val:1:${#_val}-2}"
fi
# Only set if not already provided by the caller's environment.
if [[ -z "${!_key+x}" ]]; then
export "$_key=$_val"
fi
done < "$env_file"
fi

# Official Ads CLI env vars are ACCESS_TOKEN, AD_ACCOUNT_ID, and BUSINESS_ID.
Expand Down Expand Up @@ -85,6 +107,14 @@ mk_mode() {
printf '%s\n' "${META_KIT_MODE:-mock}"
}

# Live data backend (non-mock only):
# graph — direct Meta Marketing API via curl (default; zero extra deps)
# social-cli — @vishalgojha/social-cli ("social" binary): native --level
# insights + mutations + OAuth. Requires `npm i -g @vishalgojha/social-cli`.
mk_backend() {
printf '%s\n' "${META_KIT_BACKEND:-graph}"
}

mk_output_format() {
printf '%s\n' "${META_KIT_OUTPUT:-json}"
}
Expand Down
Loading