How Billing Credit Tracking and Usage Metering Work in OpenSEO: A Technical Deep Dive

OpenSEO implements a unified billing credit and usage-metering system that tracks organizational credit balances through a centralized database schema, propagates billing context via BillingCustomerContext objects, and wraps expensive external API calls in billing envelopes that deduct credits upon task completion.

OpenSEO (from the every-app/open-seo repository) handles premium feature access through a sophisticated credit-based system that ensures accurate usage tracking across rank checking, keyword research, and site audits. The architecture separates concerns between database storage, context propagation, and feature-level metering to maintain accurate financial records while supporting both SQLite and PostgreSQL backends. This system ensures that every DataForSEO API call translates into precise credit deductions from the organization's balance.

Database Schema for Credit Storage

The foundation of OpenSEO's billing system rests in the billing_customer_status table, defined in src/db/billing.schema.ts for SQLite and mirrored in src/db/pg/billing.schema.ts for PostgreSQL deployments.

This table stores the ground truth for each organization's financial state:

  • credits or remaining_credits: The current available balance for the organization
  • usage_usd: Cumulative spending across all features
  • plan: The subscription tier (e.g., "free", "pro", "enterprise")

The schema supports multi-tenant isolation through organizationId, ensuring that credit balances remain strictly separated between different organizations using the platform.

The BillingCustomerContext Propagation Pattern

Every authenticated request in OpenSEO initializes a billing context through the buildBillingCustomer function in src/server/mcp/project-auth.ts. This function queries the billing_customer_status table and constructs a BillingCustomerContext object containing:

  • userId: The authenticated user's identifier
  • organizationId: The organization scope for the request
  • remainingCredits: Current credit balance fetched from the database
  • plan: The organization's subscription tier

This context object propagates through the entire request lifecycle, passing from server functions into workflows like RankCheckWorkflow.ts and SiteAuditWorkflow.ts. By threading the billing context through function signatures rather than relying on global state, OpenSEO ensures that every credit-consuming operation has explicit access to the organization's current financial standing.

Feature-Level Metering with Billing Envelopes

OpenSEO meters usage at the feature level through a billing envelope pattern implemented in src/server/lib/dataforseo/serp.ts. When a premium feature (such as rank checking or brand lookup) initiates a DataForSEO API call, the system constructs a billing envelope containing:

  • costUsd: The calculated cost based on the feature's cost profile
  • feature: The feature identifier (e.g., "serp", "brand-lookup")
  • path: Hierarchical identifiers for the specific operation

Cost calculations reference configurable profiles defined in scripts like scripts/brand-lookup-cost-profile.ts and scripts/backlinks-cost-profile.ts, which map DataForSEO endpoints to per-unit USD costs. The envelope attaches to the task payload under the billing key, ensuring that cost metadata travels with the request through external API execution.

The internal autumn billing service (referenced throughout the workflow files) consumes these envelopes to record usage events before and after expensive operations complete.

Credit Deduction and Persistence Flow

The billing credit tracking system follows a strict five-step workflow to maintain accurate balances:

  1. Context Creation: buildBillingCustomer in src/server/mcp/project-auth.ts fetches the current billing_customer_status row and creates the BillingCustomerContext.

  2. Feature Invocation: Premium server functions (such as those in src/server/workflows/RankCheckWorkflow.ts) receive the billing context and initiate metered operations.

  3. Envelope Construction: The DataForSEO wrapper computes costs using buildTaskBilling in src/server/lib/dataforseo/serp.ts, calculating costUsd by multiplying the task's item count by the unit cost from the relevant cost profile.

  4. Post-Task Accounting: Upon completion, src/serverFunctions/billing.ts validates the usage range and executes SQL updates to increment usage_usd and decrement remaining_credits in the billing_customer_status table.

  5. UI Exposure: The front-end queries the /billing endpoint (defined in src/shared/billing.ts) to retrieve current credit balances, usage history, and plan limits for display in the user interface.

Code Implementation Examples

Creating the Billing Context

The authentication layer constructs the billing context by querying the organization's current status:

// src/server/mcp/project-auth.ts
export async function buildBillingCustomer(
  auth: AuthInfo,
  projectId: string,
) {
  const organizationId = await getOrgIdFromAuth(auth);
  const status = await db
    .select()
    .from(billingCustomerStatus)
    .where(eq(billingCustomerStatus.organizationId, organizationId))
    .then(row => row[0]);

  return {
    userId: auth.userId,
    organizationId,
    remainingCredits: status?.credits ?? 0,
    plan: status?.plan ?? "free",
  };
}

