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.
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.
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.
ads_read, assigned to one intended ad account.campaigns.list and insights.read exist as callable operations.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.
| Operation | Permitted purpose | Retained fields | Explicitly absent |
|---|---|---|---|
campaigns.list | Map campaign IDs to names and delivery state. | campaign_id, campaign_name, status, effective_status | Create, rename, pause, delete, budget, targeting, or creative controls. |
insights.read | Build a bounded campaign-level report. | campaign_id, campaign_name, spend, impressions, clicks | Ad-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.
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.
These are deterministic policy-fixture counts. They are not Meta API conformance results and do not describe a live ad account.
| Mutation | Observed decision | Why it matters |
|---|---|---|
| Change the operation to campaign creation, budget update, or delete. | BLOCK_OPERATION | The model cannot invent a write verb the tool never exposes. |
Add ads_management or business_management. | BLOCK_PERMISSION | A reporting credential stays narrow even if broader scopes seem convenient. |
| Put the token in a workspace file, argument, or log. | BLOCK_SECRET_HANDLING | Bearer material never becomes model-visible evidence. |
Swap the allowed account, omit act_, or broaden the field list. | BLOCK_ACCOUNT / BLOCK_FIELDS | A plausible report from the wrong asset is still wrong. |
| Use today, reverse the dates, or exceed 31 days. | BLOCK_TIME_RANGE | Comparisons do not mix complete days with a partial day. |
| Continue pagination on another host, version, or account. | BLOCK_CURSOR_SCOPE | An 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.
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.
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
- Create or select the Meta developer app and add the Marketing API product.
- Assign the intended person or system user to exactly one test ad account.
- Issue a reporting credential with
ads_read; do not add management scopes. - Verify the account identity outside the agent, including name, currency, and timezone.
- Store the token in the adapter's protected runtime source. Run the secret audit before activation.
- Expose only
meta_ads_reportto the OpenClaw agent. Remove generic runtime paths that can reach Graph API. - Run
campaigns.list, then one boundedinsights.readover completed days. - Prove that write verbs, another account, extra fields, today's date, and a tampered cursor all fail before egress.
- 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
- Meta Marketing API documentation — the Graph API advertising surface and object model.
- Meta Node.js Business SDK at commit
584ba8d…— access-token initialization,act_…account form, explicit fields, separate CRUD methods, App Secret Proof recommendation, and cursor pagination. - Official SDK BasicExample — distinct account, campaign, active-ad, and insights reads with explicit fields.
- OpenClaw secrets documentation — SecretRef persistence boundary, runtime resolution, and audit expectations.
- OpenClaw sandbox and tool-policy documentation — deny/allow behavior and why generic runtime access is not a read-only guarantee.
- Local admission fixture — 36 decisions, zero mismatches, 3 scoped reads, and 33 blocked requests. SHA-256:
c59ad21f41684f26dbaf86a6bc95658e286e4a4cd4f05b6b62a07040d5e3e492.
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.