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

  1. Sign in at /login/ with GitHub.
  2. 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.
  3. 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.

POST /v1/search runs a web search.

FieldTypeNotes
querystringRequired. 1 to 128 characters. Supports the operators below.
max_resultsinteger1 to 50, default 10. Does not change the price.
include_contentbooleanDefault false. When true, each result also carries the full page text in content. This is very large. Does not change the price.
refreshbooleanDefault 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.

OperatorMeaning
site:example.comOnly results from that site
-site:example.comExclude that site
lang:enResults in that language, for example lang:zh
after:2026-01-01Published on or after the date
before:2026-06-30Published 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 fieldDescription
titlePage title
urlPage URL
snippetShort excerpt from the page. May contain Markdown.
site_nameName of the site, or null
published_atPublication date in ISO 8601, or null if unknown
scoreRelevance score. Results are sorted by it, highest first. Higher is better, and it isn't limited to 0 to 1.
authoritySource authority rank, a small integer. Higher means more authoritative. May be null.
faviconSite icon URL, or null
contentFull 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.

FieldTypeNotes
urlstringRequired. An http or https URL, up to 8192 characters, with no username or password in it.
refreshbooleanDefault 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.

HeaderMeaning
x-dipper-cacheHIT: 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-usdWhat the call cost, in USD with 6 decimals, for example 0.000160. 0.000000 for errors and empty results.
x-request-idA 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_results does not matter. A cached entry serves any value up to 50.
  • Results with include_content: true are cached separately. That entry also answers the same query with include_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: true also replaces the include_content: false entry for that query. Refreshing with include_content: false updates 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.

URLhttps://dipper-search.com/mcp
TransportStreamable HTTP. Stateless, with JSON responses and no SSE.
AuthAuthorization: Bearer dk-...
Toolsweb_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

ToolArguments
web_searchquery (required), max_results, include_content, refresh. Same rules as Search.
web_fetchurl (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 callPer 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 errorfreefree

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" } }
Statuserror.typeWhen
400invalid_request_errorThe body is not JSON, or a field is missing or out of range. Fix the request.
401authentication_errorThe key is missing, malformed or revoked. Check the Authorization header.
402insufficient_balanceYour balance can't cover the call. Add balance, then retry.
403permission_errorThe account is suspended. Contact support.
429rate_limit_errorYou went over the per-key rate limit. Back off and retry.
503upstream_error, service_unavailableUpstream 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.