Skip to content
Open
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 src/polymarket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ApiKeyCreds,
AssetType,
BalanceAllowance,
BuilderApiKeyInfo,
BuilderFeeRates,
BuilderTrade,
BuilderVolumeEntry,
Expand Down Expand Up @@ -177,6 +178,7 @@
"AsyncSecureClient",
"BalanceAllowance",
"BuilderApiKey",
"BuilderApiKeyInfo",
"BuilderFeeRates",
"BuilderTrade",
"BuilderVolumeEntry",
Expand Down
83 changes: 82 additions & 1 deletion src/polymarket/_internal/actions/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

from pydantic import TypeAdapter, ValidationError

from polymarket._internal.actions.relayer.auth import build_builder_key_headers
from polymarket._internal.l1_auth import ApiKeyAuthSignature
from polymarket.auth import BuilderApiKey
from polymarket.clients._transport import AsyncTransport, SyncTransport
from polymarket.errors import RequestRejectedError, UnexpectedResponseError
from polymarket.models.clob import ApiKeyCreds
from polymarket.models.clob import ApiKeyCreds, BuilderApiKeyInfo

_BUILDER_API_KEY_PATH = "/auth/builder-api-key"

_ApiKeysListAdapter = TypeAdapter(tuple[str, ...])
_BuilderApiKeyInfoListAdapter = TypeAdapter(tuple[BuilderApiKeyInfo, ...])


def build_l1_auth_headers(signature: ApiKeyAuthSignature) -> dict[str, str]:
Expand All @@ -33,6 +38,32 @@ def parse_api_keys_response(data: object) -> tuple[str, ...]:
raise UnexpectedResponseError("api keys response did not match expected shape") from error


def parse_builder_api_key_creds(data: object) -> BuilderApiKey:
if not isinstance(data, dict):
raise UnexpectedResponseError("builder api key response did not match expected shape")
fields = cast(dict[str, object], data)
key = fields.get("key")
secret = fields.get("secret")
passphrase = fields.get("passphrase")
if not (isinstance(key, str) and isinstance(secret, str) and isinstance(passphrase, str)):
raise UnexpectedResponseError("builder api key response did not match expected shape")
return BuilderApiKey(key=key, secret=secret, passphrase=passphrase)


def parse_builder_api_keys_response(data: object) -> tuple[BuilderApiKeyInfo, ...]:
if not isinstance(data, list):
raise UnexpectedResponseError("builder api keys response did not match expected shape")
normalized = [
{"key": item} if isinstance(item, str) else item for item in cast(list[object], data)
]
try:
return _BuilderApiKeyInfoListAdapter.validate_python(normalized)
except ValidationError as error:
raise UnexpectedResponseError(
"builder api keys response did not match expected shape"
) from error


async def create_api_key(clob: AsyncTransport, signature: ApiKeyAuthSignature) -> ApiKeyCreds:
payload = await clob.post_json("/auth/api-key", headers=build_l1_auth_headers(signature))
return parse_api_key_creds(payload)
Expand Down Expand Up @@ -67,6 +98,27 @@ async def delete_api_key(secure_clob: AsyncTransport) -> None:
)


async def create_builder_api_key(secure_clob: AsyncTransport) -> BuilderApiKey:
payload = await secure_clob.post_json(_BUILDER_API_KEY_PATH)
return parse_builder_api_key_creds(payload)


async def fetch_builder_api_keys(secure_clob: AsyncTransport) -> tuple[BuilderApiKeyInfo, ...]:
payload = await secure_clob.get_json(_BUILDER_API_KEY_PATH)
return parse_builder_api_keys_response(payload)


async def revoke_builder_api_key(clob: AsyncTransport, builder_key: BuilderApiKey) -> None:
headers = build_builder_key_headers(
creds=builder_key, method="DELETE", path=_BUILDER_API_KEY_PATH
)
payload = await clob.delete_json(_BUILDER_API_KEY_PATH, headers=headers)
if payload != "OK":
raise UnexpectedResponseError(
f"revoke builder api key response did not match expected shape: {payload!r}"
)


