---
title: "Matched Clusters"
description: "How AGG links the same event across venues, and which id to use as your cross-venue join key"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

The same real-world question is listed separately on every venue. AGG's matching pipeline links
those listings into a **matched cluster**, and discovery responses return the cluster already
assembled — you do not need to match markets yourself.

This page explains what a cluster is, which id identifies it, and where each cluster field lives on
the response.

## What a cluster is

A cluster is a set of venue events that AGG has determined are the same real-world event, plus the
market- and outcome-level pairings between them.

```
Matched cluster
  ├── venueEvent (polymarket)   ← anchor: the row discovery returns
  ├── venueEvent (kalshi)       ← member
  ├── venueEvent (predict)      ← member
  └── venueEvent (limitless)    ← member

Each venueEvent
  ├── matchedVenueEvents[]            ← the same event on the other venues
  └── venueMarkets[]
        ├── venueMarketOutcomes[]     (Yes / No)
        │     └── matchedVenueMarketOutcomes[]  ← the aligned outcome elsewhere
        └── matchedVenueMarkets[]     ← the same market on the other venues
```

The cluster is exposed at all three levels, and each level is **symmetric**: you get the whole set
regardless of which member you started from.

Matching combines deterministic canonicalization, in-house LLM scoring, and manual verification.
Its **output** is the cluster you see on the response — you consume the result, not the process.

## Which id to join on

`GET /venue-events` returns **one row per cluster**. Every cluster has one **anchor** event, and the
list collapses each cluster down to it — member rows are not returned separately.

<Tip>
  **Use the `id` of the event returned by `GET /venue-events` as your cross-venue key.** It is the
  cluster's canonical handle, and it is the only identifier that refers to the cluster rather than
  to one venue's listing of it.
</Tip>

