# Brand UGC: the creators driving attention for a brand on X, Instagram and TikTok

```
GET https://fetchapi.co/api/v1/brand/ugc?brand=<name>&x=<handle>&domain=<site>&window=30d&min_likes=10
    (same as GET /api/brand-creators?...)
header: x-api-key: gw_...
MCP: tool find_brand_creators {brand, x?, domain?, keywords?, instagram?, tiktok?, window?, since?, until?, min_likes?, sort?, platforms?, depth?, pages?, enrich?}
```

One call finds the posts other people made about a brand, groups them by creator, and ranks the creators by the
**attention** those posts drew. Use it to find the UGC creators worth contacting, the posts that did well, and who is
already paid (Instagram paid-partnership label, TikTok branded-content / ad label).

## Parameters

| Param | Default | Meaning |
|---|---|---|
| `brand` | required | Brand name; also the default handle and keyword |
| `x` | `brand` | The brand's X handle (`harmonic_ai`) |
| `domain` | none | The brand's site (`harmonic.ai`): adds X posts that link to it (`url:` operator) |
| `keywords` | none | Up to 5 comma-separated phrases to also match on X (`"harmonic ai"`). Phrases bring noise; handle + domain are precise |
| `x_query` | built | Your own full X search query, if you want operators we don't build (`filter:media`, `lang:en`, `-filter:replies`...) |
| `instagram` | `brand` | The brand's Instagram handle |
| `tiktok` | `brand` | TikTok search keywords, comma-separated (`ugc roster,roster ugc`). Use phrases for generic names |
| `tiktok_handle` | found | The brand's TikTok handle; by default found with TikTok's user search: the account whose bio links the brand's `domain` |
| `window` | none (all time) | `24h`, `7d`, `30d`, `90d` or `180d`; leave it out to search everything X returns (about 2-3 years for a mid-size brand) |
| `since`, `until` | none | `YYYY-MM-DD`; override `window` |
| `min_likes` | 0 | Drop posts below this many likes, on every platform |
| `sort` | `attention` | `attention`: most views/engagement first. `collab`: strongest partnership signals first (paid, co-authored, tagged) |
| `platforms` | `x,instagram,tiktok` | Any subset |
| `depth` | `quick` | `quick` (X up to 20 Latest pages + 3 Top, Instagram tagged to the end (up to 10 pages), TikTok handle + first keyword; ~10-30 s, ~15-25 calls) or `deep` (X 10, tagged 10, the brand's own posts, #brand, TikTok search 6 + the brand's hashtag; ~1-2 min) |
| `pages` | | X: max pages of the Latest query (1-100, default 20); Instagram tagged posts and TikTok search: 1-20 |
| `min_match` | `phrase` | Instagram confidence floor: `brand` (names the handle or domain), `phrase` (an exact keyword phrase, e.g. "ugc roster"), or `loose` (keyword-search results with the brand word and every phrase word apart; on UGC Roster about half agency boilerplate). Each post carries `match` |
| `engagers` | 0 | Instagram: also return the people commenting on the brand's latest N posts (0-10), signal `commented_on_brand`; they are creators in the brand's community, not UGC posts, so they never enter `top_posts` |
| `limit` | 100 | How many `top_posts` to return (1-500) |
| `enrich` | 0 | 0-50: look up followers for the top N Instagram/TikTok creators (one call each) for an engagement rate. X creators always carry followers |

**Generic brand names** ("harmonic", "notion", "linear"): X stays precise because it matches the @handle and the domain,
but TikTok searches the keyword and Instagram the handle, so set `tiktok=` and `instagram=` to the brand's real keyword and
handle (`tiktok=harmonic ai`), or limit to `platforms=x`. A live run with only `brand=harmonic` ranked guitar-harmonics
TikToks first; with `tiktok=harmonic ai` TikTok correctly returned nothing.

## Answer

**`top_posts[]`**, best first: every post found about the brand, ranked by attention: `platform`, `url`, `author`,
`author_name`, `author_followers`, `author_verified`, `author_url`, `at`, `views`, `likes`, `comments`, `reposts`,
`attention`, `paid`, `text`. `posts_found` is the total before `limit`.


