Dipper docs
Dipper is a web search and page fetch API for AI agents. Results are kept in a cache that all customers share, and a call answered from the cache costs less than one that goes upstream.
You can call it over REST or add it to an agent as an MCP server. Both bill the same prepaid balance at the same prices.
Quickstart
- Sign in at /login/ with GitHub.
- Create an API key in the dashboard at /app/. The key is shown once, so copy it before you close the dialog. Keys start with
dk-. You can revoke a key there at any time; it stops working within a minute. - Put the key in an environment variable and run a search.
export DIPPER_API_KEY=dk-... curl -i https://dipper-search.com/v1/search \ -H "Authorization: Bearer $DIPPER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "rust async runtime comparison lang:en", "max_results": 5}'
-i prints the response headers. If nobody has asked this query recently, Dipper searches live and you pay the miss price:
x-dipper-cache: MISS x-dipper-cost-usd: 0.000800
Send the same request again and it comes from the cache:
x-dipper-cache: HIT x-dipper-cost-usd: 0.000160
Authentication
The base URL is https://dipper-search.com. Request and response bodies are JSON. Send your key on every request:
Authorization: Bearer dk-...
An x-api-key: dk-... header works too. A missing, malformed or unknown key returns 401, and a suspended account returns 403.
Keep the key on your server. Anyone who has it can spend your balance.
Search
POST /v1/search runs a web search.
| Field | Type | Notes |
|---|---|---|
query | string | Required. 1 to 128 characters. Supports the operators below. |
max_results | integer | 1 to 50, default 10. Does not change the price. |
include_content | boolean | Default false. When true, each result also carries the full page text in content. This is very large. Does not change the price. |
refresh | boolean | Default false. When true, skips the cache. See Refresh. |
Unknown fields are ignored.
Query operators
Operators go inside query and count toward the 128 characters. You can combine them.
| Operator | Meaning |
|---|---|
site:example.com | Only results from that site |
-site:example.com | Exclude that site |
lang:en | Results in that language, for example lang:zh |
after:2026-01-01 | Published on or after the date |
before:2026-06-30 | Published before the date |
Example
{
"query": "cloudflare r2 pricing",
"max_results": 10,
"include_content": false,
"refresh": false
}
Response (200):
{
"query": "cloudflare r2 pricing",
"results": [
{
"title": "Pricing · Cloudflare R2 docs",
"url": "https://developers.cloudflare.com/r2/pricing/",
"snippet": "R2 charges based on the total volume of data stored, along with two classes of operations...",
"site_name": "Cloudflare Docs",
"published_at": "2026-09-30T16:00:00+00:00",
"score": 2.76,
"authority": 3,
"favicon": "https://developers.cloudflare.com/favicon.png"
}
]
}
query echoes the query as you sent it.
| Result field | Description |
|---|---|
title | Page title |
url | Page URL |
snippet | Short excerpt from the page. May contain Markdown. |
site_name | Name of the site, or null |
published_at | Publication date in ISO 8601, or null if unknown |
score | Relevance score. Results are sorted by it, highest first. Higher is better, and it isn't limited to 0 to 1. |
authority | Source authority rank, a small integer. Higher means more authoritative. May be null. |
favicon | Site icon URL, or null |
content | Full page text. Only present when include_content is true. |
An empty results array is a successful response and is not billed.
Fetch
POST /v1/fetch returns the main content of a page as Markdown.
| Field | Type | Notes |
|---|---|---|
url | string | Required. An http or https URL, up to 8192 characters, with no username or password in it. |
refresh | boolean | Default false. When true, fetches the live page. See Refresh. |
curl https://dipper-search.com/v1/fetch \ -H "Authorization: Bearer $DIPPER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://developers.cloudflare.com/r2/pricing/"}'
Response (200):
{
"url": "https://developers.cloudflare.com/r2/pricing/",
"title": "Pricing · Cloudflare R2 docs",
"content": "# Pricing\n\nR2 charges..."
}
url echoes the URL that was fetched, normalized. content is the page's main text as Markdown. A page with no content to extract is not billed.
Response headers
Search and fetch responses carry three headers.
| Header | Meaning |
|---|---|
x-dipper-cache | HIT: served from the shared cache. MISS: searched or fetched live. REFRESH: live because you set refresh, and the cache now holds your result. NONE: nothing was served because the call failed. |
x-dipper-cost-usd | What the call cost, in USD with 6 decimals, for example 0.000160. 0.000000 for errors and empty results. |
x-request-id | A UUID. Include it when you contact support. |
A request rejected before it reaches search or fetch has none of these headers. That covers a bad key, a rate limit and a body that is not JSON.
Account
GET /v1/account/balance
curl https://dipper-search.com/v1/account/balance \
-H "Authorization: Bearer $DIPPER_API_KEY"
{ "balance_micros": 4825350, "balance_usd": "4.825350" }
balance_micros is an integer in micro-USD. 1 USD is 1,000,000 micros.
GET /v1/account/usage?days=N
days is an integer from 1 to 90, default 7. The window is today (UTC) plus the previous days - 1 days. There is one row per day, tool and cache status. Refreshed calls are counted as miss.
{
"days": 7,
"since": "2026-10-20",
"usage": [
{ "day": "2026-10-25", "tool": "search", "cache": "hit", "calls": 120, "amount_micros": 18000, "amount_usd": "0.018000" },
{ "day": "2026-10-25", "tool": "search", "cache": "miss", "calls": 40, "amount_micros": 30000, "amount_usd": "0.030000" }
],
"total_calls": 160,
"total_micros": 48000,
"total_usd": "0.048000"
}
Caching
The cache is shared by every Dipper customer. When anyone searches a query or fetches a page, the result is stored, and the next identical request from any account is answered from it at the hit price.
How fresh cached results are
A cached result is whatever the first caller got, and it can be older than a live search would be. We don't promise how long an entry stays. When freshness matters, set refresh: true or add an after: date to the query. The response header x-dipper-cache tells you whether you got a cached copy.
What counts as the same request
- Queries that differ only in case or whitespace share an entry.
max_resultsdoes not matter. A cached entry serves any value up to 50.- Results with
include_content: trueare cached separately. That entry also answers the same query withinclude_content: false.
Results are not localized to the caller's location. Use lang: in the query instead.
Refresh
Set refresh: true on a search or fetch to skip the cache. Dipper calls upstream right away, bills the call as a miss and returns x-dipper-cache: REFRESH. Your result then replaces the shared cached copy, so everyone who sends the same request afterwards gets it at the hit price.
- Refreshing a search with
include_content: truealso replaces theinclude_content: falseentry for that query. Refreshing withinclude_content: falseupdates only that entry. - Other edge locations pick up the new result within 10 minutes.
- If the refresh comes back empty, it is free and the old entry stays.
- If an identical request is already in flight, you get its result and pay the hit price.
MCP server
The same search and fetch tools are available as a remote MCP server. Caching, pricing and billing work exactly as they do over REST.
| URL | https://dipper-search.com/mcp |
| Transport | Streamable HTTP. Stateless, with JSON responses and no SSE. |
| Auth | Authorization: Bearer dk-... |
| Tools | web_search, web_fetch |
Claude Code
claude mcp add --transport http dipper https://dipper-search.com/mcp \
--header "Authorization: Bearer $DIPPER_API_KEY"
Add --scope user to use it in every project. Check the connection with claude mcp list, or /mcp inside a session.
Other clients
Most clients that support remote servers take a JSON config like this one. The exact file and the syntax for reading an environment variable depend on the client.
{
"mcpServers": {
"dipper": {
"type": "http",
"url": "https://dipper-search.com/mcp",
"headers": { "Authorization": "Bearer ${DIPPER_API_KEY}" }
}
}
}
Don't commit a config file with the key pasted in. Clients that only speak stdio can connect through the mcp-remote bridge.
Free MCP endpoint
To try Dipper before you sign up, point your client at https://dipper-search.com/mcp/free. It needs no key and has the same two tools.
claude mcp add --transport http dipper-free https://dipper-search.com/mcp/free
It runs on a small budget that everyone shares: about 100 uncached requests per hour across all users, and a cached result counts as 0.2 of a request. A single client can use at most a fifth of each hour's budget and send about 20 requests a minute. The budget resets on the hour (UTC). Once it runs out, tool calls return an error that says when it resets.
refresh doesn't work on the free endpoint. For that, and to get away from the shared limit, create an API key and use /mcp.
Tools
| Tool | Arguments |
|---|---|
web_search | query (required), max_results, include_content, refresh. Same rules as Search. |
web_fetch | url (required), refresh. Same rules as Fetch. |
web_search returns numbered results as text. web_fetch returns the page title and its content as Markdown. Both also put the same JSON as the REST response in structuredContent. Each result ends with a line like [dipper: cache HIT, cost $0.000160]. On the free endpoint that line ends in free endpoint instead of a cost.
A failed call comes back as a tool result with isError: true and a short message the model can read. Failed calls are not billed.
Pricing
You pay per successful call. Search and fetch cost the same.
| Per call | Per 1,000 calls | |
|---|---|---|
| Cache miss | $0.00080 | $0.80 |
| Cache hit | $0.00032 | $0.32 |
| Cache hit, launch offer | $0.00016 | $0.16 |
| Empty result or error | free | free |
The launch offer runs until 31 December 2026, 23:59 Singapore time (UTC+8). A call with refresh: true is billed as a miss. max_results and include_content do not change the price. The price is fixed when the request arrives, and x-dipper-cost-usd tells you what it was.
Billing is from a prepaid balance. There is no subscription. The smallest top-up is $10. To add balance, email hello@dipper-search.com. Card payments are coming. When the balance can't cover a call, you get 402 insufficient_balance.
Free credit
Sign in with GitHub and you get free credit, no card needed: 1,000 requests if your GitHub account is older than 30 days, 300 if it's newer. Each GitHub account can claim this once. If a newer account passes 30 days later, signing in again tops it up to 1,000.
Free credit is added to your balance as dollars, priced at the miss rate: 300 requests is $0.24 and 1,000 is $0.80. A cache hit uses less of it. During the launch offer a hit costs 0.2 of a request, and after it ends a hit costs 0.4.
Errors
Errors use an OpenAI-style envelope:
{ "error": { "message": "Insufficient balance. Top up to continue.", "type": "insufficient_balance" } }
| Status | error.type | When |
|---|---|---|
400 | invalid_request_error | The body is not JSON, or a field is missing or out of range. Fix the request. |
401 | authentication_error | The key is missing, malformed or revoked. Check the Authorization header. |
402 | insufficient_balance | Your balance can't cover the call. Add balance, then retry. |
403 | permission_error | The account is suspended. Contact support. |
429 | rate_limit_error | You went over the per-key rate limit. Back off and retry. |
503 | upstream_error, service_unavailable | Upstream search or billing is briefly unavailable. Retry with backoff. |
500, 502 and 504 can also happen and are safe to retry. Errors are never billed.
Rate limits
Each key can make about 50 requests every 10 seconds. The limit covers every authenticated request, including MCP calls and the account endpoints. Past it you get 429 rate_limit_error. Wait a few seconds and retry.
The free MCP endpoint has its own limits: about 20 requests a minute per client, and the shared hourly budget described there.