# How the OpenSEO Onboarding Chat Agent Handles New User Workflows

> Discover how the OpenSEO onboarding chat agent manages new user workflows using Cloudflare Durable Objects, SQLite, and credit metering. Learn about free limits and streamed LLM responses.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-07-27

---

**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.

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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.

```tsx
// 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`](https://github.com/every-app/open-seo/blob/main/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 `questionCount` for every user message. If the count exceeds `FREE_ONBOARDING_QUESTION_LIMIT` (set to 7 in [`src/shared/onboardingChat.ts`](https://github.com/every-app/open-seo/blob/main/src/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/onboardingChatTools.ts). This function merges:

- **Core site tools**: `read_website`, `get_seo_metrics`, and `research_keywords`
- **Market analysis tools**: Competitor SERP analysis and backlink research (defined in [`onboardingMarketTools.ts`](https://github.com/every-app/open-seo/blob/main/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.

```typescript
// 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.

```typescript
// 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) via `buildOnboardingTools`.
- **Credit metering** occurs in the `onFinish` callback, 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.