def create_api_key_sync(clob: SyncTransport, signature: ApiKeyAuthSignature) -> ApiKeyCreds:
payload = clob.post_json("/auth/api-key", headers=build_l1_auth_headers(signature))
return parse_api_key_creds(payload)
Expand Down Expand Up @@ -101,10 +153,33 @@ def delete_api_key_sync(secure_clob: SyncTransport) -> None:
)


def create_builder_api_key_sync(secure_clob: SyncTransport) -> BuilderApiKey:
payload = secure_clob.post_json(_BUILDER_API_KEY_PATH)
return parse_builder_api_key_creds(payload)


def fetch_builder_api_keys_sync(secure_clob: SyncTransport) -> tuple[BuilderApiKeyInfo, ...]:
payload = secure_clob.get_json(_BUILDER_API_KEY_PATH)
return parse_builder_api_keys_response(payload)


def revoke_builder_api_key_sync(clob: SyncTransport, builder_key: BuilderApiKey) -> None:
headers = build_builder_key_headers(
creds=builder_key, method="DELETE", path=_BUILDER_API_KEY_PATH
)
payload = clob.delete_json(_BUILDER_API_KEY_PATH, headers=headers)
if payload != "OK":
raise UnexpectedResponseError(
f"revoke builder api key response did not match expected shape: {payload!r}"
)


__all__ = [
"build_l1_auth_headers",
"create_api_key",
"create_api_key_sync",
"create_builder_api_key",
"create_builder_api_key_sync",
"create_or_derive_api_key",
"create_or_derive_api_key_sync",
"delete_api_key",
Expand All @@ -113,6 +188,12 @@ def delete_api_key_sync(secure_clob: SyncTransport) -> None:
"derive_api_key_sync",
"fetch_api_keys",
"fetch_api_keys_sync",
"fetch_builder_api_keys",
"fetch_builder_api_keys_sync",
"parse_api_key_creds",
"parse_api_keys_response",
"parse_builder_api_key_creds",
"parse_builder_api_keys_response",
"revoke_builder_api_key",
"revoke_builder_api_key_sync",
]
58 changes: 30 additions & 28 deletions src/polymarket/_internal/actions/relayer/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,39 @@
SyncRelayerHeaderResolver = Callable[[str, str, str | None], Mapping[str, str]]


# Single source of truth for builder-key (POLY_BUILDER_*) auth-header construction. Shared by the
# relayer header resolvers (below) and the builder-api-key revoke path so the HMAC signing never
# drifts between copies — keep all builder-key header building here rather than inlining it.
def build_builder_key_headers(
*, creds: BuilderApiKey, method: str, path: str, body: str | None = None
) -> dict[str, str]:
"""Build the ``POLY_BUILDER_*`` headers that authenticate a request as a builder key.

Signs ``timestamp + method + path (+ body)`` with the builder key's secret. Used both by
the relayer header resolver and by the builder-api-key revoke path.
"""
timestamp = int(time.time())
signature = build_hmac_signature(
secret=creds.secret,
timestamp=timestamp,
method=method,
path=path,
body=body,
)
return {
"POLY_BUILDER_API_KEY": creds.key,
"POLY_BUILDER_PASSPHRASE": creds.passphrase,
"POLY_BUILDER_SIGNATURE": signature,
"POLY_BUILDER_TIMESTAMP": str(timestamp),
}


def make_relayer_header_resolver(api_key: ApiKey) -> RelayerHeaderResolver:
if isinstance(api_key, BuilderApiKey):
creds = api_key

async def builder_resolver(method: str, path: str, body: str | None) -> Mapping[str, str]:
timestamp = int(time.time())
signature = build_hmac_signature(
secret=creds.secret,
timestamp=timestamp,
method=method,
path=path,
body=body,
)
return {
"POLY_BUILDER_API_KEY": creds.key,
"POLY_BUILDER_PASSPHRASE": creds.passphrase,
"POLY_BUILDER_SIGNATURE": signature,
"POLY_BUILDER_TIMESTAMP": str(timestamp),
}
return build_builder_key_headers(creds=creds, method=method, path=path, body=body)

