# How DataForSEO API Billing and Rate Limiting Work in OpenSEO (2025 Implementation Guide)

> Learn how OpenSEO manages DataForSEO API billing and rate limiting in 2025. Explore credit tracking, pricing markup, and exponential backoff for 429 responses.

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

---

**OpenSEO separates authentication, billing, and rate-limit handling into distinct layers that track usage credits, apply pricing markup for hosted customers, and gracefully handle DataForSEO API 429 responses with exponential backoff.**

OpenSEO's DataForSEO integration provides a production-ready foundation for SEO data workflows. The platform abstracts the complexity of credit management, error classification, and retry policies behind a unified request wrapper. This guide breaks down the exact implementation in every-app/open-seo.

## DataForSEO API Architecture Overview

OpenSEO routes all DataForSEO calls through a **centralized authentication and request wrapper** in [`src/server/lib/dataforseo/core.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/core.ts). The `createAuthenticatedFetch` function injects the `DATAFORSEO_API_KEY` and enforces a 60-second request timeout.

This wrapper serves as the interception point for:

- Authentication header injection
- Error classification and mapping
- Retry policy enforcement
- Rate-limit detection

## Billing Classification and Error Handling

### Mapping DataForSEO Errors to Typed AppErrors

After any non-2xx response, OpenSEO classifies errors that indicate depleted account balance. The system looks for HTTP 402 status codes or messages containing "insufficient funds" and throws typed `AppError` instances.

The `createDataforseoBillingClassifier` function in [`src/server/lib/dataforseoBillingClassification.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseoBillingClassification.ts) builds these classifiers per API path:

```typescript
import { createDataforseoBillingClassifier } from "@/server/lib/dataforseoBillingClassification";

const classifyKeywordsError = createDataforseoBillingClassifier({
  pathPrefix: "/keywords/",
  billingIssueCode: "KEYWORDS_BILLING_ISSUE",
  billingIssueMessage: "The connected DataForSEO account has a billing or balance issue",
});

```

This produces error codes like:

- `BACKLINKS_BILLING_ISSUE`
- `AI_BILLING_ISSUE`
- `KEYWORDS_BILLING_ISSUE`

Each code surfaces a clear billing problem message to the user interface.

### Usage Credit Pool Management

OpenSEO tracks **two distinct credit pools**:

1. **Monthly usage credits** — deducted first
2. **Top-up credits** — used after monthly pool depletion

The conversion rate is fixed at **1 USD → 1000 DataForSEO credits**. Hosted customer pricing includes a markup factor (approximately 1.28×) applied to raw DataForSEO costs before billing to the OpenSEO subscription.

Constants and conversion helpers reside in [`src/shared/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/billing.ts):

```typescript
// Conversion constants
AUTUMN_SEO_DATA_CREDITS_PER_USD

// Markup application for hosted mode
applyBillingMarkupUsd(rawCost)

```

Self-hosted deployments bypass the OpenSEO credit pool entirely. The code checks `isHostedServerAuthMode()` before applying deductions, and these users pay DataForSEO directly using `autumnSeoDataCreditsToUsd` without markup.

## Rate Limiting and Retry Policies

### Handling DataForSEO 429 Responses

When DataForSEO returns **HTTP 429 (RATE_LIMITED)**, the wrapper maps it to a generic `RATE_LIMITED` error code in `AppError`. The calling workflow decides whether to:

- Retry the request later
- Surface a user-friendly rate-limited message

This mapping occurs in [`src/server/lib/dataforseo/core.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/core.ts):

```typescript
// Simplified error mapping logic
status === 429 ? "RATE_LIMITED" : /* other codes */

```

### Per-API Retry Configuration

The same wrapper implements differentiated retry behavior:

| Call Type | Retry Policy | Rationale |
|-----------|-------------|-----------|
| Idempotent reads (keyword data, rankings) | Up to 2 retries, 250ms backoff | Safe to repeat without side effects |
| Non-idempotent calls (Lighthouse live tests) | No retries | Prevents double-charging |

This is controlled by `DATAFORSEO_MAX_RETRIES` logic within `createAuthenticatedFetch`.

### Billing Usage Reporting with Autumn API

For hosted customers, OpenSEO queries Autumn's `events.list` endpoint to pull usage-credit events. The client implements **exponential backoff with maximum retry count** for HTTP 429 responses:

```typescript
import { getBillingUsageEvents } from "@/serverFunctions/billing";

await getBillingUsageEvents({ start: 0, end: Date.now() });
// Internally backs off on 429 and retries up to 3 times

```

The implementation in [`src/serverFunctions/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/billing.ts) includes `fetchAutumnEventsPage` with full retry logic.

## Practical DataForSEO API Integration Example

Here's a complete pattern for making DataForSEO API calls with full billing and rate-limit handling:

```typescript
import { keywordsDataApi } from "@/server/lib/dataforseo/core";
import { createDataforseoBillingClassifier } from "@/server/lib/dataforseoBillingClassification";
import { assertOk, parseTaskItems } from "@/server/lib/dataforseo/helpers";

const classifyKeywordsError = createDataforseoBillingClassifier({
  pathPrefix: "/keywords/",
  billingIssueCode: "KEYWORDS_BILLING_ISSUE",
  billingIssueMessage: "The connected DataForSEO account has a billing or balance issue",
});

async function fetchKeywordIdeas(projectId: string, kw: string) {
  const response = await keywordsDataApi(classifyKeywordsError).relatedKeywordsLive([
    { keyword: kw, location_code: 2840, language_code: "en" }
  ]);
  
  // Throws RATE_LIMITED AppError if DataForSEO returned 429
  const task = assertOk(response, {
    classify: classifyKeywordsError,
    classifyPath: "/v3/keywords_data/related_keywords/live"
  });

  // Billing metadata attached to task result
  const { data, billing } = parseTaskItems(task);
  return { data, billing };
}

```

## Self-Hosted vs. Hosted Billing Modes

| Aspect | Hosted Mode | Self-Hosted Mode |
|--------|-------------|------------------|
| Credit pool | Managed by OpenSEO | Direct DataForSEO billing |
| Cost calculation | Raw cost × 1.28 markup | Raw cost only |
| Credit tracking | Autumn events API | N/A |
| Implementation check | `isHostedServerAuthMode()` returns `true` | `isHostedServerAuthMode()` returns `false` |

The early-return logic in [`src/serverFunctions/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/billing.ts) prevents unnecessary credit operations when running self-hosted.

## Key Files Reference

| File | Purpose |
|------|---------|
| [`src/shared/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/billing.ts) | Credit pools, conversion functions, markup logic |
| [`src/server/lib/dataforseo/core.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/core.ts) | Authentication, timeout, retries, error mapping |
| [`src/server/lib/dataforseoBillingClassification.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseoBillingClassification.ts) | Balance error classification |
| [`src/serverFunctions/billing.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/billing.ts) | Autumn events fetching with backoff |
| [`src/server/mcp/tools/whoami.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.ts) | Credit balance query (no consumption) |
| [`src/server/lib/market.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/market.ts) | Location validation to prevent wasted credits |

## Summary

- **Authentication layer**: `createAuthenticatedFetch` in [`core.ts`](https://github.com/every-app/open-seo/blob/main/core.ts) centralizes all DataForSEO API calls with 60s timeout
- **Billing classification**: Per-path error classifiers convert balance errors into typed `AppError` codes
- **Credit pools**: Monthly + top-up pools with 1:1000 USD conversion and 1.28× hosted markup
- **Rate limiting**: 429 responses map to `RATE_LIMITED` errors; Autumn API calls use exponential backoff
- **Retry policies**: Idempotent reads retry twice with 250ms delay; non-idempotent calls never retry
- **Deployment modes**: Hosted uses OpenSEO credit system; self-hosted pays DataForSEO directly

## Frequently Asked Questions

### What happens when DataForSEO returns HTTP 429?

OpenSEO maps the 429 status to a `RATE_LIMITED` `AppError`. The calling workflow determines whether to retry later or display a rate-limited message to users. For Autumn billing API calls, the client automatically retries with exponential backoff up to 3 times.

### How does OpenSEO prevent double-charging for API calls?

Non-idempotent operations like Lighthouse live tests have retries disabled in `createAuthenticatedFetch`. Only idempotent read operations (keyword data, ranking checks) retry on transient 5xx errors, ensuring charged operations execute exactly once.

### What is the credit conversion rate for DataForSEO services?

OpenSEO uses a fixed rate of **1000 DataForSEO credits per 1 USD**. Hosted customers see approximately 1.28× this base cost after markup. Self-hosted users pay the raw DataForSEO rate directly without OpenSEO's credit pooling system.