# How OpenSEO Handles Billing and Credits for API Calls: A Complete Technical Guide

> Discover how OpenSEO manages API call billing and credits. Learn about the dual-pool system, usage tracking via trackUsageCreditSpend, and subscription management for your organization.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: technical-guide
- Published: 2026-09-06

---

**OpenSEO uses a dual-pool credit system powered by the Autumn subscription service, where organizations spend from monthly usage credits first, then top-up credits, with every API call tracked through `trackUsageCreditSpend` in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts).**

OpenSEO's billing infrastructure is tightly integrated with its API consumption model. Whether you're running DataForSEO requests or LLM-powered onboarding calls, every paid operation draws from a shared usage-credit pool. This guide explains exactly how the system manages customer accounts, feature flags, credit balances, and spend tracking based on the every-app/open-seo source code.

---

## Autumn Customer Representation

Every organization using OpenSEO is represented as an **Autumn customer**. The organization ID serves directly as the Autumn `customerId`, creating a 1:1 mapping between your OpenSEO org and the billing system.

The helper `getOrCreateOrganizationCustomer` in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) handles this lazily—creating the customer on first use and caching the existence flag for 24 hours to minimize API round-trips:

```typescript
import { getOrCreateOrganizationCustomer } from "@/server/billing/subscription";

const { id: customerId } = await getOrCreateOrganizationCustomer({
  organizationId: orgId,
  userEmail: user.email,
  userId: user.id,
});
// Customer created only if not exists; cached for 24h

```

This pattern ensures that billing operations never fail due to missing customer records while keeping latency low for repeated calls.

---

## Billing Feature Flags and Constants

All billing-related identifiers are centralized in [`src/shared/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/billing.ts). These constants define which Autumn features control access and credit pools:

| Constant | Purpose |
|----------|---------|
| `AUTUMN_PAID_PLAN_FEATURE_ID` | Indicates active paid subscription |
| `AUTUMM_MANAGED_ACCESS_FEATURE_ID` | Grants managed service access |
| `AUTUMN_SEO_DATA_BALANCE_FEATURE_ID` | Monthly usage credits pool |
| `AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID` | Purchased top-up credits pool |

The file also defines the credit economics:

```typescript
// src/shared/billing.ts
export const AUTUMN_SEO_DATA_CREDITS_PER_USD = 1000;  // 1000 credits = $1
export const SEO_DATA_COST_MARKUP = 1.28;              // 28% platform markup

```

The **markup** is applied to raw provider costs (like DataForSEO) before credit conversion, ensuring platform sustainability.

---

## Credit Pool Architecture

OpenSEO maintains **two distinct credit pools** per organization:

1. **Monthly balance (`usage_credits`)** – Refills automatically each billing cycle; primary spending source
2. **Top-up balance (`topup_credits`)** – Purchased manually; acts as overflow when monthly credits deplete

This dual-pool design provides predictable baseline usage with flexible overflow protection.

### Checking Credit Availability

Before any paid API call, the system verifies sufficient credits exist. The `checkUsageCreditsDepleted` function (lines 58–92 in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts)) implements defensive logic:

```typescript
// Simplified flow from source
const check = await autumn.check({
  customerId: orgId,
  featureId: AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
});

const monthly = check?.qty || 0;
const topupCheck = await autumn.check({
  customerId: orgId, 
  featureId: AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
});
const topup = topupCheck?.qty || 0;

if (monthly + topup <= 0) {
  // Double-check with full customer read to guard against stale cache
  const fullCustomer = await autumn.customers.get(orgId);
  // ...verify depletion and emit usage:credits_gate_refused event
}

```

If the initial read shows zero or negative balance, a **second full-customer read** guards against transient read errors before blocking the operation.

For simpler cases, `assertUsageCreditsAvailable` (lines 10–21) provides a throwing wrapper:

```typescript
import { assertUsageCreditsAvailable } from "@/server/billing/subscription";

await assertUsageCreditsAvailable(orgId); 
// Throws INSUFFICIENT_CREDITS if both pools are empty

```

---

## Recording API Spend with trackUsageCreditSpend

When an API call completes, `trackUsageCreditSpend` handles the actual credit deduction. This is the core billing integration point called throughout the codebase.

### The Spend Tracking Flow

1. **Apply markup** – `applyBillingMarkupUsd` multiplies raw cost by `SEO_DATA_COST_MARKUP`
2. **Convert to credits** – USD amount × `AUTUMN_SEO_DATA_CREDITS_PER_USD`
3. **Deduct from monthly first** – Primary pool depletion
4. **Deduct remainder from top-up** – Only if monthly exhausted
5. **Emit tracking events** – PostHog `usage:credits_consume` for analytics

```typescript
import { trackUsageCreditSpend } from "@/server/billing/subscription";
import { AUTUMN_SEO_DATA_BALANCE_FEATURE_ID } from "@/shared/billing";

await trackUsageCreditSpend({
  customer: { 
    organizationId: orgId, 
    userId: user.id, 
    userEmail: user.email 
  },
  customerId: orgId,
  creditFeature: AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
  costUsd: rawProviderCost,           // e.g., $0.05 from DataForSEO
  monthlyRemaining: monthlyBalance,   // From prior checkUsageCreditsDepleted
  properties: { 
    requestId: "abc-123",
    endpoint: "serp/google/organic" 
  },
});

```

The function automatically handles split deductions—charging monthly credits first, then top-up credits—making multiple `autumn.track` calls as needed.

---

## Real-World Integration Points

### DataForSEO Client

The DataForSEO integration in [`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts) (lines 8–12, 210–218) demonstrates production usage. After receiving API response costs, it:

1. Receives `BillingCustomerContext` containing org/user info
2. Calls `trackUsageCreditSpend` with actual provider charges
3. Passes through request metadata for audit trails

### Workflow Integrations

Site audit and rank-check workflows pass billing context through their execution phases:

- [`src/server/workflows/siteAuditWorkflowPhases.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowPhases.ts) – Gates audit runs on `assertUsageCreditsAvailable`
- [`src/server/workflows/RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/RankCheckWorkflow.ts) – Deducts credits per keyword checked

Both receive the same `BillingCustomerContext` shape, ensuring consistent attribution across all API-consuming operations.

---

## Summary

- **Autumn integration**: Organizations map 1:1 to Autumn customers via `getOrCreateOrganizationCustomer` with 24-hour caching
- **Dual credit pools**: Monthly usage credits spend first; top-up credits provide overflow protection
- **Defensive checking**: `checkUsageCreditsDepleted` double-reads on depletion to prevent false blocks
- **Markup and conversion**: 28% platform markup applied before converting USD to credits at 1000:1 ratio
- **Centralized spend tracking**: `trackUsageCreditSpend` handles all deductions, split spending, and analytics emission

---

## Frequently Asked Questions

### How does OpenSEO prevent API calls when credits run out?

The `assertUsageCreditsAvailable` function throws `INSUFFICIENT_CREDITS` before any paid operation begins. For additional safety, `checkUsageCreditsDepleted` performs a second full-customer read if the initial balance check shows zero credits, protecting against stale cache reads that might incorrectly allow spending.

### What happens when monthly credits are exhausted but top-up credits exist?

`trackUsageCreditSpend` automatically deducts from the monthly balance first, then charges the remainder against top-up credits. Both deductions trigger separate `autumn.track` calls with their respective feature IDs (`AUTUMN_SEO_DATA_BALANCE_FEATURE_ID` and `AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID`).

### Where is the billing markup configured?

The 28% platform markup is defined as `SEO_DATA_COST_MARKUP = 1.28` in [`src/shared/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/billing.ts). This constant is applied via `applyBillingMarkupUsd` before credit conversion, ensuring consistent pricing across all API providers.

### How is billing context passed through workflows?

All server-side API consumers receive a `BillingCustomerContext` object containing `organizationId`, `userId`, and `userEmail`. This context flows from workflow triggers through to `trackUsageCreditSpend` calls, enabling per-organization accounting with full user attribution for analytics.