`creators[]`, best first: `platform`, `username`, `profile_url`, `name`, `followers`, `verified`, `posts` (in the window),
`views`, `likes`, `comments`, `reposts`, **`attention`**, `top_post` {url, at, views, likes, comments, reposts, attention, text},
`post_urls` (up to 5), `paid_partnerships`, `signals`, `latest_post_at`, `brand_account` (the brand's own sub-accounts, ranked last).
Also `x_query` (the exact X search that ran), `filters`, and `meta` (calls, cost_usd, time_ms, pages scanned per source).

**Attention** per post = its views, or 10 x (likes + comments + reposts) where the platform shows no views (Instagram
photos); summed per creator. X reposts include quote posts.

## How it is built (recreate it with these exact calls)

Every call goes through the FetchAPI gateway with the caller's key (`POST/GET https://fetchapi.co/api/gw/<provider>/<path>`),
so each is metered and checked like a direct call. Source: `api/_collabs.js`.

**X** (twitter-api45, $0.00009 a page of ~20 posts):
```
GET /api/gw/rapidapi-twitter-api45/search.php?search_type=Latest&query=<q>[&cursor=<next_cursor>]
q = (@harmonic_ai OR url:harmonic.ai OR "keyword") -from:harmonic_ai min_faves:10 since:2026-09-11 until:2026-10-11
```
X's own advanced-search operators work through this provider: `min_faves:`, `min_retweets:`, `min_replies:`, `since:`,
`until:`, `-from:`, `url:`, `filter:media`, `-filter:replies`, `lang:`. No time limit by default. We run the query once as
`search_type=Latest` (every match, newest first), paging with `next_cursor` until X has no more (up to 20 pages, 50 with
`depth=deep`, or `pages=`), plus the first 3 pages of `search_type=Top` for older viral posts Latest stops short of, and
merge by post id. Measured on @harmonic_ai: one Latest query read 280 posts in 15 calls back to April 2024; Top added
38 older ones; splitting the window into date slices added none, so we don't. Each result carries `screen_name`, `user_info.followers_count`, `views`, `favorites`,
`retweets`, `quotes`, `replies`, `bookmarks`, `created_at`, `tweet_id`. Page with `next_cursor` until it is empty.

**Instagram.** Instagram has no caption search, so creators are reached four ways, cheapest first:
```
1 tagged   GET /api/gw/flashapi/ig/user_id?user=<handle> -> id
           GET /api/gw/flashapi/ig/tagged?id_user=<id>[&end_cursor=...]          paged to the end ($0.00099 a page)
2 hashtag  GET /api/gw/rapidapi-instagram-scraper-20251/hashtagposts/?keyword=<tag>[&pagination_token=...]   ($0.0014 a page)
           GET /api/gw/flashapi/ig/hashtag?hashtag=<tag>&tabs=clips[&next_max_id=...]   reels tab, paged; tabs=top if empty
           (Instagram sometimes answers a hashtag page with navigation only: an empty page is retried once after 1.5 s)
2b keyword POST /api/gw/apify/acts/data-slayer~instagram-search-reels/run-sync-get-dataset-items {query, maxPages}
           Instagram's own keyword search (instagram.com/explore/search/keyword/?q=...): posts that only SAY the brand in
           the caption. Keywords: the phrases given, the handle as words (roster.creators -> "roster creators"), the domain
           stem, the first phrase reversed; quick 2 keywords x 10 pages, deep 5 x 20 pages + data-slayer~instagram-keyword-
           posts-scraper {searchQueries, maxResultsPerQuery} for photos. ~$0.0022 a reel returned.
3 search   GET /api/v1/google/search?q=site:instagram.com "<term>"&limit=50  for term in handle, domain stem (brand match)
           and the first keyword phrases (phrase match); POST /api/gw/firecrawl/v1/search on owner keys when Google is down
4 deep     ig/post_info for search links that name no creator (up to 60), the brand's own posts (ig/posts), and found
           creators' latest posts (ig/posts_username)
5 engagers ig/posts (the brand's) + ig/comments?shortcode= per post (engagers=N): commenters, ~10 a call
```
Tried and dropped: the tagged posts of creators already found (6 calls, 1 new account, none naming the brand).
**Search results are read without extra calls**: a profile link (`instagram.com/<user>/`) is a creator whose profile
mentions the brand; a post description in Google's form `"69 likes, 4 comments - <user> on August 9, 2026: <caption>"`
gives the creator, likes, comments, date and caption. Only links that name no creator would need a lookup, and on UGC
Roster those 20 lookups found 0 (the captions did not name the brand), so quick mode skips them.

A tagged post always counts. A hashtag or search post counts if its text names the handle or domain (`match: "brand"`),
or else a phrase (`match: "phrase"`, signal `phrase_match`: "UGC roster" is also other agencies' generic phrase). The
brand's own posts are left out. `meta.scanned` shows each source's yield. Per post: `user.username`, `like_count`,
`comment_count`, `play_count`, `taken_at`, `is_paid_partnership`, `coauthor_producers`, `caption.text`. Instagram has
no date filter: the window and `min_likes` are applied to each post after it arrives.

Measured on UGC Roster (@roster.creators, ugcroster.com), creators per call:

| Version | Creators | Calls | Creators per call |
|---|---:|---:|---:|
| tagged only | 7 | 2 | 3.5 |
| + hashtags + search with a lookup per link | 24 | 92 | 0.26 |
| + hashtags + search read from the results (20 lookups) | 28 (21 brand, 7 phrase) | 30 | 0.93 |
| quick now, Google search (Apify) | 26 (46 posts) | 15 | 1.7, $0.013 |
| quick + `engagers=3` | 56 | 19 | 2.9, $0.017 |

Keyword search (UGC Roster, 2026-10-11): a bake-off of 7 Apify actors on "ugc roster" found data-slayer's reels search
best (35 posts / 28 creators, $0.084); the union of all 7 was 72 / 45. Paging deeper and adding phrasings ("ugcroster",
"roster ugc", "roster creators") found 152 posts / 96 creators, but most say only "roster": a caption check showed "UGC
roster" is also a generic agency phrase ("expanding our roster of UGC creators") and "roster" alone is sports, TV and
cabin-crew noise. With every source, deep: brand matches 47 posts / 16 creators; with phrase matches (default) +32 posts,
29 creators; with loose matches +69 posts, 79 creators; 88 creators counting profiles that mention the brand. 111 calls.
Instagram has no way to see Stories or posts that never name the brand, so a brand's full creator count (Roster says
300+) is larger than any public search finds.

**TikTok** (tiktok-scraper7, $0.00002 a page of ~30):
```
GET /api/gw/rapidapi-tiktok-scraper7/user/search?keywords=<domain stem or keyword>&count=10     find the brand's handle:
      the account whose signature / bio link contains the domain, most followers first (1 call)
GET /api/gw/rapidapi-tiktok-scraper7/feed/search?keywords=<handle>&count=30&region=us&sort_type=0&publish_time=0&cursor=0
GET /api/gw/rapidapi-tiktok-scraper7/feed/search?keywords=<handle>&...&sort_type=1        (most liked)
GET /api/gw/rapidapi-tiktok-scraper7/feed/search?keywords=<first keyword>&...&sort_type=1
deep adds: the other keywords x both sorts, and challenge/search + challenge/posts for #<domain stem>
```
Each search runs out after ~5 pages (TikTok returns ~140 results a keyword), so different keywords and sorts find
different videos. Measured on UGC Roster: the handle search found 28 relevant videos in 3 calls; the brand phrase by
likes took it to 44 in 8 calls, 75% of the 59 that 36 calls of every combination found. A video counts only if its
caption or tags name the handle, the domain or a keyword phrase: the bare word "roster" returned 0 relevant of 34
(wrestling, NFL and dating "rosters"). `sort_type=1` = most liked; `publish_time` = 1, 7, 30, 90 or 180 days (from the
window). Per video: `author.unique_id`, `play_count`, `digg_count`, `comment_count`, `share_count`, `create_time`,
`is_ad` / `commerce_info.branded_content_type`.

Then: drop the brand's own account, count each post once even if two sources found it, drop posts outside the window
or under `min_likes`, sum per creator, rank by attention.

## Examples (live, 2026-10-11)

`brand=roster&instagram=roster.creators&domain=ugcroster.com&tiktok=ugc roster,roster ugc&platforms=instagram,tiktok`:
TikTok handle found: @ugc.roster. **53 posts (16 Instagram, 37 TikTok) by 15 creators, 14 calls, $0.0022, 11 s**;
`depth=deep`: 65 posts, 35 calls. Top: @tadibranddeals on Instagram (911k, 738k, 524k views), @michellegetsbranddeals
on TikTok (313k, "Replying to @UGC Roster I reached out to tech brands...").

**Always pass the brand's X handle** (`x=`) when it differs from `brand`: the default `@brand` can be someone else.


`brand=harmonic&x=harmonic_ai&domain=harmonic.ai&platforms=x` (no window): **248 posts by 118 creators, Nov 2023 to
today**, 16 calls, $0.0014, 28 s. Same with `window=180d` before this change: 139 posts in 31 calls. Top posts: @hiiinternet 702k views (talks with @harmonic_ai), @pavelprata 67k ("are mega-funds taking over
seed?", Harmonic data), @TurnerNovak 30k (Hot 25 Q4), @HarryStebbings 25k, @LangChain 9k (Harmonic rebuilt Scout).
30d: 35 posts / 18 creators; 90d: 69 / 31.


`brand=harmonic&x=harmonic_ai&domain=harmonic.ai&window=30d&min_likes=10`: 6 calls, $0.0022, 4 s. Top: @TurnerNovak
(29.6k views, "The 25 hottest pre-Series B startups... according to @harmonic_ai"), @HarryStebbings (24.6k), @VCBrags.

`brand=glossier&window=30d&min_likes=100`: 6 calls, $0.0031, 10 s. 18 creators, top three TikToks 843k, 1.05M (paid
partnership) and 380k views.
