Most influencer vetting still leans on a single number. Follower count. Engagement rate. Maybe a "fake follower percentage" pulled from one tool. Each one of those metrics, in isolation, lies. A creator with 8% engagement might be paying for like-pods. A profile with a 92% alive-rate might still post to the wrong audience for your brand. A "healthy" follower-to-following ratio means nothing if the account has not shipped a video in 47 days.
Composite scores tell the truth because they force the trade-offs into one place. You stop arguing about which metric matters and start arguing about weights, which is the right argument to have. This post walks through a Composite TikTok Audience Quality Score (TAQS) built on four pillars, each backed by a real endpoint on the TikLiveAPI surface. By the end you will have a 0-100 score, sub-scores for transparency, a benchmarking table, and a worked example across ten mid-tier creators.
An audience is "high quality" for a brand when four things are simultaneously true:
digg_count over play_count./userinfo-by-username/ plus title from /user-posts/.create_time on the most recent post from /user-posts/.Each pillar produces a 0-100 sub-score. The composite is a weighted average. Sub-scores are kept in the output so a media buyer can see why a creator failed a threshold.
A single viral video distorts engagement rate by a factor of 10 or more. The fix is to compute per-video engagement across the last 30 posts, then look at both the median and the coefficient of variation. High median plus low spread equals a consistently engaged audience.
import requests
import statistics
from datetime import datetime, timezone
BASE = "https://api.tikliveapi.com"
HEADERS = {"X-Api-Key": "YOUR_API_KEY"}
def fetch_recent_posts(user_id, target=30):
posts, cursor = [], 0
while len(posts) < target:
r = requests.get(
f"{BASE}/user-posts/",
headers=HEADERS,
params={"userid": user_id, "count": 35, "cursor": cursor},
timeout=20,
).json()
videos = r.get("videos", [])
if not videos:
break
posts.extend(videos)
if not r.get("hasMore"):
break
cursor = r.get("cursor", 0)
return posts[:target]
def engagement_consistency_score(posts):
ratios = []
for v in posts:
plays = max(int(v.get("play_count", 0)), 1)
diggs = int(v.get("digg_count", 0))
ratios.append(diggs / plays)
if not ratios:
return 0.0, {}
median_eng = statistics.median(ratios)
stdev = statistics.pstdev(ratios) or 1e-9
cv = stdev / (statistics.mean(ratios) or 1e-9)
# Reward high median, penalize spread. Clamp to 0-100.
raw = (median_eng * 100 * 12) - (cv * 25)
score = max(0.0, min(100.0, raw))
return score, {"median_engagement": median_eng, "cv": cv, "n": len(ratios)}
The constants 12 and 25 are calibration knobs. A median engagement of 0.08 (8%) with low variance lands around 90; a 0.02 median with a viral spike lands around 20.
Two signals combine here. First, the alive rate: sample 200 followers from /user-followers/ and count how many have a non-zero recent post or follower count. Second, a ratio sanity check: legitimate creators almost never follow more accounts than they have followers once they cross 50k.
Note: the followers endpoint paginates using time (a Unix timestamp), not cursor. The following endpoint returns the followings key (plural).
def sample_followers(user_id, target=200):
out, t = [], 0
while len(out) < target:
r = requests.get(
f"{BASE}/user-followers/",
headers=HEADERS,
params={"userid": user_id, "count": 50, "time": t},
timeout=20,
).json()
batch = r.get("followers", [])
if not batch:
break
out.extend(batch)
if not r.get("hasMore"):
break
t = r.get("time", 0)
return out[:target]
def follower_legitimacy_score(user_stats, sample):
if not sample:
return 0.0, {}
alive = 0
for f in sample:
if int(f.get("aweme_count", 0)) > 0 or int(f.get("follower_count", 0)) > 5:
alive += 1
alive_rate = alive / len(sample)
followers = int(user_stats.get("followerCount", 0))
following = int(user_stats.get("followingCount", 1)) or 1
ratio = followers / following
# Above 50k followers, a healthy creator follows < 2000 accounts.
ratio_score = min(1.0, ratio / 25.0) if followers > 50000 else min(1.0, ratio / 5.0)
score = (alive_rate * 70) + (ratio_score * 30)
return score, {
"alive_rate": alive_rate,
"follower_following_ratio": ratio,
"sample_size": len(sample),
}
Niche fit is brand-specific. You define a keyword set ("skincare", "retinol", "SPF"), then check how often those terms appear in the creator's bio (user.signature from /userinfo-by-username/) and in the title field of the last 30 posts. The post-level signal matters more because bios are stale.
import re
def niche_fit_score(bio, posts, brand_keywords):
kw = [k.lower() for k in brand_keywords]
bio_text = (bio or "").lower()
bio_hits = sum(1 for k in kw if k in bio_text)
bio_component = min(1.0, bio_hits / max(1, len(kw) // 2))
titles = [(p.get("title") or "").lower() for p in posts]
if not titles:
title_component = 0.0
else:
per_post = []
for t in titles:
tokens = set(re.findall(r"[a-z0-9#]+", t))
hits = sum(1 for k in kw if k in t or k.lstrip("#") in tokens)
per_post.append(min(1.0, hits / 2))
title_component = sum(per_post) / len(per_post)
score = (bio_component * 30) + (title_component * 70) * 1.0
return min(100.0, score * 100 / 100), {
"bio_hits": bio_hits,
"title_match_rate": title_component,
}
The newest post's create_time is a Unix timestamp. Convert to days, then decay the score so 0-3 days is a perfect 100, 14 days is around 60, and anything past 60 days is effectively zero.
def freshness_score(posts):
if not posts:
return 0.0, {"days_since_last_post": None}
latest = max(int(p.get("create_time", 0)) for p in posts)
if latest == 0:
return 0.0, {"days_since_last_post": None}
days = (datetime.now(timezone.utc).timestamp() - latest) / 86400
if days <= 3:
score = 100.0
elif days <= 14:
score = 100 - ((days - 3) * 3.5)
elif days <= 60:
score = max(0.0, 60 - (days - 14) * 1.3)
else:
score = 0.0
return score, {"days_since_last_post": round(days, 1)}
Weights encode your brand's priorities. A performance marketer running short campaigns cares about freshness; a long-term ambassador program cares about niche fit. Recommended starting weights:
| Pillar | Weight | Rationale |
|---|---|---|
| Engagement consistency | 0.35 | Strongest predictor of campaign CTR |
| Follower legitimacy | 0.30 | Guards against bot-inflated reach |
| Niche fit | 0.20 | Context relevance, brand-specific |
| Content freshness | 0.15 | Confirms account is currently live |
A/B these weights against historical campaign results. If your post-campaign CTR correlates more strongly with niche fit than with engagement, push niche above 0.30 and pull engagement down. Two campaigns of data is enough to see directional movement.
WEIGHTS = {"engagement": 0.35, "legitimacy": 0.30, "niche": 0.20, "freshness": 0.15}
def composite_score(username, brand_keywords):
info = requests.get(
f"{BASE}/userinfo-by-username/",
headers=HEADERS,
params={"username": username},
timeout=20,
).json()
user_id = info["user"]["id"]
bio = info["user"].get("signature", "")
stats = info.get("stats", {})
posts = fetch_recent_posts(user_id, target=30)
sample = sample_followers(user_id, target=200)
eng, eng_meta = engagement_consistency_score(posts)
leg, leg_meta = follower_legitimacy_score(stats, sample)
niche, niche_meta = niche_fit_score(bio, posts, brand_keywords)
fresh, fresh_meta = freshness_score(posts)
total = (
eng * WEIGHTS["engagement"]
+ leg * WEIGHTS["legitimacy"]
+ niche * WEIGHTS["niche"]
+ fresh * WEIGHTS["freshness"]
)
return {
"username": username,
"composite": round(total, 1),
"sub_scores": {
"engagement": round(eng, 1),
"legitimacy": round(leg, 1),
"niche": round(niche, 1),
"freshness": round(fresh, 1),
},
"diagnostics": {**eng_meta, **leg_meta, **niche_meta, **fresh_meta},
}
Rough public benchmarks for mid-tier TikTok creators (50k-500k followers), assembled from agency reports and our own sampling:
| Pillar | Median sub-score | Top quartile |
|---|---|---|
| Engagement consistency | 52 | 74 |
| Follower legitimacy | 68 | 85 |
| Niche fit (brand-tuned) | 40 | 72 |
| Content freshness | 71 | 95 |
| Composite TAQS | 57 | 78 |
We ran the scorer above against ten anonymized skincare creators (brand keywords: skincare, retinol, spf, serum, acne). Output sorted by composite score:
| Creator | Followers | Eng | Leg | Niche | Fresh | TAQS |
|---|---|---|---|---|---|---|
| creator_a | 312k | 82 | 88 | 74 | 100 | 85.4 |
| creator_b | 184k | 76 | 81 | 78 | 95 | 80.6 |
| creator_c | 241k | 71 | 79 | 65 | 92 | 75.5 |
| creator_d | 96k | 68 | 72 | 70 | 88 | 72.4 |
| creator_e | 415k | 54 | 91 | 55 | 100 | 69.0 |
| creator_f | 128k | 62 | 67 | 58 | 74 | 64.4 |
| creator_g | 207k | 49 | 74 | 62 | 81 | 62.5 |
| creator_h | 89k | 71 | 52 | 48 | 90 | 59.7 |
| creator_i | 356k | 38 | 89 | 44 | 65 | 57.6 |
| creator_j | 173k | 44 | 48 | 61 | 22 | 43.3 |
Note creator_e: massive followers, perfect freshness, but mediocre engagement and niche. The composite catches what a single "follower count" or "engagement rate" view would miss. Creator_h has the inverse problem - strong engagement and freshness but a weak follower base, suggesting growth via pods or shoutouts.
You can run the full pipeline inside the playground to validate response shapes before wiring it into a workflow, and the pay-as-you-go pricing means a 200-creator audit costs roughly 200 + (30 x 200) + (4 x 200) = 6,800 credits in the worst case (full 30-post pull, four follower pages per creator).
/post-comments/ length and uniqueness checks via the id and text fields.Per creator: 1 call to /userinfo-by-username/, ~1 call to /user-posts/ for 30 videos, and ~4 calls to /user-followers/ for a 200-follower sample. Roughly 6 credits per creator at default settings.
Yes. Drop pillar 2 and renormalize the remaining weights (engagement 0.50, niche 0.30, freshness 0.20). The score is less defensive against bot-inflated reach but costs ~2 credits per creator.
The engagement and freshness functions degrade gracefully. The diagnostics object reports the actual n used so you can flag creators where the sub-score is based on thin data.
play_count or views from /post-detail/?For the bulk last-30 pull, play_count from /user-posts/ is enough and saves credits. If you need granular per-video stats for a featured post (HD URL via hdplay, full comment counts), call /post-detail/ for that one URL.
Register, verify your email, and your key appears on the profile page. Authentication uses the X-Api-Key header. For sales questions or rate-limit increases, hit contact.
More on related vetting topics over on the blog: shadowban detection, comment authenticity scoring, and trend-prediction pipelines built on the same endpoints.
Ready to put what you read into code? Try our endpoints live or grab the full reference.