2026-05-23

Connect Meta Ads to OpenClaw Without Spend Authority

A 36-case read-only adapter drill shows how to give an OpenClaw agent useful Meta campaign reporting without exposing management scopes, arbitrary fields, or a write path.

Connect Meta Ads to OpenClaw Without Spend Authority cover illustration

Read-only connector reconstruction

The safest way to connect Meta Ads to OpenClaw has no campaign-write path. Give one purpose-built adapter a read token, one ad-account allowlist, two report operations, and a deliberately short field list. Keep the token outside the prompt. Keep generic shell access outside the reporting agent.

That is less glamorous than handing an agent ads_management. It is also enough to answer the first questions that matter: which campaigns are active, what they spent, whether delivery changed, and which rows deserve a human review.

We reduced this design to a deterministic 36-case admission ledger. Three requests passed: one campaign list, one seven-day insights query, and one cursor-bound continuation. The other 33 were refused before any network call.

Retained result: 36 of 36 expected decisions matched. The gate allowed 3 scoped reads and blocked 33 cases across 15 reasons, including write verbs, excess permissions, plaintext secrets, account drift, field expansion, partial-day reporting, and cursor escape. Fixture SHA-256: c59ad21f41684f26dbaf86a6bc95658e286e4a4cd4f05b6b62a07040d5e3e492.

The dangerous part is authority, not OAuth

A valid Meta token does not make an agent safe. It proves that the caller can use whatever authority the token carries. If that authority includes campaign creation, budget changes, or deletion, a prompt such as “only prepare a report” is the last line of defense. Prompts are useful instructions. They are poor access-control systems.

The official Meta Node.js Business SDK makes the distinction unusually visible. Its README presents separate methods for reading, creating, updating, and deleting Marketing API objects. The same SDK object can list campaigns and create one. The safety boundary therefore belongs before the SDK dispatch, not inside the model's prose.

For the first useful connection, issue a credential intended for reporting and expose only a reporting-shaped tool. Do not expose the general SDK client, raw HTTP, or a generic exec route to the same agent.

Meta boundaryA token with ads_read, assigned to one intended ad account.
Adapter boundaryOnly campaigns.list and insights.read exist as callable operations.
OpenClaw boundaryThe agent sees the adapter tool, not a shell that can improvise Graph API calls.
Review boundaryThe output is a private artifact with account, timezone, date range, fields, and paging receipts.

One adapter, two verbs

The connector tested here is intentionally boring. campaigns.list retrieves identity and delivery state. insights.read retrieves a campaign-level metric slice. Everything else is an unknown operation and fails closed.

OperationPermitted purposeRetained fieldsExplicitly absent
campaigns.listMap campaign IDs to names and delivery state.campaign_id, campaign_name, status, effective_statusCreate, rename, pause, delete, budget, targeting, or creative controls.
insights.readBuild a bounded campaign-level report.campaign_id, campaign_name, spend, impressions, clicksAd-level expansion, arbitrary breakdowns, current partial day, and asynchronous jobs.

The field list is not a convenience default. It is part of the policy. Meta's SDK README recommends asking only for fields actually needed because broad field requests add response time. They also add review ambiguity: a report adapter should not quietly become an account-discovery or audience-extraction tool.

Screen-print illustration of one account key and a small set of metric shapes passing through a narrow gate while oversized control tools remain barred
The small gate is the connector contract: one account, a few fields, and no route to campaign controls.

The policy file carries no credential

Keep operational policy reviewable and keep the bearer token somewhere else. A minimal policy document can be committed because it contains identifiers and limits, not secrets:

{
  "tool": "meta_ads_report",
  "graphVersion": "v25.0",
  "allowedAccountIds": ["act_123456789012345"],
  "allowedOperations": ["campaigns.list", "insights.read"],
  "allowedFields": [
    "campaign_id", "campaign_name", "status",
    "effective_status", "spend", "impressions", "clicks"
  ],
  "maxWindowDays": 31,
  "maxRowsPerPage": 500
}

v25.0 is the pinned version in this dated fixture, not a promise that it is the right version for every deployment. Select a version your app supports, record it explicitly, test the upgrade, and change the policy as a reviewed commit. A magic value such as latest turns a vendor release into an unreviewed production change.

The token should be resolved by the adapter at runtime. Do not place it in a workspace JSON file, command argument, prompt, transcript, or report. OpenClaw's SecretRef machinery can keep supported credentials out of plaintext config, but its own documentation is clear about the limit: a SecretRef reduces persistence exposure; it is not process isolation. Any arbitrary file the agent can read remains readable.

That gives the integration two separate audits:

  • Does the adapter resolve the token from a protected runtime source without printing it?
  • Can the agent reach any generic runtime or filesystem surface that bypasses the adapter?

Passing the first audit while failing the second is security theatre.

Thirty-six requests walked into the gate

The base case queried one allowlisted act_… account over HTTPS, using a pinned Graph version, GET, ads_read, App Secret Proof, five allowed metrics, campaign level, a completed seven-day interval, and a 100-row page. The token was represented only as a SecretRef-derived value and never appeared in arguments or logs.

36synthetic requests in the retained fixture
3scoped reads allowed after every gate
33requests refused before network dispatch
15distinct block reasons kept observable

These are deterministic policy-fixture counts. They are not Meta API conformance results and do not describe a live ad account.

