Take action
Autopilot
Let Selfwin write and publish one article a day for the prompt where AI answers miss you, then measure whether the prompt moved.
Autopilot closes the loop between tracking and content. Every morning, after the night's answers are collected, it:
- Measures every article it already published: for that article's target prompt, how many answers named you, your best position, and whether the engines cited the article itself.
- Picks a target: the active prompt where you are most absent, preferring "best", "top", and "alternatives" prompts, because roundups are what answer engines cite for tool questions. A targeted prompt rests for 30 days.
- Chooses a keyword: with an OpenSEO key, the highest-volume winnable keyword among the prompt and its fanouts; otherwise the search the engines themselves ran while answering.
- Writes a listicle with the seo2 pipeline: research from the pages the engines cite, an outline, the article, an FAQ from real fanout searches, and a lint pass.
- Publishes to your connected blog, or holds the article for your approval.
Modes
- Hold for approval (default): the article waits on the Autopilot page. Review it and press Publish.
- Publish automatically: the article goes live as soon as it passes the gates below.
Even in automatic mode, an article is held for review when the linter fails, or when it would name a product that neither you track nor the engines mentioned for that prompt. Links that are not your own domain or one of the research sources are turned into plain text before publishing, so a model can never invent a URL on your site.
Destinations
| Destination | Covers | You need |
|---|---|---|
| WordPress | WordPress.org, WordPress.com Business, WP Engine, Kinsta | Site URL, username, an application password |
| Ghost | Ghost(Pro), self-hosted Ghost | Site URL, a custom integration Admin API key |
| Webflow | Webflow CMS | Collection ID and a site API token with CMS write |
| Shopify | Store blogs | Store domain, blog ID, an Admin API token with write_content |
| GitHub | Next.js, Astro, Hugo, Jekyll, Eleventy, Docusaurus, Lovable via GitHub sync | Repository, posts folder, a fine-grained token with Contents write |
| Notion | Super, Feather, Potion, Notion Sites | Database ID and an integration secret, with the database shared to it |
| Webhook | Lovable and Supabase, Next.js, Framer or Wix through Zapier or Make, any headless CMS | An HTTPS endpoint and a signing secret |
Selfwin tests every destination when you add it and records the result. Credentials are encrypted at rest and never shown again. Destinations must be public HTTPS addresses.
The webhook payload
Selfwin sends POST with Content-Type: application/json and a signature header:
X-Selfwin-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>
{
"event": "article.published",
"article": {
"title": "Best AI Visibility Tools for Startups in 2026",
"slug": "best-ai-visibility-tools-for-startups-2026",
"description": "…",
"markdown": "…",
"html": "…",
"tags": ["AI visibility"],
"faq": [{ "q": "…", "a": "…" }],
"publishedAt": "2026-10-28T09:00:00.000Z"
}
}
Reject requests whose signature does not match or whose timestamp is more than five minutes old. Answer { "url": "https://yoursite.com/blog/<slug>" } so Selfwin can link to the post and measure its citations. A ping event is sent when you test the destination; answer it with any 2xx.
Lovable (Supabase)
Lovable sites keep their data in Supabase, so the simplest route is an edge function that stores each article in a posts table, and a blog page in your Lovable app that reads it.
- In Lovable, ask: "Add a blog that lists rows from a Supabase
poststable (slug, title, description, content_html, published_at) at /blog and renders one post at /blog/:slug." - Create the function
supabase/functions/selfwin-publish/index.ts:
import { createClient } from "npm:@supabase/supabase-js@2";
const SECRET = Deno.env.get("SELFWIN_WEBHOOK_SECRET")!;
const SITE = "https://yoursite.com";
async function verify(header: string | null, body: string): Promise<boolean> {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}.${body}`)));
const expected = [...mac].map((byte) => byte.toString(16).padStart(2, "0")).join("");
let diff = expected.length ^ String(parts.v1 ?? "").length;
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ String(parts.v1).charCodeAt(i);
return diff === 0;
}
Deno.serve(async (req) => {
const body = await req.text();
if (!(await verify(req.headers.get("x-selfwin-signature"), body))) return new Response("Unauthorized", { status: 401 });
const event = JSON.parse(body);
if (event.event === "ping") return Response.json({ ok: true });
const { article } = event;
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
const { error } = await supabase.from("posts").upsert(
{
slug: article.slug,
title: article.title,
description: article.description,
content_html: article.html,
published_at: article.publishedAt,
},
{ onConflict: "slug" },
);
if (error) return Response.json({ error: error.message }, { status: 500 });
return Response.json({ url: `${SITE}/blog/${article.slug}` });
});
- Set the secret and deploy without JWT verification (Selfwin signs instead):
supabase secrets set SELFWIN_WEBHOOK_SECRET=<the same secret you give Selfwin>
supabase functions deploy selfwin-publish --no-verify-jwt
- In Selfwin, add a Webhook destination with
https://<project-ref>.supabase.co/functions/v1/selfwin-publishand the secret.
If your Lovable project syncs to GitHub and renders Markdown files, the GitHub destination works too.
Next.js
A Markdown blog in your repo (content/blog/*.md or MDX): use the GitHub destination. Each article is committed with title, description, date, slug, and tags frontmatter, and Vercel or Netlify redeploys on the push.
A blog backed by a database or CMS: add a route handler and use the Webhook destination.
// app/api/selfwin/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidatePath } from "next/cache";
function verify(secret: string, header: string | null, body: string) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
const given = Buffer.from(String(parts.v1 ?? ""), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
export async function POST(request: Request) {
const body = await request.text();
if (!verify(process.env.SELFWIN_WEBHOOK_SECRET!, request.headers.get("x-selfwin-signature"), body)) {
return new Response("Unauthorized", { status: 401 });
}
const event = JSON.parse(body);
if (event.event === "ping") return Response.json({ ok: true });
const { article } = event;
// Save article.slug, article.title, article.description, article.html to your CMS or database here.
revalidatePath("/blog");
return Response.json({ url: `https://yoursite.com/blog/${article.slug}` });
}
Measuring results
Results appear on the Autopilot page after the next collection. "Since publishing" compares how many of the prompt's answers named you on the day the article was written with the latest measured day, shows your best position, and notes when the article itself was cited. Treat movement as correlation: many things change AI answers, and one article is rarely the only cause.