How OpenSEO Integrates with DataForSEO: A Technical Deep Dive
OpenSEO integrates with DataForSEO through a thin, metered wrapper in src/server/lib/dataforseo that provides lazy SDK loading, billing-aware execution, and unified error handling.
The every-app/open-seo repository implements a billing-aware façade over the DataForSEO API. This integration shields the rest of the codebase from the raw SDK while enforcing credit limits and attributing costs to specific features. Let's examine the three-layer architecture that makes this possible.
Lazy Loading of the DataForSEO SDK
The DataForSEO SDK weighs approximately 3 MB—too heavy for server cold starts. OpenSEO solves this with lazy loading: the SDK is imported only when the first API call is made.
export function loadDataforseoSections(): Promise<DataforseoSections> {
return (sectionsPromise ??= import("@/server/lib/dataforseo/sections"));
}
As implemented in src/server/lib/dataforseo/client.ts at lines 30-34, this pattern keeps initial boot times fast by deferring the heavy import until actually needed.
The Metered Client Creator
The createDataforseoClient function returns an object whose methods are wrapped by a meter function. This meter injects billing logic, enforces credit limits, and records spend per feature.
export function createDataforseoClient(customer: BillingCustomerContext) {
return {
business: { /* businessListings, etc. */ },
backlinks: { /* backlink methods */ },
keywords: { /* keyword research methods */ },
domain: { /* domain analysis methods */ },
serp: { /* SERP checking methods */ },
labs: { /* experimental features */ },
lighthouse: { /* page speed data */ },
aiSearch: { /* AI-powered search features */ },
};
}
See src/server/lib/dataforseo/client.ts lines 63-72. The meter helper at lines 48-60 wraps each method:
function meter<I, T>(customer, pick, defaultFeature?) {
return (input) => meterDataforseoCall(
customer,
async () => pick(await loadDataforseoSections())(input),
input.creditFeature ?? defaultFeature
);
}
This adds credit-feature logic and delegates to meterDataforseoCall.
Hosted-Mode Billing and Error Handling
When OpenSEO runs as a hosted SaaS platform, every DataForSEO call undergoes three billing stages:
- Pre-flight check –
assertUsageCreditsAvailableverifies the organization's credit balance - Execution – The actual DataForSEO API call runs through the metered wrapper
- Post-flight tracking –
trackDataforseoCostrecords spend using the feature mapped from the SDK path
The meterDataforseoCall function (lines 54-78 in client.ts) implements this flow. Even charged task errors are tracked—unless they're validation-only failures, ensuring accurate billing attribution.
How the Wrapper Is Used in Practice
Most OpenSEO functionality obtains a client via createDataforseoClient(context.billing) and calls the appropriate method. Here's the "Get ranked keywords" MCP tool:
const client = createDataforseoClient(context.billing);
const keywords = await client.domain.rankedKeywords({
target: target.hostname,
locationCode: market.locationCode,
languageCode: market.languageCode,
// ...other options
});
See src/server/mcp/tools/dataforseo-research-tools.ts lines 800-806.
Common Usage Patterns Across OpenSEO
- Local business search –
client.business.businessListings - SERP competitors –
client.labs.serpCompetitors - Keyword metrics –
client.keywords.related(viafetchKeywordMetricsForList)
Every call automatically: lazy-loads the SDK, enforces credit limits (hosted mode only), attributes spend to a feature (rank_tracking, local_seo, etc.), and converts DataForSEO responses into OpenSEO's internal schema via helpers like toRankedKeywordRow.
Code Examples
Pulling Ranked Keywords for a Domain
import { createDataforseoClient } from "@/server/lib/dataforseo";
import { resolveMarketSelector } from "@/server/mcp/tools/dataforseo-research-tools";
const client = createDataforseoClient(billingContext);
const market = resolveMarketSelector(
{ locationCode: 284, languageCode: "en" }, // US-English
projectMeta,
);
const result = await client.domain.rankedKeywords({
target: "example.com",
locationCode: market.locationCode,
languageCode: market.languageCode,
limit: 20,
orderBy: ["ranked_serp_element.serp_item.rank_absolute,asc"],
});
console.log(result.items); // → array of ranked keyword rows
Searching Local Businesses
import { createDataforseoClient } from "@/server/lib/dataforseo";
const client = createDataforseoClient(billingContext);
const rows = await client.business.businessListings({
categories: ["pizza_restaurant"],
title: "Joe's Pizzeria",
locationCoordinate: "37.7749,-122.4194,5",
isClaimed: false,
limit: 10,
});
console.log(rows.map(r => r.title));
Direct Use in a Server Workflow
import { createDataforseoClient } from "@/server/lib/dataforseo";
import { parseResearchTargetOrThrow } from "@/server/lib/domainUtils";
export async function runRankCheck(project, billing, url) {
const client = createDataforseoClient(billing);
const target = parseResearchTargetOrThrow(url);
const serp = await client.serp.rankCheck({
target: target.hostname,
locationCode: project.locationCode,
languageCode: project.languageCode,
});
return serp;
}
See src/server/workflows/RankCheckWorkflow.ts lines 334-336.
Key Files in the Integration
| File | Purpose |
|---|---|
src/server/lib/dataforseo/client.ts |
Core metered client factory (createDataforseoClient) and billing wrapper |
src/server/lib/dataforseo/sections.ts |
Actual DataForSEO SDK fetchers; loaded lazily |
src/server/mcp/tools/dataforseo-research-tools.ts |
MCP tools calling the client (ranked keywords, local business, SERP competitors) |
src/server/workflows/RankCheckWorkflow.ts |
Server workflow obtaining client for rank checks |
src/server/lib/billing/subscription.ts |
Credit limit enforcement and spend recording |
src/shared/billing-credit-features.ts |
Maps DataForSEO endpoint paths to billing features |
Summary
- Lazy loading in
client.tsdefers the 3 MB DataForSEO SDK import until first use - Metered execution wraps every API call with billing logic via
meterDataforseoCall - Feature attribution maps SDK paths to credit features like
rank_trackingandlocal_seo - Unified façade exposes domain, serp, keywords, business, labs, lighthouse, and aiSearch endpoints
- Automatic response shaping converts DataForSEO payloads to OpenSEO's internal schema
Frequently Asked Questions
Does OpenSEO require the DataForSEO SDK at startup?
No. The SDK is lazily loaded via loadDataforseoSections() only when the first API call is made. This keeps server cold starts fast despite the SDK's 3 MB size.
How does OpenSEO handle billing for DataForSEO usage?
In hosted mode, the meterDataforseoCall function checks credits before execution, runs the API call, then records spend via trackDataforseoCost. Each endpoint maps to a specific credit feature like rank_tracking or local_seo.
Can I use the DataForSEO client outside of MCP tools?
Yes. The createDataforseoClient function is used throughout OpenSEO—including server workflows like RankCheckWorkflow.ts—not just MCP tools. Any code with access to a BillingCustomerContext can instantiate the client.
What happens if a DataForSEO call fails?
Charged task errors are still tracked for billing unless they're validation-only failures. The meter wrapper ensures costs are accurately attributed even when API calls don't return usable data.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →