> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Screen markets across every venue

> Sweep the catalog with cursor pagination, filter on normalized fields, and stop correctly.

One request returns at most 100 markets. Screening the catalog means paginating, and doing it
with the cursor rather than with a page count.

## Sweep

`router` searches every venue at once; a venue id scopes it to one.

```sh
curl -s "$PREDICTEFY_API_URL/api/router/fetchMarkets?query=election&status=active&limit=100&sort=volume" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
```

The parameters that matter for screening:

| Parameter    | Values                                              |
| ------------ | --------------------------------------------------- |
| `status`     | `active`, `inactive`, `closed`, `resolved`, `all`   |
| `sort`       | `volume`, `liquidity`, `newest`                     |
| `limit`      | 1–100                                               |
| `searchIn`   | `title`, `description`, `both`                      |
| `searchMode` | `lexical` (default), `semantic`, `hybrid`           |

`searchMode` is worth knowing: the default is a literal match. `semantic` finds markets that
mean the same thing without sharing words, and `hybrid` does both — useful when you are
screening a topic rather than a phrase.

```js
async function screen({ query, status = 'active', limit = 100, maxPages = 20, venue = 'router' }) {
  const out = [];
  let cursor = null;
  let pages = 0;

  do {
    const url = new URL(`/api/${venue}/fetchMarkets`, BASE);
    url.searchParams.set('status', status);
    url.searchParams.set('limit', String(limit));
    if (query) url.searchParams.set('query', query);
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.PREDICTEFY_API_KEY}` },
    });
    const body = await res.json();
    if (!body.success) {
      // A cursor older than its TTL comes back as VALIDATION_ERROR. Restart the sweep.
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }

    out.push(...body.data);
    cursor = body.page?.hasMore ? body.page.nextCursor : null;
    pages += 1;
  } while (cursor && pages < maxPages);

  return out;
}
```

:::caution[Never compute a page count from `page.total`]
`page` carries `limit`, `offset`, `total`, `hasMore` and `nextCursor`. `total` is nullable, and
a null means **not counted** — not zero. A loop that stops when it has fetched `total` rows will
stop immediately on a null and silently return one page. Drive the loop on `hasMore`, and stop
when it is false.
:::

## Filtering on the normalized record

Every market comes back in the same shape whichever venue served it. The fields worth screening
on:

| Field                             | Note                                                  |
| --------------------------------- | ----------------------------------------------------- |
| `sourceExchange`                  | The venue that served this row — **not** `venue`      |
| `volume` / `volume24h`            | All-time and rolling; `volume24h` is nullable         |
| `liquidity`                       | Nullable                                              |
| `status`                          | Matches the filter vocabulary above                   |
| `category` / `tags`               | The venue's own vocabulary                            |
| `canonicalCategory` / `canonicalTags` | Predictefy's cross-venue vocabulary, both nullable |
| `outcomes`                        | The sides you can hold; books key on these            |
| `asOf` / `provenance` / `capabilities` | Required on every record — see below             |

Prefer `canonicalCategory` and `canonicalTags` when screening across venues: `category` is
whatever the venue calls it, so filtering on it gives different results per venue. See
[Categories & tags](/guides/categories-tags/).

```js
const shortlist = rows
  .filter((m) => (m.volume ?? 0) > 50_000)
  .sort((a, b) => (b.volume ?? 0) - (a.volume ?? 0))
  .slice(0, 25);
```

Note the `?? 0` on every nullable numeric. `volume24h` and `liquidity` are declared nullable, and
a null sorts unpredictably if you do not handle it.

## The honesty fields

`asOf`, `provenance` and `capabilities` are **required** on every market record — the spec marks
them so. They are the difference between a screener that is right and one that looks right:

- **`asOf`** — when the data was true. Show it.
- **`provenance`** — where it came from.
- **`capabilities`** — `read`, `trade`, `depth`, `history` for that record. Check `depth` before
  assuming you can size against a book, and read
  [Capability-honest data](/guides/honest-data/) for what each one does and does not promise.

Do not filter venues with a hard-coded list of which ones have real books. That list changes;
`capabilities` and [Venue coverage](/reference/venues/) do not go stale.

## Cost

A catalog read is the cheapest call on the platform, but a sweep is many of them: 20 pages is 20
reads. Cap `maxPages`, and prefer a narrower `query` or a `category` filter over paginating the
whole catalog. Current weights are in [Credits & billing](/guides/credits/).

`fetchMarketsPaginated` exists as an offset-paginated alternative when you genuinely need to
jump to a position rather than walk forward.

## Related

- [Categories & tags](/guides/categories-tags/) — canonical vs venue-native vocabulary
- [Identifiers](/guides/market-ids/) — which id each verb expects
- [Compare one market across every venue](/guides/cookbook/compare-across-venues/) — from a shortlist to a comparison
