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

# API changelog

> Read API, SDK, CLI, MCP, OAuth, and feed changes, with the action each update requires.

Read updates from newest to oldest. Each entry explains what changed, whether existing clients remain compatible, and what you need to change. Documentation-only edits are not listed.

Subscribe by RSS at [docs.0xinsider.com/changelog/rss.xml](https://docs.0xinsider.com/changelog/rss.xml).

<Update label="October 7, 2026" description="Pick of the Day automatic picks publish from 3:00 AM ET; retry_at names that opening">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now publishes automatic picks from 3:00 AM to 11:30 PM US Eastern time (ET). A pick that qualifies before 3:00 AM ET publishes at 3:00 AM ET, so the earliest kickoff a pick can cover is 3:30 AM ET.

  Before this change, automatic publication opened at midnight ET. Before a day's first pick, `error.retry_at` could name the next selector attempt, about every 15 minutes from midnight.

  * Before 3:00 AM ET, and after a skipped day, the `404` `pick_not_released` response's `error.retry_at` and `Retry-After` name the next 3:00 AM ET opening. That is 07:00 UTC under daylight saving time and 08:00 UTC otherwise.
  * An automatic pick's `published_at` is never earlier than 3:00 AM ET on its `pick_date`.
  * The response fields, `release_at`, and the `404` body shape are unchanged.

  **Backward compatible.**

  **What to change:** Nothing. If you poll, keep scheduling the next read from `error.retry_at` or `Retry-After`.
</Update>

<Update label="October 6, 2026" description="Pick of the Day automatic picks now publish 30 to 45 minutes before kickoff">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now publishes each automatic pick 30 to 45 minutes before its own kickoff. Picks still arrive throughout the day, up to 15.

  Before this change, an automatic pick published as soon as it qualified, which could be hours before its game.

  * The response fields, `release_at`, and the `404` `pick_not_released` response are unchanged.
  * An explicitly scheduled pick still waits for its returned `release_at`.

  **Backward compatible.**

  **What to change:** Nothing. If you poll, keep scheduling the next read from `error.retry_at` or `Retry-After`.
</Update>

<Update label="October 6, 2026" description="Pick of the Day pick_rank can reach 20 after a cancelled pick; picks stay at 15">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now return a `pick_rank` above 15 on a day with a withdrawn pick. A pick withdrawn because its game was cancelled keeps its rank but no longer uses one of the day's 15 picks, so the next pick takes the next free rank, up to 20.

  Before this change a cancelled pick held one of the 15 slots, so that day ended with 14 picks.

  * `pick_rank` now runs from 1 to 20 in `picks`, `scheduled_picks`, `locked_picks`, `proof_pending_picks`, the archive, and the ledger, and the OpenAPI document and the Pick of the Day MCP tool's output schema say so.
  * `pick_count` is still at most 15, and a day still carries at most 15 picks.
  * `pick_rank` is deprecated. Use `pick_id` for identity and `publication_order` for order.

  **Backward compatible** for clients that read `picks` as a list.

  **What to change:** If you validate `pick_rank` against a maximum of 15, or index a fixed 15-slot array by rank, raise the bound to 20 or key by `pick_id`.
</Update>

<Update label="October 6, 2026" description="Live-score source_revision values jump to millisecond scale and stay increasing">
  [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot) now sends `sports.live_score.source_revision` and `sports.live_score.tennis_points.source_revision` as 13-digit integers, about 1,790,000,000,000 today. Each new revision is still greater than every earlier one for the same group, including after a restart of our cache.

  Before this change both values were small counters, about 2,000,000 and 50,000. A cache restart could move them backwards for up to 15 minutes, and a client that kept only strictly newer revisions froze the live score for that time.

  * The values are still integers that order updates. They are not timestamps; do not read them as a time.
  * Every value fits in a JavaScript safe integer and in a signed 64-bit integer. It does not fit in a 32-bit integer.
  * The first update after the change jumps from the small counter to the large value. A "newer than" check accepts it.

  **Backward compatible** for clients that compare revisions or store them as 64-bit integers or JavaScript numbers.

  **What to change:** If you store `source_revision` in a 32-bit integer column or variable, widen it to 64 bits. Nothing else.
</Update>

<Update label="October 6, 2026" description="MCP 2.14.3 cancels HTTP work and bounds documentation resource reads">
  `@0xinsider/mcp` 2.14.3 now forwards MCP request cancellation to every tool and resource HTTP read. Documentation resources also have a 15-second deadline covering headers and the complete response body.

  Before this change, cancelled API reads continued until their existing deadline, and documentation reads had no deadline. Caller cancellation now has the local error code `request_cancelled`; the server's own API deadline retains `request_timeout`.

  Cancelling a request stops the local HTTP read. It does not roll back a POST that already committed or guarantee that the remote server stopped its work. Tool names, arguments, results, and automatic retry behavior are unchanged.

  **Backward compatible.**

  **What to change:** Upgrade the local package to 2.14.3 once it is published. No MCP configuration changes are required.
</Update>

<Update label="October 6, 2026" description="TypeScript SDK 0.18.0 types empty picks and withheld identity fields">
  The generated declarations in `@0xinsider/sdk` 0.18.0 now describe the current API contract. API response bodies and SDK request behavior are unchanged by this release.

  Before this change, the published SDK declarations omitted these response cases:

  * `getPickOfTheDay` can return `state: "none"` with no entitled picks. Narrow on `state` before reading fields from a full pick.
  * Archive and uncommitted-ledger entries can omit `matchup` and `category`. Check whether each field is present before rendering it.
  * Sealed ledger entries withhold `kickoff`. Read the recorded kickoff from an opened entry when it becomes available.
  * `entry_authorization.policy_version` can be `7` or `8`. Update exhaustive version handling while retaining your authorization checks.
  * `MarketSnapshot.sports.live_score.tennis_points`, when present, follows the named `TennisPoints` shape. Custom response objects must use that shape.

  The SDK also exports `PickOfTheDayNoEntitledPicks`, `TennisPoints`, and `TennisRanking` by name.

  **Breaking** for TypeScript clients that depend on the previous narrower declarations.

  **What to change:** Update the affected narrowing, presence checks, and response types before upgrading to 0.18.0. Existing runtime requests remain valid.
</Update>

<Update label="October 6, 2026" description="TypeScript SDK optional fields explicitly permit existing undefined values">
  The TypeScript SDK, `@0xinsider/sdk`, now explicitly permits `undefined` in optional paging progress, parsed stream-frame, and data freshness fields that already return it. This makes the declarations usable with TypeScript's `exactOptionalPropertyTypes` option.

  Before this change, those declarations used optional properties without explicitly describing their existing `undefined` values. Callers with the stricter compiler option could therefore reject otherwise valid SDK values.

  Return values and API response bodies retain their existing shapes. The SDK also checks indexed access and control flow during development; missing optional request fields are omitted before calling Fetch.

  **Backward compatible.** This adds accurate type information without changing valid requests or returned data.

  **What to change:** Nothing. Enable `exactOptionalPropertyTypes` in your own project when you are ready to check absent properties separately from explicit `undefined`.
</Update>

<Update label="October 6, 2026" description="Referral-only paid API and feed access ends when the earned grant ends">
  Paid operations in the [REST API](/api-reference/introduction), MCP, and the [live event feed](/api-reference/endpoint/get-stream) now require an unexpired referral grant when referral rewards are the account's only source of Pro access. A refund removes the unearned portion of that grant. Independently paid subscriptions and separately authorized complimentary access retain their access.

  Before this change, an expired referral grant could leave a never-paying account authorized for Pro indefinitely. Already awarded referral access could also survive a refund of the qualifying payment.

  The response schema and authentication headers are unchanged. Once temporary paid access ends, paid API operations return the existing subscription-required refusal, and an open paid feed disconnects on its normal authorization recheck.

  **Breaking** for clients that depended on referral access continuing beyond its earned period.

  **What to change:** Handle the subscription-required response by prompting the account owner to subscribe. Reconnect paid feeds only after the account has valid paid access.
</Update>

<Update label="October 6, 2026" description="Soccer games identify home, draw, and away markets">
  [`GET /api/v1/games`](/api-reference/endpoint/list-games) and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) now identify each soccer result market and report when the game offers a draw.

  For a soccer game with home-win, draw, and away-win markets:

  * `markets[].side` identifies each market as `home`, `draw`, or `away`.
  * `draw_offered` is `true` when the game offers a draw market.

  Before this change, these soccer markets omitted `side`, and the game reported `draw_offered: false` even when a draw market was present. The SDK's `listGames` and `getGame` methods and MCP's `list_games` and `get_game` tools return the same corrected fields.

  **Backward compatible.** Existing optional fields now carry the classified values; no field or type migration is required.

  **What to change:** Nothing. Use `markets[].side` and `draw_offered` to identify result markets instead of guessing from their labels.
</Update>

<Update label="October 6, 2026" description="Pick of the Day archive excludes withdrawn picks and their statistics">
  [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) now excludes withdrawn recommendations from `picks`, `days`, `hit_rate`, and the cumulative series. A later market settlement does not return a withdrawn pick to the archive.

  Before this change, a withdrawn released pick remained in the archive and could contribute to its statistics after settlement. Withdrawn picks are also excluded from current and dated recommendations.

  The [public ledger](/api-reference/endpoint/get-pick-of-the-day-ledger) keeps every published commitment under its original `pick_id`. Its identity and proof bytes remain unchanged, and market settlement still determines the recorded result. Request and response fields are unchanged.

  **Breaking** for clients that use the archive as a complete list of published commitments.

  **What to change:** Refresh cached archive responses and use the public ledger when you need every published commitment. No field or type migration is required.
</Update>

<Update label="October 6, 2026" description="Pick of the Day excludes cancelled games from active recommendations">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now excludes a pending pick from active recommendations when its game is explicitly cancelled. New selections also check the provider's game status before publication.

  Before this change, a cancelled game could be selected or remain in the current day's recommendations.

  The [archive](/api-reference/endpoint/get-pick-of-the-day-archive) and [ledger](/api-reference/endpoint/get-pick-of-the-day-ledger) retain the released pick's identity and publication record. Market settlement still determines its recorded result.

  **Backward compatible.** Request and response fields are unchanged. The endpoint can return fewer picks or the existing `404` with `error.reason: "pick_not_released"` when no active pick remains.

  **What to change:** Nothing. Keep handling an empty day with `Retry-After` or `error.retry_at`, and store each `pick_id` rather than assuming a fixed pick count.
</Update>

<Update label="October 5, 2026" description="Pick of the Day game state covers totals and spread selections">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now return the existing `game_ended` field for totals and spread picks when the provider reports the verified game's state under a companion event.

  Before this change, the field could be absent because the selected market and its game used different provider events. A game's state is now read across its verified event family; an unavailable or stale scoreboard still leaves `game_ended` absent, which means unknown.

  **Backward compatible.** No public V1 field is added. `game_live` and `game_status` remain first-party fields and are not part of the public V1 response.

  **What to change:** Nothing. Keep treating an absent `game_ended` as unknown, and do not infer game completion from `game_started` or market settlement.
</Update>

<Update label="October 5, 2026" description="Released Pick of the Day records are permanent">
  With this release, the [Pick of the Day archive](/api-reference/endpoint/get-pick-of-the-day-archive) and [public ledger](/api-reference/endpoint/get-pick-of-the-day-ledger) keep every released pick on its original pick ID, product date and publication order through settlement and history.

  This strengthens the record guarantee: a released selection cannot be withdrawn, replaced, renumbered or moved to another product day. Its selected market and side stay recorded. Market settlement and provider corrections can still update its result.

  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) continues to describe the current product day. Use the archive or ledger for historical records.

  **Backward compatible.** Request and response formats and field shapes are unchanged. Existing access rules and proof verification still apply.

  **What to change:** Nothing. Existing clients need no migration.
</Update>

<Update label="October 5, 2026" description="Pick of the Day can include additional verified selections">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now include additional verified selections.

  Before this change, fewer selections could qualify for publication. A day can still have no published picks.

  **Backward compatible.** Request and response fields are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="October 5, 2026" description="Pick of the Day can include more S-grade-backed selections">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and the private scanner feed can now include more qualifying selections backed by S-grade wallets.

  Before this change, fewer S-grade-backed selections could qualify. Daily picks still require their remaining publication checks; 15 is the daily maximum, not a promised count.

  **Backward compatible.** Request and response fields are unchanged. Already selected and historical picks keep their recorded rules and frozen evidence.

  **What to change:** Nothing. Continue using returned picks and availability rather than assuming 15 picks will publish every day.
</Update>

<Update label="October 5, 2026" description="Tennis scores add available ATP and WTA singles rankings">
  [Market snapshots](/api-reference/endpoint/get-market-snapshot), `LiveScoreChanged` frames in the [live stream](/api-reference/endpoint/get-stream), and browser feeds now include optional `live_score.scores[].ranking` for tennis players. The [Markdown market context](/api-reference/endpoint/get-market-context-markdown) includes the same field.

  Before this change, these score entries could carry a player profile without a tour ranking.

  * `rank` is the positive singles ranking, and `tour` is `atp` or `wta`.
  * `source: "api_tennis"` identifies API-Tennis as the ranking source.
  * `observed_at` is when the ranking was retrieved, and `expires_at` is the UTC deadline after which you must hide it, including in cached responses and replayed frames.

  This is the latest retrieved ranking, not the player's ranking at match time. The field is absent when a ranking is unavailable, expired, or ambiguous; a player profile alone does not establish a rank.

  **Backward compatible.** This field is additive. Existing score fields and the lean [Games](/api-reference/endpoint/list-games) response are unchanged.

  **What to change:** Nothing for existing clients. To display a ranking, read the optional field and hide it at `expires_at`; keep an absent ranking unavailable rather than substituting zero.
</Update>

<Update label="October 5, 2026" description="Pick of the Day total labels include the exact Over/Under threshold">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and the [archive](/api-reference/endpoint/get-pick-of-the-day-archive) can return total selections with an exact `pick_outcome_label`, such as `Over 6.5` or `Under 5.5`. The label keeps the threshold recorded for that selection.

  Before this change, some total markets could not appear because their selection labels were incomplete. Other selection labels retain their existing format.

  **Backward compatible.** Response fields are unchanged.

  **What to change:** Nothing. Render the returned label as supplied instead of reconstructing a total from the market title.
</Update>

<Update label="October 5, 2026" description="Pick of the Day publishes automatic selections as soon as they qualify">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) returns automatic selections after qualification and final checks during the daily release window, from midnight to 23:30 US Eastern time. An explicitly scheduled selection keeps its returned `release_at`.

  Before this change, automatic selections waited until 30 minutes before kickoff, and the release window opened at 07:00 UTC.

  **Backward compatible.** Response fields and error formats are unchanged. Published prices, lead facts, and public holder records retain their publication basis.

  **What to change:** Nothing if you already use returned clocks. If you calculate kickoff minus 30 minutes, use `release_at`, `Retry-After`, or `error.retry_at` to schedule the next read instead; a day can still have no published picks.
</Update>

<Update label="October 5, 2026" description="Pick of the Day adds the lead wallet's profitable-event count">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now includes `lead_backer.profitable_event_count`, the number of events with positive realized P\&L in the lead wallet's recorded sport history. It uses the same sample as `directional_event_count`, `realized_pnl_usd`, and `roi`; break-even events do not count as profitable.

  Before this change, `lead_backer` returned the total event count, profit, and ROI without a profitable-event count.

  **Backward compatible.** This is an additive integer field; existing fields keep their meanings.

  **What to change:** Nothing. To display the event record, show `profitable_event_count` out of `directional_event_count` with `category`. Treat a missing count from an older server response as unavailable, not zero, and do not substitute a live wallet win rate.
</Update>

<Update label="October 5, 2026" description="Pick of the Day can return qualifying picks from previously omitted markets">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now include a qualifying pick from a market it previously omitted.

  Before this change, some qualifying markets could be omitted from the day's picks. A day can still have no published picks.

  **Backward compatible.** Response fields and error formats are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="October 4, 2026" description="Pick of the Day display_holders lists every graded holder on certified picks">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now returns the full publication roster in `display_holders` on a pick that carries `lead_backer`: the lead first, then any verified supporters, then the other S, A and B wallets that held the backed side when the pick published, ordered by shares. The OpenAPI document and the TypeScript client describe the same roster.

  Before this change, `display_holders` on these picks held only the lead and verified supporters, so a certified pick listed one wallet.

  * Only the lead and supporters are verified. The other rows are gross holdings on the backed side and may also hold the other side.
  * `holders`, `holder_count`, `smart_wallet_count`, `sharp_wallet_count`, `traders`, `side_summary` and `backed_sharp_usd` are unchanged and still cover only the certified wallets.
  * Each row carries the same fields as before, including `entry_value_usd` and the serve-time category win rate.

  **Backward compatible.** No field is added, removed or retyped; `display_holders` can carry more entries.

  **What to change:** Nothing, unless you treated every `display_holders` entry as verified support. Match `lead_backer.address` to find the lead, and read the counts above for the certified wallets.
</Update>

<Update label="October 4, 2026" description="Paid profiles can load pUSD wallet observations from Goldsky">
  The first-party session endpoint [`GET /api/trader/{address}/funding/supplemental`](https://github.com/0xinsider/0xinsider/pull/21208) adds manually requested pUSD balances and recent transfers for Pro and Max accounts. It resolves the profile's stored wallet address; it does not combine owners or proxies.

  Use `POST /api/trader/{address}/funding/supplemental/refresh` to request a refresh; GET only reads cached data. These session endpoints are separate from the public `/api/v1` API and existing confirmed funding. Before this change, these session endpoints and the balance diagnostic were unavailable.

  * Balances and amounts are raw decimal integer strings with six decimals. A missing balance remains unavailable rather than becoming zero. `fetched_at` records retrieval; `last_changed_at` records the last balance change.
  * History covers a fixed window of up to 30 days, capped at four pages of 25 transfers, including settlement and redemption. `through_block` is the finalized upper block used for the scan, not a guarantee of an atomic provider snapshot. Identical transfer rows can be legitimate; no unique event identifier is supplied.
  * `next_cursor` reads an immutable stored snapshot for up to one hour; preserve it unchanged. An expired or invalid cursor returns `400`; restart without a cursor. A refresh request can return `refresh_status: "pending"`; use bounded retries and honor the supplied retry time, then stop polling once observations load.
  * Stale successes and source failures remain visible independently. These observations do not define verified funding, spendable trading cash, wallet grades or P\&L.

  Operators can invoke the read-only balance diagnostic by setting `GOLDSKY_WALLET_PARITY_ADDRESS` before starting the backend. It samples at most four Goldsky conditional-token balance rows and compares full raw integers with Polygon at each supported row's finalized block.

  The `match_at_row_block` verdict confirms only the historical row block, not current freshness. The JSON receipt reports unsupported, unavailable and incomplete coverage explicitly; it performs no reconciliation or database repair.

  **Backward compatible.** Existing endpoints, analytics and canonical provider facts are unchanged.

  **What to change:** Nothing for existing clients. New session clients should load explicitly, display source and retrieval time, preserve cursors, and distinguish unavailable balances from zero.
</Update>

<Update label="October 4, 2026" description="Pick of the Day adds available ATP and WTA singles rankings">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now includes an optional `ranking` on tennis participants in `sports_context`. Before this change, participant objects had no tour ranking.

  * `rank` is the positive singles ranking, and `tour` identifies `atp` or `wta`.
  * `source: "api_tennis"` identifies API-Tennis as the data source.
  * `observed_at` records when the ranking was retrieved, and `expires_at` is the UTC deadline after which you must hide it, even in a cached response. Both are UTC timestamps.

  API-Tennis does not supply a ranking publication date, so `observed_at` is not that date. This is the latest retrieved ranking, not the player's ranking at match time. `ranking` is absent when a ranking is unavailable or expired, or the participant is unranked, ambiguous, or a doubles entry.

  **Backward compatible.** This field is additive.

  **What to change:** Nothing. Read `ranking` to display an available tour ranking.
</Update>

<Update label="October 4, 2026" description="Pick of the Day retains verified final scores after the live score expires">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now retains a settled pick's verified final score in its existing `sports_context` fields after the live score expires. Read each participant's `score`, or `sets_won` and `sets` for tennis.

  Before this change, a settled pick could lose its score when the live score expired or the provider stopped returning it. The pick's recorded outcome remained available independently.

  Scores remain unavailable when no final result has been verified for that game. A settled market alone does not establish the sporting final.

  **Backward compatible.** Response fields and score formats are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="October 4, 2026" description="Large trade detail shows counterparties once a pending receipt is processed">
  [`GET /api/v1/large-trades/{id}`](/api-reference/endpoint/get-large-trade) now returns a trade's counterparties on the first request after its Polygon receipt is processed, under a new `counterparty_analysis.snapshot_id`. The deprecated alias `GET /api/v1/whale-trades/{id}` and the website's trade page behave the same way.

  Before this change, a trade first requested before its receipt was processed returned `counterparty_analysis.status: "unavailable"` with no executions on every request until the next UTC day. A client that reads each new trade as soon as it appears always hit this case.

  * Each `snapshot_id` still names a fixed set of executions that lives 24 hours. The earlier snapshot does not change when a later request names a new one.
  * Execution and maker cursors stay bound to the `snapshot_id` and `analysis_id` they came from, so a client paging the earlier snapshot keeps reading the same set.

  **Backward compatible.** Response fields and paging rules are unchanged.

  **What to change:** Nothing. To get the counterparties of a trade that returned none, request its detail again and page with the `snapshot_id` from that response.
</Update>

<Update label="October 3, 2026" description="Pick of the Day raises new entry allowances from 2 cents to 5 cents">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now issues `entry_authorization.policy_version: 8` with a maximum share price of the first fresh reference ask plus 5 cents, capped at 85 cents and rounded down to the market's price increment. The TypeScript client accepts both policy versions `7` and `8`.

  Before this change, new policy `7` grants allowed 2 cents above that reference ask. Existing grants retain their recorded ceiling and expiry; this change does not rewrite them.

  * Read `entry_authorization.max_entry_price` as the API's price ceiling, not a promised fill or a profit estimate.
  * The [auto-buy trader](/guides/auto-buy-the-pick#how-slippage-and-fill-prices-work) still enforces its independent default `MAX_SLIPPAGE_PCT=3` against the published pick price. The lowest price ceiling wins.
  * The trader checks an executable quote for the requested stake, then submits a Fill-and-Kill BUY limited to that quote rounded down to the market's price increment. Actual fills can be lower or partial; fees are additional.

  **Breaking** for clients that accept only entry policy `7`. Response fields are unchanged, and existing grants keep their values. Clients that already accept returned policy values need no change.

  **What to change:** Honor the returned `max_entry_price` and expiry instead of calculating a 2-cent limit yourself. Update any policy-version validation to accept `7` and `8`; keep any stricter local price and slippage limits. `potd-trader` 0.3.1 and later already accept the new policy value, so this API change does not require a trader upgrade.
</Update>

<Update label="October 3, 2026" description="Pick of the Day lead_backer adds the net position of a two-sided lead">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now adds optional `lead_backer.net_position_usd` when the lead wallet also held the other outcome of the pick's market at publication. The TypeScript client and the Pick of the Day MCP tool schemas describe the same field.

  Before this change, `lead_backer` gave only `position_usd`, the gross value on the backed outcome, with no sign that the wallet also held the other side.

  * `net_position_usd` is the backed-outcome shares minus the other outcome's shares, valued at the backed outcome's provider price at publication, in USD.
  * The field is omitted for a one-sided lead, whose net equals `position_usd`. It is never `null`.
  * `position_usd` keeps its meaning: the gross provider-reported value on the backed outcome.

  **Backward compatible.** The field is additive and optional.

  **What to change:** Nothing. To show how much of a two-sided lead's position leans toward the pick, read `net_position_usd` when present and fall back to `position_usd` when it is absent.
</Update>

<Update label="October 3, 2026" description="Team-logo images refuse query parameters that do not match the published URL">
  With this change, team-logo images returned by [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and documented by the TypeScript SDK validate the query before immutable caching. Published URLs use no query string, an empty query string, or exactly one `v` parameter with the published content hash. Canonical team-logo URLs and API response schemas remain unchanged.

  Before this change, the image route ignored query strings and could serve the same crest with immutable caching under different query strings.

  The API origin refuses extra parameters, duplicate parameters, malformed parameters, and nonmatching versions without immutable caching. The website also checks query shape before later parameter normalization, although a fixed framework suffix can normalize before that check. Those aliases are not published URLs; this change does not promise how malformed HTTP request targets or existing cache entries behave.

  **Breaking** for clients that add query parameters or replace `v` on a team-logo URL.

  **What to change:** Treat each returned logo URL as opaque and preserve it, including `v`. Resolve a relative URL against the API origin without changing its query string.
</Update>

<Update label="October 3, 2026" description="MCP tool results return compact JSON text">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now returns `result.content[0].text` of a successful `tools/call` as compact JSON, with no indentation or line breaks. The [sandbox](https://0xinsider.com/sandbox) endpoint `POST /sandbox/api/v1/mcp` does the same, and so does the local stdio server in `@0xinsider/mcp` from version 2.14.1.

  Before this change, the text was indented JSON, 1.3 to 1.44 times the bytes of the same JSON written compactly on the documented examples. `result.structuredContent`, every field and value, `result.isError`, and the text of a failed call are unchanged.

  **Backward compatible.** The text parses to the same JSON as before.

  **What to change:** Nothing if you parse the text as JSON or read `result.structuredContent`. If you matched the text's line breaks or indentation, parse it as JSON instead.
</Update>

<Update label="October 3, 2026" description="Sandbox MCP discovery describes the current Pick of the Day access rules">
  [`POST /sandbox/api/v1/mcp`](https://0xinsider.com/sandbox) now returns current `description` and `outputSchema` metadata for the daily pick, pick ledger entry, and pick archive tools in `tools/list`.

  Before this change, the sandbox retained older pick descriptions and schemas after production added Max access and fields describing locked selections. The sandbox now reflects the published production contract; live MCP behavior and tool arguments are unchanged.

  **Backward compatible.** This corrects sandbox discovery metadata.

  **What to change:** Refresh any locally cached sandbox tool catalog.
</Update>

<Update label="October 3, 2026" description="Pick of the Day requires consistent lead history from before settlement">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now requires newly certified lead wallets to have consistent provider-reported history and acquisitions proven before the sampled events settled.

  Before this change, an acquisition after settlement or a position moving between the provider's open and closed lists could make a wallet appear to qualify. Uncertain history now prevents that wallet from certifying a new pick. These checks do not prove every historical exposure or an atomic provider snapshot.

  **Backward compatible.** Response fields and account access are unchanged. Previously published picks retain their recorded history.

  **What to change:** Nothing.
</Update>

<Update label="October 2, 2026" description="Pick of the Day releases 30 minutes before kickoff instead of 60">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now schedules automatic picks for 30 minutes before kickoff. The latest release is 23:30 America/New\_York; the window still opens at 07:00 UTC.

  Before this change, automatic picks were scheduled 60 minutes before kickoff, and the release window ended at 23:00 America/New\_York. Explicit overrides remain authoritative, and actual publication can trail the scheduled instant.

  **Backward compatible.** Response fields, account access, and pick limits are unchanged. The TypeScript client and MCP tool retain the same response shapes.

  **What to change:** Schedule reads from returned `release_at`, `error.retry_at`, or `Retry-After`. Remove any fixed 60-minute calculation, and do not poll before the returned time.
</Update>

<Update label="October 2, 2026" description="Pick of the Day adds recorded lead wallet positions and directional category history">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) adds optional full-only `lead_backer` facts for newly certified picks, also described by the TypeScript client and Pick of the Day MCP tool.

  Before this change, full picks could carry a recorded category specialist and holder roster, but no separate lead position clock or directional realized-history record. Legacy picks retain those fields and omit the new object.

  * `address`, `name`, `grade`, and `category` identify the recorded lead wallet.
  * `position_usd` is its provider-reported backed-outcome value at publication, with the start of the provider position fetch in `position_observed_at`. This conservative observation clock precedes completion. The value is not entry cost or a live balance.
  * `directional_event_count`, `realized_pnl_usd`, `entry_basis_usd`, fractional `roi`, and `recorded_at` describe the recorded directional category sample. Entry basis does not assert complete costs or fees.
  * `max_realized_drawdown_usd` measures realized P\&L declines after each event result in that sample, excluding intragame, unrealized, and account equity drawdown.
  * Newly certified `display_holders` contains only the recorded lead and verified supporters. `holders` retains its bounded S/A projection; `backed_sharp_usd` sums only certified positions, gross flow and consensus fields are omitted, and `lead_backer` replaces the legacy specialist display.

  **Backward compatible.** The new object is optional, account access is unchanged, and legacy picks retain their recorded semantics.

  **What to change:** Render `lead_backer` only when present, label its position and history clocks separately, and use the returned roster rather than reconstructing support. An omitted value is unavailable, and the wallet record is not the pick's win probability.
</Update>

<Update label="October 2, 2026" description="Market snapshots and score frames retain unexpired tennis points during detail outages">
  [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot) retains compatible, unexpired `sports.live_score.tennis_points` when the supplemental snapshot is temporarily unavailable or cannot be read because another refresh holds it. `LiveScoreChanged` frames on [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) retain the same `live_score.tennis_points` detail.

  Before this change, a failed detail refresh could remove valid current-game points. Points still disappear at expiry or when the player identity, set, game, or live state no longer matches. Their revision remains independent of the enclosing Polymarket score revision.

  SDK `getMarketSnapshot` and MCP `get_market_snapshot` inherit the snapshot behavior.

  **Backward compatible.** No response fields were added or retyped.

  **What to change:** Nothing. Keep checking `expires_at`, `set_number`, and `games` before displaying the point group.
</Update>

<Update label="October 2, 2026" description="Market snapshots retain live esports map scores through incomplete updates">
  [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot), SDK `getMarketSnapshot`, and MCP `get_market_snapshot` preserve `sports.live_score.scores[].map_score` for the same live map when a newer score update omits both sides.

  Before this change, an incomplete update removed available map detail. A present canonical pair still replaces the retained values, and a map, series score, or live-state change clears them. Series totals keep their existing meaning.

  **Backward compatible.** No response fields were added.

  **What to change:** Nothing.
</Update>

<Update label="October 2, 2026" description="Pick stats leave z-scores unavailable when contributing selections share a game">
  [`GET /api/pick-of-the-day/stats`](https://0xinsider.com/api/pick-of-the-day/stats) now returns `null` for `market_expectations.windows[].wins_z`, `units_z`, and their display strings when multiple priced, decided selections share a canonical game. The affected window also returns `numeric_unavailable: true`.

  Before this change, the comparison used an independent-outcome variance formula even when selections shared game exposure. Counts, `expected_wins`, recorded `units`, and CLV measurements retain their existing calculations. Pending, void, and unpriced selections do not trigger this guard.

  **Backward compatible.** These fields were already nullable; the response shape is unchanged.

  **What to change:** Treat a `null` z-score as unavailable, never as zero, and keep reading the other measured values. Same-game selections need not be independent.
</Update>

<Update label="October 2, 2026" description="Pick of the Day can return two compatible selections from one game">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now return up to 2 verified compatible selections from one game, within the existing 15-pick daily ceiling. The supported pair combines a team's full-game moneyline and handicap selections; each selection still qualifies individually.

  Before this change, the automatic slate allowed at most 1 selection per game. Same-game picks share exposure and need not be independent. Response fields, account allowances, and historical picks remain unchanged.

  **Breaking** for clients that assume a game can have only 1 pick.

  **What to change:** Retain each `pick_id` instead of deduplicating by game, and treat selections from the same game as related exposure. Keep honoring each selection's existing entry authorization and expiry.
</Update>

<Update label="October 2, 2026" description="Full sports board and rail feeds retain verified moneyline kickoff times">
  [`GET /api/activity/sports-board`](https://api.0xinsider.com/api/activity/sports-board?shape=v2) and [`GET /api/activity/sports-board/rail`](https://api.0xinsider.com/api/activity/sports-board/rail) now retain verified Polymarket kickoff in the optional `game_start_time` field for upcoming single-moneyline games. The full `rail` hub feed retains the same field.

  Before this change, these games omitted `game_start_time` even when Polymarket supplied a valid kickoff.

  * `game_start_time` is UTC, parsed from the moneyline market's `gameStartTime`. Missing or malformed provider values leave it omitted.
  * `event_date` keeps its existing fallbacks and is not proof of kickoff.
  * Compact rail and `rows=board` responses continue to omit this field.
  * [`GET /api/v1/games`](/api-reference/endpoint/list-games) and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) keep the same `scheduled_at` values and response fields.

  A moneyline game's consensus response also withholds pregame recommendations after its verified kickoff passes, even if the provider still lists it as scheduled.

  **Backward compatible.** An existing optional field gains coverage; no field becomes required.

  **What to change:** Nothing. Use `game_start_time` when you need verified kickoff, and keep an omitted value unknown.
</Update>

<Update label="October 2, 2026" description="Pick of the Day covers more markets within the existing 15-pick daily ceiling">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now return qualifying selections from a wider range of markets.

  Before this change, some markets were outside the selection coverage. The daily ceiling remains 15, with at most 1 selection per game. A day with fewer qualifying selections still returns fewer picks.

  **Backward compatible.** Response fields, account allowances, and rank and count limits remain unchanged.

  **What to change:** Nothing. Read the returned pick counts; the daily ceiling does not guarantee 15 picks.
</Update>

<Update label="October 2, 2026" description="Game and market score clocks include cached Polymarket event observations">
  [`GET /api/v1/games`](/api-reference/endpoint/list-games) and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) now populate `freshness.scores_observed_at` for scores read from Polymarket's Gamma events API. [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot) carries the same clock in `sports.live_score.observed_at`, including the JSON embedded in [market context as Markdown](/api-reference/endpoint/get-market-context-markdown).

  Before this change, a score without a Sports WebSocket observation could omit these clocks, so clients could not date that score observation.

  The timestamp records when 0xinsider successfully read the provider event. Cached responses retain that timestamp; it is not the provider's score update time or an official score revision. `scores_source_at` remains absent when the provider supplies no source clock.

  **Backward compatible.** Existing optional fields gain observation coverage. No required field or response type changes.

  **What to change:** Nothing. Read these optional clocks to date score observations, and keep treating an absent clock as unknown.
</Update>

<Update label="October 1, 2026" description="potd-trader 0.3.6 enforces authenticated Pro and Max daily pick limits">
  [`potd-trader` 0.3.6](https://github.com/0xinsider/potd-trader/releases/tag/v0.3.6) verifies your Pro or Max allowance from the authenticated feed response and reserves at most 5 daily picks on Pro or 15 on Max. Pro includes the designated free selection, whose presentation slot can be greater than 5.

  Before this change, the trader relied on the filtered feed and spending cap without its own account pick limit. It also warned Pro accounts against a larger publication maximum.

  * `X-Monthly-Quota-Limit` must match the current included allowance: 500,000 for Pro or 2,000,000 for Max. Missing or unknown values stop trading. Pay-as-you-go ceilings do not determine the plan.
  * Accepted, submitting, and unknown reservations count toward the daily pick limit using the `America/New_York` date. The spending cap keeps its independent UTC date and is not raised.
  * A changed allowance, including on `304`, discards cached picks and requires a fresh read. Historical wire ranks through 20 remain readable.

  **Breaking** for feeds or proxies that remove the authenticated allowance header. Existing ledger files remain readable, and the API response contract does not change.

  **What to change:** Stop older watchers, install 0.3.6, and keep the same local configuration and ledger. Check `run --dry-run` before enabling live orders yourself. See the [setup and upgrade guide](/guides/auto-buy-the-pick).
</Update>

<Update label="October 1, 2026" description="Developers can create multiple independent live API keys">
  [Developers](https://0xinsider.com/developers) and `POST /api/keys` now create independent live API keys without invalidating your existing keys. Each new key carries all scopes and stays valid until revoked.

  Before this change, an account could have only 1 active default key, and another creation request returned `409`. Replacing it from Developers revoked the old key.

  * **Create key** adds a key; the page shows the full secret once.
  * **Revoke** and `DELETE /api/keys/{id}` stop only the selected key.
  * All keys and OAuth tokens still share the account's request quota and rate limits. Creating more keys adds no allowance.
  * The legacy `POST /api/keys/regenerate` still revokes every active default key and creates 1 replacement. Named integration keys and OAuth tokens are unaffected.

  **Backward compatible.** Existing keys keep working; the create endpoint now accepts additional keys.

  **What to change:** Use `POST /api/keys` to add a key. Use `POST /api/keys/regenerate` only when you intend to replace every active default key.
</Update>

<Update label="October 1, 2026" description="Daily pick OpenAPI descriptions match the 15-pick limit">
  The OpenAPI operation descriptions for [current picks](/api-reference/endpoint/get-pick-of-the-day) and the [pick archive](/api-reference/endpoint/get-pick-of-the-day-archive) now state that Max opens up to 15 available selections.

  Before this change, these two descriptions still said 20 after the daily limit and numeric schema maxima had returned to 15. Max features, Pro access to 5 picks, the private feed's independent 15-pick limit, and historical selections remain unchanged.

  **Backward compatible.** Only the two operation descriptions change; numeric bounds, response fields, and historical proof remain unchanged.

  **What to change:** Refresh cached OpenAPI documentation. Keep reading the returned pick counts; no day guarantees 15 picks.
</Update>

<Update label="October 1, 2026" description="Pick of the Day restores the 15-pick ceiling">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) limits new daily selections to 15. Current, scheduled, proof-pending, [archive](/api-reference/endpoint/get-pick-of-the-day-archive), and identity-free `locked_picks` schemas declare rank or count maxima of 15.

  Before this change, the Max release declared a daily ceiling of 20. Pro still opens 5 picks in total, including the free selection; Max still opens every available published pick and includes all Pro features. Existing selections keep their identities, order, and historical proof.

  **Breaking** for clients that assume a 20-pick daily capacity. Response fields and plan features remain unchanged.

  **What to change:** Allow up to 15 new daily selections and read the returned counts. Neither plan guarantees its full daily limit.
</Update>

<Update label="October 1, 2026" description="Daily pick schemas allow ranks and counts through 15">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) now documents `pick_rank` and `pick_count` ceilings of 15. Scheduled picks, proof-pending picks, and [archive entries](/api-reference/endpoint/get-pick-of-the-day-archive) also allow ranks through 15.

  Before this change, the app's API schema still declared a maximum of 10, although the backend allowed up to 15 daily picks. The ceiling is a limit, not a promise that every day has 15 picks.

  **Backward compatible.** The accepted range expands; no existing field is added, removed, or retyped.

  **What to change:** Regenerate schema-based clients if they enforce the old maximum of 10.
</Update>

<Update label="October 1, 2026" description="Sports team logos add official NBA, NHL, and MLB crests">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) fields `sports_context.yes_team.logo` and `sports_context.no_team.logo` now return official crests for 30 NBA, 32 NHL, and 30 MLB clubs alongside the existing 32 NFL and 15 WNBA clubs.

  Before this change, NBA, NHL, and MLB club logos used provider-hosted artwork. Novelty and national-team rows outside these club sets keep provider artwork. Vendored crests now also carry measured `logo_mark_dark` verdicts.

  **Backward compatible.** This is additive logo coverage; response fields and provider team IDs are unchanged.

  **What to change:** Resolve relative `logo` paths against the API origin and keep accepting provider-owned absolute URLs. Use a light plate when `logo_mark_dark` is `true`. No response schema change is required.
</Update>

<Update label="October 1, 2026" description="potd-trader 0.3.5 accepts Pro and Max slates through 20 ranks">
  [potd-trader](https://github.com/0xinsider/potd-trader/releases/tag/v0.3.5) `0.3.5` accepts pick and schedule ranks through 20 and reports identity-free `locked_picks` as upgrade information, never as trade candidates.

  Before this release, `0.3.4` rejected an entire slate when a rank exceeded 10. The patch keeps existing spending caps, the 25 pUSD settings default, the init suggestion of 10 stakes, and all order guards unchanged.

  **Backward compatible** with existing settings and ledgers. A configured budget can still prevent buying every entitled pick.

  **What to change:** Use the [updated install pin](/guides/auto-buy-the-pick#install). Stop every old watcher, preserve the same configuration and ledger, and check `status` and a dry run before enabling live mode. Do not use `0.3.4` as a Max-compatible consumer.
</Update>

<Update label="October 1, 2026" description="Pick access adds Max and withholds unresolved game identity">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) now supports up to 20 daily picks. Pro opens 5 daily picks in total, including the free selection; Max opens every available published pick up to 20. A day can publish fewer picks.

  Before this change, the daily limit was 15 and Pro opened all paid picks. Unauthorized pending rows could expose game identity, and public sealed ledger entries included kickoff.

  * `locked_picks` describes unauthorized unresolved published or scheduled ranks with only `pick_rank` and `required_tier: "max"`.
  * When only locked ranks have published, a successful response has `state: "none"`, `picks: []`, and `pick_count: 0`. This is not a proof-warming error.
  * Public API `scheduled_picks` retains only entitled rows with `release_at` and `kickoff`; unauthorized scheduled ranks move into `locked_picks`. The website keeps redacted scheduled descriptors without clocks. Unauthorized pending [archive rows](/api-reference/endpoint/get-pick-of-the-day-archive) omit game identity and identifying clocks; `required_tier` states the access needed.
  * Public [sealed ledger entries](/api-reference/endpoint/get-pick-of-the-day-ledger) omit `kickoff`. Pending uncommitted entries omit `matchup` and `category`. Opened proof payloads and historical commitments remain unchanged.
  * Regenerated SDK declarations describe optional archive identity and omitted sealed kickoff. Public schedule rows retain their clocks. This entry does not announce a new published SDK version.

  **Breaking** for clients that require unresolved game identity, sealed kickoff, or Pro access beyond its 5 entitled picks.

  **What to change:** Handle `state: "none"` and `locked_picks`, allow omitted clocks and identity fields, and preserve unknown values instead of enriching locked rows. Use updated SDK declarations when released, refresh cached MCP descriptors, and use Max credentials to open additional unresolved picks. Keep stable pick IDs and versioned proof verification, and upgrade consumers whose rank validators stop at 10 before using the 20-rank contract.
</Update>

<Update label="October 1, 2026" description="API monthly allowances increase for Pro and add Max">
  [API usage](/api-reference/endpoint/get-usage) now reports 500,000 included requests per UTC month on Pro and 2,000,000 on Max. Max inherits Pro API and MCP features; the 100-request and 2,500-batch-item minute limits remain unchanged.

  Before this change, Pro included 250,000 requests per month and its pay-as-you-go ceiling was 1,000,000. Pay as you go still costs \$0.20 per 1,000 additional requests, with a ceiling of up to 4 times the plan's included allowance: 2,000,000 on Pro or 8,000,000 on Max.

  The optional nullable `monthly_quota.unavailable_reason` is `usage_price_reconciliation_required` when billing must be reconciled before pay as you go can be used. In that state, `pay_as_you_go` is `false`; it does not indicate an ordinary opt-out.

  **Backward compatible.** Existing keys, scopes, and authentication flows remain available.

  **What to change:** Read `monthly_quota.limit`, `ceiling`, and `X-Monthly-Quota-*` rather than hardcoding a quota. Show a Billing or support action for reconciliation, and keep monthly-reset retries in your scheduler.
</Update>

<Update label="October 1, 2026" description="Pick of the Day can carry up to 15 selections per day instead of 10">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now carry up to 15 selections per day. The same ceiling applies to Pick of the Day MCP reads and scheduled selections. Each game can still contribute at most one selection, and days with fewer qualifying markets carry fewer picks.

  Before this change, the daily ceiling was 10. Existing selections keep their identities, order, and historical proof.

  **Backward compatible.** No fields or response shapes change.

  **What to change:** Allow 15 items if you enforce a fixed ten-item ceiling. Otherwise, nothing.
</Update>

<Update label="October 1, 2026" description="Live tennis scores add optional current-game points and the serving player">
  [Market snapshots](/api-reference/endpoint/get-market-snapshot) and `LiveScoreChanged` frames in the [live stream](/api-reference/endpoint/get-stream) and browser feeds now carry optional `live_score.tennis_points` from API-Tennis. The [Markdown market context](/api-reference/endpoint/get-market-context-markdown) includes the same optional group.

  Before this change, tennis scores carried Polymarket's set scores without current-game points or a serving player.

  * `points`, `player_keys`, and `games` follow the same first/second order as `live_score.scores`. `match_key` and `player_keys` are API-Tennis IDs, separate from Polymarket IDs.
  * `serving_side` is `first` or `second`. It is omitted when the provider does not identify the server. An absent point group means unavailable, never zero points.
  * The group carries its own `source_revision`, `observed_at`, `expires_at`, and `set_number`. Display it only while the match is live, its current set/games match the enclosing score, and it is before `expires_at`, 30 seconds after observation. Cached responses and replayed frames can retain expired facts.
  * Subscribe with `event=LiveScoreChanged` and no market, wallet, grade, or size filters. These frames identify their match by `event_slug` and resume through the stream's `Last-Event-ID` window. The durable [event replay](/api-reference/endpoint/get-event-replay-since) route still returns large-trade events only.

  Polymarket still supplies the enclosing score, set columns, period, status, and outer revision.

  **Backward compatible.** The new group is optional, and existing score fields retain their meaning.

  **What to change:** To display points and the serving player, read this group in scoreboard order, compare its revision only with the same match's point revision, and hide it at `expires_at` or a set/game mismatch. Keep missing facts unknown. Regenerate your client from the [OpenAPI document](https://0xinsider.com/api/v1/openapi.json) for the new types.
</Update>

<Update label="October 1, 2026" description="Private trader feed coverage metadata now matches its existing candidate policy">
  The private trader feed now reports coverage metadata that agrees with its existing candidate policy.

  Before this change, coverage metadata could disagree with which markets the feed could consider. Candidate selection, response fields, and the policy version remain unchanged.

  **Backward compatible.**

  **What to change:** Nothing.
</Update>

<Update label="October 1, 2026" description="Trader styles add 6 observable labels to profiles and leaderboard filters">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) with `expand=strategy`, [Batch traders](/api-reference/endpoint/batch-get-traders), and [Leaderboard](/api-reference/endpoint/get-leaderboard) now support 6 activity labels: `two_sided`, `category_focused`, `high_activity`, `diversified`, `mixed`, and `unclassified`. The leaderboard's `strategy` filter, SDK types, and MCP `get_leaderboard` input accept these values too.

  Before this change, the classifier chose the first matching archetype from 10 labels. Fill-price patterns and activity spans could imply scalping, automation, or market making, and negative performance could replace a behavior label with `speculator`.

  * New classifications compare two-sided positions, cost basis by category, and recorded fill counts over stored lifetime history. Profit and win rate no longer determine the style.
  * `mixed` means usable history has no dominant pattern. `unclassified` means limited usable history; a wallet without a stored classification can still omit `strategy`.
  * The 10 historical IDs remain readable and accepted as filters. They select their existing rows until normal reclassification updates them; they are not aliases for the new IDs.
  * Public `confidence` remains omitted. Internal rule scores describe support for a pattern, not calibrated probabilities.

  **Backward compatible.** The new IDs are additive, and existing filter values remain accepted.

  **What to change:** Add the new IDs to closed enums and exhaustive label switches, or regenerate your client from the [OpenAPI document](https://0xinsider.com/api/v1/openapi.json). Keep support for historical IDs, and update SDK or local MCP types before sending a new filter value. See [Wallet trading styles](/concepts/strategy-types) for the evidence rules and limits.
</Update>

<Update label="October 1, 2026" description="Tennis photos carry independent revisions and immutable image versions">
  Tennis competitors in [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and sports responses now carry optional `headshot_revision` beside `headshot`. A successful new resolution or a change to image bytes increases this integer independently of the sports `source_revision`. Legacy saved photos use `0`; the field is omitted when no photo resolves.

  Before this change, a resolved portrait was not refreshed from its source, and replacing stored bytes could change what an existing versioned URL returned.

  * A `headshot` URL with `?v=` keeps its original image bytes after a refresh. Unambiguous existing 12-character hexadecimal versions continue to resolve, and new versions use the full SHA-256 content hash.
  * An unversioned image URL revalidates, so it can return the current portrait.
  * A check that finds the same image or updates its attribution leaves the image revision unchanged.
  * Source refresh retains the last good photo when its source fails. Explicitly imported tour portraits remain pinned, and a refreshed photo does not promise a recent photography date.

  **Backward compatible.** `headshot_revision` is optional, and existing headshot URLs remain supported.

  **What to change:** Compare `headshot_revision` only for the same player; use matching, non-null (`tour`, `provider_id`) values as its identity. A greater image revision replaces the portrait, an equal revision can fill a missing URL, and a lower revision must not replace a newer portrait.

  Keep using `source_revision` to order score updates; a newer image revision does not make an older score current.
</Update>

<Update label="October 1, 2026" description="Webhook verification gains durable asynchronous attempts">
  [Start webhook verification](/api-reference/endpoint/create-webhook-verification-attempt) now returns `202` with an attempt UUID and status path. [Get webhook verification](/api-reference/endpoint/get-webhook-verification-attempt) reports its current state and sanitized outcome. SDK `0.17.0` adds `createWebhookVerificationAttempt` and `getWebhookVerificationAttempt`.

  Before this change, [Verify a webhook](/api-reference/endpoint/verify-webhook) waited for the receiver. That route keeps its `200` activation and `422` failure behavior. The new routes are additive.

  * Admission accepts the current one-time token and an optional `Idempotency-Key`.
  * Repeated admission for the same endpoint and token recovers the same attempt. A key replay returns the original response; poll status for changes.
  * A signed asynchronous challenge includes a stable attempt ID. Transient failures can retry up to 4 claimed challenges, within 15 minutes or the token's earlier expiry.
  * Only a successful challenge before expiry activates the unchanged endpoint. Terminal attempts do not restart.

  **Backward compatible.** Existing synchronous clients need no changes. **What to change:** Upgrade to SDK `0.17.0` and use admission plus polling when receiver latency should not delay your API response. Correlate repeated asynchronous challenges by `verification_attempt_id`.
</Update>

<Update label="October 1, 2026" description="Pick of the Day can draw from more sports and esports markets">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can draw published picks from more sports and esports markets. Before this change, some of these markets were excluded.

  Days with fewer qualifying picks can still return fewer than the daily limit.

  **Backward compatible.**

  **What to change:** Nothing.
</Update>

<Update label="October 1, 2026" description="Exports that pass their retention deadline now finish as failed">
  [Trader exports](/api-reference/endpoint/get-trader-export-status) and [large-trade datasets](/api-reference/endpoint/get-whale-dataset-status) now finish as `failed` if they do not become downloadable before `expires_at`. The terminal event is `export_job_failed`, with `failure_reason: "expired_before_completion"` and `next_action: "resubmit"`.

  Before this change, work could continue after the deadline and emit `export_job_ready` even though status and download already reported expiry. The retention window stays the same, including when an export starts shortly before the deadline.

  **Backward compatible.** The existing failure status and event describe this outcome; no fields or event types are added.

  **What to change:** Submit a new export after expiry. Always check the authorized status and download routes when you receive a ready event, because delivery can arrive after a previously ready export expires.
</Update>

<Update label="October 1, 2026" description="SDK stream openings and refusal bodies have finite bounds">
  `@0xinsider/sdk` 0.16.1 bounds SSE connection opening with `openTimeoutMs` (15,000 ms by default) and refusal bodies with `maxErrorBodyBytes` (65,536 bytes by default). Once a readable `text/event-stream` response is established, the opening deadline is cleared and your cancellation signal remains active for the stream's full lifetime.

  Before this change, an opening or stalled refusal body could wait indefinitely, and a large refusal was buffered to its end. The new options are additive and separate from the client's REST `timeoutMs`.

  * A header deadline throws `RequestTimeoutError` for `getStream`; resilient consumers use their existing bounded reconnect policy.
  * Incomplete refusal bodies retain HTTP status, `Retry-After`, and `X-Request-ID`. `StreamRefusalBodyError` on the HTTP error's `cause` distinguishes an absent, empty, invalid JSON, oversized, stalled, or unreadable body without copying raw text.
  * Invalid stream and reconnect limits fail before a request or reconnect callback. Cursor, checkpoint, and healthy stream lifetime behavior are unchanged.

  **Breaking** for consumers that intentionally allow an opening longer than 15 seconds or need JSON refusals larger than 65,536 bytes.

  **What to change:** Set `openTimeoutMs` to a larger supported positive integer, or `null` to disable its deadline explicitly. Set `maxErrorBodyBytes` to a larger positive safe integer if you require larger refusals; continue handling HTTP status and retry guidance when the body is incomplete.
</Update>

<Update label="October 1, 2026" description="Daily picks can release earlier for all games">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) now opens its daily release window at 07:00 UTC (10:00 AM GMT+3). Before this change, the window opened at 11:00 UTC.

  Each selection still releases one hour before kickoff, and the window still closes at 23:00 US Eastern time. The selected market and side stay fixed.

  **Backward compatible.** The response fields are unchanged. **What to change:** Follow the returned `retry_at` and `next_api_request_at` timestamps instead of a hardcoded opening hour.
</Update>

<Update label="October 1, 2026" description="API usage keeps late accounting in the original UTC day">
  API usage history and monthly budget reconstruction now include request records committed after their UTC day was first closed. [Usage](/api-reference/endpoint/get-usage) keeps its existing response fields and quota limits.

  Before this change, a late accounting record could be omitted from closed-day history until the next daily update. It now stays in the day when the request was observed, and current periods remain provisional while accounting finishes.

  This corrects usage counting without changing request prices or the existing monthly admission counter. It does not claim that a historical invoice was wrong or repaired.

  **Backward compatible.** No fields, limits, or prices change.

  **What to change:** Nothing.
</Update>

<Update label="October 1, 2026" description="Frozen picks release one hour before kickoff">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) automatic selections again release one hour before kickoff. Freezing commits the chosen market and side; ordinary changes do not replace them. A kickoff change can move the unpublished release time, and cancelled, closed or invalid markets can prevent release.

  **Backward compatible.** Response fields keep their shapes. `release_at` records the scheduled release instant; `published_at` records actual publication. Historical timestamps and explicit operator overrides are preserved.

  **What to change:** Keep using the returned `release_at`, `Retry-After` or `error.retry_at`. Freezing a selection does not make it immediately public.
</Update>

<Update label="October 1, 2026" description="Automatic picks publish when their selection is frozen">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) automatic selections now become due for publication as soon as they are frozen. Previously, they waited for a release time tied to kickoff. Serious market changes or unavailable provider facts can prevent release; explicit operator schedules remain supported.

  **Backward compatible.** Response fields keep their shapes. `release_at` now records the automatic freeze instant or an explicit operator schedule; publication can trail it. Frozen historical timestamps and optional execution permission keep their contracts.

  **What to change:** Remove any assumption that picks appear an hour before kickoff. Use the returned `release_at`, `Retry-After`, or `error.retry_at` to schedule reads. An earlier publication can precede that advisory retry.
</Update>

<Update label="October 1, 2026" description="Scheduled picks keep their selected identity through release">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) keeps each scheduled selection through release. Ordinary changes after selection no longer replace or cancel it. Cancelled or already-started games remain unavailable, and missing market data can delay publication.

  Before this change, a scheduled selection could change or disappear before publication.

  **Backward compatible.** Response fields and optional execution authorization keep their contracts. A published selection alone does not grant permission for an automated entry.

  **What to change:** Nothing. Continue honoring returned execution authorization, its expiry, and the current executable book for your order size.
</Update>

<Update label="October 1, 2026" description="MCP setup pins the curated 2.14.0 package">
  [MCP setup](/integrations/mcp) now pins `@0xinsider/mcp@2.14.0`. The package uses high-level tool descriptions and the documented customer pick contract, with the same 48 tools.

  Before this release, the setup examples installed `2.13.0`. Published selections, ordinary holder facts, prices, results, identity, access, and execution permission keep their contracts; internal selection diagnostics are no longer part of the customer pick response.

  **Breaking for clients that require internal selection diagnostics.** Authentication and client configuration remain unchanged.

  **What to change:** Upgrade the package and use documented customer fields. Honor returned entry permission, its expiry, and a current executable book for your order size.
</Update>

<Update label="October 1, 2026" description="Pick notifications link to stable selection pages">
  New [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day#notification-links) email, Discord, and inbox notifications link to the selection's page at `/pick-of-the-day/picks/{pick_id}`. A stable link opens the same selection, and a replacement has its own ID and page.

  Before this change, notification links could identify a date and slot or open the current pick page. Earlier emails and messages keep their original URLs, and dated links continue to work.

  **Backward compatible.** Opening a notification keeps the selection's Free or Pro access requirement. No REST or SDK response fields change.

  **What to change:** Keep notification destinations as URLs. Use `pick_id` for selection identity instead of extracting a date or slot from a link.
</Update>

<Update label="September 30, 2026" description="Pick responses omit internal selection diagnostics">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) and its [archive](/api-reference/endpoint/get-pick-of-the-day-archive) now focus on the published selection, game, holder positions and grades, prices, results, and execution permission. Internal selection diagnostics are no longer included in customer responses.

  Before this change, responses could include internal selection evidence. Published prices, outcomes, identity, access metadata, and historical proof records keep their contracts.

  **Breaking for clients that require internal selection diagnostics.**

  **What to change:** Use the documented customer fields and honor the returned entry authorization, its expiry, and a current executable order book for your order size.
</Update>

<Update label="September 30, 2026" description="Pick of the Day admits eligible selections in a neutral order">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day) now gives new selections a neutral presentation order. `publication_order` gives their presentation order, and `is_free_selection` names the selection available to a signed-in free account.

  Before this change, eligible selections did not use a neutral presentation order. Standing selections retain their identity, slot, and release schedule; later additions fill available slots.

  The daily limit, release timing, and Free/Pro access remain intact. Historical order, dated links, and proof bytes remain unchanged, and a thin day still returns fewer selections.

  **Backward compatible.** The stable-ID and access fields keep the shapes shipped in SDK `0.15.0` and MCP `2.13.0`.

  **What to change:** Use `pick_id` for identity, `publication_order` for presentation, and `is_free_selection` for access. Stop treating a slot number or list position as a quality recommendation.
</Update>

<Update label="September 30, 2026" description="SDK 0.15.0 and MCP 2.13.0 ship stable pick lookups">
  SDK `0.15.0` and MCP `2.13.0` are published on npm. The SDK includes `getPickOfTheDayLedgerEntry(pickId)`; the local MCP package includes `get_pick_of_the_day_ledger_entry` in its 48-tool catalog.

  The earlier setup used MCP `2.12.7`, which did not include the stable-ID tool. [MCP setup](/integrations/mcp) now pins `2.13.0`, and the [TypeScript guide](/integrations/typescript-client) shows the released SDK helper. The corresponding [REST route](/api-reference/endpoint/get-pick-of-the-day-ledger-entry) is available without a key; MCP tool calls retain their Pro authentication.

  **Backward compatible.** Existing methods, tools, and legacy proof verification remain available.

  **What to change:** Install `@0xinsider/sdk@0.15.0` or `@0xinsider/mcp@2.13.0` to use the stable-ID lookup. Keep `pick_id` as a decimal string and dispatch committed proofs by `commitment_version`.
</Update>

<Update label="September 30, 2026" description="Stable Pick of the Day ledger lookups honor public keyless access">
  [`GET /api/v1/pick-of-the-day/ledger/{pick_id}`](/api-reference/endpoint/get-pick-of-the-day-ledger-entry) now honors its documented public access for concrete pick IDs. The entry keeps the same side and nonce withholding as the full ledger.

  Before this change, authorization compared a concrete URL with the literal route template. A keyless lookup could return `401` before the public handler ran.

  A malformed ID returns `400`; a missing or unpublished pick returns `404`. Declared public route parameters match exactly one nonempty path segment.

  **Backward compatible.** This restores the documented keyless contract. Protected routes keep their authentication requirements.

  **What to change:** No API key is needed for this REST lookup or the SDK helper. Keep `pick_id` as a decimal string. MCP tool calls retain their Pro authentication.
</Update>

<Update label="September 30, 2026" description="Games report Started for Dota matches with a settled map winner">
  [`GET /api/v1/games`](/api-reference/endpoint/list-games) and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) now report `status.provider_status: "Started"` for Dota matches whose scheduled kickoff has passed, whose full-match winner remains unsettled, and whose settled map winner proves play began while live status is unavailable.

  Before this change, these matches retained `status.provider_status: "Scheduled"`, `status.state: "scheduled"`, and `status.match_status: "scheduled"`. They now report `status.state: "unknown"` and omit `status.match_status`; `status.live` remains `false`. `Started` confirms earlier play, not that the match is currently live.

  **Backward compatible.** Existing response fields and the state vocabulary keep their shapes. No new field or state value is added.

  **What to change:** Nothing. Use `status.live` to identify confirmed live play.
</Update>

<Update label="September 30, 2026" description="Pick of the Day adds stable IDs and versioned commitments">
  [Pick of the Day](/api-reference/endpoint/get-pick-of-the-day), its [archive](/api-reference/endpoint/get-pick-of-the-day-archive), and its [ledger](/api-reference/endpoint/get-pick-of-the-day-ledger) now expose stable identity and explicit access metadata.

  Before this change, you had to identify a pick by its date and `pick_rank`, and infer the free selection from that slot. New fields separate identity from presentation:

  * `pick_id` is a positive decimal string that identifies the stored pick.
  * `publication_order` describes the compatibility order. It does not claim a quality score.
  * `is_free_selection` identifies the selection available to signed-in free accounts.
  * `supersedes_pick_id` names a replaced selection, or is `null` when there is no replacement.

  [`GET /api/v1/pick-of-the-day/ledger/{pick_id}`](/api-reference/endpoint/get-pick-of-the-day-ledger-entry) returns one published ledger entry by its stable ID. It is public, and live entries retain the ledger's side and nonce withholding.

  Committed ledger entries declare `commitment_version`. Version `1` retains the original eight-field payload and proof bytes. Version `2` replaces payload `pick_rank` with the decimal-string `pick_id` and adds integer `version: 2`; every other canonical serialization rule stays the same. Uncommitted disclosures carry no commitment version and make no proof claim.

  Published SDK `0.15.0` includes `getPickOfTheDayLedgerEntry(pickId)`, and MCP `2.13.0` includes `get_pick_of_the_day_ledger_entry` with a `pick_id` argument. The REST read and SDK helper need no key; MCP tool calls keep their Pro authentication.

  **Backward compatible.** Existing fields and routes remain available; `pick_rank` is a compatibility slot, and historical commitments keep version `1`.

  **What to change:** Store and link picks by `pick_id`. Read `is_free_selection` for access, and dispatch proof verification by `commitment_version`; reject an unknown version instead of guessing.
</Update>

<Update label="September 30, 2026" description="Future Pick of the Day entry authorizations stop at 0.85">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now caps newly issued `entry_authorization.max_entry_price` at `0.85`, rounded down to the Polymarket tick. The selected token's fresh order book must cover the \$100 reference principal and minimum order size within that ceiling. Fees are additional, and your own order still needs a current book check for its size.

  Before this change, an automated entry ceiling could exceed `0.85`. New preparation or refresh refuses an older authorization above that ceiling. Historical authorization records and already issued private snapshots keep their original values until those snapshots refresh.

  Automatic picks must remain eligible before publication and before a new automated entry permission is issued. Fewer picks or authorizations may be available. Neither a pick nor its authorization provides a fair probability or a profitability guarantee.

  **Backward compatible.** Response fields keep their existing shapes. Fewer future picks or authorizations may be available.

  **What to change:** Honor the returned `entry_authorization.max_entry_price` rather than calculating your own ceiling from the ask. Skip an automated entry when its authorization is missing or expired.
</Update>

<Update label="September 30, 2026" description="The TypeScript SDK is available on npm as @0xinsider/sdk 0.14.0">
  [`@0xinsider/sdk` 0.14.0](/integrations/typescript-client) is available with `npm install @0xinsider/sdk`. It includes typed API calls, cursor pagination, resumable SSE streams, typed errors, and webhook signature checks. The package needs Node.js 18 or later and ESM, includes TypeScript declarations, and has no runtime dependencies.

  Before this release, you had to clone and build the public source repository. The npm release carries provenance from that repository's GitHub publication workflow.

  **Backward compatible.** This adds a registry installation path for the existing SDK.

  **What to change:** Install `@0xinsider/sdk` from npm instead of a local build directory. You can use `OxinsiderApiClient.sandbox()` without an API key.
</Update>

<Update label="September 30, 2026" description="MCP output schemas describe null with explicit JSON Schema alternatives">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now publishes `tools/list` output descriptors that express nullable values as `anyOf` alternatives, including `type: "null"`. The `get_pick_of_the_day` tool's `outputSchema` describes `sports_context.yes_team` and `sports_context.no_team` as a team object or `null`, matching its existing response.

  Before this change, those team schemas combined `allOf` with `nullable: true` and no `type`. JSON Schema 2020-12 consumers using Ajv could reject the descriptor during compilation, before validating a tool result.

  **Backward compatible.** The explicit null alternatives are additive descriptions of existing response values. Response fields, tool names, authentication, and scopes keep their contracts.

  **What to change:** Refresh cached `tools/list` descriptors and recompile your output validators. Your tool calls stay the same.
</Update>

<Update label="September 30, 2026" description="CLI response-body network failures retain exit 7 and login backoff">
  The CLI now uses exit `7` for a network failure while reading a response body, as it already does before headers arrive. OAuth body timeouts use the same request deadline, and device login keeps its network backoff. Invalid JSON or response schemas remain exit `6`, and interruption remains `130`.

  The TypeScript SDK adds `ResponseBodyReadError` for an interrupted JSON or text body, with `responseStatus`, `phase: "response_body"`, and the original transport `cause`. SDK deadlines retain `RequestTimeoutError`, and caller cancellation retains its exact reason. Registration, refresh, revocation, and SDK body reads gain no automatic retry.

  **Compatibility:** The CLI follows its documented exit codes. The SDK replaces untyped native body-read rejections with the new error class, so consumers that branch on a native error class should handle `ResponseBodyReadError`.

  **What to change:** Upgrade the scoped CLI runtime or the bare `0xinsider` launcher installation, and handle the SDK class when using the updated source. A lost write response leaves its outcome uncertain; reconcile the operation before retrying.
</Update>

<Update label="September 30, 2026" description="RSS discovery omits a modification date that does not cover pick updates">
  [`GET /schemamap.xml`](https://0xinsider.com/schemamap.xml) now omits `lastmod` for its `/feed.xml` entry. The other feed entries keep their modification dates.

  Before this change, the RSS date covered authored article publication dates but missed pick publication and settlement updates. Consumers could treat a changed feed as unchanged.

  The RSS URL, item identifiers, publication dates, and `atom:updated` values stay the same.

  **Backward compatible.** The Schema Feed specification makes `lastmod` optional.

  **What to change:** When the RSS entry has no `lastmod`, fetch the feed instead of comparing its previous modification date.
</Update>

<Update label="September 30, 2026" description="Public page Markdown keeps article content and reports missing or unsupported formats">
  Public article pages under `/learn`, `/research`, and `/compare` requested with `Accept: text/markdown` now return the same article, title, authored dates, and canonical link as their existing `.md` URL.

  Before this change, negotiation could replace an article with generic site text and return `200` for a missing content slug. Missing content now keeps `404`, and a page without a Markdown representation returns `406` when the request does not accept HTML.

  Mixed `Accept` values select an available representation using the existing quality and wildcard rules. Curated agent documents, document-only `.md` resources, APIs, and authenticated pages keep their existing contracts.

  **Breaking for clients relying on generic Markdown at arbitrary page URLs.** Existing article `.md` URLs and curated documents remain compatible.

  **What to change:** Accept `404` for missing content and `406` for an unsupported representation. Accept `text/html` as a fallback for pages without Markdown, or use the documented API for dynamic data.
</Update>

<Update label="September 30, 2026" description="OAuth refresh keeps the existing scopes when scope is blank">
  [`POST /oauth/token`](https://0xinsider.com/auth.md) now treats an empty or whitespace-only `scope` as omitted when `grant_type` is `refresh_token`, preserving the existing grant's scopes.

  Before this change, a blank `scope` rotated the grant with no scopes, so the new access token could not use routes that require `read` or another scope.

  An explicit nonempty `scope` still narrows the grant to the requested scopes. Asking for a scope outside the existing grant still returns `invalid_scope`.

  **Backward compatible.** Omitted and valid nonempty scope requests keep their behavior. Token lifetimes and rotation stay the same.

  **What to change:** Nothing. Omit `scope` when refreshing unless you intend to narrow the grant; a blank value now preserves it too.
</Update>

<Update label="September 30, 2026" description="Checkpointed TypeScript streams stop cleanly after cancelled callback failures">
  The TypeScript SDK's `consumeStreamCheckpointed` now returns cleanly when caller cancellation accompanies an awaited event, resync, or checkpoint callback failure, including with `maxHandlerRetries: 0` or an exhausted retry budget.

  Before this change, those failures could consume the handler retry budget and throw `StreamHandlerFailedError` during ordinary shutdown.

  Failed work never advances the checkpoint. Successful callbacks and their durable checkpoint write still commit before returning after cancellation. `onHandlerError` retains the original callback error, reports `willRetry: false`, and adds optional `cancelled: true`; that flag means cancellation stopped the consumer, without claiming it caused the callback failure. The 1-based `attempt` still describes consecutive failures at that sequence.

  **Backward compatible.** The optional diagnostic field is additive. Retry and replay behavior without cancellation is unchanged. `onHandlerError` must still return synchronously without throwing.

  **What to change:** Rebuild from the updated [public SDK source](https://github.com/0xinsider/0xinsider-node). If your diagnostic treats every `willRetry: false` as exhaustion, check `cancelled` to distinguish a clean shutdown. The standalone SDK's first npm release remains pending.
</Update>

<Update label="September 30, 2026" description="TypeScript stream reconnects honor the terminal error's retry_at">
  The TypeScript SDK's `streamFeedResilient` and `consumeStreamCheckpointed` now wait until a retryable terminal error's `retry_at` before reconnecting when no usable HTTP `Retry-After` is available.

  Before this change, the SDK preserved `retry_at` on the error but used local backoff, which could reconnect before the server's published recovery time.

  HTTP `Retry-After` remains preferred. Absolute times use your machine's clock; keep it synchronized with the server. Past times permit an immediate retry, missing or invalid timing uses local backoff, and `retry: false` still stops permanently. The same `maxRetryAfterMs` ceiling, deferred-retry checkpoint, and cancellation behavior apply.

  **Backward compatible.** No public signature changes.

  **What to change:** Rebuild from the updated [public SDK source](https://github.com/0xinsider/0xinsider-node). Keep your clock synchronized and schedule `StreamRetryDeferredError.retryAt` when a wait exceeds your ceiling. The standalone SDK's first npm release remains pending.
</Update>

<Update label="September 30, 2026" description="TypeScript export helpers release object responses they cannot return">
  The TypeScript SDK in the public source and bundled with `@0xinsider/mcp` 2.12.6 now cancels an export object response when `downloadTraderExport` or `downloadWhaleDataset` cannot return it. It waits up to 2 seconds for cleanup and preserves the original failure, with cleanup rejection or unknown-completion information in its `cause`.

  Before this change, a failed object HTTP response or an error while preparing its stream could leave an unread body or reader unreleased.

  Successful downloads still stream, checksum defaults remain unchanged, and there is no new default download deadline. After a helper returns, you own consuming or cancelling `response.body`.

  **Backward compatible.** Method signatures and ordinary HTTP error class, status, and message remain unchanged. An existing cause is retained with cleanup information; a thrown value that cannot carry a cause is retained inside an `AggregateError`.

  **What to change:** Rebuild from the updated public SDK source, or update to `@0xinsider/mcp` 2.12.6 after publication. Continue consuming or cancelling returned bodies, and inspect a failure's cause when diagnosing cleanup. The standalone `@0xinsider/sdk` first npm release remains pending.
</Update>

<Update label="September 30, 2026" description="Remote MCP publishes result schemas and OAuth account-linking metadata">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now publishes result schemas and OAuth requirements in its remote tool descriptors. An OAuth call refused for a missing scope also includes an account-linking challenge.

  * `outputSchema` describes each tool's successful `structuredContent` with required result fields, or its existing `structuredContent.error` object.
  * `securitySchemes` declares `type: "oauth2"` with `read` plus any scope required by the tool's REST route. `_meta.securitySchemes` repeats the same schemes for clients that read this metadata field.
  * An insufficient-scope tool error adds `_meta["mcp/www_authenticate"]`, an array of Bearer challenges with `error="insufficient_scope"`, `scope`, `resource_metadata`, and `error_description`. A host can use it to request the required authorization before retrying.

  Before this change, remote tools returned `structuredContent` without an output schema, their OAuth requirements were implicit, and insufficient-scope tool errors carried no account-linking hint.

  **Backward compatible.** The remote server still has 47 tools. Successful payloads, API-key access, paid-access requirements, and unauthenticated `401` discovery keep their contracts; the stdio package does not change.

  **What to change:** Nothing for clients using an API key or an OAuth grant with the required scope. Refresh cached `tools/list` descriptors to read the new fields; OAuth hosts can use the linking challenge to obtain the missing scope, then retry the refused call.
</Update>

<Update label="September 30, 2026" description="SDK caller cancellation preserves its reason instead of becoming the SDK timeout">
  `@0xinsider/sdk` source and the SDK bundled in `@0xinsider/mcp` 2.12.5 preserve the exact reason from a caller's cancellation signal on API calls. Only the SDK's own deadline rejects with `RequestTimeoutError` and its configured `timeoutMs`; whichever signal aborts first determines the rejection through fetch, body consumption, and retry backoff.

  Before this change, a caller's `AbortSignal.timeout(2000)` could be reported as the default 15-second SDK timeout. Custom caller reasons named `TimeoutError` could be wrapped the same way, even though the caller cancelled first.

  **Backward compatible.** Public signatures and retry eligibility stay the same. API cancellation starts no further request attempt; signed object downloads retain their separate timeout and error behavior.

  **What to change:** Match `RequestTimeoutError` for the SDK deadline and handle your caller signal's reason separately. The first standalone npm publication remains pending; build the [public source package](https://github.com/0xinsider/0xinsider-node) until it is published.
</Update>

<Update label="September 30, 2026" description="potd-trader 0.3.4 runs directly on Windows with the same CLI and ledger">
  `potd-trader` 0.3.4 runs directly in PowerShell on Windows x64, with Python 3.12.4 or newer. The same repository, commands, configuration, and v1 JSON ledger work on Windows, macOS, and Linux.

  Before this release, Windows required WSL because the trader used POSIX file locks. Windows now uses local file locks, flushes file contents, and atomically replaces ledger and control files; it does not flush their containing directory, so a hard power loss can lose the latest reservation or stop flag.

  On Windows, `init` limits its new folder to your current user account and administrators, and files inherit that folder's access permissions.

  **Backward compatible.** Existing configuration and ledger formats stay the same. The live switch, stop flag, duplicate checks, price limits, and daily cap still apply.

  **What to change:** Follow the [installation guide](/guides/auto-buy-the-pick). Stop every old watcher, including WSL instances, before upgrading; keep one configuration and ledger in a private local folder outside OneDrive or network drives. Check `status` and run `--dry-run` before enabling live trading, and never run Windows and WSL instances for the same wallet together.
</Update>

<Update label="September 30, 2026" description="Cold SSE replicas resume after a confirmed feed sequence reset">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) emits one existing `resync` marker with `completeness.status: "reset"` and continues at the new sequence after the feed bridge confirms a counter reset. This also works when the server has replayed retained history but has not yet forwarded a live frame.

  Before this change, that server could discard new events until their IDs reached the old replay head. Ordinary replay overlap still produces no reset marker, and losing the first reset envelope does not discard the bridge's reset signal.

  **Backward compatible.** Event payloads, SSE IDs, privacy checks, and the existing marker format stay the same.

  **What to change:** Nothing. Continue handling `resync` by refreshing current state and storing the marker's ID before processing later events.
</Update>

<Update label="September 30, 2026" description="SDK pagination progress counts delivered pages and preserves the retry cursor">
  `@0xinsider/sdk` now updates `PaginationProgress.pagesFetched` before delivering each validated page. An early consumer return retains the page count and `nextCursor`; `stoppedBy` is set only when that delivered page exhausts the collection or reaches `maxPages`.

  Before this change, an early exit could report zero pages, a later failure could leave the previous request cursor, and a reused progress object could retain an old stop reason. New iterations reset owned progress fields, and failures or between-page cancellation publish the same retry cursor and completed-page count as `paginationResumePoint(error)`.

  Page counts describe pages delivered, not items processed or persisted. Original errors, cursor validation, retry policy, and public signatures are preserved.

  **Backward compatible.** This fixes existing progress and resume diagnostics.

  **What to change:** Read progress after a delivered page or failure. Continue from `nextCursor` after an early return or `max_pages`, or from `paginationResumePoint(error).cursor` after a failure. The first npm publication remains pending; the fix is included in the public source package.
</Update>

<Update label="September 30, 2026" description="Trader export download links expire within the file's retention window">
  [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export) returns a signed URL valid for at most 1 hour and no later than the job's `expires_at`. If less than 1 whole second remains when signing, the route returns `410` with `error.reason: "export_expired"`.

  Before this change, the URL was signed for 1 hour even when the export's retention window ended sooner. A client could receive a still-valid link to a file that had already been retired.

  **Backward compatible.** This fixes the existing retention boundary; routes, response fields, and error codes stay the same. Links requested near expiry can last less than 1 hour.

  **What to change:** Use the SDK download target's `expiresAt` or the job's `expires_at` rather than assuming 1 hour. Request a fresh URL while the job is retained, and submit a new export after `410 export_expired`.

  Details: [issue #19988](https://github.com/0xinsider/0xinsider/issues/19988) and [PR #19994](https://github.com/0xinsider/0xinsider/pull/19994).
</Update>

<Update label="September 30, 2026" description="Market-holder cursors preserve your position when page size changes">
  [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders) now returns cursors that preserve the next holder's position when you change `limit`. Each new cursor belongs to the same market, `outcome`, and `min_grade`; using it with different filters returns `400 bad_request` with `param: "cursor"`.

  Before this change, changing `limit` multiplied the cursor's page number by the new page size, which could skip or repeat holders even when the roster had not changed. The roster remains current state, so a refresh between pages can still change its membership or order.

  **Backward compatible.** Existing page-number cursors remain accepted without a scheduled retirement; keep their original `limit` until the next response returns a new cursor. Raw condition IDs and their `mkt_` forms identify the same market; omitted filters match their documented defaults.

  **What to change:** Send `next_cursor` unchanged with the same market and filters. Restart without a cursor when you want different filters. You may change `limit` with new cursors.
</Update>

<Update label="September 30, 2026" description="MCP keeps structured timeout and network errors through the complete response body">
  `@0xinsider/mcp` 2.12.4 returns structured `request_timeout` errors when its deadline expires before headers or while reading the response body. Other network and body-read failures return `transport_error`.

  Before this change, a body-read failure escaped the structured error handler and appeared only as unexpected-error text. Errors now retain the received HTTP status, request ID, and validated retry delay when headers arrived. Before headers, those facts remain absent.

  Fully read REST and non-JSON errors retain their existing classifications. The default 15-second deadline and explicit timeout opt-out stay the same. Neither error automatically retries a request; a POST whose response was lost may have committed.

  **Backward compatible.** Tool arguments and successful responses stay the same; network failures gain structured fields.

  **What to change:** Upgrade to `@0xinsider/mcp` 2.12.4 once published. Handle `transport_error` and `request_timeout`, and check a mutation's outcome before repeating it.
</Update>

<Update label="September 30, 2026" description="MCP retry guidance validates seconds and HTTP dates consistently">
  `@0xinsider/mcp` 2.12.3 uses one validated `Retry-After` delay in error text and `structuredContent.error.retry_after_seconds`. Whole non-negative seconds and valid HTTP dates are accepted. A future date becomes whole seconds rounded upward from header receipt; a past date becomes zero.

  Before this change, an HTTP date was missing from the structured field and appeared with a seconds suffix in the text. Negative values and numeric prefixes such as `10junk` could become retry instructions.

  Missing, malformed, fractional, or unsafe numeric values now provide no retry instruction. Integer delays are bounded by `Number.MAX_SAFE_INTEGER`. The REST `retry_at` stays separate, and the stdio client still makes no automatic retries.

  **Backward compatible.** No tool argument or successful response changes.

  **What to change:** Upgrade to `@0xinsider/mcp` 2.12.3 once published. Schedule retries only when a valid delay or REST `retry_at` is supplied.
</Update>

<Update label="September 30, 2026" description="SSE reconnects discard replay overlap without a false reset">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) now discards live frames already covered by reconnect replay without emitting a `resync` marker or sending those events again.

  Before this change, frames queued during replay could be mistaken for a sequence reset, causing duplicate events with lower SSE IDs. Genuine sequence resets still emit one `resync` marker with `completeness.status: "reset"`, followed by events from the new sequence.

  **Backward compatible.** Event payloads, filters, authentication, and the reset marker format are unchanged.

  **What to change:** Nothing. Continue handling `resync` markers and using durable event IDs to deduplicate across genuine resets.
</Update>

<Update label="September 30, 2026" description="potd-trader 0.3.3 removes pick rank filters and numbered labels">
  `potd-trader` 0.3.3 considers every released, otherwise eligible Pick of the Day selection at the same unit size. It no longer accepts `MIN_RANKS` or `MAX_RANKS` as trading filters, and its pick labels no longer display rank numbers. The API's existing slot is still used internally to prevent duplicate orders.

  Before this release, explicitly configured rank ranges could exclude otherwise eligible picks. The daily cap, price and kickoff checks, live control, and duplicate protection still apply; the default 25 pUSD cap still funds five 5 pUSD picks.

  **Breaking** for users who relied on an explicit rank range: those settings are ignored and every eligible pick can be considered within the existing cap.

  **What to change:** Check `potd-trader status`, review your unit size and daily cap, and run a dry run before restarting. Stop live trading before using `potd-trader size`, which removes obsolete rank lines from `.env`. The [main product's rank-free identity and proof migration](https://github.com/0xinsider/0xinsider/issues/19968) is tracked separately.
</Update>

<Update label="September 30, 2026" description="potd-trader 0.3.2 includes every pick and asks for unit size and daily cap">
  `potd-trader` 0.3.2 makes all 10 Pick of the Day slots eligible at one unit size by default. Its `init` command asks for unit size and daily cap, and the new `size` command changes both in a stopped existing setup. `status`, `run`, and `watch` show budget capacity and warn when the cap cannot cover 10 picks.

  Before this release, the trader defaulted to ranks 1 through 6 and a 25 pUSD cap, which funded only five 5 pUSD orders. A one-shot `run` still sees only released picks; `watch` handles later releases. When the cap cannot fund all eligible picks, release time sets priority, with token ID breaking ties; each cap skip is shown.

  **Breaking** for users who relied on the default rank 1 through 6 filter: new setups include ranks 7 through 10. Existing explicit `MIN_RANKS` and `MAX_RANKS` settings remain in force, and existing daily caps do not rise automatically.

  **What to change:** Review your unit size and cap with `potd-trader status`. Stop the watcher and turn live trading off before using `potd-trader size`; remove old rank settings only if you want every pick.
</Update>

<Update label="September 30, 2026" description="The TypeScript SDK rejects blank or overlong Idempotency-Key values before fetch">
  The `@0xinsider/sdk` source build now rejects an effective `Idempotency-Key` that is empty after trimming or exceeds 255 UTF-8 bytes, before sending a request. Validation applies to both `headers` and `idempotencyKey`, including with `maxRetries: 0`; the explicit option overrides the custom header.

  Before this change, a blank header could enable retries even though the API treated it as no replay key. Omitting the key still sends a supported mutation once, and valid keyed retries keep the same key and body.

  **Breaking** for callers supplying invalid keys: these calls now fail locally. Valid and omitted keys retain their existing behavior. npm publication remains pending; use the [documented source installation](https://github.com/0xinsider/0xinsider-node#install).

  **What to change:** Supply a stable nonempty key on a supported webhook mutation and reuse it when reconciling an unknown outcome, or omit the key for a single attempt. Do not send a blank value.
</Update>

<Update label="September 30, 2026" description="TypeScript SDK errors retain Retry-After and X-Request-ID response headers">
  The `@0xinsider/sdk` source build now retains parsed `Retry-After` on every HTTP error, including generic 502/504 errors and plain-text or empty bodies. `requestId` uses JSON `meta.request_id` first, then a nonempty received `X-Request-ID` header; a header does not create a synthetic `meta` object.

  Before this change, generic errors could discard retry guidance, and responses without JSON metadata lost their request ID. Error classes, raw bodies, automatic retry eligibility, and mutation replay requirements are unchanged.

  **Backward compatible.** Existing constructor and factory calls still work. npm publication remains pending; use the [documented source installation](https://github.com/0xinsider/0xinsider-node#install).

  **What to change:** Read `retryAfterSeconds` and `requestId` from `OxinsiderApiError` when handling a received HTTP failure. A retry header does not make a mutation safe to repeat.
</Update>

<Update label="September 29, 2026" description="Local MCP Explore results keep request and cache metadata">
  `@0xinsider/mcp` 2.12.2 includes the API's `meta` object in stdio `explore_markets` results.

  Before this change, the local MCP adapter dropped `meta`. The [REST Explore endpoint](/api-reference/endpoint/explore-markets) and remote MCP already returned it.

  * `request_id` identifies the API request.
  * `cached` says whether the API served a cached result.
  * `cache_age_s` remains absent when the API does not know the cache age. Other metadata fields pass through unchanged.

  **Backward compatible.** Tool arguments, data, facets, and pagination fields keep their meanings.

  **What to change:** Update your local MCP package to 2.12.2 to read this metadata. Your queries do not change.
</Update>

<Update label="September 29, 2026" description="WebSocket controls keep working during slow writes and price recovery">
  The WebSocket hub (`/api/ws`) now handles close, unsubscribe, credential expiry, and shutdown while a slow connection is writing or recovering prices. Latest queued quotes stay ordered, and a REST snapshot cannot replace a side that already has a streamed price.

  Before this change, a continuously replenished price buffer could postpone those controls. Protocol ping timeouts now count pings after they are written, rather than while they are still queued.

  A connection that exhausts the bounded output queue closes with `send_backpressure`. Retirement during an unfinished write can end the transport without a close frame.

  **Backward compatible.** Frame fields, topic limits, and authentication requirements are unchanged.

  **What to change:** Nothing. Keep your existing reconnect and resubscribe handling, including transport EOF without a close reason.
</Update>

<Update label="September 29, 2026" description="SSE replay checks ongoing authorization before sending more protected events">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) now applies ongoing authorization to replay as well as live delivery. A paused response checks due authorization and connection closure before sending another protected replay event.

  Before this change, buffered replay could continue before those checks ran. A terminal error uses the last sequence the connection handled, and already-delivered bytes cannot be recalled. A healthy paused response can resume after fresh authorization; protected delivery waits for that check.

  **Backward compatible.** The event format, terminal error format, and reconnect headers are unchanged.

  **What to change:** Nothing. Keep reading terminal error frames and reconnect using `Last-Event-ID` as before.
</Update>

<Update label="September 29, 2026" description="Export artifacts return the storage ETag without XML escapes">
  [`GET /api/v1/datasets/whale-trades/{job_id}`](/api-reference/endpoint/get-whale-dataset-status) and [`GET /api/v1/trader/{address}/export/status`](/api-reference/endpoint/get-trader-export-status) now return the completed storage object's ETag as its actual text value for newly completed exports.

  Before this change, multipart completion could leave XML escapes such as `&quot;` inside `artifact.etag`. Malformed completion metadata follows the existing reconciliation path. Historical artifacts and checksum values are unchanged.

  **Backward compatible.** The optional field keeps its meaning and shape.

  **What to change:** Treat new `artifact.etag` values as storage ETags; do not XML-decode them again. Check file integrity with the manifest's SHA-256 values.
</Update>

<Update label="September 29, 2026" description="Whale datasets add immutable downloads with checksums and replay continuation">
  [`POST /api/v1/datasets/whale-trades`](/api-reference/endpoint/submit-whale-dataset) now queues an immutable cross-market snapshot for a finite past window. Poll its private job, download gzip NDJSON, or cancel it through the dataset resource.

  Before this change, cross-market history required pages of at most 100 rows; asynchronous exports covered one wallet at a time.

  * The manifest includes filters, the selection clock, a safe commit horizon, row and byte counts, content and compressed SHA-256, schema, source coverage, expiry, and an overlapping replay checkpoint.
  * Continuation includes already committed arrivals after the requested window, plus later writers. Keep the same replay filters and deduplicate by whale-trade ID; this does not promise exactly-once delivery.
  * Dataset requests share owner export quotas and lifecycle controls. Signed URLs expire within 1 hour and the artifact's 24-hour retention deadline.
  * Rows are detected whale alerts with exact decimal strings, not complete provider fills or enriched grades.
  * Snapshot and replay use the same exact decimal size threshold. Pass the continuation's `min_size` string unchanged; the TypeScript replay operation accepts it without a floating-point conversion. Scientific spellings and tiny fractions use exact cent normalization.

  **Backward compatible.** Existing trader exports and history keep their contracts.

  **What to change:** Use the new dataset resource for bounded extraction, check its manifest, and retain replay cursors while deduplicating IDs.
</Update>

<Update label="September 28, 2026" description="Expanded trader categories keep live source freshness during historical fill imports.">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) with `expand=categories` now judges category-source freshness from the newest Polymarket block received.

  Before this change, a historical fill arriving after a live fill could replace the current source clock with an older block time. That could make `category_records` and `category_skill_model` report `degraded` while live fills were still arriving.

  `source_last_success_at` reports the provider block time used for source freshness. Stale, future-dated, or unavailable source clocks still produce `degraded` records with withheld scores. Other readiness checks continue to apply.

  **Backward compatible.** Fields and response formats are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 28, 2026" description="Wallet position feed updates survive interrupted refreshes">
  The wallet position feed on `/api/ws` now publishes each committed group of row updates while a refresh is running. A later refresh failure retains those updates.

  Before this change, a large refresh could lose all pending updates when its write transaction exceeded its deadline. Position events were sent only after the entire refresh committed.

  A position event describes the available facts for that row. It does not prove that every position in the wallet has finished refreshing. Missing provider values remain unavailable.

  **Backward compatible.** Event fields and authentication are unchanged.

  **What to change:** Nothing for clients that apply each position event independently. If you display a wallet-wide total, reread its endpoint rather than treating one position event as a complete wallet snapshot.
</Update>

<Update label="September 28, 2026" description="Trending wallet responses limit cache freshness to 120 seconds.">
  [GET /api/v1/leaderboard/trending](/api-reference/endpoint/list-trending-wallets) now limits HTTP cache freshness to 120 seconds from the ranking's original computation. Any `stale-while-revalidate` allowance ends before the retained ranking expires. A response whose age is unknown grants no cache window.

  Before this change, a response could advertise 300 seconds of freshness even though a server request started a refresh after 120 seconds. A ranking can still be up to 24 hours old after a quiet period. Successful responses keep `meta.cached: true`; `meta.cache_age_s` reports the original age when known and is omitted when unknown.

  The same cache policy applies to `200` and `304` responses. Response fields, ranking values, pagination cursors, and cold-cache errors are unchanged.

  **Backward compatible.**

  **What to change:** Respect `Cache-Control` and use `meta.cache_age_s` to decide whether an older ranking meets your needs. If you override the server's cache policy with a fixed 300-second window, remove that override.
</Update>

<Update label="September 28, 2026" description="Trending wallets keep the previous board when a refresh fails.">
  [GET /api/v1/leaderboard/trending](/api-reference/endpoint/list-trending-wallets) now restores the previous cached board after confirmed refresh failure handling, and waits 120 seconds before retrying. The retained board keeps its original age and expiry.

  Before this change, a failed web cache update could remove the retained board, and repeated requests could immediately restart a failed refresh. The response fields, normal refresh timing, and cold-cache `503` response are unchanged. Redis coordination failures can prevent confirmation of restoration and the cooldown; these failures are reported in server logs.

  **Backward compatible.**

  **What to change:** Nothing. Continue using the response's freshness information and handling `503` while a board is unavailable.
</Update>

<Update label="September 28, 2026" description="Sports edge observations refresh market identity before category selection.">
  [GET /api/v1/sports/pre-game-side-observations](/api-reference/endpoint/get-pre-game-side-observations), and its deprecated alias `GET /api/v1/sports-edge-observations`, now refresh a candidate market's category, title, event identity, outcomes, and token IDs before selecting the requested category. A market moved into that category can appear on the next computation.

  Before this change, those fields could remain stale for up to 10 minutes while the stored market list was reused. A candidate whose current market or Polymarket identity is missing is now omitted instead of keeping its old identity.

  **Backward compatible.** The fields and response format are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 28, 2026" description="Expanded trader category ranks and totals use the same publication.">
  [GET /api/v1/trader/{address}](/api-reference/endpoint/get-trader) with `expand=categories` now returns `category_strengths` whose `rank` and `total_in_category` use the same hourly publication. A category absent from that publication is omitted until a later publication includes it.

  Before this change, `total_in_category` could use a newer population than `rank`, and a newly added category could contain a null `rank`. Each trader's current performance values still update separately, so these fields do not describe a frozen historical record.

  **Backward compatible.** The fields and response format are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 28, 2026" description="Category-filtered positions pages keep consistent membership during updates.">
  [GET /api/v1/positions](/api-reference/endpoint/get-positions) now keeps one consistent view of category membership while building a category-filtered page.

  Before this change, a position whose category changed during the read could be missing from the page.

  **Backward compatible.** The response and cursor formats are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 27, 2026" description="Large trades take since, so a poll returns only trades recorded after one you hold">
  [Large trades](/api-reference/endpoint/get-large-trades) (`GET /api/v1/large-trades`, and its deprecated alias `GET /api/v1/whale-trades`) take a `since` query parameter: the `id` of a trade the API returned. The response keeps its shape and newest-first order, holds only the trades recorded after that one, and pages with `cursor`.

  Before this change, a client that polled for new trades downloaded a full page on every poll. `since` follows the order trades were recorded in, so a trade recorded late with an earlier `traded_at` still comes back, and a trade still being written comes back on a later poll.

  * `since` works with `sort=recent` only.
  * `400` with `error.param` set to `since`: an `id` that does not parse or names no trade, `since` with `sort=market_volume_share`, or an `id` more than 10,000 trades behind the newest. Poll once without `since` and continue from its first trade.
  * The remote MCP tools `get_large_trades` and `get_whale_trades` take `since`, and `@0xinsider/mcp` 2.12.0 brings it to the stdio server. The Node.js SDK types `since` and adds `listLargeTradesConditional()`, which returns a typed `304`.

  **Backward compatible.** A request without `since` answers exactly as before.

  **What to change:** Nothing. To poll, send `since` set to the first trade of your last response that had trades, and `If-None-Match` set to its `ETag`: a poll with nothing new is `304 Not Modified` with an empty body.
</Update>

<Update label="September 27, 2026" description="A cold sports-edge observations process answers a replica hiccup with 503, not 500">
  [`GET /api/v1/sports/pre-game-side-observations`](/api-reference/endpoint/get-pre-game-side-observations) and its deprecated alias `GET /api/v1/sports-edge-observations` now answer `503 read_model_warming` (with `Retry-After`) when a process with no cached observation universe hits a replica recovery conflict, the replica's own statement timeout, a busy connection pool, or a replica marked down while refreshing that universe.

  Before this change, the same replica-side conditions on a cold process answered `500 internal_error`. A universe refresh failure on a process that already holds a usable universe is unaffected: it keeps serving the last good universe. Any other observation-universe SQL error, or an invalid compact result, still answers `500 internal_error`.

  **Backward compatible.** This narrows one existing failure case from `500` to the route's existing `503 read_model_warming` retry contract; no field, parameter, or other error mapping changed.

  **What to change:** If you branch on `error.reason` for this route, treat a cold-process replica hiccup as `read_model_warming` (retryable per `Retry-After`) rather than `internal_error`.
</Update>

<Update label="September 27, 2026" description="Trending wallets reports ranking age and serves the previous ranking during refresh">
  [`GET /api/v1/leaderboard/trending`](/api-reference/endpoint/list-trending-wallets) now builds its ranking when it is requested, not on a schedule, and reports how old the ranking is. `meta.cached` is `true` and `meta.cache_age_s` is the ranking's age in seconds.

  Before this change the default 7-day ranking was rebuilt every 2 minutes whether or not anyone read it, so it was never more than 5 minutes old, and `meta` reported `cached: false` with no `cache_age_s`. Now:

  * A request that finds a ranking at least 2 minutes old gets it at once and starts one rebuild. A ranking read at least every 2 minutes stays about as current as before.
  * The first request after a quiet period can get a ranking up to 24 hours old; its `meta.cache_age_s` says so. The next request after the rebuild, usually within 30 seconds, gets the new ranking.
  * Just after midnight UTC, a request can get the previous day's ranking, with its own `start_date` and `end_date`.
  * `503` with `error.reason` `read_model_warming` now happens only when no ranking was built in the last 24 hours.

  **Backward compatible.** `meta.cached` and `meta.cache_age_s` were already part of the response envelope, and the `503` was already documented.

  **What to change:** If you need a ranking newer than a few minutes, read `meta.cache_age_s` and request again after about 30 seconds when it is higher than you accept.
</Update>

<Update label="September 27, 2026" description="OAuth responses include server processing duration in Server-Timing">
  [OAuth authorization and discovery responses](/authentication) now append `api;dur=<milliseconds>` to `Server-Timing`. This measures server work until the response is ready, including protocol checks and response construction.

  Before this change, OAuth routes outside `/api` did not provide this processing duration. The measurement excludes network transit and body transfer; a cached discovery response carries the timing of the original response.

  **Backward compatible.** This adds a header measurement and does not change OAuth payloads, authorization, or caching.

  **What to change:** Nothing.
</Update>

<Update label="September 26, 2026" description="Web API mirrors expose origin response timing and a web request ID">
  Requests through the [web API mirror](https://0xinsider.com/api/v1/health) now append `web_api;dur=<milliseconds>` to `Server-Timing` and return `X-Web-Request-Id` for the web origin execution. Before this change, the mirror could forward backend timing without reporting its own authentication gates, upstream waiting, retries, and response construction.

  The existing `api` timing stays present. The web duration includes upstream waiting, so adding the two durations double-counts that work.

  The new clock excludes platform routing, cold module initialization, CDN delivery, and later stream or file transfer. On a cache hit, the headers describe the earlier origin fill; static Markdown API routes do not get a request clock. This adds observations, not a latency guarantee or a new timeout.

  **Backward compatible.** Response bodies, authorization, and cache policies are unchanged.

  **What to change:** Nothing. Read `web_api` separately from `api` when diagnosing requests through `0xinsider.com`; use `X-Web-Request-Id` to correlate that origin execution.
</Update>

<Update label="September 26, 2026" description="The bare 0xinsider npm package installs the product CLI 2.x">
  [`0xinsider@2.0.0`](https://www.npmjs.com/package/0xinsider) installs the product CLI from `@0xinsider/mcp@^2.10.1`, including browser login, account identity, market and trader reads, bounded pagination, and JSON output. Install it with `npm install --global 0xinsider@2`, or run `npx --yes 0xinsider@2 --help`.

  Before this change, `0xinsider@1.0.0` selected the older MCP-only runtime through `@0xinsider/mcp@^1.2.2`. Publishing a scoped 2.x version did not upgrade users of the bare package.

  **Breaking** for Node.js 18 and 20 users: version 2 requires Node.js 22 or newer. Existing MCP startup, `serve`, and `init` remain available; the scoped package also retains `0xinsider-mcp`. `--version` reports the installed runtime version.

  **What to change:** Upgrade Node.js and reinstall `0xinsider@2`, or keep `0xinsider@1` for the older MCP runtime. If you already installed `@0xinsider/mcp` globally, keep that package or uninstall it before switching, because both packages own the `0xinsider` command. See the [CLI guide](/integrations/cli).
</Update>

<Update label="September 26, 2026" description="MCP schemas document suspicion thresholds, watch severity, and accepted ID formats">
  The remote MCP endpoint at `https://api.0xinsider.com/api/v1/mcp` and `@0xinsider/mcp` 2.11.4 now state four things in the `inputSchema` an agent reads from `tools/list`, not only in the tool description:

  * `min_suspicion` on `get_suspicious_trades` and the deprecated `get_insider_radar` reads "Minimum suspicion score (0-100); the live floor of 60 also applies". A score under 60 is never returned, so `min_suspicion=10` and `min_suspicion=60` answer the same rows.
  * `severity` on the same two tools reads `"flag" selects live rows; "watch" returns none because no live watch policy exists`. `severity=watch` is accepted and answers an empty page, not an error.
  * `id` on `get_large_trade` and `get_whale_trade` reads "Large trade ID such as wt\_123 or 123", and on `get_suspicious_trade` and `get_insider_radar_flag` "Suspicious trade ID such as rf\_123 or 123". Each used to read "Entity ID returned by the matching list endpoint", so a client that renders only the schema never saw which prefix the tool takes.
  * `limit` on `list_games` reads "Max results (1-100, default 20)". It had no description.

  Before this change the remote served the first two as "Minimum suspicion score (0-100)" and "Filter by severity level", so a caller could read the 0 lower bound as reachable and `watch` as a filter that selects something.

  **Backward compatible.** Only advertised text changed: no tool, argument, default, bound or response moved, and both transports still serve 47 read-only tools. The floor and the empty `watch` page are the behavior `GET /api/v1/suspicious-trades` and `GET /api/v1/insider-radar` have always had, and [the OpenAPI document](https://0xinsider.com/api/v1/openapi.json) already stated the floor for the same REST parameter.

  **What to change:** Nothing. If you set `min_suspicion` below 60 expecting lower-scored rows, or treat `severity=watch` as a source of rows, read the floor instead of the bound.
</Update>

<Update label="September 26, 2026" description="The sandbox's events/feed/since meta describes each page">
  [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) on the sandbox at `https://0xinsider.com/sandbox` now builds `meta` from the page it returns, as the live API does. `meta.replay.to_cursor` equals `next_cursor` on every page, and `meta.completeness.status` is `caught_up` only on a page with no events.

  Before this change, every sandbox page carried the documented example of a caught-up page:

  * `meta.replay.from_cursor` and `meta.replay.to_cursor` read `ZWYyXzBfMA`, a cursor the sandbox does not accept, beside `next_cursor: "sbx_7"`.
  * `meta.replay.from_sequence`, `meta.replay.to_sequence`, and `meta.retention.retained_events` read `0` on pages that carried events.
  * `meta.completeness.status` read `caught_up` on pages that carried events.
  * `meta.replay.expand` read `[]` on a request with `expand=trade`.

  Now `from_cursor` is the cursor you sent, or `sbx_0` when you sent none, and `from_sequence` is the `sequence` of the event that cursor points at. `to_sequence` is the `sequence` of the page's last event, `retained_events` counts the page's events, and a page with events reads `complete`. The live API is unchanged.

  **Backward compatible.**

  **What to change:** Nothing. A client that reads `meta.replay.to_cursor` or `meta.completeness.status` to decide when to stop polling now gets the same answer from the sandbox as from the live API.
</Update>

<Update label="September 26, 2026" description="Unmatched trader lookups return 404 instead of echoing the input as an address">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader), [`GET /api/v1/trader/{address}/context`](/api-reference/endpoint/get-trader-context) and [`GET /api/v1/trader/{address}/context.md`](/api-reference/endpoint/get-trader-context-markdown) now answer `404` with `error.code` `not_found` and `error.param` `address` when the path value is not a wallet address and matches no username or trader ID the API knows. The `.md` route answers the same JSON error envelope, not a Markdown document.

  Before this change, those routes answered `200` with a trader whose `address` was the value you sent, whose `id` was `trd_` plus that value, and whose `sync_status` was `unknown`. For example, `GET /api/v1/trader/definitely-not-a-real-username-zzz9` returned `"address": "definitely-not-a-real-username-zzz9"`, which is not a wallet. [`GET /api/v1/trader/{address}/categories`](/api-reference/endpoint/get-trader-category-records) already answered `404` for the same value.

  A wallet address the API does not track yet is unchanged: it still returns `200` with `sync_status` `unknown` and that wallet as `address`. `@0xinsider/mcp` 2.11.3 says the same in `get_trader`.

  **Not backward compatible for one input class.** A lookup by username or trader ID that matches no trader moves from `200` to `404`. Every lookup that resolves, and every wallet-address lookup, returns what it did before.

  **What to change:** If you treated `data.sync_status == "unknown"` after a username or trader ID lookup as "not found", handle the `404` `not_found` instead, and stop polling that value; it will not start resolving on its own. Polling a wallet address needs no change.
</Update>

<Update label="September 26, 2026" description="The sandbox's events/feed/since sends next_cursor on its last page">
  [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) on the sandbox at `https://0xinsider.com/sandbox` now sends `next_cursor` on every page, as the live API does. On the last page it names the end of the sandbox's seven rows, and a request with that cursor returns `200` with `data: []`, `has_more: false`, and the same `next_cursor`.

  Before this change, the sandbox's last page left `next_cursor` out, although the API reference lists it as required and the live API always sends it. A request with a cursor at the end of the rows returned `400 bad_request` with `error.reason` `cursor_expired`.

  Every other paginated list on the sandbox answers as before: its last page has no `next_cursor`, or `next_cursor: null` where the reference marks the field nullable. The live API is unchanged.

  **Backward compatible.**

  **What to change:** Nothing. A client that stores `next_cursor` and polls the feed can now run that loop against the sandbox.
</Update>

<Update label="September 26, 2026" description="Response platform is typed polymarket, and coverage documents one status value">
  `@0xinsider/sdk` and the OpenAPI document now type the response field `platform` as `polymarket` rather than as any string. It covers twelve response fields, across games, the leaderboard, market search, the market explorer, market flow, sharp money flows, Pick of the Day with its two commitment payloads, and report snapshots. The seven that can be empty are typed `polymarket` or `null`.

  Before this change a generated client could not tell from the type that the API serves one venue. Nothing on the wire changes, and the `platform` query parameter still accepts `polymarket` and `all`.

  [`GET /api/v1/coverage`](/api-reference/endpoint/get-coverage) and its deprecated alias [`GET /api/v1/platforms`](/api-reference/endpoint/get-platforms) now document one capability status, `supported`, the only value the API has ever returned. Before, the document and the SDK offered `supported`, `partial` and `unsupported`. `@0xinsider/mcp` 2.11.2 says the same in `get_coverage` and `get_platforms`.

  **Backward compatible.** No response body changes: the types and the tool text narrow to what the API already sends.

  **What to change:** Nothing, unless your code assigns some other string to a `platform` field or to a capability status. TypeScript refuses that now.
</Update>

<Update label="September 26, 2026" description="Local and remote MCP tools publish the same descriptions and defaults">
  The next `@0xinsider/mcp` release, 2.11.0, advertises for every tool exactly what the remote MCP endpoint at `https://api.0xinsider.com/api/v1/mcp` advertises for the same tool. 35 of the 47 tools differed in 116 advertised fields: 19 tool descriptions, 80 argument descriptions, and 17 defaults the package's schema left out.

  The defaults an agent can now read from `tools/list` on the package, each one the value the REST route already applied to a request that omitted the argument:

  * `get_large_trades` and `get_whale_trades`: `suspicious_only=false`, `sort=recent`.
  * `get_large_trades_history` and `get_whale_trades_history`: `min_size=5000`, `suspicious_only=false`, `sort=recent`.
  * `get_suspicious_trades` and `get_insider_radar`: `mode=live`.
  * `get_positions`: `consistency=live`. `min_size` still advertises no default, because the route's default is 100, or 0 when `wallet` is present.
  * `get_sharp_money_flows` and `get_smart_money_flows`: `min_grade=B`, `platform=all`.

  44 arguments that the remote describes carried no description at all on the package, among them every filter on `get_large_trades_history` and `get_event_replay_since`. The deprecated aliases -- `get_whale_trades`, `get_whale_trade`, `get_whale_trades_history`, `get_market_intel`, `batch_get_market_intel`, `get_sports_edge_signals` and `get_sports_edge_observations` -- had been sharing the canonical tool's description and now carry the text the remote serves for them, naming their own deprecated path and envelope.

  **Backward compatible.** No tool was added or removed, no argument was added or removed, and no response changed; both transports still serve 47 read-only tools. A request that omitted one of those arguments now sends the value the route was already applying, so the same rows come back.

  **What to change:** Nothing. If you pinned prompts or tests to the package's older tool text, reread `tools/list`.
</Update>

<Update label="September 26, 2026" description="Invalid queries and bodies return structured errors on every API route">
  Every `/api/v1` route now returns the documented error envelope for an invalid query or body. Previously, these failures returned a `400` with a zero-byte body; the status stays `400`.

  This includes:

  * An unparseable query such as `limit=abc` on [`GET /api/v1/positions`](/api-reference/endpoint/get-positions), [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades), or [`GET /api/v1/leaderboard`](/api-reference/endpoint/get-leaderboard).
  * A JSON body that does not fit the schema, such as the old `{"identifiers": [...]}` body on [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders).

  Before this change, either failure answered `400` with `content-type: application/json` and an empty body, so a client had no `error.code`, `error.reason`, or `error.param` to read.

  It now answers `error.code` `bad_request` with `error.reason` `invalid_query` for the query case or `invalid_body` for the body case, and `error.param` naming the parameter or JSON path.

  * [`GET /api/v1/games`](/api-reference/endpoint/list-games) had its own gap: an unparseable `limit` already answered `400` with `error.code` `bad_request`, but with no `error.reason`. It now sends `error.reason` `invalid_query`, matching every other list route. This is the only response where the `error.code` a client already reads was correct before and after; the addition is `error.reason`.
  * [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) now sets `error.param` to `wallet` on the `404` it answers when a `wallet` value resolves to no trader. The API reference already documented this field; the server was omitting it.

  No documented response shape changed: the reference already promised `error.reason` and `error.param` on these routes, and these are server fixes that make the responses match it.

  **Backward compatible.** `error.code` and the `400`/`404` status are unchanged everywhere. A client that read an empty body as a generic failure keeps working; `error.reason` and `error.param` are new information, not a replacement for anything it read before.

  **What to change:** Nothing to keep working. To branch on the specific field or cause instead of treating every `400` the same, read `error.reason` (`invalid_query` or `invalid_body`) and `error.param`.
</Update>

<Update label="September 26, 2026" description="Batch traders answers an unknown username or trader id with not_found">
  [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) now answers an identifier that names no trader with a per-item `not_found` error with `error.param` `traders`, the error the TypeScript client has always documented. The item keeps its `index` and `input`, so you can tell which of the 25 failed, and `meta.failed_items` counts it.

  Before this change the item came back as `status: "ok"` with a trader object whose `address` and `id` were built from the string you sent, and `data_quality.status` was `unavailable`. A typo in a username read as a real wallet with no data.

  A wallet address is unchanged. An address this API does not track yet still returns `status: "ok"` with `sync_status: "unknown"`, because that address is real and may still be graded, so keep polling it. Only a username, a `trd_` id, or a numeric trader id that resolves to nothing is now `not_found`.

  This is the rule [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) adopted earlier the same day, where the same identifier answers `404 not_found` with `error.param` `address`.

  **Breaking** for clients that read every batch item as `status: "ok"` and took `data.address` as a wallet.

  **What to change:** Check `status` on each item before reading `data`, and treat `error.code` `not_found` as "no such trader" rather than retrying it.
</Update>

<Update label="September 26, 2026" description="A malformed condition_id on large positions now returns 400, not an empty list">
  [`GET /api/v1/large-positions`](/api-reference/endpoint/list-large-positions) now validates `condition_id` and returns `400` with `param: "condition_id"` when the value is not a market condition id. A valid value is `0x` followed by 64 hexadecimal characters, sent on its own or with the `mkt_` prefix this API accepts on any market id.

  Before this change any string was accepted and the response was `200` with an empty list, so a typo read as "this market has no large positions".

  An omitted, empty, or whitespace-only `condition_id` still means every market.

  **Breaking** for clients that send `condition_id` in any other shape. Every condition id this API returns already matches the accepted shape, so a request that matched positions before still matches them.

  **What to change:** Send `condition_id` as the API returns it, in `market.condition_id` or as the `mkt_` prefixed `market.id`. Handle `400` on the parameter instead of reading an empty list as "no positions".
</Update>

<Update label="September 26, 2026" description="Pick of the Day archive entries carry published_at and resolved_at">
  Every entry from [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) now carries `published_at`, the RFC 3339 time the pick became public, and `resolved_at`, the time its outcome was last written to a settled value. `resolved_at` is the same instant [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) already publishes, and it moves when a corrected market re-maps an already-settled pick.

  Before this change the only date on an entry was `pick_date`, the calendar day the pick belongs to in America/New\_York. That is a day and not a time, so a client that needed an instant had to invent one, and reading the bare date as UTC midnight lands the evening before the pick existed.

  * The earliest a pick can go out is 11:00 UTC on its `pick_date`. A day's last pick can go out at 23:00 ET, which falls on the next UTC date.
  * Both fields are left out when the time is unknown. `resolved_at` is absent for a pending pick and for a pick that settled before the time was recorded, so a missing value means unknown, never unsettled, and `outcome` is what answers that.
  * The [RSS feed](https://0xinsider.com/feed.xml) reads `published_at` for the same reason. Each Pick of the Day item is now stamped with the time the day's first pick went public, instead of 00:00 GMT of that day, and carries an `atom:updated` element that moves as later picks go out and outcomes settle. Item `guid` values are unchanged, so nothing is redelivered.

  **Backward compatible.** Both fields are additive and optional, and `pick_date` is unchanged.

  **What to change:** Read `published_at` instead of parsing `pick_date` as a timestamp. In the feed, compare `atom:updated` rather than `pubDate` to decide whether to re-read an item.
</Update>

<Update label="September 26, 2026" description="Market explorer smart_label names a lean only when graded money leans against the price">
  `smart_label` on [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) now follows the market page's price-relative rule. It names an outcome only when the graded money's share of that side diverges from the side's price-implied share by at least 10 points, with at least \$500 on the leaning side and outside the crowded-side guard.

  Before, it named whichever side held more graded money, so a 90% share on an outcome priced at 95 cents read as a lean even though the money only matched the price. `smart_label` is `null` when the money sits with the price, when there is no graded money, or when the price is unavailable. `smart_score`, `smart_count` and the dollar fields are unchanged.

  **Behavior change, same schema:** more rows carry `smart_label: null`, and a row's label now agrees with the market page for the same market.

  **What to change:** If you read `smart_label` as "the side with more graded money", derive that from the dollar fields or `smart_score` instead. Read `smart_label` as the lean against the price.
</Update>

<Update label="September 25, 2026" description="Pick of the Day display_category names competitions outside the curated list">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) now return the competition's name in `display_category` when the pick's competition is not on the curated list. The name comes from Polymarket's series title, with a trailing season year removed.

  For example, a UEFA Nations League pick now reads `UEFA Nations League` instead of `Soccer`, and ATP and WTA picks read `ATP` and `WTA` instead of `Tennis`. A curated label, such as `LaLiga`, still wins, and the sport name is returned only when Polymarket gives the event no series title.

  **Backward compatible.** The field keeps its type and meaning, a free-text label for the pick's competition, and `category` still carries the sport.

  **What to change:** If you map `display_category` to a fixed set of sport names, expect competition names too, and use `category` for the sport.
</Update>

<Update label="September 24, 2026" description="Pick of the Day carries up to 10 picks a day, and one sport can fill more of the day">
  A day of picks on [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now carries up to 10 ranked picks instead of 6. `pick_rank` and `pick_count` run from 1 to 10 in `picks`, `scheduled_picks`, the archive, and the proof routes, and the Pick of the Day MCP tool and the TypeScript client describe the same range.

  A day no longer stops at 4 picks from one sport, so a weekend with most qualifying markets in soccer can carry more soccer picks. A day still carries at most one pick per game, and a day with fewer qualifying markets still carries fewer picks.

  **Backward compatible** for clients that read `picks` as a list. Days published before this change keep their ranks.

  **What to change:** If you validate `pick_rank` or `pick_count` against a maximum of 6, or size a fixed array to 6 picks, raise it to 10.
</Update>

<Update label="September 24, 2026" description="OpenAPI adds five Pick of the Day fields, sort=large_trades, and a 500 on every route">
  The [OpenAPI document](https://0xinsider.com/api/v1/openapi.json), the SDK types generated from it, and `@0xinsider/mcp` 2.10.1 now describe fields and values the API already served:

  * [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day): `polymarket_url` and `game_ended` on each pick, `last_traded_at` (always sent, `null` when unknown) and `entry_value_usd` on each holder, and `logo_mark_dark` on each team in `sports_context`.
  * [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets): `sort=large_trades`, the canonical spelling of `whales`. The stdio `explore_markets` tool in `@0xinsider/mcp` 2.10.1 accepts it; 2.10.0 and earlier refused it.
  * Every operation lists its `500` response. [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) documents its `500` as an integrity refusal, when one entry fails a consistency check.
  * `next_cursor` on [`GET /api/v1/sports/pre-game-side-observations`](/api-reference/endpoint/get-pre-game-side-observations) and [`GET /api/v1/sports-edge-observations`](/api-reference/endpoint/get-sports-edge-observations) is typed `string | null`: both routes have always sent `null` on the last page.

  Before this change the document left these out, so a client generated from it dropped the fields and refused `large_trades`.

  **Backward compatible.** No response changed. A client generated from the old document that reads `next_cursor` as `string | undefined` should also accept `null`.

  **What to change:** Nothing. Regenerate your client from the document, or update `@0xinsider/mcp` to 2.10.1 once it is published, to read the new fields and send `sort=large_trades`.
</Update>

<Update label="September 24, 2026" description="Trader P&L totals use exact daily changes, including gains or losses below 1 cent">
  [`GET /api/v1/trader/{address}/pnl`](/api-reference/endpoint/get-trader-pnl) now builds `monthly[].pnl`, `year_totals` and its daily statistics from each day's exact P\&L change. Until now each day was rounded to the cent before it was summed, so a month or year total could drift from the change in the cumulative curve over the same period. Read on 2026-09-24 for one wallet, the 2026 total was \$0.05 short of the curve.

  Two values a client can see change with it:

  * `win_days` and `loss_days` now count a day that moved by less than a cent, which the rounding used to turn into a zero day. That agrees with the daily win rate shown on the wallet's profile.
  * `daily_change` is still returned to the cent, but it is now truncated from the exact value instead of rounded from it, the same way every other money field on the route is served.

  **Backward compatible.** No field is added, removed or retyped, and every money field still carries at most two decimal places. The MCP tools that read this route change the same way.

  **What to change:** Nothing. If you compared `monthly[].pnl` or `year_totals` with a sum of `daily_change` values, expect the totals to differ from that sum by up to a cent per day, because the totals now come from the exact values.
</Update>

<Update label="September 24, 2026" description="Candles reject a from or to sent in milliseconds instead of seconds">
  [`GET /api/v1/market/{condition_id}/candles`](/api-reference/endpoint/get-market-candles) now checks the magnitude of `from` and `to` before using either as a unix-seconds timestamp. A value above `100000000000` (the year 5138 in seconds) now returns `400` with `error.param` naming the offending parameter and a message saying the value must be seconds, not milliseconds.

  Before this change, a millisecond value that Rust's date library could still parse as a far-future timestamp was accepted silently. The most common way to trigger it was passing JavaScript's `Date.now()` (13 digits) where the endpoint expects unix seconds (10 digits): a millisecond `from` with no `to` returned an empty `200` instead of the requested range, and a millisecond `to` paired with a seconds `from` passed the endpoint's inverted-range check and was applied as no upper bound at all.

  **Backward compatible.** Every `from`/`to` value under `100000000000` (about the year 5138) is unaffected.

  **What to change:** Nothing, if you already send `from`/`to` in unix seconds. If your client sent milliseconds and relied on the previous behavior without an error, send seconds instead; the new `400` names which parameter is wrong.
</Update>

<Update label="September 24, 2026" description="Report periods that end before 2024-03-01 or start after tomorrow UTC return 400">
  [`GET /api/v1/reports`](/api-reference/endpoint/get-reports), [`GET /api/v1/reports/daily`](/api-reference/endpoint/get-daily-report-snapshot), [`GET /api/v1/reports/weekly`](/api-reference/endpoint/get-weekly-report-snapshot) and [`GET /api/v1/reports/monthly`](/api-reference/endpoint/get-monthly-report-snapshot) now check the requested period before reading any data. A period must end on or after 2024-03-01, the first day report data covers, and start no later than tomorrow UTC.

  Any other period returns `400` with `error.code` `bad_request` and an `error.param` naming the parameter you sent: `date`, `week`, `month` or `period`. For an explicit weekly `from`/`to` range, `error.param` is `to` when the range ends before 2024-03-01 and `from` when it starts after tomorrow UTC.

  Before this change these routes accepted any calendar date, so `date=9999-12-31` or `period=0001-01-01` returned `200` with an empty report.

  **Backward compatible** for every period that holds data. Every period that reaches 2024-03-01, through today, returns exactly what it did before, including ISO week 2024-09, which starts on 2024-02-26.

  **What to change:** Nothing, if you only ask for periods that have report data. If you request future dates or dates before March 2024 and expect an empty `200`, handle `400 bad_request` and read `error.param` instead.
</Update>

<Update label="September 24, 2026" description="List routes clamp limit values and report them in X-Effective-Query">
  [`GET /api/v1/games`](/api-reference/endpoint/list-games), [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since), [`GET /api/v1/large-trades/{id}/counterparties/executions`](/api-reference/endpoint/get-large-trade-counterparty-executions), and [`GET /api/v1/large-trades/{id}/counterparties/executions/{execution_id}/makers`](/api-reference/endpoint/get-large-trade-counterparty-makers) (and their deprecated `whale-trades` spellings) now clamp a `limit` outside 1 to 100 into that range, the way every other list route already did. Before this change, these four answered `400` with `error.param` `limit` for `limit=0` or `limit=500`, while `GET /api/v1/positions` and the rest returned 100 rows.

  * `limit=0` now returns 1 row and `limit=500` returns the route's maximum on every list route. The [pagination](/concepts/pagination) page holds each route's default and maximum.
  * A `limit` that is not a whole number still answers `400` with `error.reason` `invalid_query`.
  * The `X-Effective-Query` response header now reports the `limit` the page used. Before this change it echoed the value you sent, so `limit=500` appeared as `limit=500` beside a page of 100 rows.
  * `GET /api/v1/events/feed/since` accepts `expand[]` as an alias for `expand`, as the trader and market snapshot routes do. Before this change `expand[]=trade` was applied but reported in `X-Query-Ignored`, and `X-Query-Validation: strict` refused it.

  **Backward compatible.** A request with a `limit` inside the published range is unchanged. A client that relied on the `400` to detect its own out-of-range `limit` now gets a full page instead; read `X-Effective-Query` for the value applied.

  **What to change:** Nothing.
</Update>

<Update label="September 24, 2026" description="MCP tools take the parameters their REST routes take, and get_positions returns snapshot">
  `@0xinsider/mcp` 2.10.0 and the remote MCP server at `https://api.0xinsider.com/api/v1/mcp` now expose four things their REST routes already had:

  * `get_positions` returns the page's `snapshot` object (`as_of`, `expires_at`, `row_count`) when you call it with `consistency=snapshot`, the same object [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) returns. Before this change both transports left it out, so a client learned that the five-minute snapshot had ended only from a `cursor_expired` error on the next page.
  * `get_large_trades`, `get_large_trades_history`, and their deprecated `get_whale_trades` and `get_whale_trades_history` spellings take `min_market_volume_share` (a fraction from 0 to 1) and `sort` (`recent` or `market_volume_share`), the parameters [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades) and [`GET /api/v1/large-trades/history`](/api-reference/endpoint/get-large-trades-history) take.
  * `batch_get_traders` accepts `trust` in `expand`, as [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) does. Before this change the remote server refused it as an unknown value.
  * `get_trader_pnl` takes `from`, `to` (calendar dates in `YYYY-MM-DD` form), and `sections` (`entries`, `stats`, `monthly`, `year_totals`, `drawdown`), the parameters [`GET /api/v1/trader/{address}/pnl`](/api-reference/endpoint/get-trader-pnl) takes.

  No tool was added or removed; both transports serve the same 47 read-only tools.

  **Backward compatible.** Every new parameter is optional, and `snapshot` appears only on a `consistency=snapshot` page.

  **What to change:** Nothing. Update to `@0xinsider/mcp` 2.10.0 to use the new parameters over stdio; the remote server serves them as soon as this change is live.
</Update>

<Update label="September 24, 2026" description="OAuth client ID metadata document URLs are checked before they are fetched">
  A `client_id` that is a client ID metadata document URL (the [CIMD](/authentication) form: an `https` URL with a path, used by clients that skip `POST /oauth/register`) is now checked against the same host and port rules a webhook URL must satisfy before this backend fetches it: port 443 only, no embedded credentials, and no `localhost`, `.internal` name, or private-range address. A URL that fails is refused with no network attempt.

  Every refused fetch, for any reason, now answers the same fixed message. Before this change, the error text varied with the cause (a specific connect or DNS failure, or the fetched URL's HTTP status), which let a caller distinguish a name that resolves to a non-public address from a DNS failure, and probe whether an internal name exists, from the wording alone.

  `GET /api/oauth/authorize/preview` and `POST /api/oauth/authorize/decision` now count toward the same per-IP rate limit (`temporarily_unavailable`, 60 requests per minute) as `POST /oauth/register`, `POST /oauth/device/code`, `POST /oauth/token`, and `POST /oauth/revoke`. Before this change, these two routes took no admission at all, unlike every other OAuth endpoint.

  **Backward compatible.** A legitimate client ID metadata document, hosted on the default HTTPS port with a publicly routable host, is unaffected. A client integrating against `preview` or `decision` at a rate the shared OAuth limiter already allows for the rest of the flow is unaffected.

  **What to change:** Nothing, unless your client ID metadata document is served on a non-standard port or from a private address; move it to a publicly routable host on port 443. If your integration calls `preview` or `decision` at a high rate, it is now subject to the same 60-per-minute-per-IP limit as the rest of the authorization flow.
</Update>

<Update label="September 24, 2026" description="The OAuth consent page no longer redirects on its own after an error">
  The `/oauth/authorize` consent page no longer navigates to a client's `redirect_uri` on its own after an error. Before this change, a request that failed validation after the `client_id` and `redirect_uri` check (an unsupported `response_type`, a missing or malformed PKCE `code_challenge`, or a `scope` outside the client's registered ceiling) rendered the error and then, four seconds later, sent the signed-in visitor's browser to that `redirect_uri` with no click.

  No client registered through `POST /oauth/register` or a client ID metadata document is reviewed, so that redirect target was never a vetted destination.

  The page still shows a "Return to the application" link to the same URL; it is now clicked, never automatic.

  Error responses from `GET /api/oauth/authorize/preview` and `POST /api/oauth/authorize/decision` also stop echoing the request's raw `response_type` or `code_challenge_method` value into `error_description`; both now read a fixed message.

  **Backward compatible.** `redirect_to` is still present in the same refusal responses, and its value is unchanged. Nothing in the JSON contract changed shape.

  **What to change:** Nothing, for a standard OAuth 2.1 client following its own redirect on receiving `code` or `error` at its `redirect_uri`. A client that depended on the browser navigating there automatically from the 0xinsider-hosted consent page, without a user action, should not have; build the same redirect into your own client-side error handling instead.
</Update>

<Update label="September 24, 2026" description="A read-scoped credential reaches trader usernames that start with export">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) and the routes under it now answer an OAuth token or integration key that has only the `read` scope when `{address}` is a username that starts with `export`, such as `/api/v1/trader/exporter` or `/api/v1/trader/exporter/pnl`. Only `/api/v1/trader/{address}/export` and the routes under it need the `export` scope.

  Before this change, those requests answered `403` with `error.code` `insufficient_scope` and named `export` as the missing scope. The export snapshot and the export job routes still need `export`, as [Authentication](/authentication) lists, and the remote MCP tools follow the same rule.

  **Backward compatible.** Requests that were refused now succeed; no route needs a scope it did not need before.

  **What to change:** Nothing.
</Update>

<Update label="September 24, 2026" description="MCP get_positions omits min_size by default for complete wallet position lists">
  The [`get_positions`](/integrations/mcp) MCP tool in `@0xinsider/mcp` 2.9.1 sends `min_size` to [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) only when you set it. A call with only `wallet` now returns every one of the wallet's open positions, including those worth under \$100, the same result as the REST route with `wallet` and no `min_size`.

  On the remote server, the tool's input schema no longer advertises a `default` of 100 for `min_size`; its description states the route's rule instead: 100, or 0 when `wallet` is present.

  Before this change, the `@0xinsider/mcp` package filled `min_size` with 100 on every call and sent it, so a wallet-scoped call dropped every position under \$100 even though the tool's description said the floor was 0 with `wallet`. The remote server never filled the value itself, but a client that pre-fills schema defaults sent the same 100.

  **Backward compatible.** A call that sets `min_size` gets exactly what it asks for, as before. A call without `wallet` and without `min_size` still gets positions worth \$100 or more.

  **What to change:** Nothing. Update to `@0xinsider/mcp` 2.9.1 to get the wallet default of 0. If you relied on the package's old floor for a wallet call, send `min_size: 100`.
</Update>

<Update label="September 24, 2026" description="Market search reports a closed but unresolved market as closed">
  [`GET /api/v1/markets/search`](/api-reference/endpoint/search-markets) now returns `status` as `closed` for a market whose trading Polymarket has closed but which has not yet resolved, and `status=active` no longer returns that market. This is the rule [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) and [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot) already applied, so the three routes now agree on every market.

  Before this change, search called a market `active` until it resolved. A finished game stayed `active` on search for hours while explore and the snapshot called it `closed`, and `status=active` returned markets you could no longer trade.

  On all three routes, `status` is now:

  * `closed`: Polymarket has closed trading, or the market has resolved.
  * `active`: neither has happened.

  **Breaking** for a client that read `status=active` on market search as "not yet resolved". The enum, the parameters, and the response shape are unchanged.

  **What to change:** Nothing, if you use `status=active` to find markets you can still trade. If you relied on it to include markets awaiting resolution, request `status=all` or `status=closed` and read each row's `status`.
</Update>

<Update label="September 24, 2026" description="The stream marks a sequence numbering restart instead of going quiet">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) now sends one `event: resync` marker with `completeness.status` set to `reset` when the server's sequence numbering restarts while your connection is open, and then keeps sending frames. The marker's `id` is one below the first `seq` of the new numbering, `from_sequence` is the last `seq` you were sent, and `to_sequence` is the first of the new numbering.

  Before this change, a restart under an open connection sent no marker and no further frames, while the keep-alive comments continued, so the connection looked healthy but was silent. It stayed silent until the new numbering passed your last `seq`, which could take days. A reconnect always recovered, because a resume point past the current `seq` answers a `truncated` marker.

  The `completeness.status` values are now:

  * `truncated`: your resume point is older than the retained window.
  * `lagged`: the stream fell behind. A `lagged` marker is no longer followed by a second marker for the same loss.
  * `reset`: the sequence numbering restarted under your open connection.

  **Backward compatible.** The new status is additive, and the frame shape is unchanged.

  **What to change:** Treat a `reset` marker like any other `resync`: refetch current state, keep the connection open, and store the marker's `id` as your cursor. Expect the `id` values after it to be lower than the ones before it. A client that accepts only `truncated` and `lagged` as status values must accept `reset`.
</Update>

<Update label="September 24, 2026" description="Trader reads say when open-position P&L could not be read">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) and [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) now report the `positions` group of `data_quality` as `unavailable`, with a `reason` that names the failed read, when the open-position P\&L behind `pnl.unrealized` could not be read for that request. That body carries `meta.cached: false` and is not kept for later callers, so a retry reads again. With `expand=trust`, `trust.unrealized_pnl` carries the same reason in its `completeness.detail`.

  Before this change, a failed open-position read served `pnl.unrealized` as absent with the ordinary reason `open-position unrealized P&L is unavailable for this wallet`, which is also what a wallet with no open positions gets, and that body was served with `meta.cached: true` to every caller for up to 60 seconds.

  * The reason begins `the open-position read failed on this request`. Match on `status` and `group`, not on the sentence: reasons are prose and may be reworded.
  * Every other group is unchanged by the failure: `sync`, `ranking`, `leaderboard_rank` and `volume` keep their own verdicts, so top-level `data_quality.status` reads `partial`.
  * `max_age_s` on `GET /api/v1/trader/{address}` still refuses such a body, because its status is not `fresh`.

  **Backward compatible.** No field was added or removed. `reason` is an existing free-text field and `unavailable` an existing status.

  **What to change:** Nothing. If you read an absent `pnl.unrealized` as "no open positions", check `data_quality.field_groups[]` for `group: "positions"` first, and retry when its `status` is `unavailable` and `meta.cached` is `false`.
</Update>

<Update label="September 23, 2026" description="The forecasting axis covers nearly every graded wallet">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) now returns `forecast_score` and `forecast_evidence` for nearly every graded wallet that has a calibration record. Before this change, a wallet's forecast was written only when its grade was recomputed in the same hourly pass, so most wallets returned no forecast. A new day's ranking row now also keeps the previous score instead of returning none until the next pass.

  The fields and their meaning are unchanged: `forecast_score` is an independent forecasting percentile from 0 to 100, and `forecast_evidence` describes support from the observed record from 0 to 1. A wallet with no calibration record still returns no forecast.

  **Backward compatible.** Fewer responses omit the fields; nothing is removed or renamed.

  **What to change:** Nothing. If you treated a missing `forecast_score` as rare, expect it on almost every graded wallet now.
</Update>

<Update label="September 23, 2026" description="Python SDK adds an async client with bounded concurrency and resumable streams">
  `oxinsider.AsyncClient` (`0xinsider` 0.6.0 on PyPI) provides the synchronous `Client` operations as `async`/`await` methods using `httpx.AsyncClient`. `max_concurrency` limits simultaneous requests and defaults to 10.

  `AsyncClient.stream()` reads [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) resumably: it reconnects after a disconnect with `Last-Event-ID` set to the last frame's `id`, backs off exponentially between attempts, and raises `oxinsider.StreamClosedError` on a terminal `event: error` frame that says the credential was permanently refused instead of reconnecting.

  Before this change, an async caller needed a hand-built thread-pool wrapper around the synchronous `Client`, and a disconnected stream had to track its own resume cursor and reconnect logic. Cancelling a call (`asyncio.CancelledError`, an `asyncio.wait_for` timeout) now closes the in-flight connection and propagates uninterrupted, rather than leaking a thread.

  **Backward compatible.** The synchronous `Client` is unchanged; `AsyncClient` is a new, additive class.

  **What to change:** Nothing is required. Install `0xinsider>=0.6.0` and use `oxinsider.AsyncClient` in place of `Client` for an async caller.
</Update>

<Update label="September 23, 2026" description="One freshness check for data_quality in every SDK and MCP">
  The TypeScript, Python and Go SDKs add one helper that turns a response's `data_quality` into a decision against your own limit: `assessDataQuality` (`@0xinsider/sdk` 0.13.0), `assess_data_quality` (`0xinsider` 0.5.0 on PyPI) and `AssessDataQuality` (`0xinsider-go`). A group passes only when its `status` is `fresh`, it has an `as_of`, and that clock is within your limit. A group with `unknown` status fails, and `untracked` groups are left out and listed.

  The MCP tools [`get_positions`](/integrations/mcp), `get_large_trades` and the large-trade history tools now return the page's top-level `data_quality`. Before this change, the tool result dropped it. The trader tools already returned it inside the trader, and their descriptions now say what it answers. The [copy-trade recipe](/recipes/copy-trade) checks it before it uses a grade.

  **Backward compatible.** The helpers and the MCP field are additive.

  **What to change:** Nothing is required. Call the helper, or read `status` and `as_of` yourself, before you act on a body. Never treat `unknown` as recent.
</Update>

<Update label="September 23, 2026" description="Coverage gets its own route; /platforms becomes a deprecated alias">
  [`GET /api/v1/coverage`](/api-reference/endpoint/get-coverage) returns which V1 reads the API serves for Polymarket. It needs no key, and the body is the one `/api/v1/platforms` already returns. The SDK adds `getCoverage` (`@0xinsider/sdk` 0.13.0) and MCP adds `get_coverage` (`@0xinsider/mcp` 2.8.0).

  Before this change, the document was only at `/api/v1/platforms`. That path stays live with the same body and now answers with a `Deprecation` header and a `Link` header naming `/api/v1/coverage`. `getPlatforms` and `get_platforms` keep working.

  **Backward compatible.** The new route, method and tool are additive.

  **What to change:** Nothing is required now. Read `/api/v1/coverage`, `getCoverage` or `get_coverage` before the `/platforms` alias is retired under the deprecation policy.
</Update>

<Update label="September 23, 2026" description="Market flow gets canonical paths; the intel paths become deprecated aliases">
  [`GET /api/v1/market/{condition_id}/flow`](/api-reference/endpoint/get-market-flow) and [`POST /api/v1/markets/flow/batch`](/api-reference/endpoint/batch-get-market-flow) now return a market's flow and top positions, with `object` set to `market_flow` and `market_flow_batch` and the `MarketFlow` and `BatchMarketFlowItem` schemas. The SDK adds `getMarketFlow` and `batchGetMarketFlow` (`@0xinsider/sdk` 0.12.0), MCP adds `get_market_flow` and `batch_get_market_flow` (`@0xinsider/mcp` 2.7.0), and the CLI reads `0xinsider market flow <id>`.

  Before this change, the same data was only at `/intel` and `/markets/intel/batch`. Those paths stay live with the same parameters and the same body, with `object` still `market_intel` and `market_intel_batch`, and now answer with a `Deprecation` header and a `Link` header naming the successor path. The `get_market_intel` and `batch_get_market_intel` MCP tools keep working.

  **Backward compatible.** The new paths, methods and tools are additive; nothing existing is removed.

  **What to change:** Nothing is required now. Move reads to the `/flow` paths, or to `getMarketFlow` and `get_market_flow`, before the `/intel` aliases are retired under the deprecation policy.
</Update>

<Update label="September 23, 2026" description="Wallet positions offer a five-minute snapshot for repeatable pagination">
  [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) now accepts `consistency=snapshot` when you pass at least one `wallet`. It returns up to 500 positions and 2 MB from one ordered result for up to five minutes. The response adds `snapshot.as_of`, `snapshot.expires_at`, `snapshot.row_count`, and a cursor that requires the same filters on each page. A new first page replaces the API key's previous snapshot.

  Before this change, every page read live positions, so a price change could move a position across a page boundary. The default `consistency=live` behavior is unchanged. A snapshot freezes the positions this API returned; it does not prove that every provider position was available.

  **Backward compatible.** The new mode and response field are additive.

  **What to change:** Add `consistency=snapshot` and `wallet` for a repeatable multi-page read. Keep the same filters on each page. If the cursor expires or another first page replaces it, request the first page again. Narrow your filters or use the live mode if the snapshot exceeds either limit.
</Update>

<Update label="September 23, 2026" description="MCP get_positions adds wallet and consistency, matching the REST route">
  The [`get_positions`](/integrations/mcp) MCP tool, on both the remote server and `@0xinsider/mcp` 2.9.0, now accepts `wallet` (a wallet or a book of up to 25) and `consistency` (`live` or `snapshot`), the same parameters [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) already takes.

  Before this change, an MCP client could only read the global positions board and could not use `consistency=snapshot`; reaching a single wallet's book or a repeatable multi-page walk needed a direct REST call instead of the tool. `wallet` is passed as a repeated query parameter on both transports, matching the REST route's wire shape.

  **Backward compatible.** The new arguments are additive; a call that omits them behaves exactly as before.

  **What to change:** Nothing is required. Pass `wallet` and `consistency: "snapshot"` in a `get_positions` tool call to scope to a wallet or start a repeatable multi-page walk over MCP.
</Update>

<Update label="September 23, 2026" description="Large-trade subscriptions add individual payloads and filters">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) now emits `LargeTradeInsertedV2` with one trade's market, wallet, grade snapshot, size, side, price, and durable `event_id`. [`POST /api/v1/webhooks`](/api-reference/endpoint/create-webhook) can subscribe to `large_trade_inserted_v2` with `trade_filters` for `condition_id`, `wallet`, `min_grade`, and `min_size_usd`.

  Before this change, `WhaleTradesInserted` and `whale_trades_inserted` carried only a count, so clients had to fetch the trades after a pulse. Those count-only events still arrive unchanged. The stream also accepts `wallet` and `min_size` filters for individual trade frames.

  **Backward compatible.** The new events and filters are additive; existing subscriptions keep their behavior.

  **What to change:** Nothing for existing clients. Subscribe to the versioned event when you need each trade without a follow-up request. Use the SSE sequence for reconnects; use the payload's `event_id` for durable trade identity.
</Update>

<Update label="September 23, 2026" description="Trader grade at a past decision time">
  [`GET /api/v1/trader/{address}/grade-at`](/api-reference/endpoint/get-trader-grade-at) now takes `as_of` and returns a grade proven visible at that time. Before this change, the API offered only current grades; a historical backtest could not query the grade known at its decision time.

  * `status` is `graded`, `ungraded`, or `unknown`. Before the first recorded observation or during an unconfirmed transition, it is `unknown`.
  * `available_from` is the first confirmed observation for the wallet. `observation.observed_at` and `published_by` bound publication, and `previous_observation_id` links retained revisions.
  * `model_version`, `model_build_sha`, and `source_observed_by` are present when recorded. Older observations leave unproven fields `null`.

  **Backward compatible.** This is an additive route; current-grade responses are unchanged.

  **What to change:** Existing clients need no change. For backtests, pass the decision time as `as_of` and use `grade` only when `status` is `graded`.
</Update>

<Update label="September 23, 2026" description="Developer API adds named integration keys with scopes and isolated rotation">
  `POST /api/keys/integrations` now lets a signed-in Pro account create a named key with `read`, `webhooks`, `export`, or `usage` scopes and a 1 to 90 day expiry. Before this change, the account held one unrestricted developer key; rotating it immediately stopped every consumer that shared it.

  * `GET /api/keys/integrations` lists key metadata, expiry, last use, and monthly requests. It never returns a key's secret.
  * `POST /api/keys/integrations/{integration_id}/rotate` issues a new key for one integration and keeps its previous key valid for at most 15 minutes. `DELETE /api/keys/integrations/{integration_id}` revokes every key in that integration immediately.
  * A request outside an integration key's scopes returns `403 insufficient_scope` and names the needed scope. Existing default keys keep unrestricted access, and every credential uses the same account quota and subscription.

  **Backward compatible.** The new routes and key type are additive; existing keys and requests keep working.

  **What to change:** Create one scoped key per integration when you want isolated rotation. Continue using your existing key if you do not need it.
</Update>

<Update label="September 23, 2026" description="Game API markets now include provider moneyline prices and availability states">
  [`GET /api/v1/games`](/api-reference/endpoint/list-games) and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) now include `markets[].prices` when the board has classified a moneyline projection. Before this change, a game named its linked markets and token ids but a client needed a separate market snapshot to read a price.

  * `prices.provider.state` is `paired`, `incomplete` or `invalid`. A paired state carries prices bound to the two competitors, their identity binding and each leg's price provenance. Incomplete and invalid states carry a reason instead of a guessed pair.
  * `prices.observed_at` is the board cache vintage or the older CLOB leg clock, identified by `prices.observation_source`. It is `null` when no reliable clock exists; it is not a Gamma-authored timestamp.
  * Markets with no classified moneyline projection omit `prices`. Existing game and market fields keep their values and positions.

  **Backward compatible.** The `prices` block is additive, and existing fields are unchanged.

  **What to change:** Nothing for existing clients. Read `prices.provider.state` before using a price, and accept new provider reason values as the source vocabulary grows.
</Update>

<Update label="September 23, 2026" description="List reads add stored data_quality">
  [`GET /api/v1/positions`](/api-reference/endpoint/get-positions), [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades), [`GET /api/v1/large-trades/history`](/api-reference/endpoint/get-large-trades-history) and their `/api/v1/whale-trades` aliases now return top-level `data_quality` beside `data`. It reports the stored age and coverage of the fields in the page.

  Before this change, these list envelopes exposed `data`, pagination fields and `meta`, but no shared age and coverage summary.

  * Positions group their position, trader and market fields under `wallet_positions.last_reconciled_at|updated_at`, `traders.last_synced` and `market_canonical.last_refreshed_at`.
  * Large-trade pages group alert, trade, trader, ranking, market and volume fields. `whale_alerts.inserted_xid` is a transaction identifier, so the alert group is `unknown`.
  * The field is part of the response representation and `ETag`. `meta.cached` and `meta.cache_age_s` continue to describe transport cache state.

  **Backward compatible.** The field is additive and existing fields keep their names and values.

  **What to change:** Nothing. Read `data_quality.status`, `data_quality.as_of` and `data_quality.field_groups` when you need a page-level freshness decision.
</Update>

<Update label="September 23, 2026" description="Trader data quality dates rank and open-position P&L">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader), [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) and [`GET /api/v1/trader/{address}/context`](/api-reference/endpoint/get-trader-context) now carry real freshness times for the existing `data_quality` and `expand=trust` metadata. `leaderboard_rank` uses the latest completed global rank refresh, and `positions` uses the latest successful `/positions` snapshot or the last completed trader sync.

  Before this change, `leaderboard_rank` and `positions` returned `unknown` because the API did not carry a clock for either value.

  * `leaderboard_rank` dates the stored global rank with the completion time of the latest refresh that finished for every candidate.
  * `positions` dates open-position unrealized P\&L with the latest successful position snapshot and falls back to the last completed trader sync when no position snapshot exists. This clock does not date closed or native accounting values.

  **Backward compatible.** The response shape is unchanged, and the new timestamps make existing metadata more useful.

  **What to change:** Nothing. Clients that use freshness can read the new `as_of` values; clients that do not use them keep working.
</Update>

<Update label="September 23, 2026" description="Trader reads can require a freshness ceiling">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) accepts `max_age_s` and returns `409` with `error.reason=freshness_ceiling_unsatisfied` when the requested whole-response age cannot be met. `error.freshness` reports the requested ceiling and the measured age when available.

  Before this change, the endpoint could return `200` without a caller-supplied bound on the stored data's age.

  **Backward compatible.**

  **What to change:** Nothing.
</Update>

<Update label="September 23, 2026" description="The webhook event catalog lists all 14 subscribable types">
  [`GET /api/v1/webhooks/events`](/api-reference/endpoint/list-webhook-events) now lists `large_trades_inserted` and `trader_synced`, the successor spellings of `whale_trades_inserted` and `whale_trader_synced`, and the OpenAPI `WebhookEventType` enum includes them. The catalog now holds all 14 types, including the four `export_job_*` events.

  Before this change, [Create a webhook](/api-reference/endpoint/create-webhook) accepted both successor spellings, but the catalog and the enum omitted them, so an SDK type rejected a name the API accepts.

  **Backward compatible.** Existing subscriptions and deliveries are unchanged; each spelling receives the same events under the name it registered.

  **What to change:** Nothing required. Prefer `large_trades_inserted` and `trader_synced` in new subscriptions (`@0xinsider/sdk` 0.11.1 types them).
</Update>

<Update label="September 23, 2026" description="Sharp money names become available alongside their smart money aliases">
  "Sharp money" is the product term for graded money on a side. The API now spells every remaining "smart money" name that way, and keeps each old name on the wire with the same value.

  * Webhooks add the event type `sharp_money_flow_detected`. It is the same event as `smart_money_flow_detected`, which stays subscribable. An endpoint that registered the old name keeps receiving `type: smart_money_flow_detected`; a new endpoint should register the new one. Either spelling also works in the `?event=` filter on [`GET /api/v1/stream`](/api-reference/endpoint/get-stream).
  * [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) returns `sharp_wallet_count` and `sharp_usd` beside `smart_wallet_count` and `smart_usd`, with the same values.
  * [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) returns `score_components.sharp_money_signal` beside `score_components.smart_money_signal`, with the same value.
  * The OpenAPI spec marks `smart_money` on [market flow](/api-reference/endpoint/get-market-intel) and [ranked flows](/api-reference/endpoint/sharp-money-flows), and the four fields above, `deprecated`.
  * `@0xinsider/mcp` 2.6.0 lists `get_sharp_money_flows` first and keeps `get_smart_money_flows` as its deprecated alias, still calling the deprecated `/api/v1/markets/smart-money-flows` path. `get_market_intel`, `get_pick_of_the_day` and the `market_report` prompt say "sharp money". No tool was added or removed.
  * `@0xinsider/sdk` 0.11.0 adds `listSharpMoneyFlows`, the `SharpMoneyFlowDetectedEvent` envelope and `SharpMoneyFlowDetectedData`. `listSmartMoneyFlows`, `SmartMoneyFlowDetectedEvent` and `SmartMoneyFlowDetectedData` stay exported and are marked `@deprecated`.

  `/api/v1/markets/smart-money-flows` itself is unchanged. It has been a deprecated alias of `/api/v1/markets/sharp-money-flows` since July 5, 2026, and it still answers with `Deprecation` and a `Link` to its successor. No path is retired.

  **Backward compatible.** Every new name is additive, and no old name, field, or event type was removed or changed value.

  **What to change:** Nothing. Prefer the `sharp_` spellings in new code. If you switch on a webhook `type`, note that an existing subscription keeps delivering the spelling it registered, so nothing changes until you register the new one.
</Update>

<Update label="September 23, 2026" description="Export jobs can be cancelled; the job adds cancel_requested and cancelled">
  [`POST /api/v1/trader/{address}/export/cancel`](/api-reference/endpoint/cancel-trader-export) cancels an export job you own and answers the job resource. A queued job reads `cancelled` at once. A running job reads `cancel_requested` until the worker stops at its next safe point, then `cancelled`.

  Before this change, an export could not be stopped: a job ran to `ready` or `failed` and its file was built either way.

  * The job resource gains the `status` values `cancel_requested` (not terminal, `next_action` `poll`) and `cancelled` (terminal, `next_action` `resubmit`), and the nullable timestamps `cancel_requested_at` and `cancelled_at`. [`GET .../export/status`](/api-reference/endpoint/get-trader-export-status) and [`POST .../export`](/api-reference/endpoint/submit-trader-export) return them too.
  * A job whose file is already being published, or that is already `ready`, `reconcile_required`, `failed`, `expired`, or `cancelled`, comes back unchanged with `200`. A cancel never deletes a ready file.
  * Cancelling returns no quota: the submit keeps counting toward the daily and per-wallet hourly caps. A later submit never reuses a job you asked to cancel, so it queues a new one.
  * Webhooks add the owner-scoped event `export_job_cancelled` with `job_id`, `status`, `format`, and `next_action`.
  * `@0xinsider/sdk` 0.11.0 adds `cancelTraderExport(address, jobId)`, retries it on a network error or a `5xx` because a repeat is safe, and types the new statuses, fields, and event.
  * An unknown job, another account's job, or a job under another wallet's path answers `404`, the same as the status route.

  **Backward compatible.** Every field is additive, and the two new `status` values appear only on a job its owner cancelled.

  **What to change:** Nothing unless you cancel. If your code switches on `status`, treat `cancel_requested` like `running` and `cancelled` like `failed`.
</Update>

<Update label="September 23, 2026" description="Trader reads always return data_quality, and trust names the real owner of each clock">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader), [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) and [`GET /api/v1/trader/{address}/context`](/api-reference/endpoint/get-trader-context) now always return `data_quality`. It tells you how old a trader body is and which part of it is weak, without asking for `expand=trust`.

  Before this change, the only answer to "how old is this" was `expand=trust`, a per-field object about twenty entries long, and half of those entries reported freshness `unknown` even where a real timestamp existed.

  * `data_quality.status` is `fresh`, `partial`, `unknown`, `untracked` or `unavailable`. It reads `fresh` only when every group does, `unavailable` only when every group does, and `partial` otherwise.
  * `data_quality.as_of` is the oldest clock the body rests on. It is omitted when no group has one.
  * `data_quality.field_groups` holds one entry per group, each with `group`, `owner` (the table and column that writes it), `status`, an `as_of` where one exists, and a `reason` where the status is not `fresh`.
  * The five groups are `sync` (`traders.last_synced`: total and realized P\&L, markets traded, both win rates, `last_active`, `synced_at`, `sync_status`), `ranking` (`trader_rankings.computed_at`: `grade`, `score`, `streak_tier`, `forecast_score`, `forecast_evidence`), `leaderboard_rank` (`traders.leaderboard_rank`), `volume` (`trader_usd_volume.observed_at`: `stats.total_volume`) and `positions` (`trader_markets`: `pnl.unrealized`).
  * `leaderboard_rank` and `positions` always read `unknown` rather than carrying a time. That column and that aggregate store no write timestamp, so the API says it does not know instead of borrowing another group's clock.
  * Every value comes from a stored timestamp, so a cached body reports the same ages a freshly computed one does. `meta.cached` and `meta.cache_age_s` are still the only fields that describe the response itself.
  * Under `expand=trust`, `grade`, `score`, `streak_tier`, `forecast_score` and `forecast_evidence` now name `trader_rankings` as their `source.owner` and carry its `freshness.as_of`; they named `0xinsider_trader_read_model` and reported `unknown`. `total_volume` now carries the volume observation time. `rank` now names `traders.leaderboard_rank` and keeps freshness `unknown`.
  * `@0xinsider/sdk` 0.11.0 types `DataQuality` and `DataQualityGroup` and adds `data_quality` to `Trader`.

  **Backward compatible.** `data_quality` is a new field, and no field or value was removed. Under `expand=trust` some `source.owner` strings and `freshness` values changed, because they were wrong before.

  **What to change:** Nothing. Read `data_quality.status` and `data_quality.as_of` instead of `expand=trust` when you only need to know whether a body is current enough for you.
</Update>

<Update label="September 23, 2026" description="Large trades add a market-significance filter and sort">
  [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades), [`GET /api/v1/large-trades/history`](/api-reference/endpoint/get-large-trades-history) and their `/api/v1/whale-trades` aliases add two query parameters: `min_market_volume_share` and `sort`.

  Before this change, `market_volume_share` was returned on every large trade but nothing could order or narrow a page by it. A \$10,000 fill that is 0.03% of a presidential market and one that is 40% of a small esports market came back side by side, in the same order.

  * `min_market_volume_share` is a fraction, not a percent: `0.01` is one percent of the market's traded volume. Valid range is `0` to `1`; anything else answers `400` with `param: "min_market_volume_share"`. A non-zero value never returns a trade whose `market_volume_share` is absent, because an unavailable share cannot be said to clear a floor.
  * `sort` takes `recent` (the default, and the order every caller got before this) or `market_volume_share`, which ranks biggest-share first and puts a trade with no share last. An unrecognized value answers `400` with `param: "sort"`.
  * `sort=market_volume_share` reads a bounded window: the last 30 days on the list route, and `from` (or the last 30 days when `from` is omitted) on `/history`. The share is computed for each request against a `markets` volume observation, so an unbounded ranking cannot be served inside the API latency budget.
  * A `next_cursor` is bound to the order it was minted in. A cursor from `sort=recent` is refused by `sort=market_volume_share` and the other way round, with `400` and `param: "cursor"`, so a walk cannot silently cross rankings and skip or repeat rows.
  * `@0xinsider/sdk` types both parameters on `listLargeTrades`, `listLargeTradesHistory` and the two deprecated whale-trade operations.

  **Backward compatible.** Both parameters are additive and omitting them serves exactly what it served before: newest first, no share predicate.

  **What to change:** Nothing. To rank by how large a trade was for its own market, send `sort=market_volume_share`; to drop the trades that were rounding error in a deep market, send `min_market_volume_share`.
</Update>

<Update label="September 23, 2026" description="Trader and positions responses add exact decimal atoms">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) adds `pnl.exact.realized` and `stats.exact.total_volume`. [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) adds an `exact` object to each valued position. These atoms carry the full-precision decimal `value` plus `unit`, `scale`, and `basis`.

  The existing numeric fields keep their names and display-safe values. An exact block or member is omitted when its verified source is unavailable, so clients must handle absence rather than treating it as zero.

  **Backward compatible.** The new fields are additive.

  **What to change:** Use a decimal-safe type for `value` when exact arithmetic matters. Keep the numeric twins for display, and use `unit`, `scale`, and `basis` to interpret an atom.
</Update>

<Update label="September 23, 2026" description="The sandbox lists all 44 MCP tools and answers tools/call with a tool result">
  The [sandbox](/sandbox) at `https://0xinsider.com/sandbox` now answers `tools/list` on `POST /api/v1/mcp` with the same 44 tools the live API lists, each carrying the live `description`, `inputSchema`, `annotations` and `_meta`. `tools/call` now answers a tool result instead of that list. The live API is unchanged.

  Before this change, the sandbox answered both methods with the endpoint's documented example, which is a `tools/list` result holding 2 tools. A client could not read the catalog it would get, and every call came back as a list of tools.

  * A call that succeeds answers `content` (the tool's data as text) and `structuredContent` (the same data as an object), with `isError: false`. The data is the sandbox's own answer for the route the tool reads, projected the way the live server projects it: a list tool carries `data`, `has_more`, `next_cursor` and the route's page fields, a single-object tool carries the object's own fields, and both carry `meta`.
  * Arguments are checked against the `inputSchema` the tool advertises before anything runs. A wrong type, a value out of range, a value outside an enum, an unknown key, a missing required argument or an argument combination the schema forbids answers `isError: true`, with the text naming each field by its JSON Pointer inside `arguments` and `structuredContent.error` carrying `status`, `code` and `param`.
  * An unknown tool name answers JSON-RPC error `-32601`, and a `tools/call` with no `name` answers `-32602`, so a client tells those apart from a tool that ran and failed.
  * A call whose route answers an error gets that error as the tool error: the text names the status, `code` and `reason`, and `structuredContent.error` carries `code`, `reason`, `param`, `doc_url`, `retry_at` and `request_id` as the live server sends them.

  **Breaking** for a sandbox client that read the 2-tool example, or that expected a `tools/call` to answer a tool list. The live API is unchanged.

  **What to change:** Nothing if your client already works against the live API. A client built against the sandbox should read `result.tools` for the catalog and `result.content` or `result.structuredContent` for a call, and treat `isError: true` as a failed call.
</Update>

<Update label="September 23, 2026" description="Large trades return review_score beside signal_score; MCP adds market_review">
  [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades), [`GET /api/v1/large-trades/{id}`](/api-reference/endpoint/get-large-trade), [`GET /api/v1/large-trades/history`](/api-reference/endpoint/get-large-trades-history), their `/api/v1/whale-trades` aliases, and `expand=trade` on [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) now return `review_score` and `recorded_review_score`. They carry the same values as `signal_score` and `recorded_signal_score`.

  Before this change, the per-trade 0 to 1 score was named only `signal_score`, and the point-in-time copy only `recorded_signal_score`. The number ranks trades to read first. It does not predict how a market resolves, so the name now says what it does.

  * `signal_score` and `recorded_signal_score` stay in every response with unchanged values. The OpenAPI spec marks them `deprecated` and names the new keys.
  * `@0xinsider/sdk` 0.10.0 types `review_score` and `recorded_review_score` on `LargeTrade`, and tags every property the spec marks deprecated with `@deprecated`.
  * `@0xinsider/mcp` 2.5.0 adds the `market_review` prompt: "Review a market: profitable wallets' side, large trades, suspicious trades", with optional `market` and `category` arguments. `trading_signals` stays registered as its deprecated alias with the same arguments and messages. The `market_report` prompt now ends in a "Review summary". Prompts ship in the stdio package only; the remote MCP endpoint serves tools.

  **Backward compatible.** Every change is additive. No field, value, or prompt id was removed.

  **What to change:** Nothing now. New code should read `review_score` and `recorded_review_score`, and MCP clients should call `market_review` instead of `trading_signals`.
</Update>

<Update label="September 23, 2026" description="Export status adds immutable artifact manifests and checksums">
  [`GET /api/v1/trader/{address}/export/status`](/api-reference/endpoint/get-trader-export-status) now returns `artifact.artifact_id` and an immutable `artifact.manifest` for newly generated ready files. The manifest carries `format`, `schema_version`, `coverage`, the source generation and watermarks, `row_count`, exact `content_size_bytes` and `compressed_size_bytes`, and separate `content_sha256` and `compressed_sha256` values.

  Before this change, a ready artifact exposed only its storage `etag`, compressed size, content type and content encoding. The download URL stays temporary and is still returned by [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export).

  * `content_sha256` covers the decompressed bytes a client reads. `compressed_sha256` covers the stored gzip bytes. The multipart `etag` is not a checksum.
  * Historical artifacts can return `manifest: null` because they were written before this contract.
  * `@0xinsider/sdk` can read the status first and verify the streamed decompressed file with `downloadTraderExport(..., { verifyChecksum: true })`. A mismatch throws `ExportIntegrityError`.

  **Backward compatible.** The fields are additive, and existing download calls keep working.

  **What to change:** If you need file integrity, read the manifest and enable `verifyChecksum` in the TypeScript SDK, or compare the decompressed stream with `content_sha256` and `content_size_bytes` yourself. Treat a `null` manifest as unavailable verification and request a newly generated export when verification is required.
</Update>

<Update label="September 23, 2026" description="A large trade must now be at least 0.1% of its market's traded volume">
  A Polymarket fill is admitted to the large-trade record only when it clears the existing absolute floor **and** is at least 0.1% of its market's recorded traded volume. [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), the live feed and the MCP large-trade tools all read that one record, so all of them see the same rows.

  Before this change, eligibility was one fixed dollar amount: 10,000 USD, or 1,000 USD in an earnings market, priced below 0.97 (0.99 in earnings markets). Those bounds are unchanged and still apply first. A market's liquidity spans four orders of magnitude, so the same 10,000 USD fill was the whole story in one market and rounding error in another, and the record could not tell them apart.

  * The denominator is the market's recorded traded volume, which Polymarket reports as a cumulative one-side taker count in **shares**, and the numerator is the fill's own share count. Both sides are the same measure.
  * A market with no stored volume figure keeps the fill. The floor only ever withholds a fill a known denominator proves to be rounding error.
  * At 0.1% a fill can only be withheld by a market 1,000 times its size, so the rule cannot reach a thin market. Measured over the 14 days to September 23, 2026, it would have withheld 351 of the 6,499 admitted fills carrying a denominator (5.4%), every one of them in a politics or geopolitics market, and no sports or esports fill at all.
  * It is not the same number as the `market_volume_share` field on a trade, which is computed at read time against a volume observation that post-dates the fill and answers a different question.

  **Not backward compatible in what is returned, additive in shape.** No field changed, no field was added or removed, and no stored row was rewritten: rows captured before September 23, 2026 were not re-filtered. Going forward, fewer rows are captured.

  **What to change:** Nothing, unless you count large trades per market per day and compare across September 23, 2026. A window that crosses that date spans two capture rules, and the counts on either side are not comparable for very large markets. The endpoint descriptions and `/llms-full.txt` state the rule and the date.
</Update>

<Update label="September 23, 2026" description="The sandbox answers the MCP handshake the way the live API does">
  The [sandbox](/sandbox) at `https://0xinsider.com/sandbox` now answers `initialize` on `POST /api/v1/mcp` with the same result the live API returns: `serverInfo` (`name`, `title`, `version`), `capabilities`, `instructions`, and a `protocolVersion`. The answer carries `Mcp-Session-Id`. The live API is unchanged.

  Before this change, the sandbox answered `initialize` with the endpoint's documented example, which is a `tools/list` result. A client that read `result.protocolVersion` or `result.serverInfo` found neither, and the reference MCP SDK refuses that body when it connects.

  * `protocolVersion` is negotiated the way the live server negotiates it: a client that asks for `2025-11-25`, `2025-06-18`, `2025-03-26` or `2024-11-05` gets that revision back, and any other value, or none, gets `2025-11-25`.
  * `Mcp-Session-Id` echoes the session the request claimed, or `a0b1c2d3e4f5a6b7c8d9eafbecfda0b1` when it claimed none. The sandbox stores no session, and neither does the live API.
  * A `MCP-Protocol-Version` request header naming a revision outside those four now answers `400` with JSON-RPC error `-32600` and `X-Mcp-Error-Code`, on `POST` and `GET`, before the body is read. The sandbox served the request before.
  * The sandbox preflight now allows the `Mcp-Session-Id` and `MCP-Protocol-Version` request headers and exposes `Mcp-Session-Id`, so a browser client can complete the handshake.
  * `ping`, `tools/list` and `tools/call` still echo the request `id`, and carry `Mcp-Session-Id` when the request sent one.

  **Breaking** for a sandbox client that read the `tools/list` result `initialize` used to return, or that sent an unsupported `MCP-Protocol-Version` header. The live API is unchanged.

  **What to change:** Nothing if your client already works against the live API. A client built against the sandbox should read `serverInfo`, `capabilities` and `protocolVersion` from the `initialize` result, and send one of the four supported revisions in `MCP-Protocol-Version` or omit the header.
</Update>

<Update label="September 23, 2026" description="Export jobs can notify their owner when ready, failed or expired">
  [`POST /api/v1/webhooks`](/api-reference/endpoint/create-webhook) can now subscribe to `export_job_ready`, `export_job_failed`, and `export_job_expired`; [`GET /api/v1/webhooks/events`](/api-reference/endpoint/list-webhook-events) describes them. The events are delivered only to webhook endpoints owned by the API-key account that created the export.

  Before this change, an export client had to poll [`GET /api/v1/trader/{address}/export/status`](/api-reference/endpoint/get-trader-export-status) to learn whether its job was ready or had failed.

  * `export_job_ready` carries `job_id`, `status: "ready"`, `format`, `next_action: "download"`, `total_trades`, `processed_trades`, `file_size`, and `data_as_of`.
  * `export_job_failed` carries `job_id`, `status: "failed"`, `format`, `next_action: "resubmit"`, and `failure_reason`.
  * `export_job_expired` carries `job_id`, `status: "expired"`, `format`, and `next_action: "resubmit"`.
  * Each event has a stable event id for that job transition. No event contains a download URL. Use the authorized status and download routes with the same API key, and keep polling as the recovery path when delivery is unavailable.

  **Backward compatible.** The event types and payload fields are additive. Existing polling, download authorization, webhook signing, retries, and redelivery behavior stay the same.

  **What to change:** Subscribe to the event types you need, then fetch the job through the authorized export routes when a delivery arrives. Treat duplicate deliveries as safe to replay by event id.
</Update>

<Update label="September 23, 2026" description="Trader profiles publish a capital-normalized forecasting axis">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) now returns `forecast_score` and `forecast_evidence` beside `grade` when forecasting data is available. `forecast_score` is a 0-100 score and `forecast_evidence` is a 0-1 value.

  Before this change, the response exposed the grade but no separate forecasting axis.

  **Backward compatible.** The fields are additive and omitted when the forecasting signal is unavailable. Existing response fields keep their names and meanings.

  **What to change:** Nothing. Read the fields when present and treat omitted values as unavailable.
</Update>

<Update label="September 23, 2026" description="Sandbox responses align with MCP errors, grades, holders, batches, and webhooks">
  The [sandbox](/sandbox) at `https://0xinsider.com/sandbox` now answers six more requests the way the live API does. The live API is unchanged.

  * `POST /api/v1/mcp` answers in the MCP transport's own shapes. A malformed message (unparseable JSON, not an object, a bad `id`, a missing or non-string `jsonrpc` or `method`, `jsonrpc` other than `2.0`, a notification with an `id`, a body over 1 MiB) is `400` with a JSON-RPC error object and `X-Mcp-Error-Code` (`-32700` or `-32600`). An unknown method with an `id` is `200` with a `-32601` error. An id-less `ping`, `notifications/initialized` or `notifications/cancelled` is `202` with no body. `Content-Type` is not checked, and a response's `id` echoes the request's. Before this change, the sandbox refused these with the V1 error envelope (`invalid_body`, `415`, `413`) and answered id-less notifications with `200`.
  * Grade fields carry each sample wallet's own grade (`S`, `A`, `A`, `B`, `B`, `C`, `D` across the seven rows), so `min_grade` drops rows the way it does live: `GET /api/v1/positions?min_grade=A` returns 3 rows. Before, every enumerated grade read `S` and `min_grade` dropped nothing.
  * [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders) lists only S, A and B holders when `min_grade` is omitted, as the live roster does. Before, it listed C and D wallets as well.
  * [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders): a sandbox wallet (`0x51ab...`), whether posted by address, `trd_` id or username, returns its own profile and username. Any other address returns no `username`, and any other username gets its own address. Before, a posted address carried another sandbox wallet's username. `"expand": null` now gets `400 invalid_body` with `error.param` `expand`, as it does live.
  * `GET /api/v1/market/{condition_id}/snapshot`, `/intel`, `/candles`, `/holders` and `/context.md` describe the `condition_id` in the path (`mkt_` prefix stripped), in `market.condition_id`, `market.id`, `data.condition_id` and each holder's `pos_` id. Before, they described the documented sample market whatever id you asked for.
  * `GET /api/v1/webhooks/events` gives all eight event types the description, `payload_shape` and `status` the live API serves. Before, seven read "Sandbox example value." and "sandbox".

  **Not backward compatible for sandbox callers.** A sandbox MCP client that read the V1 envelope on a malformed message now gets a JSON-RPC error object, and sandbox bodies change as described. The live API is unchanged.

  **What to change:** Nothing if your client already works against the live API. An MCP client should read `error.code` from the JSON-RPC object, or `X-Mcp-Error-Code`, on a `400`.
</Update>

<Update label="September 23, 2026" description="Market intel names the age of the oldest position behind top_positions">
  [`GET /api/v1/market/{condition_id}/intel`](/api-reference/endpoint/get-market-intel) and [`POST /api/v1/markets/intel/batch`](/api-reference/endpoint/batch-get-market-intel) now return `sharp_money.oldest_snapshot_as_of`, and the same field on the deprecated `smart_money` alias. It is the timestamp of the oldest position behind `top_positions`.

  Before this change, the response said nothing about how current `top_positions` was. The list is a stored read of tracked wallets' positions, not a live one, and tracked wallets refresh on different schedules, so a roster carried by a slowly refreshed wallet could be hours old and looked identical to one built a minute ago.

  * The value is the oldest refresh time among the positions the body actually returns, after the top-5 selection, so the whole list is at least that current and no more.
  * It is `null` when `top_positions` is empty or no returned position has a recorded refresh time.
  * It does not date `net_flow_usd`, `buy_volume_usd`, `sell_volume_usd` or the trade counts. Those come from large-trade event times.

  **Backward compatible.** The field is additive and present on both `sharp_money` and the deprecated `smart_money` alias. No existing field changed.

  **What to change:** Nothing. To show or check freshness, read `sharp_money.oldest_snapshot_as_of` and treat `null` as unknown rather than current. `@0xinsider/sdk` types it from 0.9.0.
</Update>

<Update label="September 23, 2026" description="The sandbox aligns 12 more request checks and response shapes with the live API">
  The [sandbox](/sandbox) at `https://0xinsider.com/sandbox` now matches the live API on request checks it skipped and on response details it got wrong. The live API is unchanged.

  Requests:

  * A body over 1 MiB answers `413` with `error.reason` `payload_too_large` and `error.param` `body`. Before this change, the sandbox read the whole body and answered `200` or `400`.
  * `null` is refused where the schema does not allow it: a body of `null`, or `{"traders": null}`, on [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) answers `400 invalid_body`. An optional field the live API reads as absent, such as `name` on `PATCH /api/v1/webhooks/{id}`, still accepts `null`.
  * Body fields are held to their `const`, length, pattern, bound and `format` rules: `jsonrpc: "1.0"` on `POST /api/v1/mcp`, or a webhook `name` over 100 characters, answers `400 invalid_body` naming the field.
  * Every occurrence of a repeated query name is checked, and `wallet` and `wallet[]` count together: 26 `wallet` values on `GET /api/v1/positions` answer `400` naming `wallet`, and `sections=stats&sections=bogus` answers `400`. Before, only the first occurrence was checked.
  * A date-time query value is checked: `from=not-a-date` on `GET /api/v1/whale-trades/history` answers `400 invalid_query`.
  * An unknown `expand` value is ignored, on the query and in the trader batch body, as the live API ignores it. Before, the sandbox answered `400`.
  * A `mkt_`, `wt_` or `rf_` value posted to `POST /api/v1/traders/batch` is a per-item error with `error.param` `address`, not a synthetic trader.

  Responses:

  * Ids take their live forms: `pos_<wallet>:<condition_id>:<outcome_index>` for positions and holders, `wt_<n>` for large trades, `rf_<n>` for radar flags, `pe_<n>` for position-timeline events, an opaque base64 id for replay events, and `mkt_<condition_id>` for every market. Before, most rows read `sbx_01` and markets read a bare condition id.
  * [`POST /api/v1/markets/intel/batch`](/api-reference/endpoint/batch-get-market-intel) returns the posted condition id in `data.market.condition_id` and, prefixed, in `data.market.id`. Before, every item described the same sample market.
  * Opt-in sections follow `expand`: a trader from `POST /api/v1/traders/batch` or `GET /api/v1/trader/{address}` carries `strategy`, `category_strengths`, `category_records`, `category_skill_model`, `quant_metrics` and `trust` only when its expansion is asked for; the market snapshot carries `trust` only with `expand=trust`; a replay event carries `trade` only with `expand=trade`.
  * `GET /api/v1/webhooks/events` lists each of the eight event types once, with `has_more: false` and no `next_cursor` or `total`. Before, it repeated one event type seven times beside `total: 7`. `GET /api/v1/webhooks` and `GET /api/v1/content/search` also answer one page.
  * `min_grade` drops lower-graded rows before the page is cut, an enumerated filter whose value a row carries (`strategy`, `side`, `timeframe`, `severity`, `cohort`, `status`) sets that value on every row, and `X-Effective-Query` names only the parameters the sandbox applied. Before, `?strategy=market_maker` was reported as applied beside rows of every strategy.

  **Not backward compatible for sandbox callers.** A sandbox request that sent one of the shapes above now gets the error the live API returns, sandbox ids change form, and a trader body read without `expand` no longer carries the opt-in sections. The live API is unchanged.

  **What to change:** Nothing if your client already works against the live API. If a sandbox call starts returning `400` or `413`, read `error.param` and `error.reason`: the live API refuses the same request. Pass `expand` for every trader section you read, and treat sandbox ids as opaque beyond their prefix.
</Update>

<Update label="September 23, 2026" description="Games: both sides, schedules and linked Polymarket markets in one response">
  Two new operations return a game instead of a market. [`GET /api/v1/games`](/api-reference/endpoint/list-games) lists what 0xinsider covers and [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game) reads one of them. Before this, building a game view meant calling `markets/search`, then `market/{condition_id}/snapshot` per market, then matching team-name strings to decide which markets belonged to the same fixture.

  Each game carries:

  * `event_slug`, the identity. It is the same key the `live_sports_updated` webhook pulse carries, so a receiver can read the full game straight from a pulse. `game_id` is the provider's Gamma `gameId`, and it is absent when the canonical owner has no single value for the slug rather than guessed.
  * `competitors`, both sides in the provider's own order, each with `provider_id`, `logo`, `score` and `record`. For a team league the provider lists the home side first. `coverage.competitors` reads `provider_ids`, `labels` or `unavailable`, so you never have to guess whether joining on a name is safe.
  * `markets`, every linked Polymarket market with its `condition_id`, `slug`, `sports_market_type`, `outcome_yes`, `outcome_no` and `outcome_token_ids` in the provider's own index order. `side` says which of `home`, `away`, `draw` or `other` the YES leg pays, and `draw_offered` says whether the game has a draw leg at all.
  * `status.state`, one of `scheduled`, `live`, `paused`, `ended`, `postponed`, `cancelled`, `suspended`, `delayed` or `unknown`. A postponement, a cancellation and a suspension keep their own state instead of folding into "not live", and a kickoff in the past is never read as live on its own.
  * `freshness` and `coverage`, per source. `freshness.delayed` applies the board half's own servable-age bar (15 seconds live, 120 seconds upcoming), which is not the same as `freshness.source_freshness`.
  * `series_format` for esports, for example `Bo3`.

  Filters are `sport`, `league`, `status`, `starts_after` and `starts_before`, with `limit` (1 to 100, default 20) and an opaque `gms_v1_` `cursor`. The list is ordered by kickoff, then by `event_slug`, with games the provider has published no kickoff for last.

  A `sport` or `status` outside the published vocabulary returns an empty page, never a `400`. Every page carries a top-level `coverage` naming the sports and leagues this deployment serves and any scope whose source was unavailable for that read, so an empty page is never ambiguous.

  Both operations return a weak semantic `ETag`; send `If-None-Match` for a `304`. `request_id`, `cost` and `as_of` are excluded from the validator.

  The response carries no price, no sharp money split and no holder identity. Those stay on [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot), [`GET /api/v1/markets/sharp-money-flows`](/api-reference/endpoint/sharp-money-flows) and [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders) under their own access rules.

  **Additive.** Nothing existing changed. `@0xinsider/sdk` 0.7.0 adds `client.listGames(params)` and `client.getGame(eventSlug)` and the `Game` type.

  **What to change:** Nothing. If you assemble games from `markets/search` and team names today, these two operations replace that walk.
</Update>

<Update label="September 23, 2026" description="SDK and CLI add keyless MCP setup, export cancellation, and safer config writes">
  * `@0xinsider/sdk`: `mcp()` sends `initialize`, `ping`, `tools/list` and `notifications/*` without an API key, as `POST /api/v1/mcp` allows; `tools/call` still needs one. Before, every `mcp()` call without a key failed locally. `mcp()` also always sends `jsonrpc: "2.0"`, even when a request passes `jsonrpc: undefined`, which before produced a `400`.
  * `@0xinsider/sdk`: in a runtime without `AbortSignal.any`, `downloadTraderExport` keeps `signal` and `downloadTimeoutMs` connected until the body is read, closed or cancelled. Before, they stopped reaching the transfer once the headers arrived.
  * `@0xinsider/mcp init`: a config path that is a dangling symlink is written at its target and stays a link, instead of being replaced by a regular file. An edit to `~/.claude.json` is merged again if Claude Code saved the file while `init` was running, instead of overwriting that save.

  **Backward compatible.** Calls that worked before behave the same.

  **What to change:** Nothing. To run the MCP discovery handshake before you have a key, call `client.mcp({ method: "initialize", ... })` with no key configured.
</Update>

<Update label="September 23, 2026" description="Export jobs report lifecycle details; expired downloads return 410">
  [`POST /api/v1/trader/{address}/export`](/api-reference/endpoint/submit-trader-export) and [`GET /api/v1/trader/{address}/export/status`](/api-reference/endpoint/get-trader-export-status) return these new job fields:

  * `terminal` says whether the job can no longer change.
  * `next_action` tells you to `poll`, `download`, or `resubmit`; `poll_after_s` provides polling timing.
  * `created_at`, `started_at`, `ready_at`, `failed_at`, and `expired_at` record lifecycle times.
  * `expires_at` is the download deadline, 24 hours after submission.
  * `data_as_of` identifies the time represented by the file, matching its `export_metadata`.
  * `attempt` and `max_attempts` report attempt counts.
  * Once ready, `artifact` includes `etag`, `compressed_size_bytes`, `content_type`, and `content_encoding`.

  `status` gains `expired`: a ready job past `expires_at` becomes `expired`, while a queued or reconciling job becomes `failed`. Failed and expired jobs remain readable for 48 hours, then return 404.

  The file's `export_metadata` also names its `job_id` and `expires_at`.

  [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export) answers `410` once the retention window has passed, with `error.code` `not_found` and the new `error.reason` `export_expired`; submit a new export. The submit route takes `fresh=true` to skip reusing a finished, running or reconciling job (only a queued one is reused); the default keeps today's reuse.

  Additive: every field, reason and parameter is new; `status` values a client does not know should be treated as terminal only when `terminal` is `true`. A client that retried a download on 404 after expiry now gets 410 and should resubmit.
</Update>

<Update label="September 23, 2026" description="Large trades use /api/v1/large-trades paths and retain whale-trade aliases">
  The large-trade feed now has canonical paths: [`GET /api/v1/large-trades`](/api-reference/endpoint/get-large-trades), [`/api/v1/large-trades/{id}`](/api-reference/endpoint/get-large-trade), [`/api/v1/large-trades/history`](/api-reference/endpoint/get-large-trades-history), and the two counterparties reads under `/api/v1/large-trades/{id}`. Their rows use the `LargeTrade` schema.

  Before this change, the only spelling was `whale`. The `/api/v1/whale-trades` paths still answer the same bodies, and each response now carries `Deprecation` and a `Link` header naming its `large-trades` successor.

  * Responses that named a whale field carry the `large` spelling beside it, with the same value: `top_large_trades`, `total_large_trades` and `total_large_trade_volume` on reports; `rep_large_trades`, `large_trade_count`, `large_trade_distinct_wallets`, `large_trade_total_usd`, `large_trade_last_at` and `large_trade_signal` on `GET /api/v1/markets/explore`; `large_trades` in platform capabilities.
  * Webhooks and the event stream accept `large_trades_inserted` and `LargeTradesInserted` wherever you name an event. A webhook endpoint registered under `large_trades_inserted` receives that `type`; an endpoint registered under `whale_trades_inserted` keeps receiving the original. SSE frames keep the original `WhaleTradesInserted` name.
  * `@0xinsider/sdk` 0.6.0 adds `listLargeTrades`, `getLargeTrade` and `listLargeTradeHistory`. The whale methods stay and are marked deprecated.
  * `@0xinsider/mcp` 2.2.0 and the remote MCP server add `get_large_trades`, `get_large_trade` and `get_large_trades_history`; the CLI adds `large-trades list`. The whale tools and `whales list` stay as deprecated aliases.

  **Backward compatible.** Every whale path, field, event name, SDK method and MCP tool still works with the same values.

  **What to change:** Nothing now. In new code, call the `large-trades` paths and read the `large` fields, and plan to move existing calls before a future version removes the whale spelling.
</Update>

<Update label="September 23, 2026" description="Insider Radar is now Suspicious trades, and the old routes stay live as aliases">
  [`GET /api/v1/suspicious-trades`](/api-reference/endpoint/get-suspicious-trades) and [`GET /api/v1/suspicious-trades/{id}`](/api-reference/endpoint/get-suspicious-trade) are the canonical reads for flagged Polymarket trades, with the same parameters, the same rows, the same `rf_` ids, and the same cursors as before. Before this change, the only routes were `GET /api/v1/insider-radar` and `GET /api/v1/insider-radar/{id}`.

  * The two `insider-radar` routes stay live as deprecated aliases with no retirement date. Every response on them now carries `Deprecation: @1790121600`, which is 2026-09-23T00:00:00Z, and a `Link` header naming the canonical route with `rel="successor-version"` and the versioning policy with `rel="deprecation"`. No `Sunset` header is sent. The alias for one id links to the canonical route for that same id.
  * The new detail read answers `object: "suspicious_trade"`. [`GET /api/v1/insider-radar/{id}`](/api-reference/endpoint/get-insider-radar-flag) keeps answering `object: "radar_flag"`, so a client that branches on the envelope is unaffected on the old path. Both list routes answer `object: "list"`.
  * The OpenAPI component schema `RadarFlag` is renamed to `SuspiciousTrade`. Every wire field is identical.
  * [`GET /api/v1/webhooks/events`](/api-reference/endpoint/list-webhook-events) adds `suspicious_trade_flagged`, the canonical spelling of `insider_radar_flag_raised`, which stays. They are one event under two spellings: either one subscribes, listing both on one endpoint stores one, and an endpoint receives every delivery under the spelling it registered, in the `x-0xinsider-event-type` header and in the payload's `type`. A subscription made before today keeps receiving `insider_radar_flag_raised`, and the payload fields are identical.
  * [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) accepts `suspicious_trade_flagged` in the `event` filter as well as `insider_radar_flag_raised`. The frame itself still carries `type: insider_radar_flag_raised`.
  * [`GET /api/v1/platforms`](/api-reference/endpoint/get-platforms) adds `platforms.polymarket.suspicious_trades` beside `insider_radar`, holding the same value.
  * MCP adds the tools `get_suspicious_trades` and `get_suspicious_trade`. `get_insider_radar` and `get_insider_radar_flag` stay as deprecated aliases that call the deprecated routes, so `tools/list` returns 37 tools.
  * The next release of `@0xinsider/sdk` adds `listSuspiciousTrades()`, `getSuspiciousTrade()`, and the types `SuspiciousTrade`, `SuspiciousTradesListParams`, `SuspiciousTradeFlaggedData` and `SuspiciousTradeFlaggedEvent`. `listInsiderRadar()`, `getInsiderRadarFlag()`, `InsiderRadarListParams`, `InsiderRadarFlagRaisedData`, `InsiderRadarFlagRaisedEvent` and `RadarFlag` all remain as deprecated aliases, so no import breaks, and `InsiderRadarFlagRaisedEvent` keeps its own `"insider_radar_flag_raised"` discriminant.

  **Backward compatible.** Everything here is added. No route, field, id, cursor, envelope or event spelling was removed or changed.

  **What to change:** Nothing. Move to the `suspicious-trades` paths when it suits you, because the `insider-radar` paths now answer with a `Deprecation` header.
</Update>

<Update label="September 23, 2026" description="The live_sports_updated webhook now delivers a bounded pulse for each live game">
  The `live_sports_updated` webhook event now sends deliveries. Subscribe to it on [`POST /api/v1/webhooks`](/api-reference/endpoint/create-webhook) and each live game sends one pulse when its `scores`, `status`, `period`, `live`, or `ended` state moves. [`GET /api/v1/webhooks/events`](/api-reference/endpoint/list-webhook-events) now lists it as `active`. It needs no Pro subscription.

  Before this change, the event was in the catalog with `status` `dormant`: you could subscribe to it and it sent nothing.

  Two rules bound the volume, and [Live game pulses](/guides/webhooks#live-game-pulses) has the full contract:

  * An ordinary change waits out 20 seconds since that game's last pulse, then goes out on the first frame after that. A game going live or final ignores the wait, and so does a game's first pulse.
  * A game that stops sending frames stops sending pulses. The frame that ends a game is exempt from the wait, so a finished game always sends its last pulse.
  * A change inside those 20 seconds is not dropped. The next pulse lists it in `changed` and carries the current state, so you see every field that moved, but not every value it passed through.
  * The game clock moving on its own never sends a pulse. `clock` still rides every pulse.

  `data` carries `event_slug`, `game_id`, `league`, `version`, `changed`, `observed_at`, `published_at`, `status`, `period`, `clock`, `live`, `ended`, `scores`, `series_format`, and `snapshot_url`. `version` is a counter for that game that goes up by one per pulse: deliveries are not ordered, so drop a pulse whose `version` you already have. `snapshot_url` is the game's page, to read after a gap instead of replaying pulses.

  **Backward compatible.** **What to change:** Nothing, unless you want the event. A client that already subscribed to `live_sports_updated` starts receiving deliveries, so make sure its handler reads the payload rather than assuming an empty one. `@0xinsider/sdk` 0.5.1 types the payload as `LiveSportsUpdatedData` and `parseWebhookEvent` narrows it on `type`.
</Update>

<Update label="September 23, 2026" description="TypeScript SDK: a terminal stream error frame throws instead of arriving as an event">
  When `GET /api/v1/stream` ends with an `event: error` frame, `@0xinsider/sdk` now throws the typed error the frame carries, such as `SubscriptionRequiredError` for a lapsed account or `InvalidApiKeyError` for a revoked key. The frame is never passed to `onEvent`, and it does not move `cursor.seq` or a checkpoint. A frame with `retry: false` is permanent, so `streamFeedResilient` and `consumeStreamCheckpointed` do not reconnect; `retry: true` follows the normal reconnect path.

  Before this change, the SDK delivered the frame as a feed event with `type: "error"` and then ended the stream normally, so `consumeStream` resolved without an error.

  **Backward compatible for normal frames.** Feed events and resync markers are unchanged.

  **What to change:** Catch `OxinsiderApiError` around your stream consumer and stop on `SubscriptionRequiredError` or `InvalidApiKeyError`. Remove any handler code that checked for `envelope.type === "error"`.
</Update>

<Update label="September 23, 2026" description="MCP: two tools now match their routes">
  Two MCP tools now advertise and return what their routes do.

  * The stdio server's `get_sports_edge_signals` returns the route's `meta`, including `directional_source` (`degraded` means the ranking fell back to raw conviction) and `request_id`. Before, the stdio tool dropped `meta`; the remote server already carried it.
  * `get_leaderboard` lists the ten `strategy` values (`accumulator`, `algo_trader`, `arbitrageur`, `directional`, `event_driven`, `market_maker`, `momentum`, `scalper`, `speculator`, `swing_trader`) on the stdio and remote servers. An unknown value is refused by the tool's input schema; before, it passed the schema and the route answered `400 bad_request`.

  **Backward compatible.** Valid calls are unchanged; the only newly refused input is a strategy the route already rejected.

  **What to change:** Nothing. Read `meta.directional_source` from `get_sports_edge_signals`.
</Update>

<Update label="September 23, 2026" description="TypeScript calls require options for operations with path parameters or a body">
  In `@0xinsider/sdk`, `call`, `list`, `text`, `paginatePages`, `paginate` and `collect` now require their options argument when the operation has path parameters or a request body. `client.call("createWebhook")` and `paginate(client, "listWebhookDeliveries")` are compile errors.

  Before this change, those calls type-checked. The first reached the API with no body and was refused; the second threw `Missing path parameter: id` at runtime.

  **Backward compatible at runtime.** Requests are unchanged. A TypeScript project that omitted required options now gets a compile error where it previously failed at runtime.

  **What to change:** Pass `path` and `body` for operations that declare them. For an operation id chosen at runtime, type the value as `ApiOperationId` to use the untyped form.
</Update>

<Update label="September 23, 2026" description="TypeScript SDK: streamFeed enforces maxFrameBytes on every frame">
  `streamFeed` in `@0xinsider/sdk` now rejects any frame larger than `maxFrameBytes` (default 1 MiB), including a frame whose blank-line delimiter arrives in the same chunk. It throws `StreamProtocolError` with `reason` `frame_too_large` before the frame is parsed, delivered, or moves `cursor.seq`, and `lastSeq` stays the last delivered event.

  Before this change, the limit applied only to a frame still waiting for its delimiter, so an oversized frame that arrived complete was parsed and delivered.

  **Backward compatible.** Frames under the limit, keep-alive comments, and LF and CRLF framing behave as before.

  **What to change:** Nothing, unless you raised `maxFrameBytes` expecting complete frames to bypass it. Handle `frame_too_large` as you already do for an incomplete frame.
</Update>

<Update label="September 23, 2026" description="TypeScript SDK: error.reason carries every documented reason">
  In `@0xinsider/sdk`, `error.reason` on an `OxinsiderApiError` now holds the response's `error.reason` whenever it is a documented reason, whichever error class is thrown. `SubscriptionRequiredError` carries `subscription_inactive`; `RateLimitedError` carries `monthly_quota_exceeded`, `ip_rate_limited` or `ip_throttled`; `BadRequestError` carries `invalid_query`, `invalid_path`, `invalid_body`, `payload_too_large`, `unsupported_media_type` or `method_not_allowed`.

  Before this change, 12 of the 24 documented reasons read `null`, so a check such as `e.reason === "subscription_inactive"` never matched and a lapsed key kept retrying.

  **Backward compatible.** An unrecognized reason still reads `null`, and the raw string stays on `error.error.reason`.

  **What to change:** Nothing required. Branch on `e.reason` to stop on `subscription_inactive` and to tell a monthly quota or an IP cooldown apart from a per-minute rate limit.
</Update>

<Update label="September 23, 2026" description="Market holders answers 200 with an empty roster for a market no graded wallet holds">
  [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders) now answers `200` with `holders: []` and zero totals when the complete holder scan finds no S, A or B wallet on either outcome. It answered `503` with `error.reason` `read_model_warming` and `Retry-After`, a retry that could never succeed for such a market.

  What a client must change: nothing, if it already handles an empty page; a client that retried on 503 stops retrying. Every other 503 (incomplete, unstable or failed scan) is unchanged.
</Update>

<Update label="September 23, 2026" description="Pre-game sides get canonical paths, and four canonical row field names">
  [`GET /api/v1/sports/pre-game-sides`](/api-reference/endpoint/get-pre-game-sides) and [`GET /api/v1/sports/pre-game-side-observations`](/api-reference/endpoint/get-pre-game-side-observations) are now the canonical paths for the two sports routes, and every row carries `side`, `ranked_at`, `backing_score`, and `side_share`.

  Before this change, the only paths were `/api/v1/sports-edge-signals` and `/api/v1/sports-edge-observations`, and the same four values were spelled `piled_side`, `signal_created_at`, `conviction_score`, and `smart_score`.

  * The old paths stay live as deprecated aliases. They take the same parameters, answer the same body, and now send `Deprecation` and a `Link` header naming the successor path.
  * The old field names stay in every response with the same values. Nothing is removed.
  * Response schemas are renamed: `SportsEdgeSignal` is now `PreGameSide`, and `SportsEdgeObservation` is now `PreGameSideObservation`.
  * `@0xinsider/sdk` 0.8.0 adds `listPreGameSides`, `listPreGameSideObservations`, and `listPreGameSideObservationsConditional`. The `listSportsEdge*` methods stay as deprecated aliases on the deprecated paths.
  * `@0xinsider/mcp` 2.3.0 adds the `get_pre_game_sides` and `get_pre_game_side_observations` tools. `get_sports_edge_signals` and `get_sports_edge_observations` stay as deprecated aliases.

  **Backward compatible.** Every existing path, field, SDK method, and MCP tool keeps working.

  **What to change:** Nothing today. In new code, call the `/api/v1/sports/pre-game-*` paths and read `side`, `ranked_at`, `backing_score`, and `side_share`.
</Update>

<Update label="September 23, 2026" description="MCP adds list_games and get_game for the game catalog">
  The [MCP servers](/integrations/mcp) add two read-only tools: `list_games`, which calls [`GET /api/v1/games`](/api-reference/endpoint/list-games), and `get_game`, which calls [`GET /api/v1/games/{event_slug}`](/api-reference/endpoint/get-game). Both the hosted server at `https://api.0xinsider.com/api/v1/mcp` and `@0xinsider/mcp` 2.4.0 expose them, and both return the same page for the same arguments.

  Before this change, no tool called the game resource, so an agent on MCP had to assemble a game from `search_markets`, per-market snapshots and team-name matching.

  * `list_games` takes `sport`, `league`, `status`, `starts_after`, `starts_before`, `limit` and `cursor`, and returns `data`, `has_more`, `next_cursor`, `as_of`, `coverage` and `meta`. Read `coverage`: it names the sports and leagues served, so an empty page is never ambiguous.
  * `get_game` takes `event_slug` and returns the game object with `meta` beside it.
  * `status` is restricted to the route's nine values, so a typo is refused by the tool schema. An unknown `sport` or `league` still returns an empty page, as the route does.
  * Neither tool returns a price, a sharp-money split or a holder identity.

  **Backward compatible.** Existing tools are unchanged. The tool count moves from 42 to 44.

  **What to change:** Nothing. To use the new tools on the local server, install `@0xinsider/mcp` 2.4.0 or later.
</Update>

<Update label="September 23, 2026" description="V1 query diagnostics decode form values and reject malformed unknown names">
  [`/api/v1`](/api-reference/introduction) now decodes `X-Effective-Query` with form-urlencoded rules, so `+` is a space, and strict validation rejects an unknown query name even when its percent escape is incomplete.

  Before this change, `X-Effective-Query` did not document the form-decoded value and strict validation could let an unknown name with an incomplete percent escape reach the handler.

  **Backward compatible.** The default still ignores unknown names and reports them in `X-Query-Ignored`; strict validation is opt in.

  **What to change:** Nothing for default callers. If you send `X-Query-Validation: strict`, handle `unknown_query_parameter` and use each operation's documented query names.
</Update>

<Update label="September 23, 2026" description="@0xinsider/mcp 2.1.3: init configures Claude Code in the file Claude Code reads">
  `npx -y @0xinsider/mcp init` now writes the Claude Code server to `~/.claude.json`, where Claude Code keeps user-scoped MCP servers. It also moves an entry an earlier version left in `~/.claude/settings.json`.

  Before this change, `init` wrote the server to `~/.claude/settings.json`, which Claude Code does not read for MCP servers. The server never appeared in `claude mcp list`, and a copy of your key sat in a file nothing read.

  * `init` writes each config through a temporary file and a rename, so a crash cannot leave `~/.claude.json` half-written, and a symlinked config stays a symlink.
  * A `~/.claude/settings.json` that `init` cannot parse is reported and left untouched.
  * 2.1.3 also ships the unpublished 2.1.1 and 2.1.2 changes: REST provenance and error details in MCP tool results, and pageable `search_markets` on the stdio server.

  **Backward compatible.** Cursor, Codex, and Gemini CLI setup is unchanged.

  **What to change:** If you ran `init` for Claude Code before, run `npx -y @0xinsider/mcp@2.1.3 init` again, open a new Claude Code session, and check `claude mcp list`.
</Update>

<Update label="September 22, 2026" description="Official Rust client, and the TypeScript client's public source repository">
  Two official clients are now public repositories: [`0xinsider/0xinsider-rust`](https://github.com/0xinsider/0xinsider-rust), the Rust client (crate `oxinsider`), and [`0xinsider/0xinsider-node`](https://github.com/0xinsider/0xinsider-node), the source npm will publish `@0xinsider/sdk` from. Before this change, the only official clients you could install were the Python and Go ones, and the TypeScript source sat in a private repository.

  * The [Rust client](/integrations/rust-client) is generated from the OpenAPI document: one async method per operation, typed errors, retries that honor `Retry-After`, cursor pagination, and a bounded reader for the event stream. Install it with `cargo add oxinsider --git https://github.com/0xinsider/0xinsider-rust` until the first crates.io release.
  * The [TypeScript client](/integrations/typescript-client) builds from `0xinsider-node` with `npm ci && npm run build` until the first npm release of `@0xinsider/sdk`.

  **Backward compatible.** No API behavior changed, and no existing client changed.

  **What to change:** Nothing. To call the API from Rust, add the crate; to use the TypeScript client, build it from the public repository.
</Update>

<Update label="September 23, 2026" description="The sandbox answers the two Markdown routes, the export download and the MCP GET">
  The [sandbox server](https://0xinsider.com/sandbox/api/v1) answers four operations it used to refuse with `400 bad_request`. A caller holding only a sandbox key from [`POST /api/v1/agents/register`](/api-reference/endpoint/register-agent) could not reach any of them, while the registration response told that caller every documented operation is answered.

  * [`GET /api/v1/trader/{address}/context.md`](/api-reference/endpoint/get-trader-context-markdown) and [`GET /api/v1/market/{condition_id}/context.md`](/api-reference/endpoint/get-market-context-markdown) answer `200` with `content-type: text/markdown`. Each document is rendered from the body the sandbox serves for the JSON route beside it, so the Markdown and the JSON describe the same sandbox trader and the same sandbox market.
  * [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export) answers the `302` it documents. `Location` points at a CSV the sandbox serves itself, with the same twelve columns as a live CSV export. No object store is involved and no row is real.
  * [`GET /api/v1/mcp`](/api-reference/endpoint/remote-mcp-stream) answers `405` with `Allow: POST` and `X-Mcp-Error-Code: -32004`, the same as the live API.
  * `GET /api/v1/stream` is still refused with `400`, because a Server-Sent Events stream is a live connection rather than a body. It is now named as the one exclusion in the OpenAPI server description, in the registration response and in the TypeScript SDK.

  The OpenAPI document also corrects the body documented for the `405` on `GET /api/v1/mcp`: it is a JSON-RPC error object, not the REST error envelope. The `401`, `403` and `429` on that route are unchanged, because those come from the credential and rate-limit layers in front of the MCP transport.

  **Backward compatible.** No live API behavior changed.

  **What to change:** Nothing. If you skipped these operations while building against the sandbox, they now answer there.
</Update>

<Update label="September 22, 2026" description="The Python SDK types every operation from the API schemas instead of Any">
  `0xinsider` 0.4.0 on PyPI generates a type for every documented request body, response envelope and schema, and puts them in the method signatures. Before, every operation took `Any` for its body and query arguments and returned `Any`, so the package shipped `py.typed` and told a type checker nothing.

  * An operation returns its own envelope: `client.get_trader("swisstony")` is a `GetTraderResponse`, `client.list_positions(...)` is a `ListPositionsResponse`, and [`GET /api/v1/trader/{address}/context.md`](/api-reference/endpoint/get-trader-context-markdown) is a `str`, not an envelope.
  * A query value takes what the document allows: `min_grade=` is `Literal["S", "A", "B", "C", "D", "F"]`, `format=` on an export is `Literal["json", "ndjson", "csv"]`. A request body is a `TypedDict`, so a missing or misspelled key fails before the call spends a request.
  * `client.with_response.<method>()` returns `ApiResponse[<the same body>]`, and every shape is importable from `oxinsider.types`.
  * A field the API can omit is an optional key. `trader["data"]["grade"]` is a type error and `trader["data"].get("grade")` is not, because an ungraded wallet carries no `grade`; `pnl.realized` behaves the same way when no verified native realized-P\&L source is available. A nullable field is `| None`. An omitted key and a null value stay different facts, and neither is a zero.
  * An enum the API returns is typed `Literal[...] | str`, so a value added to the API after a release is not a type error in a client that has not upgraded. An enum you send is a strict `Literal`. A `const` is exact, so [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) entries narrow on `entry["state"]` to the sealed, opened or uncommitted shape.
  * A conditional read is overloaded: `client.get_trader(address)` returns the response, and `client.get_trader(address, if_none_match=etag)` returns `GetTraderResponse | NotModifiedResponse`, which is when a `304` can happen.

  **Backward compatible.** No method, argument name, default or returned object changed. Nothing is validated, converted or copied at runtime: the methods return the decoded JSON exactly as before, and the annotations are deferred, so none is evaluated at import. Python 3.9 is still the floor.

  **What to change:** Nothing to run the same code. A type checker may now report reads your code was already doing: a field the API can omit read with `[]` instead of `.get()` is the common one, and it is the case where the old code would raise on a real response. To read a field this release does not know yet, call `client.request(...)`, which is typed `Any`.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK covers Markdown, JSON-RPC, registration, and export downloads">
  `@0xinsider/sdk` 0.5.0 calls the four operations it had no way to reach. Each answers something other than the JSON `{ object, data, meta }` envelope, and the client assumed every operation answered that envelope, so a Markdown body was parsed as JSON and then rejected as an invalid `200`, and the two operations without a documented `200` were absent from the client altogether.

  * [`GET /api/v1/trader/{address}/context.md`](/api-reference/endpoint/get-trader-context-markdown) and [`GET /api/v1/market/{condition_id}/context.md`](/api-reference/endpoint/get-market-context-markdown) are `client.getTraderContextMarkdown(address)` and `client.getMarketContextMarkdown(conditionId)`, returning the `text/markdown` document as a `string`. `client.text(operationId, options)` is the general form.
  * [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) is `client.mcp(request, options)`. It returns `{ status, response, sessionId? }`: `status` `200` with the JSON-RPC body, or `status` `202` with `response: null` for a notification you sent without an `id`. A JSON-RPC `error` member is returned, not thrown; only a non-2xx HTTP status throws. `sessionId` and `protocolVersion` options send `Mcp-Session-Id` and `MCP-Protocol-Version`.
  * [`POST /api/v1/agents/register`](/api-reference/endpoint/register-agent) is `client.registerAgent()`. It needs no credential and answers `201` with the `oxi_sk_test_` sandbox key.
  * [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export) is `client.getTraderExportDownloadUrl(address, jobId)`, which resolves the `302` and returns the presigned URL with its `expiresAt`, and `client.downloadTraderExport(address, jobId)`, which then fetches the object. That second request carries no headers at all, so your API key never reaches the file host; the body is streamed, not buffered, and the object fetch has no deadline unless you pass `downloadTimeoutMs`.

  Every envelope's `meta` also gains a client-set `status` holding the HTTP status: `201` on `registerAgent`, and `202` on a [`POST /api/v1/trader/{address}/export`](/api-reference/endpoint/submit-trader-export) that queued a new job against `200` for one that already existed. The two bodies are identical, so this is the only way to tell them apart.

  `call()` on an operation that does not answer the envelope now names the method that does, instead of failing on the body, and the package exports `REDIRECT_OPERATIONS` and `UNSUPPORTED_OPERATIONS` so every published operation is accounted for.

  **Backward compatible.** No existing method, argument or return type changed, and no API behavior changed. `meta.status` is a new optional field.

  **What to change:** Nothing. If you were calling the Markdown routes with your own `fetch` because the SDK could not, the methods above replace that. The Python (`0xinsider` on PyPI) and Go (`github.com/0xinsider/0xinsider-go`) clients already cover these operations.
</Update>

<Update label="September 22, 2026" description="The sandbox pages through a coherent world and refuses what the live API refuses">
  The [sandbox](/sandbox) at `https://0xinsider.com/sandbox` answers every operation with a body the live API could have produced, and checks your request against the same OpenAPI document it serves.

  * Every cursor-paginated list serves one fixed collection of 7 rows. `limit` and `cursor` cut the page, `has_more` and `next_cursor` agree with it, `total` is the collection size, and the last page reads `has_more: false` and `next_cursor: null`. Before, `limit` and `cursor` changed nothing and a list answered `has_more: false` beside `next_cursor: "sandbox"`.
  * Field values are legal for their field. `grade` is a grade letter, `platform` is `polymarket`, an address is 40 hex characters, and a trader's `id` is `trd_` plus that address. Before, an unconstrained string read `sandbox` and an unconstrained number read `1`.
  * [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) and [`POST /api/v1/markets/intel/batch`](/api-reference/endpoint/batch-get-market-intel) answer one item per identity you post, in the order you posted them, with `meta.total_items`, `meta.successful_items` and `meta.failed_items` matching the items and each item carrying `data` or `error`, never both. Before, both echoed the two identities of the documented example whatever you posted.
  * Documented query parameters and JSON request bodies are validated. A value outside its schema is `400` with `error.reason` `invalid_query`, a body that is missing, unparseable or does not fit is `400 invalid_body` with `error.param`, a body without `Content-Type: application/json` is `415 unsupported_media_type`, and a cursor the sandbox did not issue is `400` with `error.reason` `cursor_expired`. Unknown query names are ignored and reported in `X-Query-Ignored`, and `X-Query-Validation: strict` returns `400 unknown_query_parameter`, as on the live API.
  * Every sandbox response carries `X-Request-Id: req_sandbox`, matching `meta.request_id` in the body.

  **Not backward compatible for a malformed request.** A request the sandbox used to answer `200` now returns the error the live API returns. Response shapes and field names are unchanged, and the live API is unchanged by this release.

  **What to change:** Nothing if your client already sends what the document describes. If a sandbox call starts returning `400` or `415`, read `error.param` and `error.reason`: the live API would have refused the same request. If you hard-coded `"sandbox"` or `1` as an expected sandbox value, read the field instead.
</Update>

<Update label="September 22, 2026" description="The OpenAPI document now matches what the server does on 6 points">
  [`/api/v1/openapi.json`](https://0xinsider.com/api/v1/openapi.json) now describes what the server already does in 6 places. Nothing on the wire changed.

  * [`GET /api/v1/webhooks/{id}/deliveries`](/api-reference/endpoint/list-webhook-deliveries): `limit` defaults to `20`, not `50`. A value outside 1 to 100 is clamped into that range.
  * [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) no longer lists `401`, `402`, `403`, or `423`. The route is public and cannot return them.
  * [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) now lists the `404` it returns, with `error.param` `wallet`, when a `wallet` value matches no wallet.
  * [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders): the `cursor` description said a cursor starts with `mh_`. A served cursor never does, so treat it as opaque.
  * [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history): `trader` also accepts a `trd_` id.
  * A `?token=` key is refused on every route that needs a key. The public routes ignore it.

  **Backward compatible.** No request or response changed.

  **What to change:** Nothing. If you generate a client from the document, regenerate it to pick up the `limit` default and the corrected error lists.
</Update>

<Update label="September 23, 2026" description="Pay as you go stays available while a payment retry is in progress">
  API key quota admission and `/api/keys/usage` now treat a pay-as-you-go account in `past_due` status as available while payment retries continue.

  Before this change, both read only `active`. The account could be refused past its included quota while its metered usage was still sent for billing, and the usage response showed `pay_as_you_go: false`.

  * `canceled` and `unpaid` are still refused past the included quota, and they are not metered.
  * This changes behavior only. No response shape changed.

  **What to change:** Nothing. If you display `pay_as_you_go`, treat `true` as available during a payment retry.
</Update>

<Update label="September 23, 2026" description="Report grade distributions now describe the traders in the report period">
  [`GET /api/v1/reports`](/api-reference/endpoint/get-reports) and the [daily](/api-reference/endpoint/get-daily-report-snapshot), [weekly](/api-reference/endpoint/get-weekly-report-snapshot), and [monthly](/api-reference/endpoint/get-monthly-report-snapshot) routes now define `report.grade_distribution` as the current grade mix of the distinct Polymarket traders with a large trade in that report's source date range.

  Before this change, it counted every currently ranked trader site-wide. Unrelated periods showed the same census, and a closed report froze that census.

  * Traders without a current grade are left out.
  * An entry with `grade: null` means the active trader's current grade is unavailable.

  **Breaking** for a client that treated `grade_distribution` as a site-wide census. The field name and shape are unchanged, but the values now describe the report period.

  **What to change:** Stop reading `grade_distribution` as a site-wide census. A client that wants the report period's mix needs no request change.
</Update>

<Update label="September 23, 2026" description="potd-trader 0.3.0 adds shared stopping, atomic reservations, and a UTC daily budget">
  The [POTD auto-trader](/guides/auto-buy-the-pick) 0.3.0 checks an authoritative stop before every submission. `live off` now stops new submissions from running watchers that share the folder, and waits for a submission already in progress before it acknowledges. It cannot cancel an order already sent.

  Before this change, `live off` only changed the next process's configuration. Concurrent processes or duplicate feed rows could submit twice, and `DAILY_CAP_USD` used dates the feed controlled.

  * New commands: `live status`, `run --dry-run`, and `watch --dry-run`. The setup preview is forced dry, even with an inherited `LIVE=yes`.
  * The shared local JSON ledger reserves each pick and its spending under an inter-process lock before it posts.
  * `DAILY_CAP_USD` now counts locally recorded UTC submission times, and an unresolved earlier order keeps its reservation across midnight. The cap bounds order principal, not exchange fees.
  * A duplicate or inconsistent slate, incomplete safety data, a mismatched or expired `entry_authorization`, or an expired quote cannot authorize a buy.
  * The watcher follows `proof_pending_picks[].retry_at`.
  * `DAILY_CAP_USD=0` is rejected, `OXINSIDER_API_BASE` must be `https://api.0xinsider.com`, and live trading requires an active `.env` control file.
  * A watcher started dry stays dry until it restarts. `live on` can resume one that started live.

  **Breaking** for a setup with `DAILY_CAP_USD=0`, another API base, or no control file. The new CLI flags are additive, and the REST API response is unchanged.

  **What to change:** Stop every old watcher process before you upgrade, and keep the existing ledger. Set a positive cap, mount the control file for an environment-only deployment, and install the [versioned release](https://github.com/0xinsider/potd-trader/releases/tag/v0.3.0) with `uv sync --locked`. Run with `--dry-run` before you restart live.
</Update>

<Update label="September 23, 2026" description="Trader exports identify their database generation and source coverage">
  A new file from `POST /api/v1/trader/{address}/export` now reads every section from one database generation before delivery.

  Before this change, the sections could read different database states during a refresh.

  * JSON and NDJSON add `export_metadata.generation`, with `id`, `selected_at`, `consistency` (`repeatable_read`), and `source_watermarks` for positions, native P\&L, categories, and all matching trades.
  * They also carry `position_generation`, `provider_observations`, `category_data_as_of`, `pnl_date_from`, `pnl_date_to`, `pnl_rows`, `pnl_row_limit`, `markets_rows`, `markets_row_limit`, `summary_basis`, and `trades_basis`.
  * Provider observations keep their own clocks. One database generation does not mean every provider fact was observed at the same moment.
  * Summary counts cover all stored markets. The section limits are now explicit: `markets` holds at most 50,000 rows ordered by latest activity, and `pnl` holds at most the earliest 3,650 daily rows.
  * Provider lifetime P\&L is not rebuilt from the exported trade cash flows, and a missing source observation stays unavailable.
  * CSV appends `export_generation`, `export_selected_at`, and `export_consistency`.
  * The downloaded object's `x-amz-meta-export-generation`, `x-amz-meta-export-selected-at`, `x-amz-meta-export-consistency`, `x-amz-meta-export-trades`, and `x-amz-meta-export-provenance` headers describe the generation and the section watermarks, even for an empty CSV.
  * Preparation fails, rather than returning a partial file, when it runs past 600 seconds or 2 GiB of uncompressed output.

  **Backward compatible.** The fields and columns are additive, and existing files stay as they are.

  **What to change:** Nothing for JSON and NDJSON, which can ignore the new metadata. CSV clients should select columns by name and allow the three new ones. Job submission, polling, and download URLs are unchanged.
</Update>

<Update label="September 22, 2026" description="V1 reports ignored query parameters and supports opt-in strict validation">
  V1 routes now add two response headers: `X-Query-Ignored` names the query parameters the operation does not support, and `X-Effective-Query` lists the normalized names and values it applied. By default, an unknown query parameter is still ignored.

  Before this change, an unknown query parameter was ignored with no machine-readable signal.

  * Send the new `X-Query-Validation: strict` request header, or set `strictQuery: true` in the TypeScript SDK, to reject an unsupported name before the handler runs.
  * A rejected request answers `400 bad_request` with `error.reason` `unknown_query_parameter`, and `error.param` names the query key.

  **Backward compatible.** Existing callers see only the new headers.

  **What to change:** Nothing. If you opt into strict mode, handle `unknown_query_parameter` and use each operation's documented query names.
</Update>

<Update label="September 22, 2026" description="Large trades: market_volume_share never exceeds 1">
  `market_volume_share` on [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade), and the replay's `expand=trade` is a trade's `size_usd` as a share of its market's volume. It now always falls between 0 and 1, and the OpenAPI schema states `maximum: 1`.

  Before this change, the share could read far above 1. The denominator was the market's volume when the trade was stored: a cached copy of the provider's figure, often older than the trade, so it could not include the trade. Of the 3,086 trades stored since the field shipped on September 16, 412 read above 1, and the highest read 18,358.08.

  * The denominator is now a market volume figure recorded at or after the trade, used only when it is at least as large as the trade it must include.
  * When no such figure is available, the field is absent, as it already was for a market with no volume figure. It is never `0`, never `1` as a stand-in, and never capped.
  * Measured over the same window on September 22: 3,064 of 3,086 trades carry a share (99.3%), none above 1. The highest is 0.876506 and the median 0.020408.
  * A market's volume keeps growing, so the field now reports a trade's size against the market's volume today. The same trade reports a smaller share as the market trades on, and a trade only minutes old can be absent until a volume figure recorded after it arrives.

  **Breaking** for a client that stored earlier values. The field keeps its name, its type (`number`, six decimal places), and its optionality, and no other field moved.

  **What to change:** Discard stored or cached values, because the same trade can now report a different share, or none. A client that expected values above 1 can rely on the 0 to 1 range.
</Update>

<Update label="September 22, 2026" description="The OpenAPI document separates fields a response leaves out from fields it sends as null">
  [The OpenAPI document](https://0xinsider.com/api/v1/openapi.json) now says, for every response property, whether the server leaves the key out or sends it as `null`. `@0xinsider/sdk` ships regenerated types to match. Nothing on the wire changed.

  Before this change, the document had it wrong in three ways:

  * 222 properties were marked `nullable: true`, but no response ever sends them as `null`: the key is absent instead. They are now typed as their plain type and kept out of `required`. Among them: `Trader.username`, `Trader.grade`, `Trader.score`, `Trader.rank`, every leg of `Trader.pnl` and `Trader.stats`, `Position.avg_price`, `Position.cash_pnl`, `Position.realized_pnl`, `Position.trader.grade`, `Position.market.slug`, `WhaleTrade.trader.username`, `WhaleTrade.market.category`, every `LeaderboardEntry` field except `id`, `address`, and `platform`, `TrendingWallet.username`, `CounterpartyParticipant.grade`, `error.param`, `error.doc_url`, and the `next_cursor` and `total` pair on every list response.
  * 159 properties were missing from `required`, although every response sends them, as `null` when there is no value. They keep `nullable: true` and are now `required`. Among them: `WhaleTrade.outcome`, `WhaleTrade.token_id`, `Position.token_id`, most of `ExploreMarket` and its `score_components`, `SportsEdgeSignal.title`, `SportsEdgeSignal.token_id`, `MarketHolder.name`, `ExportSourceRange.*`, and `TraderPnl.freshness_at`.
  * 17 properties the server omits were listed in `required`. [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) carries 14 of them, absent when the response is a teaser rather than a full pick: `pick_date`, `thesis`, `matchup`, `position`, `outcome`, `platform`, `release_at`, `is_locked`, `side_summary`, `smart_wallet_count`, `disclaimer`, `category`, `clv_status`, and `pick_outcome_label`.

  In the SDK types, an omitted field now reads `grade?: string` instead of `grade?: string | null`, and an always-sent one reads `token_id: string | null` instead of `token_id?: string | null`.

  **Backward compatible.** No response body is different.

  **What to change:** Regenerate any client you generate from the document. Read an omitted field with `?.` or a default, and read an always-sent one as a value that can be `null`. A client that already treats absent and `null` alike needs no change.
</Update>

<Update label="September 22, 2026" description="Every whale trade carries the grade its wallet held when the trade happened">
  [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade), and the `trade` object from `expand=trade` on [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) add two fields to `trader`: `grade_at_trade` and `grade_at_trade_status`.

  Before this change, every row carried only the wallet's grade today, however old the trade.

  * `grade_at_trade` is `S` to `F`, or `null`: the grade the wallet held at `traded_at`, read from grade history recorded since September 19, 2026 23:00 UTC.
  * `grade_at_trade_status` is `graded`, `ungraded`, or `unknown`. Every trade before that history starts reads `unknown`, with `grade_at_trade` `null`, never today's grade projected backward. `unknown` never means ungraded.
  * Unchanged: `trader.grade`, `signal_score`, and the `min_grade` filter still read the wallet as it is today, on every row however old.
  * The history page now lists the archive's size floor by period.

  A backtest that selects on `trader.grade`, `signal_score`, or `min_grade` is selecting on what the wallet did after the trade. Select on `trader.grade_at_trade` and `recorded_signal_score` instead.

  **Backward compatible.**

  **What to change:** Nothing. `@0xinsider/sdk` types the new fields from the regenerated schema.
</Update>

<Update label="September 22, 2026" description="Python and Go SDKs 0.3.0: 67 operations, visible pagination failures, bounded Go requests">
  `0xinsider` 0.3.0 on PyPI (import `oxinsider`) and `github.com/0xinsider/0xinsider-go` v0.3.0 release everything both clients had merged since 0.2.0. Both implement 67 operations, up from 64, and both are generated from OpenAPI document SHA-256 `ece72cd4d303353608f144489537c5760f359bcecc9aa9b0c742f3f629c84318`.

  Before this change, the newest work in each client was only on its default branch. A user who installed 0.2.0 got 64 operations, a Python `paginate` that could not tell a malformed page from an exhausted one, no way to read a successful response's headers, and a Go request that waited forever on a stalled connection.

  Python 0.3.0:

  * `client.paginate(...)` checks each page before it yields it and before it spends another request. A response that is not a cursor-paginated list envelope, or whose `data` is not a list, raises the new `oxinsider.PaginationError` with `reason` `invalid_envelope` or `invalid_data`.
  * `has_more: true` with no usable `next_cursor` raises `missing_cursor`. A `next_cursor` the walk already requested raises `repeated_cursor`, before the duplicate request goes out.
  * `progress=` and `oxinsider.pagination_checkpoint(error)` report where a walk stopped, so a long walk resumes instead of restarting. `progress.stopped_by` is set only when the walk ended on its own terms.
  * `client.with_response.<method>(...)` returns an `ApiResponse`: the same body under `.data`, plus `.etag`, `.request_id`, `.rate_limit`, `.monthly_quota`, `.batch_rate_limit` and `.request_cost`. A header the API did not send reads `None`, never `0`.

  Go v0.3.0:

  * Every ordinary request from `oxinsider.New` is bounded: `DefaultRequestTimeout` of 30 seconds end to end, `DefaultDownloadTimeout` of 5 minutes for the export download, over a transport that bounds the dial, the TLS handshake and the wait for response headers.
  * A deadline the client applied returns `*RequestTimeoutError`, which carries `Deadline`, `Method` and `URL` and unwraps to `context.DeadlineExceeded`. A context deadline you pass still wins, and `WithRequestTimeout(d)` changes the default.
  * `GET /api/v1/stream` is excluded and bounded by `OpenStream`'s start and idle timeouts, so a healthy stream is not cut by the per-request deadline.
  * `oxinsider.Version` is `"0.3.0"` and rides in `User-Agent`.

  **Backward compatible,** with two deliberate behavior changes. Python's `paginate` raises `PaginationError` where 0.2.0 returned silently on a malformed page, which is the point of the change. A Go request that used to wait forever now returns `*RequestTimeoutError` after 30 seconds.

  **What to change:** Upgrade. In Python, catch `oxinsider.PaginationError` if you page over an operation that is not a cursor-paginated list. In Go, pass `oxinsider.WithRequestTimeout(0)` or your own context if you deliberately relied on an unbounded request.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.4.0: a stream checkpoint that moves only after your code finishes">
  `@0xinsider/sdk` 0.4.0 adds `consumeStreamCheckpointed(client, handlers, options)` for [`GET /api/v1/stream`](/api-reference/endpoint/get-stream). It keeps two positions instead of one: `options.cursor` is the last frame received, and the new `options.checkpoint` (a `StreamCheckpoint`) is the last frame your code finished with.

  Before this change, the received cursor was the only position a consumer had. `streamFeed()` writes it before a frame reaches your code, so when a handler threw, a reconnect from that cursor resumed after the event that failed.

  * `options.checkpoint` advances only after your `onEvent` handler and your own `onCheckpoint` write have both resolved. `options.cursor` keeps its meaning.
  * If `onEvent` or `onCheckpoint` rejects, the checkpoint stays before that event. The consumer closes the connection, waits a jittered 1 to 30 seconds, and reconnects from the checkpoint, so the server replays the event while it still holds it.
  * `onHandlerError(error, failure)` reports each failure with `seq`, `stage`, `attempt`, `willRetry`, `replayFrom`, and the unadvanced `checkpoint`.
  * After `maxHandlerRetries` consecutive failures at one `seq` (3 by default), it throws the new `StreamHandlerFailedError`, which carries the same fields.
  * `onResync` is awaited. The checkpoint moves past a `resync` marker only once your refresh resolves, so an interrupted refresh is retried rather than recorded as done.
  * Nothing is read from the connection while a handler runs. A slow handler applies backpressure to the socket, and no queue is kept for you.

  Delivery is at least once. A replay re-delivers every frame you have not acknowledged, and a handler that succeeded but whose checkpoint write failed sees its event again. Deduplicate on `seq`, or make the side effect idempotent.

  **Backward compatible.** `streamFeed()`, `streamFeedResilient()`, and `consumeStream()` behave exactly as before.

  **What to change:** Nothing, unless you need the guarantee. If you store `cursor.seq` as "work completed", move to `consumeStreamCheckpointed` and store the checkpoint from `onCheckpoint`.
</Update>

<Update label="September 22, 2026" description="Python SDK 0.2.0: with_response returns a call's status, headers, ETag, and budgets">
  `0xinsider` 0.2.0 on PyPI (import `oxinsider`) adds `client.with_response.<method>(...)`. It calls the same operation and returns an `ApiResponse` instead of the body alone.

  Before this change, a successful call returned only the decoded body and dropped everything else the response carried. The README advertised `if_none_match=`, but the `ETag` was reachable only on a `304`, never on the `200` that produced the data, so there was nothing to revalidate with.

  The request id, the rate-limit window, the monthly quota, the batch item budget, the request cost, and `Retry-After` were out of reach on a success too.

  * `ApiResponse.data` is exactly what the plain method returns, with `status` and `headers` beside it.
  * Its typed accessors: `etag` and `not_modified` for conditional reads; `request_id` ([`X-Request-Id`](/errors), the same id as `meta.request_id`); `retry_after`; `rate_limit` (`X-RateLimit-*` with `RateLimit-Reset`); `monthly_quota` (`X-Monthly-Quota-*`); `batch_rate_limit` and `request_cost` (`X-Batch-RateLimit-*`, `X-Request-Cost`); `server_timing_ms` (`Server-Timing: api;dur=`); `usage_accounting`; `deprecation`; `sandbox`; `content_type`; and `header(name)`.
  * Each of the three budgets is a `Budget` with `limit`, `remaining`, `reset_at` (a Unix epoch second), and `reset_after` (seconds from now, which only the per-minute window publishes). It is `None` when the response described no such window.
  * A header the API did not send reads `None`, never `0`. A budget the response did not mention is not an exhausted budget.
  * `OxinsiderApiError.response` carries a failed response the same way, so you can handle a `429` from the window the API described rather than from a guess.
  * `client.request(method, path, raw=True)` does the same for a hand-built call. [Rate limits](/rate-limits) documents the headers themselves.

  **Backward compatible.** The server did not change. Every existing method returns what it returned before, including the text of a Markdown route, `None` for a `204` or an empty body, and `{"object": "not_modified", ...}` for a `304`. The SDK sends the same requests, and an error status raises the same typed error with the same `code`, `reason`, `retry_after`, `request_id`, and `body`.

  **What to change:** Nothing. `raw=True` with `stream=True` now raises `ValueError` instead of being silently ignored, because a stream already hands back the open `httpx.Response` with its own headers. A redirect-only operation returns the same `Download` under `with_response`, because that object already carries the file's headers.
</Update>

<Update label="September 22, 2026" description="Go SDK: every request has a timeout, and the stream has start and idle timeouts">
  `github.com/0xinsider/0xinsider-go` now puts a time limit on every request a client from `oxinsider.New` makes. Before this change, a stalled connection could wait forever, because Go treats a zero `http.Client.Timeout` as no timeout and the quickstart uses `context.Background()`.

  * `DefaultRequestTimeout` (30 seconds) covers each request end to end, from the dial to the last byte of the body, across any redirect.
  * `DefaultDownloadTimeout` (5 minutes) applies instead to [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export), whose body is a file the SDK cannot size in advance.
  * The transport adds the connection limits `net/http` leaves unset: `DefaultConnectTimeout` (10 seconds) to dial, `DefaultTLSHandshakeTimeout` (10 seconds), and `DefaultResponseHeaderTimeout` (15 seconds) from the end of the request to the first response header.
  * A context that already has a deadline keeps it. A shorter deadline of yours wins, and a longer one is honored.
  * `oxinsider.WithRequestTimeout(d)` replaces the default for every request except the stream. `WithRequestTimeout(0)` removes it and keeps the connection limits.
  * A timeout the client applied returns `*oxinsider.RequestTimeoutError`, which names the limit, the method, and the path, unwraps to `context.DeadlineExceeded`, and reports `Timeout() true`. Your own deadline or cancellation still returns `context.DeadlineExceeded` or `context.Canceled`, and a connect, handshake, or header stall returns the transport's own timeout error.
  * `http.Client.Timeout` stays unset, because it would also cut off reading the event stream.

  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) has no total time limit. Instead, `WithStreamStartTimeout` (10 seconds) limits the wait for the response headers, and `WithStreamIdleTimeout` (60 seconds) ends a connection that has sent nothing, not even a keep-alive. The stream sends a keep-alive every 5 seconds, so 60 seconds of silence means 12 missed keep-alives.

  * Either limit ends the call with `*oxinsider.StreamTimeoutError`. Its `Phase` is `start` or `idle`, and its `LastSeq` is the value to resume from with `Last-Event-ID`.
  * Pass `0` to remove either limit.
  * The idle clock runs only while the reader waits for bytes, so taking your time between frames does not trigger it.

  **Backward compatible** on the wire: the server did not change, and no method was removed or renamed.

  **What to change:** Nothing, if you already pass a context with a deadline. Otherwise:

  * If you relied on an unbounded `context.Background()` call, for a slow export download or a long read, pass your own deadline or use `WithRequestTimeout`.
  * If you read the stream through `GetStream` with `RawStreamContext`, you own the raw body, and only the connection limits apply.
  * Code that branches on a timeout should match `*RequestTimeoutError` or `*StreamTimeoutError`, or keep using `errors.Is(err, context.DeadlineExceeded)`.
</Update>

<Update label="September 22, 2026" description="Python SDK 0.2.0: paginate stops on a broken page and can resume from a checkpoint">
  `0xinsider` 0.2.0 on PyPI (import `oxinsider`) makes `client.paginate(...)` check every page before it yields the page or requests the next one. A page that breaks the cursor protocol now raises the new `oxinsider.PaginationError` instead of looping or ending silently.

  Before this change, `paginate` yielded `data` only when it happened to be a list, returned silently when the page fields were missing, and followed `next_cursor` without checking progress. A repeated cursor with `has_more: true` looped and kept spending requests, and a malformed page looked the same as the end of the list.

  `PaginationError.reason` says what was wrong:

  * `invalid_envelope`: the response is not a cursor-paginated list.
  * `invalid_data`: `data` is not a list.
  * `missing_cursor`: `has_more` is `true`, but there is no usable `next_cursor`.
  * `repeated_cursor`: `next_cursor` is a cursor `paginate` already requested. It is raised before the duplicate request goes out.

  These still end pagination normally: `has_more: false`, whatever `next_cursor` says; a valid empty page; and a `304` from `if_none_match`, which ends it with `stopped_by` `not_modified`. The SDK remembers the last 1,024 cursors it requested (`oxinsider.CURSOR_HISTORY_LIMIT`), so memory use stays flat however many pages you read, and a cycle longer than that is not detected.

  New options on the call:

  * `max_pages` and `max_items` limit how far `paginate` goes. Each is a positive integer or `None`, checked before the first request, so `0`, a negative number, or a float raises `ValueError` without spending a request.
  * `progress=oxinsider.PaginationProgress()` reports `pages_fetched`, `items_yielded`, `cursor`, `next_cursor`, and `stopped_by` (`exhausted`, `max_pages`, `max_items`, `not_modified`, or `None` when pagination was interrupted). Reaching a limit is reported as that limit, not as the end of the list.
  * `oxinsider.pagination_checkpoint(error)` returns the same snapshot from a failure, and the original error is re-raised unchanged. An `except RateLimitedError` keeps working, and after a rate limit, an expired cursor, or a dropped connection you can resume from `checkpoint.cursor` instead of page 1.
  * `client.paginate_pages(...)` yields whole envelopes (`total`, `has_more`, `meta`) instead of items.

  The filters are sent unchanged on every page. Only the cursor changes.

  **Backward compatible** for a correct server, and the server did not change.

  **What to change:** Nothing for normal pagination, including a call that passes `cursor=` to resume. If you called `paginate` on an operation that is not a cursor-paginated list, you now get a `PaginationError` naming the envelope instead of an empty iterator. For example, [`GET /api/v1/whale-trades/{id}/counterparties/executions`](/api-reference/endpoint/get-whale-trade-counterparty-executions) and [its makers route](/api-reference/endpoint/get-whale-trade-counterparty-makers) take a `cursor` but answer `object: "counterparty_analysis"` with an object `data`, so call their methods directly.
</Update>

<Update label="September 22, 2026" description="The Pick of the Day ledger is public: no API key, no subscription">
  [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) no longer requires authentication. It now answers any caller, like `GET /api/v1` and `GET /api/v1/platforms`, and its operation in the OpenAPI document has an empty `security`.

  Before this change, it needed a Pro API key. The key protected nothing, because the ledger exists to be republished. It also made the public record depend on one key: on September 22 a routine key rotation revoked the mirror's key, and [github.com/0xinsider/picks](https://github.com/0xinsider/picks) stopped recording commitments for 3 hours.

  * The response is byte for byte the same. A live pick is still `sealed`, with no nonce, payload, side, or price. A settled pick is still `opened`, with the nonce and the canonical payload.
  * The key never hid a live pick's side. The server reads the nonce and the payload only for a pick whose outcome is no longer `pending`, and checks that on every row before serving it.
  * A request that still sends `Authorization` is served, not refused, even with a revoked or expired key. A mirror with a dead key starts working again without any change.
  * Requests without a key count against the shared per-IP limit for public routes, not against a key's quota.

  **Backward compatible.** Nothing a client sends has to change.

  **What to change:** Nothing. `@0xinsider/sdk` 0.3.1 calls `getPickOfTheDayLedger()` with no key configured.
</Update>

<Update label="September 22, 2026" description="GET /api/v1/positions filters by wallet, so a portfolio no longer pages the whole board">
  [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) takes a new query parameter, `wallet`, that returns the positions of one wallet or a book of up to 25. The response is the same list of positions, in the same order and with the same cursor, limited to those wallets, and a page costs the same however large the board is.

  Before this change, there was no wallet filter. The [portfolio recipe](/recipes/portfolio-tracker) paged the whole board and kept the rows it recognized, which also missed every wallet whose positions were under the board's default `min_size` of \$100.

  * Repeat the parameter up to 25 times (`wallet=a&wallet=b`), use the `wallet[]` form, or send comma-separated values in one parameter.
  * Each value is a wallet address, a known username, or a `trd_` trader id, resolved the same way as the path of [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader).
  * The order is unchanged: `current_value_usd` descending, then wallet, market, and outcome.
  * With `wallet` present, `min_size` defaults to `0` instead of `100`, so you get all of the wallet's open positions, not only those worth \$100 or more. Send `min_size` to keep a floor. Without `wallet`, nothing changes.
  * An address 0xinsider does not track still returns any positions stored for it, or an empty list. A username or trader id that matches nothing answers `404` with `error.param` `wallet`. More than 25 values answers `400`.
  * The route still does not return closed positions, positions 0xinsider has not valued yet, or positions in markets with more than two outcomes.

  **Backward compatible.**

  **What to change:** Nothing, unless you want the filter. `@0xinsider/sdk` `listPositions` accepts `wallet: string[]` and `"wallet[]": string[]`. A portfolio tracker that scanned the board should call the route once per wallet, or once per book of 25, and drop its own filter; the recipe now does.
</Update>

<Update label="September 22, 2026" description="Event replay takes four filters, and expand=trade adds the full trade to each event">
  [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) takes four new query parameters that narrow the stream, and a new `expand=trade` that puts the full trade on every event. The order events come back in, and how far back you can read, are unchanged.

  Before this change, the route had no filters, and reading the trade behind an event took one call to [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) per row.

  * `trader`: a wallet address, a `trd_` id, or a username. A trader 0xinsider does not know matches nothing.
  * `condition_id`: the raw provider id, or its `mkt_` id.
  * `min_grade`: `S` to `F`, read from the wallet's newest ranking.
  * `min_size`: the trade size in USD.

  The filters are bound to the cursor, so you cannot add or change one once you have started reading.

  * `next_cursor` from a filtered page carries the filters that produced it.
  * Sending that cursor with a different set of filters, including none, answers `400 bad_request` with `error.reason` `cursor_expired` and `error.param` `cursor`.
  * Every cursor issued before today is bound to no filters, so adding a filter after you have started is refused the same way.
  * `meta.replay.filters` shows the filters in effect on a filtered page.

  `expand=trade` adds a `trade` object to every event: the same object [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) returns for that row. 0xinsider reads them with one query per page, so a page of 100 events costs 1 request instead of 101. The parameter is repeatable and `trade` is its only value.

  * `payload` still holds the facts as they were when the event happened.
  * `trade.trader.grade`, `trade.trader.username`, `trade.signal_score`, `trade.recorded_signal_score`, `trade.suspicion_*`, and `trade.market` are read when you make the request, so they can differ from the event.
  * `trade` is `null` when the row's trader or market is not in 0xinsider's database yet. That is the same row the detail route answers `404` for.
  * `meta.replay.expand` lists the expansions applied. `expand` is not bound to the cursor.
  * Measured locally, a 100-event page with `expand=trade` was 131 KB in 148 ms. The plain page plus 100 detail calls was 159 KB in 59.5 s.

  One behavior change on every read: after a page that is not full, `next_cursor` and `meta.replay.to_cursor` now move past the last event on the page, up to the newest position where every write has finished. An empty page therefore still moves forward, so a filter that matches little stops re-reading the same range on every poll.

  `meta.replay.to_sequence` is still the last event's id, and it can sit behind the position the cursor encodes.

  **Backward compatible.** The four filters and `expand` are new parameters, and the response fields you already read are unchanged.

  **What to change:** Nothing, if you store `next_cursor` opaquely. To start using a filter, request the first page again without a cursor.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.3.0: every method is typed from its own API operation">
  `@0xinsider/sdk` 0.3.0 types every convenience method from that operation's own entry in the OpenAPI document. That covers the path parameters it takes, the query keys and values it documents, the body it requires, and the envelope it answers with, including `data` and `meta`. `call`, `list`, `paginate`, `paginatePages`, and `collect` get the same types when you pass a literal operation id.

  Before this change, `exploreMarkets` rows were typed as market intel rows and `listInsiderRadar` rows as webhook rows, and `listInsiderRadar` accepted a `min_grade` that the route ignored.

  * `exploreMarkets` rows are now `ExploreEntry`, and `listInsiderRadar` rows are now `RadarFlag`.
  * `listInsiderRadar` takes the route's own query, `min_suspicion` and `severity`.
  * A batch response's `meta` is `BatchResponseMeta` (`request_cost` and `rate_limit`), and the event replay's is `EventReplayMeta`. `meta` is always present, as the API contract says.
  * Every method takes `signal`, `timeoutMs`, `maxRetries`, and `headers` after its own parameters, so a deadline, a cancellation, or an `If-None-Match` looks the same on a list read as on a single read.
  * Webhook ids are `number`, which is what the document declares.
  * Five generated maps are exported for your own code: `OperationPath`, `OperationQuery`, `OperationBody`, `OperationData`, and `OperationResponse`.

  A query key the route does not document, a missing path parameter, or a wrong body is now a compile error instead of a request the server ignores or refuses. Nothing on the wire changes.

  **What to change:** Drop the type argument from any convenience call, so `client.getTrader<MyTrader>(...)` becomes `client.getTrader(...)`, and drop it from `paginate<T>` when the operation id is a literal. Read `meta.field` instead of `meta?.field`. `call<T>`, `list<T>`, `paginate<T>`, `paginatePages<T>`, and `collect<T>` keep the loose form, for an operation you pick at runtime or a shape you assert yourself.
</Update>

<Update label="September 22, 2026" description="Python and Go SDKs 0.2.0 cover all 64 API operations">
  `0xinsider` 0.2.0 on PyPI and `github.com/0xinsider/0xinsider-go` 0.2.0 are regenerated from the current OpenAPI document, which has 64 operations. Seven routes that had no method in the published clients now have one.

  Before this change, you reached those seven through `client.request()` in Python, or your own `http.Request` in Go.

  * [`POST /api/v1/agents/register`](/api-reference/endpoint/register-agent): `register_agent()` in Python, `RegisterAgentWithResponse` in Go, both answering `201` with the sandbox key.
  * [`GET /api/v1/trader/{address}/categories`](/api-reference/endpoint/get-trader-category-records)
  * [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger)
  * [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders)
  * [`POST /api/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`](/api-reference/endpoint/redeliver-webhook-delivery), which takes `Idempotency-Key`.
  * [`GET /api/v1/market/{condition_id}/context.md`](/api-reference/endpoint/get-market-context-markdown): Python returns the Markdown as `str` and asks for it with `Accept: text/markdown`, and Go leaves it in `Body`.
  * [`GET /api/v1/me`](/api-reference/endpoint/get-account-identity)

  In Python, `GET /api/v1/mcp` no longer has a method. The document lists that route only to refuse it (`405`, because there is no server-to-client stream), so the method could only raise. The route stays in `oxinsider.OPERATIONS`.

  Each release now names the document it was generated from, so you can tell whether it is behind the API.

  * Python exports `oxinsider.OPENAPI_SHA256` (the SHA-256 of the document bytes), `oxinsider.OPENAPI_VERSION`, `oxinsider.OPERATION_COUNT`, and `oxinsider.APP_COMMIT` (the `0xinsider/0xinsider` commit that last changed the document, or `None`).
  * Go exports `oxinsider.OpenAPISHA256`, `OpenAPIVersion`, `OperationCount`, and `AppCommit`.
  * Compare the hash with `shasum -a 256` of [the live document](https://0xinsider.com/api/v1/openapi.json).
  * Both 0.2.0 releases were generated from SHA-256 `8d648c728b525eea69814f0bb0056aa05b447525a32ec868fef7711f903aa4b5`, at app commit `a138487e97eb1681d38a624d2bc4f12175584673`.

  **Backward compatible.** No method was removed or renamed, and every existing call sends the request it sent before. The one exception is Python's two `context.md` methods, which now send `Accept: text/markdown`, and those routes serve Markdown either way.

  **What to change:** Nothing. The server did not change, and if you called one of the seven routes by hand, you can switch to its method.
</Update>

<Update label="September 22, 2026" description="The stream closes with an error frame when its key stops working">
  [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) now re-checks its API key every 30 seconds while the connection is open. If the key is revoked, expires, or is rotated, or the account is deleted, locked, or no longer subscribed, the stream sends one final `event: error` frame within 40 seconds and closes.

  Before this change, the key was checked only when the stream opened, and an open stream kept delivering until the client disconnected.

  * The frame's `id` is the last `seq` the stream sent.
  * Its body is `{ "type": "error", "error": { "code", "message", "doc_url", "reason", "retry_at" }, "retry": <boolean> }`.
  * `error` is the error a reconnect with the same key would get: `invalid_api_key` (`401`), `subscription_required` (`402`), `forbidden` (`403`, the account was deleted), `account_locked` (`423`), or `insufficient_scope` (`403`, an OAuth token without the `read` scope). All of these carry `retry: false`.
  * If 0xinsider cannot confirm the key for 90 seconds, the stream ends with `database_unavailable` and `retry: true`. Reconnect after `error.retry_at` with `Last-Event-ID` set to the frame's `id`, and the stream resumes where it stopped.

  **Backward compatible.** Data frames, `resync` markers, keep-alives, and the responses you get when you connect are unchanged.

  **What to change:** If you read the stream without the SDK, handle `event: error` and stop reconnecting when `retry` is `false`, because every reconnect will be refused the same way. A browser `EventSource` also fires its own `error` event when the connection fails, so check `event.data` before you parse it.

  `@0xinsider/sdk` needs no change: `streamFeed()` yields the frame with `type` `error`, and `streamFeedResilient()` already stops on a `401`, `402`, `403`, or `423`.
</Update>

<Update label="September 22, 2026" description="Pick of the Day is priced at a flat $1,000 stake instead of $100">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) now value every pick at a flat stake of \$1,000. That applies to every pick ever published.

  Before this change, the record was stated at a \$100 stake.

  * `stake_usd` is new on every priced pick and on `hit_rate`. Its value is `1000`.
  * `return_usd` is new on every priced pick. It is the gross return of that stake: a win pays `stake_usd / backed_price`, a loss pays `0`, and a void refunds the stake.
  * `payout_display` and `profit_display` are now formatted from `return_usd`.
  * `hit_rate.net_profit_usd`, `staked_usd`, `series[].net_profit_usd`, and the per-day `net_profit_usd` on the stats route are ten times what the same record read at \$100.
  * `return_per_100` keeps its name and its meaning, `100 / backed_price`. `unit_score`, `roi_pct`, `pct`, and every CLV field do not depend on the stake and are unchanged.

  The public ledger at [github.com/0xinsider/picks](https://github.com/0xinsider/picks) recomputes its `index.json` record on the same basis. It renames `record.profit_per_100` to `record.profit_usd` and adds a new `record.stake_usd`.

  **Backward compatible.** Nothing was removed from the wire: the two fields are new, and the fields whose values moved keep their names and types.

  **What to change:** Nothing, if you read `return_per_100` and scale it to your own stake, or read `roi_pct` and units. If you displayed `payout_display`, `profit_display`, `net_profit_usd`, or `staked_usd` as a figure per \$100, read `stake_usd` rather than assuming the stake. `@0xinsider/sdk` 0.2.6 types the new fields.
</Update>

<Update label="September 22, 2026" description="GET /api/v1/me reports whether paid data access is active or lapsed">
  [`GET /api/v1/me`](/api-reference/endpoint/get-account-identity) adds two things: `credential_status`, which says the credential itself is valid, and an `entitlement` object holding `paid_data_access` and `recovery_action`. `paid_data_access` reads `active` or `lapsed`, and `recovery_action` is `renew_subscription` when renewing the subscription is what restores access.

  Before this change, the response stopped at account and credential identity, and a valid credential with no active Pro subscription got `402`.

  [`GET /api/v1/usage`](/api-reference/endpoint/get-usage) is available to that credential too. It still reports the credential's rate-limit, daily-usage, and monthly-quota state.

  **Backward compatible.** The existing fields stay, and you can ignore the new ones. Paid data routes still answer `402 subscription_required` while access is lapsed, and revoked, expired, unknown, deleted, locked, and insufficient-scope credentials keep the refusals they had.

  **What to change:** Nothing. To tell a lapsed subscription from a broken credential, read `/api/v1/me`, then read `/api/v1/usage` when you need the budget.
</Update>

<Update label="September 22, 2026" description="Insider Radar takes mode=stable, a cursor tied to one published set of scores">
  [`GET /api/v1/insider-radar`](/api-reference/endpoint/get-insider-radar) takes a new `mode` parameter. `mode=stable` ties the cursor to one published set of scores, together with the limit, `min_suspicion`, `severity`, and the exact score boundary of the page.

  Before this change, the route had one behavior, the one now called `mode=live`. It is still the default.

  A `mode=stable` cursor answers `400` with `error.reason` `cursor_expired` once 0xinsider republishes the scores, or once you change the filters. Request the first page again.

  **Backward compatible.** Existing live requests, response fields, and live cursors all keep working.

  **What to change:** Nothing for a live read. To review several pages that all describe the same set of scores, send `mode=stable` and keep the same mode and filters on every request that follows the cursor.
</Update>

<Update label="September 22, 2026" description="Webhooks get staged secret rotation with a 1-hour signature overlap">
  Webhook signing secrets get a three-step rotation: [`POST /api/v1/webhooks/{id}/rotate-secret/prepare`](/api-reference/endpoint/prepare-webhook-secret), then [`activate`](/api-reference/endpoint/activate-webhook-secret), then [`retire`](/api-reference/endpoint/retire-webhook-secret). Webhook objects also carry two new fields, `secret_rotation.status` and the nullable `secret_rotation.overlap_expires_at`.

  Before this change, the only rotation replaced the current secret at once, and a read of the webhook said nothing about rotation.

  * `prepare` returns a new `signing_secret` once, while the current secret stays active.
  * `activate` promotes the new secret and returns it once. Deliveries then carry comma-separated `v1` HMAC candidates for a 1-hour overlap.
  * `retire` ends the overlap and stops the previous secret from authorizing any later delivery.
  * Changing the webhook's endpoint URL clears the staged state.
  * The immediate [`POST /api/v1/webhooks/{id}/rotate-secret`](/api-reference/endpoint/rotate-webhook-secret) is still there, for replacing a secret in an emergency.

  **Backward compatible.** A client that keeps using immediate rotation, or ignores the new fields, sends the same requests it sent before.

  **What to change:** Nothing, unless you want a rotation that drops no deliveries. For that, deploy the secret `prepare` returned, call `activate`, accept either signature candidate while the overlap lasts, and call `retire` once the new secret is live everywhere.
</Update>

<Update label="September 22, 2026" description="Webhook delivery records add retry_schedule_reason beside next_attempt_at">
  [`GET /api/v1/webhooks/{id}/deliveries`](/api-reference/endpoint/list-webhook-deliveries) and the success response from `POST /api/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver` add a nullable `retry_schedule_reason` next to `next_attempt_at`. It says what set the time in `next_attempt_at`.

  Before this change, you could see when a pending retry was due, but not why it was due then. That time can come from the receiver asking for it, a transient failure, a permanent or auth response, a manual redelivery, or a change to the endpoint's configuration.

  `retry_schedule_reason` is `null` while an attempt is in flight, and after the delivery reaches its final state.

  0xinsider now also reads `Retry-After` from a receiver's `408`, `429`, or `5xx` response, as delta-seconds or an HTTP-date, and clamps the wait it asks for to between 60 and 3,600 seconds. A missing, malformed, past, or non-retryable hint keeps the ordinary retry schedule. The 8-attempt delivery budget, the endpoint failure budget, and the fairness limits are unchanged.

  **Backward compatible.** No request changes, and you can ignore the new field.

  **What to change:** Nothing. If you read delivery records, you can branch on `retry_schedule_reason`, and treat `next_attempt_at` as the earliest time the next attempt runs.
</Update>

<Update label="September 22, 2026" description="Sharp money flow cursors expire when the filters or the data behind them change">
  [`GET /api/v1/markets/sharp-money-flows`](/api-reference/endpoint/sharp-money-flows) and its deprecated [`smart-money-flows` alias](/api-reference/endpoint/smart-money-flows) now tie each opaque `smf_` cursor to the filters of the first page, and to the data that page was built from.

  Before this change, the cursor carried only the first page's time bound and its position in the results. A changed filter or a late correction could move you onto different data, with nothing in the response to say so.

  A request answers `400` with `error.reason` `cursor_expired` when you change a filter, when 0xinsider rebuilds the wallet rankings, or when it corrects the flow totals. A cursor issued before this change gets the same response.

  **Breaking** for `smf_` cursors stored before this change. The route aliases, the `ETag`, and the response rows keep the shapes they had.

  **What to change:** Keep storing `next_cursor` opaquely. When `error.reason` is `cursor_expired`, discard the cursor and request the first page again.
</Update>

<Update label="September 22, 2026" description="Market candles URL-decode the query and refuse a from that is later than to">
  [`GET /api/v1/market/{condition_id}/candles`](/api-reference/endpoint/get-market-candles) now URL-decodes the query fields and values it recognizes. A request whose `from` is later than its `to` answers `400`.

  Before this change, the route did not decode a percent-encoded field name or value, and it accepted an inverted interval.

  * `from` is still an exclusive lower bound in Unix seconds, and `to` is still inclusive.
  * A recognized field sent more than once uses the last value.
  * An unknown field is ignored.

  **Breaking** for a request that sends `from` after `to`. A query written out literally returns the same fields, with the same inclusive and exclusive bounds.

  **What to change:** Correct any interval whose `from` is later than its `to`.
</Update>

<Update label="September 22, 2026" description="Trending wallet cursors are tied to the limit, the window, and one ranked board">
  [`GET /api/v1/leaderboard/trending`](/api-reference/endpoint/list-trending-wallets) now ties `next_cursor` to the `limit` and `window` you sent, and to the ranked board it was issued against.

  Before this change, the opaque cursor held only a page number. You could change `limit` or `window` and keep reading against a different slice, and a board refresh could mix rows from different boards across pages.

  A cursor sent with a different `limit` or `window`, or after the board has been rebuilt, answers `400` with `error.reason` `cursor_expired`. A page-only cursor from before this change is refused the same way.

  **Breaking** for cursors you already hold. The route and the response fields are unchanged.

  **What to change:** Send the same `limit` and `window` on every request that follows a `next_cursor`. When a cursor is refused, discard it and request page one again.
</Update>

<Update label="September 22, 2026" description="Python SDK 0.2.0: download_trader_export follows the redirect and streams the file">
  `0xinsider` 0.2.0 on PyPI (import `oxinsider`) changes what `download_trader_export(address, job_id=...)` returns. It now returns a `Download` that streams the export file.

  Before this change, the method raised the `302` from [`GET /api/v1/trader/{address}/export/download`](/api-reference/endpoint/download-trader-export) as `OxinsiderApiError`. That route answers `302` with a short-lived signed location once the job is `ready`, and the SDK went through its JSON call path and never followed it.

  The API request carries the bearer token. The redirect is followed once, with a fresh request that sends no `Authorization` and no `Cookie`, whatever `httpx.Client` you passed in.

  * `iter_bytes()` yields the decoded file. The object is served with `Content-Encoding: gzip`.
  * `iter_raw()` yields the bytes as they were sent.
  * `save(path, decode=)` writes the file and returns `SavedDownload`, with `bytes_written` and the SHA-256 of exactly what it wrote.
  * `read(max_bytes=)` refuses more than 64 MiB by default.
  * `content_type`, `content_encoding`, `content_length`, `filename` (from `Content-Disposition`), and `etag` are the file host's own headers. The API publishes no manifest or checksum for an export, and `etag` identifies the object, not its contents.

  Every download failure raises `DownloadError`, whose message names the host and never the signed URL.

  * `insecure_location`: the location is plain `http://` on a host other than loopback. Nothing is requested.
  * `unexpected_redirect`: the file host answered with a second redirect.
  * `unavailable`: the file host answered with a non-2xx, carried on `status`. A `403` means the location has expired, so call the operation again.
  * `interrupted`: the transfer stopped part way.
  * `too_large`: a `read` went past `max_bytes`.
  * `not_redirected` and `missing_location`: the API's answer was not a redirect.

  A job that is not `ready` still raises the API's own `BadRequestError` (`400`), and an unknown job still raises `NotFoundError`. `redirect_api_openapi_spec()`, the other redirect-only operation (`307` to the web origin), returns a `Download` the same way.

  **Breaking** for those two methods. Every other operation is unchanged, and the server did not change.

  **What to change:** If you caught the `302` as `OxinsiderApiError` and read `error.body` or the header yourself, take the `Download` instead and call `save` or iterate it. Every request to the API origin now sets `follow_redirects=False`, so a custom `httpx.Client` with redirect following turned on no longer follows a redirect on the SDK's behalf.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.7: verifySignature throws on a NaN, infinite, or negative tolerance">
  `@0xinsider/sdk` 0.2.7 changes `verifySignature()` for a misconfigured receiver. A `toleranceSeconds` of `NaN`, `Infinity`, `-Infinity`, or any negative number now throws a configuration error before any check runs.

  Before this change, `NaN` and `Infinity` turned the replay window off without saying so, because `Math.abs(now - timestamp) > NaN` is never true. A correctly signed delivery of any age verified as `true`, and a negative tolerance rejected everything.

  Nothing else changed:

  * The default is still 300 seconds.
  * A delivery exactly `toleranceSeconds` old still passes, and 1 second older still fails.
  * `0` still accepts only the current second.
  * A bad signature or a malformed timestamp still returns `false`.
  * The HMAC recipe and the constant-time compare are the same.
  * The server did not change.

  A receiver that passes a fixed, finite, non-negative tolerance sees no difference.

  **What to change:** If you compute the tolerance from configuration, expect a throw when that configuration is broken. Log it as your own bug, rather than answering the delivery as though it were forged.
</Update>

<Update label="September 22, 2026" description="A per-IP 429 answers the error envelope, with reason ip_rate_limited or ip_throttled">
  A `429` from the per-address budget on any `/api/v1` route now answers the standard error envelope. That budget is the 1,200 requests a minute every caller behind one IP address shares, counted before authentication, on public routes and on a refused credential alike.

  Before this change, those responses carried a flat body, `{"error":"too_many_requests","reason":"rate_limited"}` or `{"error":"rate_limited","reason":"ip_throttled",...}`, with no request id and no `retry_at`. The API reference had never described that shape. It promised the envelope on every `429`.

  * `error.code` is `rate_limited`, as before.
  * `error.reason` is `ip_rate_limited` for the per-minute budget, and `ip_throttled` for an address in a cooldown, which runs for minutes to days after sustained over-limit traffic.
  * `error.retry_at` and `meta.request_id` are now present, and `meta.request_id` equals `X-Request-Id`.
  * `Retry-After` and the `RateLimit-*` and `X-RateLimit-*` headers describe the same bucket they described before.
  * A `429` from your own key's window still carries no `error.reason`, and the monthly quota keeps `monthly_quota_exceeded`.

  `@0xinsider/sdk` 0.2.6 lists both reasons in `API_ERROR_REASONS`, and raises both as `RateLimitedError` with `reason` set. [Errors](/errors) and [Rate limits](/rate-limits) have the tables.

  **Backward compatible.** `rate_limited` and `Retry-After` are unchanged, and the two reasons are new values.

  **What to change:** Nothing, if you branch on `error.code` or sleep for `Retry-After`. If you read `error` at the top level of a `429` body, read `error.code` instead.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.6 retries a write only on the five keyed write operations">
  `@0xinsider/sdk` 0.2.6 changes which failed requests it repeats. A write is retried on `429`, `502`, `503`, `504`, or a network error only when it is one of the five operations that declare the `Idempotency-Key` header and it carries a key. Every attempt then sends the same key and the same request bytes.

  Before this change, any request carrying an `Idempotency-Key` was retried, including `verifyWebhook`, which posts a challenge to your URL on every attempt, and `submitTraderExport`, which starts a job. The API does not read the header on either one.

  * The five keyed writes are `createWebhook`, `updateWebhook`, `deleteWebhook`, `rotateWebhookSecret`, and `redeliverWebhookDelivery`, exported as `IDEMPOTENT_WRITE_OPERATIONS`.
  * `idempotencyKey`, or an `Idempotency-Key` header you set yourself, on any other operation now throws before the request is sent, and the message names the five.
  * The read-only `POST /api/v1/traders/batch` and `POST /api/v1/markets/intel/batch` are now retried the way a `GET` is. They store nothing, and each attempt is one request against your quota, exactly as a `GET` is. Before this change, no batch `POST` was retried.
  * Every other `POST`, `PATCH`, and `DELETE` is retried only as one of those keyed writes.
  * `retryEligibility(operation)` returns `"read"`, `"keyed"`, or `"never"`, and `READ_ONLY_POST_OPERATIONS` is exported.

  The README now says how to settle an unknown outcome on a keyed write: resend the same key and body, or read the resource. It also says that a key gives you a safe replay, not exactly-once delivery. The server did not change.

  **What to change:** Remove `idempotencyKey` from any operation outside those five.
</Update>

<Update label="September 22, 2026" description="Python and Go SDKs 0.2.0 send a bearer over https only, or http to loopback">
  `0xinsider` 0.2.0 on PyPI and `github.com/0xinsider/0xinsider-go` 0.2.0 refuse to send an API key or an OAuth access token over plain `http://` to any host other than loopback (`localhost`, `127.0.0.0/8`, `[::1]`).

  Before this change, both sent the bearer token wherever the base URL pointed, so a mistyped `http://` base or a plain-http proxy received the key. The TypeScript SDK has refused that since 0.1.

  In Python, `oxinsider.Client(api_key=..., base_url="http://...")` raises `InsecureTransportError` from the constructor, before any request. Every request is checked again on its final URL, so a key set later through `client.api_key = ...`, an `Authorization` passed in `headers=`, or an `auth=` on your own `httpx.Client` is refused the same way.

  The SDK never follows a redirect on its own, and `Client.download` refuses a `Location` that is not `https://` or loopback `http://`, so an `https://` answer cannot downgrade a credentialed request.

  In Go, `WithBearerToken` fails the request with `*InsecureTransportError` before it is sent. The HTTP doer that `oxinsider.New` installs refuses any request carrying `Authorization` to such a destination, whichever code set the header. The client's `CheckRedirect` refuses a credentialed redirect to plain `http://` and sets `Redirect` to `true` on the error, which is the hop where `net/http` would otherwise copy the header onto a same-domain downgrade.

  Both errors name the destination and never the credential.

  **Breaking** for one configuration: a credentialed SDK pointed at `http://<remote host>` now fails before the request, instead of sending the key in clear. A keyless client still calls the public operations (`GET /api/v1`, health, and platforms) on any base, the sandbox client is unchanged, and the server did not change.

  **What to change:** Move that base URL to `https://`, or to a loopback address when you run the backend yourself.
</Update>

<Update label="September 22, 2026" description="Go SDK 0.2.0 adds OpenStream and refuses the buffering GetStreamWithResponse">
  `github.com/0xinsider/0xinsider-go` 0.2.0 adds `OpenStream(ctx, params, opts...)`, the way to read [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) from Go. It returns a `*StreamReader` that hands you each frame as it arrives.

  Before this change, the only typed method was the generated `GetStreamWithResponse`, which read the body to EOF. On a healthy stream it never returned, and its buffer grew without bound.

  * `Next()` returns the next `StreamFrame`, with `Seq`, `Type`, `PublishedAt`, `Data`, and `Resync`.
  * The reader holds at most `WithMaxFrameBytes` (1 MiB by default) for one undelivered frame, and it closes the connection when the context is cancelled or you call `Close()`.
  * `LastSeq()` is the sequence to send back as `Last-Event-ID`. `RetryHint()` gives the SSE `retry:` field, and `Response()` carries the headers.
  * A frame that breaks the SSE contract ends the stream with a `*StreamProtocolError` carrying `LastSeq`, `FrameID`, and a `Reason`: `unexpected_media_type`, `invalid_json`, `invalid_envelope`, `unusable_sequence`, `invalid_resync`, or `frame_too_large`. A malformed frame never moves the cursor.
  * A non-200 answer is a `*StreamHTTPError` with the decoded error body and the headers, such as `Retry-After`. At most 64 KiB of that body is read.
  * Keep-alive comments are consumed silently, multi-line `data:` is joined, LF and CRLF framing both parse, and the SSE `id` is the sequence when the envelope carries no `seq`.

  This is the same decoder contract the TypeScript SDK has had since 0.2.5.

  **Breaking** for one call: a client built with `oxinsider.New` now refuses `GetStream` and `GetStreamWithResponse` on `/api/v1/stream` with `ErrStreamBuffered`, before any request, instead of hanging. Every other operation is unchanged, and the server did not change.

  **What to change:** Read the stream with `OpenStream`. If you want the raw `*http.Response`, call `GetStream` with `RawStreamContext(ctx)`, and own the body yourself.
</Update>

<Update label="September 22, 2026" description="A weekly report range is capped at 31 days and comes back marked ephemeral">
  [`GET /api/v1/reports`](/api-reference/endpoint/get-reports) and [`GET /api/v1/reports/weekly`](/api-reference/endpoint/get-weekly-report-snapshot) now answer an explicit weekly `from,to` range with the exact inclusive UTC range you asked for, for up to 31 days. That body carries the new `snapshot.storage` field set to `ephemeral`, and `snapshot.version` set to `0`. A wider range answers `400` with `error.reason` `invalid_query`.

  Before this change, every explicit range was stored and identified as a durable snapshot, and a range of any width was accepted.

  An ISO `YYYY-WW` request is unchanged. It still returns a durable canonical snapshot, as do the canonical daily and monthly requests.

  **Backward compatible.** `snapshot.storage` is a new field. The one behavior change is that an explicit range wider than 31 days is now refused.

  **What to change:** Ask for an ISO week when you need a durable snapshot you can cache. For an explicit range, keep it inside 31 inclusive UTC days, branch on `snapshot.storage`, and do not treat a `snapshot.version` of `0` as a durable cache key. Handle `invalid_query`, or split a wider request into ranges the route accepts.
</Update>

<Update label="September 22, 2026" description="A report reads final only once its body was built after snapshot.final_after">
  [`GET /api/v1/reports`](/api-reference/endpoint/get-reports) and the [daily](/api-reference/endpoint/get-daily-report-snapshot), [weekly](/api-reference/endpoint/get-weekly-report-snapshot), and [monthly](/api-reference/endpoint/get-monthly-report-snapshot) routes now answer `snapshot.status` `final` and `completeness.status` `complete` only when the served body was read from the source at or after `snapshot.final_after`. That instant is the range's UTC close plus 7,530 seconds, which is how long a whale trade has to reach the report's source.

  Before this change, a range that had ended on the calendar read `final` even when its body was built before the range ended. A daily report refreshed at 23:58Z was frozen at midnight, without the last minutes of the day or anything that arrived after them.

  A closed range still inside that budget now reads `rolling` and `partial`. It keeps refreshing every 5 minutes, is rebuilt once after `final_after`, and is frozen from then on.

  * `snapshot.period_closed` is new: the calendar range has closed.
  * `snapshot.final_after` is new: the earliest instant a final body can be built.
  * `snapshot.source_read_started_at` is new: when the body you are reading was read from the source. It is `null` for a body stored before that clock existed.
  * `snapshot.mutable_until` on a rolling body is now the date of `final_after` rather than the range's last day. It is still `null` once the body is final.

  **Backward compatible.** Enums, field names, and types are unchanged.

  **What to change:** If you waited for UTC midnight to read a settled daily figure, wait for `final_after` instead (02:05:30Z for a daily report), or poll until `snapshot.status` reads `final`. If you key a cache on `snapshot.version`, change nothing, because a final version is still immutable.

  A trade that arrives after the budget is a pipeline fault and does not move a final body, and a correction arrives as a new `snapshot.version`.
</Update>

<Update label="September 22, 2026" description="Event replay orders by commit time, so a stored cursor no longer skips a trade">
  [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since) now orders events by the moment each write became visible to every reader, then by `whale_alerts.id`. A page never reads past the oldest write that was still open when the page was built.

  Before this change, events were ordered by `whale_alerts.id` alone, and `next_cursor` was the largest id on the page. Ids are handed out when a write starts, not when it finishes, so a trade with a lower id whose write finished later fell below the stored cursor and was never delivered. It is now delivered on a later request, once every older write has finished.

  * `meta.replay.ordering` reads `commit_visibility_then_id_asc` instead of `whale_alerts_id_asc`.
  * `data[].sequence` (the `whale_alerts.id`) no longer always increases across a replay, so a lower id can follow a higher one.
  * `data[].cursor` and `next_cursor` use a new opaque format.
  * `meta.replay.pending_beyond_horizon` is new. It is `true` when committed trades are still held behind an open write, so a `caught_up` page with it set to `true` is not the end of the stream. Poll again.
  * `data[].id` is unchanged and is still the field to deduplicate on.

  Every other field name, type, and the envelope are unchanged. A cursor stored before this change stays accepted for good, and the first page it returns uses the new format.

  Expect the replay to trail the newest trade by the longest write still open on the database. That is seconds in normal operation, and the length of a backfill while one runs. `pending_beyond_horizon` tells you which of the two you are looking at.

  **What to change:** Nothing, if you store `next_cursor` opaquely and deduplicate on `data[].id`. If you ordered or deduplicated on `data[].sequence`, move to `cursor` and `id`, because `sequence` would drop exactly the trades this change recovers.
</Update>

<Update label="September 22, 2026" description="Every /api/v1 error now answers the standard envelope, the timeout included">
  Every `/api/v1` error response is now the standard envelope: `object`, `error.code`, `error.reason`, `error.param`, and `meta.request_id`. That includes the responses that used to carry plain text or nothing at all.

  Before this change, a body that was not JSON got a plain-text `400`, and a body that missed a required field or had a wrong type got a plain-text `422`. The timeout returned an empty body, and the docs told you to check the status before parsing.

  These all keep `error.code` `bad_request`:

  * `invalid_body`, on a `400`: the body is not JSON, misses a required field, or has a wrong type. `error.param` names the JSON path (`traders`, `traders[0]`) or `body`.
  * `invalid_query` and `invalid_path`, on a `400`: a query parameter or a path segment does not parse. `error.param` names it.
  * `unsupported_media_type`, on a `415`: the body arrived without `Content-Type: application/json`.
  * `payload_too_large`, on a `413`: the body is over 1,048,576 bytes.
  * `method_not_allowed`, on a `405`: the path does not serve that method. The response carries the `Allow` header.

  The 30-second timeout keeps its `408` status and now carries the envelope too, with the new `error.code` `request_timeout`. On a `GET` or `HEAD` it also carries `Retry-After: 5` and `error.retry_at`. On a `POST`, `PATCH`, or `DELETE` it carries neither, because the request may have finished on the server after the timeout fired.

  `@0xinsider/sdk` 0.2.5 throws `ServerTimeoutError` on a `408` and retries it the way it retries a `503`: on a `GET`, or on a write that carries an `Idempotency-Key`. Its `API_ERROR_CODES` and `API_ERROR_REASONS` carry the new values. [Errors](/errors) has the full table.

  **Backward compatible.** `request_timeout` is a new code, the six reasons are new values under the existing `bad_request`, and every other response is unchanged.

  **What to change:** Nothing, if you switch on `error.code` and have a default branch. If you parsed the body only when the content type was JSON, you now get a body on these responses. After a `408` on a write, check whether it went through before you repeat it, and reuse its `Idempotency-Key`.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.5: a malformed stream frame now throws instead of being skipped">
  `@0xinsider/sdk` 0.2.5 changes how `streamFeed()` and `streamFeedResilient()` handle a `GET /api/v1/stream` connection that breaks the SSE contract. Five cases now throw `StreamProtocolError` instead of being passed over, each with its own `reason`.

  Before this change, a malformed frame was skipped and the next valid frame moved the resume cursor past the gap. A malformed `resync` became an empty marker, and an unusable sequence was yielded as `NaN`. A `2xx` response that was not an event stream read as an empty stream, which the reconnect loop retried as an outage.

  * `invalid_json`: the data frame's payload is not JSON.
  * `invalid_envelope`: the payload is not an object.
  * `unusable_sequence`: the envelope has no finite `seq` and the frame has no finite SSE `id`.
  * `invalid_resync`: a `resync` frame's payload is not a resync object.
  * `unexpected_media_type`: the response is `2xx` but its `Content-Type` is not `text/event-stream`.

  A new `maxFrameBytes` option (default `DEFAULT_MAX_STREAM_FRAME_BYTES`, 1 MiB) caps one undelivered frame. Past that size the connection closes and `StreamProtocolError` `frame_too_large` is thrown. The error carries `lastSeq` (the last sequence delivered on that connection, since a malformed frame never moves the cursor), `frameId`, `event`, `bytes`, and `mediaType`, and never the raw payload.

  **Backward compatible.** A client reading a well-formed stream sees no difference. Comment lines (`: keep-alive`), LF and CRLF framing, unknown SSE fields, and unknown but valid envelope `type` values are unchanged, and the server did not change.

  **What to change:** `streamFeedResilient()` treats `StreamProtocolError` as permanent, because resuming from `lastSeq` would replay the same frame. Decide in your own code whether to resume later, resume after `frameId`, or refetch state and attach to the live stream. `decodeStreamFrame()` and `isEventStreamMediaType()` are now exported if you decode frames yourself.
</Update>

<Update label="September 22, 2026" description="Pay as you go stops at 1,000,000 requests a month, and usage reports the ceiling">
  Pay as you go now has an upper bound. From October 1, 2026, an account with pay as you go on can make up to 1,000,000 authenticated requests in a UTC calendar month. Past that, requests answer `429` with `error.code` `rate_limited` and `error.reason` `monthly_quota_exceeded` until the month resets, exactly as they do for an account without pay as you go past 250,000.

  Before this change, pay as you go had no ceiling. Nothing is refused before October 1: until then the count and the notices run as they do today.

  * A refused request is not counted and not billed.
  * `GET /api/v1/usage` adds `ceiling` to `monthly_quota`: the number that binds this account once enforcement begins. It is `250000`, `1000000` with pay as you go on, or `null` for an admin account.
  * `limit` keeps its meaning, the requests Pro includes, and `X-Monthly-Quota-Limit` is unchanged.
  * The key owner is emailed once when the month reaches the ceiling. A higher ceiling is one reply to that email.

  **Backward compatible.** A client that reads `remaining` and stops at `0` changes nothing.

  **What to change:** If you run pay as you go and meant to send more than 1,000,000 requests in a month, pace yourself under the ceiling or reply to that email to have it raised.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.4: pagination throws on a broken cursor instead of ending early">
  `@0xinsider/sdk` 0.2.4 changes how `paginate()`, `paginatePages()`, and `collect()` handle a page that breaks the list protocol. Two broken pages now throw `PaginationError` before the page is yielded, each with its own `reason`.

  Before this change, a page that promised more rows but gave no cursor ended the run as if the collection were exhausted, and a repeated cursor was requested again with no limit.

  * `missing_cursor`: the page says `has_more: true` and has no usable `next_cursor`.
  * `repeated_cursor`: the page's `next_cursor` repeats a cursor already requested, caught before the duplicate request. The last 1,024 cursors are kept for this check (`CURSOR_HISTORY_LIMIT`).
  * Both errors carry the received `page`, the `cursor` it was requested with, `nextCursor`, `pagesFetched`, and `operationId`. `paginationResumePoint(error)` returns a resume point for them, as it does for a failed request.
  * A page with `has_more: false` still ends pagination, whatever `next_cursor` says.

  `client.list()` now throws `InvalidResponseError` (`code` `invalid_response`, a client-side code, with the body on `received`) when a list response has a non-boolean `has_more` or a non-string `next_cursor`. Before, `isListEnvelope()` only checked that `has_more` was present.

  `maxPages` is checked before the first request: it must be a positive integer or `Infinity`, and `0`, a negative, fractional, or `NaN` value throws `RangeError`. A new `progress` option (`PaginationProgress`) is updated as pages arrive. When pagination ends it says whether it stopped at `maxPages` (`stoppedBy: "max_pages"`, with `nextCursor` to continue from) or at the end of the collection (`"exhausted"`).

  **Backward compatible.** A client reading a well-formed server sees no change, and the server did not change.

  **What to change:** If your code relied on pagination ending without an error on a broken page, catch `PaginationError`.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.3: a long Retry-After on the stream no longer reconnects at once">
  `@0xinsider/sdk` 0.2.3 changes what `streamFeedResilient()` does with a `429` or `503` whose `Retry-After` is longer than the new `maxRetryAfterMs` option (default 60,000 ms, the ceiling the REST retries already used). It now throws `StreamRetryDeferredError` and spends none of `maxReconnects` on the wait. The error carries `retryAt` (the instant the wait ends), `retryAfterMs`, `lastSeq` (the seq to pass as `lastEventId` when you reconnect), and the refusal as `cause`.

  Before this change, it multiplied the header by 1,000 and handed the result to `setTimeout`. Node schedules any delay above 2,147,483,647 ms (about 24.8 days) as 1 ms, so a wait until the first of next month on `monthly_quota_exceeded` reconnected immediately. Pass `maxRetryAfterMs: Infinity` to hold the process instead: the wait then runs in timer-sized chunks and is never shortened.

  The stream and REST retry loops now read `Retry-After` through one parser, exported as `parseRetryAfter(header, nowMs?)`.

  * Both delta-seconds and an HTTP-date (RFC 9110) parse.
  * A negative, non-finite, or malformed value reads as no header at all, so the loop's own backoff applies.
  * A date in the past reads as `0`.
  * `RETRY_AFTER_CEILING_MS`, `MAX_TIMER_DELAY_MS`, and `DEFAULT_MAX_STREAM_RETRY_AFTER_MS` are exported.

  **Backward compatible.** Short waits, jitter, and abort behave as before, and the server did not change.

  **What to change:** If you catch `StreamReconnectsExhaustedError`, also catch `StreamRetryDeferredError` and schedule the reconnect at `retryAt`.
</Update>

<Update label="September 22, 2026" description="Remote MCP: GET answers 405, so clients stop reconnecting every second">
  `GET /api/v1/mcp` with a credential now returns `405 Method Not Allowed`, with `Allow: POST`, a JSON-RPC error body (`code` `-32004`), and `X-Mcp-Error-Code: -32004`. That is the MCP Streamable HTTP answer for a server with no server-to-client stream, which is the case here: this server sends no notifications. A conforming client, including the reference `@modelcontextprotocol/sdk`, reads the `405` as "no stream at this endpoint" and stops retrying.

  Before this change, the same `GET` returned `200` with `Content-Type: text/event-stream` and closed the body at once. Clients read the closed stream as a disconnect and reopened it about once a second, so a connected agent could spend tens of thousands of requests a day on empty reconnects. One did.

  Without a credential the `GET` still returns `401` with the `WWW-Authenticate` challenge, which is the entry point for the OAuth flow.

  **Breaking** for a client that opened the `GET` stream, on a request that never carried data. A client that only sends JSON-RPC over `POST` changes nothing.

  **What to change:** Accept `405` on `GET /api/v1/mcp` as normal; the reference SDK already does. `@0xinsider/sdk` 0.2.4 drops the `openMcpEventStream` row from `API_CLIENT_OPERATIONS` and the `Operations` types, because the operation no longer has a `200`.
</Update>

<Update label="September 22, 2026" description="Remote MCP notifications receive HTTP 202 with no JSON-RPC reply">
  `POST /api/v1/mcp` now answers a supported ID-less `ping`, `notifications/initialized`, and `notifications/cancelled` with HTTP `202` and no body. Other ID-less methods, and unsolicited response messages, answer HTTP `400` with no body. An unknown cancellation ID is ignored and does not stop an in-flight tool call.

  Before this change, an ID-less `ping` got a JSON-RPC success body with `id: null`.

  **Breaking** for a client that relied on `id: null` standing in for an omitted ID. This corrects the notification contract rather than adding a field.

  **What to change:** Send an `id` on `initialize`, `ping`, `tools/list`, or `tools/call` when you need a JSON-RPC reply, and the server echoes that string, number, or explicit `null` ID. To send a notification, omit `id` and do not parse the HTTP `202` body.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.2: a keyless sandbox client, and a base URL keeps its path">
  `@0xinsider/sdk` 0.2.2 adds `OxinsiderApiClient.sandbox()`, a client for the sandbox server `https://0xinsider.com/sandbox` (the second `servers` entry in the OpenAPI document). `new OxinsiderApiClient({ sandbox: true })` builds the same client, and `SANDBOX_BASE_URL` and `resolveApiUrl()` are exported.

  * Every operation runs with no `apiKey`.
  * The `X-Oxi-Sandbox: true` response header is lifted to `meta.sandbox`.
  * `query: { sandbox_status: 429 }` throws the same `RateLimitedError` production would, with `retryAfterSeconds` 60 and `retryAt`.
  * A sandbox key (`oxi_sk_test_...` from `POST /api/v1/agents/register`) is optional and is sent when you give one. A live key (`oxi_sk_live_...`) is refused by the constructor in sandbox mode.
  * `streamFeed()` on a sandbox client throws the sandbox's own `400 BadRequestError`, because streams are not simulated there.

  A `baseUrl` with a path now keeps that path, so `baseUrl: "https://0xinsider.com/sandbox"` sends `GET https://0xinsider.com/sandbox/api/v1/leaderboard`. Before this change, the `/api/v1/...` operation path was resolved against the origin, so the client silently called `https://0xinsider.com/api/v1/...` while the stream builder kept the path.

  **Breaking** for a client whose `baseUrl` carries a path. A trailing `/api/v1` is still removed once, so `https://api.0xinsider.com/api/v1` keeps working, and `https://api.0xinsider.com` and a loopback `http:` base are unchanged.

  **What to change:** If your `baseUrl` carries a path, check that requests still go where you expect. A keyless bearer call on a production client still throws locally before any request is sent, as before.
</Update>

<Update label="September 22, 2026" description="Every MCP tool result carries the route's meta, and a failed call carries the error fields">
  On [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and in `@0xinsider/mcp` 2.1.1, every `tools/call` result now carries the REST route's `meta` next to the payload. That is `request_id`, `cached`, `cache_age_s`, `cost`, and whatever provenance the route reports: `source` and `completeness` on `get_whale_trades_history` (`local_replay`, `best_effort`), or `ranking_source`, `directional_source`, and the `category_skill_*` fields on `get_sports_edge_signals`. A `meta` is copied, never invented, so a route that sends none leaves the result without one.

  Before this change, the remote endpoint kept `meta` on 3 tools and dropped it on the other 32, and most stdio tools dropped it too. A degraded ranking or a best-effort replay reached an agent looking healthy and complete.

  A failed tool call keeps its text and adds `structuredContent.error`:

  * The REST error's `code`, `reason`, `param`, `doc_url`, and `retry_at`, verbatim.
  * `retry_after_seconds`, taken from the route's `Retry-After`.
  * `request_id` and `status`.
  * Invalid arguments give `bad_request`, with `param` set to the first fault's JSON Pointer inside `arguments`.
  * A token without the scope gives `insufficient_scope`, with `param` set to the scope.

  **Backward compatible.** Every existing result key and the result text are unchanged. `meta` is a new key beside them, and `structuredContent` is new on a failure.

  **What to change:** Nothing if you read `result.content[0].text`. If you parsed `retry_at` or `request_id` out of that text, read the fields instead.
</Update>

<Update label="September 22, 2026" description="The search_markets MCP tool pages by cursor on both transports">
  The `search_markets` tool on [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and in `@0xinsider/mcp` now takes a `cursor` argument and answers with `data`, `has_more`, and `next_cursor`. That is the page contract [`GET /api/v1/markets/search`](/api-reference/endpoint/search-markets) has had all along. Send `next_cursor` back with the same `q`, `status`, and `category` to read the next page, and stop when `has_more` is `false`.

  Before this change, the remote tool advertised no `cursor`, so a second page could not be asked for, and the stdio tool answered with a bare `data` array and no cursor at all.

  An invalid cursor answers with the route's `bad_request` and `param` `cursor`. It never falls back to page one.

  **Backward compatible** on the remote tool. `cursor` is a new optional argument, and the result keys are unchanged.

  **Breaking** on the stdio tool. Its text changes from a JSON array to the `{data, has_more, next_cursor, meta}` object that every other list tool in the package returns.

  **What to change:** If you parsed the stdio tool's array, read `data` instead.
</Update>

<Update label="September 22, 2026" description="Remote MCP answers 400 to an unsupported MCP-Protocol-Version header">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and [`GET /api/v1/mcp`](/api-reference/endpoint/remote-mcp-stream) now read the `MCP-Protocol-Version` request header that the Streamable HTTP transport says a client sends on every request after `initialize`. The four revisions this server serves are accepted: `2025-11-25`, `2025-06-18`, `2025-03-26`, and `2024-11-05`. Any other value answers `400` with JSON-RPC `-32600` and `X-Mcp-Error-Code: -32600`, before the body or the credential is read, and the message names those four revisions.

  Before this change, the header was ignored, and `MCP-Protocol-Version: invalid-audit-version` answered `200`. Without the header a request is still served as `2025-03-26`, the transport specification's compatibility default.

  Two related fixes shipped with it:

  * The CORS preflight on `api.0xinsider.com` now grants `mcp-protocol-version` as a request header. A browser client that listed it in `Access-Control-Request-Headers` used to be refused after the handshake.
  * `https://0xinsider.com/api/v1/mcp` now forwards `Mcp-Session-Id` and `MCP-Protocol-Version` to the server. It had dropped both, so every call started a new session.

  **Breaking** for a client that sends an arbitrary `MCP-Protocol-Version`. A client that sends the negotiated version, or no header at all, needs no change.

  **What to change:** Send the version `initialize` returned, or omit the header.
</Update>

<Update label="September 22, 2026" description="Remote MCP checks tools/call arguments against the tool's inputSchema">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now validates every `tools/call` against the `inputSchema` that `tools/list` advertises for that tool. A bad call answers `200` with `result.isError` set to `true` and a `content[0].text` that names each fault by its JSON Pointer inside `arguments`, for example `at /limit: 500 is greater than the maximum of 100`.

  Before this change, the server read the keys it knew and dropped the rest. `{"limit": {"gt": 5}}` ran as the default 20-row page, and `{"limit": 2, "foo": 1}` ran as `limit=2`.

  These are the faults it now refuses:

  * An `arguments` value that is not an object, which now includes `arguments: null`.
  * An unknown key.
  * A value of the wrong type.
  * A number outside the schema's range, or a value outside its `enum`.
  * An argument combination outside a `oneOf`.

  **Breaking** for a client that sent arguments the schema does not allow: they are refused rather than ignored. A valid call, an omitted optional argument, an absent `arguments`, and every documented default answer exactly as before. Unknown tools (`-32601`), bad JSON (`-32700`), and a missing `name` (`-32602`) keep their JSON-RPC protocol errors.

  **What to change:** If you sent extra keys, or a string where the schema says `integer`, send the shape the schema states. The stdio package `@0xinsider/mcp` already did.
</Update>

<Update label="September 22, 2026" description="One request ID: X-Request-Id and meta.request_id now carry the same value">
  Every `/api/v1` response now carries one request ID. The `X-Request-Id` header and `meta.request_id` in the body hold the same value. That value is what the request's usage record and its log lines are filed under, so it is the ID to quote in a support request.

  Before this change, the header and the body carried two different IDs, and `meta.request_id`, the one the docs told you to quote, matched nothing on the server.

  * The header is now on every `/api/v1` response, whatever produced it: `304 Not Modified`, `408 Request Timeout`, CORS preflights, and every error. A public route or a timeout used to answer without it.
  * The format is unchanged: `req_` plus 12 hex characters.
  * An `X-Request-Id` you send is ignored. The value is never adopted or echoed, so a request cannot be filed under an ID you chose.

  **Backward compatible.** A client that reads either form for support can keep reading it, and now gets the one the server knows.

  **What to change:** Nothing.
</Update>

<Update label="September 22, 2026" description="TypeScript SDK 0.2.1: batchGetTraders now sends the body the route requires">
  `@0xinsider/sdk` 0.2.1 fixes `batchGetTraders()`. It now sends `{ "traders": [...] }`, the body [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) declares.

  Before this change, it sent `{ "identifiers": [...] }`, a field the route does not declare, so every call was refused for a missing `traders`.

  A new `expand` option is sent as the body's `expand` array and applies to every item: `client.batchGetTraders(["swisstony", "trd_123"], { expand: ["strategy"] })`. The values are `strategy`, `categories`, `quant_metrics`, and `trust`, exported as `BATCH_TRADER_EXPANSIONS`.

  **Backward compatible.** The positional signature is unchanged: the first argument is still the array of wallet addresses, usernames, `trd_` ids, or integer trader ids, in input order. The route itself did not change.

  **What to change:** If you worked around this with `client.call("batchGetTraders", { body: { traders } })`, you can switch to the method or keep the workaround. Both now send the same request body.
</Update>

<Update label="September 21, 2026" description="Pro includes 250,000 API requests a month; pay as you go covers the rest">
  Every authenticated `/api/v1` request now counts against a monthly quota. Pro includes 250,000 requests per UTC calendar month, whatever your billing interval. Every authenticated response carries three new headers: `X-Monthly-Quota-Limit`, `X-Monthly-Quota-Remaining`, and `X-Monthly-Quota-Reset` (Unix seconds, the first instant of the next month).

  [`GET /api/v1/usage`](/api-reference/endpoint/get-usage) adds `monthly_quota`, with `used`, `limit`, `remaining`, `reset_at`, `enforced_from`, `binding`, and `pay_as_you_go`. Reading it spends nothing.

  From October 1, 2026, request 250,001 in a month answers `429` with `error.code` `rate_limited` and the new `error.reason` `monthly_quota_exceeded`, carrying `Retry-After` and `error.retry_at` set to the month's reset. A refused request is not counted. The counter runs from September 21, and nothing is refused before October 1.

  Switch pay as you go on at [0xinsider.com/developers](https://0xinsider.com/developers) and requests over the quota keep answering. The excess bills at \$0.20 per 1,000 requests on a monthly invoice to the card on the subscription, and `binding` reads `false` for that account.

  **Backward compatible.** A client that branches on `error.code` needs no change.

  **What to change:** If you retry on `429`, read `Retry-After` first, because the monthly reset is days away rather than seconds. An email on September 21 announced 100,000 requests from November 1; the served contract is the one above.
</Update>

<Update label="September 21, 2026" description="OAuth registration caps at 10 per IP every 10 minutes, and metadata documents at 64 KiB">
  `POST /oauth/register` now has its own per-IP budget: 10 registrations per 600 seconds, checked after the shared OAuth cap of 60 a minute. The eleventh answers `429` with `error` `temporarily_unavailable` and `Retry-After: 600`. Every other OAuth `temporarily_unavailable` keeps `Retry-After: 60`.

  A client ID metadata document (an https `client_id` with a path) is now limited to 64 KiB of decoded body, checked while it downloads, and a document over the limit is refused and never cached. At most 8 uncached documents are fetched at once per backend process.

  Past that capacity, device authorization answers `429` with `Retry-After: 30`. The browser consent preview and decision keep their `200` `ok: false` shape with `error` `temporarily_unavailable`. A registered client or a cached document does not use that capacity.

  **Backward compatible.** Open registration is unchanged and by design: RFC 7591, public clients only, PKCE S256 required, and an exact `redirect_uri` match at authorize and at the token endpoint.

  **What to change:** Nothing. A client registers once and keeps its `client_id`, so no working client meets the cap.
</Update>

<Update label="September 21, 2026" description="List every graded holder of a market">
  Added [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders): the graded holder roster of any Polymarket market, the same list a Pick of the Day shows for its market. It returns every S, A, or B wallet with open shares on either outcome, taken from a complete provider holder scan, ordered by `current_value_usd` descending. A wallet holding both outcomes is listed once, on its net side.

  Each holder carries `shares`, Polymarket's own `current_value_usd` for that outcome, `grade`, `category_win_rate` and `category_win_record` in the market's category, badges, and `last_traded_at`.

  * Filters: `outcome` (`yes`, `no`, `all`), `min_grade` (`S`, `A`, or `B`, while `C`, `D`, and `F` answer `400`), `limit`, and `cursor` (`mh_` prefix).
  * The envelope carries `market`, `scan` (`source`, `complete`, and `wallet_count`, which counts every wallet the scan saw, graded or not), `totals` by side and grade, and an exact `total`.
  * The route takes the raw `condition_id` or the `mkt_` id, and supports `ETag` and `If-None-Match`.
  * An incomplete or failed scan answers `503` with `error.reason` `read_model_warming` and `Retry-After`. You never get a short list instead.

  The roster is rebuilt at most once a minute per market and shared by every caller, so a page is cheap and a cursor survives the rebuild. Until now [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) ignored `condition_id`, and [`GET /api/v1/large-positions`](/api-reference/endpoint/list-large-positions) is a change feed with a \$50,000 floor, so neither could answer this.

  **Backward compatible.** No existing route changes.

  **What to change:** Nothing.
</Update>

<Update label="September 21, 2026" description="Get a wallet's win record in every category, and in every esports game">
  Added [`GET /api/v1/trader/{address}/categories`](/api-reference/endpoint/get-trader-category-records): one wallet's record in every canonical category it has a settled market in, busiest first. These are the counts behind the Pick of the Day holder chips.

  * Each record carries `category`, `wins`, `decided`, `win_rate`, and `status` (`measured` or `not_enough_data`).
  * The envelope carries `basis` (`settled_markets_all_sizes`), `min_decided_for_win_rate` (5), and `computed_at`.
  * A record under that floor keeps its counts, with `win_rate` set to `null`.
  * A category with no settled market is left out, never returned as a 0% record.
  * A resolved trader with no record returns `records: []` with `200`. An unknown address returns `404`.
  * `?category=` filters to one bucket through the same category rollup the rest of the API uses: `soccer`, `EPL`, and `champions league` all reach Soccer, and `football` is American football.

  The same day, the `Esports` record gained `games[]`: the wallet's record per esports title (`LoL`, `CS2`, `Dota 2`, `Valorant`, and 7 more), each with `game`, `series_slug`, `wins`, `decided`, `win_rate`, and `status` under the same floor. The titles are named as the holder entries name them.

  `games` is absent on every other category and on an Esports record with no title history, and the Esports row keeps the whole bucket's counts.

  Game records fill in from the first daily rebuild on September 22.

  This record differs from `category_strengths` on [`GET /api/v1/trader/{address}?expand=categories`](/api-reference/endpoint/get-trader), which uses a different measured sample for ranking context. Both field descriptions identify the different bases; use this endpoint for the plain settled record.

  **Backward compatible.** No existing route changes.

  **What to change:** Nothing.
</Update>

<Update label="September 21, 2026" description="Esports holder entries name the game their win rate was measured in">
  Holder entries on [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and [`GET /api/v1/market/{condition_id}/holders`](/api-reference/endpoint/get-market-holders) add `category_win_rate_game` on an esports market. It names the game that `category_win_rate` and `category_win_record` were measured in: `LoL`, `CS2`, `Dota 2`, `Valorant`, `Call of Duty`, `Honor of Kings`, `Mobile Legends: Bang Bang`, `Overwatch`, `Rainbow Six Siege`, `Rocket League`, or `StarCraft II`.

  * It is present when the wallet has at least 5 resolved markets in that game. The rate and record are then that game's rather than the whole `Esports` bucket's.
  * It is absent when the rate is the wallet's whole Esports record, on every non-esports pick, and beside every status that is not `measured`.
  * The payload-level `category` stays `Esports` either way.

  Game records fill in from the first daily rebuild on September 22. Until then every esports entry carries the bucket record, as before.

  **Backward compatible.** The field is new and optional, so existing clients are unaffected.

  **What to change:** Label the rate with `category_win_rate_game` when it is present, and with `category` when it is not.
</Update>

<Update label="September 21, 2026" description="Pick of the Day archive publishes the price behind return_per_100 and both CLV operands">
  [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) rows add `backed_price` on exactly the rows that carry `return_per_100`, and `clv_entry_price` and `clv_close_price` on exactly the rows that carry `clv_pct`. On a win, `100 / backed_price` equals `return_per_100`. A pending pick, and any pick whose backed side is withheld, still carries none of them, so no gate moved.

  `clv_explanation` on a measured row now names the entry before the close: `Entry 0.635, close 0.705: CLV = (0.705 / 0.635 - 1) x 100 = +11.0%.` The previous form printed the close first, and the arithmetic is unchanged.

  What `backed_price` means is now stated everywhere it appears: the Polymarket CLOB order book midpoint at release, written once at publication and never changed, not an executed fill. A buyer pays the ask rather than the midpoint, so an executed stake usually returns a little less than `return_per_100`.

  An API subscriber's audit had concluded the return was computed off the close; it is not, and both operands now sit next to the result.

  **What to change:** If you parse `clv_explanation`, take the first number as the entry price.
</Update>

<Update label="September 21, 2026" description="The commitment ledger carries the payload of settled pre-sealing picks">
  [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger) now returns `payload` on a settled `uncommitted` entry: the market, side, and price of a pick published before sealing began, in the same 8 fields and the same key order as an opened payload. It is not hashed, and the entry stays `pre_commitment: true`.

  Before this change, an `uncommitted` entry carried no `payload` at all. 248 of the 250 picks published by September 21 predate sealing, so without their payloads a mirror could only take them on trust.

  `payload` is `null` while the pick is pending, and on a settled pick whose stored row is missing one of those columns.

  **Backward compatible.** Sealed and opened entries are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 21, 2026" description="Positions accept condition_id, and a deep page returns as fast as the first">
  [`GET /api/v1/positions`](/api-reference/endpoint/get-positions) now honors `condition_id`, raw or as the `mkt_` id, to scope the feed to one market. Combine it with `min_size=0` for every reconciled position in that market. An unknown id returns `[]`.

  Before this change, the parameter was parsed and then silently ignored. The `get_positions` MCP tool takes the same parameter.

  A deep cursor page now returns as fast as the first. Page 11 of `category=Baseball` at `limit=100` took 3,707 ms and often timed out at 30 s; it now takes 866 ms. Rows and cursors are unchanged.
</Update>

<Update label="September 21, 2026" description="Remote MCP and @0xinsider/mcp 2.1.0 add get_sports_edge_observations">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and the stdio package `@0xinsider/mcp` 2.1.0 add `get_sports_edge_observations`, which calls [`GET /api/v1/sports-edge-observations`](/api-reference/endpoint/get-sports-edge-observations). `tools/list` now returns 35 tools.

  * `cohort` is required: `wider_holder`, `in_play`, or `emerging_pile`.
  * `category`, `limit` (1 to 100, default 20), and `cursor` are optional.
  * The result carries the route's `snapshot_as_of`, `degraded`, and `funnel` next to `data`, `has_more`, and `next_cursor`. `degraded: true` marks a page you should not read as healthy evidence.

  **Backward compatible.** Existing tools are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 21, 2026" description="Pick of the Day can be a night game: the window now ends at 23:00 America/New_York">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now selects from every game on the Eastern calendar day. The operating window runs from 11:00 UTC to 23:00 America/New\_York.

  Before this change, the window was 11:00 to 23:00 UTC. A pick releases 1 hour before kickoff, so no game kicking off at 20:00 ET or later could be the pick. On September 21 the day's strongest signal, Monday Night Football, sat outside that set and nothing published.

  **Backward compatible.** No field changed, and `scheduled_picks` and the archive keep their shape.

  **What to change:** `404 pick_not_released` and its `error.retry_at` can now point as late as 23:00 ET. If you assumed the day was settled by 23:00 UTC, schedule your next request from `retry_at`.
</Update>

<Update label="September 20, 2026" description="New endpoint: every Pick of the Day's pre-game commitment, opened once it settles">
  Added [`GET /api/v1/pick-of-the-day/ledger`](/api-reference/endpoint/get-pick-of-the-day-ledger): one entry per published `(pick_date, pick_rank)`, ascending, in one of three states. The route needs an API key on a Pro account.

  * `sealed` is a live pick. It carries `commitment_hash`, `commitment_algo` (`sha256(canonical_json(payload)||nonce)`), `sealed_at`, `kickoff`, and `permalink`, and nothing that states a side or a price.
  * `opened` is a settled pick. It carries those fields plus `commitment_nonce`, the canonical `payload` the hash was taken over (market, side, `backed_price` as a decimal string, kickoff, date, rank), `outcome`, `resolved_at`, `matchup`, and `category`.
  * `uncommitted` is a pick with no commitment, either published before the scheme existed or reaching kickoff unsealed. It is named rather than left out, and carries `pre_commitment: true`.

  To check an opened entry yourself, take the bytes of `payload` exactly as you received them, append `commitment_nonce` decoded from hex, and sha256 the result. It equals `commitment_hash`, because the payload is served byte for byte as it was hashed.

  A commitment is never written after kickoff, and never rewritten by an outcome correction.

  **Backward compatible.** The pick and archive routes are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 20, 2026" description="Large positions report each outcome on its own and list every holding">
  On [`GET /api/v1/large-positions`](/api-reference/endpoint/list-large-positions), `total_size_usd` and `position_unrealized_pnl` are now the provider's value and unrealized P\&L for the one outcome named by `outcome_label` and `token_id`. `null` means the provider supplied no value, not zero. `event_total_value_usd` is now `null` when the value of any other outcome in the same market is unknown.

  Before this change, a wallet holding both outcomes of a market had both outcomes' combined value and P\&L labelled as one outcome.

  Four fields are added, two of them required:

  * `combined_value_usd` and `combined_unrealized_pnl` are the wallet's whole-market totals. They equal the per-outcome fields unless `is_two_sided` is `true`, and the feed ranks by the combined value.
  * `is_two_sided` is `true` when the wallet holds both outcomes.
  * `holdings[]` lists every outcome the wallet holds, each with `outcome_index`, `outcome_label`, `token_id`, `share_count`, `avg_entry_price`, `current_price`, `value_usd`, and `unrealized_pnl`. `holdings[0]` restates the top-level per-outcome fields.

  **What to change:** If you read `total_size_usd` as the wallet's exposure in a two-sided market, read `combined_value_usd` instead.
</Update>

<Update label="September 20, 2026" description="Volume fields are USD cash volume, never Polymarket leaderboard shares">
  Three `/api/v1` fields had been carrying Polymarket's share counts as dollars, and they now report USD cash volume. Polymarket's v2 leaderboard reports `volume` in both-sides shares, and its own schema says the figure is never USD.

  On [`GET /api/v1/leaderboard/trending`](/api-reference/endpoint/list-trending-wallets):

  * `window_volume_usd` is now both-sides cash volume in USD, taken from Polymarket's user-volume read, and it leaves the `required` set. It is omitted when Polymarket served no volume for that wallet, never returned as zero.
  * `window_volume_shares` is added and carries the leaderboard's share figure.
  * Polymarket tracks volume in whole UTC days, so the USD window is the whole-day span that covers the window you asked for.
  * The default `min_volume_usd=5000` floor now compares dollars.

  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) `stats.total_volume`, [`GET /api/v1/leaderboard`](/api-reference/endpoint/get-leaderboard) `volume`, and the trader export's `provider_lifetime_volume` are now full-history both-sides USD cash volume from a verified observation, and are omitted or `null` without one. A wallet's figure is typically lower than before, because the previous number was a share count.

  **What to change:** If you treated `window_volume_usd` as always present, handle its absence. Field names and types are unchanged.
</Update>

<Update label="September 20, 2026" description="Grades weigh the last 90 days, and counterparty participants carry last_traded_at">
  The wallet `grade` that every `/api/v1` response carries now weighs the wallet's last 90 days beside its lifetime record. A wallet with no trade for 30 days holds no better than B, and no better than C past 120 days.

  Before this change, the only recency rule was a 365-day cliff, and the median B-graded wallet had last traded 56 days earlier.

  [`GET /api/v1/whale-trades/{id}/counterparties/executions`](/api-reference/endpoint/get-whale-trade-counterparty-executions) and [`.../makers`](/api-reference/endpoint/get-whale-trade-counterparty-makers) add `last_traded_at` on the taker and on each maker: the latest recorded trade time, across all markets, for the tracked trader whose grade is shown. It is omitted when that time is unavailable.

  **Backward compatible.** The `grade` field and its values are unchanged, and `last_traded_at` is new and optional.

  **What to change:** Nothing, but a client that filters on grade will see some wallets move down.
</Update>

<Update label="September 20, 2026" description="OAuth registration now stores and enforces grant_types">
  `POST /oauth/register` now stores `grant_types` and enforces it. Omitting the field registers `["authorization_code", "refresh_token"]`. Using a grant the client did not register answers `unauthorized_client` (`400`) at consent, at `POST /oauth/device/code`, and at `POST /oauth/token`.

  Before this change, the registration response echoed a `grant_types` the server neither stored nor checked.

  * The list must name `authorization_code` or `urn:ietf:params:oauth:grant-type:device_code`.
  * It may not be empty, and it may hold nothing outside `grant_types_supported`. Each failure answers `invalid_client_metadata`.
  * The response reports the stored list. `response_types` is `["code"]` only when the client holds `authorization_code` and registered a redirect URI, and `[]` otherwise.
  * A client without `refresh_token` gets a token response with no `refresh_token` field, and repeats its flow after the hour.

  Two authorizations completed at once for the same user and client now leave one live grant. A race could previously leave two.

  **Backward compatible.** Clients registered before this change, and every client ID metadata document, carry every supported grant.

  **What to change:** Register the grants a new client will use, because a grant it did not register is refused.
</Update>

<Update label="September 20, 2026" description="Pick of the Day holders carry the counts behind the category win rate">
  Holder entries on [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) add `category_win_record`, the two counts that `category_win_rate` is the ratio of. It is present only when `category_win_rate_status` is `measured`, and absent from payloads created before the field shipped.

  * `wins`: resolved markets in the pick's category that the wallet closed with a profit.
  * `decided`: markets the wallet closed with a profit or a loss. A market that resolved at zero realized P\&L counts in neither.

  The same day, the basis widened. The rate and the record now count every resolved Polymarket market the wallet traded in the category, at any position size, and both are rebuilt daily. Until this change only positions of \$20 or more counted, so a wallet's rate can move.

  The floor of 5 resolved markets is unchanged.

  **Backward compatible.** The field is optional.

  **What to change:** Nothing.
</Update>

<Update label="September 19, 2026" description="@0xinsider/mcp 2.0.0 adds a product CLI with browser login">
  `@0xinsider/mcp` 2.0.0 turns the existing executable into a product CLI. It adds `login` (approved in a browser, through the device authorization flow in the entry below), `whoami`, `refresh`, and `logout`, plus read commands backed by the SDK. The [CLI page](/integrations/cli) lists every command.

  The read commands paginate JSON up to a bound, keep the full response envelope, and exit nonzero when the result is incomplete. 2.0.1, released the same day, refreshes an expired or rotated access token between pages, so a long paginated read no longer fails partway.

  * 2.x needs Node 22 or newer.
  * Stored sessions need macOS or Linux.

  **Backward compatible.** Both binary names, the default stdio MCP server, `serve`, and `init` keep their behavior.

  **What to change:** Nothing. If you upgrade to 2.x, check that you are on Node 22 or newer.
</Update>

<Update label="September 19, 2026" description="Typed category evidence on the trader route and on Pick of the Day holders">
  [`GET /api/v1/trader/{address}?expand=categories`](/api-reference/endpoint/get-trader) adds `category_records[]` and `category_skill_model` beside the existing `category_strengths`. Each record holds forward-only evidence for one category, built from observed Polymarket taker fills.

  A record carries:

  * `status`: `live`, `insufficient`, `stale`, `unknown`, or `degraded`.
  * `canonical_category` and `as_of`.
  * `independent_event_count` and `resolved_condition_count`.
  * `edge_mean`, `edge_sd`, `edge_se`, `edge_lower_95`, and `brier_event_avg`.
  * `model_version` and `taxonomy_version`.

  A row whose `status` is `insufficient`, `stale`, `unknown`, or `degraded` is still returned, and it withholds the scores. The absence of a category is not proof of skill. `status` is category eligibility, not a letter grade, and the wallet's global `grade` is unchanged.

  `category_skill_model` reports how ready the model is overall, read from the same snapshot.

  Holder entries on [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) also gained optional category context in this release. Internal selection evidence was later removed from customer pick responses; refer to the current endpoint for the supported holder fields.

  **Backward compatible.** The new fields are optional, and the trader route omits them unless you ask for `expand=categories`.

  **What to change:** Nothing.
</Update>

<Update label="September 19, 2026" description="Pick of the Day holders carry wallet badges">
  Holder entries on [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) add 5 badge fields. They are stamped at serve time from the wallet's current trader record, so they are never frozen with the pick.

  * `wallet_age_days`: the wallet's age in days.
  * `is_new_wallet`: `true` when the wallet's first trade was under 30 days ago.
  * `markets_traded`: how many distinct markets the wallet has traded.
  * `is_bot`: `true` at 10,000 or more distinct markets traded. That is a breadth rule, not proof of automation.
  * `x_username`: the X handle on the wallet's Polymarket profile, without the `@`.

  All 5 are present together, and only for a wallet that carries at least one badge. All 5 absent means the wallet has no badge, or the body was cached before the fields shipped.

  **Backward compatible.** The fields are optional.

  **What to change:** Nothing.
</Update>

<Update label="September 19, 2026" description="GET /api/v1/me returns the account and credential a request authenticated as">
  Added [`GET /api/v1/me`](/api-reference/endpoint/get-account-identity). It returns the account the request authenticated as, in an account envelope: the account ID, the credential ID, the credential kind, and the approved scopes. A developer key reports `null` scopes.

  The response carries no email address and no token. The route uses the same paid-access check, `read` scope, usage accounting, and rate limits as every other read. The product CLI's `whoami` command reads it.

  **Backward compatible.** It is a new endpoint.

  **What to change:** Nothing.
</Update>

<Update label="September 19, 2026" description="OAuth adds the device authorization grant (RFC 8628)">
  OAuth now supports the device authorization grant, so an agent with no browser and no redirect receiver can hold a scoped, revocable 1-hour token instead of a pasted long-lived API key. Discovery at `/.well-known/oauth-authorization-server` now lists `device_authorization_endpoint` as `https://api.0xinsider.com/oauth/device/code`, and adds `urn:ietf:params:oauth:grant-type:device_code` to `grant_types_supported`.

  `POST /oauth/device/code` takes the form fields `client_id`, optional `scope` (default `read`), and optional `resource`. It returns `device_code`, `user_code`, `verification_uri`, `expires_in: 600`, and `interval: 5`.

  * The person opens `verification_uri`, signs in, enters the `user_code`, reviews the account and the scopes, then approves or denies. Only an active Pro account can approve.
  * Your client polls `POST /oauth/token` with `grant_type`, `client_id`, and `device_code`.
  * `authorization_pending`, `slow_down`, `access_denied`, and `expired_token` all come back as `400`. Each poll that arrives early adds 5 seconds to the interval, and a `device_code` that has already been used answers `invalid_grant`.
  * The exchange gives the same scoped grant, refresh, and per-client revocation as the authorization-code flow.
  * A device-only client can register at `POST /oauth/register` without `redirect_uris`, and asks for `response_types: []`.

  **Backward compatible.** The authorization-code flow is unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 19, 2026" description="Remote MCP adds get_sports_edge_signals, plus a Markdown document per market">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and the stdio package add `get_sports_edge_signals`, which dispatches to [`GET /api/v1/sports-edge-signals`](/api-reference/endpoint/get-sports-edge-signals). `tools/list` now returns 34 tools, and `@0xinsider/mcp` 1.3.1 serves the same catalog.

  The stdio `get_trending_wallets` now caps `limit` at 50 and keeps the gaps in the daily P\&L curve, which matches V1 and the remote tool. Before this change it advertised a limit of 100 and filled those gaps with zeros.

  Added [`GET /api/v1/market/{condition_id}/context.md`](/api-reference/endpoint/get-market-context-markdown), a self-contained `text/markdown` evidence document for one market. It needs an API key, and it is rendered from the same typed data as the [market snapshot](/api-reference/endpoint/get-market-snapshot), including that snapshot's trust fields.

  * It carries the market's identity, the outcome labels and tokens, cached quotes, liquidity, live sports, and per-source freshness or the reason a source is unavailable.
  * Text that came from a provider is encoded as indented JSON, so it stays inert.
  * The path accepts a raw `condition_id` or an `mkt_` id. An unknown market returns `404`.
  * There is no `ETag`. Use the snapshot route for conditional reads.

  **Backward compatible.** Existing tools and routes are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="September 17, 2026" description="An API key in a ?token= query parameter is refused on every route">
  An API key sent as a `?token=` query parameter is now refused on every route, with `401 invalid_api_key` and the new `error.reason` `api_key_in_query`. Before this change, [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) and its GET event stream still accepted a key that way.

  The reason: a live key does not expire, and a URL is written to access logs, proxies, shell and browser history, and client config dumps.

  * When a request carries both the header and `?token=`, the header is used and the request succeeds.
  * `initialize`, `ping`, and `tools/list` still need no credential. A request that puts a key in the URL is refused before it reaches them.
  * No working integration used the query form. Between September 4 and September 17, 2026 it recorded no accepted request.

  **Breaking** for a client that sends its key in the URL. The `invalid_api_key` code is unchanged, so a client that branches on `error.code` needs no change.

  **What to change:** Send the key as `Authorization: Bearer <key>`. A client that can only take a URL can run the `@0xinsider/mcp` stdio package with the key in its environment instead.
</Update>

<Update label="September 17, 2026" description="Refused requests report the per-IP rate-limit budget">
  A request that `/api/v1` refuses before it checks the key's own limit now carries the per-IP bucket's `RateLimit-*` and `X-RateLimit-*` headers. Before this change those responses carried no budget, so a client retrying a rejected key could not see how close it was to the per-IP limit.

  The responses that now carry the headers:

  * `401 invalid_api_key`.
  * `402 subscription_required`.
  * `403` for a deleted account, and `403 insufficient_scope`.
  * `423` for a locked account.
  * The authentication layer's own `500` and `503` responses.
  * The MCP handshake, which needs no credential.

  The numbers are always the per-IP bucket, the only budget a failed authentication spends. A caller refused because its IP is banned still gets no headers, and neither does a request whose client IP could not be resolved.

  The OpenAPI spec now documents these headers on every operation that sends them.

  **Backward compatible.** Existing clients are unaffected.

  **What to change:** Nothing.
</Update>

<Update label="September 17, 2026" description="Pick of the Day's CLV average is reported in percentage points">
  [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) adds `hit_rate.clv_avg_pp` and `hit_rate.clv_avg_pp_display`. Both hold the mean closing-line move in percentage points, `(close - entry) * 100`, for example `"-0.1 pp"`.

  The existing `clv_avg_pct` is the mean of per-pick ratios, `(close / entry - 1) * 100`. A cheap entry moves that mean more than a favorite does. A pick at $0.05 that closes at $0.10 counts as +100%, while a pick at $0.90 that closes at $0.95 counts as +5.6%.

  The percentage-point average counts every pick on the same scale, and it is now the headline figure on 0xinsider.com.

  Both averages cover the same measured picks, and both are omitted until at least 5 of them exist. `clv_avg_pct` and `clv_avg_display` are unchanged.

  **Backward compatible.** The new fields are optional.

  **What to change:** Nothing. Read `clv_avg_pp` if you want the figure 0xinsider.com shows.
</Update>

<Update label="September 16, 2026" description="Large trades carry their share of the market's volume">
  [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), and [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) add the optional field `market_volume_share`. It is the fill's `size_usd` divided by its market's volume at the moment the trade was recorded.

  `size_usd` on its own cannot tell a large bet in a thin market from a routine one in a deep market. A $10,000 fill is 0.00005 of a $200M market and 0.125 of an \$80,000 one.

  * The value is not capped at `1`, because a fill larger than the market's stored trailing volume is the case that matters most.
  * It is absent when the market carried no volume figure, and on trades recorded before September 16, 2026.
  * It is never `0` as a stand-in for a missing value.

  **Backward compatible.** The field is optional.

  **What to change:** Nothing.
</Update>

<Update label="September 8, 2026" description="A second venue removed: the API serves Polymarket only">
  0xinsider retired its second venue on September 8, 2026. Every V1 endpoint now serves Polymarket data only, and responses no longer contain that venue's markets, trades, positions, or P\&L.

  * [`GET /api/v1/platforms`](/api-reference/endpoint/get-platforms) returns `platforms.polymarket` only. The retired venue's key is gone.
  * `platform` on [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), [`GET /api/v1/markets/sharp-money-flows`](/api-reference/endpoint/sharp-money-flows), and [`GET /api/v1/markets/smart-money-flows`](/api-reference/endpoint/smart-money-flows) accepts `polymarket` or `all`, and both return Polymarket rows. Every other value returns `400 bad_request` with `error.param="platform"`.
  * [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) still accepts `platform` for backward compatibility, and returns Polymarket markets for every value.
  * [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot) returns `market.provider` as `polymarket`, and `liquidity.last_price` is always `null`.

  **Breaking** for a client that sends any `platform` value other than `polymarket` or `all`.

  **What to change:** Drop the `platform` parameter, or send `polymarket`. Earlier entries in this changelog describe the API as it shipped on their date.
</Update>

<Update label="August 29, 2026" description="Pick of the Day serves every ready pick while another pick's proof is pending">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now answers `200` when at least one published pick has readable smart-money proof. `picks` carries the picks whose proof is readable, and the new `proof_pending_picks` array lists the rest with `pick_rank`, `release_at`, `kickoff`, and `retry_at`.

  Before this change, one published pick whose proof was not readable yet turned the whole day into `503 read_model_warming`. On August 29 one pick's holder snapshot lagged its publish, and the API withheld three fully proven picks for 14 minutes. The `503` now remains only when no published pick has readable proof.

  **Backward compatible.** The field is additive.

  **What to change:** Nothing. To fetch a pending pick, read again at its `retry_at`.
</Update>

<Update label="August 29, 2026" description="Remote MCP adds two Pick of the Day tools">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) adds 2 read-only tools, and both take no arguments. `get_pick_of_the_day` dispatches to [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day), and `get_pick_of_the_day_archive` to [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive). `tools/list` now returns 33 tools.

  A tool error from any remote tool now carries the V1 `error.code`, `error.reason`, and `error.retry_at` when the handler set them. Before the day's pick is released, `get_pick_of_the_day` therefore reads `404 Not Found (not_found/pick_not_released): ... Not available before <retry_at>.`, so an agent can schedule the next call instead of polling.

  The stdio package `@0xinsider/mcp` 1.2.0 serves the same 33 tools.

  **Backward compatible.** Existing tools and REST routes are unchanged.

  **What to change:** Nothing.
</Update>

<Update label="August 29, 2026" description="Pick of the Day's 404 retry_at accounts for newly available selections">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) now sets `error.retry_at` and `Retry-After` on a `404 pick_not_released` to account for newly available selections as well as scheduled releases.

  Before this change, the value was the earliest scheduled slot's `release_at`. A newly available pick could release earlier, so a client honoring the old value could miss its release.

  Until a pick releases, keep scheduling requests from the returned retry time.

  **Backward compatible.** The response shape is unchanged.

  **What to change:** Nothing. A client that schedules from `retry_at` keeps working.
</Update>

<Update label="August 19, 2026" description="Pick of the Day lists the day's picks that have not released yet">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) adds `scheduled_picks`: the day's selected picks that have not released yet, in `pick_rank` order.

  Each slot carries exactly 3 fields: `pick_rank`, `release_at`, and `kickoff`. The shape holds no matchup, category, side, price, or holder data before release, so the schedule can be shown without revealing a pick early.

  The field is present only while at least one unreleased slot remains, so its absence means the day is fully released.

  **Backward compatible.** The field is optional.

  **What to change:** Nothing. To plan your next read, request once at the earliest `release_at` instead of polling.
</Update>

<Update label="August 16, 2026" description="@0xinsider/mcp 1.0.9 serves 29 tools locally, up from 6">
  [`@0xinsider/mcp`](/integrations/mcp) 1.0.9 is on npm, the first release since 1.0.2 on April 14. The local stdio server goes from 6 read-only tools to 29, the same set the hosted endpoint serves.

  * New tools: positions, position timeline (by wallet or by id), large positions, trending wallets, trader P\&L, market explore, sharp-money flows, market snapshots, whale-trade history and single-trade lookup, batch trader and market-intel lookups, daily, weekly and monthly report snapshots, trader exports, webhook reads, and event replay.
  * `get_smart_money_flows` still works.
  * Whale-trade tools return `recorded_signal_score`.
  * `init` hides the API key as you type it, and prints a placeholder where a configuration to paste by hand would have shown the key.

  The hosted [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) serves the same tools plus `get_report` and `search_content`, 31 in all.

  **Backward compatible.** The release adds tools.

  **What to change:** Nothing. Run `npx -y @0xinsider/mcp init` to set up or update. If an earlier `init` printed your key into output that was recorded or shared, rotate the key from [Developers](https://0xinsider.com/developers).
</Update>

<Update label="August 3, 2026" description="Large trades carry the signal score captured when the trade was recorded">
  [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), and [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) now return the optional field `recorded_signal_score` next to the existing `signal_score`.

  * `signal_score` is the current score.
  * `recorded_signal_score` is the score captured with the trade record. Use it in alerts, audits, and historical comparisons.
  * The recorded score exists only for trades recorded after this release. Older trades return `null`.

  **Backward compatible.** The field is optional.

  **What to change:** Nothing.
</Update>

<Update label="July 26, 2026" description="Pick of the Day reports the availability of stored trader context">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) added optional metadata describing whether stored trader context was available to read. Before this change, missing context did not distinguish an absent record from an unavailable one.

  The metadata described recorded context, rather than live trader statistics or entry permission. This release also documented the existing trader context and display category.

  **Backward compatible at release.** The metadata was optional. Internal selection diagnostics were later removed from customer responses; use the currently documented pick fields.

  **What to change:** Read the current endpoint reference for the customer response contract.
</Update>

<Update label="July 14, 2026" description="New observation-only endpoint for wider-holder and in-play sports markets">
  Added [`GET /api/v1/sports-edge-observations`](/api-reference/endpoint/get-sports-edge-observations), an Insider-tier endpoint that reports evidence on sports opportunities that do not enter the funded Sports Edge slate. Every row carries `observation_only: true`, provider and holder freshness fields, and an explicit directional status.

  Pick one `cohort`, which is required:

  * `wider_holder` measures pre-game markets where the graded holders currently agree, even when no recent graded whale flow exists.
  * `in_play` measures only games the provider confirms are live, and only when the holder and directional evidence is fresh.

  Use the returned provider state, freshness, and directional status to interpret each row. Every response carries a per-sport funnel that records emitted observations and the reasons other rows are unavailable.

  `capacity_limited` describes a bounded response. It does not by itself mean that the snapshot is degraded.

  Omit `category` to evaluate all 14 observation sports. That registry includes Table Tennis and Pickleball, classified from provider-confirmed tags and kept separate from Tennis. The `table-tennis`, `table tennis`, and `pickleball` filters all resolve through the canonical taxonomy, and neither sport is in the 12-sport funded projection.

  Reading the response:

  * Every `200` carries a required top-level `degraded: boolean`, the verdict for the whole snapshot. It covers both operational trouble and unknown completeness.
  * A healthy `wider_holder` request can reuse a snapshot for about 180 seconds. `in_play` never serves one older than about 30 seconds.
  * Conditional reads use a weak semantic `ETag` over the stable part of the page, which includes `next_cursor` and excludes the request-specific `meta`.
  * A malformed typed query value returns the standard JSON `400 bad_request` envelope.
  * `503` with `error.reason: "read_model_warming"` means the route ran out of time before it had the data it needs. A later failure shows up instead as a degraded `200`, with the funnel filled in where possible.
  * The API's outer 30-second transport timeout still returns a `408` with an empty body, so check the status before you parse JSON.

  These groups remain observations only and do not authorize entries or contribute to published picks.

  **Backward compatible.** It is a new endpoint, and it does not change or feed [`GET /api/v1/sports-edge-signals`](/api-reference/endpoint/get-sports-edge-signals), Pick of the Day, or any order executor.

  **What to change:** Nothing.
</Update>

<Update label="July 13, 2026" description="Sports Edge covers Golf, Formula 1, NBA Summer League, CFL, and Boxing">
  [`GET /api/v1/sports-edge-signals`](/api-reference/endpoint/get-sports-edge-signals) and [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) can now return eligible markets for Golf, Formula 1 and NASCAR, NBA Summer League, CFL, and Boxing.

  Polymarket sends only generic sports metadata for some of these markets. 0xinsider uses the provider tags to identify their sports categories. `Racing` and `Boxing` are now supported canonical buckets.

  **Correction:** an earlier version of this entry also named Table Tennis and Pickleball. The funded release did not admit either sport. Both are registered for observation-only evaluation through the endpoint added on July 14, and neither feeds funded signals or Pick of the Day.

  For the funded taxonomy changes in this release, an open market can adopt the corrected category on a provider refresh. Historical rows need a controlled refold, so they are not implied to have reclassified on their own.

  **Backward compatible.** No request or response shape changed, and no existing category was renamed.

  **What to change:** Nothing.
</Update>

<Update label="July 6, 2026" description="New endpoint: ranked pre-game sports markets with graded money on one side">
  Added [`GET /api/v1/sports-edge-signals`](/api-reference/endpoint/get-sports-edge-signals), a ranked list of upcoming pre-game sports markets (moneyline and props) where graded S, A, and B smart money sits on one side. The server computes the whole list in one call.

  Each signal returns:

  * The side the graded money is on, and its CLOB `token_id`.
  * The grade distribution on that side: `s_count`, `a_count`, and `b_count`.
  * The dollar concentration: `sharp_pct` and `backed_sharp_usd`.
  * The market's `game_start_time`, which is kickoff.
  * A calculated backing score, `conviction_score`.

  Follow the returned order when presenting the markets, and read the measured backing alongside the scores.

  The route needs a bearer token and an Insider tier account. Its `200` carries an `ETag`, and it is cursor-paginated with the cursor fixed to one snapshot.

  **Backward compatible.** It is a new endpoint.

  **What to change:** Nothing.
</Update>

<Update label="July 5, 2026" description="Pick of the Day returns 404 outside today's pick, and adds game_started">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) changes in 2 ways.

  The new boolean field `game_started` is `true` once the backed game's kickoff has passed, which means the snapshotted pre-game price is no longer actionable. It is absent on a legacy pick with no stored kickoff, and you should treat that as not started.

  The `is_locked` description was corrected at the same time. It is a pre-release embargo flag, so it is effectively always `false` on a served pick. Use `game_started` for kickoff.

  The endpoint now returns `404` when today has no published pick yet, instead of serving the most recent prior day's pick, which was often already settled. The response shape is unchanged and `404` was already a documented response. This conforms the endpoint to its "today's Pick of the Day" contract, so an automated consumer never acts on a finished game.

  Prior picks stay available through the archive endpoint.

  **Backward compatible.** `game_started` is optional, and the behavior change is non-breaking.

  **What to change:** Nothing.
</Update>

<Update label="July 4, 2026" description="Sandbox test-mode API keys removed; one live key class remains">
  Sandbox test-mode API keys (`oxi_sk_test_...`) are removed. The API now has a single key class, `oxi_sk_live_...`, sent as `Authorization: Bearer oxi_sk_live_...`. It requires an active Insider subscription and always returns live data.

  There is no fixture or deterministic-data mode any more. Every request reads live data.

  **Breaking** for a client still authenticating with an `oxi_sk_test_...` key. Those requests fail.

  **What to change:** Generate a live key from your account's API keys page, and switch your clients over before this release.
</Update>

<Update label="July 2, 2026" description="Pick of the Day: a fixed 19:00 Europe/Berlin release and any sport">
  [`GET /api/v1/pick-of-the-day`](/api-reference/endpoint/get-pick-of-the-day) and its archive change behavior in 2 ways. No field was added, removed, or retyped, but 2 values now mean something different.

  `release_at` is now a fixed daily instant: 19:00 Europe/Berlin, every day, which is 19:00 CEST in summer and 19:00 CET in winter. It used to be a kickoff-relative time that moved with whichever game was picked. `next_drop_deadline` counts down to the same fixed instant.

  When no pick is available at the release slot, publication can happen later. On a thin day the actual publication can trail `release_at` by minutes to hours.

  The pick can now cover more sports and prop markets, expanding beyond the previously narrower set of categories.

  * Sports: soccer, tennis, baseball, basketball, hockey, MMA, cricket, golf, and esports.
  * Prop markets: totals, spreads, and exact scores.
  * Expect `category` values beyond `Soccer` in the archive from now on.

  **Backward compatible.** Existing clients are unaffected.

  **What to change:** Nothing.
</Update>

<Update label="July 1, 2026" description="Whale trades expose the traded outcome and its CLOB token id">
  Each whale trade on [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades), [`/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history), and [`/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) now returns the traded `outcome` and that outcome's Polymarket CLOB `token_id`. The `outcome` is the provider's own label for the side the trade was on, for example Yes, No, or a team. The existing `side`, `BUY` or `SELL`, is unchanged.

  Before this change only `BUY` or `SELL` was exposed, so you could not tell which outcome a whale trade was on, or get that outcome's on-chain token id.

  `outcome` is `null` only for a multi-outcome market, or a market that has not synced, with no stored label. A trade from the venue retired on September 8, 2026 carries its provider label in `outcome`, and only its `token_id` is `null`, because that venue had no CLOB token; see that entry.

  **Backward compatible.** Both fields are optional and nullable.

  **What to change:** Nothing.
</Update>

<Update label="July 1, 2026" description="Every V1 YES/NO/outcome now carries its Polymarket CLOB token id">
  Reads that expose an outcome, a side, or a net-flow direction now also return that outcome's Polymarket CLOB `token_id`. You can go straight from a YES, NO, or outcome label to its on-chain ERC1155 asset id without a second lookup. Use it for order placement, per-token price history, and on-chain reconciliation.

  The value is a decimal string for a synced Polymarket market, and `null` when it is unavailable, for example on a market from the venue retired on September 8, 2026, or a market that has not synced yet. The key is always present, and it is never in a `required` set; see that entry.

  The endpoints that gained it:

  * [`GET /api/v1/positions`](/api-reference/endpoint/get-positions): `token_id` next to `side`.
  * [`GET /api/v1/large-positions`](/api-reference/endpoint/list-large-positions): `token_id` next to `outcome_label`.
  * [`GET /api/v1/market/{condition_id}/intel`](/api-reference/endpoint/get-market-intel) and [`POST /api/v1/markets/intel/batch`](/api-reference/endpoint/batch-get-market-intel): `token_id` on `smart_money`, next to `direction`, and on every `smart_money.top_positions` entry.
  * [`GET /api/v1/markets/smart-money-flows`](/api-reference/endpoint/smart-money-flows): `token_id` on `smart_money`, next to `direction`.
  * [`GET /api/v1/trader/{address}/position-timeline`](/api-reference/endpoint/get-position-timeline) and [`GET /api/v1/traders/{trader}/position-timeline`](/api-reference/endpoint/get-trader-position-timeline): `token_id` next to `outcome_side`.
  * [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets): `token_id_yes` and `token_id_no` next to `outcome_yes` and `outcome_no`. These are CLOB token ids (decimal strings), and they are different from the integer Gamma ids `outcome_yes_provider_id` and `outcome_no_provider_id`.
  * [`GET /api/v1/reports/daily`](/api-reference/endpoint/get-daily-report-snapshot), [`weekly`](/api-reference/endpoint/get-weekly-report-snapshot), and [`monthly`](/api-reference/endpoint/get-monthly-report-snapshot): `token_id` on every `top_whale_trades` entry.

  **Backward compatible.** The field is optional, so existing clients are unaffected.

  **What to change:** Nothing.
</Update>

<Update label="July 1, 2026" description="Pick of the Day archive adds a cumulative track-record series">
  [`GET /api/v1/pick-of-the-day/archive`](/api-reference/endpoint/get-pick-of-the-day-archive) now returns `hit_rate.series`, the cumulative track record of the Pick of the Day. It is ready to chart without recomputing it on the client.

  Each element is one resolved pick, a win or a loss, in ascending `pick_date` order, and carries:

  * `date`: the pick's publish date, as `YYYY-MM-DD`.
  * `net_profit_usd`: the running cumulative profit of a \$100 stake on every resolved, priced pick through that date.
  * `hit_rate_pct`: the running hit rate (wins divided by decided) through that date, to one decimal.

  The final point equals the top-level `hit_rate.net_profit_usd` and `hit_rate.pct`, so a chart of the series always ends exactly on the headline numbers. The array is empty until at least one pick has resolved.

  **Backward compatible.** Every existing `hit_rate` field is unchanged.

  **What to change:** Nothing.
</Update>

<Update label="June 29, 2026" description="Trader quant_metrics is now a documented, fixed field set">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) and [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders) now return a curated, documented `quant_metrics` object when you pass `?expand=quant_metrics`. Before this change it was an unconstrained pass-through of internal metrics.

  When expanded, the object always carries these 9 fields. Each is a number or `null`, and `null` means insufficient trade history, never `0`.

  * `copy_score` and `smart_score`: scores from 0 to 100. `smart_score` summarizes measured performance, and `copy_score` summarizes how practical the wallet's past strategy may be to follow.
  * `sharpe_30d` and `sharpe_7d`: risk-adjusted return over the trailing 30 and 7 days.
  * `profit_factor`: gross profit divided by gross loss, capped at 1,000.
  * `edge_consistency`: how stable the trader's edge is over time, from 0 to 1.
  * `sharpe_percentile`, `pf_percentile`, and `consistency_percentile`: cross-sectional ranks against all traders, from 0 to 100.

  Undocumented internal and experimental metrics that used to come through the pass-through are no longer returned.

  Every field is defined on the [trader endpoint reference](/api-reference/endpoint/get-trader), and the [Quant metrics guide](/concepts/quant-metrics) explains what each score and metric means, its range, and how to read `null`.

  **Breaking** for a client that read a metric outside this list.

  **What to change:** Nothing, if you read only the 9 documented fields.
</Update>

<Update label="June 1, 2026" description="New endpoint: ranked market discovery by smart-money net flow">
  Added [`GET /api/v1/markets/smart-money-flows`](/api-reference/endpoint/smart-money-flows), a ranked discovery feed that answers where smart money is flowing before you know a `condition_id`.

  It ranks markets by absolute net flow from S, A, and B grade traders over a `1h`, `4h`, `24h`, or `7d` window, with `platform`, `category`, `min_grade`, and `direction` filters. Pagination uses an opaque cursor anchored to the first page's `as_of`, so new whale trades do not reorder later pages.

  To look at a single market in detail, use [`GET /api/v1/market/{condition_id}/intel`](/api-reference/endpoint/get-market-intel).

  **Backward compatible.** It is a new endpoint.

  **What to change:** Nothing.
</Update>

<Update label="June 1, 2026" description="Remote MCP read parity, usage and caching headers, and round-trippable IDs">
  **Remote MCP read parity.** [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now exposes 21 read-only tools through `tools/list`, covering the public V1 reads: batch reads, history, event replay, webhook reads, reports, market snapshots, and trader exports. Webhook mutations stay REST-only, and the endpoint is tools-only, with no resources and no prompts.

  **Your own usage.** Added [`GET /api/v1/usage`](/api-reference/endpoint/get-usage), an authenticated read of your current per-minute limiter window and your UTC-day usage. It does not spend primary quota, but it has its own guard of 100 reads a minute.

  **Cheaper polling, safe retries.** Deterministic reads now support `ETag` and `If-None-Match` conditional GETs, so an unchanged read returns `304` with no body. Webhook create, update, delete, and rotate accept an `Idempotency-Key`.

  **Discovery.** Added [`GET /api/v1`](/api-reference/endpoint/get-api-discovery), an API discovery document, and [`GET /api/v1/openapi.json`](/api-reference/endpoint/redirect-api-openapi-spec), a redirect to the canonical OpenAPI spec. Operation IDs are now codegen-friendly, with examples, code samples, and response-header metadata.

  **Round-trippable IDs.** Market Intel and Market Snapshot now accept either the `mkt_...` market ID or the raw `condition_id`. New [`GET /api/v1/whale-trades/{id}`](/api-reference/endpoint/get-whale-trade) and [`GET /api/v1/insider-radar/{id}`](/api-reference/endpoint/get-insider-radar-flag) return a single `wt_...` or `rf_...` record. A prefix that is not a market prefix returns `400 bad_request`.

  **Backward compatible.** Everything here is new or optional.

  **What to change:** Nothing.
</Update>

<Update label="May 8, 2026" description="Remote MCP exposes position and market discovery tools">
  [`POST /api/v1/mcp`](/api-reference/endpoint/remote-mcp) now exposes 9 read-only tools through `tools/list`. The new ones are `get_positions`, `get_position_timeline`, and `explore_markets`.

  Each dispatches to the existing public REST handler, so an MCP client gets the same auth, pagination, and payload contracts as a direct REST caller.
</Update>

<Update label="May 8, 2026" description="New builder endpoints: batch, history, events, webhooks, reports, snapshots">
  Added task-shaped builder endpoints, so an integration needs fewer calls:

  * [`POST /api/v1/traders/batch`](/api-reference/endpoint/batch-get-traders)
  * [`POST /api/v1/markets/intel/batch`](/api-reference/endpoint/batch-get-market-intel)
  * [`GET /api/v1/whale-trades/history`](/api-reference/endpoint/get-whale-trades-history)
  * [`GET /api/v1/events/feed/since`](/api-reference/endpoint/get-event-replay-since)
  * [`GET /api/v1/market/{condition_id}/snapshot`](/api-reference/endpoint/get-market-snapshot)
  * [`GET /api/v1/reports/daily`](/api-reference/endpoint/get-daily-report-snapshot), [`weekly`](/api-reference/endpoint/get-weekly-report-snapshot), and [`monthly`](/api-reference/endpoint/get-monthly-report-snapshot)
  * [`GET /api/v1/trader/{address}/export`](/api-reference/endpoint/get-trader-export-snapshot)
  * [`GET /api/v1/webhooks`](/api-reference/endpoint/list-webhooks), plus webhook create, read, update, delete, verify, and rotate-secret operations

  Batch endpoints expose item-weighted cost headers. The new snapshot, history, and report payloads document their source, freshness, completeness, and reconciliation, so you can tell a provider-backed value from a cached, partial, stale, or unavailable one.
</Update>

<Update label="May 8, 2026" description="TypeScript client source added">
  Added [TypeScript client guidance](/integrations/typescript-client) for the repo-owned client source in `web/src/lib/api-client`.

  The client source is drift-tested against OpenAPI. It handles bearer auth, path params, repeated query params, JSON bodies, and V1 error envelopes. It is not a published npm package yet.
</Update>

<Update label="May 7, 2026" description="Market Intel rejects prefixed market IDs">
  [`GET /api/v1/market/{condition_id}/intel`](/api-reference/endpoint/get-market-intel) now returns a clearer `400 bad_request` when a caller passes a prefixed `market.id` value such as `mkt_...` instead of the raw `condition_id`.

  Call [`GET /api/v1/markets/search`](/api-reference/endpoint/search-markets) first, then pass the `condition_id` it returns into the Market Intel path.
</Update>

<Update label="April 14, 2026" description="Trader lookup accepts usernames and plain expand params">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) now accepts a known trader username in the path, as well as an Ethereum wallet address.

  You can request heavy trader fields with repeated `expand` query params. The older `expand[]` form still works for existing clients.
</Update>

<Update label="April 2, 2026" description="Leaderboard strategy fields can be null">
  [`GET /api/v1/leaderboard`](/api-reference/endpoint/get-leaderboard) now returns traders that have no classified strategy.

  Treat `strategy_type` as nullable for a ranked trader with no strategy classification.
</Update>

<Update label="April 1, 2026" description="Explore Markets excludes untitled rows">
  [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) no longer returns untitled markets.

  The response set is now titled, user-facing markets only.
</Update>

<Update label="March 28, 2026" description="Trader realized P&L corrected">
  [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader) now returns the canonical realized P\&L, read from the `realized_pnl` source-of-truth field.

  `pnl.realized` no longer derives realized P\&L from an inconsistent approximation.
</Update>

<Update label="March 26, 2026" description="Explore Markets endpoint launched">
  Added [`GET /api/v1/markets/explore`](/api-reference/endpoint/explore-markets) for cursor-paginated market discovery.

  It supports category, status, platform, and text filters. It also injects the primary market, so a grouped event includes its main market when one exists.
</Update>

<Update label="March 25, 2026" description="Initial V1 launch">
  Launched the first public developer API:

  * [`GET /api/v1/trader/{address}`](/api-reference/endpoint/get-trader)
  * [`GET /api/v1/whale-trades`](/api-reference/endpoint/get-whale-trades)
  * [`GET /api/v1/leaderboard`](/api-reference/endpoint/get-leaderboard)
  * [`GET /api/v1/markets/search`](/api-reference/endpoint/search-markets)
  * [`GET /api/v1/market/{condition_id}/intel`](/api-reference/endpoint/get-market-intel)
  * [`GET /api/v1/insider-radar`](/api-reference/endpoint/get-insider-radar)
  * [`GET /api/v1/health`](/api-reference/endpoint/health)

  The initial contract shipped with Bearer auth, `expand[]`, cursor pagination, prefixed IDs, and rate-limit headers.
</Update>

<Update label="March 25, 2026" description="Numeric precision standardized">
  Money values and scores are truncated to 2 decimal places across V1 responses.

  Prices and rates are truncated to 4 decimal places.
</Update>


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