# Twitter Competitor Analysis: AI Winning Tweets & Profile Posts — API reference

The developer detail behind the Store page: inputs the form hides, flags, presets, the example row, the run records,
every outcome and the AI's grounding rules.

## API-only inputs

Declared in the input schema (the API tab, the OpenAPI spec and MCP see them), hidden on the form so it stays short.

| Field | Default | What it does |
|---|---|---|
| `startDate`, `endDate` | — | `YYYY-MM-DD`, set both: replaces `lookbackDays` (both go to the source) |
| `splitMode` | `week` | How the default source splits the window into searches — `day` for prolific accounts |
| `sort` | `Latest` | The source's order; `Latest` reads the window evenly, which the median needs |
| `language` | `any` | One of `any`, `en`, `es`, `pt`, `fr`, `de`, `it`, `nl`, `sv`, `pl`, `tr`, `ru`, `uk`, `ar`, `hi`, `id`, `ja`, `ko`, `zh`, `th`, `vi` — passed to the source |
| `keyword` | — | One keyword the source searches inside the accounts' tweets |
| `mediaFilter` | `any` | `any` / `images` / `videos` — passed to the source |
| `minRetweets` | `0` | Reposts floor — also passed to the source |
| `minViews`, `minBookmarks` | `0` | Free floors before the median (a missing count fails the floor) |
| `minBaselinePosts` | `8` | Posts for `baselineConfidence: high`; 3–7 posts = `low`, under 3 = `baseline_too_small` |
| `minEngagementRatePct` | `0` | With `metric: engagementRate`, a tweet wins at this rate instead of the multiple |
| `excludeReplies` | `true` | Off: the account's replies to itself join their opener as `threadParts` |
| `includeRetweets` | `false` | Count plain retweets |
| `excludeAds` | `true` | Drop promoted tweets |
| `writeRemix` | `true` | Off: no remix idea or draft even with a brand |
| `topPerAccount` | `0` | Keep each account's N best winners (also on the form) |
| `failOnSourceError` | `false` | On: a source that delivers nothing (the plan refusal too) ends FAILED with the same message and `OUTPUT`, so a schedule's failed-run alert fires (also on the form) |
| `webhookHeaders`, `aiCandidateMultiplier`, `maxToProcess`, `dedupeTtlDays`, `discoveryInput`, `discoveryTimeoutSecs`, `autoSwitchToLive`, `webhookBatchSize` | template defaults | Run-size, memory and delivery knobs |

## Flags

Values to type in `targetFlags` (snake_case; camelCase works too). A winner must carry at least one; more flags rank it
higher.

| Flag | Meaning |
|---|---|
| `viral_outlier` | at least 5× the account's own median |
| `thread_format` | a thread opener (🧵, 1/, "thread" or a teaser first line) |
| `video_format` | a video post |
| `photo_format` | a photo post |
| `quote_post` | a quote post |
| `link_post` | carries an external link |
| `text_only` | plain text, no media or link |
| `question_hook` | the first line asks a question |
| `number_hook` | the first line opens with a number or a list |
| `how_to_hook` | the first line is a how-to |
| `contrarian_hook` | the first line is contrarian ("unpopular opinion", "stop", "nobody") |
| `story_hook` | the first line opens a personal story |
| `cta_in_text` | the tweet asks for a reply, follow, bookmark, repost, DM or click |
| `high_bookmarks` | bookmarks at least 20% of likes — people saved it to use later |
| `low_baseline_confidence` | the account had fewer posts in the window than `minBaselinePosts` |
| `baseline_zero` | the account's median is 0, so the multiple is undefined |
| `verified_account` | posted by a verified account |

## Preset inputs

The `preset` select replaces the sample accounts (the untouched sample counts as empty); a value you typed yourself always wins. Every preset reads 3–4 accounts × 50 tweets, capped at $0.15 of source spend until its match count is measured (docs/ROADMAP.md 2026-10-24).

```json
{"agency-two-clients": {"sourceMode": "actor", "accounts": ["levelsio", "marc_louvion", "thejustinwelsh", "dickiebush"], "lookbackDays": 30, "maxTweetsPerAccount": 50, "maxDiscoveryChargeUsd": 0.15}}
{"ai-builders": {"sourceMode": "actor", "accounts": ["simonw", "swyx", "karpathy"], "lookbackDays": 30, "maxTweetsPerAccount": 50, "focusTopic": "Building with LLMs: tools, prompts, evals and AI engineering", "maxDiscoveryChargeUsd": 0.15}}
{"creator-ghostwriting": {"sourceMode": "actor", "accounts": ["thejustinwelsh", "dickiebush", "nicolascole77"], "lookbackDays": 30, "maxTweetsPerAccount": 50, "focusTopic": "Writing online, ghostwriting and growing an audience on X", "maxDiscoveryChargeUsd": 0.15}}
{"growth-marketers": {"sourceMode": "actor", "accounts": ["lennysan", "andrewchen", "hnshah"], "lookbackDays": 30, "focusTopic": "Growth marketing: acquisition, activation, retention and newsletters", "maxTweetsPerAccount": 50, "maxDiscoveryChargeUsd": 0.15}}
{"saas-founders": {"sourceMode": "actor", "accounts": ["levelsio", "marc_louvion", "dvassallo"], "lookbackDays": 30, "maxTweetsPerAccount": 50, "focusTopic": "Bootstrapped SaaS: pricing, launches, revenue updates and building in public", "maxDiscoveryChargeUsd": 0.15}}
```

