TikTok Influencer Fake Views Audit: Build a Vetting Agent
Build a TikTok influencer fake views audit agent: pull a creator's recent posts, score each with a fake-view detector, aggregate in Python, then write a memo.

My first audit of the NBA’s TikTok account came back “review”: reach per follower was below the tier minimum. Then I looked at the 15 posts it had scored. All of them were under two days old, still collecting views. I added an age filter, and the next memo quoted a tier minimum of 0.01, the benchmark for a smaller account than one with 27 million followers. The first post in that batch was a collab whose author had 3.1 million followers, and the code had taken the tier from it. Nothing was fraudulent. The audit was measuring the wrong posts.
This tutorial builds a TikTok influencer fake views audit you can run before a paid deal. You give it a creator handle. It pulls the creator’s recent posts, skips the ones that would skew the result, runs SandBase’s tiktok/analytics/detect-fake-views endpoint on each, aggregates everything in Python, and has openai/gpt-6-luna write a short vetting memo from the computed stats only. Output: a CSV with one row per post and a memo with a screening band. It’s for brand and agency teams who vet creators, and for developers building that screening step. I tested it on three official brand accounts: Duolingo, the NBA and Ryanair.
Key takeaway
- On 45 posts from three verified brand accounts (2026-10-04), the detector marked 0 posts suspicious and gave “Minimal” confidence every time, with median fake_score 16.26, 19.65 and 19.74.
estimated_fake_viewslooks like a lookup, not a count. It equaled views times 5%, 10% or 20%, chosen by fake_score: 5% for scores up to 9.98, 10% from 12.21 to 19.99, 20% from 20.09 up.- Which posts you audit changed the result more than anything else I tried. Skipping pinned posts, posts younger than 3 days and collabs by other authors removed 6 to 65 list items per account before scoring.
- The LLM step cost $0.00022 to $0.00034 per memo. A numbers check found no invented numbers, but reading the memos still turned up two problems: a flag on 9 of 15 posts called account-level, and a “proceed with the deal” that goes further than the band.
What the agent does
| Step | Endpoint | What the code keeps |
|---|---|---|
| Recent posts | tiktok/app-v3/user-post-videos | video id, post date, views, likes, comments, shares; pinned, too new and collab posts skipped |
| Score each post | tiktok/analytics/detect-fake-views | fake_score, fake-view percentage and estimate, is_suspicious, confidence, flags, follower tier and its benchmarks |
| Aggregate | Python | medians, counts, reach vs the tier benchmarks, recurring flags, screening band |
| Memo | openai/gpt-6-luna via /v1/chat/completions | 4 to 6 bullets and a next step, then a numbers check |
The detector’s report is long. The single-video guide walks through every block of it. This agent reads only the parts that survive aggregation across posts. For the profile and post endpoints themselves, see the TikTok creator research API guide.
What this result is, and isn’t
detect-fake-views is a third-party heuristic. The reference describes it as traffic analysis based on “TikTok’s Traffic Pool theory”. It isn’t TikTok’s own data, and a high score isn’t proof that anyone bought views. I have no labeled set of known-fake videos, so this article measures how the detector behaves, not how accurate it is. That’s also why the demo uses official brand accounts only. Don’t publish a named creator as “fake” based on this output.
The data is public and read-only, and you need a SandBase API key. SandBase isn’t an official TikTok partner. There’s no access to private accounts, DMs, a creator’s own analytics or account actions.
| You need | Use |
|---|---|
| A quick screen of any public creator before outreach | This agent (public data, third-party heuristic) |
| The creator’s real audience, reach and traffic sources | Ask the creator for TikTok Studio analytics or campaign data shared through TikTok’s own creator marketplace tools |
| Data about your own account or ads | TikTok’s official tools for account owners |
Tested on 2026-10-04 (UTC)
Inputs: the public handles duolingo, nba and ryanair, and video ids those accounts published. Every data call is POST https://api.sandbase.ai/v1/api/<vendor>/<path> with Authorization: Bearer $SANDBASE_API_KEY and a JSON body of that endpoint’s fields only. This is the Model API POST route from the endpoint reference, not the catalog’s GET route; the reference’s generated curl example adds a model field to the body, which the path already identifies. The code reads only the documented outputs[0].data and raises when a run isn’t completed. Field names below are observed in these calls, not documented guarantees.

