Skip to main content
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.
Pick of the Day automatic picks publish from 3:00 AM ET; retry_at names that opening
GET /api/v1/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.
Pick of the Day automatic picks now publish 30 to 45 minutes before kickoff
GET /api/v1/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.
Pick of the Day pick_rank can reach 20 after a cancelled pick; picks stay at 15
GET /api/v1/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.
Live-score source_revision values jump to millisecond scale and stay increasing
GET /api/v1/market/{condition_id}/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.
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.
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.
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.
Referral-only paid API and feed access ends when the earned grant ends
Paid operations in the REST API, MCP, and the live event feed 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.
Soccer games identify home, draw, and away markets
GET /api/v1/games and GET /api/v1/games/{event_slug} 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.
Pick of the Day archive excludes withdrawn picks and their statistics
GET /api/v1/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 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.
Pick of the Day excludes cancelled games from active recommendations
GET /api/v1/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 and 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.
Pick of the Day game state covers totals and spread selections
GET /api/v1/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.
Released Pick of the Day records are permanent
With this release, the Pick of the Day archive and public 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 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.
Pick of the Day can include additional verified selections
GET /api/v1/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.
Pick of the Day can include more S-grade-backed selections
GET /api/v1/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.
Tennis scores add available ATP and WTA singles rankings
Market snapshots, LiveScoreChanged frames in the live stream, and browser feeds now include optional live_score.scores[].ranking for tennis players. The Markdown market context 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 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.
Pick of the Day total labels include the exact Over/Under threshold
GET /api/v1/pick-of-the-day and the 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.
Pick of the Day publishes automatic selections as soon as they qualify
GET /api/v1/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.
Pick of the Day adds the lead wallet's profitable-event count
GET /api/v1/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.
Pick of the Day can return qualifying picks from previously omitted markets
GET /api/v1/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.
Pick of the Day display_holders lists every graded holder on certified picks
GET /api/v1/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.
Paid profiles can load pUSD wallet observations from Goldsky
The first-party session endpoint GET /api/trader/{address}/funding/supplemental 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.
Pick of the Day adds available ATP and WTA singles rankings
GET /api/v1/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.
Pick of the Day retains verified final scores after the live score expires
GET /api/v1/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.
Large trade detail shows counterparties once a pending receipt is processed
GET /api/v1/large-trades/{id} 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.
Pick of the Day raises new entry allowances from 2 cents to 5 cents
GET /api/v1/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 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.
Pick of the Day lead_backer adds the net position of a two-sided lead
GET /api/v1/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.
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 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.
MCP tool results return compact JSON text
POST /api/v1/mcp now returns result.content[0].text of a successful tools/call as compact JSON, with no indentation or line breaks. The 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.
Sandbox MCP discovery describes the current Pick of the Day access rules
POST /sandbox/api/v1/mcp 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.
Pick of the Day requires consistent lead history from before settlement
GET /api/v1/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.
Pick of the Day releases 30 minutes before kickoff instead of 60
GET /api/v1/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.
Pick of the Day adds recorded lead wallet positions and directional category history
GET /api/v1/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.
Market snapshots and score frames retain unexpired tennis points during detail outages
GET /api/v1/market/{condition_id}/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 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.
Market snapshots retain live esports map scores through incomplete updates
GET /api/v1/market/{condition_id}/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.
Pick stats leave z-scores unavailable when contributing selections share a game
GET /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.
Pick of the Day can return two compatible selections from one game
GET /api/v1/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.
Full sports board and rail feeds retain verified moneyline kickoff times
GET /api/activity/sports-board and GET /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 and GET /api/v1/games/{event_slug} 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.
Pick of the Day covers more markets within the existing 15-pick daily ceiling
GET /api/v1/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.
Game and market score clocks include cached Polymarket event observations
GET /api/v1/games and GET /api/v1/games/{event_slug} now populate freshness.scores_observed_at for scores read from Polymarket’s Gamma events API. GET /api/v1/market/{condition_id}/snapshot carries the same clock in sports.live_score.observed_at, including the JSON embedded in market context as 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.
potd-trader 0.3.6 enforces authenticated Pro and Max daily pick limits
potd-trader 0.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.
Developers can create multiple independent live API keys
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.
Daily pick OpenAPI descriptions match the 15-pick limit
The OpenAPI operation descriptions for current picks and the pick 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.
Pick of the Day restores the 15-pick ceiling
Pick of the Day limits new daily selections to 15. Current, scheduled, proof-pending, 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.
Daily pick schemas allow ranks and counts through 15
Pick of the Day now documents pick_rank and pick_count ceilings of 15. Scheduled picks, proof-pending picks, and archive entries 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.
Sports team logos add official NBA, NHL, and MLB crests
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.
potd-trader 0.3.5 accepts Pro and Max slates through 20 ranks
potd-trader 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. 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.
Pick access adds Max and withholds unresolved game identity
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 omit game identity and identifying clocks; required_tier states the access needed.
  • Public sealed ledger entries 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.
API monthly allowances increase for Pro and add Max
API 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.
Pick of the Day can carry up to 15 selections per day instead of 10
GET /api/v1/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.
Live tennis scores add optional current-game points and the serving player
Market snapshots and LiveScoreChanged frames in the live stream and browser feeds now carry optional live_score.tennis_points from API-Tennis. The Markdown market context 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 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 for the new types.
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.
Trader styles add 6 observable labels to profiles and leaderboard filters
GET /api/v1/trader/{address} with expand=strategy, Batch traders, and 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. Keep support for historical IDs, and update SDK or local MCP types before sending a new filter value. See Wallet trading styles for the evidence rules and limits.
Tennis photos carry independent revisions and immutable image versions
Tennis competitors in GET /api/v1/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.
Webhook verification gains durable asynchronous attempts
Start webhook verification now returns 202 with an attempt UUID and status path. Get webhook verification reports its current state and sanitized outcome. SDK 0.17.0 adds createWebhookVerificationAttempt and getWebhookVerificationAttempt.Before this change, Verify a 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.
Pick of the Day can draw from more sports and esports markets
GET /api/v1/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.
Exports that pass their retention deadline now finish as failed
Trader exports and large-trade datasets 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.
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.
Daily picks can release earlier for all games
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.
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 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.
Frozen picks release one hour before kickoff
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.
Automatic picks publish when their selection is frozen
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.
Scheduled picks keep their selected identity through release
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.
MCP setup pins the curated 2.14.0 package
MCP setup 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.
Pick notifications link to stable selection pages
New Pick of the Day 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.
Pick responses omit internal selection diagnostics
Pick of the Day and its 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.
Pick of the Day admits eligible selections in a neutral order
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.
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 now pins 2.13.0, and the TypeScript guide shows the released SDK helper. The corresponding REST route 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.
Stable Pick of the Day ledger lookups honor public keyless access
GET /api/v1/pick-of-the-day/ledger/{pick_id} 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.
Games report Started for Dota matches with a settled map winner
GET /api/v1/games and GET /api/v1/games/{event_slug} 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.
Pick of the Day adds stable IDs and versioned commitments
Pick of the Day, its archive, and its 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} 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.
Future Pick of the Day entry authorizations stop at 0.85
GET /api/v1/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.
The TypeScript SDK is available on npm as @0xinsider/sdk 0.14.0
@0xinsider/sdk 0.14.0 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.
MCP output schemas describe null with explicit JSON Schema alternatives
POST /api/v1/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.
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.
RSS discovery omits a modification date that does not cover pick updates
GET /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.
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.
OAuth refresh keeps the existing scopes when scope is blank
POST /oauth/token 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.
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. 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.
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. Keep your clock synchronized and schedule StreamRetryDeferredError.retryAt when a wait exceeds your ceiling. The standalone SDK’s first npm release remains pending.
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.
Remote MCP publishes result schemas and OAuth account-linking metadata
POST /api/v1/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.
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 until it is published.
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. 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.
Cold SSE replicas resume after a confirmed feed sequence reset
GET /api/v1/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.
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.
Trader export download links expire within the file's retention window
GET /api/v1/trader/{address}/export/download 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 and PR #19994.
Market-holder cursors preserve your position when page size changes
GET /api/v1/market/{condition_id}/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.
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.
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.
SSE reconnects discard replay overlap without a false reset
GET /api/v1/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.
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 is tracked separately.
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.
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.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.
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.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.
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 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.
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.
SSE replay checks ongoing authorization before sending more protected events
GET /api/v1/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.
Export artifacts return the storage ETag without XML escapes
GET /api/v1/datasets/whale-trades/{job_id} and GET /api/v1/trader/{address}/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 " 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.
Whale datasets add immutable downloads with checksums and replay continuation
POST /api/v1/datasets/whale-trades 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.
Expanded trader categories keep live source freshness during historical fill imports.
GET /api/v1/trader/{address} 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.
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.
Trending wallet responses limit cache freshness to 120 seconds.
GET /api/v1/leaderboard/trending 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.
Trending wallets keep the previous board when a refresh fails.
GET /api/v1/leaderboard/trending 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.
Sports edge observations refresh market identity before category selection.
GET /api/v1/sports/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.
Expanded trader category ranks and totals use the same publication.
GET /api/v1/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.
Category-filtered positions pages keep consistent membership during updates.
GET /api/v1/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.
Large trades take since, so a poll returns only trades recorded after one you hold
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.
A cold sports-edge observations process answers a replica hiccup with 503, not 500
GET /api/v1/sports/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.
Trending wallets reports ranking age and serves the previous ranking during refresh
GET /api/v1/leaderboard/trending 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.
OAuth responses include server processing duration in Server-Timing
OAuth authorization and discovery responses 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.
Web API mirrors expose origin response timing and a web request ID
Requests through the web API mirror 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.
The bare 0xinsider npm package installs the product CLI 2.x
0xinsider@2.0.0 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.
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 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.
The sandbox's events/feed/since meta describes each page
GET /api/v1/events/feed/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.
Unmatched trader lookups return 404 instead of echoing the input as an address
GET /api/v1/trader/{address}, GET /api/v1/trader/{address}/context and GET /api/v1/trader/{address}/context.md 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 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.
The sandbox's events/feed/since sends next_cursor on its last page
GET /api/v1/events/feed/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.
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 and its deprecated alias GET /api/v1/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.
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.
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: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 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 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.
Batch traders answers an unknown username or trader id with not_found
POST /api/v1/traders/batch 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} 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.
A malformed condition_id on large positions now returns 400, not an empty list
GET /api/v1/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”.
Pick of the Day archive entries carry published_at and resolved_at
Every entry from GET /api/v1/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 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 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.
Market explorer smart_label names a lean only when graded money leans against the price
smart_label on GET /api/v1/markets/explore 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.
Pick of the Day display_category names competitions outside the curated list
GET /api/v1/pick-of-the-day and GET /api/v1/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.
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 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.
OpenAPI adds five Pick of the Day fields, sort=large_trades, and a 500 on every route
The OpenAPI document, the SDK types generated from it, and @0xinsider/mcp 2.10.1 now describe fields and values the API already served: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.
Trader P&L totals use exact daily changes, including gains or losses below 1 cent
GET /api/v1/trader/{address}/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.
Candles reject a from or to sent in milliseconds instead of seconds
GET /api/v1/market/{condition_id}/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.
Report periods that end before 2024-03-01 or start after tomorrow UTC return 400
GET /api/v1/reports, GET /api/v1/reports/daily, GET /api/v1/reports/weekly and GET /api/v1/reports/monthly 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.
List routes clamp limit values and report them in X-Effective-Query
GET /api/v1/games, GET /api/v1/events/feed/since, GET /api/v1/large-trades/{id}/counterparties/executions, and GET /api/v1/large-trades/{id}/counterparties/executions/{execution_id}/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 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.
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 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 and GET /api/v1/large-trades/history take.
  • batch_get_traders accepts trust in expand, as POST /api/v1/traders/batch 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 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.
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 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.
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.
A read-scoped credential reaches trader usernames that start with export
GET /api/v1/trader/{address} 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 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.
MCP get_positions omits min_size by default for complete wallet position lists
The get_positions MCP tool in @0xinsider/mcp 2.9.1 sends min_size to GET /api/v1/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.
Market search reports a closed but unresolved market as closed
GET /api/v1/markets/search 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 and GET /api/v1/market/{condition_id}/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.
The stream marks a sequence numbering restart instead of going quiet
GET /api/v1/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.
Trader reads say when open-position P&L could not be read
GET /api/v1/trader/{address} and POST /api/v1/traders/batch 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.
The forecasting axis covers nearly every graded wallet
GET /api/v1/trader/{address} 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.
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 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.
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, 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 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.
Coverage gets its own route; /platforms becomes a deprecated alias
GET /api/v1/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.
Market flow gets canonical paths; the intel paths become deprecated aliases
GET /api/v1/market/{condition_id}/flow and POST /api/v1/markets/flow/batch 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.
Wallet positions offer a five-minute snapshot for repeatable pagination
GET /api/v1/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.
MCP get_positions adds wallet and consistency, matching the REST route
The get_positions 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 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.
Large-trade subscriptions add individual payloads and filters
GET /api/v1/stream now emits LargeTradeInsertedV2 with one trade’s market, wallet, grade snapshot, size, side, price, and durable event_id. POST /api/v1/webhooks 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.
Trader grade at a past decision time
GET /api/v1/trader/{address}/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.
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.
Game API markets now include provider moneyline prices and availability states
GET /api/v1/games and GET /api/v1/games/{event_slug} 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.
List reads add stored data_quality
GET /api/v1/positions, GET /api/v1/large-trades, GET /api/v1/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.
Trader data quality dates rank and open-position P&L
GET /api/v1/trader/{address}, POST /api/v1/traders/batch and GET /api/v1/trader/{address}/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.
Trader reads can require a freshness ceiling
GET /api/v1/trader/{address} 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.
The webhook event catalog lists all 14 subscribable types
GET /api/v1/webhooks/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 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).
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.
  • GET /api/v1/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 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 and ranked 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.
