Skip to content
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ac8148c
feat(platform): add Meta Ads client (Graph API)
mrautela365 Jul 9, 2026
bdd7bf0
fix(review): add 429 rate-limit backoff to Meta client doRequest
mrautela365 Jul 10, 2026
00f6de1
fix(review): address Copilot findings on Meta client (PR #20)
mrautela365 Jul 10, 2026
a4ae5a7
fix(review): address second-round Copilot findings on Meta client (PR…
mrautela365 Jul 10, 2026
54d3021
fix(review): third-round Copilot findings on Meta client (PR #20)
mrautela365 Jul 11, 2026
a5b8e89
fix(review): round-4 Copilot findings on Meta client (PR #20)
mrautela365 Jul 11, 2026
d02b278
fix(review): round-5 Copilot findings on Meta client (PR #20)
mrautela365 Jul 11, 2026
cb6a533
fix(review): round-6 Copilot findings on Meta client (PR #20)
mrautela365 Jul 11, 2026
d8613b4
fix(review): ignore nil in WithHTTPClient (PR #20)
mrautela365 Jul 11, 2026
e6216df
fix(review): trim credentials at construction; don't fail throttled r…
mrautela365 Jul 11, 2026
24b1e99
fix(review): today-start ad set time; drop leads from field doc (PR #20)
mrautela365 Jul 11, 2026
2df2ba2
test(meta): pin the clock in date-based tests (PR #20)
mrautela365 Jul 11, 2026
d3d3f51
fix(review): exclude sanctioned geos, clock-pin remaining date tests,…
mrautela365 Jul 11, 2026
fdfcf31
fix(review): don't silently fall back to US; detect truncated respons…
mrautela365 Jul 11, 2026
7ae80b5
fix(review): exclude RU from Meta-eligible geos per PR #20 review
mrautela365 Jul 11, 2026
6e2fb0c
fix(review): treat ctx cancellation as fatal, fix geo guidance and SY…
mrautela365 Jul 11, 2026
d6e8ace
fix(review): validate host, surface orphan campaign id, fix test race…
mrautela365 Jul 11, 2026
446518c
fix(review): accurate ineligible-geo message and OKF log entry (PR #20)
mrautela365 Jul 11, 2026
92ad879
fix(review): distinguish caller-cancel from client HTTP timeout; hand…
mrautela365 Jul 11, 2026
d7e44aa
docs(meta): correct OKF concept doc for leads objective (PR #20)
mrautela365 Jul 11, 2026
730fbad
fix(review): use canonical tlf slug in campaign name fallback (PR #20)
mrautela365 Jul 11, 2026
7ec68e3
fix(review): honor currency offset for budget; reconcile leads mappin…
mrautela365 Jul 11, 2026
fdcd716
fix(review): surface orphaned creative id on non-fatal ad failure (PR…
mrautela365 Jul 11, 2026
02e0e6f
fix(review): account-currency budget semantics, validate copy limits,…
mrautela365 Jul 11, 2026
da6f3f6
fix(review): require explicit currency offset and non-empty project (…
mrautela365 Jul 11, 2026
228c972
fix(review): validate EventName up front; fix test-race on captured c…
mrautela365 Jul 11, 2026
48f119c
fix(review): default unset currency offset to 100 (PR #20)
mrautela365 Jul 11, 2026
51577cd
docs(test): correct CurrencyOffset test comment to match default-100 …
mrautela365 Jul 13, 2026
0c383c6
fix(review): O(1) placement dedup and note the leads mapping exceptio…
mrautela365 Jul 13, 2026
e58409b
fix(review): allocation-free truncate; guard unknown objective in bui…
mrautela365 Jul 13, 2026
5a7c99b
fix(review): use TrimPrefix to strip act_ in MetaURL (PR #20)
mrautela365 Jul 13, 2026
b569909
docs(okf): add recommended tags and timestamp frontmatter (PR: meta c…
mrautela365 Jul 13, 2026
cf24105
docs(okf): give the tags/timestamp change its own 2026-07-13 log head…
mrautela365 Jul 13, 2026
21b02fa
fix(review): reject removed Messenger Inbox placement (PR #20)
mrautela365 Jul 13, 2026
6a72ca6
fix(review): fail closed on unset Meta currency offset (PR #20)
mrautela365 Jul 13, 2026
7620e6b
fix(review): preflight-fetch Meta currency offset (PR #20)
mrautela365 Jul 13, 2026
0c12467
fix(review): guard offset overflow, correct offset docs (PR #20)
mrautela365 Jul 13, 2026
65fe6c5
fix(review): drop non-targetable ISO territories (PR #20)
mrautela365 Jul 13, 2026
95f3bed
fix(review): return partial result with created IDs on downstream fai…
mrautela365 Jul 13, 2026
766e47d
fix(review): derive currency offset from ISO code; clamp retry-after …
mrautela365 Jul 13, 2026
ccbdb72
fix(review): normalize EventName before generating names/UTM (PR #20)
mrautela365 Jul 13, 2026
6964463
fix(review): authoritative currency map + validate IDs (PR #20)
mrautela365 Jul 14, 2026
8d9383a
fix(review): reject inactive ad account; tighten currency docs (PR #20)
mrautela365 Jul 14, 2026
4be07e0
fix(review): expand supported-currency map; fix log ordering (PR #20)
mrautela365 Jul 14, 2026
0f99559
fix(review): normalize Objective (trim + lowercase) before use (PR #20)
mrautela365 Jul 14, 2026
ee04ad2
fix(review): IDR/HUF/COP/CRC/TWD are offset-1; reject conflicting off…
mrautela365 Jul 14, 2026
40b69f7
fix(review): report dropped ineligible/sanctioned geo targets (PR #20)
mrautela365 Jul 14, 2026
1dd939a
fix(review): abort on over-cap Retry-After; document authoritative of…
mrautela365 Jul 14, 2026
bd56863
fix(review): map leads to OUTCOME_TRAFFIC to avoid orphaning the camp…
mrautela365 Jul 14, 2026
5cbcf4f
fix(ci): compose test userinfo URLs at runtime to satisfy secretlint …
mrautela365 Jul 14, 2026
b86995f
fix(review): name dropped geos; preserve Graph envelope on rate-limit…
mrautela365 Jul 14, 2026
6e5c3df
fix(review): don't reject ANY_ACTIVE(201) accounts; reject malformed …
mrautela365 Jul 14, 2026
48ed506
fix(review): validate composed creative name length before any POST (…
mrautela365 Jul 14, 2026
0657619
fix(review): drop stray Date segment from Meta campaign name
mrautela365 Jul 14, 2026
f31a4a9
fix(review): normalize inputs in Meta validation and naming helpers
mrautela365 Jul 14, 2026
00388f2
fix(review): trim RegistrationURL in place so UTM build uses clean base
mrautela365 Jul 14, 2026
f3e2e7b
fix(review): restore reddit log entries, dedup audience_network, raw …
mrautela365 Jul 14, 2026
0de1bf6
fix(review): raw Retry-After as authoritative, harden buildUTMURL tri…
mrautela365 Jul 14, 2026
1caa8f9
fix(review): blank-only geos default to US, strict variant + date checks
mrautela365 Jul 14, 2026
36fe887
fix(review): ambiguous-outcome reconciliation, UTM secret stripping, …
mrautela365 Jul 14, 2026
5c8d874
fix(review): gate error-envelope unmarshal to non-2xx, move ISO table…
mrautela365 Jul 14, 2026
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
1 change: 1 addition & 0 deletions docs/knowledge/code/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [internal/infrastructure/postgres](internal-infrastructure-postgres.md) - PostgreSQL pool (otelpgx), migrations, repositories, and Ready() for readiness probes.
* [internal/middleware](internal-middleware.md) - Package middleware provides HTTP middleware for the service.
* [internal/platform/reddit](internal-platform-reddit.md) - Reddit Ads API v3 client: OAuth2 token refresh and Campaign -> Ad Group -> Ad creation.
* [internal/platform/meta](internal-platform-meta.md) - Meta (Facebook/Instagram) Ads Graph API client: Campaign -> Ad Set -> Ad creation with objective mapping and geo/budget validation.
* [internal/platform/twitter](internal-platform-twitter.md) - X (Twitter) Ads v12 client: OAuth 1.0a signing and the campaign -> line_item -> promoted_tweet creation flow.
* [internal/service](internal-service.md) - Campaign service business logic, including Readyz (DB-backed readiness) and Livez (process-only liveness).
* [pkg/constants](pkg-constants.md) - Application-wide constants, including PG* and DATABASE_URL environment variable names.
Expand Down
93 changes: 93 additions & 0 deletions docs/knowledge/code/internal-platform-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
type: "Go Package"
title: "internal/platform/meta"
description: "Meta (Facebook/Instagram) Ads Graph API client: Campaign -> Ad Set -> Ad creation with objective mapping and geo/budget validation."
resource: "internal/platform/meta"
tags:
- platform-client
- meta
- facebook-ads
- graph-api
- go-package
timestamp: "2026-07-13T19:22:00Z"
---

# internal/platform/meta

Package meta provides a Go client for the Meta (Facebook/Instagram) Ads Graph
API, ported from the upstream TypeScript `meta-ads.service.ts` client.
Credentials and account configuration are injected via `NewClient`; the client
never reads the process environment and uses only the standard library.

Authentication is a Graph API Bearer access token. `CreateCampaign` drives the
Campaign -> Ad Set -> Ad(s) hierarchy, creating everything PAUSED, with
objective->parameter mapping (awareness/traffic/engagement/leads/conversions),
placement/promoted-object building, and UTM URL construction that preserves any
URL fragment.

The `leads` objective INTENTIONALLY DIVERGES from the `@lfx-one/shared` TS
contract (`campaign.constants.ts` maps leads -> LEAD_GENERATION with a page_id
promoted object). LEAD_GENERATION optimization requires the ad creative to carry
an on-Facebook instant lead form (`lead_gen_form_id`), which this client does not
construct — it only builds a website-click creative pointing at the registration
URL. Adopting LEAD_GENERATION would fail at ad-set/ad creation, after the paid
campaign already exists. To stay fail-safe, `leads` runs an interim WEBSITE-TRAFFIC
campaign — OUTCOME_TRAFFIC optimizing for LINK_CLICKS to the registration
(lead-capture) URL, with no promoted object. OUTCOME_TRAFFIC is used (not
OUTCOME_LEADS) because OUTCOME_LEADS + LINK_CLICKS requires a `pixel_id` +
`custom_event_type` that this interim flow does not supply — that pairing would
create the campaign then fail at the ad set, orphaning it. OUTCOME_TRAFFIC
supports LINK_CLICKS with no pixel requirement, so the flow is spendable
end-to-end. Full LEAD_GENERATION / instant-form (or OUTCOME_LEADS + pixel) parity
with the TS contract is deferred (LFXV2-2665).

Inputs are validated up front, before any mutating call: geo targets are checked
against ISO 3166-1 alpha-2 and comprehensively-sanctioned countries are
excluded; per-variant copy is rejected up front when it exceeds Meta's limits
(primary text 125, headline 40, description 30 characters, counted by rune) so
over-limit copy fails before any paid campaign/ad-set exists rather than at
non-fatal creative creation; `CampaignInput.Budget` is denominated in the ad
ACCOUNT's own currency — the client does NO foreign-exchange conversion, so the
caller must pass an amount already in that currency — and it is bounded
(rejecting rounds-to-zero and overflow-scale values) then converted to minor
units by multiplying by the account's minor-unit offset
(`AccountConfig.CurrencyOffset`) rather than a hardcoded ×100. That offset is
DERIVED from the account's ISO 4217 currency code, not fetched: the Meta AdAccount
node exposes only `currency` (the ISO code) — it does NOT expose a
`currency_offset` field (only the separate Currency node does). CreateCampaign
maps the code through an AUTHORITATIVE supported-currency table
(`currencyMinorUnitOffset`), which is the single source of truth: the zero-decimal
currencies (JPY, KRW, CLP, VND, and the rest of the standard set) map to 1, and the
enumerated two-decimal currencies (USD, EUR, GBP, and the other supported majors)
map to 100. There is NO fall-through default — a code absent from the table
(blank, or a well-formed-but-unknown code such as `ZZZ`) is treated as unsupported.
The offset is never guessed: when `AccountConfig.CurrencyOffset` is unset (zero) — the normal
case for a dispatch built from a persisted connection, which carries only
account/page/app IDs — CreateCampaign fetches the account's `currency` (ISO code)
from the ad-account object during the account preflight, BEFORE any mutating call,
derives the offset from it, and fails closed if the currency is unknown or absent.
Silently defaulting to 100 would encode a zero-decimal-currency
(JPY/KRW/CLP) budget 100× too high, and a warning after resource creation cannot
prevent that budget from being activated. The account currency is
authoritative: a caller MAY set a positive `CurrencyOffset` as a FALLBACK, but if
the preflight returns a recognized currency whose true offset differs, the request
is REJECTED (a stale override would mis-scale the budget). The explicit offset is
only used when the preflight fails or its currency isn't in the supported map. The
preflight GET always runs (it also verifies access). A negative offset is
rejected as malformed. The preflight also reads `account_status`: a
successful GET is not treated as "active" — if the account is in a known-inactive
state (disabled, closed, pending review/settlement, etc.) CreateCampaign fails
BEFORE any mutating call rather than creating a paid campaign Meta would reject
later; an unreported status (0) or any value not known to be bad is allowed
through. `CampaignInput.Project` is
also required (rejected up front if empty/whitespace): the campaign name's
Project segment must be the caller-supplied canonical LFX project slug, so the
client never silently substitutes a placeholder that could mis-attribute a
campaign to the wrong project.
Dates are parsed strictly (impossible calendar dates rejected) and a
past start date is refused, with a same-day ad-set `start_time` nudged to
now+buffer. `doRequest` retries HTTP 429 and Graph rate-limit envelope codes
(4/17/32/341/613/80004) with bounded backoff, draining the body before close, and a
truncated response body is surfaced rather than reported as a false success.

See [internal/platform/meta](../../../internal/platform/meta).
5 changes: 5 additions & 0 deletions docs/knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 2026-07-13

**Creation** — Added OKF concept doc for internal/platform/meta (Meta Ads Graph
API client) with `tags`/`timestamp` frontmatter (queryable fields per OKF v0.1
§4.1), listed in the code index.

Comment thread
mrautela365 marked this conversation as resolved.
**Update** — Added OKF-recommended `tags` and `timestamp` frontmatter to the
internal/platform/reddit concept doc (queryable fields per OKF v0.1 §4.1).

Expand All @@ -18,6 +22,7 @@ errors instead of masking them as not-found. Added the
**Update** — Mount connection routes in the HTTP server (LFXV2-2556): the
`cmd/campaign-service` concept now notes that every container-wired service
must also be mounted in `server.go`, or its routes 404 despite compiling.

**Creation** — Added the `internal/platform/reddit` concept doc for the new
Reddit Ads API v3 client (OAuth2 token refresh + Campaign -> Ad Group -> Ad
creation) and listed it in the code index.
Expand Down
Loading
Loading