## Example row

One winner of the AI example input (a fictional sample account, our AI run of 2026-10-17 on the own-key default model):

```json
{
  "itemId": "f369936937da",
  "name": "@contoso_growth: 7 landing page lines that doubled our demo bookings:",
  "url": "https://example.com/x/contoso_growth/status/1971234567890120020",
  "tweetId": "1971234567890120020",
  "account": "contoso_growth",
  "accountName": "Contoso Growth (sample)",
  "accountUrl": "https://example.com/x/contoso_growth",
  "followers": 42300,
  "verified": true,
  "firstLine": "7 landing page lines that doubled our demo bookings:",
  "postedAt": "2026-08-08T14:07:00+00:00",
  "postedWeekday": "Sat",
  "postedHourUtc": 14,
  "likes": 640,
  "reposts": 130,
  "retweets": 104,
  "quotes": 26,
  "replies": 40,
  "views": 118000,
  "bookmarks": 520,
  "format": "thread",
  "hookPattern": "number",
  "engagement": 1330,
  "engagementRatePct": 3.144,
  "viewRatePct": 1.13,
  "bookmarkToLikeRatio": 0.812,
  "accountMedianEngagement": 259.5,
  "accountPostsInWindow": 16,
  "baselineConfidence": "high",
  "baselineZero": false,
  "outlierMultiple": 5.13,
  "rank": 2,
  "percentileInAccount": 93.8,
  "metricUsed": "engagement",
  "flags": [
    "viral_outlier",
    "thread_format",
    "number_hook",
    "high_bookmarks",
    "verified_account"
  ],
  "ruleScore": 79,
  "score": 94,
  "label": "hot",
  "fitScore": 94,
  "scoreReasons": [
    "Achieved an outlier multiple of 5.13× the median engagement",
    "Generated 520 bookmarks with a high bookmark-to-like ratio of 0.81",
    "Used a numbered list hook that clearly states the value upfront"
  ],
  "hookType": "number_or_list",
  "contentPillar": "landing page copy",
  "ctaType": "none",
  "tone": "educational",
  "whyItWorked": "The tweet secured 520 bookmarks (an 0.81 bookmark-to-like ratio) and reached 5.13x the median by opening with a clear number hook: \"7 landing page lines that doubled our demo bookings:\" followed by actionable examples.",
  "relevant": true,
  "remixIdea": "Use a numbered list hook presenting a breakdown of specific copy lines or product steps that drove a measurable outcome, formatted as a clean bulleted list for easy reading and high saving intent.",
  "draftPost": "7 onboarding steps that doubled our trial upgrades: → \"Invite your first teammate\" → \"No setup required\" → \"Import data in 1 click\" → \"Used by [number] founders\" → \"Test with your live metrics\" → \"Chat with an analyst\" → \"Free migration support\"",
  "aiAssessed": true,
  "aiModel": "google:gemini-3.5-flash-lite",
  "assessedBy": "ai",
  "chargedEvent": "demo",
  "billedAs": "qualified-winning-tweet-ai",
  "dedupeKey": "tw:1971234567890120020",
  "dateWindow": {
    "start": "2026-07-19",
    "end": "2026-10-17",
    "approximate": true
  },
  "sourceActor": "list"
}
```

## Which record to read

`OUTPUT` is the full run record (`RUN_SUMMARY` holds the same keys). Its keys:

| Key | Type | What it holds |
|---|---|---|
| `ok`, `outcome`, `mode` | bool, string | `ok` / `empty` / `source_refused` / `source_failed`; `demo` / `live` / `list` / `dataset` |
| `qualified`, `candidates`, `stats` | int, object | the funnel; `stats.skipped_<reason>` per skip (`below_baseline`, `retweet`, `reply`, `ad`, `off_topic`, `seen_in_previous_run`, `baseline_too_small` …) |
| `accounts` | array | per account: `postsLoaded`, `postsInBaseline`, `median`, `medianEngagement`, `baselineConfidence`, `winners`, `delivered`, `skips` (`no_rows` for an account that returned nothing) |
| `patterns` | object | `counts` (hook types, pillars, formats, CTAs, weekdays, hours, account medians), `patternNote`, `topPatterns`, `aiAssessed` |
| `dateWindow`, `metric`, `outlierMultiple`, `freePlanCapHit` | object, string, number, bool | the analysed window; `freePlanCapHit` when the source returned exactly its Free-plan 50 tweets |
| `chargedEvents`, `chargeSplit`, `freeTierRemaining` | object | real charges, demo / free / basic / AI rows |
| `aiCoverage`, `aiTokens`, `aiError`, `timing` | object | AI-assessed vs rule-scored rows, measured tokens per winner, the AI's failure reason, seconds per phase |
| `suggestedTerms` | string[] | hashtags / words recurring in ≥ 2 winners skipped as off-topic — add them to `excludeTerms` next week (also in the DIGEST) |
| `digestCharge` | string | `free` (the first 3 per Apify account), `charged` (`digest-delivered`, $0.02) or null |
| `trend`, `history`, `yieldByQuery`, `scheduleInput`, `scheduleTaskUrl`, `reportUrl`, `nextSteps` | various | run-over-run deltas, the search's series, winners per account, the saved task |

## Which call runs what

| Input shape | `OUTPUT.mode` | Can it charge? |
|---|---|---|
| `{}` or the untouched form (the sample tweets) | `demo` | no — sample rows are `chargedEvent: demo` |
| the sample tweets + `accounts` typed next to them | `live` | yes — per winning tweet; the X scan on your account, capped by `maxDiscoveryChargeUsd` |
| the same, with `autoSwitchToLive: false` | `demo` | no — the status names the accounts it did not read |
| `sourceMode: "actor"` + `accounts` | `live` | yes — as above |
| `sourceMode: "list"` + your own `itemsList` rows | `list` | yes — per winning tweet, no source run |
| `sourceMode: "dataset"` + `datasetId` | `dataset` | yes — per winning tweet, no source run |

## Webhook, Sheets, CRM and AI agents

| Outcome | Run status | Dataset | `OUTPUT` | Charged? | What to do |
|---|---|---|---|---|---|
| Free demo | SUCCEEDED | the sample's 10 winners | `mode: demo` | no | type your `accounts` |
| Success | SUCCEEDED | winners, best first | funnel, `accounts`, `chargedEvents`, `aiCoverage` | per winning tweet | — |
| Nothing matched | SUCCEEDED | empty | `qualified: 0` | not by us — the X scan runs on your plan | lower `outlierMultiple` or widen `lookbackDays` |
| Account with no rows | SUCCEEDED | the other accounts' winners | `accounts[].skips.no_rows` | no | check the handle; protected accounts return nothing |
| Plan cannot run the source | SUCCEEDED (FAILED with `failOnSourceError: true`) | empty | `ok: false`, `outcome: source_refused`, `sourceRefused: plan` | no | paste list-mode rows (any plan) |
| Source failed | FAILED | empty | `ok: false`, `outcome: source_failed`, `sourceError` | no | follow the reason in the status |
| Source partial | SUCCEEDED | winners of the rows scraped | `sourceStatus.partial: true` | per winning tweet | raise `discoveryTimeoutSecs` |
| AI unavailable | SUCCEEDED | rule-tagged winners | `aiError`, `aiCoverage` | free while your free ones last, then basic | `llmProvider: byok` with your own key |
| Invalid input | FAILED | empty | — | no | fix the field the status names |

- **Webhook** — `webhookUrl` (or a URL in `sendAlertsTo`) with `webhookBatchSize: 1` POSTs `{"event": "winning_tweet.qualified", "winning_tweet": {…}, "runId": "…"}` per row; a larger size POSTs `{"event": "winning_tweets.qualified", "winning_tweets": [{…}, …], "runId": "…"}` per batch. With `webhookSecret` every POST carries `X-Signature: sha256=<HMAC-SHA256 of the raw body>`. A 429 / 5xx / network error is retried once after 2 s; a POST that still fails never fails the run — its rows' `dedupeKey`s are in `OUTPUT.webhook.failedKeys`.
- **Which delivery field wins** — see the precedence table below; every URL is posted once.
- **Datasets of other X scrapers** — `sourceMode: dataset` reads the rows of `apidojo/tweet-scraper`, `kaitoeasyapi/twitter-x-data-tweet-scraper-pay-per-result-cheapest`, `xquik/x-tweet-scraper` and `scrape.badger/twitter-tweets-scraper` as they are (nested `author.*`, `entities.*`, snake_case counts). After every run of one of them: its Integrations tab → "Run succeeded → start this Actor" with `{"sourceMode": "dataset", "datasetId": "{{resource.defaultDatasetId}}"}` — no scan bill, you pay only for the winners.
- **Google Sheets** — a Make / n8n / Zapier "create or update row" step on the webhook, keyed on `dedupeKey`; columns as in the tracker template.
- **CRM / Notion** — the same step: title = `name`, body = `whyItWorked` + `remixIdea`, link = `url`, upsert on `dedupeKey`.
- **AI agents (MCP)** — `https://mcp.apify.com/?actors=<your-apify-username>/x-winning-tweets-ai`; free first call `{}` or `{"sourceMode": "list", "itemsList": [...]}`, then `{"accounts": [...], "dedupeAcrossRuns": false, "maxDiscoveryChargeUsd": 0.5}`.