Caption: The user-post-videos reference lists count (default 20), max_cursor for paging and unique_id, the three fields the agent sends (captured 2026-10-04).
curl -s https://api.sandbase.ai/v1/api/tiktok/app-v3/user-post-videos \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"unique_id": "duolingo", "count": 20}'
Run dfacf0c8-3704-48a8-a8bd-57869a9c2441 returned 10 posts, not 20, plus has_more: 1 and a max_cursor for the next page. The second post, with every other key omitted:
{"aweme_id": "7692128874154429709", "create_time": 1790963327, "is_top": 0,
"author": {"unique_id": "duolingo"},
"statistics": {"play_count": 354521, "digg_count": 44803, "comment_count": 1396,
"share_count": 12012, "collect_count": 8457}}
The first post in that list had is_top: 1: a pinned video from June 2025. author.unique_id is what exposes collabs, since a collab in a creator’s feed carries the other author’s handle.

Caption: The detect-fake-views reference shows item_id as the only required field and content_category as optional, and describes the method as based on TikTok’s Traffic Pool theory (captured 2026-10-04).
The detector call for a Duolingo video from 2026-09-25, {"item_id": "7689488735649402126"} (run 28568b7b-0fb3-4970-955a-cd68594413e7), returned this fake_view_analysis object in full. It’s a separate call from the batch runs below, so its numbers differ slightly from theirs:
{"component_scores": {"consistency_score": 77.78, "content_authenticity_score": 34.0,
"creator_credibility_score": 0, "distribution_score": 65.0, "engagement_score": 0.0,
"fan_growth_score": 45, "follower_correlation_score": 20, "racing_mechanism_score": 0},
"confidence_level": "Minimal", "estimated_fake_views": 666010, "fake_score": 25.64,
"fake_view_percentage": 20.0, "is_suspicious": false,
"main_detection_reason": "Statistical View Anomalies"}
From detailed_analysis.follower_correlation, with its other keys omitted:
{"follower_tier": "xxlarge",
"tier_benchmarks": {"like_follower_ratio": {"avg": 0.002, "good": 0.01, "min": 0.0005},
"view_ratio": {"avg": 0.02, "good": 0.1, "min": 0.005}}}
The consistency block explained its 77.78 with seven issues across nine days, such as “Day 3: 474265 views but 0 comments”. A video with 9,676 comments in total having zero on five of those days looks more like a gap in the daily series than a pattern in the audience. That’s the kind of reason you should read before acting on a score.
The complete program
One file, 200 lines, standard library plus requests. Run it as python3 creator_audit.py duolingo. Options: --posts (default 15), --min-age-days (default 3), --category and --model. This is the exact file I ran for the results below.
#!/usr/bin/env python3
"""TikTok creator audit agent: recent posts -> detect-fake-views per post -> stats in code -> LLM memo.
Usage: python3 creator_audit.py duolingo [--posts 15] [--category default] [--model openai/gpt-6-luna]
Writes audit_<handle>.csv (one row per post) and memo_<handle>.md (stats, memo, numbers check).
The result is a screening signal from a third-party heuristic, not evidence of fraud.
"""
import argparse
import csv
import json
import os
import re
import statistics as st
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
import requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
PRICE = {"openai/gpt-6-luna": (0.1, 0.5)} # USD per 1M input / output tokens on SandBase, 2026-10-04
def call(model: str, params: dict, retries: int = 2) -> dict:
"""POST /v1/api/<model> and return outputs[0].data; raise if the run did not complete."""
for attempt in range(retries + 1):
try:
resp = requests.post(f"{BASE}/api/{model}", headers=HEADERS, json=params, timeout=120)
except requests.RequestException: # dropped connections happen; retry them like a 5xx
if attempt == retries:
raise
time.sleep(3 * (attempt + 1))
continue
body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
outputs = body.get("outputs") or []
if resp.status_code == 200 and body.get("status") == "completed" and outputs:
return outputs[0].get("data") or {}
if resp.status_code < 500 or attempt == retries:
raise RuntimeError(f"{model}: HTTP {resp.status_code} {body.get('status')} {body.get('error')}")
time.sleep(3 * (attempt + 1))
def recent_posts(handle: str, n: int, min_age_days: float, max_pages: int = 8) -> tuple[list[dict], dict]:
"""Newest posts by this creator, at least min_age_days old (fresh posts are still collecting views)."""
posts, cursor, cutoff, skipped = [], 0, time.time() - min_age_days * 86400, Counter()
for page in range(1, max_pages + 1):
data = call("tiktok/app-v3/user-post-videos", {"unique_id": handle, "count": 20, "max_cursor": cursor})
for a in data.get("aweme_list") or []:
s = a.get("statistics") or {}
author = ((a.get("author") or {}).get("unique_id") or "").lower()
reason = ("pinned" if a.get("is_top") else "collab by another author" if author != handle.lower()
else "too new" if a.get("create_time", 0) > cutoff else "" if s.get("play_count") else "no views")
if reason:
skipped[reason] += 1
continue
posts.append({"video": a["aweme_id"], "posted": time.strftime("%Y-%m-%d", time.gmtime(a.get("create_time", 0))),
"list_views": s["play_count"], "likes": s.get("digg_count", 0),
"comments": s.get("comment_count", 0), "shares": s.get("share_count", 0)})
if len(posts) >= n or not data.get("has_more"):
break
cursor = data.get("max_cursor") or 0
return posts[:n], {"list_pages": page, **skipped}
def detect(post: dict, category: str | None) -> dict:
params = {"item_id": post["video"], **({"content_category": category} if category else {})}
d = call("tiktok/analytics/detect-fake-views", params)
fv, da = d.get("fake_view_analysis") or {}, d.get("detailed_analysis") or {}
thin = [name for name, block in da.items() if isinstance(block, dict)
and any("Insufficient" in i for i in block.get("issues") or [])]
return {**post, "fake_score": fv.get("fake_score"), "fake_pct": fv.get("fake_view_percentage"),
"est_fake_views": fv.get("estimated_fake_views"), "suspicious": fv.get("is_suspicious"),
"confidence": fv.get("confidence_level"), "reason": fv.get("main_detection_reason"),
"views": (d.get("video_metrics") or {}).get("total_views") or post["list_views"],
"insufficient": " ".join(thin),
"_flags": d.get("suspicious_features") or [], "_creator": d.get("creator_metrics") or {},
"_tier": (da.get("follower_correlation") or {}), "_bench": (da.get("engagement") or {}).get("benchmark_category")}
def med(xs: list) -> float:
xs = [x for x in xs if x is not None]
return round(st.median(xs), 4) if xs else None
def vs_tier(x: float, b: dict | None) -> str:
"""Place a ratio against the endpoint's own tier benchmarks (min / avg / good) in code, not in the LLM."""
if not b or x is None:
return "no benchmark"
return ("below tier min" if x < b["min"] else "min to avg" if x < b["avg"]
else "avg to good" if x < b["good"] else "above good")
def aggregate(handle: str, rows: list[dict]) -> dict:
followers = max(r["_creator"].get("follower_count") or 0 for r in rows)
tier = rows[0]["_tier"]
bench = tier.get("tier_benchmarks") or {}
scores = [r["fake_score"] for r in rows if r["fake_score"] is not None]
views = [r["views"] for r in rows]
flags = Counter(f for r in rows for f in set(r["_flags"]))
return {
"handle": handle, "posts": len(rows), "followers": followers, "verified": rows[0]["_creator"].get("verified"),
"follower_tier": tier.get("follower_tier"), "benchmark_category": rows[0]["_bench"],
"fake_score": {"median": med(scores), "min": min(scores), "max": max(scores)},
"suspicious_posts": sum(bool(r["suspicious"]) for r in rows),
"confidence_levels": dict(Counter(r["confidence"] for r in rows)),
"fake_pct_median": med([r["fake_pct"] for r in rows]),
"fake_pct_levels": {str(k): v for k, v in sorted(Counter(r["fake_pct"] for r in rows).items())},
"est_fake_views_total": sum(r["est_fake_views"] or 0 for r in rows), "views_total": sum(views),
"views_median": med(views), "views_max_to_median": round(max(views) / st.median(views), 2),
"view_follower_median": med([v / followers for v in views]),
"like_follower_median": med([r["likes"] / followers for r in rows]),
"tier_view_ratio": bench.get("view_ratio"), "tier_like_follower_ratio": bench.get("like_follower_ratio"),
"reach_vs_tier": vs_tier(med([v / followers for v in views]), bench.get("view_ratio")),
"likes_vs_tier": vs_tier(med([r["likes"] / followers for r in rows]), bench.get("like_follower_ratio")),
"like_view_median": med([r["likes"] / r["list_views"] for r in rows]),
"comment_like_median": med([r["comments"] / max(r["likes"], 1) for r in rows]),
"share_view_median": med([r["shares"] / r["list_views"] for r in rows]),
"insufficient_data_by_component": dict(Counter(c for r in rows for c in r["insufficient"].split())),
"recurring_flags": [{"flag": f, "posts": c} for f, c in flags.most_common(4)],
}
def band(s: dict) -> str:
"""Screening band from fixed starting thresholds (ours, not the vendor's; no ground truth behind them)."""
if s["suspicious_posts"] / s["posts"] >= 0.3:
return "escalate"
if s["suspicious_posts"] or s["reach_vs_tier"] == "below tier min":
return "review"
return "no screening concerns"
MEMO_PROMPT = """You write a short vetting memo for a brand team about a TikTok creator before a paid deal.
Use ONLY the JSON stats you are given. Copy numbers exactly as written; do not compute new numbers.
Write plain English, not JSON key names. Ratios are per view or per follower (0.0275 = 2.75%).
Compare with the follower tier only through the reach_vs_tier and likes_vs_tier labels.
The detector is a third-party heuristic, not TikTok data and not proof of fraud: say "risk signals" or
"the endpoint estimated", never "fake account". Flags that repeat on every post are account-level.
Format: 4-6 bullet points, under 160 words, then one line "Next step:" that matches the screening band."""
def memo(stats: dict, model: str) -> tuple[str, dict]:
resp = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=120,
json={"model": model, "max_tokens": 1500,
"messages": [{"role": "system", "content": MEMO_PROMPT},
{"role": "user", "content": json.dumps(stats)}]})
resp.raise_for_status()
body = resp.json()
return body["choices"][0]["message"]["content"].strip(), {"id": body.get("id"), **(body.get("usage") or {})}
def numbers_check(text: str, stats: dict) -> list[str]:
"""Every number in the memo must match a stats value (as-is, rounded, or as a percentage)."""
allowed = set()
for v in re.findall(r"-?\d+(?:\.\d+)?", json.dumps(stats)):
x = float(v)
for y in (x, x * 100):
allowed |= {f"{y:g}", f"{y:.0f}", f"{y:.1f}", f"{y:.2f}", f"{y:,.0f}"}
found = re.findall(r"\d[\d,]*(?:\.\d+)?", text)
return [n for n in found if n not in allowed and n.replace(",", "") not in allowed]
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("handle")
ap.add_argument("--posts", type=int, default=15)
ap.add_argument("--min-age-days", type=float, default=3)
ap.add_argument("--category", default=None, help="default|entertainment|education|product|verified_large")
ap.add_argument("--model", default="openai/gpt-6-luna")
a = ap.parse_args()
t0 = time.time()
posts, skipped = recent_posts(a.handle, a.posts, a.min_age_days)
if not posts:
raise SystemExit(f"no eligible posts for @{a.handle}: {skipped}")
with ThreadPoolExecutor(max_workers=4) as pool:
rows = list(pool.map(lambda p: detect(p, a.category), posts))
t1 = time.time()
stats = aggregate(a.handle, rows)
stats["min_post_age_days"], stats["skipped_posts"] = a.min_age_days, skipped
stats["screening_band"] = band(stats)
facts = {k: v for k, v in stats.items() if not k.startswith("tier_")} # raw benchmarks invite mix-ups
text, usage = memo(facts, a.model)
unmatched = numbers_check(text, facts)
pin, pout = PRICE.get(a.model, (0, 0))
usd = (usage.get("prompt_tokens", 0) * pin + usage.get("completion_tokens", 0) * pout) / 1e6
cols = ["video", "posted", "views", "list_views", "likes", "comments", "shares", "fake_score", "fake_pct",
"est_fake_views", "suspicious", "confidence", "reason", "insufficient"]
with open(f"audit_{a.handle}.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore")
w.writeheader()
w.writerows(rows)
with open(f"memo_{a.handle}.md", "w", encoding="utf-8") as f:
f.write(f"# Creator audit: @{a.handle} ({time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime())})\n\n"
f"Screening band: **{stats['screening_band']}**\n\n{text}\n\n"
f"Numbers check: {'all numbers match the stats' if not unmatched else 'UNMATCHED ' + ', '.join(unmatched)}\n\n"
f"```json\n{json.dumps(stats, indent=1)}\n```\n")
print(json.dumps(stats, indent=1))
print(f"\n{text}\n\nunmatched numbers: {unmatched}")
print(f"{len(rows)} posts, data {t1 - t0:.1f}s, total {time.time() - t0:.1f}s, "
f"LLM {usage.get('prompt_tokens')} in / {usage.get('completion_tokens')} out, ${usd:.5f}, id {usage.get('id')}")
Four choices came out of earlier runs that went wrong:
- Which posts count. Pinned posts are old and hand-picked. Posts younger than three days are still climbing, which pushed the NBA’s reach down in my first run. Collabs carry another author’s follower count, and the agent takes the tier from the first post, so one collab can swap the benchmark. All three are skipped and counted in
skipped_posts. - Tier comparisons happen in code. Placing a ratio between min, avg and good is arithmetic, so
vs_tierwrites a label and the LLM never sees the benchmark tables. The collab bug above only showed because the memo quoted a raw benchmark; the label still prints the tier name, which is the thing to eyeball. - The LLM only words the memo. Every number comes from
aggregate, the same rule our GPT-6.1 Sol agent test led to. GPT-6 Luna is the default because a 600-token memo costs a fraction of a cent; our model tier benchmark found it 51 of 51 on tool tasks. - The screening band is mine, not the vendor’s. “Escalate” at 30% suspicious posts, “review” at any suspicious post or reach below the tier minimum. An earlier draft also sent anything with median fake_score 20 or more to review. That put Ryanair in review at 21.53 with zero suspicious posts, and I dropped it. Tuning a threshold on three brand accounts is still tuning without ground truth, so treat these as starting points.
Results on three brand accounts
The final runs started at 02:00 UTC on 2026-10-04, one after another, with default options.
| Duolingo | NBA | Ryanair | |
|---|---|---|---|
| Followers (from the detector) | 18,187,032 | 27,234,175 | 2,924,849 |
| Follower tier | xxlarge | xxlarge | xlarge |
| Posts scored, post dates | 15, Sep 10 to 30 | 15, all Sep 30 | 15, Sep 14 to 30 |
| List pages read | 3 | 7 | 3 |
| Skipped: pinned / too new / collab | 1 / 4 / 1 | 3 / 52 / 10 | 3 / 3 / 0 |
| fake_score median (min to max) | 16.26 (5.36 to 22.88) | 19.65 (16.4 to 24.6) | 19.74 (9.72 to 26.83) |
| is_suspicious | 0 of 15 | 0 of 15 | 0 of 15 |
| Fake-view percentage 5 / 10 / 20 | 5 / 7 / 3 posts | 0 / 10 / 5 | 2 / 6 / 7 |
| Estimated fake views / total views | 1,337,145 / 13,623,666 | 340,026 / 1,975,011 | 793,296 / 6,102,527 |
| Median views per follower | 0.0317 (avg to good) | 0.0028 (below tier min) | 0.0431 (min to avg) |
| Median likes per view | 0.1218 | 0.0867 | 0.0416 |
| Flags on every post | 2 follower-growth flags | 1 follower-growth flag | none |
| Screening band | no screening concerns | review | no screening concerns |
Two of the NBA’s posts showed a percentage of 9.99, which I counted as 10. All 45 posts had confidence “Minimal” and an “Insufficient data” note on the racing-mechanism component.

Caption: The detect-fake-views model page showed a Free base price, sync execution and 2 input fields on 2026-10-04, under an “API Free Week” banner, so treat the price as dated (captured 2026-10-04).
What the numbers say, and what they don’t
The fake-view estimate is a step function. Across all 45 posts, estimated_fake_views equaled views times 5%, 10% or 20%, rounded down. The two NBA posts that reported 9.99 still used 10%. The percentage was 5 for every fake_score from 5.36 to 9.98, 10 from 12.21 to 19.99, and 20 from 20.09 to 26.83. No score fell in the gaps, so the exact cutoffs are a guess. Small score moves can double the estimate. One Duolingo video scored 25.64 in three runs between 01:38 and 01:46 UTC, then 16.26 at 01:50 and 02:00. Between the 01:46 and 01:50 runs its views grew by 570, and its estimate halved, from 666,129 to 333,121. Those two runs shared 43 posts: 3 moved by 6.7 to 10.06 points, the rest by 0.01 or less. The last two runs, 10 minutes apart, matched within 0.01 on all 45. So add up “estimated fake views” for a contract only if you’re comfortable with a number that can halve between two calls.
Account-level flags repeat on every post. Duolingo got “Followers grew 5.0x in only 18 days” and “Reached 1000000 followers from 500000 in only 20 days” on all 15 posts. The NBA got a 168-day version of the second. These come from the account’s follower history, not from the video. A brand account can plausibly grow fast in its first weeks on TikTok, and the flag doesn’t say when the growth happened. TikTok’s own account got a similar flag in the single-video guide. Count them once per account.
Low reach isn’t a fake-view signal. The NBA landed in “review” only because its median views per follower, 0.0028, sat below the detector’s tier minimum of 0.005. The NBA posts a lot: 52 posts were younger than three days, and 15 eligible posts covered a single day. More posts likely spread the same audience thinner, though I didn’t measure that. It’s a reason to ask questions about expected reach, not about fraud.
content_category changed nothing here. I re-ran Duolingo with education and with entertainment. All 15 scores were identical to the run without a category, and benchmark_category stayed verified_large each time. For this verified account the detector appears to pick the benchmark itself. I didn’t test unverified creators, where the category may matter.
The two endpoints agreed on views. View counts from the post list and from the detector’s video_metrics differed by about 0.01% at most on every post.
Did the memo stick to the numbers?
The numbers check passed on all three final memos: every number in them appeared in the stats. Reading them line by line found two problems it can’t catch. Ryanair’s memo said statistical-anomaly flags “recur across posts, so treat them as account-level”. They were on 9 of 15 posts, and the prompt reserves “account-level” for flags on every post. Duolingo’s memo ended with “Proceed with the deal”, which is more than “no screening concerns” means; the table below says to continue normal vetting. In earlier runs the memos twice wrote a fake-view total as just “estimated views”. A presence check only shows a number exists. It doesn’t show it’s attached to the right label. Read the memo next to the stats block it ships with.
What to do with the result
| Result | What it means | What to do |
|---|---|---|
| No screening concerns | No suspicious posts, reach at or above the tier minimum | Continue normal vetting: audience fit, brand safety, past campaign results |
| Review: reach below tier minimum | Views per follower are low for this follower tier | Ask for TikTok Studio reach and follower-activity screenshots; check posting frequency first |
| Review: some suspicious posts | The detector flagged individual videos | Read their main_detection_reason and issue lines; ask for traffic-source data for those videos |
| Escalate: 30% or more suspicious | A pattern across posts, not one outlier | Pause the deal until the creator shares first-party analytics; don’t accuse anyone on this result alone |
| Any band, flags on every post | Account history, not the videos | Ask about growth events such as a viral hit or a paid promotion; judge recent posts on their own |
In every row, the endpoint is a screening signal. The creator’s own analytics are the evidence.
Batch timing and cost
Each run scored 15 posts with four parallel detector calls. Data time was 28.8 s for Duolingo, 72.6 s for the NBA and 27.2 s for Ryanair; the NBA needed 7 list pages to find 15 eligible posts. End to end, including the memo, the runs took 35.5, 80.3 and 34.2 seconds.
The data calls cost nothing at the listed price that day: both endpoints returned base_price: "0" from GET /v1/models/<model>, which needs the same Bearer key (the public model pages show prices without one). The model page above carried an “API Free Week” banner, so check before a large batch. GPT-6 Luna was listed at $0.10 input and $0.50 output per million tokens. The three memos used 600 to 609 input and 318 to 556 output tokens. GET /v1/tasks/<id>/cost, with the same Bearer key, showed $0.000220, $0.000330 and $0.000338, matching the program’s estimate. For data runs, the cost endpoint returned $0 for a user-post-videos run and “task not found” for a detector run.
The detector was also down for a while. From about 01:18 to 01:28 UTC every call returned HTTP 503 with an upstream error, including the reference’s own example video, while tiktok/analytics/video-metrics answered normally. It was back by 01:32. The program retries 5xx responses twice, then stops with the error. For a batch of creators, catch that and retry the creator later rather than writing a partial audit.
Limits
Scope: three verified brand accounts, 15 posts each, one morning (UTC) on 2026-10-04, plus earlier runs that shaped the code. No unverified or small creators, no other regions, no labeled fake-view data, so nothing here measures accuracy. The step mapping and the category result are observations from these 45 posts, not documented behavior. The raw responses are kept internally; the method, prompt, thresholds and aggregates are in this article.
Next steps
- Read one report in full. The single-video guide explains each block and what to trust.
- Check how stable the detector is. The detection study looks at scores across many more videos and repeat calls.
- Add profile context. The creator research guide covers profiles, follower counts and post lists.
To try it, read the detect-fake-views reference, open the model page, and get a SandBase API key.
FAQ
How do I check a TikTok influencer for fake views?
Score their recent posts, not one video, and look at the pattern: how many posts the detector marks suspicious, whether reach per follower fits their tier, and which flags repeat. Then ask the creator for first-party analytics. On three brand accounts, 0 of 45 posts were marked suspicious.
What does fake_score mean?
It’s the detector’s overall risk score, built from eight component scores. The reference doesn’t state its range or the cutoff for is_suspicious. On these 45 posts it ran from 5.36 to 26.83, and is_suspicious was false every time.
Is the estimated fake views number reliable?
Treat it as a band. On these 45 posts it was always views times 5%, 10% or 20%, chosen by fake_score. One video’s estimate halved between two calls minutes apart.
Why skip collabs and new posts?
New posts haven’t finished collecting views; they put the NBA into “review” in my first run. Collabs carry the other author’s follower count, and one swapped the NBA’s tier benchmark in a later run.
How much does an audit cost?
On 2026-10-04 the two TikTok endpoints were listed free and one GPT-6 Luna memo cost $0.00022 to $0.00034. Fifteen posts took 34 to 80 seconds.