Vet a backlink donor before you pitch
Spam score, authority, and the link-mix breakdown for any domain — 30 credits to skip a bad neighborhood.
The donor list from the backlink-donors walkthrough is candidates, not vetted donors. Two calls per domain finish the job.
Export your key once.
export SEOFETCH_KEY=sof_live_… # the scripts below use jq -- brew install jq / apt-get install jq
export SEOFETCH_KEY=sof_live_… pip install requests
export SEOFETCH_KEY=sof_live_… # Node 18+ -- built-in fetch, no npm install needed
One call: authority, spam score, and the breakdown of how the links arrive.
curl https://api.seofetch.com/v1/backlinks/summary \
-H "Authorization: Bearer $SEOFETCH_KEY" \
-H "Content-Type: application/json" \
-d '{"target": "donorsite.io"}' \
-o summary.json
import json
import os
import requests
SEOFETCH_KEY = os.environ["SEOFETCH_KEY"]
resp = requests.post(
"https://api.seofetch.com/v1/backlinks/summary",
headers={"Authorization": f"Bearer {SEOFETCH_KEY}"},
json={"target": "donorsite.io"},
)
with open("summary.json", "w") as f:
json.dump(resp.json(), f, indent=2)
const SEOFETCH_KEY = process.env.SEOFETCH_KEY;
import { writeFileSync } from "node:fs";
const resp = await fetch("https://api.seofetch.com/v1/backlinks/summary", {
method: "POST",
headers: { "Authorization": `Bearer ${SEOFETCH_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ target: "donorsite.io" }),
});
const body = await resp.json();
writeFileSync("summary.json", JSON.stringify(body, null, 2));
{
"id": "back_7hch5iqmkht2lvu4gen7qm5g",
"request_id": "req_sjj2c2hke5alfmctfadhqu3unm",
"object": "backlink_summary",
"created_at": "2026-08-09T10:07:57Z",
"elapsed_ms": 180,
"cache": "miss",
"credits": {
"charged": 20,
"balance": 9857
},
"data": {
"target": "donorsite.io",
"authority": 33,
"backlinks": 617091,
"spam_score": 9,
"referring_domains": 3582,
"referring_root_domains": 3160,
"referring_pages": 210044,
"link_breakdown": {
"tlds": {
"com": 2891,
"net": 412,
"org": 279
},
"types": {
"anchor": 17226,
"image": 421,
"redirect": 12
},
"attributes": {
"nofollow": 1769,
"ugc": 340,
"external": 88,
"sponsored": 26
},
"platforms": {
"blogs": 3021,
"cms": 1104,
"news": 214
},
"page_sections": {
"article": 12904,
"footer": 2200,
"section": 1122
},
"countries": {
"US": 9821,
"GB": 2011,
"DE": 1502
}
}
}
}
spam_score is the first gate — where you draw the line is policy, but high spam plus thin authority is a pitch you skip. The breakdown tells you how the domain earns its links.
Two ratios tell you more than the raw count.
jq '{nofollow_share: (.data.link_breakdown.attributes.nofollow / .data.backlinks), sponsored_share: (.data.link_breakdown.attributes.sponsored / .data.backlinks)}' summary.json
import json
summary = json.load(open("summary.json"))["data"]
attrs = summary["link_breakdown"]["attributes"]
total = summary["backlinks"]
print({"nofollow_share": attrs["nofollow"] / total, "sponsored_share": attrs["sponsored"] / total})
import { readFileSync } from "node:fs";
const { data } = JSON.parse(readFileSync("summary.json"));
const attrs = data.link_breakdown.attributes;
const total = data.backlinks;
console.log({
nofollowShare: attrs.nofollow / total,
sponsoredShare: attrs.sponsored / total,
});
A donor whose profile is mostly nofollow or sponsored passes less than the totals suggest. Numbers over adjectives — compute them, don't eyeball.
Organic reality-check: does it rank anywhere in your market?
curl https://api.seofetch.com/v1/domains/overview \
-H "Authorization: Bearer $SEOFETCH_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "donorsite.io", "location": 2840, "language": "en"}'
import os
import requests
SEOFETCH_KEY = os.environ["SEOFETCH_KEY"]
resp = requests.post(
"https://api.seofetch.com/v1/domains/overview",
headers={"Authorization": f"Bearer {SEOFETCH_KEY}"},
json={"domain": "donorsite.io", "location": 2840, "language": "en"},
)
data = resp.json()["data"]
const SEOFETCH_KEY = process.env.SEOFETCH_KEY;
const resp = await fetch("https://api.seofetch.com/v1/domains/overview", {
method: "POST",
headers: { "Authorization": `Bearer ${SEOFETCH_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ domain: "donorsite.io", location: 2840, language: "en" }),
});
const { data } = await resp.json();
{
"id": "doma_2wco7s4uge7ppcicf7tp2jye",
"request_id": "req_uabp6blzy5ahlmzgs56ixz3wvi",
"object": "domain_overview",
"created_at": "2026-08-09T10:07:57Z",
"elapsed_ms": 180,
"cache": "miss",
"credits": {
"charged": 10,
"balance": 9857
},
"data": {
"organic": {
"keywords_count": 101,
"traffic_estimate": 70.17,
"traffic_value": 704.38,
"positions": {
"top_3": 0,
"top_10": 3,
"top_20": 11,
"top_100": 101
}
},
"paid": {
"keywords_count": 0,
"traffic_estimate": 0,
"traffic_value": 0,
"positions": {
"top_3": 0,
"top_10": 0,
"top_20": 0,
"top_100": 0
}
}
}
}
Authority with no rankings in your market is a warning, not a verdict — this is one location/language snapshot (2840 is the United States; look up others with /v1/locations). But it's the cheap question to ask before your outreach email does.
Run the whole thing.
Each language below is the full pipeline — swap in your candidates, run it after exporting SEOFETCH_KEY: domains.txt in, verdict.csv out, sorted by authority — bring your own weighting.
#!/usr/bin/env bash
# vet.sh -- vet backlink donors before you pitch: spam score + link mix + traffic reality-check.
# domains.txt: one candidate domain per line. Output: verdict.csv, highest authority first.
set -euo pipefail
: "${SEOFETCH_KEY:?export SEOFETCH_KEY first}"
api() { curl -sS --fail-with-body "https://api.seofetch.com$1" \
-H "Authorization: Bearer $SEOFETCH_KEY" \
-H "Content-Type: application/json" \
-d "$2"; }
: > verdict.csv
while IFS= read -r d; do
[ -z "$d" ] && continue
api /v1/backlinks/summary "$(jq -cn --arg t "$d" '{target: $t}')" > "summary-$d.json"
api /v1/domains/overview \
"$(jq -cn --arg d "$d" '{domain: $d, location: 2840, language: "en"}')" > "overview-$d.json"
jq -n --arg d "$d" --slurpfile s "summary-$d.json" --slurpfile o "overview-$d.json" \
'[$d, $s[0].data.authority, $s[0].data.spam_score,
($s[0].data.link_breakdown.attributes.nofollow / $s[0].data.backlinks),
$o[0].data.organic.keywords_count, $o[0].data.organic.traffic_estimate] | @csv' \
>> verdict.csv
done < domains.txt
sort -t, -k2 -rn verdict.csv -o verdict.csv
echo "verdict.csv: domain, authority, spam_score, nofollow_share, organic_keywords, traffic -- highest authority first -- bring your own weighting"
#!/usr/bin/env python3
"""vet.py -- vet backlink donors before you pitch: spam score + link mix + traffic reality-check.
domains.txt: one candidate domain per line. Output: verdict.csv, highest authority first.
"""
import csv
import os
import requests
SEOFETCH_KEY = os.environ["SEOFETCH_KEY"]
def api(path, payload):
resp = requests.post(
f"https://api.seofetch.com{path}",
headers={"Authorization": f"Bearer {SEOFETCH_KEY}"},
json=payload,
)
resp.raise_for_status()
return resp.json()["data"]
rows = []
for d in (line.strip() for line in open("domains.txt") if line.strip()):
summary = api("/v1/backlinks/summary", {"target": d})
overview = api("/v1/domains/overview", {"domain": d, "location": 2840, "language": "en"})
nofollow_share = summary["link_breakdown"]["attributes"]["nofollow"] / summary["backlinks"]
rows.append((d, summary["authority"], summary["spam_score"], round(nofollow_share, 3),
overview["organic"]["keywords_count"], overview["organic"]["traffic_estimate"]))
rows.sort(key=lambda r: -r[1])
with open("verdict.csv", "w", newline="") as f:
csv.writer(f).writerows(rows)
print("verdict.csv: domain, authority, spam_score, nofollow_share, organic_keywords, traffic -- highest authority first -- bring your own weighting")
#!/usr/bin/env node
// vet.mjs -- vet backlink donors before you pitch: spam score + link mix + traffic reality-check.
// domains.txt: one candidate domain per line. Output: verdict.csv, highest authority first.
import { readFileSync, writeFileSync } from "node:fs";
const SEOFETCH_KEY = process.env.SEOFETCH_KEY;
if (!SEOFETCH_KEY) throw new Error("export SEOFETCH_KEY first");
async function api(path, payload) {
const resp = await fetch(`https://api.seofetch.com${path}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${SEOFETCH_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`);
return (await resp.json()).data;
}
const domains = readFileSync("domains.txt", "utf8").split("\n").map((d) => d.trim()).filter(Boolean);
const rows = [];
for (const d of domains) {
const summary = await api("/v1/backlinks/summary", { target: d });
const overview = await api("/v1/domains/overview", { domain: d, location: 2840, language: "en" });
const nofollowShare = summary.link_breakdown.attributes.nofollow / summary.backlinks;
rows.push([d, summary.authority, summary.spam_score, Number(nofollowShare.toFixed(3)),
overview.organic.keywords_count, overview.organic.traffic_estimate]);
}
rows.sort((a, b) => b[1] - a[1]);
writeFileSync("verdict.csv", rows.map((r) => r.join(",")).join("\n") + "\n");
console.log("verdict.csv: domain, authority, spam_score, nofollow_share, organic_keywords, traffic -- highest authority first -- bring your own weighting");