Skip to content

Commit d48fa20

Browse files
author
Abdeladim Fadheli
committed
Add RankBits AI visibility tracker tutorial code
1 parent e62103e commit d48fa20

1 file changed

Lines changed: 344 additions & 0 deletions

File tree

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
"""
2+
Track Your AI Visibility with Python & RankBits API.
3+
4+
This script demonstrates the full workflow:
5+
1. Check your RankBits account and plan
6+
2. Create an AI visibility scan for any domain
7+
3. Poll until the scan completes
8+
4. Parse the results and generate visualizations
9+
10+
Requirements:
11+
pip install requests matplotlib
12+
13+
Usage:
14+
export RANKBITS_TOKEN="rb_your_token_here"
15+
python ai_visibility_tracker.py
16+
"""
17+
18+
import os
19+
import sys
20+
import time
21+
import json
22+
from datetime import datetime
23+
24+
import requests
25+
import matplotlib.pyplot as plt
26+
import matplotlib.ticker as mticker
27+
28+
# ---------------------------------------------------------------------------
29+
# Configuration
30+
# ---------------------------------------------------------------------------
31+
32+
TOKEN = os.environ.get("RANKBITS_TOKEN", "rb_your_token_here")
33+
BASE_URL = "https://rankbits.com/v1"
34+
HEADERS = {
35+
"Authorization": f"Bearer {TOKEN}",
36+
"Content-Type": "application/json",
37+
}
38+
39+
# The domain you want to scan
40+
TARGET_URL = "https://thepythoncode.com"
41+
42+
# Free engines to use (omit "paid" providers like openai_pro, claude_pro, gemini_pro)
43+
ENGINES = ["openai", "gemini", "perplexity", "claude", "google_ai_mode"]
44+
45+
# Number of AI-generated prompts (plan caps apply)
46+
PROMPT_COUNT = 5
47+
48+
49+
# ---------------------------------------------------------------------------
50+
# Helper: pretty-print JSON
51+
# ---------------------------------------------------------------------------
52+
53+
def print_json(obj: dict, title: str = "") -> None:
54+
"""Print a dictionary as formatted JSON."""
55+
if title:
56+
print(f"\n{'=' * 60}\n{title}\n{'=' * 60}")
57+
print(json.dumps(obj, indent=2, default=str))
58+
59+
60+
# ---------------------------------------------------------------------------
61+
# Step 1 – Check your account
62+
# ---------------------------------------------------------------------------
63+
64+
def check_account() -> dict:
65+
"""Fetch plan info and credit usage from /v1/me."""
66+
resp = requests.get(f"{BASE_URL}/me", headers=HEADERS)
67+
resp.raise_for_status()
68+
data = resp.json()
69+
plan = data["plan"]
70+
resp_info = plan["responses"]
71+
72+
print("🔑 Account")
73+
print(f" Plan: {plan['label']} (${plan['price_usd']}/mo)")
74+
print(f" Monthly: {resp_info['used']}/{resp_info['monthly_limit']} responses")
75+
print(f" Credits: {resp_info['purchased_remaining']} purchased remaining")
76+
print(f" Engines: {len(plan['allowed_provider_keys'])} available")
77+
return data
78+
79+
80+
# ---------------------------------------------------------------------------
81+
# Step 2 – Create a scan
82+
# ---------------------------------------------------------------------------
83+
84+
def create_scan(
85+
url: str,
86+
prompt_count: int = 5,
87+
providers: list[str] | None = None,
88+
) -> dict:
89+
"""Submit an async scan and return the public ID."""
90+
payload: dict = {"url": url, "prompt_count": prompt_count}
91+
if providers:
92+
payload["providers"] = providers
93+
94+
resp = requests.post(f"{BASE_URL}/scans", headers=HEADERS, json=payload)
95+
resp.raise_for_status()
96+
data = resp.json()
97+
98+
scan = data["scan"]
99+
print(f"\n🚀 Scan created")
100+
print(f" ID: {scan['public_id']}")
101+
print(f" Domain: {scan['domain']}")
102+
print(f" Status: {scan['status']}")
103+
print(f" View live: https://rankbits.com{data['links']['app']}")
104+
return data
105+
106+
107+
# ---------------------------------------------------------------------------
108+
# Step 3 – Poll until done
109+
# ---------------------------------------------------------------------------
110+
111+
def poll_scan(public_id: str, poll_seconds: float = 3.0, max_wait: float = 300.0) -> dict:
112+
"""Poll /v1/scans/{id} until status is 'done' or timeout."""
113+
url = f"{BASE_URL}/scans/{public_id}"
114+
start = time.time()
115+
last_completed = 0
116+
117+
print(f"\n⏳ Polling scan {public_id} ...")
118+
while True:
119+
elapsed = time.time() - start
120+
if elapsed > max_wait:
121+
raise TimeoutError(f"Scan did not complete within {max_wait}s")
122+
123+
resp = requests.get(url, headers=HEADERS)
124+
resp.raise_for_status()
125+
data = resp.json()
126+
127+
status = data["scan"]["status"]
128+
progress = data.get("progress", {})
129+
completed = progress.get("completed_results", 0)
130+
expected = progress.get("expected_results", 0)
131+
132+
# Print progress when it changes
133+
if completed != last_completed:
134+
pct = (completed / expected * 100) if expected else 0
135+
print(f" [{status}] {completed}/{expected} ({pct:.0f}%)")
136+
last_completed = completed
137+
138+
if status == "done":
139+
print(" ✅ Scan complete!")
140+
return data
141+
if status in ("error", "failed"):
142+
raise RuntimeError(f"Scan failed: {data}")
143+
144+
time.sleep(poll_seconds)
145+
146+
147+
# ---------------------------------------------------------------------------
148+
# Step 4 – Parse & display results
149+
# ---------------------------------------------------------------------------
150+
151+
def summarize_results(data: dict) -> None:
152+
"""Print a human-readable summary of scan results."""
153+
aggregate = data.get("aggregate", {})
154+
overall = aggregate.get("overall", {})
155+
providers = aggregate.get("providers", {})
156+
results = data.get("results", [])
157+
prompts = data.get("prompts", [])
158+
159+
# ---- 4a. Overview ----
160+
print(f"\n📊 Visibility Summary for {data['scan']['domain']}")
161+
print(f" Overall score: {overall.get('score', 'N/A')}")
162+
print(f" Mention rate: {overall.get('mention_rate', 0):.1f}%")
163+
print(f" Citation rate: {overall.get('citation_rate', 0):.1f}%")
164+
print(f" Total results: {len(results)} rows")
165+
166+
# ---- 4b. Per-engine breakdown ----
167+
print(f"\n🤖 Engine Breakdown")
168+
print(f" {'Engine':<20s} {'Score':>7s} {'Mention%':>9s} {'Citation%':>10s}")
169+
print(f" {'-'*46}")
170+
for key, pdata in sorted(providers.items(), key=lambda x: -x[1].get("score", 0)):
171+
print(
172+
f" {key:<20s} {pdata.get('score', 0):>7.1f} "
173+
f"{pdata.get('mention_rate', 0):>8.1f}% {pdata.get('citation_rate', 0):>9.1f}%"
174+
)
175+
176+
# ---- 4c. Prompts used ----
177+
print(f"\n💬 Prompts ({len(prompts)})")
178+
for p in prompts:
179+
print(f" • {p['text']}")
180+
181+
# ---- 4d. Share of voice (top 5) ----
182+
sov = aggregate.get("share_of_voice", [])
183+
if sov:
184+
print(f"\n🔗 Top Cited Domains (Share of Voice)")
185+
for entry in sov[:5]:
186+
print(f" {entry['domain']:40s} {entry.get('citation_count', 0)} citations")
187+
188+
# ---- 4e. Where we were found ----
189+
found = [r for r in results if r.get("brand_mentioned") or r.get("brand_cited")]
190+
if found:
191+
print(f"\n✅ Where {data['scan']['domain']} Appeared ({len(found)}/{len(results)})")
192+
for r in found:
193+
mentioned = "✅" if r["brand_mentioned"] else "❌"
194+
cited = "✅" if r["brand_cited"] else "❌"
195+
print(f" [{r['provider']:20s}] Mentioned: {mentioned} Cited: {cited}")
196+
print(f" Prompt: {r['prompt'][:100]}")
197+
else:
198+
print(f"\n⚠️ {data['scan']['domain']} was NOT mentioned or cited in any result!")
199+
print(" Time to improve your AI visibility! → https://rankbits.com")
200+
201+
202+
# ---------------------------------------------------------------------------
203+
# Step 5 – Generate charts
204+
# ---------------------------------------------------------------------------
205+
206+
def generate_charts(data: dict, output_dir: str = ".") -> None:
207+
"""Create matplotlib charts from scan results."""
208+
aggregate = data.get("aggregate", {})
209+
providers = aggregate.get("providers", {})
210+
domain = data["scan"]["domain"]
211+
212+
if not providers:
213+
print("⚠️ No provider data to chart.")
214+
return
215+
216+
# Sort engines by score descending
217+
engines = sorted(providers.items(), key=lambda x: -x[1].get("score", 0))
218+
names = [e[0].replace("_", " ").title() for e in engines]
219+
scores = [e[1].get("score", 0) for e in engines]
220+
mention_rates = [e[1].get("mention_rate", 0) for e in engines]
221+
citation_rates = [e[1].get("citation_rate", 0) for e in engines]
222+
223+
# Colors
224+
bar_color = "#7c3aed"
225+
mention_color = "#10b981"
226+
citation_color = "#f59e0b"
227+
228+
# ---- Chart 1: Scores by engine ----
229+
fig1, ax1 = plt.subplots(figsize=(8, 5))
230+
bars = ax1.barh(names, scores, color=bar_color, edgecolor="white", linewidth=0.5, height=0.5)
231+
ax1.set_xlabel("Visibility Score (0–100)", fontsize=11)
232+
ax1.set_title(f"AI Visibility Score by Engine — {domain}", fontsize=13, fontweight="bold")
233+
ax1.invert_yaxis()
234+
ax1.xaxis.set_major_formatter(mticker.FormatStrFormatter("%.0f"))
235+
for bar, val in zip(bars, scores):
236+
ax1.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height() / 2,
237+
f"{val:.1f}", va="center", fontsize=10, fontweight="semibold")
238+
ax1.set_xlim(0, max(scores) * 1.3 + 5 if max(scores) > 0 else 30)
239+
plt.tight_layout()
240+
fig1.savefig(f"{output_dir}/engine_scores.png", dpi=150)
241+
print(f"\n📈 Chart saved: {output_dir}/engine_scores.png")
242+
243+
# ---- Chart 2: Mention vs Citation rates ----
244+
fig2, ax2 = plt.subplots(figsize=(8, 5))
245+
x = range(len(names))
246+
width = 0.35
247+
ax2.bar([i - width / 2 for i in x], mention_rates, width, label="Mention Rate %",
248+
color=mention_color, edgecolor="white", linewidth=0.5)
249+
ax2.bar([i + width / 2 for i in x], citation_rates, width, label="Citation Rate %",
250+
color=citation_color, edgecolor="white", linewidth=0.5)
251+
ax2.set_xticks(x)
252+
ax2.set_xticklabels(names, fontsize=9)
253+
ax2.set_ylabel("Percentage (%)", fontsize=11)
254+
ax2.set_title(f"Mention vs Citation Rate — {domain}", fontsize=13, fontweight="bold")
255+
ax2.legend(fontsize=10, loc="upper right")
256+
ax2.set_ylim(0, max(max(mention_rates), max(citation_rates)) * 1.4 + 5)
257+
plt.tight_layout()
258+
fig2.savefig(f"{output_dir}/mention_vs_citation.png", dpi=150)
259+
print(f"📈 Chart saved: {output_dir}/mention_vs_citation.png")
260+
261+
# ---- Chart 3: Results grid (heatmap-style table) ----
262+
results = data.get("results", [])
263+
if results:
264+
# Build a matrix: rows=prompts, cols=engines
265+
prompt_texts = sorted({r["prompt"][:60] for r in results})
266+
engine_names = sorted({r["provider"] for r in results})
267+
268+
matrix = []
269+
for pt in prompt_texts:
270+
row = []
271+
for eng in engine_names:
272+
match = [r for r in results if r["prompt"].startswith(pt[:30]) and r["provider"] == eng]
273+
if match:
274+
m = match[0]
275+
if m["brand_cited"]:
276+
row.append(2) # cited (best)
277+
elif m["brand_mentioned"]:
278+
row.append(1) # mentioned
279+
else:
280+
row.append(0) # absent
281+
else:
282+
row.append(0)
283+
matrix.append(row)
284+
285+
fig3, ax3 = plt.subplots(figsize=(max(8, len(engine_names) * 1.2),
286+
max(5, len(prompt_texts) * 0.6)))
287+
cmap = plt.cm.RdYlGn
288+
im = ax3.imshow(matrix, cmap=cmap, aspect="auto", vmin=0, vmax=2)
289+
290+
ax3.set_xticks(range(len(engine_names)))
291+
ax3.set_xticklabels([e.replace("_", " ").title() for e in engine_names],
292+
rotation=30, ha="right", fontsize=9)
293+
ax3.set_yticks(range(len(prompt_texts)))
294+
ax3.set_yticklabels(prompt_texts, fontsize=8)
295+
296+
# Add text in each cell
297+
for i in range(len(prompt_texts)):
298+
for j in range(len(engine_names)):
299+
val = matrix[i][j]
300+
symbol = {0: "○", 1: "▲", 2: "★"}[val]
301+
ax3.text(j, i, symbol, ha="center", va="center",
302+
fontsize=14, color="black" if val == 2 else "white")
303+
304+
ax3.set_title(f"Presence Grid — {domain}\n○ Absent ▲ Mentioned ★ Cited",
305+
fontsize=12, fontweight="bold")
306+
plt.tight_layout()
307+
fig3.savefig(f"{output_dir}/presence_grid.png", dpi=150)
308+
print(f"📈 Chart saved: {output_dir}/presence_grid.png")
309+
310+
311+
# ---------------------------------------------------------------------------
312+
# Main
313+
# ---------------------------------------------------------------------------
314+
315+
def main() -> None:
316+
if TOKEN == "rb_your_token_here":
317+
print("❌ Set your RANKBITS_TOKEN environment variable first.")
318+
print(" Get one at: https://rankbits.com/signup")
319+
sys.exit(1)
320+
321+
print(f"🎯 Tracking AI visibility for: {TARGET_URL}")
322+
print(f" Engines: {', '.join(ENGINES)}")
323+
324+
# 1. Check account
325+
check_account()
326+
327+
# 2. Start scan
328+
scan_data = create_scan(TARGET_URL, prompt_count=PROMPT_COUNT, providers=ENGINES)
329+
public_id = scan_data["scan"]["public_id"]
330+
331+
# 3. Poll until complete
332+
results = poll_scan(public_id)
333+
334+
# 4. Summarize
335+
summarize_results(results)
336+
337+
# 5. Charts
338+
generate_charts(results)
339+
340+
print("\n✨ Done! Track ongoing visibility at https://rankbits.com")
341+
342+
343+
if __name__ == "__main__":
344+
main()

0 commit comments

Comments
 (0)