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:
| ID | What it identifies | Where you get it |
|---|---|---|
venueEvent.id | An 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.id | A specific market question | venueEvent.venueMarkets[].id |
venueMarketOutcome.id | A 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:
- From step 2, collect the primary market
idand allmatchedVenueMarkets[].idvalues - Pass them all as
venueMarketIdstoGET /orderbooks - The response groups orderbooks by venue under
venueOrderbooks - 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
matchedMarketstells 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:
- List matched events — only events confirmed to exist on more than one venue.
- Collect every
venueMarketId— the event's own markets plus theirmatchedVenueMarkets. - Fetch midpoints in one batch call —
GET /midpointsaccepts 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, withkind: "chainApproval" | "venueMarketAta", the cost in USD, and analreadyPaidflag indicating whether the user has already settled it on a prior fill.venueMarketAtaremains in the response type for historical compatibility; active venue routes usechainApproval.setupCostsTotal— sum ofcostUsdfor entries wherealreadyPaid: false. This is also folded into the top-leveltotalCostIncFeesso 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
| Param | Type | Description |
|---|---|---|
status | string|string[] | Market status filter: open, closed, resolved |
venues | string|string[] | Filter by active venue: polymarket, limitless, predict, etc. |
matchStatus | string|string[] | Match status: matched, verified, pending, etc. |
categoryIds | string|string[] | Filter by category ID |
search | string | Full-text search on event title/description |
sortBy | string | Sort field: volume (default), createdAt |
sortDir | string | Sort direction: desc (default), asc |
limit | number | Page size (1-100, default 50) |
cursor | string | Pagination cursor from nextCursor |
/venue-markets
| Param | Type | Description |
|---|---|---|
venueEventId | string | Filter markets by parent event |
venue | string | Filter by active venue, such as polymarket, limitless, or predict |
status | string|string[] | Market status filter: open, closed, resolved |
matchStatus | string | Match status filter |
search | string | Search on market question or event title |
categoryIds | string|string[] | Filter by category ID |
expand | string | Comma-separated: match_details, series |
limit | number | Page size (1-100, default 50) |
cursor | string | Pagination cursor |