Skip to main content

nautilus_polymarket/http/
auth.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! L1 API credential creation and derivation for the Polymarket CLOB.
17
18use std::collections::HashMap;
19
20use nautilus_core::time::get_atomic_clock_realtime;
21use nautilus_network::http::{HttpClient, Method};
22use serde::Deserialize;
23
24use crate::{
25    common::{credential::EvmPrivateKey, urls::clob_http_url},
26    http::error::{Error, Result},
27    signing::eip712::sign_clob_auth,
28};
29
30/// API credentials returned by the Polymarket CLOB auth endpoints.
31#[derive(Debug, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct ApiCredentials {
34    pub api_key: String,
35    pub secret: String,
36    pub passphrase: String,
37}
38
39/// Creates new API credentials via `POST /auth/api-key` using L1 authentication.
40///
41/// Fails if credentials already exist for this `(address, nonce)` pair.
42/// Use [`derive_api_key`] to retrieve existing credentials, or
43/// [`create_or_derive_api_key`] for idempotent behavior.
44pub async fn create_api_key(
45    private_key: &EvmPrivateKey,
46    nonce: u64,
47    base_url: Option<&str>,
48) -> Result<ApiCredentials> {
49    let (client, headers, base) = prepare_l1_request(private_key, nonce, base_url)?;
50
51    let url = format!("{base}/auth/api-key");
52    let response = client
53        .request(Method::POST, url, None, Some(headers), None, None, None)
54        .await
55        .map_err(Error::from_http_client)?;
56
57    if response.status.is_success() {
58        serde_json::from_slice(&response.body).map_err(Error::Serde)
59    } else {
60        Err(Error::from_status_code(
61            response.status.as_u16(),
62            &response.body,
63        ))
64    }
65}
66
67/// Derives existing API credentials via `GET /auth/derive-api-key` using L1 authentication.
68///
69/// Fails if no credentials exist for this `(address, nonce)` pair.
70/// Use [`create_api_key`] to create new credentials, or
71/// [`create_or_derive_api_key`] for idempotent behavior.
72pub async fn derive_api_key(
73    private_key: &EvmPrivateKey,
74    nonce: u64,
75    base_url: Option<&str>,
76) -> Result<ApiCredentials> {
77    let (client, headers, base) = prepare_l1_request(private_key, nonce, base_url)?;
78
79    let url = format!("{base}/auth/derive-api-key");
80    let response = client
81        .request(Method::GET, url, None, Some(headers), None, None, None)
82        .await
83        .map_err(Error::from_http_client)?;
84
85    if response.status.is_success() {
86        serde_json::from_slice(&response.body).map_err(Error::Serde)
87    } else {
88        Err(Error::from_status_code(
89            response.status.as_u16(),
90            &response.body,
91        ))
92    }
93}
94
95/// Creates or derives API credentials using L1 (EIP-712) authentication.
96///
97/// First attempts `POST /auth/api-key` (create). On HTTP-level errors
98/// (e.g. nonce already used), falls back to `GET /auth/derive-api-key`
99/// (derive). Transport and network errors are propagated immediately
100/// without attempting the fallback.
101pub async fn create_or_derive_api_key(
102    private_key: &EvmPrivateKey,
103    nonce: u64,
104    base_url: Option<&str>,
105) -> Result<ApiCredentials> {
106    match create_api_key(private_key, nonce, base_url).await {
107        Ok(creds) => Ok(creds),
108        Err(e) if e.is_http_status_error() => derive_api_key(private_key, nonce, base_url).await,
109        Err(e) => Err(e),
110    }
111}
112
113fn prepare_l1_request(
114    private_key: &EvmPrivateKey,
115    nonce: u64,
116    base_url: Option<&str>,
117) -> Result<(HttpClient, HashMap<String, String>, String)> {
118    let base = base_url
119        .unwrap_or_else(|| clob_http_url())
120        .trim_end_matches('/')
121        .to_string();
122    let timestamp =
123        (get_atomic_clock_realtime().get_time_ns().as_u64() / 1_000_000_000).to_string();
124    let (address, signature) = sign_clob_auth(private_key, &timestamp, nonce)?;
125    let headers = l1_headers(&address, &signature, &timestamp, nonce);
126    let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None)
127        .map_err(Error::from_http_client)?;
128    Ok((client, headers, base))
129}
130
131fn l1_headers(
132    address: &str,
133    signature: &str,
134    timestamp: &str,
135    nonce: u64,
136) -> HashMap<String, String> {
137    HashMap::from([
138        ("POLY_ADDRESS".to_string(), address.to_string()),
139        ("POLY_SIGNATURE".to_string(), signature.to_string()),
140        ("POLY_TIMESTAMP".to_string(), timestamp.to_string()),
141        ("POLY_NONCE".to_string(), nonce.to_string()),
142    ])
143}