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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ set(POLYMARKET_CLIENT_SOURCES
src/order_execution.cpp
src/polymarket_events.cpp
src/polymarket_contracts.cpp
src/environment.cpp
src/position_calls.cpp
src/approval_calls.cpp
src/trading_approvals.cpp
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ std::cout << "avg latency: " << client.get_connection_stats().avg_latency_ms <<
Order helpers round prices, sizes, and maker/taker amounts to the market's tick
size. Leave `tick_size` empty to resolve it from the client's metadata cache.

### Select an environment

`Environment` holds every endpoint and contract for one deployment. Pass it to
`ClobClient`, or build stream and position configs from it:

```cpp
#include <polymarket/environment.hpp>

const auto env = polymarket::Environment::preproduction(); // or production()
polymarket::ClobClient client(env, private_key, creds);
polymarket::UserStream stream(polymarket::Config::for_environment(env), creds);
auto position_config = polymarket::PositionClientConfig::for_environment(env);
```

`preproduction()` uses separate CLOB, Gamma, Data API, and relayer hosts, but
it is not a testnet. It runs on Polygon mainnet with the production contracts,
RPC, and CLOB WebSocket hosts, so on-chain operations and settled orders use
real funds. API keys are per environment. Copy a preset and override fields to
target a fork or a local server. The string constructors
(`ClobClient(base_url, 137, ...)`) override only the production CLOB host and
accept only chain 137.

### Route traffic through a proxy or VPN interface

```cpp
Expand Down Expand Up @@ -268,6 +290,9 @@ Build with `-DPOLYMARKET_CLIENT_BUILD_EXAMPLES=ON` and run from `build/`.
| `feed_latency_benchmark` | Compares receive timing across the Polymarket market WS and a Polygon RPC WS |
| `polymarket_arb` | Analysis-only scan of complementary YES/NO books (`--15m --symbol btc --fetch-only`) |

Examples that call Polymarket services, and `order_test`, read
`POLYMARKET_ENV` (`production` by default, or `preproduction`).

`polymarket_arb` rejects `--live`: the CLOB batch endpoint processes orders
independently, so two complementary FOK orders are not an atomic trade.

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

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

add_executable(test_position_client tests/test_position_client.cpp)
target_include_directories(test_position_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_position_client PRIVATE polymarket::client)
Expand Down Expand Up @@ -92,6 +96,10 @@ if(POLYMARKET_CLIENT_BUILD_TESTS)
target_link_libraries(test_json_rpc_transactions PRIVATE polymarket::client)
add_test(NAME test_json_rpc_transactions COMMAND test_json_rpc_transactions)

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

add_executable(test_json_rpc_ws_owner_reset tests/test_json_rpc_ws_owner_reset.cpp)
target_include_directories(test_json_rpc_ws_owner_reset PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/tests)
Expand Down Expand Up @@ -168,6 +176,11 @@ if(POLYMARKET_CLIENT_BUILD_TESTS)
add_test(NAME test_oracle_watcher_historical COMMAND test_oracle_watcher_historical)
set_tests_properties(test_oracle_watcher_historical PROPERTIES LABELS live)

add_executable(test_preproduction_live tests/test_preproduction_live.cpp)
target_link_libraries(test_preproduction_live PRIVATE polymarket::client)
add_test(NAME test_preproduction_live COMMAND test_preproduction_live)
set_tests_properties(test_preproduction_live PROPERTIES LABELS live)

add_executable(test_arb_sizing tests/test_arb_sizing.cpp)
target_include_directories(test_arb_sizing PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_arb_sizing PRIVATE polymarket::client)
Expand Down
6 changes: 4 additions & 2 deletions examples/approvals_example.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/position_client.hpp"

#include <cstdlib>
Expand All @@ -17,7 +18,8 @@
// POLYMARKET_PROXY_ADDRESS set, the approvals are set on that Gnosis Safe
// through the relayer in one transaction (gasless; needs RELAYER_API_KEY,
// optional RELAYER_API_KEY_ADDRESS). Otherwise the EOA sends one transaction
// per missing approval and pays gas in POL.
// per missing approval and pays gas in POL. POLYMARKET_ENV selects the
// relayer and contract preset (default production).

namespace
{
Expand All @@ -43,7 +45,7 @@ int main(int argc, char **argv)
execute = true;
}

PositionClientConfig config;
auto config = PositionClientConfig::for_environment(polymarket_example::selected_environment());
config.private_key = env("PRIVATE_KEY");
config.rpc_url = env("POLYGON_RPC_ENDPOINT");
if (config.private_key.empty() || config.rpc_url.empty())
Expand Down
14 changes: 14 additions & 0 deletions examples/example_environment.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "polymarket/environment.hpp"
#include <cstdlib>

namespace polymarket_example
{
// POLYMARKET_ENV: "production" (default) or "preproduction".
inline polymarket::Environment selected_environment()
{
const char *name = std::getenv("POLYMARKET_ENV");
return polymarket::Environment::from_name(name && *name ? name : "production");
}
} // namespace polymarket_example
6 changes: 4 additions & 2 deletions examples/position_example.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/position_client.hpp"

#include <cstdlib>
Expand All @@ -17,7 +18,8 @@
// Environment: PRIVATE_KEY and POLYGON_RPC_ENDPOINT are required. With
// POLYMARKET_PROXY_ADDRESS set, operations run from that Gnosis Safe through
// the relayer (gasless; needs RELAYER_API_KEY, optional RELAYER_API_KEY_ADDRESS).
// Otherwise they are sent from the EOA, which pays gas in POL.
// Otherwise they are sent from the EOA, which pays gas in POL. POLYMARKET_ENV
// selects the Gamma, relayer and contract preset (default production).

namespace
{
Expand Down Expand Up @@ -54,7 +56,7 @@ int main(int argc, char **argv)
if ((action != "split" && action != "merge" && action != "redeem") || (action == "split" && amount.empty()))
return usage();

PositionClientConfig config;
auto config = PositionClientConfig::for_environment(polymarket_example::selected_environment());
config.private_key = env("PRIVATE_KEY");
config.rpc_url = env("POLYGON_RPC_ENDPOINT");
if (config.private_key.empty() || config.rpc_url.empty())
Expand Down
13 changes: 5 additions & 8 deletions examples/rest_example.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/clob_client.hpp"
#include <iostream>
#include <cstdlib>
Expand All @@ -16,14 +17,16 @@ int main()
try
{
http_global_init();
const auto environment = polymarket_example::selected_environment();
std::cout << "Environment: " << environment.name << "\n";

// ============================================================
// PUBLIC ENDPOINTS (No authentication required)
// ============================================================

std::cout << "=== Public Endpoints ===\n\n";

ClobClient public_client{"https://clob.polymarket.com", 137};
ClobClient public_client{environment};

// Get markets
auto market_page = public_client.get_markets();
Expand Down Expand Up @@ -117,13 +120,7 @@ int main()
// Determine signature type based on funder address
SignatureType sig_type = funder_address.empty() ? SignatureType::EOA : SignatureType::POLY_PROXY;

ClobClient client{
"https://clob.polymarket.com",
137,
private_key,
creds,
sig_type,
funder_address};
ClobClient client{environment, private_key, creds, sig_type, funder_address};

// Get balance
auto balance = client.get_balance_allowance("COLLATERAL");
Expand Down
3 changes: 2 additions & 1 deletion examples/test_positions.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/clob_client.hpp"
#include <iostream>
#include <cstdlib>
Expand Down Expand Up @@ -60,7 +61,7 @@ int main()
std::cout << "Testing get_positions for address: " << funder_address << "\n\n";

// Create unauthenticated client (positions API doesn't need auth)
ClobClient client{"https://clob.polymarket.com", 137};
ClobClient client{polymarket_example::selected_environment()};

// Test get_positions
std::cout << "=== All Positions ===\n\n";
Expand Down
6 changes: 4 additions & 2 deletions examples/user_stream_example.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/clob_client.hpp"
#include "polymarket/user_stream.hpp"

Expand All @@ -24,6 +25,7 @@ int main(int argc, char **argv)

try
{
const auto environment = polymarket_example::selected_environment();
ApiCredentials credentials;
if (has_api_credentials)
{
Expand All @@ -32,11 +34,11 @@ int main(int argc, char **argv)
else
{
// Derive L2 API credentials from the signer (no orders are placed).
ClobClient client("https://clob.polymarket.com", 137, pk_env);
ClobClient client(environment, pk_env);
credentials = client.create_or_derive_api_key();
}

UserStream stream(Config{}, credentials);
UserStream stream(Config::for_environment(environment), credentials);
stream.on_order([](const UserOrderEvent &order)
{ std::cout << "[order] " << order.type << ' ' << order.side << ' '
<< order.size_matched << '/' << order.original_size
Expand Down
3 changes: 2 additions & 1 deletion examples/ws_example.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "example_environment.hpp"
#include "polymarket/websocket_client.hpp"
#include <nlohmann/json.hpp>
#include <iostream>
Expand All @@ -19,7 +20,7 @@ int main()
std::atomic<bool> connected{false};

WebSocketClient ws;
ws.set_url("wss://ws-subscriptions-clob.polymarket.com/ws/market");
ws.set_url(polymarket_example::selected_environment().clob_market_ws_url);
ws.set_ping_interval_ms(10000);
ws.set_auto_reconnect(false);

Expand Down
26 changes: 23 additions & 3 deletions include/polymarket/clob_client.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "polymarket/clob_types.hpp"
#include "polymarket/environment.hpp"
#include "polymarket/geoblock.hpp"
#include "polymarket/types.hpp"
#include "polymarket/http_client.hpp"
Expand Down Expand Up @@ -43,6 +44,19 @@ namespace polymarket
const std::string &funder_address,
const HttpClientOptions &http_options);

// The constructors above change only the production CLOB host (chain 137 only).
// These take every host and contract from a validated `environment`.
explicit ClobClient(const Environment &environment,
const std::optional<HttpClientOptions> &http_options = std::nullopt);
ClobClient(const Environment &environment, const std::string &private_key,
SignatureType sig_type = SignatureType::EOA,
const std::string &funder_address = "",
const std::optional<HttpClientOptions> &http_options = std::nullopt);
ClobClient(const Environment &environment, const std::string &private_key,
const ApiCredentials &creds, SignatureType sig_type = SignatureType::EOA,
const std::string &funder_address = "",
const std::optional<HttpClientOptions> &http_options = std::nullopt);

~ClobClient();

// Disable copy
Expand Down Expand Up @@ -243,7 +257,8 @@ namespace polymarket
HttpClient::ConnectionStats get_connection_stats() const { return http_.get_stats(); }
RequestMetrics get_last_request_metrics() const { return http_.get_last_request_metrics(); }

// Get exchange address for the chain
const Environment &environment() const { return environment_; }

std::string get_exchange_address() const;
std::string get_neg_risk_exchange_address() const;

Expand All @@ -266,15 +281,20 @@ namespace polymarket
private:
HttpClient http_;
HttpClient data_http_;
int chain_id_;
std::string base_url_;
Environment environment_;
std::string funder_address_;
SignatureType sig_type_;

// Order signer (null for public access)
std::unique_ptr<OrderSigner> order_signer_;
std::unique_ptr<ApiCredentials> api_creds_;

// Every public constructor delegates here; null private_key means public access.
ClobClient(Environment environment, const std::string *private_key,
const ApiCredentials *creds, SignatureType sig_type,
const std::string &funder_address,
const std::optional<HttpClientOptions> &http_options);

template <typename Value>
struct MetadataCacheEntry
{
Expand Down
39 changes: 39 additions & 0 deletions include/polymarket/environment.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include "polymarket/polymarket_contracts.hpp"
#include <string>
#include <string_view>

namespace polymarket
{
// Every endpoint and contract for one Polymarket deployment. production()
// and preproduction() match the official py-sdk and ts-sdk presets. Copy a
// preset and override fields to target a fork or a local server.
//
// preproduction() is not a testnet: it uses separate CLOB, Gamma, Data API
// and relayer hosts, but runs on Polygon mainnet (chain 137) with the
// production contracts, RPC and CLOB WebSocket hosts.
struct Environment
{
std::string name;
std::string rpc_url; // Polygon JSON-RPC over HTTP(S)
PolymarketContracts contracts; // contracts.chain_id is the chain ID

std::string clob_url;
std::string clob_market_ws_url;
std::string clob_user_ws_url;
std::string gamma_url;
std::string data_url;
std::string relayer_url;
std::string rtds_ws_url;

static Environment production();
static Environment preproduction();
// "production" or "preproduction"; throws std::invalid_argument otherwise.
static Environment from_name(std::string_view name);

// Throws std::invalid_argument naming the first invalid field.
void validate() const;
};

} // namespace polymarket
6 changes: 6 additions & 0 deletions include/polymarket/position_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

namespace polymarket
{
struct Environment;

// One contract call inside a transaction.
struct ContractCall
{
Expand Down Expand Up @@ -168,6 +170,10 @@ 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;

// RPC, Gamma, relayer and contracts from `environment`, which is
// validated first. The caller still sets private_key and the wallet.
static PositionClientConfig for_environment(const Environment &environment);
};

// Split, merge and redeem binary market positions (CTF and Protocol V2
Expand Down
5 changes: 5 additions & 0 deletions include/polymarket/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace polymarket
{
struct Environment;

// Price level in orderbook
struct PriceLevel
Expand Down Expand Up @@ -278,6 +279,10 @@ namespace polymarket
std::vector<std::string> crypto_tickers = {
"btc", "eth", "xrp", "sol", "doge", "bnb",
"ada", "avax", "matic", "link", "dot", "ltc"};

// CLOB, Gamma and RTDS endpoints from `environment`, which is validated
// first; every other field keeps its default.
static Config for_environment(const Environment &environment);
};

// Monotonic nanoseconds for receipt ages and latency measurements.
Expand Down
Loading
Loading