Export jobs can be cancelled; the job adds cancel_requested and cancelled
POST /api/v1/trader/{address}/export/cancel 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 and POST .../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.
Trader reads always return data_quality, and trust names the real owner of each clock
GET /api/v1/trader/{address}, POST /api/v1/traders/batch and GET /api/v1/trader/{address}/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.
Large trades add a market-significance filter and sort
GET /api/v1/large-trades, GET /api/v1/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.
Trader and positions responses add exact decimal atoms
GET /api/v1/trader/{address} adds pnl.exact.realized and stats.exact.total_volume. GET /api/v1/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.
The sandbox lists all 44 MCP tools and answers tools/call with a tool result
The 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.
Large trades return review_score beside signal_score; MCP adds market_review
GET /api/v1/large-trades, GET /api/v1/large-trades/{id}, GET /api/v1/large-trades/history, their /api/v1/whale-trades aliases, and expand=trade on GET /api/v1/events/feed/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.
Export status adds immutable artifact manifests and checksums
GET /api/v1/trader/{address}/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.
  • 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.
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, GET /api/v1/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.
The sandbox answers the MCP handshake the way the live API does
The 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.
Export jobs can notify their owner when ready, failed or expired
POST /api/v1/webhooks can now subscribe to export_job_ready, export_job_failed, and export_job_expired; GET /api/v1/webhooks/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 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.
Trader profiles publish a capital-normalized forecasting axis
GET /api/v1/trader/{address} 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.
Sandbox responses align with MCP errors, grades, holders, batches, and webhooks
The 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 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: 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.
Market intel names the age of the oldest position behind top_positions
GET /api/v1/market/{condition_id}/intel and POST /api/v1/markets/intel/batch 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.
The sandbox aligns 12 more request checks and response shapes with the live API
The 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 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 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.
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 lists what 0xinsider covers and GET /api/v1/games/{event_slug} 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, GET /api/v1/markets/sharp-money-flows and GET /api/v1/market/{condition_id}/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.
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.
Export jobs report lifecycle details; expired downloads return 410
POST /api/v1/trader/{address}/export and GET /api/v1/trader/{address}/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 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.
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/v1/large-trades/{id}, /api/v1/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.
Insider Radar is now Suspicious trades, and the old routes stay live as aliases
GET /api/v1/suspicious-trades and GET /api/v1/suspicious-trades/{id} 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} 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 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 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 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.
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 and each live game sends one pulse when its scores, status, period, live, or ended state moves. GET /api/v1/webhooks/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 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.
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".
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.
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.
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.
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.
Market holders answers 200 with an empty roster for a market no graded wallet holds
GET /api/v1/market/{condition_id}/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.
Pre-game sides get canonical paths, and four canonical row field names
GET /api/v1/sports/pre-game-sides and GET /api/v1/sports/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.
MCP adds list_games and get_game for the game catalog
The MCP servers add two read-only tools: list_games, which calls GET /api/v1/games, and get_game, which calls GET /api/v1/games/{event_slug}. 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.
V1 query diagnostics decode form values and reject malformed unknown names
/api/v1 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.
@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.
Official Rust client, and the TypeScript client's public source repository
Two official clients are now public repositories: 0xinsider/0xinsider-rust, the Rust client (crate oxinsider), and 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 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 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.
The sandbox answers the two Markdown routes, the export download and the MCP GET
The sandbox server answers four operations it used to refuse with 400 bad_request. A caller holding only a sandbox key from POST /api/v1/agents/register 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 and GET /api/v1/market/{condition_id}/context.md 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 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 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.
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 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 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.
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 and GET /api/v1/market/{condition_id}/context.md 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 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 is client.registerAgent(). It needs no credential and answers 201 with the oxi_sk_test_ sandbox key.
  • GET /api/v1/trader/{address}/export/download 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 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.
