> ## 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.

# Instruments and money on the wire

> Walk the instrument hierarchy, then convert prices, quantities, cash and fees with the two scales you read off the instrument.

Every order names one instrument symbol, and every money field on that order is scaled by two numbers that live on that same instrument. You read the symbol and both scales off one response, so discovery and unit conversion are one job, not two.

<Info>
  Before this page: a working access token and the `read:instruments` scope granted against your
  client ([Authentication](/authentication)). Scopes are granted server-side — you
  cannot request them on the token exchange, and you must re-mint your token after we add a grant.

  Instrument reads do not need `x-participant-id`. Neither does `/v1/refdata/*` or
  `CreateInstrumentStateChangeSubscription`, which needs `read:instruments` only. See
  [Firms, participants and accounts](/identity) for where the header
  does apply.
</Info>

<Snippet file="endpoints.mdx" />

<Info>
  **API reference:** <a href="https://docs.polymarket.us/institutional/refdata/overview" target="_blank" rel="noreferrer">Reference data</a> · <a href="https://docs.polymarket.us/institutional/orderbook/overview" target="_blank" rel="noreferrer">Order book</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>

## Find instruments

### The hierarchy

Reference data is five levels deep:

**category → series → event → market → instrument (symbol)**

Only the bottom level is tradeable. `order.symbol` names an instrument, market-data subscriptions take
instrument symbols, and there is no RPC that accepts a category, series, event or market as a trading
identifier. If your data model stops at "event", you cannot place an order.

The two levels above the instrument are where partners lose time, because grouping is ambiguous:

* `eventAttributes.eventId` groups instruments into events.
* `metadata.event_id` groups `aec`, `asc` and `tsc` instruments into the *same* event.

Those are two different groupings of the same instruments, and which one is authoritative is not
documented.

<Warning>
  **do not build a single event key yet.** `eventAttributes.eventId` and
  `metadata.event_id` group differently and we have not published which is canonical. Key your own
  rows on the instrument `symbol`, which is unambiguous, and carry both event identifiers alongside it
  rather than choosing one.
</Warning>

### Two spellings for the same field

