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

# Running it

> Export a day of trades, close your books daily, track breaking changes, and report a problem so it can be diagnosed.

This is the work that continues after go-live: pulling activity out, closing the books against it, catching a change before it breaks you, and getting an answer when something does. Each section stands alone; read the reporting one first, because the reconciliation close depends on it.

<Info>
  Prerequisites: an access token with `read:reports`
  ([Authentication](/authentication)) and the `participantId` we issued you
  ([Firms, participants and accounts](/identity)) — reports are
  account-scoped, so report calls carry `x-participant-id`; a running drop-copy consumer and
  balance-ledger consumer ([Choose a stream](/streams#choose-a-stream)); a pinned copy of the proto
  bundle you build against ([Protos and SDKs](/environments#protos-and-sdks)); and a shared Slack channel
  with us plus a named integration lead. If you do not have the last two, start at
  [Path to production](/start#path-to-production).
</Info>

<Info>
  **API reference:** <a href="https://docs.polymarket.us/institutional/report/overview" target="_blank" rel="noreferrer">Report</a>. Opens on the public documentation site in a new tab.
  Where it disagrees with this page, this page is authoritative for the partner surface.
</Info>

## Reporting and bulk export

<Note>
  **If you are an IB, this differs.**
  This section and [Daily reconciliation](#daily-reconciliation) are where your **reporting pack**
  comes from. Read [Reporting pack](/regulatory#reporting-pack) alongside them: it maps the ten
  named reports to the surfaces below and names the five that have no source today.
</Note>

Pull a day of activity by sharding your report queries by symbol; an unfiltered query times out at the edge about two thirds of the time.

### The number that decides your export design

**`POST /v1/report/trades/search` unfiltered hits the 30-second CloudFront 504 about two thirds of the time. The same call with a symbol filter returns in \~130 ms, reliably.**

A 504 means you spent more than 30 seconds at the edge. It is not a retryable blip: an unfiltered query will keep costing you 30 seconds and then failing, so retrying it is the wrong response. Narrowing it is the right one. Filter by symbol, and sweep a day by iterating over the symbols you care about.

<Warning>
  **No async report job, no published window limit and no documented bulk-export path exist.**

  There is no "request a file, collect it later" facility. Sharding by symbol inside the 30-second
  budget is the whole mechanism. Because no window limit is published, treat one day per symbol as
  your unit and measure before you widen it.
</Warning>

### Pagination

Pages come back with a `nextPageToken`. Keep requesting until it is absent.

**Page tokens are offset-based, not cursor-based.** An offset does not pin the underlying rows, so a
row can appear on two pages or be skipped between them if data lands mid-sweep. **Dedupe as you
sweep**, keyed per symbol — that is why sharding by symbol is also what makes the dedupe tractable:
each shard is a small, self-contained set you can hold in memory and reconcile.

EOF behaviour on the last page is a known defect that partners have chased 404s over. Treat an
absent `nextPageToken` as the end of the sweep and do not issue one more request to confirm it.

### Runnable export

This sweeps one day, one symbol at a time, dedupes each shard, and writes newline-delimited JSON. It
raises on any terminal failure and never swallows one.

<Warning>
  **Two names in this snippet are not published and are set as constants at the top.**

  Confirm `ROWS_KEY`, `PAGE_TOKEN_FIELD` and the filter shape with your integration lead and set them
  once. Everything else in the snippet is verified.
</Warning>

<CodeGroup>
  ```python export_trades.py theme={null}
  # pip install requests "pyjwt[crypto]"
  import json, os, time, uuid
  import jwt, requests

  CLIENT_ID   = os.environ["PMX_CLIENT_ID"]        # from the clientid.txt we share with you
  PRIVATE_KEY = open(os.environ["PMX_PRIVATE_KEY"]).read()  # private half of the keypair you sent us
  PARTICIPANT = os.environ["PMX_PARTICIPANT_ID"]   # participantId from the kyc.approved webhook
  TOKEN_URL   = "https://pmx-preprod.us.auth0.com/oauth/token"
  API_BASE    = "https://api.preprod.polymarketexchange.com"

  SYMBOLS = ["aec-cfb-clmsn-lsu-2026-09-05"]       # the symbols you traded; shard one call per symbol
  DAY     = "2026-09-14"                           # the traded day you are exporting

  ROWS_KEY         = "trades"        # VERIFY with your integration lead
  PAGE_TOKEN_FIELD = "pageToken"     # VERIFY with your integration lead
  TIMEOUT_S        = 35              # above the 30s edge cut, so a 504 is visible rather than a hang

  def access_token():
      now = int(time.time())
      assertion = jwt.encode(
          {"iss": CLIENT_ID, "sub": CLIENT_ID,
           "aud": TOKEN_URL,          # aud is the Auth0 token endpoint, NOT the API
           "iat": now, "exp": now + 240, "jti": str(uuid.uuid4())},
          PRIVATE_KEY, algorithm="RS256")
      r = requests.post(TOKEN_URL, timeout=10, data={
          "grant_type": "client_credentials",
          "client_id": CLIENT_ID,
          "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
          "client_assertion": assertion,
          "audience": API_BASE,       # never request scopes here; scopes are granted server-side
      })
      r.raise_for_status()
      b = r.json()
      # honour expires_in (180) minus a 30s buffer; do not hardcode 180
      return b["access_token"], now + int(b["expires_in"]) - 30

  def post(path, body, tok):
      r = requests.post(f"{API_BASE}{path}", json=body, timeout=TIMEOUT_S, headers={
          "authorization": f"Bearer {tok}",
          "x-participant-id": PARTICIPANT,   # reports are account-scoped
          "content-type": "application/json",
      })
      if r.status_code == 429:
          time.sleep(float(r.headers.get("Retry-After", "5")))   # 429 carries Retry-After
          return post(path, body, tok)
      if r.status_code == 504:
          raise RuntimeError(f"504 at the edge on {path}: over 30s. Narrow the query, do not retry it.")
      r.raise_for_status()
      return r.json()

  def sweep(symbol, tok, out):
      seen, page = set(), None
      while True:
          body = {"where_clause": f"symbol = '{symbol}'",   # VERIFY filter shape
                  "start_time": f"{DAY}T00:00:00Z", "end_time": f"{DAY}T23:59:59Z"}
          if page:
              body[PAGE_TOKEN_FIELD] = page
          t0 = time.time()
          res = post("/v1/report/trades/search", body, tok)
          print(f"{symbol} page in {int((time.time()-t0)*1000)} ms")   # expect ~130 ms
          for row in res.get(ROWS_KEY, []):
              key = json.dumps(row, sort_keys=True)   # offset-based tokens can repeat a row
              if key in seen:
                  continue
              seen.add(key)
              out.write(key + "\n")
          page = res.get("nextPageToken")
          if not page:                                # absent token is EOF; do not probe once more
              return len(seen)

  tok, exp = access_token()
  with open(f"trades-{DAY}.ndjson", "w") as out:
      for s in SYMBOLS:
          if time.time() > exp:
              tok, exp = access_token()
          print(s, "rows:", sweep(s, tok, out))
  ```

  ```typescript export-trades.ts theme={null}
  // npm i jose   (Node 18+ for global fetch)
  import { readFileSync, writeFileSync } from "node:fs";
  import { randomUUID } from "node:crypto";
  import { SignJWT, importPKCS8 } from "jose";

  const CLIENT_ID = process.env.PMX_CLIENT_ID!;              // from the clientid.txt we share with you
  const PRIVATE_KEY = readFileSync(process.env.PMX_PRIVATE_KEY!, "utf8"); // your private key
  const PARTICIPANT = process.env.PMX_PARTICIPANT_ID!;       // from the kyc.approved webhook
  const TOKEN_URL = "https://pmx-preprod.us.auth0.com/oauth/token";
  const API_BASE = "https://api.preprod.polymarketexchange.com";

  const SYMBOLS = ["aec-cfb-clmsn-lsu-2026-09-05"];          // one call per symbol
  const DAY = "2026-09-14";
  const ROWS_KEY = "trades";            // VERIFY with your integration lead
  const PAGE_TOKEN_FIELD = "pageToken"; // VERIFY with your integration lead
  const TIMEOUT_MS = 35_000;            // above the 30s edge cut

  async function accessToken(): Promise<{ token: string; expiresAt: number }> {
    const key = await importPKCS8(PRIVATE_KEY, "RS256");
    const assertion = await new SignJWT({ jti: randomUUID() })
      .setProtectedHeader({ alg: "RS256" })
      .setIssuer(CLIENT_ID).setSubject(CLIENT_ID)
      .setAudience(TOKEN_URL)            // the token endpoint, NOT the API
      .setIssuedAt().setExpirationTime("4m")
      .sign(key);
    const res = await fetch(TOKEN_URL, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        client_id: CLIENT_ID,
        client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        client_assertion: assertion,
        audience: API_BASE,              // no scope parameter; scopes are server-side grants
      }),
    });
    if (!res.ok) throw new Error(`token mint failed: ${res.status} ${await res.text()}`);
    const b = await res.json();
    // honour expires_in (180) minus a 30s buffer
    return { token: b.access_token, expiresAt: Date.now() + (b.expires_in - 30) * 1000 };
  }

  async function post(path: string, body: unknown, token: string): Promise<any> {
    const res = await fetch(`${API_BASE}${path}`, {
      method: "POST",
      signal: AbortSignal.timeout(TIMEOUT_MS),
      headers: {
        authorization: `Bearer ${token}`,
        "x-participant-id": PARTICIPANT,  // reports are account-scoped
        "content-type": "application/json",
      },
      body: JSON.stringify(body),
    });
    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") ?? 5);
      await new Promise((r) => setTimeout(r, wait * 1000));
      return post(path, body, token);
    }
    if (res.status === 504)
      throw new Error(`504 at the edge on ${path}: over 30s. Narrow the query, do not retry it.`);
    if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
    return res.json();
  }

  const rows: string[] = [];
  let { token, expiresAt } = await accessToken();
  for (const symbol of SYMBOLS) {
    const seen = new Set<string>();
    let page: string | undefined;
    do {
      if (Date.now() > expiresAt) ({ token, expiresAt } = await accessToken());
      const body: Record<string, unknown> = {
        where_clause: `symbol = '${symbol}'`,            // VERIFY filter shape
        start_time: `${DAY}T00:00:00Z`, end_time: `${DAY}T23:59:59Z`,
      };
      if (page) body[PAGE_TOKEN_FIELD] = page;
      const t0 = Date.now();
      const res = await post("/v1/report/trades/search", body, token);
      console.log(`${symbol} page in ${Date.now() - t0} ms`);   // expect ~130 ms
      for (const row of res[ROWS_KEY] ?? []) {
        const key = JSON.stringify(row, Object.keys(row).sort()); // offset tokens can repeat a row
        if (!seen.has(key)) { seen.add(key); rows.push(key); }
      }
      page = res.nextPageToken;                          // absent means EOF; do not probe again
    } while (page);
    console.log(symbol, "rows:", seen.size);
  }
  writeFileSync(`trades-${DAY}.ndjson`, rows.join("\n") + "\n");
  ```
</CodeGroup>

The only response field this loop branches on:

```json theme={null}
{
  "nextPageToken": "b2Zmc2V0PTUwMA"
}
```

Present means another page. Absent means the sweep is done.

### `GetTradeStats`

The semantics below were explained to partners by hand and published nowhere. All four have caused a
wrong number in a partner's system.

<ResponseField name="bars" type="int">
  **A divisor, not an interval.** The window you request is split into that many equal slices, so
  `bars: 6` over 14 days gives 2.8-day buckets — not six one-day bars. If you want daily buckets, set
  `bars` to the number of days in your window.
</ResponseField>

<ResponseField name="where_clause" type="string">
  **Use this, not `field_filter`.** `field_filter` validates against a fixed column allowlist that
  excludes `update_time` and `create_time`, so a time cursor expressed there returns
  `InvalidArgument: field not allowed`.
</ResponseField>

<ResponseField name="cleared_*" type="number">
  Reflects post-clearing state, so these **read zero for recently executed trades.** A zero here is
  not a data error and is not a reason to re-query.
</ResponseField>

**Empty buckets are returned, not omitted**, as an all-zero object. **Treat zero as "no trades",
never as a print at price 0.** A partner averaging bucket prices across a sparse window without this
rule computes a price that never traded.

`/v1/report/trades/stats` does **not** require `x-participant-id`.

<Warning>
  **One current page lists trade stats among endpoints that do not require
  `x-participant-id`, and a partner hit `PERMISSION_DENIED` on exactly that call.**&#x20;
  If you get `PERMISSION_DENIED` on trade stats, send the header and tell us, rather than assuming a
  missing scope.
</Warning>

### Ledger CSV downloads

**Ledger CSV downloads are limited to roughly 5 per minute per firm.** Exceeding it puts you into the
standard 429 path, which carries `Retry-After`. Five per minute per firm is a firm-wide budget, so a
second process pulling CSVs shares it with you — run exports from one place.

<Warning>
  **The ledger CSV download path is not published here.**&#x20;
  The limit is published; the call is not. Get the path from your integration lead.
</Warning>

### Rate limits that shape a sweep

All values below are **per firm** unless stated. Exceeding any of them returns 429 with a
`Retry-After` header.

| Call                | Published limit                 | Scope     |
| ------------------- | ------------------------------- | --------- |
| `SearchTrades`      | 12/min                          | Per firm  |
| `SearchOrders`      | 12/min                          | Per firm  |
| `SearchExecutions`  | 12/min                          | Per firm  |
| `GetTradeStats`     | 60/min                          | Per firm  |
| `ListSymbols`       | 6/min                           | Per firm  |
| Ledger CSV download | \~5/min                         | Per firm  |
| All REST            | 100 req/sec, one-minute average | Firm-wide |

At 12 calls per minute for `SearchTrades`, a symbol-sharded sweep is rate-limited long before it is
latency-limited: 60 symbols is five minutes of wall clock at the cap. Size your sweep window
accordingly rather than parallelising into 429s.

There is also an **endpoint-level rung** that is not on the published table and was unknown even to
support:

```json theme={null}
{"code":8,"message":"rate limit exceeded for /polymarket.v1.PositionAPI/ListAccountBalances (rung \"endpoint\"); retry after 126ms"}
```

Read the `retry after` value out of that message rather than assuming your usual backoff.

<Warning>
  **No preprod-specific values are published for any of these limits.**&#x20;
  Assume the production numbers apply in preprod and confirm before you tune a sweep against them.
  The published summary table also states "0.5–60 req/min" for query endpoints; **nothing is
  documented at 0.5/min** and the lowest real value is 6/min.
</Warning>

## Daily reconciliation

With an export path that survives the edge timeout, you can close the day. Reconcile your own persisted stream record against a small number of anchor reads, per concern, once a day.

### The three rules

#### 1. Streams are the system of record. Unary reads are anchors.

Drop copy carries execution reports and trade capture and is resumable — its `resume_token` is
populated, at roughly 576 bytes. Persist every message as it arrives and treat your own store as
authoritative.

The anchor reads exist to catch a gap in your consumer, not to rebuild your book. Two reasons they
cannot be the record:

* **Stored execution history is archived during maintenance, and execution queries for the period
  before a window return empty.** An anchor read after an overnight window can legitimately return
  less than your stream record. If the read were your record, a maintenance window would delete your
  books.
* The read caps are low: `SearchOrders`, `SearchExecutions` and `SearchTrades` are **12/min per
  firm**. You cannot poll your way to a book.

#### 2. One elected leader per stream.

A subscription session is keyed on **caller identity — the token subject plus the participant
header — not on the accounts or symbols you requested.** A second process holding the same identity
and opening the same subscription takes the session away from the first, which surfaces as
`13 INTERNAL: Subscription manager revoked session`. Nine partners investigated that error
independently before this was written down.

So: elect one consumer per stream, run the others as hot standbys that consume nothing until they win
the election, and have the winner resume from the shared persisted checkpoint. The cap reinforces it:
**20 concurrent streams per firm across all gRPC subscriptions**, so duplicated consumers are also
spending a budget you need for market data.

**Known workaround for repeated revocations: pass an explicit list of markets on
`CreateOrderSubscription`** — there is no symbol cap on that path.

#### 3. A process restart is not a cold start.

On restart, read your persisted checkpoint and resume the stream against your **existing** local
database. Do not re-run the day's anchor reads, and do not replay from the start of the day.

A restart that re-reads is how partners hit 429 on a 12/min endpoint during an incident, which is the
worst possible moment to lose your reads. It is also how a maintenance-archived window silently
truncates a book that was complete in your store a minute earlier.

Cold start means exactly one thing: you have no checkpoint at all. Everything else resumes.

| Stream                  | Resumable                                          | What you persist                                          |
| ----------------------- | -------------------------------------------------- | --------------------------------------------------------- |
| Drop copy               | **Yes** — `resume_token` populated (\~576 bytes)   | The token, plus every execution report                    |
| Position change         | **Yes**                                            | The token, plus every position delta                      |
| Balance ledger          | Replay then live phases                            | The newest entry you have applied, per account            |
| Instrument state change | **Declares `resume_token` and never populates it** | Your own last-seen state per symbol; you cannot resume it |
| Market data             | **No resume field at all**                         | Nothing resumable. Re-snapshot on reconnect               |

Snapshot-then-delta applies on subscribe, delivery is at-least-once, and redelivery happens. Dedupe
on your own key before you apply anything.

### Per-concern reconciliation

| Concern              | Stream (the record)                                                  | Anchor read                                                                     | What you compare                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Orders               | Order subscription (`CreateOrderSubscription`, explicit market list) | `SearchOrders`, 12/min per firm                                                 | Count of orders you submitted, by `clord_id` and `idempotency_key`, against the exchange's set. Every `ACCEPTED` must be present; an `EXPIRED` one must have no execution |
| Executions and fills | **Drop copy**                                                        | `SearchExecutions`, or `POST /v1/report/trades/search` filtered by symbol       | Count and notional per symbol. Any execution in the anchor that is not in your store is a consumer gap                                                                    |
| Positions            | Position change (`CreatePositionChangeSubscription`)                 | See the warning below                                                           | Net contracts per symbol per participant, de-scaled with `fractionalQtyScale`                                                                                             |
| Cash                 | Balance ledger (per account)                                         | `GetAccountBalance` for a firm account; `GetFundingAccountBalance` for the pool | Closing balance against the newest ledger entry's `afterBalance`                                                                                                          |
| Fees                 | Drop copy `commission_notional_collected`                            | **None exists.** The daily Vendor Fees report                                   | Your own accrual record against our commission figures                                                                                                                    |

Position change is also the stream that answers *"did a market I hold resolve?"* — not instrument
state change, which carries the updated `Instrument` and **not** the settlement result fields. One
partner built settlement against instrument state change and had to be redirected.

<Warning>
  **No positions anchor read is published.**&#x20;
  `GetAccountBalance` returns `InvalidArgument: invalid account` for a **participant clearing
  account** while succeeding for a firm account, and partners currently derive a participant's cash
  from the newest balance-ledger entry's `afterBalance` — a ledger scan standing in for a one-field
  read. Ask your integration lead which read anchors positions before you design a break report
  around it.
</Warning>

### The identities you can assert

These four close with the facts available today. Assert them daily and alert on any break.

1. **Cash, per participant account.**
   ```
   opening balance
     + Σ DEPOSIT transfers
     − Σ WITHDRAWAL transfers
     ± Σ VENDOR_FEES transfers        (direction is fixed by the reason, not by you)
     + Σ net ORDER_EXECUTION entries  (commission is folded into this single net entry)
     + Σ RESOLUTION entries
   = closing balance = newest ledger entry's afterBalance
   ```
2. **Executions.** Every execution report in your drop-copy store for day *D* appears in a
   symbol-filtered `POST /v1/report/trades/search` for day *D*, and the reverse. A row present only in
   the anchor is a consumer gap; a row present only in your store, after a maintenance window, is
   expected.
3. **Fees, per fill.**
   ```
   commission_notional_collected / (priceScale × fractionalQtyScale)
     == Θ × C × p × (1 − p), banker's rounded to the cent
   ```
   `C` is the **de-scaled** contract count. Using raw `order_qty` on a `fractional_quantity_scale = 100`
   instrument overstates the fee 100×, and on a scale-`1` instrument the naive math is accidentally
   correct, so this break only ever shows up on your first scale-100 fill. Θ is the **taker**
   coefficient; you are structurally always the taker and can never earn the maker rebate.
4. **Orders to effects.** Every `EXPIRED` order produces no execution, no ledger entry and no position
   change. An `EXPIRED` order with any downstream effect is a real break and worth escalating
   immediately.

<Snippet file="scales-warning.mdx" />

One dollar is `priceScale × fractionalQtyScale` notional units. When both are 100,
`commission_notional_collected = 100` means **$0.01, not $1.00** — reading it one scale short turned a
$0.01 fee into $1.00 on a 32-cent trade, exceeded a customer's prefund and suppressed their refund.

<Note>
  **The taker coefficient changed on 2026-09-14, and there is no fee-schedule endpoint.** Store Θ with
  an effective date in your own configuration and reconcile each fill against the coefficient in force
  on its traded day, not against today's. Every partner hand-edited their systems for that change.
</Note>

### Runnable end-of-day skeleton

This resumes from a checkpoint, never replays the day, reconciles executions and fees per symbol
against the anchor read, and exits non-zero on a break. It reuses the auth and `post` helpers from
[Reporting and bulk export](#reporting-and-bulk-export) rather than restating them.

<CodeGroup>
  ```python reconcile_eod.py theme={null}
  # pip install requests "pyjwt[crypto]"
  import json, sqlite3, sys, time
  from decimal import Decimal, ROUND_HALF_EVEN
  from export_trades import access_token, post   # the snippet in "Reporting and bulk export" above

  DB          = "book.sqlite"     # your existing local database, written by your drop-copy consumer
  DAY         = "2026-09-14"      # the traded day you are closing
  SYMBOLS     = ["aec-cfb-clmsn-lsu-2026-09-05"]  # symbols you traded that day
  THETA_TAKER = Decimal("0.06")   # taker Θ in force on DAY, from the published Fee Schedule. It changed
                                  # on 2026-09-14; store it per effective date, never as one constant
  ROWS_KEY    = "trades"          # VERIFY with your integration lead, as in the export above

  db = sqlite3.connect(DB)

  def checkpoint(stream):
      """A restart resumes from here. If this returns None you are genuinely cold-starting."""
      row = db.execute("SELECT resume_token FROM checkpoints WHERE stream = ?", (stream,)).fetchone()
      return row[0] if row else None

  def expected_fee(contracts: Decimal, price: Decimal) -> Decimal:
      """Fee = Θ × C × p × (1 − p), banker's rounded to the cent."""
      return (THETA_TAKER * contracts * price * (Decimal(1) - price)).quantize(
          Decimal("0.01"), rounding=ROUND_HALF_EVEN)

  breaks = []

  if checkpoint("dropcopy") is None:
      breaks.append("no drop-copy checkpoint: this is a cold start, not a restart")

  token, expires_at = access_token()
  for symbol in SYMBOLS:
      if time.time() > expires_at:
          token, expires_at = access_token()

      # Your store is the record. Read it first; do not rebuild it from the anchor.
      local = db.execute(
          "SELECT price_wire, qty_wire, commission_wire, price_scale, qty_scale "
          "FROM executions WHERE traded_day = ? AND symbol = ?", (DAY, symbol)).fetchall()

      # The anchor read. Always symbol-filtered: unfiltered hits the 30s edge 504 two thirds of the time.
      anchor, page = [], None
      while True:
          body = {"where_clause": f"symbol = '{symbol}'",      # VERIFY filter shape
                  "start_time": f"{DAY}T00:00:00Z", "end_time": f"{DAY}T23:59:59Z"}
          if page:
              body["pageToken"] = page                          # VERIFY field name
          res = post("/v1/report/trades/search", body, token)
          anchor.extend(res.get(ROWS_KEY, []))
          page = res.get("nextPageToken")
          if not page:                                          # absent token is EOF
              break
      # Offset-based page tokens can repeat a row within a shard.
      anchor = {json.dumps(r, sort_keys=True) for r in anchor}

      if len(local) != len(anchor):
          breaks.append(f"{symbol}: {len(local)} executions locally, {len(anchor)} in the anchor read. "
                        f"Fewer in the anchor after an overnight window is expected (history is "
                        f"archived during maintenance); more in the anchor is a consumer gap")

      for price_wire, qty_wire, commission_wire, price_scale, qty_scale in local:
          # Scales are read per instrument and multiply. Never hard-code the divisor.
          price     = Decimal(price_wire) / Decimal(price_scale)
          contracts = Decimal(qty_wire) / Decimal(qty_scale)
          # One dollar is price_scale × qty_scale notional units.
          fee       = Decimal(commission_wire) / (Decimal(price_scale) * Decimal(qty_scale))
          want      = expected_fee(contracts, price)
          if fee != want:
              breaks.append(f"{symbol}: commission {fee} != expected {want} "
                            f"(C={contracts} p={price} Θ={THETA_TAKER})")

  for b in breaks:
      print("BREAK:", b, file=sys.stderr)
  print(f"{DAY}: {len(breaks)} breaks across {len(SYMBOLS)} symbols")
  sys.exit(1 if breaks else 0)
  ```
</CodeGroup>

The break report is the deliverable, not a clean run. Alert on the count, keep the per-symbol detail,
and never let the job repair your store automatically — a repair that trusts the anchor read will
delete executions that maintenance archived.

### Where reconciliation does not close today

State these three as known differences in your own controls, because none of them has a fix you can
implement.

**Sub-cent residue.** Transfers accept at most **2 decimal places**, and balances carry more —
`172.395` and `1377.57275` are both on record. There is no sanctioned handling for the residue, so a
participant balance cannot be swept to exactly zero. Carry it as a standing, named difference rather
than forcing it.

**Accrued vendor fees are not exposed on any endpoint.** The platform never knows your fee basis, so
you track accruals yourself and reconcile against the daily Vendor Fees report. Your spendable-cash
figure must subtract your own accrued, uncollected fees, because nothing on the API does it for you.

<Warning>
  **The Vendor Fees report has no delivery mechanism.**&#x20;
  The fee-collection loop has no defined channel today. Agree a manual delivery with your integration
  lead and record which side owns the comparison until this ships.
</Warning>

**No firm-level ledger stream.** The balance-ledger stream is **per account** and counts against the
20-streams-per-firm cap, so you cannot run one per participant. The firm-level stream is "coming
soon" with no date, which leaves no scalable push channel for `RESOLUTION` for any ISV at scale.

<Warning>
  **Three settlement-side questions are open and two of them affect a daily close.**

  Until the gross-versus-net question is answered, do not assume `RESOLUTION` behaves like
  `ORDER_EXECUTION`. Identity 1 above holds either way; a per-entry fee attribution does not.
</Warning>

Two smaller ones worth encoding: **twelve internal `LedgerEntryType` values are suppressed**
(`NETTING`, `GIVE_UP`, `INTEREST`, `SETTLEMENT_FEE` among them) and requesting one returns `Aborted`
/ 409, so filter your ledger queries to the allowlisted types. And proto3 does not populate scalar
fields at their default value, so **an absent field and a zero are indistinguishable on the wire** —
do not treat a missing number as a break.

## Breaking changes and the changelog

A daily close only holds if the surface underneath it does not move without telling you. Subscribe to the changelog by RSS or Slack `/feed subscribe`, and treat it as the only authoritative record of a change.

### What to subscribe to

| Channel                         | What it carries                                                                                   | How you subscribe                                                            |
| ------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **Changelog**                   | `<Update>` entries, one per release. 84 entries from v0.0.22 (2026-03-30) to v0.0.90 (2026-09-13) | RSS, or Slack `/feed subscribe` in your own workspace                        |
| Your shared Slack channel       | Operational notices, incident traffic, answers                                                    | You are already in it. Tag the Polymarket side; untagged messages get missed |
| `status.polymarketexchange.com` | Production status                                                                                 | Subscribe there. It does **not** cover preprod                               |

Subscribe both a human and a machine-readable consumer. If a feed entry arrives with no release date,
that is a known authoring defect on our side rather than a malformed release: a JSX fragment used as
an entry label slugified the anchor to `#object-object` and dropped the date from the feed. Labels are
plain strings now; report an undated entry.

### The commitment

**Every RPC or field removal gets a changelog entry, a migration note naming the replacement, and a
deprecation window.** An entry with no migration note is incomplete and worth pushing back on in your
shared channel.

<Warning>
  **The length of the deprecation window is not yet published.**&#x20;
  Do not plan a migration against an assumed window. Ask your integration lead for the committed
  minimum, and until it is published, treat any announced removal as immediate work.
</Warning>

### The honest history

This section exists because the commitment above has not always been met. Four events, all on the record:

**2026-08-06 — three RPCs removed with no changelog entry.** `CreateFundedOrder`,
`PreviewFundedOrder` and `OdfSweep` were removed and replaced by `CreateVendorOrder` plus `Transfer`.
No entry shipped. **A partner discovered the removal through gRPC reflection when their entire money
path broke.** That discovery path is not even generally available: reflection is entitlement-gated and
returns `PermissionDenied: method not permitted` without the grant, so most partners would have had no
way to find out at all. A backdated `Breaking Change` entry for this removal is part of the current
overhaul.

**Exchange migration dates moved at least three times, and partners reconstructed the timeline
themselves.** If your plan depends on a migration date, hold it as a range in your own tracker and
re-confirm it in writing before you commit a client date to it.

**Two changelog corrections were issued as Slack replies rather than changelog amendments.** A reader
of the changelog therefore held a wrong value with no way to know it had been corrected.

**A correction shipped and did not propagate.** Changelog v0.0.25 on 2026-04-17 corrected the gRPC
hostname form to `grpc-api.{env}.polymarketexchange.com`, but missed `/trader-guide/environments`,
which still publishes `grpc-preprod.polymarketexchange.com`. A changelog entry does not guarantee
every page was updated; the changelog wins over a page.

### The rule that follows

**The changelog is the only authoritative channel for a change. A Slack correction is not a change
record.**

Concretely, for you:

* If a value you rely on is corrected in Slack, ask for a changelog entry or amendment in the same
  thread. Quote the corrected value back so the thread contains it either way.
* If a change reaches you only through Slack, treat it as unconfirmed until it appears in the
  changelog, and say so in the thread.
* Diff the changelog against your own integration surface on a schedule, rather than reading entries
  as they arrive. Four of the changes below were only actionable once someone compared a release note
  to their own code.

### Changes that have cost partners work

Read these as the shapes to expect, not as a complete list.

| Date       | Release | Change                                                        | What it cost                                                                                                                                |
| ---------- | ------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-08-06 | none    | `CreateFundedOrder`, `PreviewFundedOrder`, `OdfSweep` removed | A partner's whole money path, found via reflection                                                                                          |
| 2026-09-08 | v0.0.86 | 71 `market_sport_type` values added                           | A partner waited 13 days for the enum inventory                                                                                             |
| 2026-09-09 | v0.0.87 | 8 more `market_sport_type` values added                       | The same enum, two releases apart                                                                                                           |
| 2026-09-13 | v0.0.89 | Day orders **cancel** at the traded-day roll; use GTD         | Not applicable to the FOK-only partner surface, but it changes any seeded resting order in preprod. → [Test harness](/preprod#test-harness) |
| 2026-09-14 | —       | The taker fee coefficient Θ changed                           | **Every partner hand-edited their systems**, because there is no fee-schedule endpoint                                                      |

<Warning>
  **There is no fee-schedule endpoint.**&#x20;
  Store Θ in your own configuration with an effective date, and reconcile each fill against the
  coefficient in force on its traded day. → [Daily reconciliation](#daily-reconciliation)
</Warning>

An enum addition is a breaking change for you even though it is additive for us: code that switches
exhaustively over `market_sport_type` will throw on a value added in a release you did not read.
Default your enum handling to a safe unknown branch and log it.

### Pin what you build against

Two properties of what we ship make version drift your problem to manage:

* **The proto bundle is an unversioned, anonymous Drive zip** (`polymarket-protos.zip`) with no
  changelog and no checksum. **Neither side can tell which build you hold.** Checksum your copy
  yourself, record the hash with your release, and quote it in any ticket about a field or RPC.
* **The bundle advertises 14 services and defines 5.** `OrderFundingService` and
  `CashMovementService` are absent from it entirely, which is exactly where your money path lives.
  `CreatePositionSubscription` has no proto definition anywhere.

The published `CreateCashMovement` proto has also shipped with a **wrong field number**: `transfer` is
field 4 in the real service, and the published copy omitted an intent occupying field 2 and renumbered
`transfer` down to fill the gap. A partner's `"20.00"` therefore arrived as that internal intent,
which is why the error named `amount`. If an error names a field you did not set, suspect your proto
build before you suspect your code, and quote your checksum. → [Protos and SDKs](/environments#protos-and-sdks)

### Stability labels

<Snippet file="beta.mdx" />

<Warning>
  **There is no published convention for what alpha, beta and GA mean on this surface.**

  Until one exists, "beta" on a page tells you a surface is enabled per partner and may change. It
  does not tell you what notice you get. Ask for the notice commitment per surface you depend on,
  in writing.
</Warning>

## Support and escalation

Where to take a problem, and what to put in the message, depends on your partner type.

<CardGroup cols={2}>
  <Card title="ISV support and escalation" icon="headset" href="/support">
    Routing, what to include by category, severity, and the routes that are not yet defined.
  </Card>

  <Card title="IB support and escalation" icon="scale-balanced" href="/ib/support">
    The same, plus IB-specific routes and what a regulatory or reporting question needs.
  </Card>
</CardGroup>

## What can go wrong

| Part             | Symptom                                                        | Cause                                                                                             | What you do                                                                                                                                           |
| ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Reporting        | `504` after 30 seconds on a report call                        | Unfiltered query at the CloudFront edge; happens about two thirds of the time                     | Add a symbol filter and shard. Do not retry the unfiltered call                                                                                       |
| Reporting        | The same trade appears twice in your export                    | Page tokens are offset-based and rows moved under the sweep                                       | Dedupe per symbol shard, as the snippet does                                                                                                          |
| Reporting        | A 404 while paging                                             | EOF behaviour on the last page is a known defect                                                  | Stop when `nextPageToken` is absent; do not issue a confirming request                                                                                |
| Reporting        | `InvalidArgument: field not allowed` on a time cursor          | You used `field_filter`, whose allowlist excludes `update_time` and `create_time`                 | Use `where_clause`                                                                                                                                    |
| Reporting        | Trade stats show a price of 0 in some buckets                  | Empty buckets are returned as all-zero objects                                                    | Treat zero as no-trades; never as a print at 0                                                                                                        |
| Reporting        | `cleared_*` are all zero for today's trades                    | They reflect post-clearing state                                                                  | Expected. Reconcile cleared figures the next day                                                                                                      |
| Reporting        | Buckets are 2.8 days wide when you asked for 6                 | `bars` is a divisor over the window                                                               | Set `bars` to the bucket count you want across that exact window                                                                                      |
| Reporting        | Yesterday's trades return nothing after an overnight window    | Stored execution history is archived during maintenance                                           | Export from your own persisted drop-copy record. → [Daily reconciliation](#daily-reconciliation)                                                      |
| Reporting        | 429 on a sweep you thought was under the cap                   | A second process shares the firm-wide budget, or you hit the endpoint rung                        | Run exports from one place; honour `Retry-After` and the rung's `retry after` value                                                                   |
| Reconciliation   | `13 INTERNAL: Subscription manager revoked session`            | A second process with the same token subject and participant header opened the same subscription  | Elect one leader. Pass an explicit market list on `CreateOrderSubscription`                                                                           |
| Reconciliation   | `SocketError: other side closed` after ten minutes of quiet    | The 10-minute ALB timeout; gRPC streams bypass the 30-second gateway idle timeout                 | Reconnect and resume from your checkpoint. This is not data loss                                                                                      |
| Reconciliation   | Anchor read returns fewer executions than your store           | Maintenance archived stored execution history for that period                                     | Expected. Keep your store; do not repair from the anchor                                                                                              |
| Reconciliation   | Anchor read returns more than your store                       | A gap in your consumer, or a dropped redelivery                                                   | Backfill from the anchor and investigate the consumer window                                                                                          |
| Reconciliation   | Fees are off by exactly 100×                                   | Raw `order_qty` used as the contract count on a scale-100 instrument                              | De-scale with `fractionalQtyScale`                                                                                                                    |
| Reconciliation   | Fees are off by exactly 100× the other way                     | `commission_notional_collected` read at `priceScale` instead of `priceScale × fractionalQtyScale` | Divide by the product                                                                                                                                 |
| Reconciliation   | Fees are off by a small, uniform ratio on one day              | Θ changed and your constant did not                                                               | Store Θ per effective date; it changed on 2026-09-14                                                                                                  |
| Reconciliation   | A participant balance never reaches zero                       | Sub-cent residue: transfers take 2 decimals, balances carry more                                  | Carry it as a named difference                                                                                                                        |
| Reconciliation   | `InvalidArgument: invalid account` from `GetAccountBalance`    | You asked for a participant clearing account; it succeeds for a firm account                      | Derive the figure from the newest ledger entry's `afterBalance`                                                                                       |
| Reconciliation   | `Aborted` / 409 on a ledger query                              | You requested a suppressed `LedgerEntryType`                                                      | Filter to the allowlisted types                                                                                                                       |
| Reconciliation   | 429 on a 12/min read during an incident                        | A restart re-ran the day's reads instead of resuming                                              | Resume from the checkpoint; a restart is not a cold start                                                                                             |
| Breaking changes | An RPC you use returns `UNIMPLEMENTED` overnight               | It was removed. This has shipped without a changelog entry before                                 | Check the changelog, then post in Slack naming the RPC and your proto checksum                                                                        |
| Breaking changes | `PermissionDenied: method not permitted` on reflection         | Reflection is entitlement-gated                                                                   | Ask for the grant. Do not treat reflection as your change-detection mechanism                                                                         |
| Breaking changes | An error names a field you never set                           | Your proto build has a wrong or shifted field number                                              | Re-pull the bundle, checksum it, and quote the hash                                                                                                   |
| Breaking changes | Your enum switch throws on a live value                        | Values were added in v0.0.86 and v0.0.87 without an inventory                                     | Add a safe unknown branch and log the value                                                                                                           |
| Breaking changes | Your fee figures drift from ours after a specific date         | Θ changed and your constant did not                                                               | Store Θ per effective date                                                                                                                            |
| Breaking changes | A value you read in the docs is wrong and Slack says otherwise | A correction was issued as a Slack reply, or a correction missed a page                           | The changelog wins over a page. Ask for the entry or amendment in the thread                                                                          |
| Breaking changes | A changelog feed entry has no date                             | The known label-slugification defect                                                              | Report it; the entry itself is valid                                                                                                                  |
| Support          | Your report gets no reply                                      | The message was untagged, or tagged in the namespace nobody watches                               | Re-post and tag a person by name. Ask which handle to tag and pin the answer                                                                          |
| Support          | You are asked for an identifier you did not keep               | The category's key identifier was not captured at the time                                        | Log `idempotency_key`, `clord_id`, `external_id`, `event_id` and `resume_token` presence on every call, before you need them                          |
| Support          | An email to `institutional@polymarket.us` goes nowhere useful  | It is the published route for inbound, not for an integrating partner                             | Use your shared channel                                                                                                                               |
| Support          | You cannot tell whether preprod is down                        | There is no preprod status page, calendar or health endpoint                                      | Report it with a timestamp. Assume ours, not yours                                                                                                    |
| Support          | A `PERMISSION_DENIED` persists after we grant a scope          | The token predates the grant                                                                      | Re-mint the token. → [Authentication](/authentication)                                                                                                |
| Support          | A support answer contradicts a docs page                       | A correction was issued in Slack rather than as a changelog entry                                 | The changelog is the only authoritative change record. Ask for the entry. → [Breaking changes and the changelog](#breaking-changes-and-the-changelog) |

## Next

[Start here](/start)
