Skip to main content

polymarket_sdk/
client.rs

1//! `PolymarketClient` — top-level entry point with **type-level authentication guards**.
2//!
3//! The client uses Rust generics to enforce authentication at compile time:
4//!
5//! ```rust,no_run
6//! use polymarket_sdk::PolymarketClient;
7//!
8//! # async fn example() -> Result<(), polymarket_sdk::PolymarketError> {
9//! // Public client — can only call market data endpoints
10//! let public = PolymarketClient::new_public(None)?;
11//! let book = public.get_order_book("12345").await?;
12//!
13//! // Authenticated client — can also call trading endpoints
14//! use polymarket_sdk::ApiCredentials;
15//! let creds = ApiCredentials {
16//!     api_key: "key".into(),
17//!     secret: "secret".into(),
18//!     passphrase: "pass".into(),
19//! };
20//! let authed = public.authenticate(creds);
21//! let orders = authed.get_orders().await?;
22//! # Ok(())
23//! # }
24//! ```
25
26use alloy::signers::Signer;
27use std::collections::HashMap;
28use std::marker::PhantomData;
29
30use crate::auth::{l1, l2};
31use crate::constants::{CLOB_API_URL, GAMMA_API_URL, POLYGON_CHAIN_ID};
32use crate::error::{PolymarketError, Result};
33use crate::http::HttpClient;
34use crate::signing::order_builder;
35use crate::types::*;
36
37// ---------------------------------------------------------------------------
38// Auth state markers (zero-sized types)
39// ---------------------------------------------------------------------------
40
41/// Marker: client has no credentials (public-only).
42#[derive(Debug, Clone, Copy)]
43pub struct Public;
44
45/// Marker: client has L2 credentials (can trade).
46#[derive(Debug, Clone, Copy)]
47pub struct Authenticated;
48
49// ---------------------------------------------------------------------------
50// Core client struct
51// ---------------------------------------------------------------------------
52
53/// Main client for interacting with the Polymarket CLOB API.
54///
55/// The type parameter `A` tracks the authentication state at compile time:
56/// - `PolymarketClient<Public>` — can only call public endpoints
57/// - `PolymarketClient<Authenticated>` — can call all endpoints including trading
58#[derive(Debug)]
59pub struct PolymarketClient<A = Public> {
60    /// CLOB API HTTP client.
61    clob: HttpClient,
62    /// Gamma API HTTP client.
63    gamma: HttpClient,
64    /// Chain ID (137 for Polygon mainnet, 80002 for Amoy).
65    chain_id: u64,
66    /// L2 API credentials (only present in Authenticated state).
67    creds: Option<ApiCredentials>,
68    /// Funder address (for proxy wallets).
69    funder: Option<alloy::primitives::Address>,
70    /// Signature type for order signing.
71    signature_type: SignatureType,
72    /// Phantom data to hold the auth state type.
73    _auth: PhantomData<A>,
74}
75
76// ---------------------------------------------------------------------------
77// Constructors (available on any state)
78// ---------------------------------------------------------------------------
79
80impl PolymarketClient<Public> {
81    /// Create a public-only client (no authentication, market data only).
82    ///
83    /// Pass `None` for the host to use the default mainnet URL.
84    pub fn new_public(host: Option<&str>) -> Result<Self> {
85        let base = host.unwrap_or(CLOB_API_URL);
86        Ok(Self {
87            clob: HttpClient::new(base)?,
88            gamma: HttpClient::new(GAMMA_API_URL)?,
89            chain_id: POLYGON_CHAIN_ID,
90            creds: None,
91            funder: None,
92            signature_type: SignatureType::Eoa,
93            _auth: PhantomData,
94        })
95    }
96
97    /// Upgrade this client to an authenticated client by providing L2 credentials.
98    ///
99    /// This is a **zero-cost** state transition — it just adds credentials.
100    pub fn authenticate(self, creds: ApiCredentials) -> PolymarketClient<Authenticated> {
101        PolymarketClient {
102            clob: self.clob,
103            gamma: self.gamma,
104            chain_id: self.chain_id,
105            creds: Some(creds),
106            funder: self.funder,
107            signature_type: self.signature_type,
108            _auth: PhantomData,
109        }
110    }
111}
112
113impl PolymarketClient<Authenticated> {
114    /// Create a client with L2 credentials for trading.
115    pub fn with_l2(
116        host: Option<&str>,
117        chain_id: u64,
118        creds: ApiCredentials,
119    ) -> Result<Self> {
120        let base = host.unwrap_or(CLOB_API_URL);
121        Ok(Self {
122            clob: HttpClient::new(base)?,
123            gamma: HttpClient::new(GAMMA_API_URL)?,
124            chain_id,
125            creds: Some(creds),
126            funder: None,
127            signature_type: SignatureType::Eoa,
128            _auth: PhantomData,
129        })
130    }
131}
132
133/// Builder for configuring a `PolymarketClient`.
134#[derive(Debug)]
135pub struct PolymarketClientBuilder {
136    host: Option<String>,
137    gamma_host: Option<String>,
138    chain_id: u64,
139    creds: Option<ApiCredentials>,
140    funder: Option<alloy::primitives::Address>,
141    signature_type: SignatureType,
142}
143
144impl Default for PolymarketClientBuilder {
145    fn default() -> Self {
146        Self {
147            host: None,
148            gamma_host: None,
149            chain_id: POLYGON_CHAIN_ID,
150            creds: None,
151            funder: None,
152            signature_type: SignatureType::Eoa,
153        }
154    }
155}
156
157impl PolymarketClientBuilder {
158    /// Set the CLOB API host URL.
159    pub fn host(mut self, host: impl Into<String>) -> Self {
160        self.host = Some(host.into());
161        self
162    }
163
164    /// Set the Gamma API host URL.
165    pub fn gamma_host(mut self, host: impl Into<String>) -> Self {
166        self.gamma_host = Some(host.into());
167        self
168    }
169
170    /// Set the chain ID.
171    pub fn chain_id(mut self, chain_id: u64) -> Self {
172        self.chain_id = chain_id;
173        self
174    }
175
176    /// Set the L2 API credentials.
177    pub fn credentials(mut self, creds: ApiCredentials) -> Self {
178        self.creds = Some(creds);
179        self
180    }
181
182    /// Set the funder address (for proxy wallets).
183    pub fn funder(mut self, funder: alloy::primitives::Address) -> Self {
184        self.funder = Some(funder);
185        self
186    }
187
188    /// Set the signature type.
189    pub fn signature_type(mut self, sig_type: SignatureType) -> Self {
190        self.signature_type = sig_type;
191        self
192    }
193
194    /// Build a public-only client.
195    pub fn build_public(self) -> Result<PolymarketClient<Public>> {
196        let clob_url = self.host.as_deref().unwrap_or(CLOB_API_URL);
197        let gamma_url = self.gamma_host.as_deref().unwrap_or(GAMMA_API_URL);
198
199        Ok(PolymarketClient {
200            clob: HttpClient::new(clob_url)?,
201            gamma: HttpClient::new(gamma_url)?,
202            chain_id: self.chain_id,
203            creds: None,
204            funder: self.funder,
205            signature_type: self.signature_type,
206            _auth: PhantomData,
207        })
208    }
209
210    /// Build an authenticated client. Requires credentials to be set.
211    pub fn build(self) -> Result<PolymarketClient<Authenticated>> {
212        let creds = self.creds.ok_or_else(|| {
213            PolymarketError::Auth("Credentials required when building authenticated client. Use build_public() for public-only.".into())
214        })?;
215        let clob_url = self.host.as_deref().unwrap_or(CLOB_API_URL);
216        let gamma_url = self.gamma_host.as_deref().unwrap_or(GAMMA_API_URL);
217
218        Ok(PolymarketClient {
219            clob: HttpClient::new(clob_url)?,
220            gamma: HttpClient::new(gamma_url)?,
221            chain_id: self.chain_id,
222            creds: Some(creds),
223            funder: self.funder,
224            signature_type: self.signature_type,
225            _auth: PhantomData,
226        })
227    }
228}
229
230// ---------------------------------------------------------------------------
231// Public Endpoints — available on ALL client types (Public + Authenticated)
232// ---------------------------------------------------------------------------
233
234impl<A> PolymarketClient<A> {
235    /// Builder-style: create a fully configured client.
236    pub fn builder() -> PolymarketClientBuilder {
237        PolymarketClientBuilder::default()
238    }
239
240    /// Get the configured chain ID.
241    pub fn chain_id(&self) -> u64 {
242        self.chain_id
243    }
244
245    /// Check if L2 credentials are configured.
246    pub fn has_credentials(&self) -> bool {
247        self.creds.is_some()
248    }
249
250    /// Set the funder address (for proxy wallets).
251    pub fn set_funder(&mut self, funder: alloy::primitives::Address) {
252        self.funder = Some(funder);
253    }
254
255    /// Set the signature type.
256    pub fn set_signature_type(&mut self, sig_type: SignatureType) {
257        self.signature_type = sig_type;
258    }
259
260    // -----------------------------------------------------------------------
261    // CLOB public endpoints (L0)
262    // -----------------------------------------------------------------------
263
264    /// Get simplified markets.
265    #[cfg(feature = "clob")]
266    pub async fn get_sampling_simplified_markets(
267        &self,
268        next_cursor: Option<&str>,
269    ) -> Result<Vec<SimplifiedMarket>> {
270        let mut query = vec![];
271        if let Some(cursor) = next_cursor {
272            query.push(("next_cursor", cursor));
273        }
274        let q = if query.is_empty() { None } else { Some(query.as_slice()) };
275        self.clob.get("/sampling-simplified-markets", q, None).await
276    }
277
278    /// Get simplified markets (paginated).
279    #[cfg(feature = "clob")]
280    pub async fn get_simplified_markets(
281        &self,
282        next_cursor: Option<&str>,
283    ) -> Result<Vec<SimplifiedMarket>> {
284        let mut query = vec![];
285        if let Some(cursor) = next_cursor {
286            query.push(("next_cursor", cursor));
287        }
288        let q = if query.is_empty() { None } else { Some(query.as_slice()) };
289        self.clob.get("/simplified-markets", q, None).await
290    }
291
292    /// Get markets from the CLOB API.
293    #[cfg(feature = "clob")]
294    pub async fn get_markets(
295        &self,
296        next_cursor: Option<&str>,
297    ) -> Result<Vec<SimplifiedMarket>> {
298        let mut query = vec![];
299        if let Some(cursor) = next_cursor {
300            query.push(("next_cursor", cursor));
301        }
302        let q = if query.is_empty() { None } else { Some(query.as_slice()) };
303        self.clob.get("/markets", q, None).await
304    }
305
306    /// Get a single market by condition ID.
307    #[cfg(feature = "clob")]
308    pub async fn get_market(&self, condition_id: &str) -> Result<SimplifiedMarket> {
309        let path = format!("/markets/{}", condition_id);
310        self.clob.get(&path, None, None).await
311    }
312
313    /// Get the order book for a token.
314    #[cfg(feature = "clob")]
315    pub async fn get_order_book(&self, token_id: &str) -> Result<OrderBook> {
316        let query = [("token_id", token_id)];
317        self.clob.get("/book", Some(&query), None).await
318    }
319
320    /// Get the midpoint price for a token.
321    #[cfg(feature = "clob")]
322    pub async fn get_midpoint(&self, token_id: &str) -> Result<PriceResponse> {
323        let query = [("token_id", token_id)];
324        self.clob.get("/midpoint", Some(&query), None).await
325    }
326
327    /// Get the current price for a token.
328    #[cfg(feature = "clob")]
329    pub async fn get_price(&self, token_id: &str) -> Result<PriceResponse> {
330        let query = [("token_id", token_id)];
331        self.clob.get("/price", Some(&query), None).await
332    }
333
334    /// Get the spread for a token.
335    #[cfg(feature = "clob")]
336    pub async fn get_spread(&self, token_id: &str) -> Result<SpreadResponse> {
337        let query = [("token_id", token_id)];
338        self.clob.get("/spread", Some(&query), None).await
339    }
340
341    /// Get the last trade price for a token.
342    #[cfg(feature = "clob")]
343    pub async fn get_last_trade_price(&self, token_id: &str) -> Result<LastTradePriceResponse> {
344        let query = [("token_id", token_id)];
345        self.clob.get("/last-trade-price", Some(&query), None).await
346    }
347
348    /// Get price history for a token.
349    #[cfg(feature = "clob")]
350    pub async fn get_prices_history(
351        &self,
352        token_id: &str,
353        start_ts: Option<i64>,
354        end_ts: Option<i64>,
355        fidelity: Option<u32>,
356    ) -> Result<Vec<PriceHistoryEntry>> {
357        let mut query: Vec<(&str, String)> = vec![("token_id", token_id.to_string())];
358        if let Some(s) = start_ts {
359            query.push(("startTs", s.to_string()));
360        }
361        if let Some(e) = end_ts {
362            query.push(("endTs", e.to_string()));
363        }
364        if let Some(f) = fidelity {
365            query.push(("fidelity", f.to_string()));
366        }
367        let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
368        self.clob.get("/prices-history", Some(&pairs), None).await
369    }
370
371    /// Get tick size for a token.
372    #[cfg(feature = "clob")]
373    pub async fn get_tick_size(&self, token_id: &str) -> Result<TickSizeInfo> {
374        let query = [("token_id", token_id)];
375        self.clob.get("/tick-size", Some(&query), None).await
376    }
377
378    // -----------------------------------------------------------------------
379    // Gamma API (Market Discovery)
380    // -----------------------------------------------------------------------
381
382    /// Get markets from the Gamma API.
383    #[cfg(feature = "gamma")]
384    pub async fn get_gamma_markets(
385        &self,
386        limit: Option<u32>,
387        offset: Option<u32>,
388    ) -> Result<Vec<GammaMarket>> {
389        let mut query: Vec<(&str, String)> = vec![];
390        if let Some(l) = limit {
391            query.push(("limit", l.to_string()));
392        }
393        if let Some(o) = offset {
394            query.push(("offset", o.to_string()));
395        }
396        let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
397        let q = if pairs.is_empty() { None } else { Some(pairs.as_slice()) };
398        self.gamma.get("/markets", q, None).await
399    }
400
401    /// Search Gamma markets by slug.
402    #[cfg(feature = "gamma")]
403    pub async fn get_gamma_market_by_slug(&self, slug: &str) -> Result<Vec<GammaMarket>> {
404        let query = [("slug", slug)];
405        self.gamma.get("/markets", Some(&query), None).await
406    }
407
408    /// Get events from the Gamma API.
409    #[cfg(feature = "gamma")]
410    pub async fn get_events(
411        &self,
412        limit: Option<u32>,
413        offset: Option<u32>,
414    ) -> Result<Vec<GammaEvent>> {
415        let mut query: Vec<(&str, String)> = vec![];
416        if let Some(l) = limit {
417            query.push(("limit", l.to_string()));
418        }
419        if let Some(o) = offset {
420            query.push(("offset", o.to_string()));
421        }
422        let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
423        let q = if pairs.is_empty() { None } else { Some(pairs.as_slice()) };
424        self.gamma.get("/events", q, None).await
425    }
426
427    /// Get a single event by ID.
428    #[cfg(feature = "gamma")]
429    pub async fn get_event(&self, event_id: &str) -> Result<GammaEvent> {
430        let path = format!("/events/{}", event_id);
431        self.gamma.get(&path, None, None).await
432    }
433
434    /// Search markets by text query.
435    #[cfg(feature = "gamma")]
436    pub async fn search_markets(&self, query: &str) -> Result<Vec<GammaMarket>> {
437        let q = [("_q", query)];
438        self.gamma.get("/markets", Some(&q), None).await
439    }
440
441    // -----------------------------------------------------------------------
442    // L1 Endpoints (API Key Management) — public, but needs a signer
443    // -----------------------------------------------------------------------
444
445    /// Create or derive API keys using L1 authentication.
446    pub async fn create_or_derive_api_key<S: Signer + Send + Sync>(
447        &self,
448        signer: &S,
449        nonce: Option<u64>,
450    ) -> Result<ApiKeyResponse> {
451        let headers = l1::create_l1_headers(signer, self.chain_id, nonce).await?;
452        self.clob.get("/auth/api-key", None, Some(&headers)).await
453    }
454
455    /// Derive an existing API key.
456    pub async fn derive_api_key<S: Signer + Send + Sync>(
457        &self,
458        signer: &S,
459    ) -> Result<ApiKeyResponse> {
460        let headers = l1::create_l1_headers(signer, self.chain_id, None).await?;
461        self.clob.get("/auth/derive-api-key", None, Some(&headers)).await
462    }
463
464    /// Delete an API key.
465    pub async fn delete_api_key<S: Signer + Send + Sync>(
466        &self,
467        signer: &S,
468    ) -> Result<serde_json::Value> {
469        let headers = l1::create_l1_headers(signer, self.chain_id, None).await?;
470        self.clob.delete("/auth/api-key", None, Some(&headers)).await
471    }
472}
473
474// ---------------------------------------------------------------------------
475// Authenticated-only endpoints (L2 trading)
476// ---------------------------------------------------------------------------
477
478impl PolymarketClient<Authenticated> {
479    /// Get L2 auth headers. Panics are impossible here because `Authenticated`
480    /// state guarantees credentials exist.
481    fn l2_headers(&self, method: &str, path: &str, body: &str) -> Result<HashMap<String, String>> {
482        let creds = self.creds.as_ref().expect("Authenticated client must have creds");
483        l2::create_l2_headers(creds, method, path, body)
484    }
485
486    // -----------------------------------------------------------------------
487    // Order management
488    // -----------------------------------------------------------------------
489
490    /// Get open orders for the authenticated user.
491    pub async fn get_orders(&self) -> Result<Vec<OpenOrder>> {
492        let headers = self.l2_headers("GET", "/orders", "")?;
493        self.clob.get("/orders", None, Some(&headers)).await
494    }
495
496    /// Get a single order by ID.
497    pub async fn get_order(&self, order_id: &str) -> Result<OpenOrder> {
498        let path = format!("/orders/{}", order_id);
499        let headers = self.l2_headers("GET", &path, "")?;
500        self.clob.get(&path, None, Some(&headers)).await
501    }
502
503    /// Post a pre-signed order to the API.
504    pub async fn post_order(&self, signed_order: &SignedOrder) -> Result<OrderResponse> {
505        let body = serde_json::to_value(signed_order)?;
506        let body_str = serde_json::to_string(signed_order)?;
507        let headers = self.l2_headers("POST", "/order", &body_str)?;
508        self.clob.post("/order", &body, Some(&headers)).await
509    }
510
511    /// Build, sign, and post an order in one call.
512    ///
513    /// This is the most convenient way to place an order.
514    pub async fn create_and_post_order<S: Signer + Send + Sync>(
515        &self,
516        signer: &S,
517        params: &TradeParams,
518    ) -> Result<OrderResponse> {
519        let signed = order_builder::build_and_sign_order(
520            signer,
521            params,
522            self.chain_id,
523            self.signature_type,
524            self.funder,
525        )
526        .await?;
527
528        self.post_order(&signed).await
529    }
530
531    /// Cancel an order by ID.
532    pub async fn cancel_order(&self, order_id: &str) -> Result<CancelResponse> {
533        let body = serde_json::json!({ "orderID": order_id });
534        let body_str = serde_json::to_string(&body)?;
535        let headers = self.l2_headers("DELETE", "/order", &body_str)?;
536        self.clob.delete("/order", Some(&body), Some(&headers)).await
537    }
538
539    /// Cancel multiple orders.
540    pub async fn cancel_orders(&self, order_ids: &[&str]) -> Result<CancelResponse> {
541        let body = serde_json::json!({ "orderIDs": order_ids });
542        let body_str = serde_json::to_string(&body)?;
543        let headers = self.l2_headers("DELETE", "/orders", &body_str)?;
544        self.clob.delete("/orders", Some(&body), Some(&headers)).await
545    }
546
547    /// Cancel all open orders.
548    pub async fn cancel_all_orders(&self) -> Result<CancelResponse> {
549        let headers = self.l2_headers("DELETE", "/cancel-all", "")?;
550        self.clob.delete("/cancel-all", None, Some(&headers)).await
551    }
552
553    // -----------------------------------------------------------------------
554    // Data API (user-specific data)
555    // -----------------------------------------------------------------------
556
557    /// Get trade history for the authenticated user.
558    #[cfg(feature = "data")]
559    pub async fn get_trades(&self) -> Result<Vec<TradeRecord>> {
560        let headers = self.l2_headers("GET", "/trades", "")?;
561        self.clob.get("/trades", None, Some(&headers)).await
562    }
563
564    /// Get balance allowances for the authenticated user.
565    #[cfg(feature = "data")]
566    pub async fn get_balance_allowances(
567        &self,
568        asset_type: Option<&str>,
569    ) -> Result<serde_json::Value> {
570        let mut query = vec![];
571        if let Some(at) = asset_type {
572            query.push(("asset_type", at));
573        }
574        let q = if query.is_empty() { None } else { Some(query.as_slice()) };
575        let headers = self.l2_headers("GET", "/balance-allowance", "")?;
576        self.clob.get("/balance-allowance", q, Some(&headers)).await
577    }
578
579    /// Get current market positions for the authenticated user.
580    #[cfg(feature = "data")]
581    pub async fn get_positions(&self) -> Result<Vec<Position>> {
582        let headers = self.l2_headers("GET", "/positions", "")?;
583        self.clob.get("/positions", None, Some(&headers)).await
584    }
585
586    /// Get notifications for the authenticated user.
587    #[cfg(feature = "data")]
588    pub async fn get_notifications(&self) -> Result<serde_json::Value> {
589        let headers = self.l2_headers("GET", "/notifications", "")?;
590        self.clob.get("/notifications", None, Some(&headers)).await
591    }
592}