# Universal search

`GET https://api.onekey.dev/v1/search/universal?q=...` takes one plain-language query and returns one ranked list. It picks the platforms, sends each a search that fits its own rules, runs the searches in parallel, and merges the answers by rank. It is built only from pieces that already exist and have their own pages: the search endpoints, [multi-platform search](https://onekey.dev/docs/search-multi.md), rank fusion, and the optional [relevance](https://onekey.dev/docs/relevance.md) and [themes](https://onekey.dev/docs/themes.md) steps.

Nothing is ranked by a judgment of ours unless you asked for one. The planner and the fusion are rules and arithmetic. The only model involved is the optional AI part, and the response says which method produced it.

```bash
curl "https://api.onekey.dev/v1/search/universal?q=rust%20async%20runtime%20on%20hackernews%20and%20devto" \
  -H "x-api-key: YOUR_API_KEY"
```

## When to use it

- You have a question in words, not a list of platforms, and you want one merged result.
- You want to see exactly what was asked of each platform. The plan is in the answer.
- You want the per-stage cost and timing in the answer, not a single opaque number.

If you already know the platforms and the exact query for each, [`/v1/search/multi`](https://onekey.dev/docs/search-multi.md) is the same fan-out without the planner.

## The pipeline

Every call runs up to five stages, in this order. `metadata.pipeline` lists all five with a `status` (`ok`, `partial`, `failed`, `skipped` or `unavailable`), the milliseconds it took, and the credits it cost.

| # | Stage | What runs | Price |
| --- | --- | --- | --- |
| 1 | `plan` | The planner reads the query and chooses platforms and a sub-query per platform. With `plan=ai` it adds one metered call (see [AI planning](#planning-with-ai)). | 0 for the default plan. |
| 2 | `fan_out` | One search per planned platform, in parallel, through the same machinery as [multi-platform search](https://onekey.dev/docs/search-multi.md): each leg is isolated, so one failing platform does not fail the call. | The legs that succeeded, each at its endpoint's price. A [cache hit](https://onekey.dev/docs/caching.md) is free, a failed leg is refunded. |
| 3 | `rerank` | Only with `relevance=`. Each row is judged against the cleaned query; `filter` drops rows under `threshold` before fusion. | Metered per judged row. |
| 4 | `fuse` | Reciprocal rank fusion (k = 60). When a relevance step ran, judged rows come first by `on_topic`, ties by fusion order. | 0. |
| 5 | `cluster` | Only with `themes=true`. Rows are clustered by their words and each cluster is named. | Metered. |

`metadata.credits_charged` is the sum of the credits of the five stages. The request log gets one 0-credit line per stage; the money is on the per-leg lines and the AI lines, as everywhere else, so nothing is counted twice.

## Parameters

`GET` with query parameters, or `POST` with the same names as a JSON body (`platforms` may then be a list). Unknown parameters are refused with [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params).

| Parameter | Values | What it does |
| --- | --- | --- |
| `q` | text, required, up to 200 characters | The query. Platform hints in it (below) choose platforms and are removed before searching. |
| `platforms` | comma list | Search exactly these platforms. Each must have a search endpoint, else [`UNSUPPORTED_PLATFORM`](https://onekey.dev/docs/errors.md#unsupported-platform). Wins over hints in `q`. |
| `category` | a [docs category](https://onekey.dev/docs/categories.md) id | Restrict automatic selection to one category. |
| `max_platforms` | 1 to 12, default 6 | Cap on the platform list. Naming more platforms than this is a free [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params) that tells you to raise it. |
| `limit_per_platform` | 1 to 100 | Trim each platform's rows before fusion. The leg is still fetched and charged at its own page size. |
| `plan` | `deterministic` (default) or `ai` | See [AI planning](#planning-with-ai). |
| `relevance` | `score` or `filter` | Judge each row against the cleaned query. See [Relevance](https://onekey.dev/docs/relevance.md). |
| `threshold` | 0 to 1, default 0.5 | Cut-off for `relevance=filter`. |
| `themes` | `true` | Cluster the kept rows and name each cluster. See [Themes](https://onekey.dev/docs/themes.md). |
| `strict` | `true` (default) or `false` | What happens when an AI step was asked for and nothing could be judged. See [Without the AI part](#without-the-ai-part). |
| `dry_run` | `true` | Plan and price the call without running it. See [Dry run](#dry-run). |

`include` and `label` are not accepted here. For per-row labels use [`/v1/search/multi`](https://onekey.dev/docs/search-multi.md) or the [brand mentions report](https://onekey.dev/docs/reports/brand-mentions.md).

## The planner

The planner is deterministic. The same query and the same catalog always give the same plan, and the plan is returned in `metadata.plan`.

### Platform hints

Words in the query can choose the platforms:

- A preposition followed by one or more names: `on reddit`, `from hackernews and devto`, `in hn, gitlab or bluesky`. The prepositions are `on`, `in`, `from`, `via`, `at` and `across`.
- A name followed by a content noun: `github repos about x`.
- `r/rust` means Reddit. The subreddit name stays a keyword, because the Reddit search cannot be limited to a subreddit.
- Common aliases work: `hn`, `hacker news`, `twitter`, `bsky`, `stack overflow`, `hugging face`, `dev.to`, `docker hub`, `wiki`, `yt`, `gh`, `ig`.

The hints, and leading phrases such as "search for", "find me" and "show me", are removed. What is left is the `cleaned` query. A hint that names a platform with no search endpoint is recorded with `supported: false`. If every hint is unsupported, the call is a free [`NO_SEARCHABLE_PLATFORMS`](https://onekey.dev/docs/errors.md#no-searchable-platforms).

### What else the plan records

- **Quoted phrases** are kept in the sub-query and listed in `phrases`.
- **Keywords** are the cleaned query's words minus a short stopword list.
- **Language** is detected from the cleaned query. It is `null` when unclear; short queries are `null` by design.

### Choosing platforms

In this order: your explicit `platforms`, else the hinted platforms, else automatic selection over every search platform (optionally limited by `category`). Automatic selection is deterministic: stable before beta, then platforms whose search accepts AI enrichment (so rerank and themes can cover them), then cheaper first, then by platform id. It is capped at `max_platforms`. `considered` and `capped` say what was cut.

### Fitting the query to each platform

Each platform's search has its own parameter rules. The planner tries the cleaned query first, then the keywords joined by a space, then the first keyword alone. `plan.adapted` says which form a platform got, and `plan.skipped` lists platforms no form fits. Those get no leg: nothing is sent blind.

### What the planner cannot do

- **It is string rules, not understanding.** A hint needs a preposition or a content noun. A bare platform name, as in "reddit rust", is not a hint. `github issues about x` selects GitHub's one search endpoint, which searches repositories, not issues, and the hint's `note` says so.
- **Tag-style searches get one keyword.** Some searches take a tag or a single token rather than free text. The DEV Community search accepts only `[a-z0-9]{1,40}`, so it always gets one keyword: "rust "async runtime"" becomes `rust` there. The planner fits by the endpoint's parameter rules only. It cannot know what a platform semantically wants beyond what those rules say.
- **Reranking is per platform.** The relevance judgment compares each row with the cleaned query. There is no cross-platform reranking model.

## Platforms it can search

19 platforms have a search endpoint today, and 7 of them also accept relevance and themes. The table is generated from each endpoint's own declaration.

| Platform | Search leg | List price (credits) | Status | Relevance and themes |
| --- | --- | --- | --- | --- |
| [Bluesky](https://onekey.dev/docs/api/bluesky/search.md) | `bluesky.search` | 1 | stable | yes |
| [crates.io](https://onekey.dev/docs/api/crates/search.md) | `crates.search` | 5 | stable | no |
| [DEV Community](https://onekey.dev/docs/api/devto/search.md) | `devto.search` | 5 | stable | yes |
| [Docker Hub](https://onekey.dev/docs/api/dockerhub/search.md) | `dockerhub.search` | 5 | stable | no |
| [GitHub](https://onekey.dev/docs/api/github/search_repos.md) | `github.search_repos` | 5 | stable | no |
| [GitLab](https://onekey.dev/docs/api/gitlab/search.md) | `gitlab.search` | 5 | stable | yes |
| [Hacker News](https://onekey.dev/docs/api/hackernews/search.md) | `hackernews.search` | 1 | stable | yes |
| [Huggingface](https://onekey.dev/docs/api/huggingface/models.md) | `huggingface.models` | 5 | stable | no |
| [Lemmy](https://onekey.dev/docs/api/lemmy/search_posts.md) | `lemmy.search_posts` | 5 | stable | yes |
| [Mastodon](https://onekey.dev/docs/api/mastodon/search_accounts.md) | `mastodon.search_accounts` | 5 | stable | no |
| [npm](https://onekey.dev/docs/api/npm/search.md) | `npm.search` | 5 | stable | no |
| [Reddit](https://onekey.dev/docs/api/reddit/search_posts.md) | `reddit.search_posts` | 2 | beta | yes |
| [SoundCloud](https://onekey.dev/docs/api/soundcloud/search_tracks.md) | `soundcloud.search_tracks` | 3 | beta | no |
| [Stack Exchange](https://onekey.dev/docs/api/stackexchange/search.md) | `stackexchange.search` | 5 | stable | yes |
| [TikTok](https://onekey.dev/docs/api/tiktok/search_videos.md) | `tiktok.search_videos` | 2 | beta | no |
| [Twitch](https://onekey.dev/docs/api/twitch/search_channels.md) | `twitch.search_channels` | 5 | beta | no |
| [Wikipedia](https://onekey.dev/docs/api/wikipedia/search.md) | `wikipedia.search` | 5 | stable | no |
| [X (Twitter)](https://onekey.dev/docs/api/x/search.md) | `x.search` | 2 | stable | no |
| [YouTube](https://onekey.dev/docs/api/youtube/search_videos.md) | `youtube.search_videos` | 2 | stable | no |

A platform with "no" in the last column is still fetched and fused. Its rows are never judged (`relevance.unsupported` lists it) and its fused entries have `on_topic: null` and come after the judged ones.

## A recorded call

A dry run of a query with two hints, so the plan can be read without spending anything (recorded on the dev API, shortened):

```bash
curl "https://api.onekey.dev/v1/search/universal?q=rust%20%22async%20runtime%22%20on%20hackernews%20and%20devto&dry_run=true" \
  -H "x-api-key: YOUR_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "plan": {
      "method": "deterministic",
      "query": "rust \"async runtime\" on hackernews and devto",
      "cleaned": "rust \"async runtime\"",
      "keywords": ["rust", "async", "runtime"],
      "phrases": ["async runtime"],
      "language": null,
      "hints": [
        { "text": "on hackernews and devto", "platform": "hackernews", "supported": true },
        { "text": "on hackernews and devto", "platform": "devto", "supported": true }
      ],
      "selection": "hints",
      "platforms": ["hackernews", "devto"],
      "endpoints": { "hackernews": "hackernews.search", "devto": "devto.search" },
      "subqueries": { "hackernews": "rust \"async runtime\"", "devto": "rust" },
      "considered": 2, "capped": false, "max_platforms": 6, "limit_per_platform": null, "category": null,
      "adapted": { "devto": "first keyword only" },
      "skipped": {}
    },
    "legs": [
      { "platform": "hackernews", "endpoint": "hackernews.search", "params": { "q": "rust \"async runtime\"" }, "credits": 1.0, "worst_case_credits": 1.0 },
      { "platform": "devto", "endpoint": "devto.search", "params": { "q": "rust" }, "credits": 5.0, "worst_case_credits": 5.0 }
    ],
    "credits": { "legs_worst_case": 6.0, "legs_list_price": 6.0, "ai": null, "note": "AI steps (relevance, themes, labels) are metered per judged row after the fetch and cannot be priced before the rows exist; cache hits make legs cheaper than the worst case" },
    "balance_credits": 790.917,
    "balance_covers_worst_case": true,
    "ai_enabled": true
  },
  "metadata": { "recipe": "search_universal", "dry_run": true, "legs": 2, "credits_charged": 0.0, "request_id": "...", "idempotent_replay": false }
}
```

Read the plan: two hints chose the platforms, the DEV Community search got only the first keyword, and the plan says so in `adapted`.

## The response

Without the AI part, the answer has the same shape as [multi-platform search](https://onekey.dev/docs/search-multi.md) with `fuse=rrf`. `data.<platform>` holds each platform's rows, keeping their normal shape. `data.fused` is always present. `metadata` adds `plan`, `fuse`, `pipeline` and the usual `legs`, `succeeded`, `failed` and `credits_charged`.

| Field | Meaning |
| --- | --- |
| `data.<platform>` | One row per leg: `status`, `data` (its rows), `error`, `credits_charged`, `cached`, `pagination`. |
| `data.fused` | The merged list. Each entry has `score`, `platform`, `platforms`, `ranks` (rank per platform), `key` (how duplicates were matched) and `item`, the row itself. |
| `metadata.plan` | The plan, as above. |
| `metadata.pipeline` | The five stages, in order. |
| `metadata.credits_charged` | Total charged, AI included. |
| `metadata.extra_credits` | Present when an AI step ran: the part of the total that is AI. |

A call whose legs all fail answers with the first leg's error, and nothing is charged. Otherwise a failed platform is a row with its `status` and `error` next to the ones that worked.

## The optional AI part

> [!NOTE] Everything above this heading (planning, fan-out, fusion) is deterministic and needs no AI. The sections below only apply when you ask for `plan=ai`, `relevance` or `themes`.

### Relevance and themes

> [!NOTE] **Not enabled yet.** AI enrichment ships when a production model is connected; every number on this page comes from the pre-release test provider and will change. It is switched on per account, so nothing here describes real-world judgment quality yet. Where it is not enabled, a request that asks for it with the default `strict=true` is refused with `503 AI_UNAVAILABLE` at no charge; with `strict=false` you get the rows back unjudged. Enrichment is also switched on per platform: only the endpoints in the [table on the Labels page](https://onekey.dev/docs/labels.md#which-endpoints-accept-it) accept these parameters.

`relevance=filter` or `relevance=score` judges each row's `on_topic` probability against the cleaned query. `filter` also drops the rows under `threshold` and lists them in `dropped_ids`; a row that could not be judged is never dropped. `themes=true` clusters the kept rows of the platforms that accept enrichment, and names each cluster. Both blocks are the same ones described on [Relevance](https://onekey.dev/docs/relevance.md) and [Themes](https://onekey.dev/docs/themes.md), inside `data` beside the rows and `fused`.

The same query for real, with both steps (recorded on the dev API with the pre-release test provider, heavily shortened; 20 rows were fetched, 4 DEV Community rows were dropped as off topic, 16 were clustered):

```bash
curl "https://api.onekey.dev/v1/search/universal?q=rust%20%22async%20runtime%22%20on%20hackernews%20and%20devto&relevance=filter&themes=true" \
  -H "x-api-key: YOUR_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "hackernews": { "platform": "hackernews", "status": 200, "data": ["..."], "credits_charged": 9.541, "extra_credits": 4.541, "dropped": 0, "dropped_ids": [] },
    "devto": { "platform": "devto", "status": 200, "data": ["..."], "credits_charged": 11.185, "extra_credits": 6.185, "dropped": 4, "dropped_ids": ["devto:4773537", "..."] },
    "fused": [
      {
        "score": 0.016393, "platform": "hackernews", "platforms": ["hackernews"], "ranks": { "hackernews": 1 }, "key": "url", "on_topic": 1.0,
        "item": {
          "content": { "id": "29493340", "title": "Monoio – A thread-per-core Rust async runtime with io_uring", "url": "https://github.com/bytedance/monoio" },
          "author": { "username": "losfair" },
          "engagement": { "likes": 158, "comments": 81 },
          "computed": { "engagement_rate": null, "language": null, "relevance": { "on_topic": 1.0 } }
        }
      }
    ],
    "relevance": { "mode": "sync", "legs": 2, "status": "complete", "query": "rust \"async runtime\"", "threshold": 0.5, "rows": 20, "labelled": 20, "dropped": 4, "extra_credits": 10.726, "methodology_version": "fake/1" },
    "dropped": 4,
    "themes": { "status": "complete", "rows": 16, "clustered": 16, "named": true, "extra_credits": 4.641, "methodology_version": "fake/1", "clustering": "cluster/1", "themes": [ { "theme": "Runtime Rust Async", "size": 10, "terms": ["runtime", "rust", "async", "tokio", "io"] } ] }
  },
  "metadata": {
    "recipe": "search_universal",
    "plan": { "...": "as above" },
    "fuse": { "method": "rrf", "k": 60, "reranked_by": "on_topic" },
    "ai": { "strict": true, "relevance": "filter", "threshold": 0.5, "themes": true },
    "legs": 2, "succeeded": 2, "failed": 0, "credits_charged": 25.367, "extra_credits": 15.367,
    "pipeline": [
      { "stage": "plan", "status": "ok", "ms": 1.8, "credits": 0.0, "method": "deterministic" },
      { "stage": "fan_out", "status": "ok", "ms": 1414.0, "credits": 10.0, "legs": 2, "succeeded": 2, "failed": 0 },
      { "stage": "rerank", "status": "ok", "ms": 27.7, "credits": 10.726, "mode": "filter", "threshold": 0.5, "dropped": 4, "methodology_version": "fake/1" },
      { "stage": "fuse", "status": "ok", "ms": 0.4, "credits": 0.0, "items": 15, "reranked_by": "on_topic" },
      { "stage": "cluster", "status": "ok", "ms": 15.0, "credits": 4.641, "themes": 7, "methodology_version": "fake/1" }
    ]
  }
}
```

Two things to read honestly here. First, `methodology_version` here is the test provider's method id: the pre-release test provider, a keyword heuristic. Its `on_topic` is word overlap: the DEV Community leg was searched for `rust` only, so it lost 4 of 10 rows against "rust async runtime". A production method would judge differently, and these figures are not a measure of quality. Second, 16 rows were kept and the fused list has 15 entries.

### Planning with AI

> [!NOTE] **Not enabled yet.** This part is an AI step: it ships when a production model is connected and is switched on per account; the numbers shown come from the pre-release test provider and will change. It is metered like the other [enrichment](https://onekey.dev/docs/ai-pricing.md) steps. The rest of this page needs no AI.

`plan=ai` adds one metered call over the candidate platforms (at most 12, in the deterministic order). The provider scores how useful each platform is for the query (`use`, 0 to 1) and rewrites the sub-query. Platforms scoring 0.5 or more are kept, capped by `max_platforms`, and every rewritten sub-query is still fitted to that platform's rules, so an AI plan cannot send a value the endpoint would refuse.

**`plan=ai` falls back to the deterministic plan, and says why.** When the provider is off, when it is the pre-release test provider, after a provider failure, and when it selects nothing, the deterministic plan runs at no charge, and `plan.ai` carries a `reason` and a `note`. Recorded on the pre-release test provider:

```json
{
  "plan": {
    "method": "deterministic",
    "selection": "auto",
    "platforms": ["bluesky", "devto", "gitlab"],
    "subqueries": { "bluesky": "postgres vector search", "devto": "postgres", "gitlab": "postgres vector search" },
    "adapted": { "devto": "first keyword only" },
    "ai": { "status": "unavailable", "extra_credits": 0.0, "methodology_version": "none", "candidates": 0, "chosen": 0, "reason": "fake_provider", "note": "the fake provider cannot plan; deterministic plan used (nothing charged)" }
  }
}
```

The value of `reason` is `provider_off`, `fake_provider` or a provider failure code. The metered path is exercised in tests with a stub provider only: `plan=ai` has not been run against a production model, and this page makes no claim about the quality of its plans. A dry run skips the AI plan (`reason: dry_run`), so it really costs 0.

## Without the AI part

When AI is not enabled for your account:

- A call with `relevance` or `themes` and the default `strict=true` is refused with [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) (503) before anything is fetched, so it costs nothing.
- With `strict=false` it answers 200 at the legs' price only: `relevance.status` is `unavailable` with `reason: provider_off`, nothing is dropped, themes come back named `Theme 1`, `Theme 2` and so on with `named: false`, and the AI stages in `pipeline` show `unavailable` with 0 credits.
- Planning, fan-out and fusion work exactly as normal.

## Dry run

`dry_run=true` returns what the call would do, for 0 credits: the plan, each leg with its endpoint, params, list price and worst-case price, your balance, whether the balance covers the worst case, and `ai_enabled`. Nothing runs and an `Idempotency-Key` header is ignored. It works even when AI is off (`ai_enabled` says so).

Only the search legs can be priced beforehand. AI steps are metered per judged row after the rows exist, so a dry run says `credits.ai: null` and explains why. Use the worst case as the ceiling for the legs and add the AI part separately.

## Costs

- **Search legs** are the normal price of each platform's search endpoint (see the table above), charged per leg that succeeded. A [cache hit](https://onekey.dev/docs/caching.md) is free, and a failed leg is refunded.
- **Planning, fusion and the request itself** are free. A default plan costs 0.
- **Relevance and themes** are metered per judged row, exactly as on [AI pricing](https://onekey.dev/docs/ai-pricing.md): reserved, settled to actual, cached and unjudged rows free.
- **A worst-case check runs first.** If the legs' worst case is above your balance the call is refused before anything runs, with [`INSUFFICIENT_CREDITS`](https://onekey.dev/docs/errors.md#insufficient-credits). An AI reservation that your key's [credit limit](https://onekey.dev/docs/key-limits.md) cannot cover is [`KEY_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#key-budget-exceeded) naming the step; the legs stay paid, and because their data is cached, a retry is a free hit.
- A call counts as one request against your [rate limit](https://onekey.dev/docs/rate-limits.md).

Measured on the pre-release test provider (synthetic usage, so it shows how the charge scales, not what production judgments will cost): the call in the recorded example cost 10 credits for the two legs, 10.726 for 20 relevance judgments (0.54 per row) and 4.641 for naming 7 clusters, 25.367 in all. The same call again cost 0.6: two cache hits on the legs and every judgment cached. Do not budget AI from these numbers.

## Streaming

Send `Accept: text/event-stream` and the call answers as [Server-Sent Events](https://onekey.dev/docs/streaming.md). Frames arrive in this order:

| Event | How many | Payload |
| --- | --- | --- |
| `plan` | One, first | The plan, before anything is fetched. |
| `leg` | One per leg, as each settles | One row of the JSON form, as on [Streaming](https://onekey.dev/docs/streaming.md#frame-types). |
| `fused` | One | The complete `data.fused` list. |
| `enrich` | One per AI step that ran (`relevance`, `themes`) | `{"step": "relevance", "summary": {...}, "legs": [...]}` |
| `done` | One, last | The JSON form's `metadata` (`pipeline` and `plan` included) plus `credits_remaining`. |

Recorded (shortened), with `relevance=score` on the pre-release test provider:

```text
event: plan
data: {"method":"deterministic","hints":[...],"selection":"hints","platforms":["hackernews","devto"],"subqueries":{"hackernews":"zig compiler","devto":"zig"},...}

event: leg
data: {"index":0,"platform":"hackernews","endpoint":"search","params":{"q":"zig compiler"},"status":200,"data":[...],"credits_charged":5.0,"cached":false,...}

event: leg
data: {"index":1,"platform":"devto","endpoint":"search","params":{"q":"zig"},"status":200,"data":[...],"credits_charged":5.0,"cached":false,...}

event: fused
data: [{"score":0.016393,"platform":"hackernews","ranks":{"hackernews":1},"on_topic":1.0,...}, ...]

event: enrich
data: {"step":"relevance","summary":{"legs":2,"status":"complete","rows":20,"labelled":20,"extra_credits":10.253,"methodology_version":"fake/1",...},"legs":[...]}

event: done
data: {"recipe":"search_universal","legs":2,"succeeded":2,"credits_charged":20.253,"extra_credits":10.253,"credits_remaining":745.297,"pipeline":[...],"plan":{...},...}
```

Anything refused up front (bad parameters, an unsupported platform, a worst case above your balance or key limit) is an ordinary JSON error, never a stream. Check the `Content-Type` before parsing. A replayed stream has the same frame order.

## Idempotency

`GET` and `POST` share one fingerprint, so an [`Idempotency-Key`](https://onekey.dev/docs/idempotency.md) sent on one form replays on the other. A replay lasts 24 hours and answers with `credits_charged: 0` and `idempotent_replay: true`, with the stored `plan` and `pipeline` included. A streamed `GET` that carries the header is [`INVALID_REQUEST`](https://onekey.dev/docs/errors.md#invalid-request) (400); a streamed `POST` accepts it. A different set of AI parameters is a different request ([`IDEMPOTENCY_KEY_REUSED`](https://onekey.dev/docs/errors.md#idempotency-key-reused)).

## POST

The same call as a JSON body, with a platform list and a per-platform cap:

```bash
curl -X POST "https://api.onekey.dev/v1/search/universal" \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{"q": "postgres vector search", "platforms": ["hackernews", "devto"], "limit_per_platform": 3}'
```

Recorded: 200, `credits_charged: 5.0` (the Hacker News leg was a cache hit), `data.fused` with 6 entries, and `plan.subqueries` of `"postgres vector search"` for Hacker News and `"postgres"` for DEV Community.

## Errors

| Status | Type | When |
| --- | --- | --- |
| 422 | [`NO_SEARCHABLE_PLATFORMS`](https://onekey.dev/docs/errors.md#no-searchable-platforms) | The platform hints, the `category` or the automatic selection left no platform with a search endpoint, for example `x on instagram`. Free. |
| 422 | [`UNSUPPORTED_PLATFORM`](https://onekey.dev/docs/errors.md#unsupported-platform) | A platform in `platforms` has no search endpoint. Free. |
| 422 | [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params) | `q` missing, empty after removing hints, or over 200 characters; a value out of range; an unknown parameter; `include` or `label`. Free. |
| 402 | [`INSUFFICIENT_CREDITS`](https://onekey.dev/docs/errors.md#insufficient-credits) | The worst case of the legs is above your balance. Nothing ran. |
| 402 | [`KEY_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#key-budget-exceeded) | The key's limit cannot cover the legs, or an AI reservation. |
| 503 | [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) | `relevance` or `themes` was requested, AI is not enabled for your account, and `strict` is `true`. Free, before any fetch. |
| 429 | [`AI_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#ai-budget-exceeded) | A daily AI allowance is used up and nothing could be judged. |

The recorded [`NO_SEARCHABLE_PLATFORMS`](https://onekey.dev/docs/errors.md#no-searchable-platforms) for a query whose only hint has no search endpoint:

```json
{
  "success": false,
  "data": null,
  "error": {
    "code": "no_searchable_platforms",
    "type": "NO_SEARCHABLE_PLATFORMS",
    "message": "The platform(s) named in the query have no search endpoint: instagram",
    "doc_url": "https://onekey.dev/errors/NO_SEARCHABLE_PLATFORMS"
  }
}
```

## Limits, in one list

- The planner is string rules: hints need a preposition or a content noun.
- Tag-style searches receive one keyword.
- Reranking is per platform against the cleaned query. There is no cross-platform model.
- AI parts run on the pre-release test provider only. `plan=ai` is untested against a production model.
- Timings (`ms`) for `fan_out` cover the per-leg pipeline; the AI stages' `ms` are the enrichment calls.

## Next

- [Multi-platform search](https://onekey.dev/docs/search-multi.md): the fan-out and fusion without the planner.
- [Relevance](https://onekey.dev/docs/relevance.md), [Themes](https://onekey.dev/docs/themes.md) and [AI pricing](https://onekey.dev/docs/ai-pricing.md).
- [Reports](https://onekey.dev/docs/reports.md): brand mentions, creator vetting and demand signals, built on the same legs.
- The hosted [MCP server](https://onekey.dev/docs/mcp.md) exposes this as the `search_universal` tool.