MutationObserved decisionWhy it matters
Change the operation to campaign creation, budget update, or delete.BLOCK_OPERATIONThe model cannot invent a write verb the tool never exposes.
Add ads_management or business_management.BLOCK_PERMISSIONA reporting credential stays narrow even if broader scopes seem convenient.
Put the token in a workspace file, argument, or log.BLOCK_SECRET_HANDLINGBearer material never becomes model-visible evidence.
Swap the allowed account, omit act_, or broaden the field list.BLOCK_ACCOUNT / BLOCK_FIELDSA plausible report from the wrong asset is still wrong.
Use today, reverse the dates, or exceed 31 days.BLOCK_TIME_RANGEComparisons do not mix complete days with a partial day.
Continue pagination on another host, version, or account.BLOCK_CURSOR_SCOPEAn opaque continuation cannot silently escape the original query envelope.

“Read only” in a prompt is not a control

OpenClaw separates tool policy from sandbox placement. Its documentation says deny wins, and that a non-empty allowlist blocks tools not listed. It also calls out the uncomfortable detail: allowing generic exec does not become read-only merely because file-editing tools are denied. Shell commands can still make network requests and mutate remote systems.

So the Meta reporting agent should receive the named adapter and the minimum unrelated tools it needs. If the agent can run arbitrary curl, import the full Business SDK, or read the token source directly, the adapter is advisory rather than authoritative.

The same logic applies on Meta's side. ads_read is the credential boundary for this design. Do not request ads_management “for later.” Later should require a different credential, a different tool, a reviewed payload schema, and an approval record tied to the exact object and proposed value.

Operational rule: the reporting agent may explain and draft. It cannot pause delivery, change a budget, create an ad, edit targeting, or broaden its own fields. None of those actions exists in its tool contract.

Pagination is a fresh security decision

Meta collections are cursor-paginated. The official SDK represents a page as a cursor-aware collection with next and previous methods. That convenience can hide a second request behind a harmless-looking continuation.

Store the cursor as opaque data, but bind the continuation to the original host, Graph version, account, operation, fields, level, and date range. Reject a cursor if any of those values changes. Do not follow an arbitrary URL supplied to the model, and never copy a bearer token from one origin to another.

Screen-print illustration of report cards circulating through one anchored scope ring while an off-scope continuation is diverted into a closed chute
A cursor continues the same report. If it changes the host, version, or account, it is a different request and the gate closes.

The fixture allowed one properly scoped continuation and blocked host, version, and account substitutions. A production adapter should also cap total pages, total rows, elapsed time, and retry count. Rate-limit headers belong in telemetry, not in the prompt.

A report needs receipts, not just a summary

An agent can write a confident paragraph from incomplete data. The artifact behind the paragraph should make incompleteness obvious. Save the account ID, account timezone, Graph version, exact date interval, requested fields, level, page count, row count, retrieval time, and a hash of the raw response. Mark whether pagination completed.

Use complete days for routine comparisons. “Yesterday” must be resolved in the ad account's timezone, not the server timezone and not the operator's laptop timezone. Current-day spend can be useful for pacing, but it is a separate product with separate labels and thresholds.

For a first daily report, five metrics are enough to catch many operational problems:

  • spend — how much delivery consumed;
  • impressions — whether delivery occurred;
  • clicks — a simple response signal;
  • campaign identity — the stable object being discussed;
  • delivery state — whether a campaign was actually eligible to run.

Conversions, attribution windows, action types, currency normalization, and breakdowns deserve their own evidence contract. Adding them casually can make two rows look comparable when Meta calculated them under different dimensions.

The first run should be impossible to bill

  1. Create or select the Meta developer app and add the Marketing API product.
  2. Assign the intended person or system user to exactly one test ad account.
  3. Issue a reporting credential with ads_read; do not add management scopes.
  4. Verify the account identity outside the agent, including name, currency, and timezone.
  5. Store the token in the adapter's protected runtime source. Run the secret audit before activation.
  6. Expose only meta_ads_report to the OpenClaw agent. Remove generic runtime paths that can reach Graph API.
  7. Run campaigns.list, then one bounded insights.read over completed days.
  8. Prove that write verbs, another account, extra fields, today's date, and a tampered cursor all fail before egress.
  9. Archive the raw response hash and the report receipt beside the human-readable summary.

This sequence creates value before it creates spend authority. If reporting fails, the worst outcome is a missing report—not a live campaign mutation.

Writes belong in a different system

Eventually, a team may want an agent to draft a budget change or pause recommendation. Drafting does not require Meta write access. The agent can emit a proposed change document containing the object ID, current value, proposed value, reason, evidence window, rollback condition, and expiry.

An independent executor can validate that document, show the exact diff to a human, require approval, use a separate short-lived management credential, perform one bounded mutation, and verify the result. That executor should have idempotency, an audit log, rate limits, and a kill switch. It should not be the reporting adapter with one extra flag.

The tradeoff is extra plumbing. It is worthwhile because report retrieval is frequent and low consequence, while spend mutation is rare and high consequence. Combining them optimizes for code reuse at the expense of incident containment.

Pinned evidence and the untested boundary

The evidence was static official source review plus deterministic local JavaScript. We did not access a Meta account, developer app, Business Manager, ad account, access token, app secret, Graph API endpoint, campaign, insight row, credential value, external write, ad spend, or production OpenClaw configuration. Before calling a real connector production-ready, repeat the negative cases against a dedicated test account and record the actual Meta error and rate-limit behavior.