You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Landing campaign trials (POST /me/landing-campaigns/start) currently have no run-count limit at any level. Budget control exists only per chat (landingCampaignMaxAgentTurns / landingCampaignMaxEstimatedTokens), and every new repo gets a fresh chat with a fresh budget. This leaves the official runtime open to unlimited free compute:
A single budget can be unbounded. Default is 6 turns per chat, but landingCampaignMaxEstimatedTokens defaults to undefined (unlimited tokens per turn). See packages/shared/src/config/ growth config.
Switching repos is free and scriptable. The trial does not require owning the target repo — any public GitHub repo URL works, and each distinct repo mints a new chat with a full budget (ensureTrialChatAndBootstrap keys the chat on campaign + repo.canonicalKey, packages/server/src/services/landing-campaigns/start.ts).
Per-org limits alone would be useless.selfCreateOrganization (packages/server/src/services/membership.ts:596) lets any user self-serve unlimited orgs, so the only meaningful identity anchor is the user (GitHub account via OAuth).
There is no rate limiting anywhere in the server, and organizations.maxMessagesPerMinute is a dead column (never enforced). The official runtime is a single shared client, so trial flooding is an availability problem for legitimate users, not just a billing problem.
Plan (deliberately minimal)
Three controls. Two count queries + three config values. No new tables, no middleware, no UI work.
1. Give the token cap a real default
Change landingCampaignMaxEstimatedTokens from undefined (unlimited) to a real number. Calibrate from one real production-scan run × 1.5. One-line change in the shared config schema.
2. Quota assertion: one function, two counts
Add assertTrialQuota(db, config, userId) to packages/server/src/services/landing-campaigns/guards.ts (existing assertion-helper file — do not create a new file):
Per-user daily cap (default 5/day): count today's trial chats created by this user across all orgs — chats.creatorAgentId IN (SELECT agentId FROM members WHERE userId = :userId) AND metadata contains landingCampaignTrial AND createdAt >= today. Must be cross-org (see problem refactor: migrate project scaffold from Python to TypeScript #3).
Global daily circuit breaker (default 100/day, deployment-tunable): count all trial chats created today. This is the only bound against sybil (bulk GitHub accounts) and doubles as an ops kill-switch.
Call site defines the counting semantics: invoke it only in the create-new-chat branch of ensureTrialChatAndBootstrap, right before createChat. Idempotent replays (same repo returns the existing chat) and retry re-bootstraps never reach that branch, so they consume no quota — no extra checks needed.
On limit: throw the existing ForbiddenError with a clear message (e.g. "Daily trial limit reached — set up your own First Tree workspace to keep going"). No new error class, no 429 plumbing, no web changes — quickstart already surfaces API error messages.
Changes
File
What
Size
packages/shared/src/config/
landingCampaignMaxTrialsPerUserPerDay (default 5), landingCampaignMaxTrialsPerDay (default 100), real default for landingCampaignMaxEstimatedTokens
Repo-ownership requirement (caller must own the target repo) — the hardest anti-farming lever, but it kills the low-friction "paste a repo and try" motion. Hold in reserve; escalate only if post-launch data shows actual farming.
Priority
Should land on mainbeforefirst-tree-ai/launch-readiness-scan#27 merges — that merge takes the landing flow live (see the #1479/#1480 deploy chain), and this is the only spend bound on the trial funnel.
Problem
Landing campaign trials (
POST /me/landing-campaigns/start) currently have no run-count limit at any level. Budget control exists only per chat (landingCampaignMaxAgentTurns/landingCampaignMaxEstimatedTokens), and every new repo gets a fresh chat with a fresh budget. This leaves the official runtime open to unlimited free compute:landingCampaignMaxEstimatedTokensdefaults toundefined(unlimited tokens per turn). Seepackages/shared/src/config/growth config.ensureTrialChatAndBootstrapkeys the chat oncampaign + repo.canonicalKey,packages/server/src/services/landing-campaigns/start.ts).selfCreateOrganization(packages/server/src/services/membership.ts:596) lets any user self-serve unlimited orgs, so the only meaningful identity anchor is the user (GitHub account via OAuth).organizations.maxMessagesPerMinuteis a dead column (never enforced). The official runtime is a single shared client, so trial flooding is an availability problem for legitimate users, not just a billing problem.Plan (deliberately minimal)
Three controls. Two count queries + three config values. No new tables, no middleware, no UI work.
1. Give the token cap a real default
Change
landingCampaignMaxEstimatedTokensfromundefined(unlimited) to a real number. Calibrate from one real production-scan run × 1.5. One-line change in the shared config schema.2. Quota assertion: one function, two counts
Add
assertTrialQuota(db, config, userId)topackages/server/src/services/landing-campaigns/guards.ts(existing assertion-helper file — do not create a new file):chats.creatorAgentId IN (SELECT agentId FROM members WHERE userId = :userId)AND metadata containslandingCampaignTrialANDcreatedAt >= today. Must be cross-org (see problem refactor: migrate project scaffold from Python to TypeScript #3).Call site defines the counting semantics: invoke it only in the create-new-chat branch of
ensureTrialChatAndBootstrap, right beforecreateChat. Idempotent replays (same repo returns the existing chat) and retry re-bootstraps never reach that branch, so they consume no quota — no extra checks needed.On limit: throw the existing
ForbiddenErrorwith a clear message (e.g. "Daily trial limit reached — set up your own First Tree workspace to keep going"). No new error class, no 429 plumbing, no web changes — quickstart already surfaces API error messages.Changes
packages/shared/src/config/landingCampaignMaxTrialsPerUserPerDay(default 5),landingCampaignMaxTrialsPerDay(default 100), real default forlandingCampaignMaxEstimatedTokenspackages/server/src/services/landing-campaigns/guards.tsassertTrialQuotawith the two count queriespackages/server/src/services/landing-campaigns/start.tspackages/server/src/__tests__/landing-campaigns-start.test.tsExplicitly out of scope
chatsare fine at trial scale.Priority
Should land on
mainbefore first-tree-ai/launch-readiness-scan#27 merges — that merge takes the landing flow live (see the #1479/#1480 deploy chain), and this is the only spend bound on the trial funnel.