You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fetchBuilderFeeRates in packages/client/src/actions/clob.ts:398-409 doesn't translate or guard the GET /fees/builder-fees/<code> 404 case, and fetchBuilderTakerFeeRate in packages/client/src/actions/orders/market.ts:222-232 calls it unconditionally whenever builderCode is set on a market order. The result is that any prepareMarketOrder / prepareMarketBuyOrder / prepareMarketSellOrder call carrying an unregistered (or stale) builder code throws on the fee fetch and never reaches order construction.
The unwrap call surfaces any non-2xx response as a RequestRejectedError, which is in the public error union — but fetchBuilderTakerFeeRate does not catch it, and the public prepareMarketOrder path treats every error here as fatal.
Live server behavior
The endpoint returns 404 Not Found with body {"error":"builder code not found"} for any code the server doesn't have registered:
CORS is fine — the failure is purely the SDK propagating the 404 as an unwrap throw and killing the request pipeline. (Aside: the original V2-transitional report at Polymarket/clob-client-v2#51 claimed CORS was missing on this route; that part of the report is incorrect, the route does set ACAO: *. The behavioral bug — 404 throw → killed createOrder — is real.)
Same shape on clob-client-v2
This is structurally the same as Polymarket/clob-client-v2#51, with PR #72 by @Cassxbt proposing a silent try/catch in ensureBuilderFeeRateCached. That PR has been open since 2026-05-21 without maintainer engagement; the underlying behavior question (silent skip vs typed error) seems unresolved.
Design question
Three approaches I can see, ranked by how invasive they are:
Silent skip (@Cassxbt's clob-v2 PR docs: clarify order book and subscription types #72 approach): wrap the call in try/catch, return a zero BuilderFeeRates. Lets the order proceed but drops builder attribution silently — bad UX if the caller actually intended attribution.
Typed error mapping: ResultAsync.mapErr on the request pipeline to translate RequestRejectedError.status === 404 into a new UnknownBuilderCodeError. Add it to FetchBuilderFeeRatesError and to prepareMarketOrderError's union. Caller has to decide policy.
Server-side change: the route returns 404 only when the builder code is unknown. If the platform also wants to surface "registered but not active" or similar states, distinguish them.
I'd lean toward (2) — it preserves information (the caller can tell "this builder code isn't registered" apart from "transport error / 500"), follows the SDK's existing pattern of mapping HTTP failures into typed errors (packages/client/src/errors.ts), and matches the AGENTS.md guidance about using mapErr over try/catch around unwrap. But this is a design call and could go differently.
Impact
Limit orders are unaffected (limit.ts doesn't call fetchBuilderTakerFeeRate). The break is scoped to market orders with builder attribution — but those exist precisely because builder attribution is the documented V2 monetization path. A new builder who pastes their builder code from an older test environment, or whose code hasn't yet propagated to the fee table, gets a confusing RequestRejectedError from a market-order call rather than from the builder-fee call.
Cross-SDK
Same pattern in py-sdk: src/polymarket/_internal/actions/orders/market_data.py:75-83 (fetch_builder_fee_rates / fetch_builder_fee_rates_sync) raises on 404, callers in _internal/actions/orders/market.py:185-217 re-raise. Filing companion issue.
fetchBuilderFeeRatesinpackages/client/src/actions/clob.ts:398-409doesn't translate or guard theGET /fees/builder-fees/<code>404 case, andfetchBuilderTakerFeeRateinpackages/client/src/actions/orders/market.ts:222-232calls it unconditionally wheneverbuilderCodeis set on a market order. The result is that anyprepareMarketOrder/prepareMarketBuyOrder/prepareMarketSellOrdercall carrying an unregistered (or stale) builder code throws on the fee fetch and never reaches order construction.Current code
The
unwrapcall surfaces any non-2xx response as aRequestRejectedError, which is in the public error union — butfetchBuilderTakerFeeRatedoes not catch it, and the publicprepareMarketOrderpath treats every error here as fatal.Live server behavior
The endpoint returns
404 Not Foundwith body{"error":"builder code not found"}for any code the server doesn't have registered:CORS is fine — the failure is purely the SDK propagating the 404 as an unwrap throw and killing the request pipeline. (Aside: the original V2-transitional report at Polymarket/clob-client-v2#51 claimed CORS was missing on this route; that part of the report is incorrect, the route does set
ACAO: *. The behavioral bug — 404 throw → killedcreateOrder— is real.)Same shape on clob-client-v2
This is structurally the same as Polymarket/clob-client-v2#51, with PR #72 by @Cassxbt proposing a silent try/catch in
ensureBuilderFeeRateCached. That PR has been open since 2026-05-21 without maintainer engagement; the underlying behavior question (silent skip vs typed error) seems unresolved.Design question
Three approaches I can see, ranked by how invasive they are:
@Cassxbt's clob-v2 PR docs: clarify order book and subscription types #72 approach): wrap the call in try/catch, return a zeroBuilderFeeRates. Lets the order proceed but drops builder attribution silently — bad UX if the caller actually intended attribution.ResultAsync.mapErron the request pipeline to translateRequestRejectedError.status === 404into a newUnknownBuilderCodeError. Add it toFetchBuilderFeeRatesErrorand toprepareMarketOrderError's union. Caller has to decide policy.I'd lean toward (2) — it preserves information (the caller can tell "this builder code isn't registered" apart from "transport error / 500"), follows the SDK's existing pattern of mapping HTTP failures into typed errors (
packages/client/src/errors.ts), and matches the AGENTS.md guidance about usingmapErrover try/catch aroundunwrap. But this is a design call and could go differently.Impact
Limit orders are unaffected (
limit.tsdoesn't callfetchBuilderTakerFeeRate). The break is scoped to market orders with builder attribution — but those exist precisely because builder attribution is the documented V2 monetization path. A new builder who pastes their builder code from an older test environment, or whose code hasn't yet propagated to the fee table, gets a confusingRequestRejectedErrorfrom a market-order call rather than from the builder-fee call.Cross-SDK
Same pattern in py-sdk:
src/polymarket/_internal/actions/orders/market_data.py:75-83(fetch_builder_fee_rates/fetch_builder_fee_rates_sync) raises on 404, callers in_internal/actions/orders/market.py:185-217re-raise. Filing companion issue.Happy to PR once you've picked a direction.