> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vatic.trading/llms.txt
> Use this file to discover all available pages before exploring further.

# Build on the Game Graph

> From "which games are on" to a live price and the identifiers to place an order, across every platform that offers a match.

This guide is for teams building a frontend, a bot or a data product on top of the Game Graph (Bomba, Vatic's own apps, or any startup). It walks one path from a match to an order you can build, and says what to trust and what to check yourself.

<Note>
  Vatic gives you **information**: which platforms offer a game, what each outcome means, live prices and the identifiers to act on an outcome. Vatic never places or signs orders for you. Orders are built and signed by your code, with your wallet or your platform account.
</Note>

## The path

```
GET /games            which games, when, status and score, and which platforms offer each
GET /games/{id}/markets   the markets of one game, with a canonical meaning and a trade object per outcome
GET /games/{id}/streams   one call: the WebSocket URLs and subscribe messages of every platform for this game
   open the sockets        live books straight from each platform (nothing passes through Vatic)
trade object             the identifiers and parameters to build an order on the platform you pick
```

## 1. Find the game

```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
  "https://api.vatic.trading/api/v1/games?day=2026-10-11&days=1&tz=America/Mexico_City&include=platforms,coverage&limit=200"
```

Each game has one stable id (`espn:401878773`), `status` (`scheduled`, `live`, `finished`, `postponed`, `cancelled`), a `time` block (kickoff, estimated end, 90th-percentile end) and, once it has started, a `score`:

```json theme={null}
"score": { "home": 2, "away": 1, "clock": "67'", "period": 2, "detail": "67'", "updated_at": "2026-10-11T17:29:03.000Z" }
```

`score` is `null` before kickoff. It is refreshed about once a minute while the game is live, so it is minute-fresh, not tick-by-tick: do not use it to react faster than the markets themselves. `platforms` lists where the game is offered, and `coverage` tells you what we know about the rest (read [Games and platforms](/guides/game-graph) before treating a missing platform as "not offered").

If you already hold a platform's own id (a Polymarket slug, for example), `GET /games/lookup?source=polymarket&external_id=...` returns our game id.

## 2. Compare the same bet across platforms

`GET /games/{id}/markets` returns every stored market of the game. Each has a canonical meaning so the same bet lines up across platforms:

| `canonical.key` | Outcomes |
| - | - |
| `1x2` | `home`, `draw`, `away` (each its own binary market on most platforms) |
| `moneyline` | `home`, `away` |
| `total:<line>` | `over`, `under` |
| `spread:<home line>` | `home`, `away`, always from the home team's point of view |
| `btts` | `yes`, `no` |
| `null` | unclassified: listed, never compared across platforms |

Compare only markets with the same `(key, period, includes_overtime)`. Outcomes called `not:<x>` are complements: show the positive outcome and derive the other.

## 3. Open the live order books

```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
  "https://api.vatic.trading/api/v1/games/espn:401878773/streams?markets=main"
```

`markets=main` is the 1X2 or moneyline, `classified` adds every market with a canonical meaning (totals, spreads, both-teams-to-score and their half-time variants), and `all` includes the rest. For each platform you get the socket URL, the identifiers to subscribe to, and the exact subscribe messages. Open **one socket per platform**, never one per outcome.

| Platform | Socket | Notes |
| - | - | - |
| Polymarket | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | send `{"assets_ids":[...],"type":"market"}`, then `PING` every 10 s (answered with `PONG`). Snapshot (`book`) then deltas (`price_change`, size 0 removes a level) |
| Hyperliquid | `wss://api.hyperliquid.xyz/ws` | one `l2Book` subscription per coin. Every message is the full top of book. The server closes a connection that has received nothing for 60 s: send `{"method":"ping"}` (reply `{"channel":"pong"}`) at least every 30 s. Limit: 10 sockets and 1000 subscriptions per IP |
| Limitless | `wss://ws.limitless.exchange/socket.io/?EIO=4&transport=websocket` | Engine.IO v4: the server pings (`2`) every 25 s, reply `3`. Send **all** market slugs in one `subscribe_market_prices` call, a later call replaces the earlier one. Sizes arrive in 6-decimal units |
| Predict.fun | `wss://ws.predict.fun/ws` | one `predictOrderbook/<marketId>` subscription per market. Every message is the full book. **Echo the heartbeat**: the server sends `{"type":"M","topic":"heartbeat","data":<ts>}` about every 15 s and closes the socket at about 31 s unless you reply `{"method":"heartbeat","data":<same ts>}` |

Limitless and Predict.fun publish only the YES book of a market. The NO side is derived: price `1 - p`, bids and asks swapped (the `book.side` of each outcome tells you which side you are looking at).

<Note>
  In our tests (2026-10-10) the Predict.fun WebSocket streamed order books without an API key, while its REST API needs one. That is observed behaviour, not a documented guarantee: if your product depends on it, confirm with Predict.fun.
</Note>

A complete, runnable example that does all of this (find a game, open every socket, print the best bid and ask per outcome across platforms with the identifiers of the platform that has it) is in the repository: `server/docs/game-graph/examples/best_price.js`.

```bash theme={null}
npm i ws
VATIC_API_KEY=YOUR_API_KEY node best_price.js espn:401878773
```

## 4. Act on an outcome: the `trade` object

Every outcome carries `trade`. Pick the outcome with the best price, then read the identifiers for its platform:

| Platform | What identifies the outcome | What else you get |
| - | - | - |
| **Polymarket** (CLOB V2, Polygon) | `instrument.token_id` (the CLOB token id, also the position id of the conditional token), `condition_id`, `neg_risk` | tick size, minimum size, fee schedule, the V2 exchange contract for that market, pUSD collateral |
| **Hyperliquid** (outcome markets) | `instrument.asset` = `100000000 + 10 * outcome + side` (side 0 = Yes, 1 = No); also `coin` (`#<n>`) and `token_name` (`+<n>`) | the `order` action shape. Each side is its own asset with its own book |
| **Limitless** (Base) | `instrument.market_slug` and `token_id` (YES and NO each have one) | the exchange contract (the EIP-712 verifying contract), the adapter, the collateral token |
| **Predict.fun** (BNB Chain) | `instrument.market_id`, `token_id` (the outcome's on-chain id), `is_neg_risk`, `is_yield_bearing`, `fee_rate_bps` | the exchange contract that follows from those flags |

Every `trade` also has `complement` (the opposite outcome's identifiers, handy for selling by buying the other side), `tradable`, `submit` (how to build and send the order), `caveats` and `reference` links. Fields a platform does not give us are `null`.

### What building the order looks like

These are the shapes from each platform's own documentation or SDK. Check them against the platform's current docs before you rely on them in production, and test with small sizes.

**Polymarket**, with `@polymarket/clob-client-v2` (our own validated V2 setup):

```ts theme={null}
const t = outcome.trade;
await client.createAndPostOrder(
  { tokenID: t.instrument.token_id, price, size, side: Side.BUY },
  { tickSize: t.order.price.tick, negRisk: t.instrument.neg_risk },
  OrderType.GTC,
);
```

**Hyperliquid**, an `order` action to `POST https://api.hyperliquid.xyz/exchange`, signed as an L1 action (use an official SDK for signing). The asset is the field from the trade object:

```json theme={null}
{ "type": "order", "orders": [{ "a": 100079850, "b": true, "p": "0.42", "s": "100", "r": false, "t": { "limit": { "tif": "Gtc" } } }], "grouping": "na" }
```

**Limitless**, `POST https://api.limitless.exchange/orders` with `marketSlug`, your `ownerId` (your own profile id), `orderType` and an order signed with EIP-712. The domain is `Limitless CTF Exchange`, version `1`, chain `8453`, and `verifyingContract` is `trade.contracts.exchange`. The signed struct has `salt, maker, signer, taker, tokenId, makerAmount, takerAmount, expiration, nonce, feeRateBps, side, signatureType`, and `tokenId` is `trade.instrument.token_id`.

**Predict.fun**, with `@predictdotfun/sdk`:

```ts theme={null}
const t = outcome.trade;
const order = orderBuilder.buildOrder("LIMIT", {
  maker, signer, side: Side.BUY, tokenId: t.instrument.token_id,
  makerAmount, takerAmount, nonce: 0n, feeRateBps: t.instrument.fee_rate_bps,
});
const typedData = orderBuilder.buildTypedData(order, {
  isNegRisk: t.instrument.is_neg_risk, isYieldBearing: t.instrument.is_yield_bearing,
});
```

Placing orders with Predict.fun needs your own Predict.fun API key.

### What we do not store (so read it from the platform)

The `caveats` array of each trade object says this per outcome. In short: Hyperliquid does not publish tick size, lot size or minimum order for outcome markets (the quote token, USDC, is stored; its price rule is 5 significant figures and at most 8 minus szDecimals decimals); Limitless's `settings.minSize` is exposed as `order.min_size_raw` exactly as returned (a 6-decimal raw integer string) because its unit is not documented, and it publishes no tick; Predict.fun publishes no minimum size; the collateral of Predict.fun yield-bearing markets is not published with the market; and Polymarket reports its minimum size without a unit. `is_yield_bearing` (Predict.fun) and the collateral (Limitless) fill in as soon as the worker re-reads a market and are `null` until then.

`instrument.share_decimals` is the number of decimals of the outcome (share) token, for sells, balances and rounding a size: Polymarket 6, Limitless 6, Predict.fun 18, Hyperliquid 0. `share_decimals_basis` says how we know. For the three CTF-based platforms one unit of collateral mints one unit of outcome token, so shares use the collateral's decimals, which we read on-chain (Polygon pUSD 6, Base USDC 6, BNB USDT 18). Hyperliquid's 0 is observed (integer sizes only), not published. For Predict.fun yield-bearing markets it is `null`, because their collateral is not published. Buying with a USD amount does not need it; sells, positions and size rounding do.

Some values are labelled by how we know them, so you can decide how far to trust them:

* **Hyperliquid** `order.observed`: sizes were integers only and prices had at most 5 significant figures on every live outcome book we sampled (1,396 levels over 54 books, 2026-10-11). This is observed, not published by Hyperliquid.
* **Predict.fun** `order.price` (`min`, `max`, `tick`, with `derived: true`): derived from `decimal_precision`, not published as such. `instrument.index_set` is the outcome's `indexSet` exactly as its market payload gives it (1 and 2); Predict.fun documents it for redemption only, so confirm its use for orders with one small order.
* **Limitless** `order.fees`: `enabled` is the market's `metadata.fee` flag and `fee_free` is its negation (`schedule` is `null` for fee-free markets). `schedule` is the CLOB taker curve published at docs.limitless.exchange/user-guide/fees (only the published points; takers only, buy fees paid in outcome tokens, sell fees in collateral). The per-user `feeRateBps` comes from your own profile (`rank.feeRateBps`), not from the market, and the signed order must carry the same value.
* **Limitless** `order.min_size_raw` is `settings.minSize`, described by Limitless as "minimum trade size in wei" (6-decimal base units); it does not say whether that counts shares or collateral. `order.fees.enabled` is the market's `metadata.fee` flag; no fee rate is published in the market data.

## 5. Keep it healthy

* **Keys and limits.** Send `x-api-key` or `Authorization: Bearer`. Limits are per key and reported in `X-RateLimit-*`; on `429` wait `Retry-After`. Data refreshes about every 15 seconds on our side, so poll `/games` no faster than that and send `If-None-Match` (a `304` is almost free).
* **Prices come from the platforms, not from us.** Treat our REST data as discovery and identity, and the sockets as the source of live prices. A stale book (no update for about a minute) should be treated as unknown, not as the last price.
* **Re-resolve when things change.** A game can move (`time.previous_kickoffs` keeps the history) and markets open and close. Re-read `/markets` or `/streams` when a game's status changes and before a session.
* **Your platform accounts are yours.** Wallets, allowances, API keys and fees are between you and each platform. Vatic does not hold funds and does not see your orders.
* **What you can show to whom.** Every source carries a `redistributable` flag. Keys only receive data from sources cleared for redistribution; if a game or platform you expect is missing, `meta.withheld` says how many games were held back and why.

## Related

<CardGroup cols={3}>
  <Card title="Games and platforms" icon="calendar" href="/guides/game-graph">Games, coverage and canonical markets in detail.</Card>
  <Card title="Streams endpoint" icon="bolt" href="/api-reference/games/streams">Subscribe messages and identifiers per platform.</Card>
  <Card title="Markets endpoint" icon="list" href="/api-reference/games/markets">Markets with outcomes, order book keys and trade objects.</Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.