Install-time opportunity audit

Date: 2026-08-15

Origin: Christian's idea, 2026-08-13. Idea 11 in the free-tier list.

Pillar: installs (net), then trials

Status: Draft. Needs approval. Blocked on a dependency, see below.

The problem, measured

FactSource
29 of 58 deactivations happen inside 24 hoursplugin_installs.first_seen vs deactivated_reported_at
17 of those 29 never started a trialjoined against site_trials
22 of those 29 already ran Rank Math, Yoast or AIOSEOdeactivation_metadata.active_plugins
A fresh install renders as lockeddashboard.php maps no_trial_no_keys to 'locked'
Generation throws without a keyai-requests.php:412
Installs run 1 to 2 per day, deactivations matchplugin_installs, 9 weeks

A keyless user opens the plugin, sees a trial CTA and nothing else, and half of

them delete it the same day.

The idea

Run a real DataForSEO opportunity audit on the domain the moment the plugin

phones home. When the user opens the plugin, the answer is already there. The

first screen is not a CTA. It is their own ranking data.

DEPENDENCY: this is mostly already built, in review

**christpet/wp-autopost-premium#83 (CP-162: Seed the rank tracker from the

site's existing rankings) and christpet/wp-autopost#80 (CP-162: Link each

tracked keyword to the page that ranks)** are both OPEN as of 2026-08-15.

PR #83 (rank tracker seeding) already delivers:

PieceDetail
ranked_keywords calldataForSEOProvider.ts, +88 lines
Selection rule8 keywords, half wins and half opportunities, positions past 20 dropped
Seeding servicerankSeedService.ts, 256 lines
Admin triggerPOST /api/admin/seed-rank-tracker, supports {"domain":...} and {"all":true,"dryRun":true}
Tests10 new, 236/236 backend tests pass

It also answers the cost question I could not. Verified against the live

API, not the docs:

DomainKeywords foundBest positionCost
extrahelpmovers.com973$0.024
googledrivedownloader.com48414$0.018

About $0.02 per domain. At 1.5 installs/day that is roughly **$0.90 a

month**. Cost is not a reason to say no.

Do not duplicate any of this. This workplan starts after PR #83 merges.

What is actually left to build

PR #83 seeds at trial setup (automatedSetupService.ts). This idea needs it

at install, before any trial exists. Three gaps.

Gap 1. DECIDED: option B. Split the service, store keyless results separately.

RankSeedService.seedFromExistingRankings(siteId, domain) takes a siteId and

writes site_keywords keyed on site_id. A keyless install has no sites row.

I measured the three options rather than pick by preference.

Why not A (create a sites row at install). I first thought the risk was

that many queries treat sites as "a customer". Only 4 of 62 reads run

without a row filter, and none of them is dangerous: a health check

(healthController.ts:41), a cron connectivity check

(trigger-wp-cron.ts:39), an upsert (siteContextService.ts:706), and

internal shadow-eval sampling capped at 50 (shadowEvalService.ts:1193).

So A is safer than I claimed. What kills it is the table's meaning:

TableRows
sites103
of those, with a trial95
of those, without a trial8, all test artefacts (raw IPs, mailbridge.app)
site_trials112
plugin_installs251

sites currently means "a site that started a trial". Adding roughly 45

keyless installs a month inverts that within two months, and

shadowEvalService would begin sampling non-customers.

Why not C (a JSON blob on plugin_installs). Cheapest to write and useless

afterwards. We will want to ask which keywords we showed against who converted,

and a blob cannot answer that.

Why B. Extract the fetch and the selection out of rankSeedService (they are

effectively pure) and store keyless results in a new install_audits table,

unique on domain. Insert before the DataForSEO call so a repeated ping cannot

double-spend.

The one objection to B was that an audit user who later starts a trial would

need their data migrated into site_keywords. That objection is void. PR #83

(CP-162 rank tracker seeding) already seeds at trial setup through

automatedSetupService.ts, so the trial re-seeds from scratch. Nothing needs to

move. The audit is a pre-trial display artefact and nothing more.

Gap 1b. Disclose DataForSEO in the readme. Approved by Christian 2026-08-15.

Required whatever else we ship. Add to == Third Party Services ==, which today

lists OpenAI, Anthropic, Google AI, ClearPost Premium and ClearPost Analytics,

and omits DataForSEO entirely.

Proposed text, matching the existing entries:

**SEO Data Provider (Used by the Premium Add-On and the Free Site Audit)**

* **DataForSEO** - Supplies search volume, keyword difficulty and ranking data
  - Service: https://dataforseo.com/
  - Your site's domain is sent to DataForSEO to retrieve keyword opportunities
    and current search rankings for that domain. No site content, no personal
    information and no API keys are transmitted.
  - This runs only when you request an audit or use premium SEO features
  - Privacy Policy: https://dataforseo.com/privacy-policy
  - Terms of Service: https://dataforseo.com/terms-and-conditions

Two points that must stay true, or the text becomes a lie:

bulk_keyword_difficulty, which all take a domain. Check any future endpoint

against this sentence before adding it.

This ships in the same plugin release as the audit, not after it.

Gap 2. The trigger

The install ping already reaches us. saiap_schedule_context_updates

(site-context.php:1115) hooks wp_loaded and schedules the daily cron with

time(). updateSiteContext calls trackInstallActivity(domain) before

validatePremiumAccess, so we hold the domain even when the request 403s.

But wp-cron only fires when somebody loads a page. On a new site with no

traffic that first load is the admin opening wp-admin, which is the same moment

they open the plugin. The audit will not be ready on first open.

Fix, and both halves are needed:

Gap 3. Presentation

Replace the locked-state widget (dashboard-widget.php:65,

saiap_widget_no_trial_no_keys) with the audit. Plugin PR #80 already links a

tracked keyword to the page that ranks for it, so that part is done.

Reach: 9 in 10 installs

Measured across all 251 install rows:

Domain typeCountShare
Real public domains22590%
Local, IP, .test, .local146%
Staging or auto-subdomain125%

172.18.0.1:18090, findadoctor.test and wordpress.local return nothing from

DataForSEO. Detect them and skip the call rather than pay for an empty result.

What the audit shows. Corrected by Christian, 2026-08-15.

I had this backwards. I built the plan around ranked_keywords, which shows

where the site stands today. Christian's point: the goal is to show their

top keyword opportunities and the traffic attached to them, so the user sees

the ROI of ClearPost, tailored to their site, before they click anything.

That inversion matters, and it dissolves the open question I raised. A brand

new site with no rankings still has opportunities. Opportunities exist for every

site. Rankings do not.

Opportunities are the lead. Rankings are a secondary layer.

LayerSourceStatus
What they could target, with volumekeywords_for_site, dataForSEOProvider.ts:386Already built. Returns up to 100 keywords with search_volume, cpc, competition, competition_index
How hard each one isbulk_keyword_difficulty/livePR #83 (CP-162 rank tracker seeding) adds it
Where they rank today, if at allranked_keywords/livePR #83 (CP-162 rank tracker seeding) adds it

keywords_for_site returns keyword_difficulty: null (dataForSEOProvider.ts:398),

which is exactly the gap bulk_keyword_difficulty fills. The two endpoints

compose.

The ROI number, and why this version is honest

keywords_for_site already returns cpc. Volume multiplied by CPC gives the

cost of buying that traffic on Google Ads. That is a real, checkable, tailored

number, and it is ROI without any forecast of our own performance:

These 14 keywords carry 3,400 searches a month. You rank for none of them.
Buying that traffic on Google Ads costs about $2,100 a month at current CPC.
ClearPost publishes 30 posts a month targeting them, for $99.

Every figure comes from DataForSEO or our own price list. Nothing is predicted.

I withdraw my earlier objection. I argued against "we think ClearPost can

drive X traffic in 30 days" because a traffic forecast needs a CTR curve we would

have to defend. The CPC framing gets the same persuasive force from data we

already fetch, so the disagreement disappears.

Opt-in: required. This is a compliance question, not a taste question.

Christian asked me to research whether this needs an opt-in button. It does.

Three findings, all from our own files.

1. DataForSEO is not disclosed anywhere

The readme has a == Third Party Services == section. It lists OpenAI,

Anthropic, Google AI, ClearPost Premium and ClearPost Analytics.

DataForSEO does not appear in it. The data path is plugin, to our server, to

DataForSEO, so the user's domain reaches a third party we never named.

This must be fixed regardless of the opt-in decision. WP.org requires every

external service to be disclosed. Add DataForSEO to that section before any of

this ships.

2. Our own readme calls the premium service "Fully Optional"

The exact heading is Premium Add-On Service (Fully Optional). An audit that

fires automatically on activation, and reaches our server, contradicts the words

we published.

3. The plugin already has a consent mechanism, and it is stricter than this

saiap_track_event returns immediately unless saiap_tracking_consent === 'yes'

(analytics.php:39). We already gate anonymous analytics behind explicit

consent. Sending the user's domain to a paid third-party SEO API is a larger

step than the thing we already gate.

Recommendation

An explicit button. Not a checkbox buried in settings, and not automatic.

The button is not a cost. It is the offer:

Which keywords can your site actually rank for?
We check your domain against live search data and show you the keywords worth
targeting, how hard each one is, and what that traffic costs to buy on Google
Ads today.
[ Show me my keywords ]
Takes about a minute. No account or API key needed.

The headline is the question they already have. The friction line moves to the

bottom in small text, where it removes an objection instead of leading with one.

That converts a compliance requirement into the strongest CTA on the page. It

also answers "should we spend money on a domain whose owner agreed to nothing",

which was my other open question. The click is the agreement.

How the user knows it is running, and when it is done

The machinery already exists

setup-stream.php and assets/js/setup-stream.js already render a **live

narration view driven by an SSE stream**, with a short-lived token flow

(saiap_ajax_setup_stream_token, 5 minutes, via /api/trial/setup/token). It

is what trial setup already uses instead of a bare spinner.

Gap: saiap_render_setup_stream_mount() returns early when there is no

licence key (setup-stream.php:114), and saiap_enqueue_setup_stream only

