# Multi-Engine SERP API for Agents - Brave, DuckDuckGo, Mojeek (`yasaslive/serp-multi`) Actor

One normalized search API across Brave, DuckDuckGo and Mojeek. Uniform JSON schema, BYO API keys, per-query pay-per-event pricing, and low-latency Standby HTTP mode for AI agents.

- **URL**: https://apify.com/yasaslive/serp-multi.md
- **Developed by:** [Eonix Pvt Ltd](https://apify.com/yasaslive) (community)
- **Categories:** Agents, Automation, Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.00005 / actor start

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Multi-Engine SERP API for Agents — Brave, DuckDuckGo, Mojeek

**One search call, three engines, one schema.** `serp-multi` gives AI agents (and the humans building them) a normalized web-search API across **Brave**, **DuckDuckGo**, and **Mojeek** — with predictable per-query pricing, bring-your-own API keys, cross-engine deduplication, optional LLM re-ranking, and a **low-latency Standby HTTP endpoint** so your agent gets an answer in one round-trip instead of waiting for a batch run.

Built for agent consumption first:

- **Uniform JSON schema** — every engine's results come back as the same record shape. Your agent parses one format, forever.
- **Predictable cost** — pay-per-event: a flat price per query × engine executed. No token surprises, no per-GB ambiguity. Failed engine calls are never charged.
- **Real-time via Standby mode** — `GET /search?q=…` returns JSON in the response body with cold-start-free latency. Perfect as an agent tool or an MCP tool.
- **BYO keys where engines have official APIs** — Brave and Mojeek run through their official APIs with *your* keys (both have free tiers), so you control quotas and stay in each engine's terms. DuckDuckGo needs no key.
- **Cross-engine consensus** — results pointing at the same canonical URL are merged, keeping each engine's rank in `enginesRanks`. Agreement across independent indexes is a strong relevance signal.

### Supported engines

| Engine | How | Key needed? | Notes |
|---|---|---|---|
| `brave` | Official [Brave Search API](https://brave.com/search/api/) | Yes — `braveApiKey` (free tier: 2,000 queries/mo) | Skipped with a logged warning if no key |
| `duckduckgo` | HTML endpoint via got-scraping + Cheerio | No | Graceful degradation on blocks: one proxy retry, then partial results with a warning |
| `mojeek` | Official [Mojeek Search API](https://www.mojeek.com/services/search/web-search-api/) | Yes — `mojeekApiKey` | Independent index — great diversity signal. Skipped if no key |

### Quick start (batch mode)

Run the Actor with:

```json
{
    "queries": ["apify actor pricing", "best vector database"],
    "engines": ["brave", "duckduckgo", "mojeek"],
    "resultsPerEngine": 10,
    "mergeResults": true,
    "braveApiKey": "YOUR_BRAVE_KEY",
    "mojeekApiKey": "YOUR_MOJEEK_KEY"
}
```

Every query is searched on every enabled engine (concurrency-limited, retried with exponential backoff, 429 `Retry-After` respected) and normalized records land in the default dataset.

### Standby mode — the real-time agent endpoint

This Actor supports [Apify Actor Standby](https://docs.apify.com/platform/actors/running/standby): the platform keeps a warm instance running an HTTP server, so requests answer in real time.

```
GET https://<your-username>--serp-multi.apify.actor/search?q=best+vector+database&engines=duckduckgo,brave
Authorization: Bearer <YOUR_APIFY_TOKEN>
```

Query parameters (all optional except `q`):

| Param | Meaning | Default |
|---|---|---|
| `q` | The search query (**required**) | — |
| `engines` | Comma-separated subset of `brave,duckduckgo,mojeek` | run input / all |
| `resultsPerEngine` (alias `count`) | 1–50 results per engine | run input / 10 |
| `country`, `language` | Two-letter localization codes | `us`, `en` |
| `mergeResults` (alias `merge`) | `true`/`false` cross-engine merge | `true` |
| `rerank` | `true` to LLM-rerank (needs `openaiApiKey` in the standby run input) | `false` |

**Secrets never travel in the URL.** API keys (`braveApiKey`, `mojeekApiKey`, `openaiApiKey`) are configured once in the standby run's input; per-request params only tune non-secret options.

The endpoint returns the same normalized records as batch mode, in a JSON envelope:

```json
{ "query": "…", "count": 12, "results": [ … ], "engines": { "executed": ["brave"], "skipped": [], "failed": [] }, "reranked": false, "warnings": [], "latencyMs": 913 }
```

Standby requests charge the **same** pay-per-event prices as batch runs — one `serp-query` event per engine executed.

### Use as an MCP tool

Apify exposes every Actor as an [MCP (Model Context Protocol) tool](https://docs.apify.com/platform/integrations/mcp) — no extra code in this Actor needed. Point your MCP client (Claude, or any MCP-capable agent framework) at the [Apify MCP server](https://mcp.apify.com), allow `serp-multi`, and your agent can call multi-engine search natively as a tool. Combined with Standby mode, that gives your agent sub-second, fixed-price web search.

### Input reference

| Field | Type | Default | Description |
|---|---|---|---|
| `queries` | `string[]` | — | Search queries. **Required in batch mode**; ignored in Standby (use `?q=`). Max 500 per run. |
| `engines` | `string[]` | all three | Any of `brave`, `duckduckgo`, `mojeek`. Keyless engines requiring a key are skipped with a warning. |
| `braveApiKey` | secret string | — | Brave Search API subscription token. |
| `mojeekApiKey` | secret string | — | Mojeek Search API key. |
| `country` | string | `us` | Two-letter country code (Brave `country`, DDG region). |
| `language` | string | `en` | Two-letter language code (Brave `search_lang`, DDG region). |
| `resultsPerEngine` | integer | `10` | 1–50 organic results per engine per query. |
| `mergeResults` | boolean | `true` | Merge duplicates across engines (see below). `false` → one record per engine per result. |
| `rerank` | boolean | `false` | LLM re-rank merged results per query (needs `openaiApiKey`). |
| `openaiApiKey` | secret string | — | Only used when `rerank` is `true`. |
| `proxyConfiguration` | proxy | none | **DuckDuckGo only** — regular egress if enabled, and always the retry path when DDG serves a CAPTCHA. |

### Declared outputs

The Actor ships a machine-readable [output schema](.actor/output_schema.json), so Console, the API, and MCP clients know exactly what a run produces:

| Output | Where it lives | What it is |
|---|---|---|
| `results` | Default dataset (`/items`) | Every normalized SERP record — the main output |
| `runSummary` | Key-value store, key `OUTPUT` | One JSON object: queries/records processed, per-engine execution counts, engines skipped for missing keys, failures with reasons, warnings, and the exact PPE events charged |
| `standbySearchEndpoint` | The run's container URL | `…/search` — the live real-time endpoint while running in Standby |

The dataset also ships a [dataset schema](.actor/dataset_schema.json) with typed, documented fields (so agents can interpret each column) and two views: **Overview** and **Cross-engine consensus**, which flattens `enginesRanks` into one column per engine to show which URLs several independent engines agree on.

Real `OUTPUT` summary from the sample run below:

```json
{
    "mode": "batch",
    "startedAt": "2026-08-15T15:33:20.353Z",
    "finishedAt": "2026-08-15T15:33:23.005Z",
    "queries": 1,
    "records": 10,
    "settings": { "engines": ["duckduckgo"], "resultsPerEngine": 10, "mergeResults": true, "rerank": false, "country": "us", "language": "en" },
    "engines": { "executedByEngine": { "duckduckgo": 1 }, "skipped": [] },
    "charged": { "serp-query": 1, "rerank": 0 },
    "failures": [],
    "warnings": []
}
```

Note that the summary lives in the key-value store, not the dataset: every dataset item keeps one uniform shape, which is the point of this Actor for agent consumers.

### Output schema & real sample

Each dataset record (and each element of the Standby `results` array):

| Field | Type | Notes |
|---|---|---|
| `query` | string | The query that produced this record |
| `engine` | string | Producing engine; for merged records, the best-ranked contributor |
| `rank` | number | Rank within its engine; for merged records, the best rank across engines |
| `title`, `url`, `snippet` | string | Normalized result fields |
| `fetchedAt` | ISO-8601 string | When the result was fetched |
| `enginesRanks` | object | Merged records only: `{ engine: rank }` for every engine that listed this URL |
| `rerankScore` | number 0–1 | Only when `rerank` ran: higher = more relevant |

Real output from a local run with `{ "queries": ["apify actor pricing"], "engines": ["duckduckgo"] }`:

```json
[
    {
        "query": "apify actor pricing",
        "engine": "duckduckgo",
        "rank": 1,
        "title": "Apify pricing - plans for data collection at any scale · Apify",
        "url": "https://apify.com/pricing",
        "snippet": "The Apify platform has a number of services that are charged based on usages, such as Actors, proxies, data transfer, and storage. See pricing for the full list of platform services. Each subscription plan comes with a certain amount of prepaid platform usage that is used to pay for services. If your platform usage in a given billing cycle exceeds this prepaid amount, the excess usage will be ...",
        "fetchedAt": "2026-08-14T15:02:28.111Z",
        "enginesRanks": { "duckduckgo": 1 }
    },
    {
        "query": "apify actor pricing",
        "engine": "duckduckgo",
        "rank": 2,
        "title": "Apify Actor Cost Estimator",
        "url": "https://apify.com/agentictools/actor-cost-estimator",
        "snippet": "Estimate what an Apify Actor will cost before you run it, across its pricing model and your workload.",
        "fetchedAt": "2026-08-14T15:02:28.111Z",
        "enginesRanks": { "duckduckgo": 2 }
    },
    {
        "query": "apify actor pricing",
        "engine": "duckduckgo",
        "rank": 3,
        "title": "How to Monetize Apify Actors: Pricing, Payouts & Publishing | Use Apify",
        "url": "https://use-apify.com/docs/apify-for-developers/monetize-actors",
        "snippet": "Monetize Apify Actors with pay-per-event, pay-per-result, or rental pricing. Commission split, PayPal and bank payouts, and steps to publish a paid Store listing.",
        "fetchedAt": "2026-08-14T15:02:28.111Z",
        "enginesRanks": { "duckduckgo": 3 }
    }
]
```

### How merging works

With `mergeResults: true` (default), URLs are canonicalized — tracking params (`utm_*`, `gclid`, `fbclid`, …) stripped, trailing slash removed, host lowercased, `http`/`https` folded — and results from different engines pointing at the same canonical URL become **one record**:

- `enginesRanks` keeps every engine's rank: `{ "brave": 2, "duckduckgo": 1 }` means both engines found it — a strong consensus signal for agents.
- Title/snippet/`rank` come from the best-ranked contributor.
- Output is ordered by best rank; ties go to results more engines agree on.

With `mergeResults: false` you get the raw per-engine view: one record per engine per result.

### LLM re-ranking (optional)

With `rerank: true` and an `openaiApiKey`, one structured `gpt-4o-mini` call per query re-orders the merged results by relevance to the query intent and attaches `rerankScore` (0–1). If the call fails, the run continues with engine order — and the `rerank` event is **not** charged.

### Pricing (pay-per-event)

| Event | Description | Charged | Suggested price |
|---|---|---|---|
| `serp-query` | One executed search of one query on one engine, including normalization, dedupe, and record delivery | Per query × engine actually executed — only **after** its records are stored | **$0.003** |
| `rerank` | One AI relevance re-ranking pass over a query's merged results, adding `rerankScore` | Per successfully re-ranked query — only after records are stored | **$0.005** |

Cost intuition: a 3-engine merged search is ~$0.009; a 100-query × 2-engine batch is ~$0.60. Skipped engines (missing key) and failed engine calls cost nothing.

Run it at **512 MB** — measured peak memory is 76 MB, so the platform default of 4 GB just multiplies your compute bill. See [MONETIZATION.md](MONETIZATION.md) for the full Console setup and measured margins.

### Three concrete use cases

1. **Agent web-search tool with a fixed unit cost.** Wire the Standby endpoint (or MCP tool) into your agent framework as its `web_search` function. Finance knows exactly what 10,000 agent searches cost before the month starts.
2. **RAG freshness pipeline.** Nightly batch run over your product's top 500 questions across all three engines; merged, deduped records feed your retrieval index with a cross-engine consensus signal (`enginesRanks`) for source weighting.
3. **Brand & SEO monitoring across independent indexes.** Track where your domain ranks on Brave vs DuckDuckGo vs Mojeek for target keywords — three genuinely different indexes, one normalized dataset you can diff over time.

### FAQ

**Why bring my own Brave/Mojeek keys?** Both offer official APIs with free tiers. BYO keys keep you inside each engine's terms, give you your own quota, and keep this Actor's per-query price flat regardless of your plan.

**What happens if I only provide some keys?** Engines without keys are skipped with a logged warning; everything else runs. DuckDuckGo always works keyless.

**Is Google or Bing supported?** Not in this Actor — it focuses on engines that are agent-friendly via official APIs or stable endpoints. For Google, use a dedicated Google SERP actor from the Store.

**What if DuckDuckGo blocks the request?** The engine retries once through your `proxyConfiguration` (fresh session), then returns whatever it parsed with a warning instead of failing the whole request. For heavy DDG use, enable residential proxy.

**Do standby requests also write to the dataset?** Yes — records are pushed to the run's default dataset *and* returned in the response, and charging happens only after the push succeeds.

**How fresh are results?** Every record carries `fetchedAt`. Nothing is cached by the Actor — every query hits the engines live.

**Rate limits?** Global concurrency is capped at 5 in-flight engine requests (DuckDuckGo held to 2), with exponential backoff honoring `Retry-After` on 429s.

### Local development

```bash
npm install
npm run build      # tsc — zero errors expected
npm test           # unit tests: URL canonicalizer + standby server (engine calls mocked)
apify run          # batch run against storage/key_value_stores/default/INPUT.json

## Local standby server on :3123 — APIFY_META_ORIGIN is what selects the mode,
## exactly as the platform does it (the port var alone is set on every run).
APIFY_META_ORIGIN=STANDBY ACTOR_STANDBY_PORT=3123 npm start
```

### Changelog

#### 0.1.0

- Initial release: Brave / DuckDuckGo / Mojeek engines, cross-engine merge with `enginesRanks`, batch + Standby modes, optional LLM re-rank, pay-per-event charging (`serp-query`, `rerank`).

# Actor input Schema

## `queries` (type: `array`):

Queries to search for. Required in batch mode (normal runs). In Standby mode the query comes from the `q` URL parameter instead and this field may be empty.

## `engines` (type: `array`):

Which search engines to query. Brave and Mojeek need your own (free-tier available) API keys and are skipped with a warning when the key is missing. DuckDuckGo needs no key.

## `braveApiKey` (type: `string`):

Your Brave Search API subscription token (https://api-dashboard.search.brave.com). If empty, the Brave engine is skipped with a logged warning.

## `mojeekApiKey` (type: `string`):

Your Mojeek Search API key (https://www.mojeek.com/services/search/web-search-api/). If empty, the Mojeek engine is skipped with a logged warning.

## `country` (type: `string`):

Two-letter country code used for result localization (Brave `country`, DuckDuckGo region).

## `language` (type: `string`):

Two-letter language code used for result localization (Brave `search_lang`, DuckDuckGo region).

## `resultsPerEngine` (type: `integer`):

Maximum organic results to collect from each engine per query.

## `mergeResults` (type: `boolean`):

When enabled (default), results pointing at the same canonical URL are merged into one record that keeps every engine's rank in `enginesRanks`. When disabled, one record per engine per result is emitted.

## `rerank` (type: `boolean`):

Re-order merged results by relevance to the query intent with one structured OpenAI call per query. Requires `openaiApiKey`. Adds `rerankScore` to records and charges the `rerank` event.

## `openaiApiKey` (type: `string`):

Your OpenAI API key, only used when `rerank` is enabled.

## `proxyConfiguration` (type: `object`):

Proxy used only by the DuckDuckGo engine — first as its regular egress if enabled, and always as the one-shot retry path when DuckDuckGo serves a CAPTCHA/blank page. Brave and Mojeek are official APIs and never go through a proxy.

## Actor input object example

```json
{
  "queries": [
    "apify actor pricing"
  ],
  "engines": [
    "brave",
    "duckduckgo",
    "mojeek"
  ],
  "country": "us",
  "language": "en",
  "resultsPerEngine": 10,
  "mergeResults": true,
  "rerank": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All normalized SERP records for this run: one JSON object per result with query, engine, rank, title, url, snippet, fetchedAt, and (when merged) enginesRanks holding every engine's rank for that URL. This is the main output to consume.

## `runSummary` (type: `string`):

Single JSON object summarizing the run: queries and records processed, per-engine execution counts, engines skipped for missing API keys, engine failures with reasons, warnings, and the exact pay-per-event counts charged. Read this to verify coverage and cost without scanning the dataset.

## `standbySearchEndpoint` (type: `string`):

HTTP endpoint of this run's Standby server. Append a URL-encoded query, e.g. ?q=best+vector+database\&engines=brave,duckduckgo, to get the same normalized results synchronously in the response body. Only meaningful while the Actor runs in Standby mode.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "apify actor pricing"
    ],
    "engines": [
        "brave",
        "duckduckgo",
        "mojeek"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("yasaslive/serp-multi").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["apify actor pricing"],
    "engines": [
        "brave",
        "duckduckgo",
        "mojeek",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("yasaslive/serp-multi").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "apify actor pricing"
  ],
  "engines": [
    "brave",
    "duckduckgo",
    "mojeek"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call yasaslive/serp-multi --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yasaslive/serp-multi"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/SX0LWOhQxlGkdEjTo/builds/k8Rt4GuRVFkRsJ1l6/openapi.json