Wrapping Tasks with Billing Metadata

The DataForSEO integration attaches cost calculations to each outgoing request:

// src/server/lib/dataforseo/serp.ts
function buildTaskBilling(task: DataForSeoTask): BillingEnvelope {
  const costUsd = costProfile[task.type] * task.itemCount;
  return {
    costUsd,
    path: [task.type, ...task.params],
    feature: task.type,
  };
}

// When sending the request:
const client = createDataforseoClient(billingCustomer);
const result = await client.runSerp({
  ...payload,
  billing: buildTaskBilling(payload),
});

Recording Usage After Completion

The billing endpoint updates the database atomically to reflect consumed credits:

// src/serverFunctions/billing.ts
export async function recordUsage(
  ctx: BillingCustomerContext,
  usage: { costUsd: number; feature: string },
) {
  await db
    .update(billingCustomerStatus)
    .set({
      usage_usd: sql`usage_usd + ${usage.costUsd}`,
      remaining_credits: sql`remaining_credits - ${usage.costUsd}`,
    })
    .where(eq(billingCustomerStatus.organizationId, ctx.organizationId));
}

Exposing Billing Data to the Frontend

Server functions return current balances for UI rendering:

// src/serverFunctions/billing.ts
export async function getBillingInfo(ctx: BillingCustomerContext) {
  const status = await db
    .select()
    .from(billingCustomerStatus)
    .where(eq(billingCustomerStatus.organizationId, ctx.organizationId));

  return {
    credits: status[0].remaining_credits,
    usageUsd: status[0].usage_usd,
    plan: status[0].plan,
  };
}

Frontend Consumption

The React frontend consumes billing data through the shared route constant:

// Frontend usage with TanStack Query
const { data } = useQuery(['billing', orgId], () =>
  fetch('/billing').then(r => r.json())
);

Summary

  • Unified Schema: The billing_customer_status table in src/db/billing.schema.ts stores credit balances, usage totals, and plan tiers for each organization.
  • Context Propagation: BillingCustomerContext objects created in src/server/mcp/project-auth.ts thread billing metadata through every server function and workflow.
  • Envelope Pattern: src/server/lib/dataforseo/serp.ts wraps external API calls in billing envelopes that calculate USD costs using configurable profiles from the scripts/ directory.
  • Atomic Updates: src/serverFunctions/billing.ts executes SQL operations to decrement remaining_credits and increment usage_usd after task completion.
  • Configurable Features: src/shared/billing-credit-features.ts defines which features consume credits and at what rates, allowing flexible plan configurations.

Frequently Asked Questions

How does OpenSEO calculate the cost for each API request?

OpenSEO calculates costs using cost profiles defined in scripts like scripts/brand-lookup-cost-profile.ts and scripts/backlinks-cost-profile.ts. When building a DataForSEO task in src/server/lib/dataforseo/serp.ts, the buildTaskBilling function multiplies the task's itemCount by the unit cost defined in the profile for that specific endpoint. This calculated costUsd value travels with the request in the billing envelope and gets persisted upon completion.

What happens if an organization runs out of credits during a workflow?

The buildBillingCustomer function in src/server/mcp/project-auth.ts fetches the current remaining_credits value at the start of each request. Workflows like RankCheckWorkflow.ts receive this context and can validate available credits before initiating expensive operations. If credits are insufficient, the system prevents the task from entering the billing envelope stage, effectively blocking the operation before incurring external API costs.

Can the billing system handle both SQLite and PostgreSQL deployments?

Yes. OpenSEO maintains schema definitions in both src/db/billing.schema.ts (SQLite) and src/db/pg/billing.schema.ts (PostgreSQL). The billing utilities in src/shared/billing.ts and src/serverFunctions/billing.ts use Drizzle ORM queries that abstract the underlying database dialect, ensuring consistent credit tracking behavior regardless of the chosen persistence layer.

How does the frontend know when to display credit warnings?

The frontend queries the /billing endpoint—defined via the BILLING_ROUTE constant in src/shared/billing.ts—which executes getBillingInfo from src/serverFunctions/billing.ts. This endpoint returns the organization's remaining_credits, usage_usd, and plan tier. The UI can then compare current usage against plan limits using the feature definitions in src/shared/billing-credit-features.ts to trigger warning states or upgrade prompts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →