---
name: serp
description: "Search results from Bing and DuckDuckGo as structured JSON: organic results with position, title, URL and snippet, Bing ads and related searches, result count, pagination, country and language. No browser, no Google. Use when the user wants web search results for one or more queries: rank tracking, checking who ranks for a keyword, competitor and ad research, finding pages on a site with `site:`, or related searches for keyword ideas. Calls the Pullwire API with the key in PULLWIRE_API_KEY; costs 1 credit per search result page."
metadata:
  openclaw:
    requires:
      env:
        - PULLWIRE_API_KEY
      bins:
        - curl
    primaryEnv: PULLWIRE_API_KEY
    homepage: https://pullwire.dev/tools/serp
---

# Bing & DuckDuckGo SERP API

Search results from Bing and DuckDuckGo as structured JSON: organic results with position, title, URL and snippet, Bing ads and related searches, result count, pagination, country and language. No browser, no Google. The data comes from the Pullwire API: you send the input as JSON and get structured JSON back. Only public data.

## Setup (once)

The API key must be in the environment variable `PULLWIRE_API_KEY`. Keys start with `af_live_`. Check it without printing it:

```bash
test -n "$PULLWIRE_API_KEY" && echo "key is set" || echo "PULLWIRE_API_KEY is missing"
```

If it is missing, stop and tell the user: sign up at https://pullwire.dev/login (new accounts get 2,000 free credits), create a key at https://pullwire.dev/app/keys, then run `export PULLWIRE_API_KEY="af_live_..."` in the shell the agent uses. Never print the key, write it into a file or repeat it in the chat.

## Run it

```bash
curl -sS -X POST "https://pullwire.dev/api/v1/tools/serp/runs" \
  -H "x-api-key: $PULLWIRE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"queries":["best running shoes"],"maxResults":3}'
```

The request waits up to 25 seconds and answers with `{"run": {...}, "results": [...]}`. Check `run.status` (`succeeded` or `failed`) and `run.error`: when it is set, the run stopped early and `results` is partial (everything in it was charged). `run.creditsUsed` is what the run cost. For big answers add `-o result.json` and read only the fields you need.

Good to know:

- Put every query into one run (`queries`, up to 100) instead of one run per query.
- One result is one result page with about 10 organic results; set `maxPagesPerQuery` (up to 10) for deeper rankings. `maxResults` caps result pages across all queries.
- Set `country` (two-letter code such as `us`, `gb`, `de`) and `language` when the market matters; rankings differ by country. For DuckDuckGo, a country without a DuckDuckGo region is rejected before the run starts.
- DuckDuckGo shows no ads on this route, and its related searches cost one extra request per query; set `includeRelatedSearches` to `false` when you do not need them.
- A query with no results returns nothing and costs nothing.

## Input

The JSON body is the tool input. Fields:

| Field | Type | Required | Default | What it does |
| --- | --- | --- | --- | --- |
| `queries` | string[] | yes |  | Search queries: One search per entry, exactly as you would type it into the search box. Operators like site: work. |
| `engine` | "bing" \| "duckduckgo" | no | `"bing"` | Search engine: Bing returns organic results, ads, related searches and the result count. DuckDuckGo returns organic results; its related searches come from DuckDuckGo’s own query suggestions and it shows no ads on this route. |
| `country` | string | no |  | Country: Two-letter country code for the result market, e.g. "us", "gb", "de". Empty: the engine picks the market (Bing by location, DuckDuckGo "All regions"). |
| `language` | string | no |  | Language: Interface and result language, e.g. "en", "de", "fr". For DuckDuckGo it picks between a country’s regions (e.g. Canada + fr). |
| `maxPagesPerQuery` | integer | no | `1` | Pages per query: How many result pages to fetch for each query (about 10 organic results per page). |
| `includeRelatedSearches` | boolean | no | `true` | Related searches: Include related searches. Free on Bing (same page); one extra request per query on DuckDuckGo. |
| `maxResults` | integer | no |  | Max result pages: Stop after this many result pages in total across all queries. Empty: no extra cap. |

Without `maxResults` a run returns at most 20 results; the maximum is 200 per run. Credits for `maxResults` results are reserved before the run starts and the unused part is given back, so ask for what you need.

## Output

Each item in `results` has these fields:

| Field | Type | Always there |
| --- | --- | --- |
| `engine` | "bing" \| "duckduckgo" | yes |
| `query` | string | yes |
| `page` | integer | yes |
| `country` | string | no |
| `language` | string | no |
| `searchUrl` | string | yes |
| `totalResults` | integer | no |
| `organicResults` | object[] | yes |
| `ads` | object[] | yes |
| `relatedSearches` | string[] | yes |
| `hasNextPage` | boolean | yes |
| `scrapedAt` | string | yes |

## Cost

1 credit per search result page. One search result page (about 10 organic results plus ads and related searches where the engine shows them). Queries with no results are not charged. Check the balance with:

```bash
curl -sS "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```

The answer has `balance` in credits. Tell the user what a run cost when it was more than a few credits.

## Bigger jobs

Add `?async=1` to the run URL to start a background run (up to 50 seconds). It answers `202` with `run.id` right away. Poll every few seconds until `status` is `succeeded` or `failed`, then fetch the results:

```bash
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID" -H "x-api-key: $PULLWIRE_API_KEY"
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID/results" -H "x-api-key: $PULLWIRE_API_KEY"
```

Add `?format=csv` to the results URL when the user wants a spreadsheet.

## Errors

Errors answer `{"error": {"code": "...", "message": "..."}}`.

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | The message names the field. Fix the input and retry; nothing was charged. |
| 401 | `UNAUTHORIZED` | The key is missing, wrong or revoked. Ask the user to set a valid key in `PULLWIRE_API_KEY`. |
| 402 | `INSUFFICIENT_CREDITS` | The balance cannot cover the reservation. Lower `maxResults` or tell the user the balance is too low. |
| 404 | `NOT_FOUND` | Unknown tool or run ID. |
| 429 | `RATE_LIMITED` | Used up its burst of 120 requests and its 60 per minute. Wait, then retry. |
| 500 | `INTERNAL_ERROR` | Retry later. If the body has `runId`, fetch the partial results with `GET /runs/{runId}/results`. |
| 503 | `TOOL_UNAVAILABLE` | Our latest automatic checks against this site failed, so its runs are paused. Nothing was reserved or charged. Tell the user and try again later. |
| 503 | `SERVICE_UNAVAILABLE` | Retry later. |

Docs: https://pullwire.dev/docs · Tool page: https://pullwire.dev/tools/serp
