Skip to main content
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.
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.
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.
Do not invent your own cross-venue key from titles, slugs or aggKey (see 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.
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.
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 below. This is the durable pattern for watchlists, positions, reconciliation and anything you persist.

Cluster fields, and where they live

Two different scopes are easy to confuse:
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.
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 call, or subscribe to those ids on the WebSocket midpoint stream.

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

The response row is the Kalshi event itself, and matchedVenueEvents[] carries the rest of the cluster:
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

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

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 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.
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.

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: 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

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.
GET /venue-events/{id} returns the event’s markets uncapped. Both embedded arrays are deprecated: fetch markets from GET /venue-markets?venueEventId=, which is filterable, paginated, and returns the same matchedVenueMarkets[] siblings per market. ?venueEventId= is cluster-scoped by default: it returns every venue’s markets for that real-world event, with matched siblings folded into a single anchor row rather than repeated, and each row still carrying its counterparts inline in matchedVenueMarkets[]. Name any member of the cluster and you get the same page in the same order, so you do not need to know which id is the anchor. It is also the only way to reach markets that trade on a single venue and so have no cross-venue counterpart. Send cluster=false for the older behaviour — that one event’s own markets, 1:1 with the ids in its venueMarkets[]. Use it when you are rendering one venue’s book specifically, and pass the same value to GET /venue-events/{id}, whose marketCount follows the same flag.
cluster accepts the literal strings true and false only. Any other value — TRUE, 0, 1, no, or an empty value — is rejected with 400 Request validation failed, not quietly treated as the default:
If you build the query string by hand, send the lowercase literal. A boolean coerced by your HTTP client (String(false)) is already correct; a templated 0/1 is not.
marketCount counts exactly the rows GET /venue-markets?venueEventId= returns for this event (open markets, matched siblings folded into their anchor), so you can page to it. Pass status=open on the market list to see the same set — without a status filter the list returns closed and resolved markets too, which marketCount does not count. The rolled-up volume is the cluster’s total traded value and sums every market in it, including the siblings folded out of that list — the two are not totals over the same set.

Companion events: why a cluster can look empty

A venue often splits one game into several events: a base plus companions like … - More Markets, … - Exact Score, … : Spread, … : BTTS. They share one aggKey and each carries its own id. Two events from the same venue never share a cluster, with one exception: Hyperliquid deployers. So a companion clusters with the other venues’ equivalent companions — not with the game’s main cluster. Ask for a companion and you can legitimately get an empty matchedVenueEvents[] for a game that trades on four venues:
That response is correct, not a gap — groupParentId is non-null, which is how you tell. Follow it and the same game looks like this:
To reach the whole game from a companion, use either handle:
  • aggKeyGET /venue-events?aggKey={aggKey} returns every event of that game, across companions and venues. This is the route the game-detail page uses; see Sports Market Detail.
  • groupParentIdGET /venue-events/{groupParentId} for the base event, whose matchedVenueEvents[] is the game’s cross-venue cluster.
Cross-venue pricing for a companion’s markets never depended on this. Each market carries its own matchedVenueMarkets[] regardless of how the events cluster, so best-price and arbitrage flows work on a companion exactly as they do on a base event — and GET /venue-markets?venueEventId= on a companion returns its markets in full, cluster scope or not. An empty matchedVenueEvents[] never means an empty market list.
Pass grouped=true on the listing to fold companions under their base and get one tile per game — opt-in, because it hides the companions that a game-detail page is expected to re-surface via aggKey.

Where aggKey fits

aggKey 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.

Look Up by Venue Identifier

Resolve a Kalshi ticker or Polymarket token_id to its cluster, in batch, at any level.

Comparing Venue Prices

Fetch matched events and their midpoints to build a cross-venue price comparison.

Building Market Views

Compose discovery and orderbook endpoints into event grids and trading views.

Canonical Market Key (aggKey)

What aggKey is, when it is null, and the one supported way to parse it.

Settlement Differences

Surface resolution-criteria differences between venues in a cluster.