return builder_resolver

Expand All @@ -52,20 +66,7 @@ def make_relayer_header_resolver_sync(api_key: ApiKey) -> SyncRelayerHeaderResol
creds = api_key

def builder_resolver(method: str, path: str, body: str | None) -> Mapping[str, str]:
timestamp = int(time.time())
signature = build_hmac_signature(
secret=creds.secret,
timestamp=timestamp,
method=method,
path=path,
body=body,
)
return {
"POLY_BUILDER_API_KEY": creds.key,
"POLY_BUILDER_PASSPHRASE": creds.passphrase,
"POLY_BUILDER_SIGNATURE": signature,
"POLY_BUILDER_TIMESTAMP": str(timestamp),
}
return build_builder_key_headers(creds=creds, method=method, path=path, body=body)

return builder_resolver

Expand All @@ -86,6 +87,7 @@ def relayer_resolver(method: str, path: str, body: str | None) -> Mapping[str, s
__all__ = [
"RelayerHeaderResolver",
"SyncRelayerHeaderResolver",
"build_builder_key_headers",
"make_relayer_header_resolver",
"make_relayer_header_resolver_sync",
]
25 changes: 24 additions & 1 deletion src/polymarket/clients/async_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
derive_current_deposit_wallet_address,
signature_type_for,
)
from polymarket.auth import ApiKey
from polymarket.auth import ApiKey, BuilderApiKey
from polymarket.clients._transport import AsyncTransport
from polymarket.clients.async_public import AsyncPublicClient
from polymarket.environments import PRODUCTION, Environment
Expand Down Expand Up @@ -168,6 +168,7 @@
TagReference,
Team,
)
from polymarket.models.clob.api_key import BuilderApiKeyInfo
from polymarket.models.clob.cancel import CancelOrdersResponse
from polymarket.models.clob.market_events import MarketEvent
from polymarket.models.clob.order_response import OrderResponse
Expand Down Expand Up @@ -1599,6 +1600,28 @@ async def delete_api_key(self) -> None:
"""Delete the API key currently used by this client."""
await _auth_actions.delete_api_key(self._ctx.secure_clob)

async def create_builder_api_key(self) -> BuilderApiKey:
"""Create a new builder API key for the authenticated account."""
return await _auth_actions.create_builder_api_key(self._ctx.secure_clob)

async def fetch_builder_api_keys(self) -> tuple[BuilderApiKeyInfo, ...]:
"""List the builder API keys for the authenticated account."""
return await _auth_actions.fetch_builder_api_keys(self._ctx.secure_clob)

async def revoke_builder_api_key(self) -> None:
"""Revoke the builder API key this client is configured with.

The revocation is authenticated by the builder key itself, so the client must have been
created with the key to revoke (``AsyncSecureClient.create(api_key=BuilderApiKey(...))``).
"""
builder_key = self._ctx.api_key
if not isinstance(builder_key, BuilderApiKey):
raise UserInputError(
"revoke_builder_api_key requires a client created with the builder key to "
"revoke (pass api_key=BuilderApiKey(...) to AsyncSecureClient.create)."
)
await _auth_actions.revoke_builder_api_key(self._ctx.clob, builder_key)

async def end_authentication(self) -> "AsyncPublicClient":
"""Delete current credentials, close this client, and return an async public client."""
environment = self._ctx.environment
Expand Down
26 changes: 24 additions & 2 deletions src/polymarket/clients/secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
derive_current_deposit_wallet_address_sync,
signature_type_for,
)
from polymarket.auth import ApiKey
from polymarket.auth import ApiKey, BuilderApiKey
from polymarket.clients._transport import SyncHeaderResolver, SyncTransport
from polymarket.environments import PRODUCTION, Environment
from polymarket.errors import (
Expand Down Expand Up @@ -153,7 +153,7 @@
TagReference,
Team,
)
from polymarket.models.clob import BuilderTrade
from polymarket.models.clob import BuilderApiKeyInfo, BuilderTrade
from polymarket.models.clob.cancel import CancelOrdersResponse
from polymarket.models.clob.order_response import OrderResponse
from polymarket.models.clob.orders import MarketOrderType, SignedOrder
Expand Down Expand Up @@ -1405,6 +1405,28 @@ def delete_api_key(self) -> None:
"""Delete the API key currently used by this client."""
_auth_actions.delete_api_key_sync(self._ctx.secure_clob)

