# Market-data API and browser integration

Use `https://api.agg.market` for production API requests and your assigned API URL in other environments. This guide covers the event bootstrap, immutable chart manifest, and outcome snapshot APIs, plus chart delivery states, instant search, and browser Worker integration. These additions preserve the existing SDK methods and response fields.

The canonical download is [agg.market/docs/market-data-api.md](https://agg.market/docs/market-data-api.md). The [API reference](https://docs.agg.market/api/overview) contains the complete request and response schemas.

## Authentication and visibility

All three new routes are app-tier reads. Send `x-app-id`; normal app authorization still applies, including allowed browser origins, configured API-key requirements, and applicable user access rules. App identity is not authorization to read every venue or category: disabled venues/categories, unavailable outcomes, and resolved market handling remain enforced by the API.

Server integrations can supply a validated `x-app-api-key` for their app. Keep API keys on your backend, never in a public bundle or Worker asset. A user token does not replace `x-app-id`. See [server API keys](https://docs.agg.market/recipes/server-api-keys) and [authentication](https://docs.agg.market/recipes/authentication).

Signed history URLs are temporary access capabilities. Obtain them through the authorized manifest endpoint, preserve their query strings, and do not share them in a public response cache. Manifests use `Cache-Control: private, no-store`; finalized object bytes are immutable. Protected app reads must not be cached publicly simply because their responses resemble public market data.

## SDK setup

The examples below share this client. Replace the app ID with your configured app; server-side applications may also set `apiKey` from a secret manager.

```typescript
import { createAggClient, isAggApiError, loadImmutableHistory } from "@agg-build/sdk";

const client = createAggClient({
  baseUrl: "https://api.agg.market",
  appId: "YOUR_APP_ID",
  wsUrl: "wss://ws.agg.market/ws",
});
```

Pass an `AbortSignal` and abort obsolete navigation, search, and range requests. The SDK does not make a display-ready response into an executable quote; balance, fee, inventory, and execution validation still happen in the trading flow.

## Event bootstrap

`GET /venue-events/:id/bootstrap?limit=20` returns the first useful event view. `limit` is optional and must be between 1 and 20.

| Field                    | Meaning                                                                                      |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| `event`                  | Event metadata and display fields; its embedded `venueMarkets` array is not the market page. |
| `markets`                | First market slice, with `data`, `hasMore`, and `nextCursor`.                                |
| `selectedOutcomeIds`     | Initial outcome IDs for history and live subscriptions, not venue market IDs.                |
| `references`             | History manifest path and live channel reference.                                            |
| `freshness.assembledAt`  | Response assembly time in Unix milliseconds, not a venue update timestamp.                   |
| `freshness.marketStatus` | The open-market selection scope, or `null`; not a trade authorization.                       |
| `detailsDeferred`        | `true`: descriptions/rules and other detail data may need a later read.                      |

```typescript
async function loadFirstEventView(eventId: string, signal: AbortSignal) {
  const bootstrap = await client.getEventBootstrap(eventId, { limit: 20, signal });
  return {
    event: bootstrap.event,
    firstMarkets: bootstrap.markets.data,
    nextCursor: bootstrap.markets.nextCursor,
    hasMore: bootstrap.markets.hasMore,
    outcomeIds: bootstrap.selectedOutcomeIds,
  };
}
```

For subsequent market pages, use `GET /venue-markets` / `client.getVenueMarkets` with the same event ID and the returned cursor. Fetch rules, wallets, personalized quotes, and offscreen pages separately. Bootstrap is bounded; do not treat an initial slice or a search preview as the full event.

## Immutable chart history and the mutable tail

`GET /charts/history/manifest` accepts the chart parameters `venueMarketOutcomeId`, `resolution`, `from`, `to`, and `countBack`. `to` is required. Wire resolutions are `1`, `5`, `60`, and `1D`; SDK methods accept `1m`, `5m`, `1h`, and `1d`. Timestamps are Unix milliseconds. `countBack` is bounded to 1–5,000.

A version-1 manifest contains:

- `revision`, outcome/venue identity, `semantics`, and the effective `resolution`.
- Requested `from`/`to` bounds and `finalizedThrough`.
- At most eight `chunks`, each with `key`, signed `url`, SHA-256 `sha256`, bounds, and at most 512 points.
- `meta` delivery state and freshness information.

The service can select a coarser published resolution to bound downloads. Use the returned resolution. Content-addressed keys include the venue, outcome, series semantics, resolution, and chunk boundaries. Corrections/backfills publish new objects and a new manifest revision; immutable objects are not overwritten.

```typescript
async function readFinalizedHistory(outcomeId: string, signal: AbortSignal) {
  const manifest = await client.getChartHistoryManifest(
    {
      venueMarketOutcomeId: outcomeId,
      resolution: "5m",
      from: Date.now() - 24 * 60 * 60 * 1000,
      to: Date.now(),
    },
    { signal },
  );

  if (manifest.meta.state !== "ready" || !manifest.meta.complete) {
    return { manifest, finalized: null };
  }
  const finalized = await loadImmutableHistory({ manifest, signal });
  return { manifest, finalized };
}
```

`loadImmutableHistory` fetches the signed HTTPS objects directly, bounds request concurrency and object size, verifies SHA-256 and identity/bounds, and returns normalized bars. It supports Node and browser environments; runtimes without WebCrypto can inject a SHA-256 `digest` implementation. Do not replace verification with a filename-only check or forward API credentials to the CDN.

Render verified finalized bars as soon as available. If `finalizedThrough < to`, fetch the unfinished remainder through `client.getChartBars` using the manifest's effective resolution and `from: Math.max(manifest.from, manifest.finalizedThrough)`. `meta.complete` on a ready manifest means it supplies a usable continuous prefix with a bounded remaining tail, not necessarily that history is finalized through the current time.

If publication is unavailable or warming, use a bounded `/charts/bars` fallback and honor retry hints. Manifest URLs currently expire after five minutes; obtain a fresh authorized manifest when they expire. Hook-managed charts can opt into immutable history with the client option `preferImmutableHistory: true`, after publication is ready. With the default `false`, those hooks continue through REST without a manifest request on every chart. Direct `client.getChartBars` calls remain REST reads; SDK-only applications explicitly use the manifest and immutable-history loader shown above.

### Chart delivery and source semantics

`GET /charts/bars` remains the canonical chart route. The deprecated SDK method `getChartCandlesticks` delegates to it; there is no separate HTTP `/charts/candlesticks` route.

Bars are ascending by `t`, use the requested outcome's direction, and carry probability prices in `o`, `h`, `l`, and `c`; `v` may be `null` when volume is unavailable. Optional `mean` preserves a supplied mean price. Do not invent volume, bridge genuine gaps, invert a No series twice, or describe sampled-price history as observed trade OHLCV. The manifest's `semantics` distinguishes `venue-ohlcv-v1`, `polymarket-price-samples-v1`, and `kalshi-ohlcv-quote-fallback-v2`.

| `meta.state`  | Client behavior                                                                                                    |
| ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `ready`       | Usable delivery; still inspect points, `complete`, and freshness.                                                  |
| `warming`     | Data/publication is not ready. Preserve an existing view and retry within a bounded budget using `retryAfterMs`.   |
| `unsupported` | This series or delivery path is unavailable. Use an applicable fallback; do not keep polling the unsupported path. |
| `failed`      | Delivery failed. Show an error/retry state rather than counting empty data as success.                             |
| `empty`       | No bars were delivered for the range. This is not a valid nonempty chart.                                          |

The state fields are additive and optional for older compatible servers. An HTTP `200` with zero bars or incomplete delivery is not evidence of a healthy chart. `serverTime`, `lastBarOpenTime`, and `fetchedAtMs` are milliseconds; `staleSeconds` is seconds. Assembly/publication times measure AGG processing, not upstream venue delivery latency. Historical ranges can naturally have old last-bar times; interpret freshness with the requested range and resolution.

## Outcome snapshots and book integrity

`GET /orderbooks/outcomes?venueMarketOutcomeIds=outcome-a,outcome-b&depth=50` takes 1–100 unique outcome IDs. Repeated query parameters are also accepted. `depth` is optional and bounded to 1–500. Deduplicate and chunk larger selections before sending them.

```typescript
async function readOutcomeSnapshots(outcomeIds: string[], signal: AbortSignal) {
  const ids = [...new Set(outcomeIds)];
  if (ids.length > 100) throw new Error("Split the selection into batches of at most 100 outcomes");
  if (ids.length === 0) return [];
  const response = await client.getOutcomeOrderbooks(
    { venueMarketOutcomeIds: ids, depth: 50 },
    { signal },
  );
  return response.data;
}
```

The response is `{ data: [...] }` in requested outcome order. Missing, inaccessible, or unavailable entries are `null`; a systemic engine failure returns `503` rather than a fabricated empty success. A non-null row contains identity, levels, midpoint/spread, sequence, timestamps, and mark/tick metadata. Check its availability and freshness before displaying it as a live book.

`GET /orderbooks` remains supported for venue-market-ID reads and returns per-item status/error information. `/orderbooks/outcomes` serves outcome-ID snapshots. Neither route replaces sequence-checked WebSocket updates or the validation required to execute an order. `GET /orderbook/outcome/:outcomeId` also remains active (`client.getOutcomeOrderbook`) and costs one unit in the same snapshot budget. Use each route's documented response shape.

| Integrity/time field                                            | Meaning                                                                                                                        |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| REST `timestamp` and raw WebSocket `timestamp`                  | Unix milliseconds.                                                                                                             |
| SDK `OrderbookState.timestamp` and decoded SDK trade timestamps | Unix seconds; convert only at the transport boundary.                                                                          |
| `seq`                                                           | Monotonic sequence within the applicable connection/book generation. Reset/recovery generations must be reconciled explicitly. |
| `checksum`                                                      | Full engine book checksum; do not recompute it from a truncated REST depth slice.                                              |
| `snapshotChecksum`                                              | Additive CRC checksum of the returned depth-limited levels.                                                                    |
| History chunk `sha256`                                          | Hash of immutable object bytes, separate from book CRC checksums.                                                              |

Prefer a valid existing snapshot or a healthy socket snapshot, with a bounded batched REST fallback when necessary. Cancel or reconcile competing responses by sequence and connection generation. Process raw deltas in order and request recovery on a gap/checksum failure; presentation batching must not discard raw updates.

## Instant and full search

`GET /search` adds `mode=instant|full`; omission preserves `full`. `type=events|markets` and `q` are required. `limit`, cursor, category, and status filters remain supported.

```typescript
async function suggestions(q: string, signal: AbortSignal) {
  return client.search({ q, type: "events", mode: "instant", limit: 5 }, { signal });
}

async function submittedSearch(q: string, signal: AbortSignal) {
  return client.search({ q, type: "events", mode: "full", deep: true, limit: 20 }, { signal });
}
```

Instant mode is the bounded title/candidate path for typeahead. Description, semantic, and optional reranking work belongs to full search; `deep: true` maps to `deepSearch=true` on the wire and does not enable that work in instant mode. Debounce input and abort superseded requests. Preserve the returned cursor and filters when paging; use listing endpoints for ordinary filtered browsing.

Search may return additive `meta.complete` and `meta.degradedSignals` when optional ranking signals miss their budgets. `meta.complete` describes search execution, not whether there are more result pages; inspect `hasMore` separately. An overall search-budget failure returns `503`, not a cacheable empty success.

## Rate limits and retry responses

The following route-family limits apply to requests that reach the API origin over an exact rolling 60-second window. Origin response-cache hits consume quota too; changing query parameters, cursors, or search mode does not create a new route-family budget. They apply in staging and local environments by default as well as production; an explicit operator `RATE_LIMIT_DISABLED` override disables the limiter.

| Route family                                                        | Per IP | Browser identity | Validated API key | Charged unit                                                  |
| ------------------------------------------------------------------- | -----: | ---------------: | ----------------: | ------------------------------------------------------------- |
| `/venue-events/:id/bootstrap`                                       |    120 |              240 |             1,200 | Requests                                                      |
| `/charts/history/manifest`                                          |    240 |              480 |             2,400 | Requests                                                      |
| `/charts/bars`                                                      |    120 |              240 |             1,200 | Requests                                                      |
| `/search` (instant and full combined)                               |    120 |              240 |             1,200 | Requests                                                      |
| `/orderbooks/outcomes` and `/orderbook/outcome/:outcomeId` combined |    600 |            1,200 |             6,000 | Distinct outcome IDs per batch; one unit for a single outcome |

For browsers, the IP bucket and an app/user identity bucket both apply. Anonymous requests use the app/IP identity instead of a user ID; this is not one shared quota for every user of an app. For example, requesting ten different outcomes costs ten snapshot units each time; duplicate IDs in that request count once. A batch still cannot exceed 100 unique outcomes. Switching from the batch endpoint to single-outcome reads does not reset or bypass the shared budget.

These route limits are additional to applicable global limits. Production defaults are 1,000 requests per 60 seconds per IP plus 18,000 per browser identity; existing deployment-specific global overrides remain separate. A proven API key has its own global quota (18,000 by default, or a configured per-key quota) and is exempt from the browser IP bucket, but must still satisfy its route-family key limit. Raising a key's global quota does not bypass the route caps. Other endpoint-specific/auth limits can also apply.

A route quota exhaustion returns a positive retry delay in seconds:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Cache-Control: private, no-store
Content-Type: application/json

{"statusCode":429,"message":"Too many requests.","code":"rate_limited","retryAfter":12}
```

Honor `Retry-After` and preserve cancellation instead of retrying every animation frame. The SDK exposes the HTTP failure as `AggApiError`; narrow an unknown error with `isAggApiError` before reading it. Its `retryAfterMs` value is in milliseconds. HTTP `Retry-After` and JSON `retryAfter` are seconds, while chart `meta.retryAfterMs` is milliseconds. Treat 429/503 responses as failures, not empty successful data. Read hooks honor server retry hints while preserving the configured QueryClient retry policy. A hint beyond the safe JavaScript timer range stops automatic retry rather than wrapping into an immediate retry; expose a manual retry state. These read APIs do not authorize automatic order submission or retrying a trade.

```typescript
function readRetryDelayMs(error: unknown): number | null {
  if (!isAggApiError(error) || (error.status !== 429 && error.status !== 503)) return null;
  // The field is additive; older SDK versions may not expose a retry hint.
  return "retryAfterMs" in error && typeof error.retryAfterMs === "number"
    ? error.retryAfterMs
    : null;
}
```

If the scoped limiter cannot reach Redis, the protected read fails with `503`, `Retry-After: 1`, and `Cache-Control: private, no-store`; clients should back off. The JSON body is `{ "statusCode": 503, "code": "rate_limit_unavailable", "message": "Request admission unavailable.", "retryAfter": 1 }`. The existing global limiter's fail-open behavior is unchanged. Rate-limit and unavailable responses must not be placed in shared caches.

The origin limiter cannot count responses served entirely by CloudFront or immutable CDN objects. Existing edge/WAF protections apply there. Signed capabilities, app visibility checks, and limited object size remain necessary; the table is not a claim of a combined origin-plus-CDN request quota.

## Browser Worker and cache ownership

The demo stays a static export. Its app-owned build produces a fingerprinted native ES module Worker asset, served with JavaScript MIME type and immutable caching. Entry HTML/release references have bounded freshness, assets are published before references change, and prior referenced assets are retained.

For a custom browser host, build a Worker entry as an ES module:

```typescript
// market-data.worker.ts — a separate Worker entry, not application startup code.
import { expose } from "comlink";
import { MarketDataEngine } from "@agg-build/sdk/market-data";

expose(new MarketDataEngine());
```

Inject your emitted static asset URL; do not ship an uncompiled TypeScript URL or assume a bundler preserves module-Worker output without inspecting the export.

```typescript
import { createWorkerWebSocket } from "@agg-build/sdk/browser";

function createBrowserClient(workerAssetUrl: string) {
  return createAggClient({
    baseUrl: "https://api.agg.market",
    appId: "YOUR_APP_ID",
    wsUrl: "wss://ws.agg.market/ws",
    webSocketFactory: (options) =>
      createWorkerWebSocket(options, () => new Worker(workerAssetUrl, { type: "module" })),
  });
}
```

Keep one client/provider/Worker across navigation. The SDK engine owns the socket lifecycle, ordered delta/checksum recovery, price-keyed book state, candles, history decoding, and bounded view publication. It processes raw updates before publishing views, normally every 50 ms, with at most one Worker-to-main delivery in flight. Direct consumers opt into `onMarketView`; callers requiring the legacy raw `onTrade` callback contract use the direct transport.

React DOM, interaction, and chart drawing stay on the main thread. Hooks expose stable per-outcome snapshots through an external store; TanStack Query remains responsible for ordinary REST and history reads. The hooks provider limits inactive histories to 16 queries / 20,000 candle points, and inactive live snapshots to 32 snapshots / 16,384 level entries with a five-second TTL. Active views are protected. A bounded inactive cache is not a promise that all application metadata is discarded immediately on navigation.

The exported demo's 50-distinct-market validation checks one Worker/connection, nonempty history, sequence/checksum parity, bounded caches and post-GC memory, navigation cancellation, and subscription release. These are lab regression gates, not production latency guarantees. Keep subscription scope tied to visible/selected outcomes and unsubscribe when they leave scope; destroy the transport when the provider/tab ends. Worker failure falls back to the compatible direct transport with explicit cleanup and snapshot recovery.

The default `@agg-build/sdk` entry remains framework-independent for browsers, Node.js, and React Native. Worker APIs live in the optional `/browser` entry; the app supplies the native Worker. Do not instantiate a browser Worker in Node or React Native merely to reuse the SDK's protocol implementation.

The [browsing performance follow-up](performance-follow-up.md) describes the visible-card subscription budget, progressive bootstrap pagination and the 150-market regression checks, with measured SQL results separated from pending staging validation.
