# How OpenSEO Determines If a Project's Domain Ranks for Tracked Keywords

> Discover how OpenSEO checks if your domain ranks for keywords. Learn about its efficient process using DataForSEO API for continuous rank monitoring.

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

---

**OpenSEO determines whether a project's domain ranks for tracked keywords by normalizing the input target, querying DataForSEO's ranked-keywords API, matching returned SERP hostnames against the configured domain, and persisting the absolute rank position for continuous monitoring.**

OpenSEO is an open-source SEO analytics platform built in the `every-app/open-seo` repository that automates SERP monitoring for multiple projects. Understanding **how OpenSEO determines if a project's domain ranks for tracked keywords** requires examining the normalization pipeline, the DataForSEO integration, and the domain-matching logic that translates raw SERP data into actionable ranking reports.

## Normalizing and Detecting the Target Domain

Before any rank check occurs, OpenSEO must identify whether user input represents a domain or a keyword. The `detectTarget` helper in [[`src/shared/targetDetection.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/targetDetection.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/targetDetection.ts) applies heuristics to classify the input.

When the input resembles a hostname, the utility normalizes it by stripping protocols (`http://` or `https://`), removing the `www.` subdomain, and trimming trailing slashes. This ensures consistent matching later in the pipeline when comparing against SERP results.

```typescript
// Normalize user input to identify domain targets
import { detectTarget } from "@/shared/targetDetection";

const input = "https://www.example.com/";
const { type, value } = detectTarget(input);
// type === "domain", value === "example.com"

```

## Storing the Rank-Tracking Configuration

Once normalized, the domain is stored in a `RankTrackingConfig` defined in [[`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts)](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts). The `RankTrackingService.createConfig` and `RankTrackingService.updateConfig` methods in [[`src/server/features/rank-tracking/services/RankTrackingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts) persist the configuration.

A config records the **domain**, **projectMarket** (location and language codes), **devices** (desktop, mobile, or both), **serpDepth** (number of results to fetch, specified as total count), and **scheduleInterval** (frequency of automated checks).

```typescript
// Create a rank-tracking configuration for continuous monitoring
import { RankTrackingService } from "@/server/features/rank-tracking/services/RankTrackingService";

await RankTrackingService.createConfig({
  projectId: "proj_123",
  projectMarket: { locationCode: 2840, languageCode: "en" },
  domain: "example.com",
  serpDepth: 30,            // Equivalent to 3 SERP pages
  devices: "both",
  scheduleInterval: "weekly",
});

```

## Executing Live and Queued Rank Checks

OpenSEO supports two execution modes defined by the `RankCheckMethod` type (`"live" | "queued"`) in [[`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts). The `RankTrackingService.triggerCheck` method initiates the process, first calling `estimateRankCheckCredits` to ensure the request does not exceed a user-specified `maxCostCredits` limit.

For live checks, the service invokes the DataForSEO client directly. For queued checks, it uses `client.rankCheckTaskPost` to create a background task. The request respects the configured `serpDepth` and device targeting.

```typescript
// Trigger an immediate rank check with credit protection
import { RankTrackingService } from "@/server/features/rank-tracking/services/RankTrackingService";

await RankTrackingService.triggerCheck({
  configId: "cfg_abc",
  projectId: "proj_123",
  billingCustomer: { /* billing context */ },
  maxCostCredits: 500,      // Prevents exceeding budget
});

```

## Fetching SERP Data from DataForSEO

The actual rank data originates from DataForSEO's **ranked-keywords** endpoint. The MCP tool `get_ranked_keywords` in [[`src/server/mcp/tools/get-ranked_keywords.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/get-ranked_keywords.ts)](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/get-ranked_keywords.ts) invokes `client.domain.rankedKeywords` via the DataForSEO client wrapper in [[`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts).

The API response contains an array of `ranked_serp_element.serp_item` objects. Each object includes a **`rank_absolute`** field (the numeric position on the SERP) and a **`domain`** field containing the exact hostname of the ranking page.

## Matching Domains and Extracting Rank Positions

The critical matching logic resides in [[`src/server/features/domain/services/domainKeywordMapper.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/domain/services/domainKeywordMapper.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/domain/services/domainKeywordMapper.ts). For each `serp_item` returned by DataForSEO, the service extracts the result hostname from the `domain` property.

OpenSEO performs a case-insensitive comparison between the normalized domain stored in the `RankTrackingConfig` and the hostname in the SERP result. When the values match, the corresponding `rank_absolute` value represents the current rank for that keyword-device pair.

## Persisting Results and Scheduling Continuous Monitoring

After matching, the `RankTrackingRepository` in [[`src/server/features/rank-tracking/repositories/RankTrackingRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/repositories/RankTrackingRepository.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/repositories/RankTrackingRepository.ts) writes the rank, keyword, device, and timestamp to the `rank_snapshots` table.

To enable ongoing monitoring, the `computeNextCheckAt` function in [[`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) calculates the next execution time based on the `scheduleInterval`. The `ScheduledRankChecks` service in [[`src/server/features/rank-tracking/services/scheduledRankChecks.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/scheduledRankChecks.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/scheduledRankChecks.ts) uses this timestamp to queue subsequent jobs automatically.

```typescript
// Retrieve the latest ranking results for dashboard display
const { config, results } = await RankTrackingService.getTracker(
  "cfg_abc",
  "proj_123",
);
// results contains: [{ keyword: "seo tools", device: "desktop", rank: 3, checkedAt: "2024-01-15T..." }]

```

## Summary

OpenSEO implements a robust pipeline for determining domain keyword rankings:

- **Normalization**: The `detectTarget` utility standardizes domain inputs by removing protocols and `www` prefixes.
- **Configuration**: `RankTrackingService` stores domain, location, device, and depth settings in `RankTrackingConfig`.
- **Execution**: Supports both `live` instant checks and `queued` scheduled monitoring via DataForSEO integration.
- **Data Retrieval**: Fetches SERP items from the ranked-keywords endpoint, capturing `rank_absolute` and hostname data.
- **Matching**: Compares configured domains against SERP hostnames case-insensitively using the domain keyword mapper.
- **Persistence**: Stores rank snapshots in the database and schedules the next check using `computeNextCheckAt`.

## Frequently Asked Questions

### What external API does OpenSEO use to fetch keyword ranking data?

OpenSEO integrates with **DataForSEO** via the client wrapper in [`src/server/lib/dataforseo/client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/client.ts). Specifically, it uses the `client.domain.rankedKeywords` method to retrieve SERP data containing absolute rank positions and result hostnames for tracked keywords.

### How does OpenSEO distinguish between a domain and a keyword when adding tracking?

The `detectTarget` function in [`src/shared/targetDetection.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/targetDetection.ts) applies pattern matching heuristics to input text. If the input parses as a hostname (containing dots and valid TLD structure), it is normalized and classified as a domain target; otherwise, it is treated as a keyword phrase for competitive tracking.

### Can OpenSEO perform both real-time and scheduled rank checks?

Yes. The `RankCheckMethod` type in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) explicitly supports `"live"` for synchronous, immediate checks and `"queued"` for asynchronous background processing. The `triggerCheck` method handles both modes while respecting credit limits via `estimateRankCheckCredits`.

### Where does OpenSEO store historical ranking positions for trend analysis?

Historical ranks are persisted in the **`rank_snapshots`** table by the `RankTrackingRepository`. Each snapshot records the keyword, device type, absolute rank, and check timestamp, enabling the UI to display ranking trends and velocity over time.