# Search recipes

Written for an agent. Every example is a real call against this API.

## Pick the right tool first

Three of the six tools search. Which one you pick changes the answer's shape and
how much of your context window it costs.

| The question | Tool | Why |
|---|---|---|
| "What happened with X?" | `list_stories` | One event, returned once, with the outlets that covered it. Forty outlets running the same wire story collapse to one row. |
| "What has been written about X?" | `search_news` | Coverage itself is the answer — you want the articles, not the events. |
| "What is happening right now?" | `top_headlines` | Recency, not relevance. No query needed; filters still apply. |

**Reach for `list_stories` by default when results go into a context window.**
It is the single biggest token saving available here, and the article count and
outlet count tell you how big a story is — information plain search throws away.

## Ranking mode

`mode` is `keyword`, `semantic` or `hybrid`.

Use `hybrid` when *you* wrote the query. Models paraphrase — you will write
"central bank tightening" for an article that says "Fed raises rates" — and
keyword ranking cannot bridge that. Use `keyword` when the user gave you an
exact phrase, a product name, or a proper noun they want matched literally.

```
search_news { "q": "semiconductor export restrictions", "mode": "hybrid", "limit": 10 }
```

## Narrow with filters, not with more words

Adding words to `q` makes ranking fuzzier. Filters are exact matches on indexed
attributes and cost nothing:

```
search_news {
  "q": "port congestion",
  "language": "eng",
  "publisher_country": "SG",
  "from": "2026-08-01"
}
```

`topic`, `organization`, `person`, `country`, `location`, `city`, `region`,
`author`, `source`, `source_type`, `name`, `mentioned_date`, `quote_verb` and
`amount_object` all work the same way. They are exact — take the values from a
previous result rather than inventing them.

## Find out what exists before you filter

Facets count over the *whole* match set, not just the page you were handed:

```
search_news { "q": "lithium supply", "facets": ["source", "country", "topic"], "limit": 5 }
```

That tells you which outlets and countries are actually covering something, so
the next call can filter to one of them instead of guessing.

## Signals

Each article carries versioned industry, business-context and risk-context
intensities. Range-filter on them to find coverage with a particular character
rather than a particular word:

```
search_news {
  "q": "quarterly results",
  "signals": [{ "name": "financial_uncertainty", "min": 2.0 }]
}
```

Several constraints are ANDed. This is how you ask for "supply-chain news that
also reads as a disruption" without hoping the word "disruption" appears.

## Follow a thread

1. `search_news` or `list_stories` to find something.
2. `get_article` with the `id` for the full record — names, structured locations
   and dates, quotations, amounts, media, links.
3. `find_related` with the same `id` for what else covered it, deduplicated
   across outlets and excluding the seed.
4. Or filter a new search by the hit's `story_id` to expand one cluster into all
   its articles.

None of those needs a new query, so none of them risks drifting off the topic
the user actually asked about.

## Time ranges, and what your plan reaches

`from` and `to` accept `2026-07-09` or `20260709120000`.

**Omitting `from` is not free.** The corpus is sharded by time; a query with no
lower bound is a request to search every shard that has ever existed. Name the
window you actually mean.

Your plan also bounds it. If you ask for more history than the plan allows,
`from` is pulled forward rather than the request being refused, and the response
tells you:

```json
{ "history_days": 30, "from_clamped": true, "results": [...] }
```

`from_clamped: true` means the window you asked for was narrowed. Fewer results
than you expected is then a plan boundary you can see, not coverage you have to
guess at. Call `account_status` — it is free — to learn `history_days` up front
and pick a range that fits.

## Budget

Every call counts against a monthly allowance, whichever transport it arrives
on, and there is a per-minute rate on top. `account_status` reports the plan, the
per-minute rate, the quota and how much of it is used. Check it rather than
discovering the limits by hitting them.

When you do hit the per-minute rate, the refusal is transient — wait and retry.
When the monthly quota is exhausted on a key that stops at its quota, retrying
will not help until the period resets; the error says how long that is.
