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
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,25 @@ REDIS_URL=
# Environment
ENVIRONMENT=development
LOG_LEVEL=INFO

# CrewAI
CREW_MEMORY_ENABLED=false

# =============================================================================
# Meta Ads API Integration
# =============================================================================
# System user access token — generate in Meta Business Manager:
# Business Settings → System Users → <your system user> → Generate Token
# Required scopes: ads_management, ads_read, business_management
META_ACCESS_TOKEN=

# Ad account ID — assign system user to ad account first:
# Business Settings → System Users → Add Assets → Ad Accounts
# Format: act_XXXXXXXXX
META_AD_ACCOUNT_ID=

# Facebook Page ID — Business Manager → Pages → click page → copy ID from URL
META_PAGE_ID=

# Graph API version (for reach estimates)
META_API_VERSION=v21.0
241 changes: 241 additions & 0 deletions docs/integration/meta-ads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Meta Ads Integration

The buyer agent integrates with the Meta Marketing API to book and report on social channel campaigns across Facebook and Instagram. This page covers authentication setup, the booking flow, and reporting endpoints.

The buyer agent calls `graph.facebook.com` directly using a system user access token. This is the same endpoint used by the official Meta Marketing API SDKs — no browser OAuth is required.

## Configuration

| Variable | Type | Default | Description |
|---|---|---|---|
| `META_ACCESS_TOKEN` | `str` | `""` | System user access token from Meta Business Manager |
| `META_AD_ACCOUNT_ID` | `str` | `""` | Ad account ID — format `act_XXXXXXXXX` |
| `META_PAGE_ID` | `str` | `""` | Facebook Page ID — required for ad creative creation |
| `META_API_VERSION` | `str` | `v21.0` | Meta Graph API version |

Add these to your `.env` file:

```bash
META_ACCESS_TOKEN=your-system-user-token
META_AD_ACCOUNT_ID=act_XXXXXXXXX
META_PAGE_ID=XXXXXXXXX
META_API_VERSION=v21.0
```

### Generating a System User Token

1. Open **Meta Business Manager → Business Settings → System Users**
2. Create or select a system user
3. Click **Generate Token** → select your app
4. Required scopes: `ads_management`, `ads_read`, `business_management`
5. Assign the system user to your ad account: **Business Settings → System Users → Add Assets → Ad Accounts**

### Installation

```bash
pip install -e ".[meta]"
```

!!! note "Sandbox accounts"
Meta provides sandbox ad accounts for development. Campaigns created in a sandbox account are API-only and not visible in the Ads Manager UI. They are created in `PAUSED` state and never serve impressions.

---

## Booking Flow

When a booking brief includes `"channels": ["social"]` or `"channels": ["meta"]`, the buyer agent routes through the Meta booking path.

```mermaid
sequenceDiagram
participant Buyer as Buyer Agent
participant Meta as graph.facebook.com

Buyer->>Meta: GET /{account}/reachestimate
Meta-->>Buyer: Reach + CPM estimates

Note over Buyer: Awaiting approval

loop Per placement (Instagram Reels, Facebook Feed, etc.)
Buyer->>Meta: POST /act_{id}/campaigns
Meta-->>Buyer: campaign_id (PAUSED)

Buyer->>Meta: POST /act_{id}/adsets
Meta-->>Buyer: ad_set_id (PAUSED)
end
```

### Research Phase

The `SocialCrew` uses `MetaInventoryTool` to call `GET /{account}/reachestimate` and estimate reach and CPM for four placements:

- Instagram Reels
- Facebook Video Feeds
- Instagram Feed
- Facebook Feed

If the reach estimate API returns an error, the tool falls back to static estimates.

### Booking Phase

After the user approves recommendations, the buyer agent creates two resources per placement:

| Step | API Call | Status |
|---|---|---|
| 1 | `POST /act_{id}/campaigns` | PAUSED |
| 2 | `POST /act_{id}/adsets` | PAUSED |

