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

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).
Related
- 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