Skip to main content
These are the API changelog entries from September 24 to 28, 2026, newest first. The newest changes are on the API changelog.
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.
Four-decimal price values preserve small provider division residues
avg_price on GET /api/v1/positions and other price fields served with four-decimal precision now preserve the four-decimal step when their source value is within 0.0000002 of it. For example, 0.4899999968 displays as 0.49 instead of 0.4899.Before this change, provider division residue within that range could lower the last displayed digit. Values outside the tolerance still truncate: 0.003499 becomes 0.0034.A snapped display value can be up to 0.0000002 above its source. Use exact.avg_price when you need decimal calculations from the source precision.Backward compatible. Field names, types, and response shapes are unchanged. Money truncation is unchanged.What to change: Nothing. Continue using exact decimal fields for calculations and display numbers for presentation.
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 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 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.
  • A publication’s UTC date can differ from pick_date, which follows America/New_York. Read the returned timestamps instead of deriving them from the 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.
Settled market exports sum each side's shares from its own cost and entry price
Files from POST /api/v1/trader/{address}/export now calculate total_shares for a settled two-sided position from each side’s own cost and entry price when both cost bases are known and no live share count remains.Before this change, the calculation divided the combined cost by one side’s price. A settled position with several legs on the same outcome now also uses that side’s combined values and weighted entry price, rather than the last leg’s price. A position with unknown per-side cost keeps the existing fallback.Backward compatible. total_shares, entry_price_yes, and entry_price_no keep their names and types. Only affected computed values change.What to change: Nothing. Download a new export to read updated stored values; a previously downloaded file does not change.
API prices and money values keep exact published digits before truncation
Price, rate, and money fields on /api/v1, the SDKs, and the MCP servers now preserve exact published digits when floating-point scaling introduces a tiny error. A price of 0.57 returns 0.57, and a money value of $0.29 returns $0.29.Before this change, those values could return 0.5699 and $0.28. The correction applies to fill prices and sizes on Large trades, candles, book quotes, and the position and trader money fields.Fields still truncate values beyond their published precision. This correction does not guarantee that a provider’s raw value equals the truncated API value.Backward compatible. No field is added, removed, or retyped.What to change: Nothing. A cached value from before this correction can differ by one cent or one price unit.
Trader edge_consistency requires at least 10 days with realized P&L
quant_metrics.edge_consistency on GET /api/v1/trader/{address} with expand=quant_metrics, and POST /api/v1/traders/batch, now returns null below 10 days with realized P&L.The value is the share of the trader’s last 30 days with realized P&L that closed positive, from 0 to 1. The window counts observed P&L days rather than calendar days, so it can span months. Before this change, one profitable day could return 1.0.The field’s OpenAPI description and generated SDK comment now explain the same window and minimum. consistency_percentile remains null when edge_consistency is null.Backward compatible. Both fields already accepted null, which means unavailable and never zero.What to change: Nothing, if you already handle null. Keep an unavailable value separate from a measured score.
Leaderboard score ties use consistent ranking, and older cursors must restart
GET /api/v1/leaderboard now orders wallets with the same displayed score consistently with the website leaderboard and profile rank.Before this change, tied wallets were ordered by address alone, and their relative positions could disagree across these pages.
  • Wallets inside a former tie can change places. The response fields and the meaning of score are unchanged.
  • A cursor issued in the previous format returns 400, with error.code: "bad_request" and error.param: "cursor".
  • The lbv1_ prefix is unchanged. Treat next_cursor as opaque.
Breaking for clients that replay a cursor issued before this change.What to change: Discard a refused cursor and restart at the first page. Do not assume a stored page boundary identifies the same wallet after the ranking changes.
Trader P&L drawdown includes losses below break-even
The drawdown series on GET /api/v1/trader/{address}/pnl now measures cumulative P&L against a running peak that starts at zero. A point below that peak carries the negative dollar difference, with a difference smaller than 1 cent represented as 0.Before this change, the peak started at negative infinity, and the series reported a drawdown only after the curve had reached positive cumulative P&L. An opening losing period therefore returned 0, even when the wallet had substantial losses.
  • A cumulative curve of [-1000, -50000, -20000] now has drawdowns of [-1000, -50000, -20000], instead of [0, 0, 0].
  • Points before the first positive peak can change even when the wallet becomes profitable later.
  • Points after a positive peak retain the previous calculation. The response shape, date window, and other P&L sections are unchanged.
Breaking for clients that interpreted an all-zero drawdown series as proof that the wallet had no losses.What to change: Use the returned drawdown values. A maximum drawdown computed from them can now be nonzero for a wallet that previously showed 0.
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 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.