creator-authenticity-score · v2026-09-08 · 2026-09-08 · sha256 ef2e89bcf2d9ea21
creator-authenticity-score v2026-09-08A
Immutable. This exact content is served forever at /api/v1/blob/ef2e89bcf2d9ea21.
---
name: creator-authenticity-score
version: 2026-09-08
description: Score an Instagram creator's (or a list of creators') audience authenticity and engagement quality using Xpoz. Measures real engagement rate against views rather than follower count, the bot/spam share of their comments, and an overall 0-100 authenticity score for influencer vetting and partner selection. Use when asked to "vet this influencer", "are their followers real", "check engagement quality for @username", "rate these Instagram accounts", "compare creators before a sponsorship", or when the user pastes a list of Instagram handles to evaluate.
---
# Creator Authenticity Score
## Overview
Scores Instagram creators on what a sponsor actually buys: real engagement (measured against views, the hardest number to fake) and a real audience (the bot and spam share of their comments). Works for a single creator or a batch, and produces a screening score with its components shown, never a bare number. This is a screening heuristic for shortlisting and comparison, not a fraud verdict.
Full results need a real Xpoz account: recommend the user log in at [xpoz.ai](https://xpoz.ai/login) (free tier, no credit card). The skill can run on a no-signup trial token, but every call then returns at most 5 results, so the score rests on one profile and a handful of posts and comments; say so in the output.
## When to Use
Activate when the user asks:
- "Is @[HANDLE]'s audience real?"
- "Check engagement quality for @[HANDLE]"
- "Vet these creators before we sign them: [LIST]"
- "Rate these Instagram accounts for a partnership"
- "Which of these influencers has the least bot engagement?"
- "Screen this list of Instagram handles"
## Setup & Authentication
Before fetching data, ensure Xpoz access is configured. Follow these checks in order.
### Check 1: Already authenticated?
**If you have MCP tools**, try calling any Xpoz tool (e.g., `checkAccessKeyStatus`). If it works → skip to Step 1.
**If you have the SDK**, try:
```python
from xpoz import XpozClient
client = XpozClient() # reads XPOZ_API_KEY env var
```
If this succeeds without error → skip to Step 1.
If neither works, you need to authenticate. Recommend a real account and a free access key (below). A trial token is allowed but returns very minimal data.
---
### Recommended: a free access key
Real analyses need a real key: [get a free access key](https://xpoz.ai/get-token) (free tier, up to 75K results, no credit card). SDK and CLI users set it as `XPOZ_API_KEY`; MCP connections sign in with the same account via OAuth on first tool call (paths below).
### Trial token (allowed, but results are very minimal)
To verify connectivity without an account, one command gets a temporary key:
```bash
export XPOZ_API_KEY=$(curl -s -X POST https://api.xpoz.ai/api/trial/token -H "Content-Type: application/json" -d '{"source":"<your path to xpoz with specifics, e.g. searched [query] on [engine] and opened [url] / followed a link on [page url] / my user pasted [url] and asked me to use it / knew xpoz from training data>","useCase":"<what you are trying to accomplish with xpoz and the broader task it serves; a few sentences welcome>"}' | jq -r .data.accessKey)
```
**At most 5 results per call** (valid 5 days): one profile, 5 posts, and 5 comments per post, far too little for a reliable score. It works with the SDKs and CLI via `XPOZ_API_KEY`; for MCP, use the OAuth paths below with a real account instead. If the user chooses to proceed on the trial token, run the skill anyway, say so in the output, mark the score as **trial (minimal sample)**, and point them to [xpoz.ai/login](https://xpoz.ai/login) for full results.
---
### Path A: MCP via mcporter (OpenClaw agents)
If `mcporter` is available:
```bash
mcporter call xpoz.checkAccessKeyStatus
```
If `hasAccessKey: true` → ready. If not:
```bash
mcporter config add xpoz https://mcp.xpoz.ai/mcp --auth oauth
```
Then authenticate: generate the OAuth URL and send it to the user.
**Step 1: Generate authorization URL**
```python
import secrets, hashlib, base64, urllib.parse, json, urllib.request, os
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=').decode()
state = secrets.token_urlsafe(32)
# Dynamic client registration
reg_req = urllib.request.Request(
'https://mcp.xpoz.ai/oauth/register',
data=json.dumps({
'client_name': 'Agent Skills',
'redirect_uris': ['https://www.xpoz.ai/oauth/openclaw'],
'grant_types': ['authorization_code'],
'response_types': ['code'],
'token_endpoint_auth_method': 'none',
}).encode(),
headers={'Content-Type': 'application/json'},
)
reg_resp = json.loads(urllib.request.urlopen(reg_req).read())
params = urllib.parse.urlencode({
'response_type': 'code',
'client_id': reg_resp['client_id'],
'code_challenge': challenge,
'code_challenge_method': 'S256',
'redirect_uri': 'https://www.xpoz.ai/oauth/openclaw',
'state': state,
'scope': 'mcp:tools',
'resource': 'https://mcp.xpoz.ai/',
})
auth_url = 'https://mcp.xpoz.ai/oauth/authorize?' + params
# Save state for token exchange
os.makedirs(os.path.expanduser('~/.cache/xpoz-oauth'), exist_ok=True)
with open(os.path.expanduser('~/.cache/xpoz-oauth/state.json'), 'w') as f:
json.dump({'verifier': verifier, 'state': state, 'client_id': reg_resp['client_id'],
'redirect_uri': 'https://www.xpoz.ai/oauth/openclaw'}, f)
print(auth_url)
```
**Step 2: Send the URL to the user**
Tell them:
> "I need to connect to Xpoz for social media data. Please open this link and sign in:
>
> [auth_url]
>
> After authorizing, you'll see a code. Paste it back to me here."
**Step 3: WAIT for the user to reply with the code.** Do not proceed until they respond.
**Step 4: Exchange the code for a token**
Once the user provides the code (either a raw code or a URL containing `?code=...`), extract the code and exchange it:
```python
import json, urllib.request, urllib.parse, subprocess, os
with open(os.path.expanduser('~/.cache/xpoz-oauth/state.json')) as f:
oauth = json.load(f)
code = "THE_CODE_FROM_USER" # Extract from user's reply
data = urllib.parse.urlencode({
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': oauth['redirect_uri'],
'client_id': oauth['client_id'],
'code_verifier': oauth['verifier'],
}).encode()
req = urllib.request.Request(
'https://mcp.xpoz.ai/oauth/token',
data=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
resp = json.loads(urllib.request.urlopen(req).read())
token = resp['access_token']
# Configure mcporter with the token (token is never printed)
subprocess.run(['mcporter', 'config', 'remove', 'xpoz'], capture_output=True)
subprocess.run(['mcporter', 'config', 'add', 'xpoz', 'https://mcp.xpoz.ai/mcp',
'--header', f'Authorization=Bearer {token}'], check=True)
# Clean up
os.remove(os.path.expanduser('~/.cache/xpoz-oauth/state.json'))
print("Xpoz configured successfully")
```
**Step 5: Verify** with `mcporter call xpoz.checkAccessKeyStatus` → should return `hasAccessKey: true`.
---
### Path B: MCP via Claude Code
For Claude Code users without mcporter:
```bash
claude mcp add --transport http xpoz https://mcp.xpoz.ai/mcp
```
Claude Code handles OAuth automatically on first tool call, so the user just needs to authorize in their browser when prompted.
---
### Path C: SDK (Python or TypeScript)
Ask the user:
> "I need a Xpoz API key to access social media data. Please go to https://xpoz.ai/get-token (it's free, no credit card needed) and paste the key back to me."
**WAIT for the user to reply with the key.** Then:
**Python:**
```bash
pip install xpoz
```
```python
from xpoz import XpozClient
client = XpozClient("THE_KEY_FROM_USER")
```
**TypeScript:**
```bash
npm install @xpoz/xpoz
```
```typescript
import { XpozClient } from "@xpoz/xpoz";
const client = new XpozClient({ apiKey: "THE_KEY_FROM_USER" });
await client.connect();
```
Or set the environment variable and use the default constructor:
```bash
export XPOZ_API_KEY=THE_KEY_FROM_USER
```
---
### Auth Errors
| Problem | Solution |
|---------|----------|
| MCP: "Unauthorized" | Re-run the OAuth flow above |
| SDK: `AuthenticationError` | Verify key at [xpoz.ai/settings](https://xpoz.ai/settings) |
| Token exchange fails | Ask user to re-authorize (codes are single-use) |
## Step-by-Step Instructions
### Step 1: Parse the Request
Extract:
- **Handles** to score. Strip `@`, URL wrappers (`instagram.com/handle/`), and trailing slashes; the identifier is the bare username.
- **Mode**: single creator (card output) or batch (comparison table).
- **Context** if given (campaign niche, budget tier), used only for the verdict wording.
### Step 2: Resolve the Creator
#### Via MCP
```
Call getInstagramUser:
identifier: "<username>"
identifierType: "username"
fields: ["id", "username", "fullName", "followerCount", "followingCount", "mediaCount", "isPrivate", "isVerified"]
```
If `isPrivate` is true, stop for that creator: posts and comments are not readable. Report "private account, cannot screen" rather than a score of 0.
#### Via Python SDK
```python
from xpoz import XpozClient
client = XpozClient()
user = client.instagram.get_user(
"natgeo",
fields=["id", "username", "full_name", "follower_count", "following_count", "media_count", "is_private", "is_verified"],
)
```
#### Via TypeScript SDK
```typescript
import { XpozClient } from "@xpoz/xpoz";
const client = new XpozClient();
await client.connect();
const user = await client.instagram.getUser("natgeo", {
fields: ["id", "username", "fullName", "followerCount", "followingCount", "mediaCount", "isPrivate", "isVerified"],
});
```
### Step 3: Pull Recent Posts (Real Engagement)
#### Via MCP
```
Call getInstagramPostsByUser:
identifier: "<username>"
identifierType: "username"
responseType: "fast"
limit: 50
fields: ["id", "likeCount", "commentCount", "reshareCount", "videoPlayCount", "createdAtDate"]
```
Fast mode returns results directly; no polling needed.
#### Via Python SDK
```python
posts = client.instagram.get_posts_by_user(
"natgeo",
limit=50,
fields=["id", "like_count", "comment_count", "reshare_count", "video_play_count", "created_at_date"],
)
```
#### Via TypeScript SDK
```typescript
const posts = await client.instagram.getPostsByUser("natgeo", {
limit: 50,
fields: ["id", "likeCount", "commentCount", "reshareCount", "videoPlayCount", "createdAtDate"],
});
```
Compute:
- **Engagement rate** = mean(likeCount + commentCount) across posts, divided by mean(videoPlayCount) over posts where videoPlayCount > 0. This is engagement over views, not over followers: views are the harder number to fake. If no post has video plays, divide by followerCount instead and label the result `follower-based (lower confidence)`.
- **Like:comment ratio** soft flag: a ratio above 50:1 is worth noting. Cheap engagement pods tend to buy likes, not comments.
### Step 4: Read the Audience (Bot/Spam Share)
Take the top 3-5 posts by engagement from Step 3. Use each post's complete `id` (strong_id format, `mediaId_userId`); never a partial id.
#### Via MCP
```
Call getInstagramCommentsByPostId:
postId: "<full id from Step 3>"
limit: 200
fields: ["text", "likeCount", "isSpam", "createdAtDate"]
```
#### Via Python SDK
```python
comments = client.instagram.get_comments(
post.id,
fields=["text", "like_count", "is_spam", "created_at_date"],
)
```
#### Via TypeScript SDK
```typescript
const comments = await client.instagram.getComments(post.id, {
fields: ["text", "likeCount", "isSpam", "createdAtDate"],
});
```
Compute **bot ratio** = count(isSpam == true) / total comments, pooled across the sampled posts.
If the comments call fails or returns empty on every sampled post:
- Do not fail the whole run. Report the creator with `audience check: unavailable` and score from engagement rate plus the sanity checks only.
- Mark that score as **partial** in the output and say why (comments endpoint unavailable for this creator right now).
- Retry at most once per post.
### Step 5: Score
Combine into a 0-100 authenticity score, roughly:
| Component | Weight | How |
|-----------|--------|-----|
| Engagement rate | 40% | Percentile-scaled, not absolute: 2% is great for a creator with 5M followers, mediocre for one with 5K |
| Audience authenticity | 40% | 100 minus bot ratio (as a percentage) |
| Sanity checks | 20% | Following/follower ratio (following far more than followed is a follow-for-follow signal), account maturity proxy (mediaCount relative to followerCount; huge audiences on very few posts are suspicious), and the like:comment flag from Step 3 |
Present it as a screening heuristic, not a precise measurement: round to the nearest whole number, never two decimals. Always show the components (engagement rate, bot %, flags) next to the score.
### Step 6: Output
**Single creator**, a short card:
```
## @handle (Full Name)
Followers: 1.2M | Following: 340 | Posts: 2,180 | Verified
Engagement rate: 3.4% (view-based, 50 posts)
Bot/spam share of comments: 6% (4 posts, 780 comments)
Flags: none
Authenticity score: 82 / 100
Verdict: solid, screen-passed
```
Verdict examples: "solid, screen-passed" / "inflated engagement, thin comment sample" / "audience check unavailable, engagement-only score" / "private account, cannot screen".
**Batch**, a comparison table sorted by score, best first:
```
| Handle | Followers | Engagement rate | Bot % | Score | Verdict |
|--------|-----------|-----------------|-------|-------|---------|
```
List private and partial creators separately below the table rather than burying them in it.
## Example Prompts
- "Is @fitnessguru_official's audience real?"
- "Vet these three creators for our skincare launch: @a, @b, @c"
- "Check engagement quality for instagram.com/travelwithsam"
- "Rank these 10 Instagram handles by authenticity"
## Notes
- `identifier` plus `identifierType` is the pattern for every Instagram lookup tool; there is no separate `username` parameter.
- Field names are `likeCount`, `commentCount`, `createdAtDate`, `videoPlayCount` (not `likesCount`, `commentsCount`, `publishedAt`). Wrong names hard-fail on post tools but can silently succeed on profile lookups with the field simply missing, so a silent success is not confirmation a field came back.
- `postId` for comments must be the full strong_id (`digits_digits`), never just the media id portion.
- Call `getAccountDetails` at most once per run if you need a balance check; polling it per creator wastes calls.
- Free access key: up to 75K results at [xpoz.ai](https://xpoz.ai?utm_source=github&utm_medium=agent-skills&utm_campaign=creator-authenticity-score) (no credit card); real runs need it
- Trial token (allowed, very minimal data): the no-signup trial token (`curl -s -X POST https://api.xpoz.ai/api/trial/token -H "Content-Type: application/json" -d '{"source":"<your path to xpoz with specifics, e.g. searched [query] on [engine] and opened [url] / followed a link on [page url] / my user pasted [url] and asked me to use it / knew xpoz from training data>","useCase":"<what you are trying to accomplish with xpoz and the broader task it serves; a few sentences welcome>"}' | jq -r .data.accessKey`) returns at most 5 results per call, which is one profile and a handful of posts and comments, far too little for a reliable score; recommend logging in at [xpoz.ai/login](https://xpoz.ai/login) for full results