How the OpenSEO Onboarding Chat Agent Handles New User Workflows
The OpenSEO onboarding chat agent uses a Cloudflare Durable Object to persist conversations in SQLite, enforces a 7-question free limit, and streams LLM responses while metering credit usage against the organization's balance.
The onboarding chat agent in the every-app/open-seo repository guides new users through a bounded, interactive SEO strategy session. It combines a React frontend with a stateful Durable Object backend to create a persistent, credit-gated conversation that can read websites, fetch metrics, and analyze competitors. This architecture ensures that free-plan users receive a trial experience while the platform accurately tracks resource consumption.
Client-Side Initialization
The workflow begins when a new user opens the onboarding dialog in the React UI. The client first calls the server function getOnboardingChatState to retrieve the user’s default project identifiers.
// src/serverFunctions/onboardingChat.ts
export const getOnboardingChatState = createServerFn({ method: "GET" })
.middleware(requireAuthenticatedContext)
.handler(async ({ context }) => {
const [project] = await ProjectService.listProjectsEnsuringOne(
context.organizationId,
);
if (!project) throw new AppError("NOT_FOUND");
return { projectId: project.id, domain: project.domain };
});
Once the projectId is available, the OnboardingChat.tsx component creates an AI-chat agent bound to that specific project. The useAgent hook attaches to a unique Durable Object instance, ensuring that each project maintains its own isolated conversation state.
// src/client/features/onboarding/OnboardingChat.tsx
const stateQuery = useQuery(onboardingChatStateQueryOptions());
const { projectId } = stateQuery.data!;
const chatAgent = useAgent({ name: projectId });
Durable Object Orchestration
At the heart of the system lies the OnboardingChatAgent class, a Cloudflare Durable Object defined in src/server/features/onboarding/OnboardingChatAgent.ts. One Durable Object is instantiated per projectId, and it persists all chat messages in its own SQLite database via this.messages. This guarantees conversation continuity even if the user refreshes the page or disconnects.
The agent initializes by loading the project context and preparing the message history. It then enters a request-processing loop that handles incoming user messages, applies gating logic, and orchestrates LLM responses.
Enforcing Free Limits and Credit Checks
Before invoking the language model, the agent validates entitlement. Two layers of gating protect the system from abuse:
- Free-question limit – The agent increments a
questionCountfor every user message. If the count exceedsFREE_ONBOARDING_QUESTION_LIMIT(set to 7 insrc/shared/onboardingChat.ts) and the organization lacks managed access, the agent returns a static "Subscribe to continue" response without calling the LLM. - Credit balance verification – In hosted mode, the agent checks the organization's onboarding credit balance via
checkUsageCreditsDepleted. If credits are exhausted, subsequent turns abort early.
These checks occur in the OnboardingChatAgent request handler, ensuring that resource-intensive operations are never triggered for unauthorized users.
Building the System Prompt and Tool Set
When a request passes gating, the agent constructs a rich system prompt that defines the persona "Sam," answer formatting rules, and tool usage policies. The prompt embeds the OpenSEO fact sheet (openseo-fact-sheet.md) to ground responses in product-specific knowledge.
The agent then assembles a comprehensive tool set via buildOnboardingTools in src/server/features/onboarding/onboardingChatTools.ts. This function merges:
- Core site tools:
read_website,get_seo_metrics, andresearch_keywords - Market analysis tools: Competitor SERP analysis and backlink research (defined in
onboardingMarketTools.ts)
The tool context includes the project object, billing customer ID, and a DataForSEO client, ensuring every external API call is metered against the onboarding credit feature.
// src/server/features/onboarding/onboardingChatTools.ts
const tools = buildOnboardingTools({
project,
billingCustomer,
dataForSeoClient
});
Streaming Responses and Metering Usage
With the prompt and tools ready, the agent invokes streamText from the AI SDK. The configuration caps output at 4000 tokens and stops generation after 5 reasoning steps to control costs.
// src/server/features/onboarding/OnboardingChatAgent.ts
const result = streamText({
model,
system: buildSystemPrompt(project.domain),
messages: await convertToModelMessages(this.messages),
maxOutputTokens: 4000,
stopWhen: stepCountIs(5),
tools,
onFinish: async (event) => {
// Credit metering
if (hostingMode === "cloudflare") {
await trackUsageCreditSpend({
feature: "onboarding_chat",
amountUsd: event.openRouterCostUsd,
organizationId: project.organizationId,
});
}
// Persist assistant turn
await onFinish(event);
},
});
The onFinish callback performs critical housekeeping. It tallies the per-step cost (openRouterCostUsd) and records the spend via trackUsageCreditSpend in hosted environments. Finally, it persists the assistant's response to the Durable Object's SQLite store, ensuring durability.
Error Handling and Data Persistence
The agent implements resilient persistence logic. If SQLite write operations fail during persistMessages, the system retries up to three times before surfacing an error. Stream-level errors are caught and presented to the user as friendly fallback messages rather than raw stack traces.
Additionally, separate server functions handle configuration updates. When a user selects their domain and location, saveOnboardingSite validates the location code and updates the projects table, ensuring the agent always operates against current project settings.
Summary
- The onboarding chat agent is implemented as a Cloudflare Durable Object (
OnboardingChatAgent) with one instance per project. - Conversations persist in SQLite within the Durable Object, surviving page reloads.
- A 7-question free limit (
FREE_ONBOARDING_QUESTION_LIMIT) and credit balance checks prevent abuse by unauthenticated or depleted accounts. - The agent assembles core and market SEO tools (
read_website,get_seo_metrics, competitor analysis) viabuildOnboardingTools. - Credit metering occurs in the
onFinishcallback, tracking actual LLM costs against the organization's onboarding credit balance. - Retry logic (up to 3 attempts) protects against transient SQLite failures during message persistence.
Frequently Asked Questions
What happens when a free user exceeds the 7-question limit?
When questionCount exceeds FREE_ONBOARDING_QUESTION_LIMIT (7) and the organization lacks managed access, the agent immediately returns a static "Subscribe to continue" response. It does not invoke the LLM or consume additional credits, effectively gating the conversation until the user upgrades.
How does the onboarding chat agent persist conversations across page reloads?
The agent uses a Cloudflare Durable Object with a dedicated SQLite instance (this.messages). Every user and assistant turn is written to this store via persistMessages. When the user reconnects, the React client reattaches to the same Durable Object using useAgent({ name: projectId }), loading the full conversation history from SQLite.
Which SEO tools are available during the onboarding chat?
The agent exposes a merged tool set including read_website (web scraping), get_seo_metrics (performance data), research_keywords (keyword suggestions), and market analysis tools for competitor SERP and backlink research. These are assembled in buildOnboardingTools and receive context from the DataForSEO client.
How is credit usage tracked during the conversation?
Credit metering occurs in the onFinish callback after each LLM turn. The agent extracts openRouterCostUsd from the streaming result and calls trackUsageCreditSpend to deduct the actual dollar cost from the organization's onboarding credit balance. This only executes in hosted mode; self-hosted instances bypass credit checks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →