How Autumn Billing Enforces Per-Organization Credit Balances in Open-SEO

Autumn billing enforces per-organization credit balances by mapping each organization to a distinct Autumn customer and storing monthly and top-up credits as separate feature balances, then atomically deducting credits through a gating pipeline that verifies balances before allowing usage-consuming operations.

The open-seo repository implements a robust billing system using Autumn to manage per-organization credit balances independently. Every organization operates as an isolated billing entity with separate credit pools for monthly allocations and rolled-over top-ups. The enforcement mechanism relies on distinct Autumn feature IDs and a strict verification pipeline defined in the billing modules.

Credit Balance Architecture

Autumn stores per-organization credit balances as two distinct feature IDs defined in [src/shared/billing.ts]:

  • usage_credits (AUTUMN_SEO_DATA_BALANCE_FEATURE_ID): Represents monthly shared usage credits allocated to the organization
  • topup_credits (AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID): Represents rolled-over top-up credits purchased separately

This dual-balance approach allows the system to prioritize monthly credits before consuming top-up reserves. Each organization ID maps directly to an Autumn customerId, ensuring complete isolation between organizations' financial data.

The Enforcement Pipeline

The billing system implements a four-stage enforcement pipeline within [src/server/billing/subscription.ts] to gate credit-consuming operations like DataForSEO lookups and LLM queries.

Step 1: Customer Provisioning

Before any balance checks occur, the system ensures the organization exists as an Autumn customer using getOrCreateOrganizationCustomer (lines 34-64). This function lazily creates the customer record and caches the existence check using a customerEnsuredKey to eliminate redundant round-trips on subsequent calls.

const { id: customerId } = await getOrCreateOrganizationCustomer({
  organizationId: orgId,
  userEmail: user.email,
  userId: user.id,
});

Step 2: Balance Verification with Double-Read Protection

The system retrieves current balances through getUsageCreditsRemaining (lines 98-44), which executes parallel autumn.check calls for both feature IDs. If the monthly balance read fails transiently, the system retries once, treating missing monthly balances as internal errors since every hosted organization must maintain this feature.

To prevent stale read errors from locking out paying customers, checkUsageCreditsDepleted (lines 58-93) implements a double-read pattern. The function first sums the remaining credits; if the sum is ≤ 0, it performs a second authoritative read via autumn.customers.getOrCreate before confirming depletion. The assertUsageCreditsAvailable wrapper (lines 106-22) throws INSUFFICIENT_CREDITS when both balances exhaust.

const { depleted } = await checkUsageCreditsDepleted({
  organizationId: orgId,
  userEmail: user.email,
  userId: user.id,
});
if (depleted) {
  throw new Error("Your credit balance is exhausted.");
}

Step 3: Atomic Credit Deduction

Upon successful usage, trackUsageCreditSpend (lines 23-104) deducts credits atomically using autumn.track. The function converts USD costs to credits using AUTUMN_SEO_DATA_CREDITS_PER_USD, applies the platform markup (SEO_DATA_COST_MARKUP), and rounds via roundUsdForBilling.

The deduction logic prioritizes monthly credits:

  1. Deduct from monthly credits up to the current remaining balance (clamped to 0)
  2. Apply remainder to top-up credits

Two separate autumn.track calls execute the deductions, utilizing retry options defined in [src/server/billing/autumn.ts]. After tracking, the system emits a usage:credits_consume PostHog event for analytics.

await trackUsageCreditSpend({
  customer: { organizationId: orgId, userEmail: user.email, userId: user.id },
  customerId,
  creditFeature: "seo_data_usage",
  costUsd: rawDataforseoCost,
  monthlyRemaining: monthlyCreditsLeft,
});

Error Handling and Observability

When balance reads return unexpected states, the system logs billing.credits-gate disagreement errors and proceeds with the authoritative customer object. All denial events route through captureServerEvent for operational observability (lines 80-90).

Key Implementation Files

The billing system spans several critical files that maintain per-organization credit balances:

File Role
[src/shared/billing.ts] Defines AUTUMN_SEO_DATA_BALANCE_FEATURE_ID, AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID, and conversion constants
[src/server/billing/autumn.ts] Lazy-loads the autumn-js SDK and provides the façade interface (check, track, customers.getOrCreate)
[src/server/billing/subscription.ts] Core logic for customer creation, balance gating, and credit tracking (lines 23-104, 106-22)
[src/server/billing/subscription.test.ts] Unit tests mocking Autumn interactions to verify balance enforcement
[src/server/workflows/RankCheckWorkflow.ts] Production workflow example implementing credit gates before rank checks

Summary

  • Autumn billing maintains isolated credit pools for each organization by mapping organization IDs to unique Autumn customer records.
  • Two distinct feature balances—usage_credits and topup_credits—store monthly and rolled-over credits respectively in [src/shared/billing.ts].
  • The enforcement pipeline uses getOrCreateOrganizationCustomer to provision customers lazily while caching the result.
  • Double-read protection in checkUsageCreditsDepleted prevents false depletion errors by verifying negative balances against the authoritative customer object.
  • Atomic deductions via trackUsageCreditSpend prioritize monthly credits before consuming top-up balances, applying platform markup and USD-to-credit conversions.
  • All credit consumption routes through [src/server/billing/subscription.ts] with comprehensive error handling and PostHog analytics integration.

Frequently Asked Questions

How does the system prevent race conditions when deducting credits?

The system relies on Autumn's atomic track operations through the autumn.track API calls in trackUsageCreditSpend. While the application calculates the split between monthly and top-up credits locally, each deduction executes as a separate atomic operation against Autumn's backend, ensuring that concurrent requests for the same organization resolve correctly according to Autumn's internal consistency guarantees.

What happens if the monthly credit balance read fails?

The getUsageCreditsRemaining function implements a single retry mechanism for transient read errors. If the monthly balance remains unreadable after retry, the system treats this as an internal error rather than assuming zero balance, because every hosted organization must maintain the usage_credits feature. The error propagates up to prevent the request from proceeding on stale data.

Why are credits deducted from monthly balances before top-up balances?

The deduction priority ensures that organizations consume their time-bound monthly allocations before depleting permanently stored top-up credits. This expiration-style logic encourages usage of monthly recurring credits while preserving purchased top-up reserves for future billing cycles. The trackUsageCreditSpend function explicitly clamps monthly deductions to the current remaining balance before routing overflow to top-up credits.

Where is the USD-to-credits conversion rate defined?

The conversion logic resides in [src/shared/billing.ts] via the constant AUTUMN_SEO_DATA_CREDITS_PER_USD. The trackUsageCreditSpend function applies this rate after adding the platform markup percentage (SEO_DATA_COST_MARKUP) and rounding the USD value, ensuring consistent credit valuation across all organizations in the Open-SEO platform.

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 →