Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7d78954
feat(platform): add X/Twitter Ads client (OAuth 1.0a)
mrautela365 Jul 9, 2026
e5714e4
fix(review): address Copilot findings on X/Twitter client (PR #19)
mrautela365 Jul 11, 2026
0665fef
fix(review): pagination-cap error + fragment-safe UTM URL (PR #19)
mrautela365 Jul 11, 2026
7e83312
fix(review): harden Twitter client per PR #19 review
mrautela365 Jul 11, 2026
6cefc65
fix(review): enforce 255-char names, guard funding id, cover query si…
mrautela365 Jul 11, 2026
629b613
fix(review): count EventName in runes, fix unreachable 429-exhausted …
mrautela365 Jul 11, 2026
a97c659
fix(review): drop unsupported campaign flight dates, fix budget/statu…
mrautela365 Jul 11, 2026
35f8cc3
fix(review): canonical tlf slug, document rate-limit scope, don't swa…
mrautela365 Jul 11, 2026
5e2cd43
fix(review): guard nil http client, make write pacing injectable (PR …
mrautela365 Jul 11, 2026
2db4c88
fix(review): scope line-item lookup by campaign_ids; correct OKF doc …
mrautela365 Jul 11, 2026
ad40a96
fix(review): RFC-correct OAuth param sort, error on id-less match, ti…
mrautela365 Jul 11, 2026
8bcc5dd
docs(test): fix stale write-delay comment (PR #19)
mrautela365 Jul 11, 2026
53cf430
fix(review): trim TweetID before use (PR #19)
mrautela365 Jul 11, 2026
3a323fb
fix(review): trim+use account/funding ids, validate TweetID format up…
mrautela365 Jul 11, 2026
a7ad106
fix(review): tighten TweetID to a valid snowflake shape (PR #19)
mrautela365 Jul 11, 2026
be5a7f2
fix(review): reject out-of-int64-range TweetID up front (PR #19)
mrautela365 Jul 11, 2026
596e4c6
fix(review): duplicate-promoted-tweet is a warning not success; valid…
mrautela365 Jul 11, 2026
0aa4ead
fix(review): route verifyAccount through doRequest, cap computed back…
mrautela365 Jul 13, 2026
642d605
fix(review): honor X-Account-Rate-Limit-Reset, reject empty project, …
mrautela365 Jul 13, 2026
f9255f7
test(review): make guard tests reach their branches; fix misplaced do…
mrautela365 Jul 13, 2026
94d5ab0
fix(review): surface response-body read errors in doRequest (PR #19)
mrautela365 Jul 13, 2026
69af112
fix(review): request max page size in find-or-create lookup (PR #19)
mrautela365 Jul 13, 2026
1282828
fix(review): use X Ads q= name filter in find-or-create (PR #19)
mrautela365 Jul 13, 2026
19774f8
fix(review): guard Retry-After overflow; return partial result on dow…
mrautela365 Jul 13, 2026
daf75fa
docs(okf): add recommended tags and timestamp frontmatter (PR #19)
mrautela365 Jul 13, 2026
94bdbe6
fix(review): RFC 5849 URI normalization in OAuth base string (PR #19)
mrautela365 Jul 13, 2026
d075484
test+fix(review): cover URI normalization; warn on name-reuse config …
mrautela365 Jul 13, 2026
9e39210
fix(review): preserve escaped path and IPv6 brackets in OAuth base st…
mrautela365 Jul 13, 2026
d085658
fix(review): accurate partial-result wording on reuse; race-safe call…
mrautela365 Jul 13, 2026
64184c1
fix(review): sign all multi-valued query params; trim base-URL traili…
mrautela365 Jul 13, 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 @@ -5,6 +5,7 @@
* [internal/infrastructure/config](internal-infrastructure-config.md) - Application configuration from CLI flags and env vars, including PG* composition into a PostgreSQL DSN.
* [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/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.
* [pkg/log](pkg-log.md) - Package log provides structured logging utilities for context-aware logging.
Expand Down
45 changes: 45 additions & 0 deletions docs/knowledge/code/internal-platform-twitter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
type: "Go Package"
title: "internal/platform/twitter"
description: "X (Twitter) Ads v12 client: OAuth 1.0a signing and the campaign -> line_item -> promoted_tweet creation flow."
resource: "internal/platform/twitter"
---

# internal/platform/twitter

Package twitter is the X (Twitter) Ads API v12 platform client. It implements
OAuth 1.0a (HMAC-SHA1) request signing and drives the
campaign -> line_item -> promoted_tweet creation flow. Credentials and account
configuration are injected via `NewClient`; the package never reads environment
variables or touches the database.

`CreateCampaign` is only PARTIALLY idempotent: it reuses existing campaigns and
line items by name (paged cursor lookups via `findByName`) before creating new
ones, and a lookup that fails transiently propagates an error so the caller
aborts rather than creating a duplicate. The promoted-tweet association, however,
is always re-POSTed on a repeat call. A recognizable duplicate response
(`DUPLICATE_PROMOTABLE_ENTITY`) is NOT treated as idempotent success: X returns
that code even when the tweet is already promoted by a DIFFERENT line item, so it
is surfaced as a warning (on `PromotedTweetWarning` and in the step log) to be
verified manually rather than assumed to attach to this line item. A
lost/malformed first response likewise produces a warning. True cross-call
idempotency (idempotency keys) is explicitly deferred and tracked in LFXV2-2665. Only the campaign and line item are created with
`entity_status=PAUSED`; the promoted-tweet endpoint does not accept
`entity_status`, so the API creates that association `ACTIVE`. It cannot serve,
though, because the parent line item is paused — delivery is gated by the paused
line item, not by the association's own status.

Per the X Ads v12 contract, create endpoints take their parameters as URL query
parameters (not a JSON body); the client folds those params into the OAuth
signature base string. Flight dates (`start_time`/`end_time`, ISO8601 UTC) are
sent only on the line-item create, where they are required; the campaign
endpoint does not accept them in v12, so the campaign create omits them. Dates
are validated for both shape and real-calendar validity (`time.Parse`) before
any mutating call. The client paces sequential writes within a SINGLE dispatch
toward the 1-req/sec limit; it does NOT enforce the account-wide write limit
across concurrent dispatches or replicas (that needs cross-replica coordination,
tracked in LFXV2-2665), so operators must not rely on this stateless client for
cross-dispatch rate limiting. When the account limit is hit anyway, 429s are
retried with backoff bounded by `Retry-After` / `X-Rate-Limit-Reset`.

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

## 2026-07-10

**Update** — Addressed Copilot review on the X/Twitter Ads client (PR #19):
create calls now send params as URL query parameters (not a JSON body) per the
X Ads v12 contract, use `entity_status=PAUSED`, and line items carry the
required `start_time`/`end_time` with `bid_strategy` (not `bid_type`); dates are
strictly parsed to reject impossible calendar values; name lookups propagate
errors instead of masking them as not-found. Added the
`internal/platform/twitter` code concept and index entry.


**Update** — Dropped the Goa CLI path allowlist; twitter-api-secret FP is
fingerprint-only in `.gitleaksignore`. Clarified `.grype.yaml` rationale
(Engine fixes exist; Go module path not yet upgradeable via migrate/dktest).
Expand Down
Loading
Loading