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

Building Market Views

How to use venue-events, venue-markets, and orderbook endpoints together to build prediction market UIs

Building Market Views

This guide explains how the discovery and orderbook endpoints compose to power a prediction market UI — from the home page event grid down to the live trading view.

Live discovery, orderbook, and routing surfaces include active venues only. Retired venue identifiers can still appear in historical positions, activity, and claim flows.

Data model overview

VenueEvent (group of related markets)
  └── venueMarkets[] (individual yes/no questions)
        ├── venueMarketOutcomes[] (Yes, No)
        └── matchedVenueMarkets[] (the same market on other venues)

Note that matchedVenueMarkets hangs off each market, not off the event. See Matched Clusters for how cross-venue linkage works and which id to use as a join key.

Key IDs the frontend threads through:

IDWhat it identifiesWhere you get it
venueEvent.idAn event (election, game, etc.). For a matched event this is the cluster anchor — use it as your cross-venue join key.GET /venue-events
venueMarket.idA specific market questionvenueEvent.venueMarkets[].id
venueMarketOutcome.idA tradeable outcome (Yes/No)venueMarket.venueMarketOutcomes[].id

Prices are not included in discovery responses. GET /venue-events and GET /venue-markets return the market structure (events, markets, outcomes) but no outcome prices. Fetch live prices in a separate batch call to GET /midpoints using the venueMarket.ids you collected (see Bulk midpoint fetch), or subscribe to the WebSocket midpoint stream for continuous updates.

1. Home page — event grid

Fetch the top events sorted by volume. The default sort is volume desc and the default feed uses category-interleaved ranking for diversity.

GET /venue-events?status=open&limit=12&sortBy=volume&sortDir=desc

For matched-only events (cross-venue):

GET /venue-events?status=open&matchStatus=matched&matchStatus=verified&limit=12

The list returns one row per matched cluster, and each embedded market carries its counterparts on other venues in matchedVenueMarkets[]. See Matched Clusters for how the cluster is assembled and which id to join on.

Response shape (simplified):

{
  "data": [
    {
      "id": "ve_abc",
      "title": "2024 Presidential Election",
      "image": "https://cdn.example.com/election.webp",
      "venue": "polymarket",
      "volume": 5000000,
      "status": "open",
      "venues": ["polymarket", "limitless"],
      "venueCount": 2,
      "categories": [{ "id": "c1", "category": { "id": "c1", "name": "Politics" } }],
      "venueMarkets": [
        {
          "id": "vm_1",
          "venue": "polymarket",
          "question": "Will candidate X win?",
          "volume": 3000000,
          "venueMarketOutcomes": [
            { "id": "vmo_1", "label": "Yes" },
            { "id": "vmo_2", "label": "No" }
          ]
        },
        {
          "id": "vm_2",
          "venue": "limitless",
          "question": "Will candidate X win the election?",
          "volume": 2000000,
          "venueMarketOutcomes": [
            { "id": "vmo_3", "label": "Yes" },
            { "id": "vmo_4", "label": "No" }
          ]
        }
      ]
    }
  ],
  "nextCursor": "ve_def",
  "hasMore": true
}

What to render: Each event becomes a card. Show title, image, and the first market's outcomes. For the price display, batch the page's venueMarkets[].ids into one GET /midpoints call (see Bulk midpoint fetch) and key the returned per-outcome midpoints by venueMarketOutcomeId. If multiple venues are present, the midpoints response also carries each sibling's mark so you can show the best price across venues. Use venues and venueCount to display venue badges (e.g. "Available on Polymarket + Limitless") without needing to inspect nested markets.

The embedded venueMarkets on list items is a preview — up to three markets chosen to represent the event on a card, not the complete set, and not necessarily the largest ones. Render counts and totals from marketCount, or fetch the full list from GET /venue-markets?venueEventId=. The detail endpoint (GET /venue-events/:id) returns markets uncapped.

Pagination: Use cursor for infinite scroll. Pass nextCursor as cursor in the next request.

2. Event detail — market list

When the user clicks an event card, fetch the full event detail including all markets and their cross-venue matches:

GET /venue-events/ve_abc

Each market in the response includes matchedVenueMarkets — the same market on other venues with their outcomes.

Deprecation: the embedded venueMarkets array on GET /venue-events/:id is deprecated and will be removed. Fetch an event's markets from GET /venue-markets?venueEventId= instead — it is filterable, paginated, and returns the same matched siblings per market. Markets are still embedded by default today; once you've migrated you can request the lean response by sending the expand query without markets (e.g. ?expand=). A future release will make lean the default — send ?expand=markets to keep them inline across that change. The per-item description on the list endpoint (GET /venue-events) is likewise deprecated — fetch the full event (with description) via GET /venue-events/:id.

For the market list, use venue-markets:

GET /venue-markets?venueEventId=ve_abc&status=open

Both endpoints return matched siblings per market.

Event detail response (GET /venue-events/:id):

{
  "id": "ve_abc",
  "title": "2024 Presidential Election",
  "image": "https://cdn.example.com/election.webp",
  "venue": "polymarket",
  "volume": 5000000,
  "status": "open",
  "venues": ["polymarket", "limitless"],
  "venueCount": 2,
  "venueMarkets": [
    {
      "id": "vm_1",
      "venue": "polymarket",
      "question": "Will candidate X win?",
      "volume": 3000000,
      "venueMarketOutcomes": [
        { "id": "vmo_1", "label": "Yes" },
        { "id": "vmo_2", "label": "No" }
      ],
      "matchedVenueMarkets": [
        {
          "id": "vm_2",
          "venue": "limitless",
          "question": "Will candidate X win the election?",
          "venueMarketOutcomes": [
            { "id": "vmo_3", "label": "Yes" },
            { "id": "vmo_4", "label": "No" }
          ]
        }
      ]
    }
  ]
}

Use matchedVenueMarkets on each market to collect the venueMarket.ids for the GET /midpoints batch call (cross-venue price comparison) and the venueMarketOutcome.id values for orderbook subscriptions.

Venue markets response (GET /venue-markets):

{
  "data": [
    {
      "id": "vm_1",
      "venue": "polymarket",
      "question": "Will candidate X win?",
      "volume": 3000000,
      "status": "open",
      "venues": ["polymarket", "limitless"],
      "venueCount": 2,
      "venueEvent": {
        "id": "ve_abc",
        "title": "2024 Presidential Election",
        "slug": "2024-presidential-election"
      },
      "matchedVenueMarkets": [
        {
          "id": "vm_2",
          "venue": "limitless",
          "question": "Will candidate X win the election?",
          "venueMarketOutcomes": [
            { "id": "vmo_3", "label": "Yes" },
            { "id": "vmo_4", "label": "No" }
          ]
        }
      ],
      "venueMarketOutcomes": [
        { "id": "vmo_1", "label": "Yes" },
        { "id": "vmo_2", "label": "No" }
      ]
    }
  ],
  "nextCursor": null,
  "hasMore": false
}

What to render: Each market is a row or card. Show the question, outcomes, and volume, with prices from the batched GET /midpoints call. matchedVenueMarkets gives you the same market on other venues — include those ids in the midpoints batch for cross-venue price comparison.

3. Market detail — live orderbook

When the user selects a specific outcome to trade, fetch the live orderbook.

Single-outcome orderbook

For a single outcome on a single venue:

GET /orderbook/outcome/vmo_1

Returns per-venue bid/ask levels:

{
  "venueMarketOutcomeId": "vmo_1",
  "venueMarketId": "vm_1",
  "venue": "polymarket",
  "orderbook": {
    "bids": [{ "price": 0.55, "size": 1500 }, { "price": 0.54, "size": 900 }],
    "asks": [{ "price": 0.56, "size": 1200 }, { "price": 0.57, "size": 800 }]
  },
  "midpoint": 0.555,
  "spread": 0.01,
  "timestamp": 1710000000000
}

Merged cross-venue orderbook (aggregated view)

To show a merged orderbook across multiple venues for the same market, pass multiple venueMarketIds from matched markets. You get these IDs from matchedVenueMarkets in step 2.

GET /orderbooks?venueMarketIds=vm_1&venueMarketIds=vm_2&depth=20

The response includes per-venue orderbooks keyed by venue name, plus metadata about matched markets:

{
  "data": [
    {
      "venueMarketId": "vm_1",
      "status": "ok",
      "error": null,
      "requestedMarket": {
        "venueMarketId": "vm_1",
        "venue": "polymarket",
        "marketStatus": "open",
        "tickSize": 0.01,
        "endDate": "2024-11-06T00:00:00Z",
        "resolutionDate": null
      },
      "venueOrderbooks": {
        "polymarket": {
          "venueMarketId": "vm_1",
          "tickSize": 0.01,
          "orderbook": {
            "bids": [{ "price": 0.55, "size": 1500 }, { "price": 0.54, "size": 900 }],
            "asks": [{ "price": 0.56, "size": 1200 }]
          }
        },
        "limitless": {
          "venueMarketId": "vm_2",
          "tickSize": 0.01,
          "orderbook": {
            "bids": [{ "price": 0.53, "size": 800 }],
            "asks": [{ "price": 0.55, "size": 600 }, { "price": 0.57, "size": 400 }]
          }
        }
      },
      "matchedMarkets": [
        {
          "venue": "limitless",
          "venueMarketId": "vm_2",
          "marketStatus": "open",
          "tickSize": 0.01,
          "hasOrderbook": true
        }
      ]
    }
  ],
  "meta": {
    "requestedCount": 1,
    "okCount": 1,
    "errorCount": 0
  }
}

How to build a merged view:

  1. From step 2, collect the primary market id and all matchedVenueMarkets[].id values
  2. Pass them all as venueMarketIds to GET /orderbooks
  3. The response groups orderbooks by venue under venueOrderbooks
  4. Merge bids/asks client-side: combine all venue bids at the same price level, sort descending. Same for asks ascending. Each level shows the total size and per-venue attribution
  5. matchedMarkets tells you which other venues have this market and whether they have orderbook data (hasOrderbook)

The @agg-build/sdk useLiveMarket hook and the WebSocket aggregated orderbook handle this merging automatically when you subscribe with multiple outcome IDs.

WebSocket orderbook (live updates)

After the initial REST load, subscribe to the WebSocket for real-time updates. Pass outcome IDs from both the primary and matched markets to receive the aggregated cross-venue stream:

{
  "action": "subscribe",
  "channel": "orderbook",
  "outcomeIds": ["vmo_1", "vmo_3"]
}

The WebSocket aggregated orderbook snapshot already includes per-venue attribution and merged levels:

{
  "type": "orderbook_snapshot",
  "outcomeId": "vmo_1",
  "bids": [
    [0.55, 2300, { "limitless": 800, "polymarket": 1500 }],
    [0.54, 900, { "polymarket": 900 }],
    [0.53, 800, { "limitless": 800 }]
  ],
  "asks": [
    [0.55, 600, { "limitless": 600 }],
    [0.56, 1200, { "polymarket": 1200 }],
    [0.57, 1200, { "polymarket": 800, "limitless": 400 }]
  ],
  "venueOrderbooks": {
    "limitless": { "bids": [[0.55, 800], [0.53, 800]], "asks": [[0.55, 600], [0.57, 400]] },
    "polymarket": { "bids": [[0.55, 1500], [0.54, 900]], "asks": [[0.56, 1200], [0.57, 800]] }
  },
  "midpoint": 0.555,
  "spread": 0.005,
  "timestamp": 1710000000000
}

You'll receive a full snapshot followed by incremental deltas. See the WebSocket Protocol docs for sequencing, checksums, and resync logic.

4. Bulk midpoint fetch — matched events and markets

When you want a price-comparison view across many events at once (e.g. a homepage grid that shows live "best price across venues"), you typically:

  1. List matched events — only events confirmed to exist on more than one venue.
  2. Collect every venueMarketId — the event's own markets plus their matchedVenueMarkets.
  3. Fetch midpoints in one batch callGET /midpoints accepts up to 200 IDs.

List matched events

GET /venue-events?status=open&matchStatus=matched&matchStatus=verified&limit=50

Each event's response includes venueMarkets[] (the event's markets) and, because we passed confirmed match statuses, the markets from sibling events on other venues. Walk the response to gather every venueMarketId you'll want a midpoint for:

const ids = new Set<string>();
for (const event of events) {
  for (const market of event.venueMarkets ?? []) {
    ids.add(market.id);
    for (const matched of market.matchedVenueMarkets ?? []) {
      ids.add(matched.id);
    }
  }
}

Batch midpoints

Pass the collected IDs to /midpoints. The endpoint proxies the live orderbook engine and returns the current Yes-side mark per market, plus per-outcome midpoints and the sibling markets the engine considered.

GET /midpoints?venueMarketIds=vm_1&venueMarketIds=vm_2&venueMarketIds=vm_3

Each entry includes the headline midpoint, per-outcome midpoints, and the matched siblings the engine considered. See the API Reference for the full response schema, including markSource provenance.

Cap each request at 200 IDs. For larger universes, chunk the IDs into batches of 200 and fire the requests in parallel — the endpoint is read-only and idempotent.

For a worked SDK example that compares prices across venues, see Comparing Venue Prices.

5. Trading — smart route and execution

When the user wants to place a trade, compute the optimal route across venues:

GET /orderbook/vmo_1/route?maxSpend=100&slipCapBps=50

This returns a quote with the best fills across all active venues where the market is available:

{
  "quoteId": "q_abc",
  "fills": [
    { "venue": "polymarket", "amount": 60, "price": 0.55 },
    { "venue": "limitless", "amount": 40, "price": 0.53 }
  ],
  "totalCost": "5340000",
  "estimatedAvgPrice": 0.534
}

Execute the quote:

POST /execution/fill
{ "quoteId": "q_abc" }

Track progress via WebSocket order_event messages (step progress, fill confirmation, errors).

Deep cost estimate (deepEstimate=true)

Some venues require one-time setup before a user can trade — ERC-20 and venue contract approvals on the venue's chain (Polymarket / Polygon, Limitless / Base, predict.fun / BNB). These are paid through the gas paymaster on the user's first BUY on each (venue, chain) pair and never again.

Add deepEstimate=true to the route call to surface those costs in the response:

GET /orderbook/vmo_1/route?maxSpend=100&deepEstimate=true

The response's feeBreakdown then includes:

  • setupCosts — one entry per setup item the route would touch, with kind: "chainApproval" | "venueMarketAta", the cost in USD, and an alreadyPaid flag indicating whether the user has already settled it on a prior fill. venueMarketAta remains in the response type for historical compatibility; active venue routes use chainApproval.
  • setupCostsTotal — sum of costUsd for entries where alreadyPaid: false. This is also folded into the top-level totalCostIncFees so a single number reflects the realistic total the user will see at execution time.
{
  "feeBreakdown": {
    "rawExecCost": 53.40,
    "venueFees": 0.21,
    "bridgeFees": 0.40,
    "executionGas": 0.05,
    "totalCost": 54.06,
    "setupCosts": [
      { "kind": "chainApproval", "venue": "polymarket", "chainId": 137,
        "costUsd": 0.05, "alreadyPaid": true },
      { "kind": "chainApproval", "venue": "limitless", "chainId": 8453,
        "costUsd": 0.05, "alreadyPaid": false }
    ],
    "setupCostsTotal": 0.05
  }
}

Use the per-line alreadyPaid to render strike-through "first-time fee" chips for new users without double-charging returning ones. The deep surface is buy-only (sells never trigger first-time approvals) and is omitted on quotes where the engine could not produce an executable plan (status !== "ok").

End-to-end data flow

┌─────────────────────────────────────────────────────────┐
│  Home Page                                              │
│  GET /venue-events?status=open&sortBy=volume&sortDir=desc│
│  → Event cards with nested markets (structure, no prices)│
│  GET /midpoints?venueMarketIds=…    (batched live prices)│
└──────────────┬──────────────────────────────────────────┘
               │ user clicks event
               ▼
┌─────────────────────────────────────────────────────────┐
│  Event Detail                                           │
│  GET /venue-markets?venueEventId=ve_abc&status=open     │
│  → Market list with outcomes + cross-venue matches      │
└──────────────┬──────────────────────────────────────────┘
               │ user clicks outcome
               ▼
┌─────────────────────────────────────────────────────────┐
│  Trading View                                           │
│  GET /orderbooks?venueMarketIds=vm_1&venueMarketIds=vm_2│
│    → merged cross-venue orderbook (REST initial load)   │
│  WS subscribe orderbook vmo_1,vmo_3  (live aggregated)  │
│  GET /orderbook/vmo_1/route          (compute quote)    │
│  POST /execution/fill                (execute trade)    │
│  WS order_event                      (track progress)   │
└─────────────────────────────────────────────────────────┘

Filters reference

/venue-events

ParamTypeDescription
statusstring|string[]Market status filter: open, closed, resolved
venuesstring|string[]Filter by active venue: polymarket, limitless, predict, etc.
matchStatusstring|string[]Match status: matched, verified, pending, etc.
categoryIdsstring|string[]Filter by category ID
searchstringFull-text search on event title/description
sortBystringSort field: volume (default), createdAt
sortDirstringSort direction: desc (default), asc
limitnumberPage size (1-100, default 50)
cursorstringPagination cursor from nextCursor

/venue-markets

ParamTypeDescription
venueEventIdstringFilter markets by parent event
venuestringFilter by active venue, such as polymarket, limitless, or predict
statusstring|string[]Market status filter: open, closed, resolved
matchStatusstringMatch status filter
searchstringSearch on market question or event title
categoryIdsstring|string[]Filter by category ID
expandstringComma-separated: match_details, series
limitnumberPage size (1-100, default 50)
cursorstringPagination cursor
Comparing Venue Prices

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

WebSocket Protocol

Live orderbook, trades, and order events over WebSocket.

Real-Time Orderbook

SDK and hooks for live orderbook rendering.