loads the bundle when a trial or licence is present. A keyless installer has

neither, so the stream must be given a keyless token path.

The three states

"Reading your site. Found 43 pages." → "Checking 1,200 keywords against live

search data." → "Scoring 14 by volume and difficulty." A minute of honest

narration is more convincing than an instant result.

notice, so they see it from any admin page, not only ours:

"Your ClearPost audit is ready. 14 keywords, 3,400 searches a month."

If they close the tab

The audit finishes server-side regardless. On the next admin page load the

notice is waiting. saiap_admin_notices (analytics.php:84) already exists as

the pattern to follow.

Email is a later step, not part of this. A keyless user has given us no

email, and taking admin_email without asking repeats the mistake from the

auto-trial design.

Plugin changes: three CTAs already exist and all point at the wrong thing

A keyless user is shown "Start Free Trial" in three places today. None of

them needs to be created. Each needs its target changed.

#SurfaceWhereToday
1The plugin pagedashboard-widget.php:65, saiap_widget_no_trial_no_keys()"Start Generating Content on Autopilot" + Start Free 7-Day Trial
2The WordPress dashboard homedashboard-widget.php:31, wp_dashboard_setup renders the same widgetthe same CTA
3The Plugins list rowsimple-ai-auto-post.php:318, saiap_plugin_action_linksa "Start Free Trial" link

Surface 3 is the one that matters most and I nearly missed it. The Plugins

list is the page a person opens to deactivate us. Half of our installs die

within 24 hours, and that is the screen where it happens. A link reading "Run my

free audit" sits inches from the Deactivate link.

Surface 2 is second. The WordPress dashboard home is the first screen after

login. It reaches people who never open our plugin at all.

The change

Write the offer once and call it from all three. The scenario switch already

exists in each file, so this is a substitution rather than new plumbing.

posts" pointing at the trial

Do not add a sixth admin notice

The plugin already hooks admin_notices five times. A sixth competes with the

other five and reads as nagging. The single completion notice specified earlier

replaces one of the existing keyless notices rather than adding to them.

Do not touch the admin bar

admin_bar_menu is currently unhooked. Keep it that way. It appears on the

front end as well as in wp-admin, and it is not worth the intrusion.

Readme changes

Four edits, plus the DataForSEO disclosure already specified in gap 1b.

1. = Free Version Features (Free Forever) =

Add the audit as the first item. It is now the only feature that needs neither a

key nor a trial, so it belongs at the top.

* **Free SEO opportunity audit** - find out which keywords your site can
  realistically rank for, how much traffic each one carries, and what that
  traffic would cost you on Google Ads. Runs on your domain in about a minute,
  with no account and no API key.

2. == Description ==, the opening paragraph

The first line currently sells autonomous publishing, which needs a key. Add one

sentence naming the question a visitor gets answered immediately:

Not sure what to write about? ClearPost checks your domain against live search
data and shows you which keywords you can realistically rank for, before you
configure anything.

2b. The short description, 150 characters

This is the line that shows in every WP.org search result, so it is the highest

leverage copy we own. Candidate:

Find out which keywords your WordPress site can rank for, free. Then let AI
write and publish the posts that target them.

That is 128 characters. It leads with the answer and keeps the autoblogging

promise behind it.

Do not ship this in the same release as anything else. Per the 2026-08-13

sweep, exp-1 and exp-3 shipped the same day and neither could ever be

attributed.

3. FAQ: two answers are now wrong

API keys". That is no longer the whole truth once the audit ships.

one. After this ships, the audit does not.

Both must change in the same release, or the readme is inaccurate.

4. Changelog

A normal entry. Name DataForSEO in it, since that is where a reviewer looks.

This readme edit is also an acquisition lever

Per the 2026-08-07 and 2026-08-13 keyword sweeps, WP.org rank tracks the plugin

NAME and almost nothing else, and tags do nothing. But the description and the

short description drive whether a listing view becomes an install, which is a

separate step from rank.

Lead with the question, not the absence of friction. "No API key needed"

describes what the audit does not cost. "Which keywords can your site actually

rank for" is the question a WordPress owner already has, and it is the thing

only we answer for their specific domain before they commit anything.

The friction line still earns its place, but as a supporting clause. It removes

the objection that 22 of our 29 day-one quitters plainly had, and it should not

be the headline. Worth measuring as its own listing change,

though the measurement rules from the 2026-08-13 sweep apply: at 1.97

installs/day the minimum detectable effect is 75% over 7 days and 31% over 42,

so it needs a long window and one change at a time.

Phases

Add install_audits, unique on domain. Insert before the call so a repeat

ping cannot double-spend.

Measurement

Window: 6 weeks, ending 2026-09-26. At 1.5 installs/day the minimum

detectable effect is 75% over 7 days and 31% over 42. Judging this early is how

exp-1 (short description) was called positive and exp-4 (tag retarget) negative,

both wrongly, one of them by me.

Open questions I cannot answer alone

opportunities from keywords_for_site, so the audit is never empty. Worth one

live test against a brand new domain to confirm the endpoint returns something

useful when the site has almost no content.

never spend on a domain whose owner has not asked us to.