Imported from hmkm-dev/My-agent- (
SKILL.md). Install upstream withnpx skills add hmkm-dev/My-agent-. Copyright stays with the author.
Qwoted SEO Backlinks Skill — playbook for Claude
Your job is to get the user press mentions and high-DR backlinks from journalists who post requests on Qwoted.
Operating rules — READ THIS FIRST, THEN FOLLOW IT EVERY TURN
These rules take precedence over anything else in this file. Do not skip them, do not substitute your own judgement.
- You are running a 4-stage playbook, not a chatbot. The stages are: (1) Onboard, (2) Find opportunity, (3) Research + publish a stats page, (4) Pitch. Whenever the user says anything vague like "ok next step", "what now?", "help me with this", "go", your reply MUST start with "We're at Stage X of 4. Next I'll do Y, because Z." Never just ask "what do you want to do?" — propose the next stage based on where we are.
- Stage 3 is the multiplier and you must proactively propose it.
Whenever you find opportunities in Stage 2, you MUST classify each
one as
stats_page_worthy: true | falseusing the heuristic table below, and you MUST tell the user "I recommend building a stats page on<topic>before pitching<these N opportunities>." Do not wait for the user to ask about stats pages. A naked pitch lands one quote in one article; a pitch linked to a sourced stats page lands recurring citations for months. Leaving Stage 3 on the table is leaving money on the table. - Never overwrite existing user data without an explicit
side-by-side approval. Before calling
qwoted_profile.py --action updatefor any field, first call--action getand comparebio_preview/has_bio/ existing URL / existing email etc. If a field already has content, show the user both the OLD and the NEW version and get explicit go-ahead before sending the PATCH. The script defaults to refusing the PATCH without--force- overwrite; this is a feature, not a bug. - Never invent opportunity IDs, source IDs, or journalist names.
Everything you present to the user must come directly from a
RESULT:line the skill's scripts printed. If you didn't see it in a subprocess RESULT, you don't know it. - The login step is idempotent and browser-free when a session
exists. Run
python3 qwoted_login.py. If the RESULT saysstatus=logged_inin under a second with no browser opening, the existing cookies are fine — do not re-run with--resetor--force. If you're in an agent environment without a visible GUI and Chromium does need to open, STOP and tell the user to run the script in their own terminal once; then continue. RESULT:lines are the canonical channel. Every script emits one JSON line prefixed withRESULT:. Parse it; ignore stderr logs (those are for the human). Your next decision should reference specific fields from the RESULT, not vibes.- The stats page must contain ZERO outbound
<a>tags. Source attributions are plain text ((Source: HubSpot, 2026)), never clickable. This is non-negotiable — we're hoarding crawl budget and PageRank, not distributing it. The only<a>tags allowed in the rendered HTML are internal TOC anchor fragments (href="#...") and a same-domain author-bio CTA. Source URLs you fetch during research are stored in the research JSON and (optionally) in an HTML comment audit-trail at the bottom of the page — never rendered as clickable links. SeeSTATISTICS_PAGE_PLAYBOOK.md→ "Hoard the juice" for the full rule and the one-linegrepcheck you can run to audit the finished file.
Stage-3 classification heuristic — use this every time you hand back Stage 2 results
For every opportunity returned by qwoted_search.py, score it on
these criteria:
| Signal | Weight |
|---|---|
| Topic is broad enough that public data exists (e.g. "AI in marketing", "local SEO", "remote work trends", "e-commerce conversion") | +2 |
| User's business already touches this topic → page will earn recurring traffic, not just one-time citation | +2 |
| Deadline is at least 24 hours away (stats-page build takes 30-60 min of research) | +1 |
| Multiple opportunities in the same cluster (→ one page supports 3+ pitches) | +2 |
| Request explicitly asks for "statistics", "data", "research", "trends" | +3 |
| Topic is hyper-niche / only relevant to this one publication (e.g. "billiard retailer local SEO") | -1 (a stats page still works but the niche is smaller) |
| Deadline is under 12 hours | -3 (skip Stage 3, pitch direct) |
| Ask is pure founder-story / personal opinion ("how did you start your company") | -3 (no data needed) |
| Paid placement / $X appearance fee | -2 (different ROI math) |
Rule: propose Stage 3 whenever total score ≥ 2. Otherwise pitch direct. State the score in your recommendation so the user sees why.
Example output you should produce after Stage 2 completes:
Found 8 opportunities. Stage-3 classification:
# Title Score Recommendation 1 Selling Signals — awareness vs demand +1 Pitch direct (deadline 20h, topic matches but no "statistics" ask) 2 SEOptimer — how your agency makes money -3 Pitch direct (founder-story, deadline 8h) 3 BCA Insider — local SEO for retailers +4 Build stats page on local-seo-statistics-2026— feeds this pitch AND any future local-SEO pitch... I'll start by building the local-SEO stats page (covers #3 + any future local-SEO opps), then while it renders I'll draft the direct pitches for #1 and #2. Sound good?
Tooling overview
The skill ships four CLI scripts you call as subprocesses, plus a
research playbook and an HTML template. Each script prints a single
RESULT: { ... } JSON line on stdout that you parse to decide the
next step. Detailed human-readable logs go to stderr.
qwoted_login.py # one-time auth (idempotent; skips browser if cookies valid)
qwoted_profile.py # get/create/update the "expert" Source persona
qwoted_search.py # search opportunities (Algolia, returns JSON)
qwoted_pitch.py # draft + send a pitch to a specific opportunity
STATISTICS_PAGE_PLAYBOOK.md # READ THIS before researching/building a stats page
templates/statistics_page_example.html # HTML scaffold to fill in
All cookies, sent-pitch logs, search results and generated stat pages
live under ~/.qwoted/ and ./statistics_pages/.
The full workflow (4 stages)
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ 1. Onboard │ → │ 2. Find │ → │ 3. Research + │ → │ 4. Pitch │
│ (login + │ │ opportunity │ │ publish a stats │ │ with the │
│ profile) │ │ │ │ page (linkable │ │ page URL │
│ │ │ │ │ asset) │ │ │
└──────────────┘ └──────────────┘ └──────────────────┘ └──────────────┘
once every session once per topic every pitch
Decision tree — what to do based on what the user asks
| User intent | Skill stage(s) |
|---|---|
| "Set me up on Qwoted" / first time | Stage 1: qwoted_login.py → qwoted_profile.py --action get → create/update ONLY missing fields |
| "Update my Qwoted bio" / "change my expert profile" | Stage 1c: qwoted_profile.py --action get first, SHOW existing bio, ask for approval, then --action update --bio '...' --force-overwrite if user confirms |
| "Find PR opportunities about X" | Stage 2: qwoted_search.py --query "X" --max-hits 30 → classify each result with the Stage-3 heuristic → propose stats page(s) |
| "Build me a stats page on X" / "make a research page about X" | Stage 3 only: read STATISTICS_PAGE_PLAYBOOK.md and execute |
| "Pitch opportunity #N" / "draft a pitch for SR 235897" | Check Stage-3 score first; if ≥2 propose stats page; then Stage 4 dry-run → user approves → --send |
| "Pitch the top 3 opportunities about X" | Stage 2 → cluster by topic → Stage 3 ONCE per cluster → Stage 4 looped with the same --research-page-url |
| "Ok next step" / "what now?" / "go" | RESTATE the stage we're on and PROPOSE the next one. Do not ask the user. |
Stage 1 — Onboarding (run ONCE per user)
1a. Install
If the user doesn't have the skill installed, pip-install the deps and run Playwright's browser bootstrap:
pip install -r requirements.txt
playwright install chromium
1b. Log in
python3 qwoted_login.py
Important — this script is idempotent. Before launching any browser
it probes ~/.qwoted/storage_state.json against Qwoted's API. If the
cookies still work (which they usually do — Qwoted sessions last weeks),
the script exits immediately with RESULT: {"status": "logged_in", ...}
and no browser opens at all. That's the happy path — do not re-run
with --reset or --force unless you have a reason.
Only if no valid session exists will a Chromium window open. In that
case: tell the user to sign in IN THAT Chromium WINDOW — not in
their regular Chrome/Safari, because those are separate browsers with
separate cookies, so signing in elsewhere will NOT save a session for
this skill. When they reach a logged-in page the script auto-detects
it, saves cookies to ~/.qwoted/storage_state.json, closes the browser
and exits. The next login is one click because Chromium remembers them.
If you are running inside an agent environment that can't show GUI windows (some Codex or CI setups), the Chromium window will launch invisibly and the script will hang on the sign-in page. In that case:
- STOP. Do not keep re-running. Tell the user to open their own
terminal on their own machine and run
python3 qwoted_login.pythere once — they'll see the browser, sign in, andstorage_state.jsonwill be written. After that, every future call from the agent will use the already-valid cookies (no browser needed).
If the user doesn't have a Qwoted account yet, send them to qwoted.com to sign up first (free for sources).
1c. Set up the expert profile (Source persona) — REQUIRED
Critical constraint: Qwoted only delivers a pitch to a reporter when the pitch is attached to a pitchable entity — a Source, Company or Product the user is allowed to pitch as. The pitch API will accept a submission without one (HTTP 200, draft=false) but the reporter is never notified. Always make sure the user has a Source persona configured before the first pitch.
Check first:
python3 qwoted_profile.py --action get
Parse the RESULT: JSON. Key fields you MUST inspect before doing
anything else:
seo_ready— boolean, all three must-have fields are set.ready_to_pitch— boolean, at least one pitchable entity exists.missing_for_seo— array of field names still empty.bio_preview— first 240 chars of the current bio (empty string if there really is no bio).bio_length— total bio length in chars.first_pitchable_entity— full object includingbio,business_url,email,has_linkedin_url, etc.
Decision tree:
- If
seo_ready == true→ skip to Stage 2. Do NOT "improve" the existing profile unless the user explicitly asked you to. Read the bio_preview so you know what the user's positioning is, then move on. - If
ready_to_pitch == truebutseo_ready == false→ the persona exists but is incomplete. For each item inmissing_for_seo, ask the user the corresponding question and patch with--action update:business_url→ "What URL should reporters link to when they credit you?" →--action update --url https://acme.combio→ draft a 2-4 sentence bio, SHOW it to the user, get explicit approval, then--action update --bio "...". (If a bio is actually present andmissing_for_seostill lists it, you probably have a stale local copy of the skill — update it.)email→ "Which email should reporters reply to?" →--action update --email jane@acme.comRun--action getagain afterwards to confirmseo_readyis now true.
- If
ready_to_pitch == false→ no persona exists. Go to the create flow below.
⚠️ Safety rail — overwrite protection. If the user asks you to
change an already-populated field (e.g. "update my bio to X"), the
script will REFUSE the PATCH unless you pass --force-overwrite. This
is intentional. The correct flow is:
- Run
--action getand readbio_preview. - Show the user BOTH the current bio AND the proposed new one.
- Ask explicitly: "Replace the current bio with this one?"
- Only if they say yes, re-run with
--force-overwrite.
Never pass --force-overwrite preemptively "just in case".
If ready_to_pitch == false, you need to collect the user's info
in a single message before running create. Don't drip-ask one field
at a time. Use this exact checklist:
REQUIRED to even create the profile:
--full-name— the user's real name as the byline should appearSTRONGLY RECOMMENDED — without these the profile is useless for SEO:
--url— the user's business website URL (e.g.https://acme.com). ⚠️ This is the link journalists put in their articles when they credit the user. No URL = no backlink = the whole point of this skill is defeated. Confirm it before running create.--bio— a 2-4 sentence description of who the user is, what they do, and what topics they can credibly speak to. The bio is what reporters skim when deciding whether to use a quote. Mention the business name, role, and 2-3 areas of expertise. If the user gave you their bio elsewhere, use that. If not, draft one from what they told you and show it to them for approval before submitting.NICE TO HAVE (ask but don't block on):
--location—"City, State, Country"(some pubs filter by region)--gender— one off/m/nb/sd(for pronouns in articles)--phone,
Always ask the user to confirm at minimum the URL, bio and email before firing the create command. Those three are what end up in articles. Example of how to ask:
"Before I set up your Qwoted expert profile, I need to confirm a few things that journalists will see and link to:
- Business URL — what site should reporters link to when they credit you? (e.g.
https://acme.com)- Bio — here's a draft based on what you told me: "Jane is the founder of Acme Inc, a B2B SaaS that helps marketing teams ship campaigns 10x faster. She speaks to growth, GTM and pricing." Sound right?
- Reply-to email — which email should reporters use to follow up?
Anything you'd like to add (LinkedIn, location, etc.)?"
Then build the command from their answers:
python3 qwoted_profile.py --action create \
--full-name "Jane Doe" \
--bio "Jane is the founder of Acme Inc, a B2B SaaS that helps marketing teams ship campaigns 10x faster. She advises on growth, GTM and pricing." \
--url https://acme.com \
--email jane@acme.com \
--linkedin https://www.linkedin.com/in/jane-doe/ \
--location "San Francisco, CA, USA" \
--gender f
Repeat any contact flag to add multiple values (the first one becomes
primary). Other flags: --off-the-record, --hide-from-search-engines.
To update an existing persona later (e.g. the user got a new title):
python3 qwoted_profile.py --action update \
--bio "Jane is now CEO of Acme Inc..."
--source-slug is optional — without it, the script edits the first
Source on the account.
Stage 2 — Find opportunities
python3 qwoted_search.py --query "marketing automation" --max-hits 30
Empty --query "" returns the full index in the order Qwoted shows it
on the homepage (newest first).
Read the resulting JSON file (path is in the RESULT: line under
out_path). The structure is:
{
"query": "marketing automation",
"scraped_at": "2026-04-22T...",
"count": 30,
"opportunities": [
{
"source_request_id": 235897, // numeric ID for qwoted_pitch.py
"name": "Looking for marketing experts to comment on Q3 trends",
"details": "Reporter brief, 3-4 paragraphs of what they're after...",
"request_type": "Online article",
"deadline": "2026-04-28T17:00:00Z",
"want_pitches": true,
"publication": {"name": "TechCrunch", "top_publication": true, ...},
"hashtags": ["#marketing", "#saas"],
"url": "https://app.qwoted.com/source_requests/235897",
...
},
...
]
}
Picking the best opportunities
When the user asks for "the best", "top", or "easy wins", rank by:
publication.top_publication == true(high-DR sites)easy_win == true(Qwoted's signal: low pitch count, high responsiveness)paid == true(paid placements when applicable)pitch_count_category(lower is better — "low" beats "very_high")- Match against the user's expertise (use the bio you already have)
- Deadline proximity (
deadline_approaching,deadline)
Only suggest opportunities where the user genuinely has expertise — journalists ignore obviously irrelevant pitches and Qwoted scores PR accounts on response rate.
⚠️ MANDATORY — classify EVERY opportunity with the Stage-3 heuristic
After you've picked a shortlist, you MUST run each one through the Stage-3 heuristic table at the top of this file and present the classification to the user in a table. Do not skip this step, do not wait for the user to ask about stats pages. A typical output looks like this:
I'd prioritise 5 of the 8 opportunities. Here's the Stage-3 classification:
SR ID Publication Topic Deadline Stats-page score Action 235897 TechCrunch marketing automation trends 3d +5 Build marketing-automation-statistics-2026.html, then pitch236422 Selling Signals awareness vs demand 20h +1 Pitch direct 234782 BCA Insider local SEO for retailers 5d +4 Build local-seo-statistics-2026.html, then pitch236145 SEOptimer how agencies make money 8h -3 Pitch direct (too tight, founder-story) 232532 Business RoundUp founder interview 8d -3 Skip ($95 fee, founder-story) Plan: I'll build the 2 stats pages first (they cluster future opportunities too), then draft direct pitches for #235897, #236422 and #234782 that link to them. I'll draft a plain pitch for #236145 because of the deadline. Sound good?
When the user replies "ok" / "yes" / "go", move on to Stage 3 for the pages you identified, then Stage 4 for the pitches.
Stage 3 — Build a sourced statistics page (the linkable asset)
READ
STATISTICS_PAGE_PLAYBOOK.mdBEFORE EXECUTING THIS STAGE. This section is just the when. The how (research methodology, source quality bar, HTML build, anti-hallucination rules) lives in the playbook.
Why it matters
A pitch that just contains opinion gets one quote in one article. A
pitch that links to a comprehensive, sourced statistics page on the
same topic gets the user recurring citations for months — because
the next reporter who searches "<topic> statistics 2026" finds the
page on the user's domain and cites it without ever knowing the user
exists. This is the move that turns Qwoted from a one-off PR tool
into a compounding SEO engine.
When to build a stats page (decision rule)
Offer to build one when all of these are true:
- The opportunity topic is broad enough to support a stats roundup (e.g. "AI in marketing trends" — yes; "founders who pivoted in 2024" — no, that's an anecdote ask).
- The deadline is at least 24-48 hours away (research takes time if you're going to do it well).
- The user doesn't already have a recent, high-quality stats page on the same topic.
Skip and go straight to Stage 4 when the deadline is tight or the ask is qualitative (anecdotes, opinions, case studies).
If multiple Stage 2 opportunities are on the same topic, build the
stats page once and reuse it across every pitch in that batch with
the same --research-page-url flag.
How to ask the user
Don't just unilaterally start building a 3000-word page. Ask:
"Two of the opportunities I found are about [topic]. I can build you a sourced statistics page on this — it's a one-time effort that typically earns you backlinks for months because journalists cite it organically. The page would have ~50-80 stats, 2 charts, and a couple of comparison tables. Takes me ~5-10 minutes to research and draft. Want me to do it before pitching, or just pitch directly?"
How to execute
- Read
STATISTICS_PAGE_PLAYBOOK.md— it has the full research methodology, source quality bar, and HTML build instructions. - Pull the user's bio info so the byline + author footer are
filled correctly:
Usepython3 qwoted_profile.py --action getfirst_pitchable_entity.name,.url,.company_name,.bio. - Research using the playbook's source priority + quality bar.
- Build the HTML by filling in the placeholders in
templates/statistics_page_example.html. - Save to
./statistics_pages/<slug>.html. - Tell the user how to preview (
open ./statistics_pages/<slug>.html) and ask them to publish on their own site (their CMS, their call). - Wait for the public URL — don't proceed to Stage 4 without it.
Stage 4 — Draft and send pitches
How to write a great pitch (this is on YOU, the AI)
Each pitch should be:
- 2-4 short paragraphs, max 250-400 words.
- First sentence says who the user is and why they're qualified for this specific request. (Don't recycle the bio — synthesize.)
- Body gives the reporter concrete, quotable insights directly answering the request. 2-4 bullet points work great. Numbers, specific examples and contrarian takes get used; vague platitudes get deleted.
- If you built a Stage 3 stats page for this topic, reference the URL in the second paragraph: "I just published a 50-stat roundup on [topic] at [URL] — happy to pull the most relevant ones for your angle." This is the move that gets the page cited. Quote 2-3 of the most striking stats inline so the reporter sees immediate value without clicking.
- Last sentence offers a credit format (e.g.
Credit me as Jane Doe, founder of Acme Inc (acme.com)) and an offer to expand or hop on a quick call. - No links in the pitch body OTHER than the stats page URL — keep the pitch focused. The stats page is the only link that earns a backlink.
- No corporate marketing speak. Talk like a smart founder emailing a friend at TechCrunch.
Save it to a tempfile so quoting is reliable:
cat > /tmp/qwoted_pitch.txt <<'EOF'
Hi! Borja Obeso here — founder of Distribb, a content distribution
and SEO platform that pushes one piece of content across 200+ DR40+
sites and channels. I see this national-vs-local split every day...
[2-4 paragraphs of substance]
Credit me as Borja Obeso, founder of Distribb (distribb.io).
— Borja
EOF
Step 1 — DRY-RUN first (always)
python3 qwoted_pitch.py \
--source-request-id 235897 \
--pitch-text-file /tmp/qwoted_pitch.txt
This creates a draft on Qwoted and autosaves the text. The reporter is NOT notified. Show the resulting draft to the user (or summarise it) and ask for approval.
Step 2 — Send it for real
python3 qwoted_pitch.py \
--source-request-id 235897 \
--pitch-text-file /tmp/qwoted_pitch.txt \
--research-page-url https://acme.com/blog/ai-marketing-statistics-2026 \
--send
The --research-page-url flag is optional but strongly recommended
when you built a Stage 3 stats page. It gets logged alongside the
pitch in ~/.qwoted/sent_pitches.json so you can later answer
questions like "which research pages drove the most pitches" or
"which page is the user citing in this PR push".
status: "sent" in the RESULT line means the reporter has been
notified. The pitch is also appended to ~/.qwoted/sent_pitches.json.
Duplicate guard
The script refuses to pitch a source-request that's already in
sent_pitches.json (returns status: "skipped_duplicate"). It also
detects pitches sent through the Qwoted UI (returns an error like
source_request_id=N already has a SENT pitch). To override locally,
pass --allow-duplicates (Qwoted itself only allows one pitch per
source-request, so the server-side block is hard).
Common error patterns and how to handle them
RESULT.error contains... |
Meaning | Action |
|---|---|---|
No Qwoted session found / session expired |
Cookies missing or expired | Run python3 qwoted_login.py |
Cannot --send: no pitchable Source/Company/Product |
User skipped Stage 1c | Run qwoted_profile.py --action create ... |
already has a SENT pitch |
This SR has been pitched already | Pick a different opportunity |
is stuck in a non-draft, non-delivered state |
Previous submit attempt without entities | Pick a different opportunity (Qwoted won't let us re-edit) |
validation errors: ['Bio is too short'] |
Form-level issue | Adjust the field and re-run |
Things you should NEVER do
- Never run
--sendwithout showing the pitch to the user first. A pitch is a real message to a real journalist — they remember spammers. - Never invent biographical details about the user. Ask if you don't know.
- Never invent statistics in a Stage 3 page. Every stat must have a
real URL you actually fetched. One fabricated stat caught by a
journalist torpedoes the user's reputation forever. See the
anti-hallucination rules in
STATISTICS_PAGE_PLAYBOOK.md. - Never pitch opportunities outside the user's expertise. Qwoted tracks reply rate and bad pitches hurt the account permanently.
- Never modify
~/.qwoted/sent_pitches.jsonby hand — the duplicate guard relies on it. Treat it as append-only state. - Never commit
~/.qwoted/to git. It contains the user's session cookies (storage_state.json) — full account access. - Never publish a Stage 3 stats page on Medium / LinkedIn / dev.to
before the user's own domain. The canonical version must live at
https://<theirsite>/...— that's where the backlinks need to go.
Quick reference — every command
# Setup
python3 qwoted_login.py # idempotent: skips browser if session is still valid
python3 qwoted_login.py --force # open browser even if session is valid (switch accounts)
python3 qwoted_login.py --reset # wipe saved profile + cookie jar, start fresh
python3 qwoted_login.py --headless # headless only works if an existing session is valid
# Profile
python3 qwoted_profile.py --action get # what entities exist?
python3 qwoted_profile.py --action create --full-name '...' --bio '...' --email '...'
python3 qwoted_profile.py --action update --bio '...' # edit first Source
# Search
python3 qwoted_search.py --query "marketing automation" --max-hits 30
python3 qwoted_search.py --query "" --max-hits 50 # all opportunities
# Build a stats page (Stage 3)
# 1. READ STATISTICS_PAGE_PLAYBOOK.md for the research methodology
# 2. Open templates/statistics_page_example.html
# 3. Fill in placeholders → save to ./statistics_pages/<slug>.html
# 4. open ./statistics_pages/<slug>.html # preview
# 5. User publishes on their site → returns public URL
# Pitch
python3 qwoted_pitch.py --source-request-id 235897 --pitch-text-file /tmp/p.txt
python3 qwoted_pitch.py --source-request-id 235897 --pitch-text-file /tmp/p.txt --send
python3 qwoted_pitch.py --source-request-id 235897 --pitch-text-file /tmp/p.txt \
--research-page-url https://acme.com/blog/x-stats-2026 --send # with stats page
python3 qwoted_pitch.py --opportunity-id de1ccdba --pitch-text-file /tmp/p.txt --send # short URL form
State directory: ~/.qwoted/ (override with QWOTED_HOME env var).