AGG is currently in alpha. APIs, components, and docs may change.
Agg.market — Prediction Market Aggregator logo

Matched Clusters

How AGG links the same event across venues, and which id to use as your cross-venue join key

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.

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:

FieldScopeWhat it tells you
venues[], venueCountthe event rowWhich venues that row's cluster spans — use it for venue badges.
matchedVenueEvents[]the event rowThe 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, groupMarketCountthe event rowHow many markets that row carries.
matchedVenueMarkets[]each marketThe same market on the other venues, with their outcomes.
matchedVenueMarketOutcomes[]each outcome{ venueMarketId, venueMarketOutcomeId } references to the aligned outcome on each matched venue.
settlementDiffthe clusterResolution-criteria differences between members. See Settlement Differences.

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

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:

{
  "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

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

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

Anchor idMember id
matchedVenueEvents[]full clusterfull cluster
matchedVenueMarkets[] per marketfull clusterfull cluster
settlementDiffcluster-widecluster-wide
venueMarkets[]the anchor's own marketsthat member's own markets
marketCount, groupMarketCountanchor-scopedmember-scoped
venues[], venueCountthe cluster's venuesusually the same; narrower if that member has no matched markets of its own
title, description, image, slugthe anchor'sthat 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

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.

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