### Delivery fields — precedence

Use `sendAlertsTo` (one URL per line; the Actor recognises the service) and `notifyEmail`. The five per-service fields
are **deprecated aliases of `sendAlertsTo`**, kept for API callers of earlier builds.

| Field | Gets | Precedence |
|---|---|---|
| `notifyEmail` | the DIGEST as an e-mail | independent of the URLs |
| `sendAlertsTo` | per URL: Slack / Discord / Teams / Google Chat / Telegram → the DIGEST as a chat message; any other https URL → the rows as JSON (as `webhookUrl`) | one URL per service; a per-service field or `webhookUrl` given too wins for that service |
| `webhookUrl` | the rows as JSON (`winning_tweet.qualified` or batches) | a chat URL here is treated as that chat's field (digest, posted once) — never a second POST of rows |
| `slackWebhookUrl`, `discordWebhookUrl`, `teamsWebhookUrl`, `googleChatWebhookUrl`, `telegramUrl` (deprecated) | the DIGEST for that service | win over the same service in `sendAlertsTo` |

The same URL in two fields is posted once.

### Remix drafts into your post scheduler

Typefully, Buffer or Hypefury as the destination of every `draftPost`, as a draft you edit and schedule:

1. Make / n8n: a **Webhook** trigger; put its URL in `sendAlertsTo` with `webhookBatchSize: 1`.
2. A filter: `winning_tweet.draftPost` is not empty.
3. **Typefully** — HTTP POST `https://api.typefully.com/v1/drafts/` with header `X-API-KEY: Bearer <your Typefully key>`
   and body `{"content": "{{winning_tweet.draftPost}}"}` · **Buffer** / **Hypefury** — their Make / Zapier "create
   draft / add to queue" action with the same text.
4. Optional: put `winning_tweet.url` in the draft's notes so you can see the original.

## How the AI works in detail

- **Only winners reach the model.** The baseline (FR-3 to FR-5) runs first; the model never sees below-median tweets,
  retweets, replies or ads, so you never pay AI on losers.
- **One call per winner** returns the typed analysis: `hookType` (12 values), `contentPillar` (≤ 3 words), `ctaType`,
  `tone`, `whyItWorked` (≤ 240 chars), `relevant` against your `focusTopic`, and with a brand `remixIdea` + `draftPost`
  (≤ 280 chars each).
- **Grounding, checked after the model answers:** an enum value outside the list becomes `other` / `none` / null;
  `whyItWorked` must name a number of the input, else the rule template replaces it; a remix or draft sharing a run of 7
  words with the original is dropped (the template stands in for the idea); the analysed account's handle and hashtags
  are stripped; a %, an amount or a multiple the brand description does not state becomes `[your number]`.
- **The run's pattern note (LLM-4)** — one call per run over the counts of hook types, pillars, formats, CTAs and hours;
  every statement must name a count, else only the counts ship. Never charged.
- **Model and tokens.** On Apify the default is `anthropic/claude-haiku-4.5` through Apify's model access (tokens on
  your Apify bill); measured on our 2026-10-17 run (google:gemini-3.5-flash-lite): 1,725 input / 273 output tokens per winner,
  the run note included. Your own OpenAI / Anthropic / Gemini / Groq key with `llmProvider: byok`.
- **Fallback.** A model error leaves the rule tags and template why on the row (`aiAssessed: false`), billed as basic; a
  model outage never fails the run.

## More questions

**How much will one run cost me?** 5 accounts × 90 days ≈ $0.25 X scan on your account + $0.003 per winner here ($0.015 with
AI + ≈ $0.003 tokens) + $0.02 for the run's digest: about $0.45 with rules and $1.35 with AI for ≈ 60 winners; the first
25 winners and 3 digests are free.

**Can I use my own model, and what if the AI fails?** Yes: "My own API key", or any OpenRouter slug in "AI model". If the
model fails, the tweet keeps its rule tags and template why at the basic price (free while your free winners last); the
run never fails.

**Is the data public / is this legal?** Only public posts of public accounts, no login of yours; you are responsible for
using them within X's terms and the law.