The sandbox pages through a coherent world and refuses what the live API refuses
The 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 and POST /api/v1/markets/intel/batch 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.
The OpenAPI document now matches what the server does on 6 points
/api/v1/openapi.json now describes what the server already does in 6 places. Nothing on the wire changed.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.
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.
Report grade distributions now describe the traders in the report period
GET /api/v1/reports and the daily, weekly, and monthly 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.
potd-trader 0.3.0 adds shared stopping, atomic reservations, and a UTC daily budget
The POTD auto-trader 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 with uv sync --locked. Run with --dry-run before you restart live.
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.
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.
Large trades: market_volume_share never exceeds 1
market_volume_share on GET /api/v1/whale-trades, GET /api/v1/whale-trades/history, GET /api/v1/whale-trades/{id}, 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.
The OpenAPI document separates fields a response leaves out from fields it sends as null
The OpenAPI document 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 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.
Every whale trade carries the grade its wallet held when the trade happened
GET /api/v1/whale-trades, GET /api/v1/whale-trades/history, GET /api/v1/whale-trades/{id}, and the trade object from expand=trade on GET /api/v1/events/feed/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.
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.
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. 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.
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, 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 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.
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, 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 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).
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 and its makers route take a cursor but answer object: "counterparty_analysis" with an object data, so call their methods directly.
The Pick of the Day ledger is public: no API key, no subscription
GET /api/v1/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 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.
GET /api/v1/positions filters by wallet, so a portfolio no longer pages the whole board
GET /api/v1/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 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}.
  • 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.
Event replay takes four filters, and expand=trade adds the full trade to each event
GET /api/v1/events/feed/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} 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} 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.
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.
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.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.
  • 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.
The stream closes with an error frame when its key stops working
GET /api/v1/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.
Pick of the Day is priced at a flat $1,000 stake instead of $100
GET /api/v1/pick-of-the-day and GET /api/v1/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 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.
GET /api/v1/me reports whether paid data access is active or lapsed
GET /api/v1/me 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 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.
Insider Radar takes mode=stable, a cursor tied to one published set of scores
GET /api/v1/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.
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, then activate, then retire. 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 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.
Webhook delivery records add retry_schedule_reason beside next_attempt_at
GET /api/v1/webhooks/{id}/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.
Sharp money flow cursors expire when the filters or the data behind them change
GET /api/v1/markets/sharp-money-flows and its deprecated smart-money-flows alias 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.
Market candles URL-decode the query and refuse a from that is later than to
GET /api/v1/market/{condition_id}/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.
Trending wallet cursors are tied to the limit, the window, and one ranked board
GET /api/v1/leaderboard/trending 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.
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 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.
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.
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 and 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.
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.
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.
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 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.
A weekly report range is capped at 31 days and comes back marked ephemeral
GET /api/v1/reports and GET /api/v1/reports/weekly 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.
A report reads final only once its body was built after snapshot.final_after
GET /api/v1/reports and the daily, weekly, and monthly 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.
Event replay orders by commit time, so a stored cursor no longer skips a trade
GET /api/v1/events/feed/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.
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 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.
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.
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.
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.
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.
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.
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.
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.
Every MCP tool result carries the route's meta, and a failed call carries the error fields
On POST /api/v1/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.
The search_markets MCP tool pages by cursor on both transports
The search_markets tool on POST /api/v1/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 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.
Remote MCP answers 400 to an unsupported MCP-Protocol-Version header
POST /api/v1/mcp and GET /api/v1/mcp 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.
Remote MCP checks tools/call arguments against the tool's inputSchema
POST /api/v1/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.
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.
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 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.
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 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 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.
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.
List every graded holder of a market
Added GET /api/v1/market/{condition_id}/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 ignored condition_id, and GET /api/v1/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.
Get a wallet's win record in every category, and in every esports game
Added GET /api/v1/trader/{address}/categories: 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, 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.
Esports holder entries name the game their win rate was measured in
Holder entries on GET /api/v1/pick-of-the-day and GET /api/v1/market/{condition_id}/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.
Pick of the Day archive publishes the price behind return_per_100 and both CLV operands
GET /api/v1/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.
The commitment ledger carries the payload of settled pre-sealing picks
GET /api/v1/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.
Positions accept condition_id, and a deep page returns as fast as the first
GET /api/v1/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.
Remote MCP and @0xinsider/mcp 2.1.0 add get_sports_edge_observations
POST /api/v1/mcp and the stdio package @0xinsider/mcp 2.1.0 add get_sports_edge_observations, which calls GET /api/v1/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.
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 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.
New endpoint: every Pick of the Day's pre-game commitment, opened once it settles
Added GET /api/v1/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.
Large positions report each outcome on its own and list every holding
On GET /api/v1/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.
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:
  • 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} stats.total_volume, GET /api/v1/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.
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 and .../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.
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.
Pick of the Day holders carry the counts behind the category win rate
Holder entries on GET /api/v1/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.
@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 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.
Typed category evidence on the trader route and on Pick of the Day holders
GET /api/v1/trader/{address}?expand=categories 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 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.
Pick of the Day holders carry wallet badges
Holder entries on GET /api/v1/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.
GET /api/v1/me returns the account and credential a request authenticated as
Added GET /api/v1/me. 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.
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.
Remote MCP adds get_sports_edge_signals, plus a Markdown document per market
POST /api/v1/mcp and the stdio package add get_sports_edge_signals, which dispatches to GET /api/v1/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, 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, 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.
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 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.
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.
Pick of the Day's CLV average is reported in percentage points
GET /api/v1/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.05thatclosesat0.05 that closes at 0.10 counts as +100%, while a pick at 0.90thatclosesat0.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.
Large trades carry their share of the market's volume
GET /api/v1/whale-trades, GET /api/v1/whale-trades/history, and GET /api/v1/whale-trades/{id} 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,000fillis0.00005ofa10,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.
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.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.
Pick of the Day serves every ready pick while another pick's proof is pending
GET /api/v1/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.
Remote MCP adds two Pick of the Day tools
POST /api/v1/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, and get_pick_of_the_day_archive to GET /api/v1/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.
Pick of the Day's 404 retry_at accounts for newly available selections
GET /api/v1/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.
Pick of the Day lists the day's picks that have not released yet
GET /api/v1/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.
@0xinsider/mcp 1.0.9 serves 29 tools locally, up from 6
@0xinsider/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 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.
Large trades carry the signal score captured when the trade was recorded
GET /api/v1/whale-trades, GET /api/v1/whale-trades/history, and GET /api/v1/whale-trades/{id} 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.
Pick of the Day reports the availability of stored trader context
GET /api/v1/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.
New observation-only endpoint for wider-holder and in-play sports markets
Added GET /api/v1/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, Pick of the Day, or any order executor.What to change: Nothing.
Sports Edge covers Golf, Formula 1, NBA Summer League, CFL, and Boxing
GET /api/v1/sports-edge-signals and GET /api/v1/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.
New endpoint: ranked pre-game sports markets with graded money on one side
Added GET /api/v1/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.
Pick of the Day returns 404 outside today's pick, and adds game_started
GET /api/v1/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.
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.
Pick of the Day: a fixed 19:00 Europe/Berlin release and any sport
GET /api/v1/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.
Whale trades expose the traded outcome and its CLOB token id
Each whale trade on GET /api/v1/whale-trades, /whale-trades/history, and /whale-trades/{id} 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.
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:Backward compatible. The field is optional, so existing clients are unaffected.What to change: Nothing.
Pick of the Day archive adds a cumulative track-record series
GET /api/v1/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.
Trader quant_metrics is now a documented, fixed field set
GET /api/v1/trader/{address} and POST /api/v1/traders/batch 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, and the Quant metrics guide 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.
New endpoint: ranked market discovery by smart-money net flow
Added GET /api/v1/markets/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.Backward compatible. It is a new endpoint.What to change: Nothing.
Remote MCP read parity, usage and caching headers, and round-trippable IDs
Remote MCP read parity. POST /api/v1/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, 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, an API discovery document, and GET /api/v1/openapi.json, 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} and GET /api/v1/insider-radar/{id} 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.
Remote MCP exposes position and market discovery tools
POST /api/v1/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.
New builder endpoints: batch, history, events, webhooks, reports, snapshots
Added task-shaped builder endpoints, so an integration needs fewer calls: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.
TypeScript client source added
Added TypeScript client guidance 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.
Market Intel rejects prefixed market IDs
GET /api/v1/market/{condition_id}/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 first, then pass the condition_id it returns into the Market Intel path.
Trader lookup accepts usernames and plain expand params
GET /api/v1/trader/{address} 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.
Leaderboard strategy fields can be null
GET /api/v1/leaderboard now returns traders that have no classified strategy.Treat strategy_type as nullable for a ranked trader with no strategy classification.
Explore Markets excludes untitled rows
GET /api/v1/markets/explore no longer returns untitled markets.The response set is now titled, user-facing markets only.
Trader realized P&L corrected
GET /api/v1/trader/{address} 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.
Explore Markets endpoint launched
Added GET /api/v1/markets/explore 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.
Initial V1 launch
Launched the first public developer API:The initial contract shipped with Bearer auth, expand[], cursor pagination, prefixed IDs, and rate-limit headers.
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.