!!! note "Creative step"
Ad creative creation (step 3) requires an uploaded image asset. This step is skipped — campaign and ad set creation is sufficient to confirm booking.

### Objective Mapping

| IAB Objective | Meta Objective |
|---|---|
| `brand_awareness`, `reach` | `OUTCOME_AWARENESS` |
| `traffic` | `OUTCOME_TRAFFIC` |
| `conversions` | `OUTCOME_SALES` |
| `video_views` | `OUTCOME_ENGAGEMENT` |
| `lead_generation` | `OUTCOME_LEADS` |

### Budget Allocation

The `PortfolioCrew` LLM allocates budget across channels based on campaign objectives, audience fit, and KPIs. If `channels` is specified in the brief, the LLM is instructed to allocate only to those channels:

```json
{
"channels": ["branding", "ctv", "social"],
"budget": 15000
}
```

The LLM will distribute the `$15,000` across `branding`, `ctv`, and `social` based on which best fits the campaign objectives.

### Example Booking Request

```
POST /bookings
Content-Type: application/json
```

```json
{
"brief": {
"name": "Summer Campaign 2026",
"objectives": ["brand_awareness", "reach"],
"budget": 5000,
"start_date": "2026-06-01",
"end_date": "2026-06-30",
"channels": ["social"],
"target_audience": {
"demographics": {"age": "18-45"},
"interests": ["technology", "gaming"]
}
},
"auto_approve": false
}
```

Poll `GET /bookings/{job_id}` until `status: awaiting_approval`, then approve:

```
POST /bookings/{job_id}/approve-all
```

Booked lines for the social channel will have `booking_status: "paused"`.

---

## Reporting

### List Campaigns

Returns all campaigns in the ad account — no booking job ID required.

```
GET /meta/campaigns?limit=10
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | `int` | `10` | Number of campaigns to return |

```json
{
"ad_account_id": "act_XXXXXXXXX",
"campaigns": [
{
"id": "23856xxxxxxxxx",
"name": "Summer Campaign 2026 — Instagram Reels",
"effective_status": "PAUSED",
"objective": "OUTCOME_AWARENESS",
"daily_budget": "125000",
"created_time": "2026-05-11T15:35:59+0530"
}
],
"count": 10
}
```

### Campaign Report

Returns campaign details combined with delivery insights for one or more campaign IDs.

```
GET /meta/report?campaign_ids=CAMPAIGN_ID_1,CAMPAIGN_ID_2&date_preset=last_30d
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `campaign_ids` | `string` | required | Comma-separated Meta campaign IDs |
| `date_preset` | `string` | `last_30d` | `last_7d` / `last_14d` / `last_30d` / `last_90d` |

```json
{
"ad_account_id": "act_XXXXXXXXX",
"date_preset": "last_30d",
"campaigns": [
{
"campaign_id": "23856xxxxxxxxx",
"campaign_name": "Summer Campaign 2026 — Instagram Reels",
"status": "PAUSED",
"objective": "OUTCOME_AWARENESS",
"daily_budget": "125000",
"created_time": "2026-05-11T15:35:59+0530",
"spend": 0.0,
"impressions": 0,
"reach": 0,
"frequency": 0.0,
"clicks": 0,
"ctr": 0.0,
"cpm": 0.0
}
],
"summary": {
"total_spend": 0.0,
"total_impressions": 0,
"total_clicks": 0,
"total_reach": 0
}
}
```

### Job-Scoped Report

To report on all campaigns booked within a specific job:

```
GET /reports/{job_id}?date_range=last_30d
```

The buyer agent automatically identifies Meta campaign IDs from `booked_lines` and pulls insights for each. IAB OpenDirect order IDs in the same job are routed to the seller agent's delivery performance endpoint. See [Bookings API](../api/bookings.md) for details.

!!! tip "Access token security"
The Meta access token is never exposed in API error responses. It is automatically redacted to `***` before any error message reaches the HTTP response.

