Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,13 @@ set(POLYMARKET_CLIENT_SOURCES
src/order_signer_domain.cpp
src/order_signer_v2.cpp
src/sdk_error.cpp
src/rate_limit.cpp
src/clob_account.cpp
src/clob_activity.cpp
src/clob_activity_pagination.cpp
src/clob_books.cpp
src/clob_client.cpp
src/clob_rate_limit.cpp
src/clob_market_parsing.cpp
src/clob_market_metadata.cpp
src/clob_mutation_parsing.cpp
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,29 @@ if (!result) {
```

`SdkError` includes the endpoint, HTTP status, response excerpt, retryability,
and the request ID when the server returns one.
and the request ID when the server returns one. Rejections also carry
`retry_after_seconds` and the `Poly-RateLimit-*` state when the server sends them.

### Rate limits

Reads that get HTTP 429 are retried up to twice, each after exactly the
server's `Retry-After` delay (1 s when absent). A requested delay above 5 s
returns the error instead. Orders, cancellations, and other writes are never
retried; check `retry_after_seconds` on their error and decide yourself.

```cpp
client.set_rate_limit_retry(polymarket::RateLimitRetry{1, std::chrono::seconds(2)});
client.set_rate_limit_retry(std::nullopt); // fail on the first 429

client.set_rate_limit_listener([](const polymarket::RateLimitUpdate &update) {
if (update.warning) std::cerr << "order rate would be rejected under enforcement\n";
});
```

The listener receives the `Poly-RateLimit-*` headers from order and cancel
responses. `PositionClientConfig::rate_limit_retry` and `Config::rate_limit_retry`
set the same policy for `PositionClient` and `MarketFetcher`. Unlike the official
SDKs, CLOB reads are retried too; disable it for latency-critical paths.

### On-chain positions

Expand Down
14 changes: 14 additions & 0 deletions cmake/PolymarketTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ if(POLYMARKET_CLIENT_BUILD_TESTS)
target_link_libraries(test_sdk_error PRIVATE polymarket::client)
add_test(NAME test_sdk_error COMMAND test_sdk_error)

add_executable(test_rate_limit tests/test_rate_limit.cpp)
target_include_directories(test_rate_limit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_rate_limit PRIVATE polymarket::client)
add_test(NAME test_rate_limit COMMAND test_rate_limit)

add_executable(test_clob_rate_limit tests/test_clob_rate_limit.cpp)
target_link_libraries(test_clob_rate_limit PRIVATE polymarket::client)
add_test(NAME test_clob_rate_limit COMMAND test_clob_rate_limit)

add_executable(test_position_rate_limit tests/test_position_rate_limit.cpp)
target_include_directories(test_position_rate_limit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_position_rate_limit PRIVATE polymarket::client)
add_test(NAME test_position_rate_limit COMMAND test_position_rate_limit)

add_executable(test_websocket_resilience tests/test_websocket_resilience.cpp)
target_include_directories(test_websocket_resilience PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_websocket_resilience PRIVATE polymarket::client)
Expand Down
21 changes: 21 additions & 0 deletions include/polymarket/clob_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <vector>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <optional>
#include <set>
#include <map>
Expand Down Expand Up @@ -235,6 +236,16 @@ namespace polymarket
// TCP keepalive probe interval
void set_keepalive_interval(long seconds) { http_.set_keepalive_interval(seconds); data_http_.set_keepalive_interval(seconds); }

// Receives Poly-RateLimit-* state reported by order and cancel responses,
// on the thread that made the request. Listener exceptions are ignored;
// an empty function removes the listener.
void set_rate_limit_listener(std::function<void(const RateLimitUpdate &)> listener);

// Read requests that get HTTP 429 are retried after the server's
// Retry-After delay; orders, cancellations and other writes never are.
// Defaults to RateLimitRetry{}; std::nullopt disables retries.
void set_rate_limit_retry(std::optional<RateLimitRetry> policy);

// ============================================================
// CONNECTION WARMING (for low-latency trading)
// ============================================================
Expand Down Expand Up @@ -308,6 +319,16 @@ namespace polymarket
std::map<std::string, MetadataCacheEntry<TickSizeInfo>> tick_size_cache_;
std::map<std::string, MetadataCacheEntry<NegRiskInfo>> neg_risk_cache_;

using RateLimitListener = std::function<void(const RateLimitUpdate &)>;
mutable std::mutex rate_limit_mutex_;
std::shared_ptr<const RateLimitListener> rate_limit_listener_;
std::optional<RateLimitRetry> rate_limit_retry_{RateLimitRetry{}};
void notify_rate_limit(const HttpResponse &response, RateLimitUpdate::Bucket bucket) const;
std::optional<RateLimitRetry> rate_limit_retry() const;
// Runs a read request under the retry policy; attempt() must rebuild
// signed headers. Defined in clob_client_internal.hpp.
template <typename Attempt> HttpResponse read(Attempt &&attempt) const;

// Helper methods
std::map<std::string, std::string> get_l2_headers(const std::string &method,
const std::string &path,
Expand Down
3 changes: 3 additions & 0 deletions include/polymarket/position_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "polymarket/json_rpc_client.hpp"
#include "polymarket/order_signer.hpp"
#include "polymarket/polymarket_contracts.hpp"
#include "polymarket/rate_limit.hpp"
#include "polymarket/trading_approvals.hpp"
#include <array>
#include <chrono>
Expand Down Expand Up @@ -170,6 +171,8 @@ namespace polymarket
std::string relayer_api_key_address; // defaults to the signer address
long relayer_retry_delay_ms = 2000; // between retried submits
int relayer_max_submit_retries = 10;
// Retries for rate-limited Gamma and relayer reads; nullopt disables.
std::optional<RateLimitRetry> rate_limit_retry = RateLimitRetry{};

// RPC, Gamma, relayer and contracts from `environment`, which is
// validated first. The caller still sets private_key and the wallet.
Expand Down
41 changes: 41 additions & 0 deletions include/polymarket/rate_limit.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <chrono>
#include <cstdint>
#include <optional>
#include <string>

namespace polymarket
{
// Per-signer rate-limit state from the Poly-RateLimit-* response headers.
// Each field is set only when its header is present and well formed.
struct RateLimitUpdate
{
enum class Bucket : std::uint8_t
{
None,
Order,
Cancel
};

Bucket bucket{Bucket::None};
// Tokens left in the bucket; may be negative. Zero alone does not mean
// the bucket is exhausted, so do not back off on it.
std::optional<long long> remaining;
std::optional<long long> reset; // Unix seconds when the wait period ends
std::optional<std::string> tier;
// True when the limiter runs in warning mode and would have rejected
// this request under enforcement.
bool warning{false};
};

// Retries for HTTP 429 on read requests. Each retry waits exactly the
// server's Retry-After delay (1 s when absent); a longer requested delay
// than max_delay returns the error instead.
struct RateLimitRetry
{
int retries{2}; // after the first attempt
std::chrono::milliseconds max_delay{5000};
};

} // namespace polymarket
4 changes: 4 additions & 0 deletions include/polymarket/sdk_error.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "polymarket/http_client.hpp"
#include "polymarket/rate_limit.hpp"
#include <optional>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -29,6 +30,9 @@ namespace polymarket
std::string response_body_excerpt;
std::string request_id;
bool retryable{false};
// Server-suggested wait from Retry-After or a retry_after_seconds body field.
std::optional<double> retry_after_seconds;
std::optional<RateLimitUpdate> rate_limit;
};

template <typename T>
Expand Down
3 changes: 3 additions & 0 deletions include/polymarket/types.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "polymarket/rate_limit.hpp"
#include <string>
#include <vector>
#include <cstdint>
Expand Down Expand Up @@ -274,6 +275,8 @@ namespace polymarket
uint64_t max_book_age_ms = 2000;
int http_timeout_ms = 5000;
int max_markets = 50;
// MarketFetcher retries rate-limited reads with this policy; nullopt disables.
std::optional<RateLimitRetry> rate_limit_retry = RateLimitRetry{};

// Crypto tickers for 15m/4h/1h markets
std::vector<std::string> crypto_tickers = {
Expand Down
25 changes: 13 additions & 12 deletions src/clob_account.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ namespace polymarket
{
std::vector<std::string> result;

auto headers = get_l2_headers("GET", "/auth/api-keys", "");
auto response = http_.get("/auth/api-keys", headers);
auto response = read(
[&]
{ return http_.get("/auth/api-keys", get_l2_headers("GET", "/auth/api-keys", "")); });

if (!response.ok())
return result;
Expand Down Expand Up @@ -106,8 +107,8 @@ namespace polymarket
return Result<std::optional<OpenOrder>>::failure(make_auth_error("Client not authenticated", endpoint));
}

auto headers = get_l2_headers("GET", endpoint, "");
auto response = http_.get(endpoint, headers);
auto response =
read([&] { return http_.get(endpoint, get_l2_headers("GET", endpoint, "")); });

if (!response.ok())
{
Expand Down Expand Up @@ -158,8 +159,8 @@ namespace polymarket
path += market.empty() ? "?" : "?market=" + percent_encode_query_value(market) + "&";
path += "next_cursor=" + percent_encode_query_value(pagination.cursor());

auto headers = get_l2_headers("GET", endpoint, "");
auto response = http_.get(path, headers);
auto response =
read([&] { return http_.get(path, get_l2_headers("GET", endpoint, "")); });

if (!response.ok())
{
Expand Down Expand Up @@ -201,8 +202,8 @@ namespace polymarket

const std::string path = std::string(endpoint) +
"?next_cursor=" + percent_encode_query_value(pagination.cursor());
auto headers = get_l2_headers("GET", endpoint, "");
auto response = http_.get(path, headers);
auto response =
read([&] { return http_.get(path, get_l2_headers("GET", endpoint, "")); });

if (!response.ok())
return {};
Expand Down Expand Up @@ -230,8 +231,8 @@ namespace polymarket
"&signature_type=" + std::to_string(static_cast<int>(sig_type_));
if (!token_id.empty())
path += "&token_id=" + percent_encode_query_value(token_id);
auto headers = get_l2_headers("GET", request_path, "");
auto response = http_.get(path, headers);
auto response =
read([&] { return http_.get(path, get_l2_headers("GET", request_path, "")); });

if (!response.ok())
return std::nullopt;
Expand Down Expand Up @@ -269,8 +270,8 @@ namespace polymarket
"&signature_type=" + std::to_string(static_cast<int>(sig_type_));
if (!token_id.empty())
path += "&token_id=" + percent_encode_query_value(token_id);
auto headers = get_l2_headers("GET", request_path, "");
auto response = http_.get(path, headers);
auto response =
read([&] { return http_.get(path, get_l2_headers("GET", request_path, "")); });
return response.ok();
}
}
36 changes: 23 additions & 13 deletions src/clob_activity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ namespace polymarket
std::optional<OrderScoringResult> ClobClient::is_order_scoring(const std::string &order_id)
{
constexpr const char *endpoint = "/order-scoring";
auto headers = get_l2_headers("GET", endpoint, "");
auto response = http_.get(std::string(endpoint) +
"?order_id=" + percent_encode_query_value(order_id),
headers);
auto response = read(
[&]
{
return http_.get(std::string(endpoint) +
"?order_id=" + percent_encode_query_value(order_id),
get_l2_headers("GET", endpoint, ""));
});

if (!response.ok())
return std::nullopt;
Expand Down Expand Up @@ -52,8 +55,12 @@ namespace polymarket
return results;
const json body = order_ids;
std::string body_str = body.dump();
auto headers = get_l2_headers("POST", "/orders-scoring", body_str);
auto response = http_.post("/orders-scoring", body_str, headers);
auto response = read(
[&]
{
return http_.post("/orders-scoring", body_str,
get_l2_headers("POST", "/orders-scoring", body_str));
});

if (!response.ok())
return results;
Expand Down Expand Up @@ -92,10 +99,14 @@ namespace polymarket
{
std::vector<Notification> result;

auto headers = get_l2_headers("GET", "/notifications", "");
auto response = http_.get("/notifications?signature_type=" +
percent_encode_query_value(std::to_string(static_cast<int>(sig_type_))),
headers);
auto response = read(
[&]
{
return http_.get(
"/notifications?signature_type=" +
percent_encode_query_value(std::to_string(static_cast<int>(sig_type_))),
get_l2_headers("GET", "/notifications", ""));
});

if (!response.ok())
return result;
Expand Down Expand Up @@ -161,10 +172,9 @@ namespace polymarket
std::vector<ClobClient::EarningsInfo> ClobClient::get_total_earnings_for_user_for_day_all(const std::string &date)
{
constexpr const char *endpoint = "/rewards/user/total";
auto headers = get_l2_headers("GET", endpoint, "");
const std::string path = std::string(endpoint) + "?date=" + percent_encode_query_value(date) +
"&signature_type=" + std::to_string(static_cast<int>(sig_type_));
auto response = http_.get(path, headers);
auto response = read([&] { return http_.get(path, get_l2_headers("GET", endpoint, "")); });
if (!response.ok())
return {};
std::vector<EarningsInfo> result;
Expand All @@ -191,7 +201,7 @@ namespace polymarket
if (token_id.empty())
return std::nullopt;
const std::string path = "/fee-rate?token_id=" + percent_encode_query_value(token_id);
auto response = http_.get(path);
auto response = read([&] { return http_.get(path); });

if (!response.ok())
return std::nullopt;
Expand Down
14 changes: 9 additions & 5 deletions src/clob_activity_pagination.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ namespace polymarket
return {};
}

auto response = http_.get("/rewards/markets/current?next_cursor=" +
percent_encode_query_value(pagination.cursor()));
auto response = read(
[&]
{
return http_.get("/rewards/markets/current?next_cursor=" +
percent_encode_query_value(pagination.cursor()));
});
if (!response.ok())
return {};
try
Expand Down Expand Up @@ -77,7 +81,7 @@ namespace polymarket
const std::string path = "/rewards/markets/" + condition_id +
"?next_cursor=" +
percent_encode_query_value(pagination.cursor());
auto response = http_.get(path);
auto response = read([&] { return http_.get(path); });
if (!response.ok())
return {};
try
Expand Down Expand Up @@ -133,8 +137,8 @@ namespace polymarket
"?date=" + percent_encode_query_value(date) +
"&signature_type=" + std::to_string(static_cast<int>(sig_type_)) +
"&next_cursor=" + percent_encode_query_value(pagination.cursor());
auto headers = get_l2_headers("GET", endpoint, "");
auto response = http_.get(path, headers);
auto response =
read([&] { return http_.get(path, get_l2_headers("GET", endpoint, "")); });
if (!response.ok())
return {};
try
Expand Down
Loading
Loading