* feat(rank-tracking): raise scheduler budget to 2000 units/tick
The 200-unit budget used ~2-6% of DataForSEO's 2,000 req/min account
cap and would take days to drain the post-#462 backlog. Scheduled checks
run through the task queue (1 task_post per 100 units + free task_get
polls), so a full 2,000-unit tick peaks around 1,200 req/min — still
leaving headroom for the other DataForSEO products on the account.
Also raise the due-config fetch limit to 500 so skip-heavy stretches
(free orgs, keywordless configs) drain more than 200 rows per tick.
* fix(rank-tracking): retune budget to 1000 with accurate sizing and a tick deadline
Review corrections to the 10x bump: task_get polling is one call per unit
per round and rounds wake synchronized per tick, with up to three ~15-min
poll windows overlapping the */5 cron — 2000 units/tick could saturate
DataForSEO's 2000 req/min cap, silently aging throttled polls into the
~3x-cost live fallback billed to customers. 1000/tick keeps real headroom
and is still ~45x steady-state demand.
Add a 3-minute wall-clock deadline to the per-config loop (stoppedByDeadline
in the tick summary): a skip-heavy 500-candidate tick pays serial Autumn
round-trips per distinct org and could otherwise run into the 15-minute cron
kill. Name the fetch limit (DUE_CONFIGS_PER_TICK) and correct its comment.
Test fixtures now derive from MAX_KEYWORDS_PER_CONFIG instead of asserting
an unreachable 1500-keyword config.
* fix(cron): run the audit watchdog before the rank loop
reconcileStaleAudits ran after runScheduledRankChecks in the same
invocation, so a slow rank tick would delay the watchdog and a wall-clock
kill would skip it entirely.
* fix(cron): preserve watchdog failure signal; codex review polish
Rethrow a caught reconcileStaleAudits error after the rank loop so the
invocation still reports failed (matching pre-reorder semantics), use an
inclusive deadline comparison, and note overlapping-tick poll residue in
the sizing comment.
* fix(mcp): stop breaking public OAuth clients at token refresh
The DCR shim force-upgraded every public client (token_endpoint_auth_method
"none" or omitted) to client_secret_post so Perplexity would accept the
registration response. That made the stored client confidential, so the
token endpoint demanded client authentication on every grant — and MCP
clients that discard the secret (Codex) lost their session at first token
expiry with "invalid_client: missing client_secret".
Register those clients as true public clients instead (PKCE + the
provider's grant-to-client binding secure that flow), and satisfy
Perplexity by decorating only the registration response with a placeholder
client_secret and client_secret_post. The provider skips secret validation
for public clients, so clients that send the placeholder and clients that
never store it both keep working, including on refresh.
* refactor(mcp): only rebuild DCR requests that actually change
* fix(mcp): satisfy type-aware lint in DCR shims
oxlint --type-aware rejected the Record<string, unknown> assertions used to
read untrusted DCR payloads. Parse both with loose Zod schemas instead, per
the repo's trust-boundary convention, which also replaces the hand-rolled
object guards.
* feat(skills): add beginner-seo-audit agent skill
A portable agent skill that audits a domain with OpenSEO and delivers a
one-page, plain-language HTML report for a non-SEO owner, centered on a
single do-this-week action. Ships with a frozen report template and a
docs page under Agent Skills.
* feat(skills): add starting-focus keyword step; cross-link from coach and project setup
- beginner-seo-audit now proposes a 'Where to focus first' area on healthy
sites: one research_keywords call, one theme, 3-5 winnable keywords with
what to make for each. Skipped entirely when the site is down.
- seo-coach and seo-project-setup now recommend beginner-seo-audit as the
first workflow for a beginner with an existing site.
- Docs pages updated to match.
* refactor(skills): rename beginner-seo-audit to seo-audit
Audience-neutral name; the plain-language, one-action framing moves to
the description. Docs slug, meta.json, and coach/project-setup
cross-references updated to match.
A user's #1 organic result showed as position 3-4 because rank_absolute
counts SERP features (local pack, PAA, AI overviews) as positions. Rank
tracker, keyword-research SERP list, and onboarding market snapshot now
prefer rank_group; MCP get-serp-results keeps rank_absolute since it
returns all SERP item types.