# WireForAI — Market News API

WireForAI provides clean, structured financial news for AI agents, trading bots and
analytics pipelines. Articles are enriched with ticker extraction, sentiment
analysis, and asset-class classification, and are searchable semantically.

Base URL: `https://wireforai.com` — all endpoints return JSON, no authentication.

Interactive documentation is also available: **[/docs](https://wireforai.com/docs)**
(Swagger UI), **[/redoc](https://wireforai.com/redoc)**, and the machine-readable
**[/openapi.json](https://wireforai.com/openapi.json)** (OpenAPI 3.1).

## Quick start

```bash
# Latest articles
curl "https://wireforai.com/api/articles?limit=5"

# Filter by asset class, source, or ticker
curl "https://wireforai.com/api/articles?asset_class=crypto&limit=5"
curl "https://wireforai.com/api/articles?source=CNBC%20Top%20News&limit=5"

# Semantic search
curl "https://wireforai.com/api/search?q=Fed+rate+decision&limit=5"

# What do we actually track?
curl "https://wireforai.com/api/coverage"

# Highest-impact recent articles
curl "https://wireforai.com/api/market/movers?limit=5"

# Sentiment for one ticker
curl "https://wireforai.com/api/market/sentiment?ticker=NVDA"
```

## Response fields

| Field | Meaning |
|---|---|
| `summary` | Publisher-supplied teaser. Present when the feed provides one. |
| `body_markdown` | Article body as markdown, **only when a body was actually extracted**. `null` when the feed carried only a teaser. This is **not** licensed full article text — most feeds do not publish bodies. Rely on `summary` plus the structured fields. |
| `total` | **True count of matching articles in the corpus**, independent of `limit`. Safe to drive pagination with. |
| `tickers` | Ticker symbols detected in the article. |
| `asset_class` | `equity` \| `crypto` \| `commodity` \| `forex` \| `bond`, or `null` when unclassified. |
| `sentiment` | `bullish` \| `bearish` \| `neutral` (keyword-based). |
| `mentioned_assets` | Ticker mentions with price/change where extractable. |
| `similarity` | Cosine similarity — present **only on semantic-search results**. The embedding model does not calibrate well in absolute terms, so if you need a stricter bar than the default, threshold on this yourself. |

## Coverage — read this before trusting a zero

`{"ticker":"IREN","articles":0}` is ambiguous on its own. Use `coverage` to tell
the two cases apart:

| `coverage` | Meaning |
|---|---|
| `tracked` | The ticker is in our universe. `articles: 0` means genuinely no recent news. |
| `not_tracked` | We do not track this name at all. A zero here says nothing about the company. |

`GET /api/coverage` lists the whole universe with per-ticker counts and `last_seen`.
Always check this before drawing a conclusion from an empty result.

## Endpoints

### Articles
```
GET /api/articles?limit=20&page=1&source=&asset_class=&ticker=&classified=
```
| Param | Effect |
|---|---|
| `source` | Exact source-name match |
| `asset_class` | `equity` / `crypto` / `commodity` / `forex` / `bond` |
| `ticker` | Article mentions this ticker |
| `classified` | `true` returns only articles with a resolved `asset_class` |
| `page`, `limit` | `limit` max 100; `total` reports the full count |

### Single article
```
GET /api/articles/{id}
```

### Semantic search
```
GET /api/search?q=...&limit=10&min_similarity=
```
Vector search over 384-dimension embeddings. A relevance floor is applied by
default so that queries with no matching content return an **empty list** rather
than unrelated articles. Override `min_similarity` (0.0–1.0) to trade precision
for recall.

### Ticker queries

If a query names a ticker we track (`NVDA`, `$IREN`), it is answered by **exact
lookup** on the `tickers` column rather than by similarity. So a name we hold no
articles for returns an empty list instead of unrelated stories:

```bash
curl "https://wireforai.com/api/search?q=IREN"
# {"articles":[],"total":0,"ticker":"IREN","coverage":"tracked"}

curl "https://wireforai.com/api/search?q=NVDA&limit=5"
# {"articles":[...5...],"total":320,"ticker":"NVDA","coverage":"tracked"}
```

Detection is strict: the token must be an exact uppercase match against the
tracked universe. `iren` (lowercase) and English words that happen to be symbols
(`so`, `arm`, `hood`) are treated as ordinary prose, because guessing would
silently turn a normal query into an empty one.

A ticker plus other terms ranks *within* that ticker's articles:
`q=NVDA earnings` searches what is being said about NVDA, not the whole corpus.

The response includes `ticker` and `coverage` (`tracked` / `not_tracked`) so an
empty result is never ambiguous. Prose queries are unaffected and still use
semantic search with the relevance floor.

### Market news
```
GET /api/market/news?asset_class=&ticker=&sentiment=&classified=&page=&limit=
```

### Market movers
```
GET /api/market/movers?limit=10&window_days=7
```
Ranked by ticker breadth decayed by age, within `window_days`. Round-up/listicle
titles are excluded. "Most ticker mentions" is a rough proxy for impact, not a
measured impact score.

### Ticker sentiment
```
GET /api/market/sentiment?ticker=NVDA&window_days=30
```
```json
{
  "ticker": "NVDA",
  "articles": 85,
  "returned": 50,
  "truncated": true,
  "window_days": 30,
  "coverage": "tracked",
  "sentiment_counts": {"bullish": 26, "bearish": 8, "neutral": 16},
  "weighted_sentiment": {"bullish": 25.1, "bearish": 7.6, "neutral": 15.2}
}
```
`articles` is the true total in the window; `returned` is how many were sampled for
the counts (cap 50). When `truncated` is true the counts describe a recent sample,
not the whole window. `weighted_sentiment` applies a per-source editorial weight
so a wire story and a listicle do not count equally.

### Asset-class counts
```
GET /api/market/assets
```

### Coverage
```
GET /api/coverage?window_days=30&tracked_only=true
```

### Agent-optimised search (MCP)
```
GET /api/mcp/search?q=...&limit=5
```
Returns both `articles` (full objects, identical shape to `/api/search`) and
`results` (a slim alias, **deprecated** — migrate to `articles`).

## Asset classes

`equity` · `crypto` · `commodity` · `forex` · `bond`

## Rate limits

| Endpoint | Rate | Burst |
|---|---|---|
| `/api/articles`, `/api/market/*`, `/api/coverage` | 30 req/min | 10 |
| `/api/search`, `/api/mcp/search` | 20 req/min | 5 |
| `/health`, static files | unlimited | — |

Exceeding a limit returns **HTTP 429** with accurate headers and a JSON body:

```
retry-after: 2
x-ratelimit-limit: 20 requests/minute
{"error":"rate_limited","retry_after_seconds":2,"limit":"20 requests/minute"}
```

`X-RateLimit-Remaining` is **not** provided — nginx cannot report per-zone
remaining quota, and we would rather omit it than report a wrong number.

## Notes for automated consumers

- **Use `total` for pagination**, not the page length.
- **Do not infer "no news" from an empty result** without checking `coverage`.
- **`body_markdown` is often null** by design — this is not an error.
- Set a descriptive `User-Agent`. Requests from Python's default `urllib` agent are
  currently rejected by our edge with an opaque `403`; `requests`, `httpx`, `curl`,
  Node and Go clients are unaffected.
- Corpus retention: articles back to 2025-09. `published_at` is not rewritten.