The same instrument value is spelled differently depending on which surface you read it from: REST
responses are camelCase (`priceScale`, `fractionalQtyScale`, `eventAttributes.eventId`) and the
institutional proto surface is snake\_case (`fractional_quantity_scale`, `metadata.event_id`,
`market_sport_type`, `long_participant_id`). Normalize on ingest. A parser that looks for
`fractionalQtyScale` on a proto message finds nothing, and an absent scale is not a scale of 1 — see
[the proto3 caveat](#two-wire-format-traps).

### What you need off an instrument before you can order

| What you need     | Field                                              | Why the order fails without it                                                                                                |
| ----------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Instrument symbol | `symbol`                                           | The only tradeable identifier.                                                                                                |
| Price scale       | `priceScale` (`price_scale` on proto)              | Multiplies every price and every `cash_order_qty`. 100 and 1000 are both live in production.                                  |
| Quantity scale    | `fractionalQtyScale` (`fractional_quantity_scale`) | Multiplies every quantity. Reading raw `order_qty` as a contract count overstates a fee 100× on a scale-100 instrument.       |
| Tick size         | Not published **\[VERIFY]**                        | 0.5¢ and 0.25¢ ticks are both live; combos (`caoc`) are fixed at \$0.001. A price off-tick has no published rejection string. |
| Minimum quantity  | Not published **\[VERIFY]**                        |                                                                                                                               |
| Lifecycle state   | `InstrumentState`                                  | Only some states accept an order.                                                                                             |

`InstrumentState` has **nine** values, including `PENDING`, `CLOSED`, `TERMINATED` and
`MATCH_AND_CLOSE_AUCTION`. Treat the enum as open — code that switches exhaustively over a five-state
subset will silently mishandle the other four.

<Warning>
  **Not yet published.**&#x20;
  We have not published the complete `InstrumentState` inventory or which states accept an order.
  Confirm both with your integration lead, and until then treat any state you do not recognise as
  not-tradeable rather than tradeable.
</Warning>

### Read the instrument list

<Note>
  The generated stub names below assume you ran `protoc` over `polymarket-protos.zip` with the package
  layout that bundle ships. The bundle is unversioned, has no checksum and no changelog, so neither
  side can tell which build you hold — check your generated package path before copying these imports.
  `OrderFundingService` and `CashMovementService` are absent from the bundle entirely.
</Note>

<CodeGroup>
  ```python Python theme={null}
  # Lists instruments and prints the six values you need before ordering.
  # PolymarketClient is the canonical auth client published on /connect#authentication:
  # it mints a private_key_jwt access token and refreshes on expires_in minus 30 seconds.
  from polymarket_auth import PolymarketClient

  # Generated from polymarket-protos.zip. VERIFY your own package path.
  from polymarket.v1 import refdata_pb2, refdata_pb2_grpc

  # The gRPC target is GRPC_TARGET in polymarket_auth.py — one place, both environments.
  pmx = PolymarketClient()

  with pmx.channel() as ch:
      stub = refdata_pb2_grpc.ReferenceDataAPIStub(ch)
      # ListInstruments is capped at 6 requests/min. Page, cache, do not poll.
      # /v1/refdata/* is not account-scoped: no x-participant-id.
      resp = stub.ListInstruments(refdata_pb2.ListInstrumentsRequest(), metadata=pmx.metadata())

  for inst in resp.instruments:
      # Both scales are multipliers. One dollar is price_scale * fractional_quantity_scale units.
      print(
          inst.symbol,
          inst.price_scale,                 # 100 on most instruments, 1000 on some
          inst.fractional_quantity_scale,   # 100 on most instruments, 1 on some
          inst.state,
      )
  ```

  ```typescript TypeScript theme={null}
  // Lists instruments and prints the six values you need before ordering.
  // PolymarketClient is the canonical auth client published on /connect#authentication.
  import { PolymarketClient } from "./polymarketAuth";

  // Generated from polymarket-protos.zip. VERIFY your own package path.
  import { ReferenceDataAPIClient } from "./gen/polymarket/v1/refdata";

  // The gRPC target is GRPC_TARGET in polymarketAuth.ts — one place, both environments.
  const pmx = new PolymarketClient();

  const client = new ReferenceDataAPIClient(pmx.channel());

  // ListInstruments is capped at 6 requests/min. Page, cache, do not poll.
  // /v1/refdata/* is not account-scoped: no x-participant-id.
  const resp = await client.listInstruments({}, await pmx.metadata());

  for (const inst of resp.instruments) {
    // Both scales are multipliers. One dollar is priceScale * fractionalQtyScale units.
    console.log(
      inst.symbol,
      inst.priceScale,          // 100 on most instruments, 1000 on some
      inst.fractionalQtyScale,  // 100 on most instruments, 1 on some
      inst.state,
    );
  }
  ```

  ```bash grpcurl theme={null}
  # curl cannot speak gRPC. Use grpcurl.
  # Server reflection is entitlement-gated and returns
  #   PermissionDenied: method not permitted
  # without the grant, so pass the protos explicitly rather than relying on -use-reflection.
  TOKEN="$(./token.sh)"   # token.sh is published on /connect#authentication; stdout is the token

  grpcurl \
    -import-path ./polymarket-protos \
    -proto polymarket/v1/refdata.proto \
    -H "authorization: Bearer ${TOKEN}" \
    -d '{}' \
    grpc-api.preprod.polymarketexchange.com:443 \
    polymarket.v1.ReferenceDataAPI/ListInstruments \
    || { echo "ListInstruments failed"; exit 1; }
  ```

  ```go Go theme={null}
  // Lists instruments and prints the six values you need before ordering.
  package main

  import (
  	"context"
  	"fmt"
  	"log"

  	"google.golang.org/grpc/metadata"

  	// Canonical auth client from /connect#authentication. The gRPC target is
  	// pmauth.GRPCTarget — one place, both environments.
  	pmauth "example.com/yourfirm/pmauth"

  	// Generated from polymarket-protos.zip. VERIFY your own package path.
  	refdata "github.com/yourfirm/gen/polymarket/v1"
  )

  func main() {
  	ctx := context.Background()
  	pm := pmauth.New()

  	conn, err := pm.Channel()
  	if err != nil {
  		log.Fatalf("dial: %v", err)
  	}
  	defer conn.Close()

  	// /v1/refdata/* is not account-scoped: pass "" for the participant ID.
  	md, err := pm.Metadata("")
  	if err != nil {
  		log.Fatalf("metadata: %v", err)
  	}

  	// ListInstruments is capped at 6 requests/min. Page, cache, do not poll.
  	resp, err := refdata.NewReferenceDataAPIClient(conn).ListInstruments(
  		metadata.NewOutgoingContext(ctx, md), &refdata.ListInstrumentsRequest{})
  	if err != nil {
  		log.Fatalf("ListInstruments: %v", err)
  	}

  	for _, inst := range resp.GetInstruments() {
  		// Both scales are multipliers. One dollar is PriceScale * FractionalQuantityScale units.
  		fmt.Println(
  			inst.GetSymbol(),
  			inst.GetPriceScale(),              // 100 on most instruments, 1000 on some
  			inst.GetFractionalQuantityScale(), // 100 on most instruments, 1 on some
  			inst.GetState(),
  		)
  	}
  }
  ```
</CodeGroup>

The response shape, with only the fields you will branch on:

```json theme={null}
{
  "instruments": [
    {
      "symbol": "aec-cfb-clmsn-lsu-2026-09-05",
      "priceScale": 100,
      "fractionalQtyScale": 100,
      "state": "INSTRUMENT_STATE_EXPIRED",
      "eventAttributes": { "eventId": "..." },
      "metadata": { "event_id": "..." },
      "market_sport_type": "...",
      "long_participant_name": "Tigers",
      "short_participant_name": "Tigers",
      "home_team_name": "Tigers",
      "away_team_name": "Tigers"
    }
  ]
}
```

#### Rate limit

`ListInstruments` is **6 requests per minute, per firm**. `ListSymbols` is the same; `GetOrderBook`
and `GetBBO` are 12/min each. Exceeding a REST limit returns `429` with a `Retry-After` header.

There is also an undocumented per-endpoint rung that support did not know about, which returns gRPC
code 8 with its own backoff hint:

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

Honour the `retry after` value in that message rather than your own backoff curve.

<Warning>
  **Not yet published.**&#x20;
  No separate preprod value is published for the reference-data limits. Assume they are the same in
  both environments and confirm with your integration lead before you build a refresh schedule around
  them.
</Warning>

### The four gaps you will hit

These are the real reasons a sports integration stalls. None of them has a workaround we have not
listed.

#### 1. There is no main line, and you compute it

\[GAP] There is **no `main_line` and no `is_main` field** on any instrument. The decision on record is
that partners compute the main line themselves. That decision exists only in a Slack message, so
nothing in reference data will ever mark it for you.

Rule: derive the main line from the instruments you already hold — do not wait for a field to appear,
and do not treat the first instrument in a list as the main line, because list order is not documented
as stable.

<Warning>
  **Not yet published.**&#x20;
  We have not published a recommended derivation. Agree yours with your integration lead in writing,
  because it will determine which line your users see as the headline price.
</Warning>

#### 2. Home and away are not recoverable by name

\[GAP] On a same-nickname matchup, **all four name fields carry the same string**. For
`aec-cfb-clmsn-lsu-2026-09-05` — Clemson at LSU — `long_participant_name`,
`short_participant_name`, `home_team_name` and `away_team_name` all come through as `"Tigers"`. There
is no name-based way to tell which side is home. This is common in CFB and CBB, not an edge case.

Rule: resolve sides from the symbol's team slugs (`clmsn`, `lsu` in the example) or from
`/v1/sports/teams`, never from the four name fields. A display layer that reads `home_team_name` will
print "Tigers vs Tigers".

#### 3. Tennis name fields are populated in preprod and empty in prod

<Warning>
  **do not gate on these fields.** Tennis `home_team_name`, `away_team_name` and
  `tournament_name` are populated in preprod and **empty in production**. A tennis integration that
  passes preprod acceptance on those fields ships blank rows to production users.

  Build your tennis display to render correctly with all three fields empty, and source names from
  `/v1/sports/players` instead.
</Warning>

#### 4. The sports market type enum grows without warning

The value lives on `sportsMarketType` on the retail surface and `market_sport_type` on the
institutional surface. It is not a fixed vocabulary: **71 values were added on 2026-09-08 (v0.0.86)
and 8 more on 2026-09-09 (v0.0.87) — 79 new values in two days.** One partner waited 13 days to be
given the enum inventory.

Rule: treat it as an open string. Map the values you support, route everything else to a generic
renderer, and alert on unmapped values instead of dropping the instrument. Subscribe to the changelog
feed — additions ship there, not to a schema endpoint.

### Players and teams

`/v1/sports/players` and `/v1/sports/teams` exist and are how you get participant metadata.
`long_participant_id` keys to SportRadar and SDIO provider IDs, so join your own provider data on
`long_participant_id` rather than on any name field. These two endpoints were undocumented for six
months and that blocked one partner's player props from March to September 2026.

<Warning>
  **Both endpoints are on `gateway.polymarket.us`, which is the retail surface.** Engineering guidance
  is that partners should not build against `gateway.polymarket.us`, and `/v2/home` and `/v2/live` on
  it are internal-only. There is no institutional equivalent of players and teams today, so the only
  route to that data and the guidance about that host point in opposite directions.

  Raise this with your integration lead before you make player props a launch feature.
</Warning>

Credentials do not carry across. `gateway.polymarket.us` and `api.polymarket.us` are a different
product with `X-PM-Access-Key` / `X-PM-Timestamp` / `X-PM-Signature` (Ed25519) auth. Those credentials
never work against `api.*.polymarketexchange.com`, and the reverse is also true.

### Books diverge between surfaces

<Warning>
  **the gateway book and the exchange book disagree.** They diverge in both depth and
  values, and which one is authoritative is undocumented. Partners who compared them found the gateway
  book "more reasonable", which is not a basis for pricing customer orders.

  Price against the exchange book you trade on — `GET /v1/orderbook/{symbol}` or the market-data
  stream on `api.*.polymarketexchange.com` — and do not reconcile your fills against gateway depth.
</Warning>

## Money on the wire

Convert every price, quantity, cash amount and fee between your decimal ledger and the wire using the two scales published on the instrument you are trading — the same two scales you just read off it.

This section caused a real customer-money loss. Every number in it is load-bearing.

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

### The three conversions

```
wire price       = decimal YES price × priceScale
wire quantity    = decimal contracts × fractionalQtyScale
wire cash qty    = decimal USD principal × priceScale
```

Divide by the same scales to go back. **They are multipliers, not exponents and not decimal places.**

A `priceScale` of 100 does not mean "two decimal places" and does not mean "10²". It means multiply
by 100. Code that treats the scale as an exponent computes `10^100` where it should have multiplied
by 100 — a number no money field can hold, on an instrument whose scale is one of the two live
values.

Note the third line: `cash_order_qty` is a USD amount and it is scaled by **`priceScale`**, not by a
cash scale and not by `priceScale × fractionalQtyScale`. There is no separate cash scale on the
instrument.

### Derive both scales per instrument. Never hard-code them.

`priceScale` 100 and `priceScale` 1000 are both live in production. So are `fractionalQtyScale` 100
and `fractionalQtyScale` 1.

**Two of 121 open ATP instruments publish `priceScale: 1000` with `fractionalQtyScale: 100`, which
makes the notional divisor 100,000 rather than 10,000.** A hard-coded divisor of 10,000 is correct
**only where both scales are 100** — it is wrong by 10× on those two instruments, and wrong by 100×
on any instrument publishing `fractionalQtyScale: 1`, where the real divisor is 100. The table below
gives all three combinations.

That is the worst available failure shape: it reconciles perfectly for weeks, passes preprod
acceptance, and then breaks on two symbols out of 121 with no error anywhere. Read both scales off
the instrument on every order and every fill.

### One dollar is `priceScale × fractionalQtyScale` notional units

Notional fields — `commission_notional_collected` above all — are quoted in units of
`1 / (priceScale × fractionalQtyScale)` dollars.

| `priceScale` | `fractionalQtyScale` | Units per dollar | `commission_notional_collected: "100"` means |
| ------------ | -------------------- | ---------------- | -------------------------------------------- |
| 100          | 100                  | 10,000           | **\$0.01**                                   |
| 100          | 1                    | 100              | \$1.00                                       |
| 1000         | 100                  | **100,000**      | \$0.001                                      |

When both scales are 100, `commission_notional_collected = 100` means **$0.01, not $1.00**.

<Warning>
  **`commission_notional_collected` has no scale field and no comment**, in the protos or in the docs.
  Nothing on the wire tells you which divisor to use — you must compute it from the instrument's two
  scales yourself.

  Reading it at `priceScale` instead of `priceScale × fractionalQtyScale` turns a $0.01 fee into
      $1.00 on a 32-cent trade. That is exactly what happened: the inflated fee exceeded a real customer's
  entire prefund and suppressed their refund.
</Warning>

### The most common reconciliation mistake

Using raw `order_qty` as the contract count.

On an instrument with `fractional_quantity_scale = 100`, `order_qty: "75"` is **0.75 contracts**, not
75\. Feeding 75 into the fee formula overstates the fee by 100×.

It hides because instruments with `fractionalQtyScale = 1` make the naive math accidentally correct.
Your reconciliation passes on every scale-1 market, so the error usually surfaces on your **first fill
in a scale-100 market** — often in production, because preprod liquidity is concentrated in different
symbols than production volume.

Rule: divide `order_qty` by that instrument's `fractionalQtyScale` before it touches any formula, and
assert the scale was read from reference data rather than defaulted.

### Fees

```
Fee = Θ × C × p × (1 − p)
```

where `C` is the de-scaled contract count and `p` is the de-scaled YES price. Round the result **to
the cent with banker's rounding** (round-half-to-even): a fee of $0.045 rounds to $0.04 and a fee of
$0.055 rounds to $0.06. Round-half-up drifts against you by a cent on every tie.

The published schedule has two coefficients:

| Role  | Θ                    | Can you ever pay it?                                                  |
| ----- | -------------------- | --------------------------------------------------------------------- |
| Taker | `0.06`               | **Yes — this is the only rate you can ever pay.**                     |
| Maker | `−0.0125` (a rebate) | No. See [You are always the taker](/orders#you-are-always-the-taker). |

<Warning>
  **The taker coefficient changed from `0.06` to `0.0695` on 2026-09-14, and \[GAP] there is no
  fee-schedule endpoint.** Every partner had to hand-edit their systems, because Θ is not readable
  from the API at all.

  Do not hard-code Θ in more than one place, and confirm today's value with your integration lead
  before you invoice a user for a fee. The worked example below uses `0.06`.
</Warning>

### Combos are fixed constants

Combo instruments (`caoc`) do not vary: **tick size \$0.001, `priceScale` 1000, `qtyScale` 100.**
Those are fixed, so hard-coding them for `caoc` is correct — this is the one place the per-instrument
rule does not apply.

You have to hard-code them, because **the RFQ stream does not carry scales at all** and per-combo
reference-data lookups are not viable at roughly 100 combos per second against a 6/min
`ListInstruments` limit.

### Settlement prices

`settlementPriceScale` is **reserved and reads 0**. It is not the scale of `settlementPx`.

**De-scale `settlementPx` with `priceScale`.** A divide by `settlementPriceScale` is a divide by zero.

### Two wire-format traps

**`int64` fields are serialized as strings in JSON.** `order_qty`, `cash_order_qty` and
`commission_notional_collected` arrive as `"75"`, not `75`. A JavaScript client that does
`Number(report.commission_notional_collected)` works until a value exceeds 2^53; parse them as
`BigInt` or `Decimal`, never as a float.

**proto3 default-value caveat: a scalar at its default value is not populated on the wire, so an
absent field and a zero are indistinguishable.** You cannot tell "no scale published" from
"`priceScale` is 0". Treat a missing or zero `priceScale` or `fractionalQtyScale` as a hard error and
refuse to place the order — never fall back to 1, and never fall back to 100.

Tick sizes of 0.5¢ and 0.25¢ are both live, so a price that is a valid multiple of one tick can be
off-tick on another instrument.

### A worked decode

A real-shaped execution report for a fill of **0.75 contracts at \$0.32** on an instrument with
`priceScale: 100` and `fractionalQtyScale: 100`:

<Warning>
  **\[VERIFY] — the field name carrying the executed price is not published.** The block below spells it
  `"price"`, and that spelling is **unconfirmed**; so is the rest of the execution report's field set.
  `clord_id`, `order_qty` and `commission_notional_collected` are named in verified sources.

  The **lesson** below — that the fee is $0.01 and not $1.00 — depends on the two scales and on
  `commission_notional_collected`, not on this spelling. Read the price field's real name off your own
  execution report before you wire up a decoder.
</Warning>

```json theme={null}
{
  "clord_id": "examplefirm-7f3c1a90",
  "price": "32",
  "order_qty": "75",
  "commission_notional_collected": "100"
}
```

Decoded, that report is: 0.75 contracts, $0.32, **$0.24 principal and a $0.01 fee.** Read the fee at `priceScale` alone and you get **$1.00\*\* — four times the entire principal of the trade.

<CodeGroup>
  ```python Python theme={null}
  # Decodes one execution report and checks the fee against the schedule.
  # Exact decimal arithmetic. Never use float for money.
  from decimal import Decimal, ROUND_HALF_EVEN

  # Read both scales off the instrument (see #find-instruments). Never hard-code them.
  PRICE_SCALE = Decimal(100)
  QTY_SCALE = Decimal(100)
  THETA_TAKER = Decimal("0.06")  # taker coefficient; there is no fee-schedule endpoint

  # int64 fields arrive as JSON strings.
  WIRE_PRICE = Decimal("32")
  WIRE_QTY = Decimal("75")
  WIRE_COMMISSION = Decimal("100")

  if PRICE_SCALE <= 0 or QTY_SCALE <= 0:
      raise ValueError("scale absent or zero: proto3 cannot distinguish these — refuse the order")

  price = WIRE_PRICE / PRICE_SCALE            # 0.32
  contracts = WIRE_QTY / QTY_SCALE            # 0.75  (NOT 75)
  principal = contracts * price               # 0.24
  dollar_units = PRICE_SCALE * QTY_SCALE      # 10000 notional units per dollar

  commission = WIRE_COMMISSION / dollar_units                  # 0.01  <- correct
  commission_wrong = WIRE_COMMISSION / PRICE_SCALE             # 1.00  <- the 100x error

  expected = (THETA_TAKER * contracts * price * (1 - price)).quantize(
      Decimal("0.01"), rounding=ROUND_HALF_EVEN                # banker's rounding, to the cent
  )

  def usd(amount: Decimal) -> str:
      return f"${amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)}"

  print(f"principal={usd(principal)} fee={usd(commission)}")
  print(f"reading the fee at priceScale alone would give {usd(commission_wrong)}")

  if commission != expected:
      raise AssertionError(f"fee mismatch: wire {commission} vs schedule {expected}")
  ```

  ```typescript TypeScript theme={null}
  // Decodes one execution report and checks the fee against the schedule.
  // Integer arithmetic on BigInt. Never use Number for money.

  // Read both scales off the instrument (see #find-instruments). Never hard-code them.
  const PRICE_SCALE = 100n;
  const QTY_SCALE = 100n;
  const THETA_NUM = 6n, THETA_DEN = 100n; // taker Θ = 0.06; there is no fee-schedule endpoint

  // int64 fields arrive as JSON strings.
  const WIRE_PRICE = BigInt("32");
  const WIRE_QTY = BigInt("75");
  const WIRE_COMMISSION = BigInt("100");

  if (PRICE_SCALE <= 0n || QTY_SCALE <= 0n) {
    throw new Error("scale absent or zero: proto3 cannot distinguish these — refuse the order");
  }

  // Banker's rounding on an exact integer ratio.
  function halfEven(n: bigint, d: bigint): bigint {
    let q = n / d;
    const r = n % d;
    if (2n * r > d) q += 1n;
    else if (2n * r === d && q % 2n === 1n) q += 1n;
    return q;
  }
  const cents = (x: bigint) => `$${(x / 100n)}.${String(x % 100n).padStart(2, "0")}`;

  const DOLLAR_UNITS = PRICE_SCALE * QTY_SCALE; // 10000 notional units per dollar

  // principal in cents = WIRE_QTY * WIRE_PRICE * 100 / (QTY_SCALE * PRICE_SCALE)
  const principalCents = halfEven(WIRE_QTY * WIRE_PRICE * 100n, QTY_SCALE * PRICE_SCALE); // 24
  const feeCents = halfEven(WIRE_COMMISSION * 100n, DOLLAR_UNITS);                        // 1
  const feeCentsWrong = halfEven(WIRE_COMMISSION * 100n, PRICE_SCALE);                    // 100 = the 100x error

  // Fee = Θ × C × p × (1 − p), to the cent, banker's rounding.
  const expectedCents = halfEven(
    THETA_NUM * 100n * WIRE_QTY * WIRE_PRICE * (PRICE_SCALE - WIRE_PRICE),
    THETA_DEN * QTY_SCALE * PRICE_SCALE * PRICE_SCALE,
  );

  console.log(`principal=${cents(principalCents)} fee=${cents(feeCents)}`);
  console.log(`reading the fee at priceScale alone would give ${cents(feeCentsWrong)}`);

  if (feeCents !== expectedCents) {
    throw new Error(`fee mismatch: wire ${feeCents}c vs schedule ${expectedCents}c`);
  }
  ```

  ```bash Shell theme={null}
  #!/usr/bin/env bash
  # Decodes one execution report and checks the fee against the schedule.
  # Integer arithmetic only. bash has no decimals, which is the right constraint for money.
  set -euo pipefail

  # Read both scales off the instrument (see #find-instruments). Never hard-code them.
  PRICE_SCALE=100
  QTY_SCALE=100
  THETA_NUM=6; THETA_DEN=100   # taker Θ = 0.06; there is no fee-schedule endpoint

  # int64 fields arrive as JSON strings.
  WIRE_PRICE=32
  WIRE_QTY=75
  WIRE_COMMISSION=100

  (( PRICE_SCALE > 0 && QTY_SCALE > 0 )) || {
    echo "scale absent or zero: proto3 cannot distinguish these — refuse the order" >&2; exit 1; }

  # Banker's rounding on an exact integer ratio.
  half_even() {
    local n=$1 d=$2 q r
    q=$(( n / d )); r=$(( n % d ))
    if   (( 2*r >  d ));                    then q=$(( q + 1 ))
    elif (( 2*r == d && q % 2 == 1 ));      then q=$(( q + 1 ))
    fi
    echo "$q"
  }
  show() { printf '$%d.%02d' $(( $1 / 100 )) $(( $1 % 100 )); }

  DOLLAR_UNITS=$(( PRICE_SCALE * QTY_SCALE ))   # 10000 notional units per dollar

  PRINCIPAL_C=$(half_even $(( WIRE_QTY * WIRE_PRICE * 100 )) $(( QTY_SCALE * PRICE_SCALE )))  # 24
  FEE_C=$(half_even       $(( WIRE_COMMISSION * 100 ))       "$DOLLAR_UNITS")                 # 1
  FEE_C_WRONG=$(half_even $(( WIRE_COMMISSION * 100 ))       "$PRICE_SCALE")                  # 100

  # Fee = Θ × C × p × (1 − p), to the cent, banker's rounding.
  EXPECTED_C=$(half_even \
    $(( THETA_NUM * 100 * WIRE_QTY * WIRE_PRICE * (PRICE_SCALE - WIRE_PRICE) )) \
    $(( THETA_DEN * QTY_SCALE * PRICE_SCALE * PRICE_SCALE )) )

  echo "principal=$(show "$PRINCIPAL_C") fee=$(show "$FEE_C")"
  echo "reading the fee at priceScale alone would give $(show "$FEE_C_WRONG")"

  (( FEE_C == EXPECTED_C )) || { echo "fee mismatch: $FEE_C vs $EXPECTED_C" >&2; exit 1; }
  ```

  ```go Go theme={null}
  // Decodes one execution report and checks the fee against the schedule.
  // Integer arithmetic on int64. Never use float64 for money.
  package main

  import (
  	"fmt"
  	"log"
  	"strconv"
  )

  // Read both scales off the instrument (see #find-instruments). Never hard-code them.
  const (
  	priceScale = int64(100)
  	qtyScale   = int64(100)
  	thetaNum   = int64(6) // taker Θ = 0.06; there is no fee-schedule endpoint
  	thetaDen   = int64(100)
  )

  // halfEven divides n by d with banker's rounding.
  func halfEven(n, d int64) int64 {
  	q, r := n/d, n%d
  	switch {
  	case 2*r > d:
  		q++
  	case 2*r == d && q%2 == 1:
  		q++
  	}
  	return q
  }

  func show(cents int64) string { return fmt.Sprintf("$%d.%02d", cents/100, cents%100) }

  func main() {
  	// int64 fields arrive as JSON strings.
  	wirePrice, err := strconv.ParseInt("32", 10, 64)
  	if err != nil {
  		log.Fatalf("price: %v", err)
  	}
  	wireQty, err := strconv.ParseInt("75", 10, 64)
  	if err != nil {
  		log.Fatalf("order_qty: %v", err)
  	}
  	wireCommission, err := strconv.ParseInt("100", 10, 64)
  	if err != nil {
  		log.Fatalf("commission_notional_collected: %v", err)
  	}

  	if priceScale <= 0 || qtyScale <= 0 {
  		log.Fatal("scale absent or zero: proto3 cannot distinguish these — refuse the order")
  	}

  	dollarUnits := priceScale * qtyScale // 10000 notional units per dollar

  	principalC := halfEven(wireQty*wirePrice*100, qtyScale*priceScale) // 24
  	feeC := halfEven(wireCommission*100, dollarUnits)                  // 1
  	feeCWrong := halfEven(wireCommission*100, priceScale)              // 100 = the 100x error

  	// Fee = Θ × C × p × (1 − p), to the cent, banker's rounding.
  	expectedC := halfEven(
  		thetaNum*100*wireQty*wirePrice*(priceScale-wirePrice),
  		thetaDen*qtyScale*priceScale*priceScale,
  	)

  	fmt.Printf("principal=%s fee=%s\n", show(principalC), show(feeC))
  	fmt.Printf("reading the fee at priceScale alone would give %s\n", show(feeCWrong))

  	if feeC != expectedC {
  		log.Fatalf("fee mismatch: %dc vs schedule %dc", feeC, expectedC)
  	}
  }
  ```
</CodeGroup>

All four print the same two lines:

```
principal=$0.24 fee=$0.01
reading the fee at priceScale alone would give $1.00
```

## What can go wrong

| Where             | Symptom                                                              | Cause                                                                                                | What you do                                                                                                                   |
| ----------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Find instruments  | `PERMISSION_DENIED` on `ListInstruments`                             | `read:instruments` is not granted on your client, or you minted the token before we added the grant  | Ask for the grant, then **re-mint your token**. Scopes are granted server-side and are not requestable on the token exchange. |
| Find instruments  | `429` with `Retry-After`                                             | Over 6 req/min on `ListInstruments`, per firm                                                        | Cache reference data. It is not a per-request lookup.                                                                         |
| Find instruments  | `{"code":8, ... (rung "endpoint"); retry after 126ms}`               | The undocumented per-endpoint rate-limit rung                                                        | Sleep for the `retry after` value in the message, then retry once.                                                            |
| Find instruments  | `PermissionDenied: method not permitted` from `grpcurl`              | gRPC server reflection is entitlement-gated                                                          | Pass `-import-path` / `-proto` explicitly, or ask for the reflection entitlement.                                             |
| Find instruments  | Display shows "Tigers vs Tigers"                                     | Reading `home_team_name` / `away_team_name` on a same-nickname matchup                               | Resolve sides from the symbol slugs or `/v1/sports/teams`.                                                                    |
| Find instruments  | Tennis rows render blank in prod but not preprod                     | `home_team_name`, `away_team_name`, `tournament_name` are empty in prod                              | Render with those fields empty; source names from `/v1/sports/players`.                                                       |
| Find instruments  | An instrument you expected to be tradeable rejects or ignores orders | Its `InstrumentState` is one of the four values outside the commonly-published subset                | Treat unrecognised states as not-tradeable and log the raw value.                                                             |
| Find instruments  | Your event groups split or merge unexpectedly                        | You keyed on one of `eventAttributes.eventId` / `metadata.event_id`                                  | Key on `symbol`; carry both event identifiers.                                                                                |
| Find instruments  | Order rejected after a schema change you did not make                | A new `market_sport_type` value, or a scale you cached                                               | Re-read reference data; never cache scales across a session.                                                                  |
| Money on the wire | Fees are 100× too large on some markets                              | Reading `commission_notional_collected` at `priceScale` instead of `priceScale × fractionalQtyScale` | Divide by both scales. This is the error that suppressed a real customer's refund.                                            |
| Money on the wire | Fees are 100× too large in your own fee model                        | Using raw `order_qty` as the contract count                                                          | Divide `order_qty` by `fractionalQtyScale` first.                                                                             |
| Money on the wire | Reconciliation is exact for weeks, then off by 10× on two symbols    | Hard-coded divisor of 10,000 against `priceScale: 1000` + `fractionalQtyScale: 100`                  | Derive the divisor per instrument, per fill. 2 of 121 open ATP instruments are in this shape.                                 |
| Money on the wire | Your fee and ours differ by exactly one cent on some fills           | Round-half-up instead of banker's rounding                                                           | Round half-to-even.                                                                                                           |
| Money on the wire | Your fee and ours differ by roughly 16% on every fill                | Θ changed from `0.06` to `0.0695` on 2026-09-14                                                      | Confirm today's coefficient with your integration lead — there is no fee-schedule endpoint to read.                           |
| Money on the wire | A `cash_order_qty` order is 100× the intended size                   | Scaling cash by `priceScale × fractionalQtyScale`                                                    | `cash_order_qty` is scaled by `priceScale` only.                                                                              |
| Money on the wire | Settlement prices come out as zero or `Infinity`                     | De-scaling `settlementPx` with `settlementPriceScale`                                                | `settlementPriceScale` is reserved and reads 0. De-scale with `priceScale`.                                                   |
| Money on the wire | Large commissions lose their last digits in a JS client              | `int64` parsed as a `Number`                                                                         | Parse `int64` JSON strings as `BigInt` or `Decimal`.                                                                          |
| Money on the wire | A market prices as if free, or as if 100×                            | An absent scale defaulted to 1 or to 100 by your code                                                | proto3 cannot distinguish absent from zero. Treat missing or zero scales as a hard error.                                     |
| Money on the wire | RFQ combo prices are 1000× off                                       | Assuming a combo carries the same scales as the underlying, or expecting scales on the RFQ stream    | `caoc` is fixed at `priceScale` 1000, `qtyScale` 100, tick \$0.001. The RFQ stream carries no scales.                         |

<Snippet file="support.mdx" />

## Next

[Place an order](/orders)