---

## Related

- [Seller Agent Integration](seller-agent.md) --- How buyer and seller agents communicate
- [Bookings API](../api/bookings.md) --- Full booking flow reference
- [Configuration Reference](../guides/configuration.md) --- All environment variables
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ nav:
- Integration:
- Seller Agent Guide: integration/seller-agent.md
- OpenDirect Protocol: integration/opendirect.md
- Meta Ads: integration/meta-ads.md
- AI Assistant Setup:
- Claude (Desktop & Web): claude-desktop-setup.md
- ChatGPT, Codex & AI IDEs: multi-client-setup.md
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ dependencies = [
]

[project.optional-dependencies]
meta = [
"meta-ads-cli>=0.1.0",
]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
Expand Down
31 changes: 31 additions & 0 deletions src/ad_buyer/agents/level2/social_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Social media channel specialist agent (Meta Ads)."""

from crewai import Agent

from ...config.settings import settings


def create_social_agent() -> Agent:
"""Create the Social Media Specialist agent for Meta Ads campaigns."""
return Agent(
role="Social Media Advertising Specialist",
goal=(
"Identify the best Meta Ads placements (Facebook, Instagram, Audience Network) "
"for campaigns targeting social media audiences. Evaluate reach, CPM, and "
"audience alignment. Only recommend placements actually returned by the "
"search_meta_placements tool."
),
backstory=(
"Expert in Meta Ads ecosystem with deep knowledge of Facebook Feed, Instagram Reels, "
"Stories, and Audience Network inventory. Skilled at matching campaign objectives "
"(brand awareness, reach, conversions) to optimal Meta placements "
"and bidding strategies."
),
llm=settings.manager_llm_model,
verbose=settings.crew_verbose,
allow_delegation=False,
max_iter=settings.crew_max_iterations,
)
76 changes: 76 additions & 0 deletions src/ad_buyer/clients/meta_ads_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Meta Ads reach-estimate client — Graph API reach estimates only.

Used in the research phase to estimate audience reach + CPM before booking.
All other operations (booking, reporting, lifecycle) use MetaAdsClient
(meta_ads_client.py).
"""

import json

import httpx


class MetaAdsAPIClient:
"""Graph API httpx client scoped to reach estimation.

Calls graph.facebook.com/v{version}/{account}/reachestimate
directly using a system user access token.
"""

def __init__(
self,
access_token: str,
ad_account_id: str,
api_version: str = "v21.0",
):
self._token = access_token
self._account_id = (
ad_account_id if ad_account_id.startswith("act_") else f"act_{ad_account_id}"
)
self._base = f"https://graph.facebook.com/{api_version}"

def _get(self, path: str, params: dict | None = None) -> dict:
all_params = {"access_token": self._token, **(params or {})}
r = httpx.get(f"{self._base}/{path}", params=all_params, timeout=30.0)
r.raise_for_status()
return r.json()

def get_reach_estimate(
self,
targeting: dict,
daily_budget: float,
optimize_for: str = "REACH",
) -> dict:
"""Estimate reach for a targeting + daily budget combination.

Args:
targeting: Graph API targeting spec:
{
"geo_locations": {"countries": ["US"]},
"age_min": 25, "age_max": 54
}
daily_budget: Daily budget in USD (converted to cents internally)
optimize_for: REACH | IMPRESSIONS | LINK_CLICKS

Returns:
{ "users_lower_bound": int, "users_upper_bound": int,
"estimate_ready": bool }
"""
return self._get(
f"{self._account_id}/reachestimate",
{
"targeting_spec": json.dumps(targeting),
"optimize_for": optimize_for,
"daily_budget": int(daily_budget * 100),
},
)

def get_ad_account(self) -> dict:
"""Get ad account metadata — name, currency, timezone."""
return self._get(
self._account_id,
{"fields": "id,name,currency,timezone_name,account_status"},
)
Loading
Loading