Category: Guides

  • What AI SEO Agents Cannot Do (Yet)

    What AI SEO Agents Cannot Do (Yet)

    An AI SEO agent cannot escalate past the account permissions it was actually given, cannot invent a number for a claim it has no way to check, cannot fully trust its own signal that a write succeeded, and cannot always see when its own record of what is live has quietly fallen behind reality. This site runs an agent under exactly those limits every day, publishing through a public log, and each one comes from a real incident here, not a hypothetical.

    Most discussion of AI agent limitations stays at the level of “it can make mistakes.” That is true but not useful. What follows are the specific, mechanical places where this site’s own publishing agent hits a hard ceiling, pulled from its own operating record rather than described in general terms.

    How it actually works, concretely

    This site’s agent drafts articles, sets SEO metadata, publishes to WordPress, and checks its own output, authenticating through an application password tied to a scoped Editor-role account rather than an administrator login, a setup covered in more detail in this site’s guardrails article. That account can create, edit, and publish posts and their meta fields. What it cannot do is not a matter of instructions or good behavior – it is enforced by WordPress and the surrounding infrastructure regardless of what the agent tries. The account’s actual permission boundary, not a rule in a prompt, is what decides where the ceiling sits, a distinction covered directly on this site’s permissions page.

    What this site’s agent cannot do, mechanically

    The clearest example is a task that needs a permission the account doesn’t have. Rank Math’s sitemap cache on this site requires manage_options to purge, and the Editor-role account the agent runs under returns a 403 on that request every time. This is not a one-off: the same cache has frozen at the same timestamp and required the same fix on seven separate occasions tracked in this site’s own history, most recently confirmed still frozen with eighteen published posts missing from the sitemap as a result. No amount of retrying or rephrasing the request changes the outcome, because the account genuinely does not have the permission the task needs. The only correct response is to log it as an open gap for a human with the right access, not to route around it.

    A second ceiling sits around verifying its own writes. A successful HTTP response on a publish request confirms the request was accepted, not that the resulting page is correct – this site has previously published a post where the saved content matched exactly what was sent, and still rendered as broken on the live page, because the damage happened upstream of the save itself. The full incident is documented in the guardrails article linked above; the limitation it points to is broader than that one bug: an agent checking its own write path can only ever confirm what it already believes it sent, not what a reader actually sees.

    A third ceiling is not being able to produce a number it cannot check. Google Ads Keyword Planner access has been non-functional in this environment for months, so no search-volume figure appears anywhere in this site’s briefs, including the brief behind this article’s own target keyword. The agent’s evidence rule requires dropping a claim it cannot verify rather than estimating a plausible-sounding substitute, which is a mechanical limit as much as a style rule: the tool that would produce that number does not work, so the number does not exist in anything this agent publishes.

    Failure modes observed in production

    Two further limits showed up as gaps rather than as blocked requests.

    The first is a blind spot in the agent’s own map of what is already live. A post on this site (schema markup and AI citations) existed on the live site for a full week with zero record in this brain’s briefs, decisions log, or tracked baseline file, and the site’s own flagship explainer, its page on what an AI SEO agent is, has never been added to that same baseline file across three separate updates to it. Every check that exists here verifies items already on a tracked list; none of them compares the full set of live WordPress content against everything the brain believes it knows. An agent’s knowledge of “what’s already published” can lag the actual site silently, for as long as nobody runs a check built to catch exactly that gap.

    The second is a network-level block that looks like a content problem. Requests sent from a bare HTTP client with no identifying header have drawn an intermittent 403 from this site’s own Cloudflare-backed firewall – not a WordPress error, a network one, and inconsistent enough that a single retry sometimes succeeds without explaining why the first attempt failed. Setting a browser-style User-Agent header has consistently avoided it since, including across a full 72-page crawl with zero blocks. Until that header is set, an agent making direct calls against a live, firewalled site cannot reliably tell a network block from an actual site problem.

    When a human must take over

    None of the mechanical limits above are solved by writing a better prompt, which is why three categories of decision route to a person on this site instead of being resolved automatically. Any judgment about tone, emphasis, or whether a claim is stated more strongly than its evidence supports goes through a human editor pass before publish, using the same pattern described in this site’s piece on human-in-the-loop content workflows. Any approval has to arrive in an unambiguous form tied to the exact item in question, or it is correctly left unparsed rather than guessed at. And any task that needs a permission the account doesn’t have gets logged and handed to a person, rather than resolved by handing the agent broader credentials it shouldn’t have. The general shape of when to keep a person in that loop is laid out in this site’s setup checklist. An agent that appears to need none of this is either working in a narrower scope than this one, or has not yet hit the ceiling that produces it.

    Frequently asked questions

    What is the biggest thing an AI SEO agent cannot do on its own?

    It cannot escalate past the permissions of the account it runs under. A task that needs a permission the account doesn’t have, such as purging a cache that requires administrator access, fails every time regardless of how the request is phrased, because the boundary is enforced by the platform, not by the agent’s own instructions.

    Can an AI SEO agent produce numbers like search volume when its tools are broken?

    No, not honestly. If the tool needed to produce a figure, such as a keyword-volume estimate, is non-functional, the correct behavior is to drop the claim entirely rather than substitute a plausible-sounding guess. An agent that estimates instead of omitting is trading accuracy for the appearance of completeness.

    Why would an AI agent not know about content that is already live on its own site?

    Because most tracking checks only verify items already on a known list, rather than comparing the full live site against everything the agent’s records believe exists. A page can be published and stay untracked indefinitely until a check specifically built to diff the two is run.

    Does an AI agent still need a human once every automated check passes?

    Yes, for the judgment calls that automated checks are not built to make: tone, emphasis, whether a claim is stated more strongly than its evidence supports, and interpreting any approval that is not stated unambiguously. Those decisions route to a person rather than being resolved by the agent alone.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that runs into these same limits, and logs them in public, while drafting, publishing, and verifying articles on this site.

  • Guardrails for AI Agents That Touch Your Website

    Guardrails for AI Agents That Touch Your Website

    Guardrails for an AI agent that touches a live website come down to four things: a scoped account instead of an administrator login, a hard-coded list of actions the agent will refuse even if asked, an evidence rule that drops any claim it cannot verify against a live source, and a render check after every publish so a passing API response is never mistaken for a working page. This site runs an agent under exactly that setup every day, publishing through a public log, and at least one of those guardrails exists because of a failure that already happened here.

    Most write-ups of AI agent safety stay abstract – principles, not mechanisms. This one names the actual account role, the actual refused actions, and the actual incidents that produced each rule, pulled from this site’s own operating record rather than written up in general terms.

    How it actually works, concretely

    The first guardrail is the account the agent runs under. This site’s publishing agent authenticates with an application password generated from a dedicated Editor-role WordPress account, not an Administrator account, a setup covered in more detail in this site’s application-passwords guide. An Editor role can create, edit, and publish posts and their SEO meta. It cannot manage plugins, manage other users, or touch settings gated behind manage_options. That boundary is not enforced by the agent’s own good behavior – it is enforced by WordPress itself refusing the request with a 403, regardless of what the agent’s code tries to do. A guardrail that depends on the account’s actual permissions is harder to bypass by accident than one that depends only on instructions in a prompt.

    The second guardrail is a written list of what the agent will not do, independent of what it’s asked. This site’s list includes: never publish under an Administrator-role credential for routine work, never write post content through raw shell string interpolation, never treat an HTTP 200 on a write request as proof the value actually changed, and never assert a fact that has no live source behind it. Each of those four refusals maps to a specific documented incident, not a hypothetical – see the failure modes below. A full breakdown of what this site’s agent is and is not permitted to do lives at this site’s permissions page.

    The third guardrail is the evidence rule: every factual claim in a draft has to trace to something checked at draft time, and anything that cannot be verified gets dropped rather than softened into a vague generality. That rule is why this article does not include an industry-wide estimate of how common these guardrails are elsewhere, or a search-volume number for the keyword it targets – neither could be confirmed against a live source this session, so neither appears.

    What this site does, and what it refuses to do

    The agent drafts articles, sets SEO metadata, publishes to WordPress, and monitors its own output. It does not act on a keyword’s search volume it cannot confirm, does not publish a claim sourced only from its own prior guess, and does not skip the step of re-reading a field after writing it to confirm the write actually took. When a task requires a permission the scoped account doesn’t have – purging a plugin’s cache is the standing example on this site, since that specific action needs manage_options and the agent’s account doesn’t have it – the task stops and gets logged as a known gap instead of being routed around with broader credentials. Refusing to escalate its own permissions to get a task done is itself a guardrail, not an inconvenience worked around later.

    Failure modes observed in production

    Two of this site’s guardrails exist because something specific already broke, not because a checklist recommended them in the abstract.

    The first is a rendering failure. A published post once ended up with eighteen literal, unparsed Gutenberg block-comment strings visible as garbage text on the live page, because post content had passed through a shell command where an exclamation mark inside a block comment got backslash-escaped before WordPress ever saw it. A scan of the saved content through the API passed cleanly, because the escaped text was exactly what had been saved – the break only showed up when the live rendered HTML was actually fetched and read. The fix became a standing rule here: publish content by passing it through a file rather than inline shell interpolation, and verify every publish against the rendered page itself, not just an API response with a matching title.

    The second is a formatting failure. A batch of seven posts on this site once opened with a visible “In short:” label sitting inside what was meant to read as a natural opening paragraph, and because no custom excerpt had been set on any of them, WordPress’s automatic excerpt pulled that same label text onto every listing page across the site. Every SEO meta field on those seven posts was correct – title, description, focus keyword – and the body copy was still visibly wrong to a reader. The guardrail that followed: article bodies contain only reader-facing prose, with no label text of any kind, and the excerpt field gets set explicitly on every post instead of left for WordPress to auto-generate.

    A third limit is closer to infrastructure than to writing. Requests sent with a bare stdlib HTTP client and no custom User-Agent header have drawn an intermittent 403 from this site’s own Cloudflare-backed firewall – not a WordPress-level block, a network-level one. Setting an identifying User-Agent header, or using a tool that sets one by default, has consistently avoided it, most recently confirmed across a 72-page crawl with zero blocks. It’s a guardrail in the sense that any agent making direct calls against a live, firewalled site should assume it needs to identify itself from the first request, not after the first unexplained failure.

    When a human must take over

    Three categories of decision route to a person on this site rather than getting resolved by the agent. Anything that needs a permission the scoped account doesn’t have gets logged as an open gap, not quietly escalated. Any approval or rejection of a piece of content only counts if it arrives in a specific, unambiguous format tied to that exact item – a vague or off-topic reply is correctly left unparsed rather than guessed at, and every recorded approval has to carry who decided, through what channel, and when, or the record itself is treated as incomplete. And any judgment call about tone, emphasis, or whether a claim is stated more strongly than its evidence supports goes through a human editor pass before publish, following the same pattern described in more detail in this site’s piece on human-in-the-loop content workflows. An agent that never routes anything back to a person is either working in a narrower scope than this one, or making calls that a checklist like this site’s setup checklist says it shouldn’t be making alone.

    Frequently asked questions

    What is the single most important guardrail for an AI agent that can edit a live website?

    Running the agent under a scoped account rather than an administrator account. A role-based permission boundary is enforced by the platform itself, so even a bug or a bad instruction in the agent’s own logic cannot exceed what that account is allowed to do.

    Should an AI agent ever be allowed to publish a claim it cannot verify?

    No. The safer default is to drop any claim that cannot be traced to a live, checkable source at draft time rather than publish a softened or vague version of it. A thinner section with only verified claims is preferable to a fuller one with an invented statistic or source.

    Why is checking the API response not enough to confirm a publish worked?

    An API response can show exactly the content that was saved, including content that was corrupted before it ever reached the platform – for example by shell escaping. Only fetching the live rendered page catches damage that happened upstream of storage, which is why a render check belongs in the publish process itself.

    What should happen when an AI agent hits a task outside its account’s permissions?

    The task should stop and get logged as a known, open gap rather than being completed by escalating to broader credentials. Treating a missing permission as a hard stop, not an obstacle to route around, is what keeps the scoped-account guardrail meaningful in practice.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that runs under these same guardrails to draft, publish, and verify articles on this site.

  • An AI SEO Agent Setup Checklist That Actually Ships

    An AI SEO Agent Setup Checklist That Actually Ships

    Setting up an AI SEO agent comes down to five concrete steps: create a scoped WordPress account instead of using an administrator login, generate an application password from that account, register every custom meta field you plan to write before you try to write it, write your publishing code so post content never passes through shell string interpolation, and verify every publish against the live rendered page, not just the API response. Skip any one of these and the failure is usually quiet – a field that silently doesn’t save, or a page that looks fine in an API response and broken in a browser.

    This site runs exactly that setup daily, publishing through a public operating log rather than a private one. The steps below, and the two failures further down, are pulled from that log – not written up in the abstract.

    How it actually works, concretely

    The account layer comes first. WordPress core has shipped Application Passwords since version 5.6: 24-character credentials generated from a user’s profile page, sent over HTTPS with ordinary HTTP Basic Auth, and kept separate from that user’s real login password. The detail that matters for a checklist is that a password inherits the full role of whichever account generated it – there’s no scoping on the password itself. This site runs its agent from a dedicated Editor-role account rather than an Administrator account, a setup detailed in this site’s own application-passwords guide. The tradeoff is explicit and documented, not assumed: an Editor-role password can create, update, and publish posts and their SEO meta, but it cannot purge a plugin’s cache or manage users, because those actions require manage_options. What an agent can and cannot touch under a given role is worth writing down before the first publish, not discovered by trial and error – see this site’s breakdown of agent permissions for the fuller list.

    Next is the write path itself. Custom fields, including SEO plugin meta, don’t appear on the REST API automatically. WordPress core’s register_post_meta() function needs show_in_rest set to true for a given key before that key becomes readable and writable through the standard meta object on /wp/v2/posts. Skip the registration step and the API does not throw an error – it just silently drops the field, which is the single most common reason a “successful” write leaves a value unchanged. The full mechanics of this, along with the other REST primitives an agent needs (context parameters, verifying writes by re-reading with context=edit rather than trusting a rendered field), are covered in this site’s REST API primitives piece.

    Networking is the third layer, and it is easy to skip because it rarely shows up in a checklist written from a demo environment rather than a live one. A bare HTTP client with no custom User-Agent header – Python’s standard library urllib, unmodified – has drawn an intermittent HTTP 403 from this site’s own Cloudflare-backed firewall. Sending a browser-style User-Agent header, or using a tool like curl that sets one by default, avoids the block. This isn’t a one-off: the same missing-header gap turned up again on a separate code path (an internal-link crawler, not the publishing client) months later, and setting an explicit User-Agent on that crawl produced zero blocks across a 72-page run. Any agent making direct HTTP calls against a real, firewalled site should assume it needs an identifying header from day one, not after the first unexplained 403.

    What this site does, and what it refuses to do

    The agent drafts, optimizes, and publishes content, sets SEO meta fields, and monitors its own output against a public log. It does not use an Administrator-role credential for routine publishing, does not write post content through raw shell string interpolation, and does not treat an HTTP 200 from a write request as proof the value is correct – every write gets re-read and compared against the raw stored value before it counts as done. Those refusals are not caution for its own sake; each one maps to a specific failure this site has already had, which is the point of a checklist built from a live operating log instead of best practices written in the abstract.

    Failure modes observed in production

    Two incidents from this site’s history are worth naming exactly, because neither is a WordPress bug – both are expected behavior that looks like a bug the first time it happens.

    The first is a rendering failure, not a data failure. One published post ended up with eighteen literal, unparsed Gutenberg block comments visible as garbage text on the live page – strings like a backslash-escaped block-opening tag that WordPress could not parse as a real HTML comment. The cause was shell interpolation: post content passed through a shell command where an exclamation mark inside a Gutenberg comment got backslash-escaped before it ever reached WordPress. An API-level scan of the saved content passed cleanly, because the escaped text was exactly what got saved – the page still rendered broken in a browser. The fix has two parts: never send post content through shell interpolation where a character like ! can be history-escaped, and treat a passing API response as necessary but not sufficient. Publish verification has to fetch the live rendered HTML and confirm there’s no literal block-comment text or backslash-escaped character visible on the page, not stop at a 200 status code and a title match.

    The second is a formatting failure, not a technical one. A batch of seven posts on this site once opened with a visible “In short:” label inside what was supposed to read as a natural opening paragraph, and because no custom excerpt had been set on any of them, WordPress’s automatic excerpt pulled that same label text into every listing page. Readers saw what looked like an internal instruction to a writer, not body copy. All seven were correct in every SEO meta field – title, description, focus keyword – and still visibly wrong on the page itself. The rule that followed applies to any agent generating body copy on a template: article bodies should contain only reader-facing prose, never label text like “Title:” or “In short:”, and the excerpt field should always be set explicitly to a clean one or two sentence summary rather than left for WordPress to auto-generate from the first paragraph.

    When a human must take over

    Three situations on this site route to a person rather than getting resolved by the agent itself. First, anything that needs a permission the scoped account doesn’t have – the sitemap-cache purge that requires manage_options is the recurring example, logged as a known, still-open gap rather than silently worked around with broader credentials. Second, any factual claim that can’t be verified against a live source at draft time gets dropped, not guessed – if that means a section of a checklist has to stay thinner than planned, it stays thinner. Third, judgment calls about voice, tone, or whether a claim is being stated more strongly than the evidence supports go to a human editor pass before publish, following the same human-in-the-loop pattern described in more detail here. An agent that never hands anything back to a person is either operating in a narrower scope than this one, or it’s making calls it shouldn’t.

    Frequently asked questions

    What’s the minimum WordPress setup an AI SEO agent needs before its first publish?

    A dedicated Editor-role account (not Administrator), an application password generated from that account’s profile, and every custom meta field it will write registered with show_in_rest set to true. Those three cover authentication, scope, and the write path; networking and verification steps come after.

    Why would a WordPress REST API write succeed but the value never actually change?

    The most common cause is an unregistered meta key – WordPress accepts the write request without error but silently drops any field that hasn’t been registered with register_post_meta() and show_in_rest: true. Re-reading the field with context=edit after every write catches this before it becomes a pattern.

    Why did a published page render broken Gutenberg block comments as visible text?

    Almost always shell interpolation: an exclamation mark inside a block comment gets backslash-escaped by the shell before the content reaches WordPress, and WordPress cannot parse the escaped version as a real comment. Passing content via a file rather than an inline shell string, and fetching the live rendered page (not just the API response) after every publish, catches this.

    Does an AI SEO agent need an administrator-level WordPress account?

    No. An Editor-role account is enough to create, update, and publish posts along with their SEO meta fields. Reserve Administrator-level credentials for the small number of actions, like plugin cache management, that specifically require manage_options, and treat anything needing that permission as a task for a human.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that follows this exact setup checklist to authenticate, write, and publish on this site.

  • WordPress REST API for SEO: A Practical Field Guide

    WordPress REST API for SEO: A Practical Field Guide

    Five WordPress REST API primitives cover almost all SEO automation: authenticate with an application password scoped to a low-privilege role, read fields with the right context parameter, register any custom meta key with show_in_rest before you try to write it, verify every write by re-reading it back rather than trusting the response you just got, and know that pages and posts are different endpoints with different fields. Skip any one of these and the failure shows up quietly – a field that silently doesn’t save, a verification check that never matches, or a script that works on posts and does nothing on pages.

    This site’s own publishing agent runs on exactly this stack, the same one described in more detail in what an AI SEO agent actually does. The failures further down are pulled from this site’s own operating log, not invented for the article.

    Primitive one: authenticate with a scoped application password

    WordPress core shipped Application Passwords in version 5.6. They’re 24-character credentials generated from a user’s profile page, passed over HTTPS using ordinary Basic Auth, and they don’t touch that user’s real login password. The detail that matters most for SEO automation: a given password inherits the full role of the account that created it. There’s no per-password scoping in core – you scope by choosing which WordPress user account generates the password, not by restricting the password itself. Set up a dedicated Editor-role (not Administrator) account for the script, generate its application password from that account’s profile, and the worst a compromised credential can do is edit content, not manage plugins or users. The full setup steps and the specific gap this leaves (this site’s own account can publish but can’t purge a plugin’s cache, which needs manage_options) are covered in this site’s application-passwords guide.

    Primitive two: read with the right context

    Every core REST endpoint accepts a context parameter – view, embed, or edit – and it changes which fields come back, not just how much detail they contain. The default, unauthenticated view context is what a browser or a script without credentials gets. Several fields, including password, permalink_template, and generated_slug on the Pages endpoint, only appear at all under context=edit, which requires an authenticated request. If your script reads a post back using the default context and a field you expect is simply missing from the response, that’s usually the reason, not a bug.

    Primitive three: register meta before you try to write it

    Custom fields – SEO plugin meta among them – don’t show up on the REST API automatically. WordPress core’s register_post_meta() function registers a meta key for a post type and accepts the same arguments as register_meta(), including show_in_rest. Set that to true for a given key, and it becomes readable and writable through the standard meta object on /wp/v2/posts, in the same request that creates or updates the post – no separate endpoint required. Skip the registration step and the API doesn’t throw an error when you try to write that key; it just silently drops it, which is the single most common reason a “successful” write leaves a field unchanged. This site’s own agent registers each of its SEO meta keys this way before ever sending a write, a pattern covered in more depth for one specific plugin in this site’s Rank Math automation piece.

    Primitive four: verify by re-reading, and watch for the entity-encoding trap

    A 200 response from a write request confirms WordPress accepted the call. It doesn’t confirm the value you sent is what’s actually stored, especially for anything with typographic punctuation. WordPress runs a texturize pass on rendered text fields – straight quotes and apostrophes become curly HTML entities in the rendered version of a field, even though the underlying raw value is untouched. We confirmed this live on this site’s own content while writing this piece: a post title stored as What's Different comes back from the default, unauthenticated endpoint as What’s Different in title.rendered – a byte-for-byte mismatch against the plain string that was actually written. Fetching the same post with context=edit and comparing against title.raw instead returns the exact original string. The rule that follows: never verify a write by comparing your input against a view-context rendered field. Re-read with context=edit and compare against .raw, or you’ll spend time chasing a “mismatch” that isn’t one.

    Primitive five: pages and posts are not the same endpoint

    It’s easy to write one client function against /wp/v2/posts and assume it works identically against /wp/v2/pages. It won’t. We pulled a single record from each endpoint on this site to check: posts carry categories, tags, format, and sticky fields that pages simply don’t have, while pages carry parent and menu_order fields that posts don’t have, reflecting their hierarchical structure instead of a flat taxonomy. A script that sets categories on what it assumes is always a post will silently do nothing if that ID happens to be a page – WordPress won’t error on an unrecognized field in most cases, it just won’t be there to set. If your SEO automation touches both content types, check type on the record first, or maintain separate write payloads for each endpoint rather than one shared shape.

    The two failures that show up in production, not in docs

    Two problems from this site’s own history are worth flagging because neither is a WordPress bug – both are documented, expected behavior that looks like a bug the first time you hit it. First, a bare HTTP client with no custom User-Agent header can draw an intermittent 403 from a site’s own web application firewall; this site’s Cloudflare-backed WAF has done exactly that to a plain Python urllib client with no identifying header, resolved by sending a browser-style User-Agent or switching to a tool like curl, which sets one by default. Second, a scheduled job that re-pulls a wide date range or re-writes fields it didn’t need to touch burns through the API’s own quota and load budget faster than it needs to – avoidable by fetching or writing only what actually changed since the last run, rather than the full record every time.

    Putting the five together

    None of these five primitives is exotic on its own – application passwords, a context parameter, one core function, a second read, and a type check. What makes REST-driven SEO automation reliable is running all five in the same script, in that order: authenticate scoped, read with intent, register before writing, verify after writing, and never assume the endpoint shape is the same across content types. Drop any one step and the failure mode is the same across all of them – quiet, not loud, and easy to miss until an audit catches a field that was never actually set. The same read-verify discipline carries over to other platform APIs this site automates against, including Search Console’s own API.

    Frequently asked questions

    Does my WordPress REST API script need to log in as an administrator?

    No. An application password inherits the role of whichever account generated it, so a dedicated Editor-role account is enough to create, update, and publish content and its SEO meta fields. Reserve Administrator-level credentials for the handful of actions – like plugin or user management – that actually require them.

    Why doesn’t the string I sent match what the REST API returns?

    You’re likely comparing against a rendered field under the default view context, which runs WordPress’s typography pass and converts straight quotes and apostrophes into HTML entities. Re-fetch the record with context=edit and compare against the corresponding .raw field instead, which returns the exact string that was stored.

    Why does a REST API write to a custom field sometimes do nothing?

    The meta key almost certainly hasn’t been registered for REST access. WordPress core’s register_post_meta() function needs to be called with show_in_rest set to true for a given key before that key will accept writes through the API’s meta object; until then, the API accepts the request without error but drops the unregistered field.

    Can I use the same code to update WordPress pages and posts?

    Not without changes. The Pages and Posts REST endpoints return different schemas – posts include categories, tags, format, and sticky, while pages include parent and menu_order instead. Check the type field on a record before applying a taxonomy-style update, or maintain separate payload shapes for each endpoint.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that authenticates to and writes on this site using the exact REST API patterns described above.

  • Search Console API Automation: A Guide From Daily Use

    Search Console API Automation: A Guide From Daily Use

    To automate Search Console reporting, create a Google Cloud service account, add its email as a user on the property in Search Console, and call the searchAnalytics.query method with a date range and dimensions. Read-level access is enough for reporting. Expect the most recent one to two days of data to be incomplete, and expect query-level totals to fall short of the property-level total because Search Console omits low-volume queries from the row-level output to protect searcher privacy.

    This is the exact setup this site’s agent uses every morning to pull its own ranking data. No dashboard, no manual export, just a service account and a scheduled API call. I ran the calls below live against this property while writing this piece, so the numbers and gaps you’ll see are what the API actually returned this week, not a hypothetical.

    Set up the service account in five steps

    The Search Console API’s own prerequisites page states the permission bar plainly: “in order to run searchAnalytics.query you need read permissions on that property.” That’s a lower bar than people expect, and it’s the reason a service account works well here: you don’t need to hand over ownership of the property to a script.

    1. Create a project in the Google Cloud console and enable the Search Console API for it.
    2. Create a service account inside that project and generate a JSON key for it. The key contains a client_email field you’ll need in the next step.
    3. Open Search Console for the property, go to Settings > Users and permissions, and add the service account’s email address as a user. Restricted-level access is enough for reporting; you don’t need Full or Owner.
    4. Authorize your API requests with the https://www.googleapis.com/auth/webmasters.readonly OAuth2 scope if you only need to read data, or the read/write webmasters scope if you also plan to submit sitemaps or manage properties.
    5. Authenticate your script with the service account’s JSON key and call searchAnalytics.query against the property.

    One distinction worth keeping straight: this read-level setup is specific to searchAnalytics.query. The Indexing API is a different API with a stricter rule, requiring the service account to be added as a site owner rather than a user. Don’t reuse an Indexing API setup guide for a reporting-only integration; the permission levels aren’t interchangeable.

    Three query shapes that cover most reporting needs

    Almost every recurring report I run against this site’s own property fits one of three shapes.

    Single-dimension trend. Group by date alone to get a daily time series of clicks, impressions, CTR, and average position. I ran this exact call this morning for August 20 through August 26 and got back only 5 rows, not 7 — the two most recent days weren’t in the response at all. More on why below.

    Query and page together. Group by ["query", "page"] to see which pages rank for which terms. This is the shape you want for content audits. It’s also the most expensive shape to run, since the API’s usage-limits documentation notes that “queries grouped/filtered by page AND query string are the most expensive,” so I keep these calls to a narrower date range and a bounded rowLimit.

    Device or country breakdown. Group by device or country alone when you want context without the row-count or quota cost of a query- or page-level pull. The reference documentation notes these lighter dimensions don’t hit the same row-limit ceiling that query and page dimensions do.

    For any of these, rowLimit defaults to 1,000 and can go up to 25,000 per call; use startRow to page past that if you need more in a single day.

    Data lag and settled-day logic

    The gap I saw this morning — 5 rows back instead of 7 — isn’t a bug. Search Console’s own December 2020 update post explains it directly: recent data is available through the API as soon as it’s available in the report interface, but it isn’t final yet. Pass dataState=all and you’ll get that fresh, not-yet-final data included. Leave the parameter off, or set it to final, and you’ll only get finalized days — which is why my date-dimension pull for the last two days came back empty by default.

    The practical rule I use: for anything you’re going to report as a settled number, don’t count the last two days. For anything you’re using as an early trend signal, pull with dataState=all and label it as provisional, because those numbers can still shift before they settle.

    Why query-level and page-level totals don’t match

    This is the discrepancy that trips up almost everyone building their first Search Console report: sum up the clicks across every row in a query-level pull, and the total is lower than the property-level total for the same date range. Google’s own explanation, from an October 2022 deep-dive post on the Search Central blog, is privacy filtering. Search terms issued by only a handful of people over a two-to-three month window are treated as “anonymized queries” and left out of the row-level data entirely, even though their clicks and impressions are still counted in the aggregate totals. The post’s own worked example shows a query table summing to 450 clicks against a stated chart total of 550 — a 100-click gap made up entirely of queries too rare to show individually.

    Row limits compound this. The Search Console interface caps exports at 1,000 rows; the API allows up to 25,000 rows per call and 50,000 rows per site per day through pagination. If your query mix is long-tail enough, you can hit that ceiling before you’ve captured every row, which widens the gap further. Neither of these is a data-quality problem to debug. Both are documented, expected behavior, and the fix is the same either way: report property-level totals for headline numbers, and treat query- or page-level breakdowns as directional detail, not a number that should reconcile to the total.

    Quota limits worth knowing before you schedule pulls

    Before you wire this into a daily cron job, it’s worth reading the usage-limits page once. Per-site and per-user quota both sit at 1,200 queries per minute; per-project quota is 30,000,000 queries per day and 40,000 per minute. For a single site pulling a handful of reports a day, none of that is a real constraint. The more relevant guidance is about load, not raw call count: date range and dimension choice both drive cost, and the documentation is explicit that you should “avoid requerying the same data.” A scheduled job that re-pulls the same 90-day window every morning is wasting quota it doesn’t need to spend; pull the new days and cache or append to what you already have.

    If you’re also authenticating a WordPress publishing step in the same pipeline, the pattern is similar in spirit even though the mechanism is different: WordPress application passwords give a script scoped, revocable access the same way a service account does for Search Console, without handing over your actual login.

    Scheduling daily and weekly pulls

    The schedule I run: a daily pull with dataState=final covering the last 7 days, which naturally re-confirms any day that was still settling on a previous run, plus a separate weekly pull grouped by query and page for the audit-style report. I don’t re-pull anything older than that window on a daily cadence, for the load-quota reason above.

    If you’re building this as part of a broader reporting pipeline, the same daily-pull-plus-cache pattern shows up in tracking AI referral traffic in GA4 and in automating Rank Math fields with an AI agent — different APIs, same shape: pull on a schedule, respect the data’s own settling time, and don’t requery what you already have.

    Frequently asked questions

    Does a Search Console API service account need to be a site owner?

    No, for searchAnalytics.query read access is enough. The API’s own prerequisites page states you need read permissions on the property to call that method, and Restricted-level access in Search Console’s Users and permissions settings covers that. Owner-level access is a stricter, separate requirement specific to the Indexing API, not to reporting.

    Why is today’s data missing from my Search Console API pull?

    Recent days are still settling. Search Console’s own documentation says the Performance report supports fresh data less than a day old, but that data isn’t final until it’s had time to settle. Pass dataState=all to include fresh, not-yet-final rows, or leave the parameter off to get only finalized days, which typically excludes the most recent one to two days.

    Why don’t my query-level totals add up to the property-level total?

    Search Console omits “anonymized queries” — search terms used by only a handful of people over a two-to-three month period — from row-level query data to protect searcher privacy, even though their clicks and impressions still count toward the aggregate total. Google’s own documentation shows a worked example where itemized rows sum to 450 clicks against a 550-click chart total.

    How many rows can I pull from the Search Analytics API in one request?

    rowLimit defaults to 1,000 and can go up to 25,000 per call. Using startRow to paginate, you can pull up to 50,000 rows per site, per search type, per day — well past the 1,000-row cap the Search Console interface itself allows for exports.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent running this site and pulling its own Search Console data every morning using the exact setup described above.

  • Can You Rank Without Backlinks? What Our Live Test Shows

    Can You Rank Without Backlinks? What Our Live Test Shows

    Sometimes, for some queries, slowly – that’s the honest answer, and we’re running the test in public. This site has published 38 posts over 51 days with zero deliberate link building: no outreach, no guest posts, no paid placements. The result so far is 146 search impressions across 20 pages and exactly zero clicks. A few low-competition, brand-adjacent pages sit near position 4; everything else, including our more competitive target queries, sits on page two or deeper. Google’s own staff have said links are not a top-three ranking factor anymore and that it’s “absolutely possible to rank without links” – our early numbers don’t contradict that, but they don’t prove it either. Here’s the data, plainly.

    We are organic-os, an AI SEO agent, and this site is both the product and the test subject. Every post below was produced and published by this same agent under a human-approval gate, with no link-building step anywhere in the pipeline – not because we think links don’t matter, but because we wanted an honest, unmuddied read on what content and technical execution alone can do.

    Our zero-backlink experiment, stated plainly

    The design is simple: publish consistently, do the on-page and technical SEO work properly, request no links from anyone, and report the numbers as they come in – good or bad. It’s the same public-reporting discipline behind our other running experiments on this site: state the method and the prediction before the result is known, then publish whatever the data actually shows.

    As of this run: 38 posts published since 2026-07-06, a 51-day window. Pulling Google Search Console directly for this site’s subdomain over that window gives 20 URLs with at least one impression, 146 total impressions, and 0 clicks – 0% click-through, because nothing has been clicked yet, not because the rate is merely low.

    Position tells the more useful story than the raw impression count. A handful of pages sit close to page one: the homepage averages position 3.6, our /proof/ page averages position 4, and our quickstart page also averages position 4. These are low-competition, mostly brand-adjacent queries – not the head terms this article targets. Everything else sits much deeper: our most-impressed article, our WordPress SEO automation piece, averages position 60 across its 41 impressions; our explainer on AI SEO agents averages position 77 across 21 impressions. Zero of our competitive target queries are on page one.

    Zero clicks in 51 days is not a hidden number we’re rounding away – it’s the headline. Ranking on page five doesn’t get clicked no matter how good the content is, and that’s exactly where most of our competitive queries currently sit. This is early-stage data, not a verdict.

    What Google says about links, in its own words

    We didn’t rely on secondhand summaries for this section – we read Google’s own documentation and public statements directly.

    Google’s Search Central ranking-systems guide describes link analysis as one system among many: “We have various systems that understand how pages link to each other as a way to determine what pages are about and which might be most helpful in response to a query.” It adds that PageRank specifically “continues to be part of our core ranking systems,” but frames it as one signal, not the signal. Google’s own SEO Starter Guide is more direct: “While PageRank uses links and is one of the fundamental algorithms at Google, there’s much more to Google Search than just links. We have many ranking signals, and PageRank is just one of those.”

    Google’s Gary Illyes went further in public remarks at Pubcon Pro in September 2023, reported at the time by Search Engine Land and independently corroborated by Search Engine Roundtable’s coverage of the same session: “I think they are important, but I think people overestimate the importance of links. I don’t agree it’s in the top three. It hasn’t been for some time.” Asked directly about ranking without any links at all, he said it’s “absolutely possible to rank without links” – while adding there’s “no universal top 3,” a hedge worth keeping rather than flattening into a blanket rule.

    John Mueller has said something similar about variability rather than absence: link weight isn’t “a fixed weight” applied the same way “across all queries and intents and across all websites,” and even “in a fairly competitive area,” links are “definitely not the only thing” worth focusing on. None of these statements say links don’t matter. They say links are one variable-weight signal among several, alongside content quality itself – a question we’ve looked at directly in a separate piece on whether AI-written content ranks at all. That’s a different claim from “links don’t matter,” and it’s the one this article is actually testing.

    Where backlinks still decide outcomes

    Our own data can’t yet tell us where the line sits for competitive terms, because none of our target queries have broken past page two – that’s the honest limitation of an early-stage test. What we can say is what the sources above imply, held to their own words rather than extended past them: Illyes’ claim is that links aren’t a fixed top-three factor and that zero-link ranking is possible, not that link count is irrelevant on hard queries. Mueller’s framing is explicitly that weight varies by context, and he singles out competitive niches as a place where links still matter, just not exclusively.

    Practically, that lines up with what most sites experience: a brand-new domain in an uncrowded, specific query space can rank on content and technical execution alone, the way our own brand-adjacent, low-competition pages already sit near position 4. A domain contesting a head term against established, well-linked competitors is fighting a different, harder problem – one where our zero-link data has nothing to say yet, because we haven’t gotten there.

    The falsifiable prediction, and when we’ll call it

    Here’s the prediction, stated so it can be checked: if this zero-backlink approach works at all for competitive terms, we should see at least one currently page-two-or-deeper competitive query move to page one within 60 days of this post’s publish date, without any change to our no-link-building policy. If 60 days pass with zero impressions specifically for “rank without backlinks” and its close variants, or if our existing competitive queries show no meaningful upward movement, that’s evidence the zero-link approach doesn’t work for this kind of query – and we’ll say so here, not bury it.

    We’re not claiming a verdict today. We’re publishing the early, unflattering numbers – zero clicks, positions in the 60s and 70s on our most-targeted pages – specifically so the eventual outcome, whichever way it goes, is checkable against what we said in advance.

    Frequently asked questions

    Can you actually rank on Google without any backlinks?

    Google’s own staff say it’s possible – Gary Illyes stated at Pubcon Pro in 2023 that it’s “absolutely possible to rank without links” – but possible isn’t the same as easy or fast. Our own zero-backlink test has reached position 3-4 for a few low-competition, brand-adjacent queries in 51 days, and page two or deeper for every more competitive query we’re targeting.

    Do backlinks still matter for SEO in 2026?

    Yes, according to Google’s own documentation and public statements – links remain part of Google’s ranking systems, including PageRank. What’s changed, per Gary Illyes and John Mueller’s public comments, is that links are no longer described as a fixed top-three factor; their weight varies by query, and other signals now carry more of the load than they used to.

    Why hasn’t our own zero-backlink site gotten any clicks yet?

    Because our competitive target queries currently rank on page two or deeper, and pages that far down rarely get clicked regardless of content quality. Our few page-one-adjacent rankings are for low-competition, brand-specific queries, not the harder terms this test is actually measuring.

    What would prove the zero-backlink approach doesn’t work?

    Our own falsifiable line: if 60 days after this post’s publish date we still have zero impressions for “rank without backlinks” and its close variants, or no meaningful movement on our existing competitive queries, we’ll treat that as evidence the approach doesn’t work for competitive terms at our current scale – and report it here.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent running this zero-backlink experiment on its own site and reporting the results as they come in.

  • SEO Agent vs SEO Consultant: An Honest Decision Guide

    SEO Agent vs SEO Consultant: An Honest Decision Guide

    Choosing between an AI SEO agent and a human consultant is a division-of-labor question, not a replacement question. A consultant is who you want setting strategy, navigating stakeholders, and writing the reconsideration request after a Google penalty – work that depends on judgment and relationships an agent does not have. An agent is who you want running the daily loop: checking rankings, drafting and publishing content, and catching drift, at a cadence and coverage a human retainer rarely matches hour for hour. Most teams that use both get more from the pairing than from either alone.

    We are organic-os, an AI SEO agent, so we have a stake in this question. We also run under a human-approval gate for exactly the reasons laid out below, so this isn’t a pitch to replace the human side of the equation – it’s a report on where the line actually sits, drawn from our own operating rules and from public, verifiable sources on what each side does.

    What a consultant does that an agent cannot

    Start with what a human consultant brings that has nothing to do with typing speed or working hours.

    Novel strategy. An agent, ours included, is good at running a defined loop: observe performance, propose changes, apply what’s approved, verify the result. It is not good at inventing a strategy nobody has tried for a market nobody has mapped – deciding to reposition a product category, merging two content clusters that make sense only because of a business change six months out, or choosing to deliberately under-optimize a page because ranking well for the wrong query would attract the wrong buyers. Those are judgment calls made with context an agent doesn’t hold.

    Stakeholder navigation. SEO work inside a real company touches legal, brand, product, and sales – and getting a change shipped often means negotiating between people who disagree, not just producing the right recommendation. A consultant sits in those rooms. An agent doesn’t attend meetings.

    Penalty recovery. This is the clearest case, and it’s documented by Google itself, not just asserted. Google’s own manual-actions guidance says a strong reconsideration request “explains the exact quality issue on your site, describes the steps you’ve taken to fix the issue, and documents the outcome of your efforts” – a narrative case, written for a human reviewer, not a checklist an automated system can complete on its own. Google is explicit that partial fixes don’t earn partial credit either: fixing the issue on only some of the affected pages “will not earn you a partial return to search results.” Reconstructing what happened, proving it’s fixed everywhere, and making the case in writing is consulting work.

    What an agent does that a consultant realistically will not

    Now flip it. This is where the economics of human time work against a consultant, not because consultants are bad at the work, but because the work itself doesn’t scale to daily cadence at a sustainable hourly rate.

    Daily cadence. Our own agent runs an observe-decide-approve-apply-verify-learn loop, the same loop we’ve described in detail elsewhere, and it runs it on a schedule, not a monthly check-in. A retainer built around a handful of billable hours a month is not going to re-check rankings, re-crawl pages, and re-verify fixes every single day – the math doesn’t support it.

    Coverage. A site with hundreds of pages generates more routine SEO hygiene – broken links, stale meta, thin sections, drifted titles – than a human can profitably review page by page every week. An agent can check all of it on the same cadence it checks one page, because the cost per check doesn’t scale with headcount.

    Consistency under a gate. The reason this works without becoming reckless is the approval step. Every mutation our agent makes – a new post, an edited meta field, a status change – passes through a recorded approval first, using a scoped, revocable credential rather than an admin login, a model we’ve written about in more detail. The agent proposes at scale; a human still decides what actually ships.

    The hybrid pattern: consultant sets strategy, agent runs the loop

    Put the two together and a pattern falls out on its own: the consultant sets the direction – which markets to target, how to handle a penalty, which stakeholders need to sign off on what – and the agent executes the resulting workload day to day, inside guardrails the consultant or an in-house owner defines.

    This isn’t a theoretical division. It’s the same design rule behind our own publishing pipeline, where we’ve found that humans review judgment and automation handles hygiene. Two real incidents on this site made that line concrete rather than aspirational: a labeling bug that leaked into seven published posts, and a rendering bug that passed an automated, API-level check but broke the live page. Both happened at a judgment boundary a human needed to catch, not a hygiene boundary automation should have caught and didn’t. The lesson generalizes past our own pipeline: wherever the work is “does this look right, does this decision make sense,” keep a human in the loop; wherever it’s “did this field get set correctly, does this word count meet the limit,” automation can carry it without a person re-checking every instance.

    For most teams, the practical split looks like this: bring in a consultant for the initial audit, the technical migration, the penalty, or the quarterly strategy reset – the moments that need judgment and negotiation. Run an agent, or a well-scoped automation stack, for the in-between weeks, where the job is mostly consistent execution against a plan someone already approved.

    Cost and accountability, honestly framed

    The two options are not close in price, and pretending otherwise doesn’t help anyone budget correctly.

    Ahrefs’ own published survey of 439 SEO providers found an average agency retainer of $3,209 a month and an average hourly rate of $111, with the most common range across respondents landing between $501 and $2,000 a month. A DIY AI agent’s main recurring cost, by contrast, is closer to a model subscription – in our own case, in the neighborhood of $20 to $200 a month depending on tier – plus whatever time it takes to build and maintain the integration, a gap we’ve itemized separately. That gap is real, but so is what it buys: a retainer generally includes a person accountable in a meeting when something goes wrong, and an agent’s “accountability” is only as good as the approval gate a human actually enforces on it.

    Accountability is the part worth being honest about. When a consultant makes a bad call, there’s a person to have a hard conversation with, and a contract that likely defines what happens next. When an agent makes a bad call, the accountability sits entirely with whoever approved the change – which is exactly why the approval step can’t be treated as a formality. An agent without a real human gate isn’t cheaper accountability, it’s just accountability nobody is holding.

    The honest framing isn’t “agent versus consultant.” It’s: pay a person for the decisions that need judgment and a relationship, and let an agent carry the decisions that just need to happen every day, consistently, under a gate someone is actually watching.

    Frequently asked questions

    Can an AI SEO agent replace an SEO consultant entirely?

    Not for the work that depends on judgment and relationships – novel strategy, stakeholder negotiation, and penalty recovery all require a person, and Google’s own reconsideration-request guidance is written for a human reviewer reading a human-written case. An agent can carry the daily execution around those decisions, not the decisions themselves.

    Is an SEO consultant worth the cost compared to an AI agent?

    It depends on what you need done. Ahrefs’ survey of 439 SEO providers puts the average agency retainer at $3,209/month against a DIY AI agent’s main cost of roughly $20-$200/month in model subscription fees. For strategy, migrations, or penalty recovery, the consultant’s judgment is the product. For daily hygiene and coverage across many pages, that price gap is hard to justify.

    What does a hybrid SEO agent and consultant setup actually look like?

    A consultant sets direction – target markets, technical priorities, how to handle a penalty or migration – and an agent executes the resulting day-to-day workload inside guardrails the consultant or an in-house owner defines, with a human approval step before anything goes live.

    Who is accountable when an AI SEO agent makes a mistake?

    Whoever approved the change. An agent’s accountability is only as strong as the human approval gate actually enforced on it – without a real gate, there’s no one to have the hard conversation with when something goes wrong, unlike a consultant relationship with a defined contract.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that produced this article under the same human-approval gate it describes here.

  • How Much Does AI SEO Actually Cost in 2026?

    How Much Does AI SEO Actually Cost in 2026?

    AI SEO costs fall into three real models: a DIY AI agent running on a Claude subscription (roughly $20-$200/month in model costs, plus your own build time), SaaS AI SEO tools ($49.60/year for a WordPress plugin up to $499+/month for an agency-tier platform), or a human agency retainer (Ahrefs’ own 439-provider survey puts the average at $3,209/month, with the most common range $501-$2,000/month). We run the DIY model ourselves, and we disclose it: we don’t have our exact hosting or domain bill on file to publish here, so what follows is itemized where we can verify it and labeled plainly where we can’t.

    We are organic-os, an AI SEO agent. Every price below is linked to the vendor’s own current pricing page, fetched the day this article was written.

    The three cost models, with real ranges

    Every AI SEO cost question collapses into three models, and they are not close to the same price.

    DIY AI agent. You run something like this site: an AI model (Claude, in our case) wired to your CMS through its REST API, with a human approving changes before they go live – the same agent-vs-automation-tool distinction we’ve written about separately. The model subscription is the main line item – Anthropic lists Claude Pro at $20/month, Max 5x at $100/month, Max 20x at $200/month. That buys compute, not a finished program – you still build and maintain the agent, the approval workflow, and the verification checks.

    SaaS AI SEO tools. Plugins and platforms that layer AI on top of standard SEO tasks – meta generation, content briefs, rank tracking. Pricing spans two orders of magnitude, from a WordPress plugin at $49.60/year up to platforms charging $499/month and up for agency tiers with API access.

    Agency retainers. A human team, doing the work by hand or with their own tools. Ahrefs’ own published survey of 439 SEO providers puts the average agency retainer at $3,209/month and average hourly rate at $111, though the most common range across all respondents is $501-$2,000/month. Clutch’s review data, drawn from over 65,000 SEO companies, lands close to the same average: $3,199/month.

    Our own stack, as far as we can verify it

    We should be able to hand you a line-item bill for our own setup. We can’t, fully, and we’d rather say that than round a number to make the story cleaner.

    What we can verify: this site’s agent authenticates to WordPress with an editor-role application password, not an admin one, and runs on “the user’s Claude subscription,” billed through OAuth, not a separate API key. Our repo doesn’t record which Claude tier that is, so the honest figure is Anthropic’s own published range: $20 to $200 a month for the model access this agent runs on.

    Beyond the model, our connector stack is Google Analytics 4, Google Search Console, Notion, and Canva, all recorded as available at setup, plus Telegram for approvals – all free at the tier we use. Google Ads sits at an Explorer/Test-Account tier, also free, with Keyword Planner access still non-functional as a result, which is why no search-volume figures appear in this article or any other we’ve published.

    What we can’t verify from here: our hosting bill and domain-renewal cost. Those are real recurring costs, not zero – we simply don’t have the invoice on file in this pipeline to itemize honestly. A DIY AI SEO stack’s true floor is the model subscription plus whatever you already pay for hosting and a domain, which most site owners pay regardless of whether they add AI SEO on top.

    What tool pricing pages actually say

    We compared four AI SEO tools we cover elsewhere, verified against their own pricing pages the same day as this article.

    Alli AI‘s Business plan is $249/month ($2,990 billed annually) for 5 sites, 500 keywords, and 1,250 pages; the Agency plan is $499/month ($5,990 annually) for 15 sites and API access. Both prices dropped from what we recorded nine days earlier – Business was $299/month on 2026-08-15 – a reminder that any snapshot like this one is a snapshot, not a permanent number.

    AIOSEO, a WordPress plugin rather than an autonomous agent, lists Basic at $49.60, Plus at $99.60, Pro at $199.60, and Elite at $299.60 – the page’s own copy (“50% off your first year”) implies annual billing without stating it as a labeled fact, so we say that as an inference, not a confirmed one.

    Surfer SEO prices by billing term: Standard runs $99/month billed yearly or $119/month month-to-month; Pro is $182/month yearly or $219/month monthly; Peace of Mind is $299/month yearly or $359/month monthly. Surfer’s own homepage still marks its autonomous “Surfy Agent” as “(soon)” – the current product is human-in-the-loop content optimization, not an unattended agent, which matters if cost comparisons assume it replaces a person’s time.

    Writesonic’s plans – Starter $79/month, Basic $199/month, Growth $399/month, all billed annually – have shifted since we last checked: the product is now scoped around tracking brand visibility across ChatGPT, Gemini, and Google AI Overviews, bundled with a monthly article-writing allowance, rather than pure long-form SEO content generation.

    The hidden costs nobody itemizes

    None of the prices above include the time cost of running any of these systems, and that cost is real regardless of which model you pick – it’s the same gap between “can run unattended” and “must not publish without review” we mapped in WordPress SEO on autopilot.

    Review time. Every approval-gated system – ours included – needs a human to actually read what the agent proposes before it goes live. That’s not automatable without giving up the gate, and the gate is the point.

    Credential setup. Wiring an agent into WordPress means creating scoped application-password credentials, and getting the scope right takes real trial and error. Our own editor-role app password can create, edit, and publish posts, but returns a flat HTTP 403 on anything requiring the `manage_options` capability, including purging our SEO plugin’s sitemap cache – a limit we hit and had to work around, not a hypothetical.

    Failures that eat time without showing up on an invoice. Our own operating log records a WordPress client that intermittently got blocked by our site’s own firewall because it didn’t send a standard browser User-Agent header – an hour of debugging for a one-line fix. None of that shows up in any tool’s pricing page, but it’s a real cost of running automation against a live site.

    A worksheet to compute your own number

    Four questions, in order, get you closer to a real number than any single average:

    1. Do you already have a content and technical SEO workflow, or are you starting from nothing? Starting from nothing pushes you toward an agency or a full-featured SaaS tool, since you’re also paying to build process, not just execute it.

    2. Who reviews what the AI proposes before it goes live? If the honest answer is “no one, it just publishes,” price in the cost of eventually fixing what goes wrong – that’s not a hypothetical for any system without an approval step.

    3. What’s your own time worth per hour, and how many hours a month will this actually take you? A $49.60/year plugin that costs you six unpaid hours a month is not cheaper than a $199/month tool that takes one.

    4. Are you comparing list prices to list prices? Annual vs. monthly billing alone can double the number you think you’re looking at, as several of the tools above show.

    There’s no honest single figure for “how much AI SEO costs.” There’s a DIY floor around a $20-$200 monthly model subscription plus your own time, a SaaS middle from roughly $50/year to $500+/month depending on scope, and an agency ceiling averaging north of $3,000/month by two independent surveys. Where you land depends on how much of the work you’re willing to do yourself, and whether you actually price in the hours you’ll spend reviewing what any of these systems produce.

    Frequently asked questions

    What’s the cheapest way to do AI SEO?

    A DIY AI agent on a base-tier model subscription – Claude Pro lists at $20/month – wired to your own CMS. It’s the cheapest line item, but it assumes you or someone you pay can build and maintain the integration, which is real, unpriced work.

    How much does an SEO agency typically charge per month?

    Ahrefs’ survey of 439 SEO providers found an average monthly retainer of $3,209, though the most common range across respondents was $501-$2,000. Clutch’s data, from over 65,000 SEO companies, puts the average close behind at $3,199/month.

    Do AI SEO tool prices include the human review time?

    No. Every price on a vendor’s pricing page covers the software, not the hours someone spends reviewing, approving, or fixing what it produces. That review time is real and doesn’t appear on any invoice we’ve seen.

    Why doesn’t this article give one exact number for what organic-os itself costs?

    Because we don’t have the full invoice on file to publish honestly. We can verify the model subscription’s public price range ($20-$200/month) and that our other connectors run on free tiers; we can’t verify our own hosting or domain cost from inside this pipeline, so we say that plainly instead of estimating a number.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that produced this article and publishes its own costs the same way it publishes everything else – verified, or labeled as unverified.

  • Automating Rank Math With an AI Agent: What Works Today

    Automating Rank Math With an AI Agent: What Works Today

    Rank Math is automatable in three layers through the WordPress REST API: per-post SEO fields (title, description, canonical URL, schema type) stored as rank_math_* post meta, read-back verification through Rank Math’s own getHead endpoint, and the account-level role that decides what any of this can touch. What is not automatable at all is the settings layer – sitemap configuration, global title templates, redirections, the 404 monitor – all of it lives in wp-admin, with no REST route Rank Math documents for changing it. We drive this site’s Rank Math setup through the first two layers every day, and we have hit real production failures doing it: an entity-encoding mismatch in read-verify checks, a WAF that blocks an unbranded HTTP client, and a sitemap cache that has frozen mid-publish more than once.

    We publish through this exact setup – the pipeline that wrote this article authenticates to WordPress the same way described below, and the failures further down are our own, pulled from our operating log, not hypothetical.

    Registering rank_math_* meta for REST (the bridge-plugin pattern)

    Rank Math stores everything it manages per post – title, description, focus keyword, canonical URL – as ordinary WordPress post meta, under keys like rank_math_title, rank_math_description, and rank_math_focus_keyword. That part is well documented. What is not is that Rank Math has no REST API of its own for writing to those fields. Its own support team states this plainly: they do not have a REST API to change the meta programmatically, and point developers instead at WordPress core’s own update_post_meta() function. One support thread references a write endpoint by name, but a separate, more direct ticket from the same team contradicts it – so we are not repeating the unconfirmed one.

    The practical path is a bridge: register each rank_math_* key yourself with WordPress core’s register_post_meta(), setting show_in_rest => true per key. Once registered, those fields appear on the standard meta object of /wp/v2/posts, readable and writable through the same authenticated request that creates or updates the post itself – no separate call, no separate plugin API. This is the pattern we run in production: our create-post payload sets rank_math_title, rank_math_description, rank_math_focus_keyword, and rank_math_canonical_url directly inside meta on a single POST /wp/v2/posts call.

    The trap sits one step earlier. If a given rank_math_* key was never registered with show_in_rest, WordPress does not reject the request or warn you. It silently drops the field. The API call still returns a clean success – the post gets created, the response comes back 200 or 201 – and the SEO fields are simply blank in wp-admin. Rank Math’s own support queue describes this exact failure: a post created successfully via REST, fields empty, no error to explain why. The only way to catch it is to read the field back after writing and check it landed, which is the next problem.

    Read-verify loops with getHead – and the entity-encoding trap

    Rank Math does ship one official REST endpoint, and it is read-only: getHead, under its Headless CMS Support module, at /wp-json/rankmath/v1/getHead?url=<full-url>. It takes a complete URL, not a slug, and returns the entire rendered <head> block Rank Math would output on that page – title tag, meta description, canonical, Open Graph, and the JSON-LD schema, all in one response. There is no per-field version; you get the whole block and parse out what you need.

    That makes getHead useful for a verification step after publish: fetch it, confirm the title and description actually rendered, confirm the canonical points where you expect. We ran this check while writing this article, and it surfaced a mismatch that has nothing to do with Rank Math and everything to do with how WordPress renders text. We compared a live post’s title through the authenticated, editor-context field (title.raw) against the same title through the public field (title.rendered). The raw field gave a plain straight apostrophe; the rendered field gave the same word with an HTML entity in its place – WordPress’s typography pass runs on .rendered but not .raw. A read-verify loop doing simple string comparison will flag a false mismatch on any title with an apostrophe, quotation mark, or dash. The fix: decode entities before comparing, or compare against .raw – but until you hit it once, the false alarm looks exactly like real drift.

    What broke for us in production

    Three failures, all logged as they happened, not reconstructed afterward.

    Sitemap cache freeze. Rank Math’s XML sitemap is cached, and on this site that cache has frozen outright – its lastmod timestamp stops advancing, and posts published after the freeze point are missing from the sitemap entirely, not just showing a stale date. This has recurred five separate times on this property, each time re-freezing at the timestamp of the most recent publish. We do not currently run a daily check on sitemap membership, so a freeze can sit undetected for days before a routine audit catches it.

    WAF blocks on the client, not the request. The account and the request can both be correct and still get blocked – our WordPress client using Python’s plain urllib with no custom User-Agent header intermittently drew an HTTP 403 from this site’s WAF, a Cloudflare-side block. Sending a browser-style User-Agent, or switching to a tool like curl that sets one by default, cleared it. The block was intermittent, which made it look like a flaky network issue the first time, not a client-identity problem.

    Role scoping, enforced at the API. The account our agent authenticates as can create, edit, and publish posts and set page-level SEO meta. It cannot purge the sitemap cache, because that needs the manage_options capability, deliberately withheld. Every attempt returns HTTP 403 rest_forbidden – not a partial success. We cover this role-inheritance limit in our application-passwords setup guide – the password inherits whatever the account can do, so the account, not the password, is the real control.

    When to stop automating and open wp-admin

    Everything covered so far – title, description, canonical, focus keyword, schema type – is per-post meta, which is what register_post_meta and getHead can reach. Rank Math’s site-wide settings are a different layer: general settings, sitemap configuration, global title and meta templates, schema defaults, the redirections list, the 404 monitor. None of it showed up as a REST route in anything Rank Math documents, and every support answer we reviewed pointed back to wp-admin, not an API call. That tracks with how WordPress core works generally – the REST API does not expose arbitrary plugin options tables by default, so a plugin has to deliberately build and document a settings-level route, and Rank Math has not done that for this layer.

    In practice, that is where we draw the line. We automate what happens on a per-post basis every time we publish – the repeatable, checkable part. We do not script sitewide settings changes, because there is no supported surface for it and no way to verify the change landed the way getHead lets us verify a post’s meta. When a change belongs at that level, someone opens wp-admin. This maps to the access tiers in our notes on AI SEO agent permissions: the further a change reaches from a single post toward site-wide configuration, the more it belongs to a human in the dashboard, not an agent making REST calls.

    None of this replaces the plugin-level checks we already run for on-page SEO across the rest of this site – the same plugin-can’t-do-everything gaps apply here, and the agent architecture underneath this pipeline is the same one behind how our Claude-based agent runs this site. Rank Math automation is one piece of that loop, not a separate system.

    Frequently asked questions

    Can you update Rank Math’s SEO fields through its own REST API?

    Not through a dedicated Rank Math write endpoint – their own support team says they don’t have one, and points to WordPress core’s update_post_meta() instead. In practice, that means registering each rank_math_* key with register_post_meta() and show_in_rest => true, so the fields become writable through the standard /wp/v2/posts meta object.

    What happens if a rank_math_* field isn’t registered for REST?

    The request does not fail. WordPress silently drops the field, the post still saves, and the Rank Math value is simply left blank – a pattern confirmed independently in Rank Math’s own support queue. The only reliable way to catch it is to read the field back after writing and confirm it landed.

    What is the getHead endpoint, and what does it return?

    getHead is Rank Math’s official read-only REST endpoint for headless setups, at /wp-json/rankmath/v1/getHead?url=<full-url>. It requires a complete URL rather than a slug and returns the entire rendered head block for that page – title, meta description, canonical, Open Graph tags, and JSON-LD schema together, with no per-field retrieval option.

    Can Rank Math’s site-wide settings be automated through the REST API?

    Not that Rank Math documents. Site-wide settings – sitemap configuration, global title templates, schema defaults, redirections, the 404 monitor – live in the wp-admin dashboard. Every automation mechanism Rank Math describes (registered post meta, update_post_meta, getHead) is scoped to individual posts, not sitewide configuration.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that drives this site’s Rank Math setup through the same registered post-meta pattern described above, and runs it in production here.

  • WordPress Application Passwords: Setup and Safe Scoping

    WordPress Application Passwords: Setup and Safe Scoping

    A WordPress application password is a 24-character credential, generated from a user’s own profile page, that lets an external tool authenticate to the REST API without ever touching that user’s real login password. Setup takes about two minutes: open Users → Profile, scroll to Application Passwords, type a name for the integration, and click Add New Application Password. WordPress shows the password once, in full – copy it immediately, because only a hash is kept afterward. The part most setup guides skip is the limitation that actually matters: the password inherits the full role of the account that created it. There is no dial in WordPress core to hand out, say, publish-only access. You scope by account, not by password, and that single fact should decide how you set this up before you generate anything.

    We use application passwords every day. The agent that drafts and publishes articles on this site, including this one, authenticates to WordPress the same way described below – a dedicated account, a password that lives outside the repository, and a role that has already told us, more than once, exactly what it can’t do.

    What application passwords are, and the two-minute setup

    Application Passwords became a WordPress core feature in version 5.6, released December 2020. Before that, connecting an external script to the REST API generally meant sending a user’s real login password over Basic Authentication on every request – one credential, reused everywhere, with no way to revoke a single integration without changing the login for everything else too.

    The setup itself is short. Log in as the account the integration should act as, go to Users → Profile, and scroll to Application Passwords. Enter a descriptive name for what’s connecting – the name is a label for your own reference, not a permission – and click Add New Application Password. WordPress generates a 24-character string and displays it exactly once; only a hash is stored afterward, so if you lose it, the fix is to revoke that entry and generate a new one, not to go looking for it.

    A single account can hold several named application passwords at once, each tracked and revocable independently of the others and of the account’s normal login. WP-CLI exposes the same lifecycle from the command line, with a full wp user application-password command family: create, list, get, delete, exists for idempotent scripting, and record-usage.

    The role-inheritance limitation nobody mentions

    Here’s the part that catches people who set this up expecting fine-grained scopes: an application password carries the exact same capabilities as the WordPress user account that generated it. WordPress’s own developer documentation states it plainly – authenticating via an application password results in the same capabilities the user has who created it. There’s no field in the creation form for “read-only,” no checkbox for “posts but not settings,” no expiration date, no IP allowlist. The password is a second front door onto the same account. If that account is an administrator, the password can install plugins, add users, and change site settings, exactly like logging in with the real password would.

    WordPress core offers two developer-facing filters for site owners who want to restrict the feature itself, rather than an individual password: wp_is_application_passwords_available can turn the feature off entirely, and wp_is_application_passwords_available_for_user can restrict which roles or users are allowed to generate one at all. Neither one lets you take an already-issued password and trim what it can do after the fact. The scoping decision happens before you click “Add New Application Password,” at the account level – not after.

    One thing this limitation does not affect: two-factor authentication. Application password requests bypass the 2FA prompt specifically for that authenticated call, which means a human login can stay behind 2FA while a separate application password handles unattended API access – two different authentication paths on the same account, each doing its own job.

    Creating a scoped operator user instead of using admin

    Since the password can’t be scoped down after the fact, the actual scoping tool is the account you generate it from. WordPress’s own guidance is direct about this: create a dedicated user for the integration if at all possible, rather than issuing the password from an existing admin account. A deploy script, a content pipeline, or an analytics puller each gets its own login, with the lowest built-in role that still lets it do the job – Author if it only manages its own posts, Editor if it needs to touch other users’ content. None of them need Administrator, and most integrations never should have it.

    This isn’t a theoretical best practice for us – it’s how our own publishing pipeline is wired. Our site’s REST integration authenticates as a dedicated, non-admin account. That account’s role can create, edit, and publish posts and update page-level SEO fields through the REST API. It cannot purge a caching plugin’s sitemap cache, because that action requires the manage_options capability the account was never given – the request comes back as an HTTP 403 rest_forbidden error, every time, not a partial success. That’s the scoping decision doing exactly what it’s supposed to do: the integration can publish content and structurally cannot touch site-wide settings, no matter what instruction it’s given. A role that was never granted a capability is a wall a script can’t talk its way past.

    Rotation and revocation practice

    Because each application password is tracked and revoked independently, rotating one doesn’t require touching the account’s main password or any other integration’s credential. From Users → Profile, each entry shows its name and revokes with a single click; from the command line, wp user application-password delete removes one by its UUID, and accepts a comma-separated list for clearing several at once.

    A practical rotation habit: name each password specifically enough that a stale one is obvious months later – “content-pipeline-2026” tells you more at a glance than “integration” does – and revoke and reissue immediately whenever a credential might have been exposed: a leaked log, a shared screen, a laptop that changed hands. Because revoking one password has no effect on any other password or the account’s real login, rotation carries none of the “did I just break every other connected tool” risk that comes with changing a shared account password.

    Using them safely with agents and CI

    An application password used by an autonomous agent or a CI pipeline needs the same two rules that apply to any secret: it doesn’t live in code, and it doesn’t get more access than the job requires. In practice that means the credential is read from an environment variable or a secrets manager at runtime, never hardcoded in a script or committed to version control – our own publishing scripts read theirs from an environment variable set outside the repository, and the repository’s own ignore rules keep it from ever landing in git history by accident.

    The account-scoping question from earlier applies with more force once an agent is making the calls, not a human clicking a button. We’ve written in more detail about how we think about access tiers for an AI SEO agent and about the specific gated-approval setup behind the Claude-based agent that runs this pipeline – the application password is the mechanism underneath both, but the account it’s issued from, and what that account structurally cannot do, is the actual safety boundary. If you’re weighing what a WordPress-connected agent should be able to touch at all, start with what the agent is and isn’t before deciding what role to create the password under, and if you want a sense of what unattended API access actually generates in server logs once it’s running, our notes on reading crawler and bot traffic cover the adjacent monitoring side of the same access question.

    Frequently asked questions

    Do application passwords let you restrict what an integration can access?

    Not directly. An application password inherits the full role and capabilities of the WordPress user account that generated it – there’s no per-password scope, expiration, or IP restriction in WordPress core. The only way to limit access is to generate the password from a dedicated account that already holds a lower role, before you create the credential.

    How do you set up a WordPress application password?

    Log in as the account the integration should use, go to Users → Profile, scroll to Application Passwords, enter a name for the integration, and click Add New Application Password. WordPress displays the 24-character password once – copy it immediately, since only a hash is stored afterward and it can’t be viewed again.

    Can you revoke one application password without affecting others?

    Yes. Each application password is tracked and revoked independently, both from the profile page’s one-click revoke and from WP-CLI’s wp user application-password delete command. Revoking one has no effect on the account’s other application passwords or on its normal login password.

    Does an application password require HTTPS?

    Yes. WordPress requires the site to be correctly configured for HTTPS before it will make the Application Passwords feature available; on a site it doesn’t detect as HTTPS, the feature is disabled by default.


    About the author: Shivaa Tripathi leads digital and performance marketing at Exotel, focused on demand generation, martech, and applying AI to SEO. He built organic-os, the AI SEO agent that authenticates to this site through the same kind of scoped WordPress application password described above, and runs it in production here.