def create_builder_api_key(self) -> BuilderApiKey:
"""Create a new builder API key for the authenticated account."""
return _auth_actions.create_builder_api_key_sync(self._ctx.secure_clob)

def fetch_builder_api_keys(self) -> tuple[BuilderApiKeyInfo, ...]:
"""List the builder API keys for the authenticated account."""
return _auth_actions.fetch_builder_api_keys_sync(self._ctx.secure_clob)

def revoke_builder_api_key(self) -> None:
"""Revoke the builder API key this client is configured with.

The revocation is authenticated by the builder key itself, so the client must have been
created with the key to revoke (``SecureClient.create(api_key=BuilderApiKey(...))``).
"""
builder_key = self._ctx.api_key
if not isinstance(builder_key, BuilderApiKey):
raise UserInputError(
"revoke_builder_api_key requires a client created with the builder key to "
"revoke (pass api_key=BuilderApiKey(...) to SecureClient.create)."
)
_auth_actions.revoke_builder_api_key_sync(self._ctx.clob, builder_key)

def end_authentication(self) -> "PublicClient":
"""Delete current credentials, close this client, and return a public client."""
from polymarket.clients.public import PublicClient
Expand Down
2 changes: 2 additions & 0 deletions src/polymarket/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ApiKeyCreds,
AssetType,
BalanceAllowance,
BuilderApiKeyInfo,
BuilderFeeRates,
BuilderTrade,
CancelOrdersResponse,
Expand Down Expand Up @@ -122,6 +123,7 @@
"ApiKeyCreds",
"AssetType",
"BalanceAllowance",
"BuilderApiKeyInfo",
"BuilderFeeRates",
"BuilderTrade",
"CancelOrdersResponse",
Expand Down
3 changes: 2 additions & 1 deletion src/polymarket/models/clob/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Notification,
OpenOrder,
)
from polymarket.models.clob.api_key import ApiKeyCreds
from polymarket.models.clob.api_key import ApiKeyCreds, BuilderApiKeyInfo
from polymarket.models.clob.builder import BuilderFeeRates, BuilderTrade
from polymarket.models.clob.cancel import CancelOrdersResponse
from polymarket.models.clob.last_trade import LastTradePrice, LastTradePriceForToken
Expand Down Expand Up @@ -45,6 +45,7 @@
"ApiKeyCreds",
"AssetType",
"BalanceAllowance",
"BuilderApiKeyInfo",
"BuilderFeeRates",
"BuilderTrade",
"CancelOrdersResponse",
Expand Down
15 changes: 14 additions & 1 deletion src/polymarket/models/clob/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pydantic import Field

from polymarket.models.base import BaseModel
from polymarket.models.clob._validators import EpochMsOrIsoTimestamp


class ApiKeyCreds(BaseModel):
Expand All @@ -22,4 +23,16 @@ def _repr_html_(self) -> str:
)


__all__ = ["ApiKeyCreds"]
class BuilderApiKeyInfo(BaseModel):
"""A builder API key as listed for an account — identity and lifecycle, no secret.

Returned by ``fetch_builder_api_keys``. ``revoked_at`` is ``None`` while the key is
active and set to the revocation time once revoked.
"""

key: str
created_at: EpochMsOrIsoTimestamp = Field(default=None, validation_alias="createdAt")
revoked_at: EpochMsOrIsoTimestamp = Field(default=None, validation_alias="revokedAt")


__all__ = ["ApiKeyCreds", "BuilderApiKeyInfo"]
Loading
Loading