#17397
fix(backend): Search spend: analytics shows flat price × count, Costs page omits search, several providers bill variably
Summary
Search spend is hard to see and, for several providers, recorded below what the provider bills:
- The Search analytics page does not use per-call cost.
/api/v1/search/analyticscomputes cost as registrycostPerQuery× number of calls (src/app/api/v1/search/analytics/route.ts:37).call_logshas no cost column, so the page cannot reflect per-mode prices, extra results or content surcharges, even when the handler computed them (#17394, #17396). - The Costs dashboard never shows search spend.
/dashboard/costsreadsusage_history/account_eventsthrough/api/usage/analytics, and its Pricing tab lists model prices only. No search provider appears there. - The only accurate per-call record is
request_cost_ledger(service_tier = "search", written byrecordSearchUsageCost()inopen-sse/handlers/search/reportedCost.ts). It feeds per-key USD quotas, and a row is written only when the search came in with an OmniRoute API key. - Several providers bill variably, but the registry records a flat price. Audit below.
Provider pricing audit (open-sse/config/searchRegistry.ts, release/v3.8.53)
How each row was checked: live = real API calls comparing Exa's costDollars; docs = the provider's own pricing docs; 3rd-party = only non-provider sources found, unverified; not checked = flat price assumed.
| Provider | Registry | What the provider bills | Gap | Source |
|---|---|---|---|---|
exa-search | $0.007 flat | auto $0.007 / lite $0.004 for 10 results, +$0.001 per extra result, +$0.001 per result with contents.summary | Fixed by #17394 + #17396 (the recorded cost matches costDollars in every live case) | live |
tavily-search | $0.008 flat | basic 1 credit, advanced 2 credits, $0.008/credit | search_depth: "advanced" (allow-listed in provider_options) recorded at half price. fast / ultra-fast not covered by the credits page. The include_usage passthrough may let the response report credits (needs checking). | docs |
linkup-search | $0.005 flat | fast/standard $0.005, deep $0.05; +$0.001 for sourcedAnswer/structured | depth: "deep" (a provider setting the builder honors) recorded at 1/10th | docs |
firecrawl | $0.002 flat | 2 credits per 10 results, rounded up; scraping adds per-page credits; $/credit depends on the plan (~$0.001–0.005) | Requests above 10 results (max 100) under-recorded; $/credit is plan-specific, so a static price can only be approximate | docs |
serper-search | $0.001 flat | Reportedly 1 credit up to 10 results, 2 credits for 11–100 | num > 10 possibly recorded at half price | 3rd-party (1, 2); Serper's own pricing page returns 404 |
x-search | $0 | A Grok call with the x_search tool: model tokens plus tool fees (reportedly per post/profile fetched since 2026-09-21) | Recorded at $0 although every call is paid | 3rd-party (1, 2); xAI tool docs say tools bill tokens + invocations |
perplexity-search | $0.005 flat | $5/1k; search_type: "fast" $1/1k | None today (the builder doesn't send fast) | docs |
jina-search | $0.002 flat | Token-based, at least 10,000 tokens per search | Depends on plan pricing; not verified | docs (price table not reachable) |
parallel-search | per mode + overage | Already modelled with costByMode / includedResults | — | existing |
brave-search, google-pse-search, searchapi-search, youcom-search, nimble-search | flat | — | Not checked. You.com livecrawl (sent when full_page is requested) may cost more | not checked |
searxng-search, duckduckgo-free, context7, ollama-search, zai-search, anysearch-search, xquik-search | $0 or small flat | free, self-hosted or flat | Not checked | — |
Questions for the maintainer
- Analytics source of truth. Should the Search analytics page sum real per-call cost? Two ways: add a
cost_usdcolumn tocall_logsfor search rows (a migration), or readrequest_cost_ledger(which misses calls made without an API key). - Costs dashboard. Should search spend appear on
/dashboard/costs, for example as a "Search" service tier or provider rows next to LLM spend? - Provider-reported cost. Exa returns
costDollars.totalon every response, and Tavily can return usage withinclude_usage. TodayreportedCost.tsonly trusts a reported cost when the operator opts in (trustReportedCost), to stop a spoofed endpoint setting its own price. For providers whose base URL callers cannot override (allauthType: "apikey"providers, GHSA-3f8g-pfh9-j687), should their native cost field be trusted by default, behind the opt-in, or not at all? - Static pricing fixes. Are PRs like #17396 welcome for Tavily
advanced, Linkupdeepand Firecrawl's per-10-results billing? Those three are documented by the providers themselves. Serper and xAI need someone with a key to confirm first.
I'm happy to implement whichever direction you choose. #17394 and #17396 cover Exa only and don't depend on these answers.
On the
nimble-searchrow, which is currently "not checked": Nimble prices bysearch_depth, not as a flat per-query rate.litestandard(API default)full_content: trueOnly successful requests are billed. The free tier is 5,000 requests per month with no card required. The Search rate limit is 10 QPS per account, and requests over it get a
429withRetry-After.Pricing: https://docs.nimbleway.com/nimble-sdk/admin/pricing Search API: https://docs.nimbleway.com/nimble-sdk/web-tools/search
Happy to confirm any other Nimble billing details the audit needs.