All documentation

Plans and limits

What is measured against your plan, what is simply on or off, and what happens the moment you cross either line.

Billing screen showing the current plan, prompt slot and project usage meters, and what the plan includes

What one plan covers

Every project you own draws on the same plan: the one tied to your account, not to that individual project. That is why the settings navigation puts Billing in its "Your account" group, alongside Account, Integrations and All projects, while the "This project" group holds Project, Team, Keywords, Tags, Activity and Exports: the plan sits above any single project, not inside one (frontend/src/routes/(app)/settings/+layout.svelte#groups). Prompt slots follow the same account-wide shape: they pool across every project you own together, counted once regardless of which of your projects a given prompt belongs to, not reset per project (api/services/limits.py#get_active_prompt_slot_count, #check_query_limit).

The checks below all resolve the plan from the project rather than from the person: they read Project.user_id for the project the request names and look the plan up from that, so what governs the work is the project owner's plan, not the plan of whoever happens to be looking at the screen. The competitor, team-member and article checks each begin with that lookup (api/services/limits.py#check_competitor_limit, #check_team_limit, #check_article_limit), the project-count re-check that runs when a paused project is resumed is handed the project's own owner id (api/routers/projects.py#update_project; the same check at creation time is handed the creator's id, who is that project's owner by definition), and CSV exports resolve the plan the same way (api/routers/exports.py#exports_index, #download_export, api/services/plan.py#get_user_plan). CSV exports make the effect concrete: a download is allowed or refused on the project owner's plan, and nothing in that check additionally looks at whether the person clicking download has write access to the project, so at the API level a viewer on someone else's paid project can download the same exports an editor or the owner could.

The prompt-slot check gets there by a slightly different route: it is told who the owner is instead of looking the owner up itself. It takes a user id and counts active prompt targets across the projects that user owns, so it is only a correct meter when the id handed to it is the project owner's (api/services/limits.py#check_query_limit, #get_active_prompt_slot_count). It has four call sites, and that is the whole set: creating a tracked prompt and editing one both pass the project's own user_id (api/routers/prompts.py#create_prompt, #update_prompt), accepting a suggested prompt reads the same field off the project it is accepting into (api/routers/prompt_candidates.py#accept_candidate), and creating a project during onboarding passes the caller, who is that project's owner because the project does not exist until the request succeeds (api/routers/projects.py#create_onboarding_project). So an editor adding a prompt to a project someone else owns spends the owner's slots against the owner's plan, which is the same account the prompt itself lands on.

So if you are an editor or a viewer on a project someone else owns, every limit and gate you meet on that project is the owner's, with no exception. Reaching that project's screens in the app is a separate question, and the answer is that you can: GET /projects returns the projects you were added to alongside the ones you created; see Team for what each role can then do once a screen is open.

What's metered

Five numeric limits live as plain integer columns on the plan itself, not as logic scattered through the code: active prompt slots, active projects, active competitors per project, team members, and articles generated per week. Each plan row, seeded at api/services/seed.py#DEFAULT_PLANS, sets its own value for each of the five. A column holding negative one turns that particular ceiling off entirely: the check that reads it returns immediately, so nothing on that plan is ever compared against it, let alone rejected (api/services/limits.py#check_project_limit, #check_query_limit, #check_competitor_total_limit, #check_team_limit, #check_article_limit). The actual figures for each plan, and which plan has which, live on Pricing and nowhere else in this section, so there is exactly one place to keep them current.

What each column governs, and where it is checked:

  • Prompt slots: active prompt targets, pooled across every project the project's owner owns, as described above. Checked before a slot is added, in every place one can be: creating a project during onboarding, creating or editing a tracked prompt (including turning a paused one back on), and accepting a suggested prompt (api/services/limits.py#check_query_limit, api/routers/projects.py#create_onboarding_project, api/routers/prompts.py#create_prompt, #update_prompt, api/routers/prompt_candidates.py#accept_candidate).
  • Projects: active projects you own. Checked before a new one is created, and checked again whenever a paused project is resumed; see below (api/services/limits.py#check_project_limit).
  • Competitors per project: active competitors tracked on one project. Checked per project, both when a project is first set up and whenever a competitor is added afterward (api/services/limits.py#check_competitor_limit, #check_competitor_total_limit).
  • Team members: checked whenever someone is added to a project's team, and again whenever a paused member's access is switched back on (api/routers/team.py#invite_member, #update_member). The check tallies the project's own active membership rows and compares that count against the column, with no extra seat added for the owner: the project's creator already holds one of those rows, written when the project is created (api/services/limits.py#check_team_limit, api/routers/projects.py#create_project). A cap therefore admits that many people in total, the owner among them, and the request is refused only once the seats are genuinely full rather than one short of it.
  • Articles per week: articles started in the current calendar week, Monday through Sunday, counting every article whose status is not failed. A failed attempt does not count against the week, so retrying one is free (api/services/limits.py#check_article_limit).

What's gated

Separate from the five metered columns, a set of capabilities are simply on or off for a plan, checked against a Python constant keyed on the plan's name rather than a database column. Every gate below is one a customer can actually reach from a screen in the app:

  • Weekly automatic prompt-suggestion refresh, the toggle that keeps the Suggestions queue topped up on its own. Turning it on is rejected unless your plan's discovery tier allows it; turning it back off is always allowed (api/services/prompt_discovery.py#_PLAN_TIERS, #_tier_for_plan, api/routers/projects.py#update_project).
  • On-demand "Suggest more prompts", read from the same discovery tier table as the toggle above, but as its own separate flag rather than tied to whether weekly refresh is on (api/routers/prompt_candidates.py#generate_more_candidates).
  • Weekly citation-gap opportunity generation, which runs as a background job rather than from a click. The entry plan's cap in this table is zero, so the weekly job for a project on that plan returns nothing and logs that it skipped, every week, indefinitely (api/services/citation_gap.py#_PLAN_LIMITS, #_limit_for_plan, #find_gaps_for_project).
  • On-demand citation-gap refresh, the button that runs the same underlying work immediately. This one is restricted to a single named plan through its own separate check, one that does not read the weekly cap table at all, so the button's availability and the weekly job's output are governed by two different rules that happen to describe the same feature (api/routers/citation_gaps.py#_require_on_demand, #refresh_project_citation_gaps).
  • Weekly page-optimization opportunity generation, a background job shaped like the citation-gap one above it: zero on the entry plan, a per-week cap on the rest. One difference between the two is worth knowing if you ever go looking in the logs: the citation-gap run records a line saying it skipped, while this one returns an empty list and records nothing at all (api/services/optimization/service.py#_optimization_limit, #score_optimization_opportunities_for_project).
  • On-demand page-optimization refresh, the Refresh button on the Optimizations screen, for a whole project or for one prompt. Unlike the citation-gap button above, neither endpoint behind it reads a plan: both call the same generator, which returns nothing at all on a cap of zero, and both then report success with nothing produced. What keeps a person off that path is the screen, which does not draw the button on the entry plan (api/routers/optimizations.py#refresh_project_optimizations, #refresh_prompt_optimizations, api/services/optimization/service.py#run_optimization_for_prompt, frontend/src/routes/(app)/optimizations/+page.svelte#canRefresh).
  • Article generation, gated by the metered weekly column above, but a zero on that column is not a silent skip here: starting an article, whether from a citation-gap topic or one you typed yourself, is a request a person clicked, so a zero refuses it outright with a message instead of quietly producing nothing (api/services/limits.py#check_article_limit). Which drafting strategy the writer then uses for an article that is allowed to start, a research-grounded pass or a single one-shot draft, also varies by plan; see Articles for what that changes about the result.
  • Growth Advisor: both the manual "Refresh now" trigger and the weekly auto-refresh toggle reject the entry plan, by the same rule written out twice, in two separate checks whose messages differ only in their last few words (api/routers/growth_advisor.py#trigger_briefing, api/routers/projects.py#update_project).
  • llms.txt Advisor: the same shape as Growth Advisor, a manual trigger and a toggle, both rejecting the entry plan (api/routers/llms_txt_advisor.py#trigger_audit, api/routers/projects.py#update_project).
  • CSV exports: available once the project owner's plan carries any price above zero, checked as a number rather than a plan name. As described above, this reads the project owner's plan and does not check the downloading person's own role on the project (api/routers/exports.py#exports_index, #download_export).
  • Which AI engines actually run your prompts is plan-dependent too, through a different mechanism again: each plan is linked in the database to a set of engines, and only an engine linked to your plan, and still available at all, is ever queried on your behalf. Nothing here checks a plan name (api/services/plan.py#get_user_provider_ids).

What happens when you hit one

The five metered columns, and every gate checked from a toggle or from a button whose endpoint reads a plan, share one behavior: the request is refused outright, with a message, before anything happens. Nothing is created, nothing is half-applied.

Two things behave differently, and both can leave a screen empty with nothing said. The first is the two weekly background jobs. When the owner's plan resolves to a cap of zero for citation gaps or for page optimizations, the weekly run for that project returns nothing and moves on; no browser is waiting on it, so there is no request to fail and no message to show. The result looks identical to a week where the job ran and genuinely found nothing worth surfacing: an empty list either way (api/services/citation_gap.py#find_gaps_for_project, api/services/optimization/service.py#score_optimization_opportunities_for_project).

The second is the page-optimization Refresh button, which is a click with a browser waiting on it, and whose two endpoints read no plan at all: on a cap of zero the same generator returns nothing, and the request reports success with an empty result instead of a refusal. What keeps a person off that path is the Optimizations screen, which does not draw the button on the entry plan, so what you meet there is a missing control rather than a message (api/routers/optimizations.py#refresh_project_optimizations, #refresh_prompt_optimizations, frontend/src/routes/(app)/optimizations/+page.svelte#canRefresh). The citation-gap button is not shaped this way: it has its own plan check and answers a click with a refusal (api/routers/citation_gaps.py#_require_on_demand).

This is the most useful distinction on this page: whether a limit shows up as a message you can act on, or as a queue that quietly never fills.

The billing screen

Billing shows your plan's name and its monthly price, a status chip ("no card on file" in place of the raw status when nothing is subscribed), and one of three renewal states: renewing on a coming date, cancelling on a coming date and reverting to the entry plan then, or no renewal date at all because nothing is currently being charged (api/routers/billing.py#billing_page, frontend/src/routes/(app)/settings/billing/+page.svelte). When a plan change is already queued, a banner names the plan you are moving to and when it starts. If that plan's features have already been switched on ahead of the billing date, the banner also names which plan is still being billed until then; if they have not, it says only that billing stays where it is until that date. Either way it warns that an upgrade can trigger a small, temporary authentication charge from the payment gateway itself, separate from the plan's own price (api/routers/billing.py#_subscription_info, frontend/src/routes/(app)/settings/billing/+page.svelte#sub).

Underneath, prompt slots and projects render as real usage bars, used against total. When the plan's column carries no ceiling, the total reads as unlimited, the bar is drawn full rather than as a fraction, and the note under it says there is no limit on this plan (frontend/src/routes/(app)/settings/billing/+page.svelte#promptMeter, #projectMeter). Team members render only as a stated cap, not a bar. The number the endpoint reports as "used" for that row is a real count now, rather than one of two fixed values: it is the active membership rows on whichever project you own has the most of them, counted the same way the team-member check counts, which makes it the project sitting closest to the cap (api/routers/billing.py#_largest_team_size, #_subscription_info). The screen still does not draw it: that row reads as a cap and a link through to the Team page (frontend/src/routes/(app)/settings/billing/+page.svelte#sub). Note what the count is scoped to, if you ever see it through the API: projects you own, so a project you were only added to contributes nothing to it. Below that, a features list keyed to your plan's name, and a payments list capped at a fixed number of recent rows with a note when it is showing only the newest slice (api/routers/billing.py#PLAN_FEATURES, #billing_page).

Changing plans defaults in one direction only: moving to a plan that costs more takes effect immediately, and moving to one that costs less is scheduled for the end of the period you have already paid for, rather than applied right away. That is the rule the plan-picker always computes; the screen exposes no separate control to override it (api/services/billing.py#default_plan_change_mode). Cancelling follows the same end-of-period default: the request asks the payment gateway to cancel at the end of the current cycle first, and only cancels immediately when the gateway reports there is no cancellable current cycle left to defer to, or when the gateway no longer recognizes the subscription at all, in which case it is marked cancelled locally instead (api/routers/billing.py#cancel_subscription, #_cancel_gateway_subscription, #_is_no_current_cycle_error, #_is_gateway_subscription_missing). Cancelling again once a cancellation is already scheduled is refused rather than accepted a second time, and so is cancelling when there is no current subscription at all or when its status is one of the four the endpoint reads as leaving nothing to cancel: created, expired, completed or cancelled.

One more re-check belongs here even though it happens on a different screen: resuming a paused project runs through the exact same active-project check that creating a brand new one does, not a lighter version of it. If you paused a project specifically to get under your project limit, resuming it later can be refused the same way starting a new project would be, if you are still at that limit when you try (api/routers/projects.py#update_project, api/services/limits.py#check_project_limit).

  • Create your account: what a project is, how sign-in works, and how team roles are assigned before any of the limits above come into play
  • Suggestions and opportunities: the on-demand and weekly-refresh entitlements for prompt discovery, and what counts against a prompt slot
  • Citation gaps: the weekly opportunity generation this page describes as a background gate, from the screen where its results actually appear
  • Articles: the weekly article quota in practice, and how the drafting strategy itself changes by plan

Last verified 2026-08-11

Start monitoring your AI visibility.

See how AI search engines talk about your brand.

Free to start. No credit card required.