Do not invent your own *cross-venue* key from titles, slugs or `aggKey` (see
[Where aggKey fits](#where-aggkey-fits) below), and do not assume two venues' identifiers can be
related to each other without AGG's matching — that is the problem matching exists to solve. Storing
a *single* venue's identifier as a durable handle you re-resolve from is a different thing, and is
supported: see the tip below.

<Warning>
  **Re-resolve rather than treating the id as permanent.** Clusters can merge as matching improves;
  when they do, the surviving cluster keeps one anchor and the other's id becomes an ordinary member
  id. It stays a valid event id and still resolves, but it no longer represents the cluster.

  Caching it for a session or a page is fine. If you store it long-term — a watchlist, a saved
  view — refresh it from discovery rather than assuming it will always come back as the cluster
  head.
</Warning>

<Tip>
  **For long-term storage, key on a venue's own identifier instead.** A cluster anchor can change
  when clusters merge; `(venue, externalIdentifier)` never does — it is the venue's own permanent
  handle for that listing. Store that, then resolve it back to the live cluster whenever you need it
  via [Starting from a venue's own identifier](#starting-from-a-venue-identifier) below. This
  is the durable pattern for watchlists, positions, reconciliation and anything you persist.
</Tip>

## Cluster fields, and where they live

Two different scopes are easy to confuse:

| Field | Scope | What it tells you |
|-------|-------|-------------------|
| `venues[]`, `venueCount` | the **event row** | Which venues that row's cluster spans — use it for venue badges. |
| `matchedVenueEvents[]` | the **event row** | The same event on the other venues: `id`, `venue`, `externalIdentifier`, `title`, `slug`, `status`, and `series.externalIdentifier` (needed to build a canonical Kalshi URL). Empty array when the event is in no cluster. |
| `marketCount`, `groupMarketCount` | the **event row** | How many markets that row carries. |
| `matchedVenueMarkets[]` | each **market** | The same market on the other venues, with their outcomes. |
| `matchedVenueMarketOutcomes[]` | each **outcome** | `{ venueMarketId, venueMarketOutcomeId }` references to the aligned outcome on each matched venue. |
| `settlementDiff` | the **cluster** | Resolution-criteria differences between members. See [Settlement Differences](/recipes/settlement-key-differences). |

<Note>
  `venues[]` tells you **how many** venues a cluster spans; `matchedVenueEvents[]` tells you **which
  events** they are. Before, the only way to enumerate the members was to flatten
  `venueMarkets[].matchedVenueMarkets[].venueEvent` — which under-reported, because the embedded
  market array on list responses is a top-three preview and a member with no matched markets never
  appeared at all. Use `matchedVenueEvents[]` instead.
</Note>

Cross-venue pricing hangs off `matchedVenueMarkets[]`, not off the event row. To show a best price,
collect each market's own id plus its `matchedVenueMarkets[].id`, then batch them into one
[`GET /midpoints`](/api-reference/orderbook/get-live-midpoints-for-multiple-markets) call, or subscribe to those ids on the
[WebSocket midpoint stream](/api/websocket).

## Starting from a venue identifier

If you already hold a venue's identifier — a Kalshi ticker, a Polymarket id, a Predict slug — you can
resolve it straight to its cluster. This works at all three levels, and it is **bidirectional**: it
does not matter whether the identifier you hold belongs to the cluster's anchor or to one of its
members. You get the same cluster either way.

### Events

```bash
curl -G "https://api.agg.market/venue-events" \
  --data-urlencode "venue=kalshi" \
  --data-urlencode "externalIdentifier=KXCS2GAME-26AUG1214003DMAXNAVI" \
  -H "x-app-id: $AGG_APP_ID"
```

The response row is the Kalshi event itself, and `matchedVenueEvents[]` carries the rest of the
cluster:

```jsonc
{
  "venue": "kalshi",
  "externalIdentifier": "KXCS2GAME-26AUG1214003DMAXNAVI",
  "venueCount": 4,
  "matchedVenueEvents": [
    { "venue": "polymarket", "externalIdentifier": "824593",                  "id": "…" },
    { "venue": "limitless",  "externalIdentifier": "353490",                  "id": "…" },
    { "venue": "predict",    "externalIdentifier": "cs2-navi-3dmax-2026-08-12","id": "…" }
  ]
}
```

Query any of those three identifiers instead and you get that venue's row back, with the other three
in its `matchedVenueEvents[]`. Same cluster, four different entry points.

### Markets

```bash
curl -G "https://api.agg.market/venue-markets" \
  --data-urlencode "venue=polymarket" \
  --data-urlencode "externalIdentifier=3471056" \
  -H "x-app-id: $AGG_APP_ID"
```

`matchedVenueMarkets[]` carries the same market on every other venue. Note that a cluster can contain
**two markets from the same venue** — Kalshi lists a head-to-head as one market per team, and both are
matched in, so `[kalshi, kalshi, limitless, predict]` is a correct result, not a duplicate.

### Outcomes

```bash
curl -G "https://api.agg.market/venue-market-outcomes" \
  --data-urlencode "externalIdentifier=$TOKEN_ID" \
  -H "x-app-id: $AGG_APP_ID"
```

`matchedVenueMarketOutcomes[]` gives the aligned outcome on each matched venue as
`{ venueMarketId, venueMarketOutcomeId }` id pairs. Alignment is by meaning, not by label position —
where two venues frame the same question in opposite directions, the pairing still points at the
outcome that means the same thing. Fetch the full objects from
[`GET /venue-markets`](/api-reference/discovery/list-venue-markets) if you need more than ids.

### Rules

- **`venue` is required** on `/venue-events` and `/venue-markets`, because a venue identifier is only
  unique within its venue. Omitting it returns `400`.
- **`venue` is optional** on `/venue-market-outcomes` — outcome identifiers are globally unique — where
  it acts as a narrowing filter only.
- **Batch up to 20 identifiers** per request by repeating the parameter. Over 20 returns `400`.
- **Unknown identifiers are omitted** from `data` rather than returned as nulls, so a batch of 20 may
  return fewer than 20 rows. Match responses back to your input by reading `externalIdentifier` on each
  item.
- **Members are returned as themselves.** These lookups deliberately do not collapse a cluster to its
  anchor the way the browse listing does — you asked for a specific venue's row, so you get it.

<Warning>
  **Kalshi outcome identifiers cover one side only.** Kalshi puts its ticker on the Yes-side outcome;
  the No side has no identifier and cannot be looked up directly. Fetch the market instead and read
  both outcomes from `venueMarketOutcomes[]`. Polymarket, Predict, Limitless, Myriad, Opinion and
  Hyperliquid carry an identifier on every outcome.
</Warning>

## Querying a member id

`GET /venue-events/{id}` does not resolve a member id up to its anchor — it returns exactly the row
you asked for. That row is still usable for rendering, because each of its markets carries the full
set of `matchedVenueMarkets[]` siblings regardless of which member you queried.

What differs between an anchor and a member response:

| | Anchor id | Member id |
|---|---|---|
| `matchedVenueEvents[]` | full cluster | full cluster |
| `matchedVenueMarkets[]` per market | full cluster | full cluster |
| `settlementDiff` | cluster-wide | cluster-wide |
| `venueMarkets[]` | the anchor's own markets | **that member's own markets** |
| `marketCount`, `groupMarketCount` | anchor-scoped | **member-scoped** |
| `venues[]`, `venueCount` | the cluster's venues | usually the same; narrower if that member has no matched markets of its own |
| `title`, `description`, `image`, `slug` | the anchor's | **that venue's copy** |

So a member row can be missing markets the anchor carries — props or extra legs with no counterpart
elsewhere — and its title and description are the member venue's wording. For a canonical view of
the cluster, query the id you got from `GET /venue-events`.

What does **not** differ is the cluster itself: `matchedVenueEvents[]`, `matchedVenueMarkets[]` and
`matchedVenueMarketOutcomes[]` are complete from any member. If all you need is "which venues carry
this, and under which identifiers", any member answers it.

## Market lists: preview vs full

<Warning>
  The `venueMarkets` array embedded on **`GET /venue-events`** list items is a preview — up to three
  markets chosen to represent the event on a card, not the complete set. Which three depends on the
  event's shape, so do not treat them as the largest or most liquid markets, and do not render
  counts or totals from the array length. Use `marketCount`.
</Warning>

`GET /venue-events/{id}` returns the event's markets uncapped. Both embedded arrays are deprecated:
fetch markets from [`GET /venue-markets?venueEventId=`](/api-reference/discovery/list-venue-markets),
which is filterable, paginated, and returns the same `matchedVenueMarkets[]` siblings per market.

Note that `?venueEventId=` is event-scoped by design: it returns that event's own markets, with
cross-venue siblings inline on each market — not a flattened list of every member's markets.

## Where aggKey fits

[`aggKey`](/recipes/agg-key) is a deterministic canonical key computed at discovery time from each
venue's native data. It is one **input** to matching and a convenient fetch filter — it is not the
mechanism that produces clusters, and it is not a join key.

- It is `null` for a substantial share of rows. Grouping by `aggKey` silently under-groups every
  cluster whose members could not be canonicalized.
- It is guaranteed identical across venues only where every venue exposes the same normalizable
  inputs — sports head-to-head and crypto up/down markets. Elsewhere it is derived from each venue's
  own slug or ticker and its own resolution timestamp, so two members of the same cluster can carry
  different keys.

Use `aggKey` to fetch related rows for a key you already hold. Use the cluster and its anchor `id`
for identity.

## Related

<Columns cols={2}>
  <Card title="Look Up by Venue Identifier" icon="magnifying-glass" href="/recipes/external-identifier-lookup">
    Resolve a Kalshi ticker or Polymarket token_id to its cluster, in batch, at any level.
  </Card>
  <Card title="Comparing Venue Prices" icon="scale-balanced" href="/recipes/comparing-venue-prices">
    Fetch matched events and their midpoints to build a cross-venue price comparison.
  </Card>
  <Card title="Building Market Views" icon="layer-group" href="/recipes/building-market-views">
    Compose discovery and orderbook endpoints into event grids and trading views.
  </Card>
  <Card title="Canonical Market Key (aggKey)" icon="key" href="/recipes/agg-key">
    What aggKey is, when it is null, and the one supported way to parse it.
  </Card>
  <Card title="Settlement Differences" icon="scale-unbalanced" href="/recipes/settlement-key-differences">
    Surface resolution-criteria differences between venues in a cluster.
  </Card>
</Columns>
