nexode-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nexode/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ from .client import Nexode
2
+ from .enums import EventSort, MarketStatus, OrderSide, OrderStatus, Outcome, TimeInForce
3
+ from .errors import (
4
+ AuthError,
5
+ NexodeError,
6
+ NotFoundError,
7
+ RateLimitError,
8
+ ServerError,
9
+ UnprocessableError,
10
+ ValidationError,
11
+ )
12
+ from .models import (
13
+ ApiKey,
14
+ BookSnapshot,
15
+ Burn,
16
+ Event,
17
+ Instrument,
18
+ Market,
19
+ MarketEvent,
20
+ MarketTrade,
21
+ Mint,
22
+ Order,
23
+ PortfolioSnapshot,
24
+ PriceLevel,
25
+ Redeem,
26
+ Ticker,
27
+ Token,
28
+ Trade,
29
+ )
30
+
31
+ __all__ = [
32
+ "Nexode",
33
+ # enums
34
+ "Outcome",
35
+ "OrderSide",
36
+ "OrderStatus",
37
+ "MarketStatus",
38
+ "EventSort",
39
+ "TimeInForce",
40
+ # models
41
+ "Market",
42
+ "Event",
43
+ "MarketEvent",
44
+ "Instrument",
45
+ "Order",
46
+ "Trade",
47
+ "MarketTrade",
48
+ "Ticker",
49
+ "PriceLevel",
50
+ "BookSnapshot",
51
+ "Token",
52
+ "Mint",
53
+ "Redeem",
54
+ "Burn",
55
+ "PortfolioSnapshot",
56
+ "ApiKey",
57
+ # errors
58
+ "NexodeError",
59
+ "AuthError",
60
+ "ValidationError",
61
+ "UnprocessableError",
62
+ "NotFoundError",
63
+ "RateLimitError",
64
+ "ServerError",
65
+ ]
66
+
67
+ __version__ = "0.1.0"
nexode/_resolver.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Iterable, Optional, Tuple
4
+
5
+ from ._transport import Transport
6
+ from .enums import Outcome
7
+
8
+ _OUTCOME_KEYS = (
9
+ (Outcome.YES, "atomix_yes_market_id"),
10
+ (Outcome.NO, "atomix_no_market_id"),
11
+ )
12
+
13
+
14
+ class Resolver:
15
+ """Translates between a (market, outcome) and its order book, both ways.
16
+
17
+ The platform runs a separate order book per outcome; the SDK hides that by
18
+ routing on (market, outcome). Lookups are cached, so a market resolves once.
19
+ """
20
+
21
+ def __init__(self, nexode: Transport) -> None:
22
+ self._n = nexode
23
+ self._to_book: Dict[Tuple[str, Outcome], str] = {}
24
+ self._from_book: Dict[str, Tuple[str, Outcome]] = {}
25
+
26
+ def order_book_id(self, market_id: str, outcome) -> str:
27
+ outcome = Outcome(outcome)
28
+ if (market_id, outcome) not in self._to_book:
29
+ self._load_market(market_id)
30
+ try:
31
+ return self._to_book[(market_id, outcome)]
32
+ except KeyError:
33
+ raise ValueError(f"market {market_id!r} has no {outcome.value} order book")
34
+
35
+ def resolve(self, order_book_ids: Iterable[str]) -> Dict[str, Tuple[str, Outcome]]:
36
+ ids = list(order_book_ids)
37
+ unknown = sorted({i for i in ids if i not in self._from_book})
38
+ if unknown:
39
+ body = self._n.get(
40
+ "/v0/markets/by-atomix-id", params={"ids": ",".join(unknown)}
41
+ )
42
+ for m in body["mappings"]:
43
+ self._index(m["atomix_market_id"], m["market"]["id"], Outcome(m["outcome"]))
44
+ return {i: self._from_book[i] for i in ids if i in self._from_book}
45
+
46
+ def market_outcome(self, order_book_id: str) -> Optional[Tuple[str, Outcome]]:
47
+ return self.resolve([order_book_id]).get(order_book_id)
48
+
49
+ def _load_market(self, market_id: str) -> None:
50
+ metadata = self._n.get(f"/v0/markets/{market_id}")["market"].get("metadata") or {}
51
+ for outcome, key in _OUTCOME_KEYS:
52
+ if metadata.get(key):
53
+ self._index(metadata[key], market_id, outcome)
54
+
55
+ def _index(self, order_book_id: str, market_id: str, outcome: Outcome) -> None:
56
+ self._to_book[(market_id, outcome)] = order_book_id
57
+ self._from_book[order_book_id] = (market_id, outcome)
nexode/_transport.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .errors import error_from_response
8
+
9
+
10
+ class Transport:
11
+ """Issues requests against a fixed base URL and returns the flattened body.
12
+
13
+ Builds absolute URLs (base + path) rather than relying on httpx base_url
14
+ merging, so a base that includes a path prefix (e.g. ``.../atomix``) is
15
+ preserved even though request paths start with ``/``.
16
+ """
17
+
18
+ def __init__(self, client: httpx.Client, base_url: str) -> None:
19
+ self._client = client
20
+ self._base = base_url.rstrip("/")
21
+
22
+ def request(self, method: str, path: str, **kwargs: Any) -> dict:
23
+ response = self._client.request(method, self._base + path, **kwargs)
24
+ body = response.json() if response.content else {}
25
+ if response.status_code >= 400:
26
+ raise error_from_response(response.status_code, body)
27
+ return body
28
+
29
+ def get(self, path: str, **kwargs: Any) -> dict:
30
+ return self.request("GET", path, **kwargs)
31
+
32
+ def post(self, path: str, **kwargs: Any) -> dict:
33
+ return self.request("POST", path, **kwargs)
34
+
35
+ def delete(self, path: str, **kwargs: Any) -> dict:
36
+ return self.request("DELETE", path, **kwargs)
nexode/auth.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Generator
5
+
6
+ import httpx
7
+
8
+ from .errors import AuthError
9
+
10
+ # Re-exchange this many seconds before the token's stated expiry.
11
+ _EXPIRY_SKEW_SECONDS = 30
12
+
13
+
14
+ class ApiKeyAuth(httpx.Auth):
15
+ """Exchanges a long-lived API key for short-lived access tokens, caches
16
+ them, and re-exchanges transparently on expiry or 401."""
17
+
18
+ def __init__(self, api_key: str, *, exchange_url: str) -> None:
19
+ self._api_key = api_key
20
+ self._exchange_url = exchange_url
21
+ self._token: str | None = None
22
+ self._expires_at: float = 0.0
23
+
24
+ def sync_auth_flow(
25
+ self, request: httpx.Request
26
+ ) -> Generator[httpx.Request, httpx.Response, None]:
27
+ if not self._fresh():
28
+ response = yield self._exchange()
29
+ response.read()
30
+ self._store(response)
31
+ self._apply(request)
32
+ response = yield request
33
+ if response.status_code == 401:
34
+ response = yield self._exchange()
35
+ response.read()
36
+ self._store(response)
37
+ self._apply(request)
38
+ yield request
39
+
40
+ async def async_auth_flow(
41
+ self, request: httpx.Request
42
+ ) -> Generator[httpx.Request, httpx.Response, None]:
43
+ if not self._fresh():
44
+ response = yield self._exchange()
45
+ await response.aread()
46
+ self._store(response)
47
+ self._apply(request)
48
+ response = yield request
49
+ if response.status_code == 401:
50
+ response = yield self._exchange()
51
+ await response.aread()
52
+ self._store(response)
53
+ self._apply(request)
54
+ yield request
55
+
56
+ def _fresh(self) -> bool:
57
+ return self._token is not None and time.time() < self._expires_at - _EXPIRY_SKEW_SECONDS
58
+
59
+ def _exchange(self) -> httpx.Request:
60
+ # Absolute URL so the exchange always targets the auth endpoint.
61
+ return httpx.Request("POST", self._exchange_url, json={"api_key": self._api_key})
62
+
63
+ def _apply(self, request: httpx.Request) -> None:
64
+ request.headers["Authorization"] = f"Bearer {self._token}"
65
+
66
+ def _store(self, response: httpx.Response) -> None:
67
+ if response.status_code != 200:
68
+ raise AuthError("API key exchange failed.", status=response.status_code)
69
+ data = response.json()
70
+ self._token = data["access_token"]
71
+ self._expires_at = float(data["access_expires_at"])
nexode/client.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ import httpx
7
+
8
+ from ._resolver import Resolver
9
+ from ._transport import Transport
10
+ from .auth import ApiKeyAuth
11
+ from .resources import (
12
+ ApiKeys,
13
+ Burns,
14
+ Events,
15
+ Markets,
16
+ Mints,
17
+ Orders,
18
+ Portfolio,
19
+ Redeems,
20
+ Tokens,
21
+ Trades,
22
+ )
23
+
24
+ DEFAULT_API_BASE = "https://api.nexode.io"
25
+ DEFAULT_ATOMIX_BASE = "https://api.nexode.io/atomix"
26
+
27
+
28
+ class Nexode:
29
+ """Synchronous Nexode client.
30
+
31
+ A single API key authenticates the whole platform — prediction markets,
32
+ the order book, and your account activity.
33
+
34
+ Args:
35
+ api_key: A ``nxd_...`` key. Falls back to ``$NEXODE_API_KEY``.
36
+ api_base: Override the Nexode API base URL.
37
+ atomix_base: Override the order-book (Atomix) base URL.
38
+ timeout: Per-request timeout in seconds.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ api_key: Optional[str] = None,
44
+ *,
45
+ api_base: str = DEFAULT_API_BASE,
46
+ atomix_base: str = DEFAULT_ATOMIX_BASE,
47
+ timeout: float = 30.0,
48
+ ) -> None:
49
+ api_key = api_key or os.environ.get("NEXODE_API_KEY")
50
+ if not api_key:
51
+ raise ValueError("api_key is required — pass api_key=... or set NEXODE_API_KEY.")
52
+
53
+ auth = ApiKeyAuth(api_key, exchange_url=f"{api_base.rstrip('/')}/v0/api-keys/exchange")
54
+ self._http = httpx.Client(auth=auth, timeout=timeout)
55
+ nexode = Transport(self._http, api_base)
56
+ atomix = Transport(self._http, atomix_base)
57
+ resolver = Resolver(nexode)
58
+
59
+ self.markets = Markets(nexode, atomix, resolver)
60
+ self.events = Events(nexode)
61
+ self.orders = Orders(atomix, resolver)
62
+ self.trades = Trades(atomix, resolver)
63
+ self.tokens = Tokens(atomix)
64
+ self.mints = Mints(nexode)
65
+ self.redeems = Redeems(nexode)
66
+ self.burns = Burns(nexode)
67
+ self.portfolio = Portfolio(atomix=atomix, nexode=nexode, resolver=resolver)
68
+ self.api_keys = ApiKeys(nexode)
69
+
70
+ def close(self) -> None:
71
+ self._http.close()
72
+
73
+ def __enter__(self) -> "Nexode":
74
+ return self
75
+
76
+ def __exit__(self, *_exc: object) -> None:
77
+ self.close()
nexode/enums.py ADDED
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Outcome(str, Enum):
5
+ YES = "yes"
6
+ NO = "no"
7
+
8
+
9
+ class OrderSide(str, Enum):
10
+ BUY = "buy"
11
+ SELL = "sell"
12
+
13
+
14
+ class OrderStatus(str, Enum):
15
+ OPEN = "open"
16
+ PARTIALLY_FILLED = "partially_filled"
17
+ FILLED = "filled"
18
+ CANCELLED = "cancelled"
19
+
20
+
21
+ class MarketStatus(str, Enum):
22
+ ACTIVE = "active"
23
+ RESOLVED = "resolved"
24
+ VOIDED = "voided"
25
+
26
+
27
+ class EventSort(str, Enum):
28
+ """Sort order for the events list. Every mode keeps active events ahead
29
+ of overdue and settled ones; the mode picks the tiebreaker."""
30
+
31
+ ENDING = "ending"
32
+ TRENDING = "trending"
33
+ NEW = "new"
34
+ MARKETS = "markets"
35
+ NAME = "name"
36
+
37
+
38
+ class TimeInForce(str, Enum):
39
+ GTC = "gtc"
40
+ IOC = "ioc"
41
+ FOK = "fok"
42
+ GTD = "gtd"
43
+ DAY = "day"
nexode/errors.py ADDED
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Optional, Type
4
+
5
+
6
+ class NexodeError(Exception):
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ *,
11
+ status: Optional[int] = None,
12
+ trace_id: Optional[str] = None,
13
+ ) -> None:
14
+ super().__init__(message)
15
+ self.message = message
16
+ self.status = status
17
+ self.trace_id = trace_id
18
+
19
+
20
+ class AuthError(NexodeError):
21
+ """Invalid or expired credentials (401/403)."""
22
+
23
+
24
+ class ValidationError(NexodeError):
25
+ """The request was malformed (400)."""
26
+
27
+
28
+ class UnprocessableError(NexodeError):
29
+ """A well-formed request rejected by a business rule — insufficient funds,
30
+ missing trade delegation, market halted, etc. (422)."""
31
+
32
+
33
+ class NotFoundError(NexodeError):
34
+ """The resource does not exist (404)."""
35
+
36
+
37
+ class RateLimitError(NexodeError):
38
+ """Too many requests (429)."""
39
+
40
+
41
+ class ServerError(NexodeError):
42
+ """The server failed to handle the request (5xx)."""
43
+
44
+
45
+ def error_from_response(status: int, body: Mapping[str, Any]) -> NexodeError:
46
+ match status:
47
+ case 400:
48
+ cls: Type[NexodeError] = ValidationError
49
+ case 401 | 403:
50
+ cls = AuthError
51
+ case 404:
52
+ cls = NotFoundError
53
+ case 422:
54
+ cls = UnprocessableError
55
+ case 429:
56
+ cls = RateLimitError
57
+ case s if s >= 500:
58
+ cls = ServerError
59
+ case _:
60
+ cls = NexodeError
61
+ return cls(
62
+ body.get("message") or f"HTTP {status}",
63
+ status=status,
64
+ trace_id=body.get("trace_id"),
65
+ )
@@ -0,0 +1,27 @@
1
+ from .activity import Burn, Mint, Redeem
2
+ from .api_key import ApiKey
3
+ from .market import BookSnapshot, PriceLevel, Ticker
4
+ from .nexode import Event, Instrument, Market, MarketEvent
5
+ from .order import Order
6
+ from .portfolio import PortfolioSnapshot
7
+ from .token import Token
8
+ from .trade import MarketTrade, Trade
9
+
10
+ __all__ = [
11
+ "Market",
12
+ "Event",
13
+ "MarketEvent",
14
+ "Instrument",
15
+ "Order",
16
+ "Trade",
17
+ "MarketTrade",
18
+ "Ticker",
19
+ "PriceLevel",
20
+ "BookSnapshot",
21
+ "Token",
22
+ "Mint",
23
+ "Redeem",
24
+ "Burn",
25
+ "PortfolioSnapshot",
26
+ "ApiKey",
27
+ ]
nexode/models/_base.py ADDED
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class Model(BaseModel):
5
+ model_config = ConfigDict(extra="ignore", frozen=True)
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Optional
6
+
7
+ from ._base import Model
8
+
9
+
10
+ class Mint(Model):
11
+ """Collateral paid in to create a YES+NO pair."""
12
+
13
+ id: str
14
+ market_id: str
15
+ party_id: str
16
+ amount: Decimal
17
+ created_at: datetime
18
+ transaction_id: str
19
+
20
+
21
+ class Redeem(Model):
22
+ """Payout claimed on a resolved market."""
23
+
24
+ id: str
25
+ market_id: str
26
+ party_id: str
27
+ status: str
28
+ amount: Optional[Decimal] = None
29
+ reason: Optional[str] = None
30
+ created_at: datetime
31
+ started_at: Optional[datetime] = None
32
+ completed_at: Optional[datetime] = None
33
+ transaction_id: Optional[str] = None
34
+
35
+
36
+ class Burn(Model):
37
+ """A YES+NO pair burned back to collateral."""
38
+
39
+ id: str
40
+ market_id: str
41
+ party_id: str
42
+ status: str
43
+ amount: Decimal
44
+ reason: Optional[str] = None
45
+ created_at: datetime
46
+ started_at: Optional[datetime] = None
47
+ completed_at: Optional[datetime] = None
48
+ transaction_id: Optional[str] = None
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from ._base import Model
7
+
8
+
9
+ class ApiKey(Model):
10
+ """Masked metadata for an API key — the plaintext is never re-shown."""
11
+
12
+ id: str
13
+ last_four: str
14
+ name: Optional[str] = None
15
+ created_at: datetime
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import List, Optional
6
+
7
+ from ._base import Model
8
+
9
+
10
+ class Ticker(Model):
11
+ best_bid: Optional[Decimal] = None
12
+ best_ask: Optional[Decimal] = None
13
+ last_trade_price: Optional[Decimal] = None
14
+ captured_at: datetime
15
+
16
+
17
+ class PriceLevel(Model):
18
+ price: Decimal
19
+ quantity: Decimal
20
+
21
+
22
+ class BookSnapshot(Model):
23
+ bids: List[PriceLevel]
24
+ asks: List[PriceLevel]
25
+ captured_at: datetime
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ from pydantic import model_validator
8
+
9
+ from ..enums import MarketStatus, Outcome
10
+ from ._base import Model
11
+
12
+ _DEFAULT_LABELS = {Outcome.YES: "Yes", Outcome.NO: "No"}
13
+
14
+
15
+ class Instrument(Model):
16
+ id: str
17
+ admin_id: str
18
+
19
+
20
+ class MarketEvent(Model):
21
+ id: str
22
+ slug: str
23
+ title: str
24
+ image_url: Optional[str] = None
25
+ market_count: int
26
+
27
+
28
+ class Market(Model):
29
+ """A prediction market (the question), with both outcomes' order books."""
30
+
31
+ id: str
32
+ slug: str
33
+ name: str
34
+ status: str
35
+ category: Optional[str] = None
36
+ title: Optional[str] = None
37
+ yes_label: Optional[str] = None
38
+ no_label: Optional[str] = None
39
+ collateral_price: Decimal
40
+ redeem_fee_rate: Decimal
41
+ collateral_instrument: Instrument
42
+ yes_instrument: Instrument
43
+ no_instrument: Instrument
44
+ yes_order_book_id: Optional[str] = None
45
+ no_order_book_id: Optional[str] = None
46
+ liquidity: Optional[Decimal] = None
47
+ resolved_outcome: Optional[Outcome] = None
48
+ resolved_at: Optional[datetime] = None
49
+ voided_at: Optional[datetime] = None
50
+ event: Optional[MarketEvent] = None
51
+
52
+ @model_validator(mode="before")
53
+ @classmethod
54
+ def _lift_metadata(cls, data: Any) -> Any:
55
+ if isinstance(data, dict):
56
+ meta = data.get("metadata") or {}
57
+ data = {
58
+ **data,
59
+ "category": meta.get("category"),
60
+ "title": meta.get("title"),
61
+ "yes_label": meta.get("yes_label"),
62
+ "no_label": meta.get("no_label"),
63
+ "yes_order_book_id": meta.get("atomix_yes_market_id"),
64
+ "no_order_book_id": meta.get("atomix_no_market_id"),
65
+ }
66
+ return data
67
+
68
+ @property
69
+ def is_active(self) -> bool:
70
+ return self.status == MarketStatus.ACTIVE
71
+
72
+ @property
73
+ def is_resolved(self) -> bool:
74
+ return self.status == MarketStatus.RESOLVED
75
+
76
+ @property
77
+ def is_voided(self) -> bool:
78
+ return self.status == MarketStatus.VOIDED
79
+
80
+ def outcome_label(self, outcome: Union[Outcome, str]) -> str:
81
+ """Display label for an outcome — e.g. ``"Lakers"``/``"Celtics"`` on a
82
+ game market. Falls back to ``"Yes"``/``"No"`` when unset."""
83
+ outcome = Outcome(outcome)
84
+ raw = self.yes_label if outcome is Outcome.YES else self.no_label
85
+ return (raw or "").strip() or _DEFAULT_LABELS[outcome]
86
+
87
+ @property
88
+ def labels(self) -> Dict[Outcome, str]:
89
+ """Both outcomes' display labels, keyed by :class:`Outcome`."""
90
+ return {o: self.outcome_label(o) for o in Outcome}
91
+
92
+ def outcome_for_label(self, label: str) -> Outcome:
93
+ """Map a display label (or a literal ``"yes"``/``"no"``) back to its
94
+ outcome side. Matching is case-insensitive."""
95
+ needle = label.strip().casefold()
96
+ for outcome in Outcome:
97
+ if needle in (self.outcome_label(outcome).casefold(), outcome.value):
98
+ return outcome
99
+ raise ValueError(
100
+ f"label {label!r} does not match either outcome of market {self.slug!r} "
101
+ f"({self.outcome_label(Outcome.YES)!r} / {self.outcome_label(Outcome.NO)!r})"
102
+ )
103
+
104
+ @property
105
+ def winning_outcome(self) -> Optional[Outcome]:
106
+ """The resolved outcome, or ``None`` while unresolved/voided."""
107
+ return self.resolved_outcome
108
+
109
+ @property
110
+ def winning_label(self) -> Optional[str]:
111
+ """Display label of the winning outcome, or ``None`` while unresolved."""
112
+ return None if self.resolved_outcome is None else self.outcome_label(self.resolved_outcome)
113
+
114
+
115
+ class Event(Model):
116
+ id: str
117
+ slug: str
118
+ admin_party: str
119
+ title: Optional[str] = None
120
+ category: Optional[str] = None
121
+ description: Optional[str] = None
122
+ image_url: Optional[str] = None
123
+ created_at: Optional[datetime] = None
124
+ market_ids: List[str]
125
+ markets: List[Market]
126
+
127
+ @model_validator(mode="before")
128
+ @classmethod
129
+ def _lift_metadata(cls, data: Any) -> Any:
130
+ if isinstance(data, dict):
131
+ meta = data.get("metadata") or {}
132
+ data = {
133
+ **data,
134
+ "title": meta.get("title"),
135
+ "category": meta.get("category"),
136
+ "description": meta.get("description"),
137
+ "image_url": meta.get("image_url"),
138
+ "created_at": meta.get("created_at"),
139
+ }
140
+ return data
nexode/models/order.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Optional
6
+
7
+ from ..enums import Outcome, OrderSide, OrderStatus
8
+ from ._base import Model
9
+
10
+
11
+ class Order(Model):
12
+ id: str
13
+ market_id: str
14
+ outcome: Outcome
15
+ party_id: str
16
+ side: OrderSide
17
+ status: OrderStatus
18
+ quantity: Decimal
19
+ filled_quantity: Decimal
20
+ remaining_quantity: Decimal
21
+ price: Optional[Decimal] = None
22
+ cancel_reason: Optional[str] = None
23
+ created_at: datetime
24
+ updated_at: datetime
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List
4
+
5
+ from .activity import Burn, Mint, Redeem
6
+ from .market import Ticker
7
+ from .nexode import Market
8
+ from .order import Order
9
+ from .trade import Trade
10
+ from ._base import Model
11
+
12
+
13
+ class PortfolioSnapshot(Model):
14
+ """Everything needed to compute PnL — raw data, no math.
15
+
16
+ A position is reconstructed from `mints` (+ buys − sells) and `trades`; use
17
+ `markets` for cost-basis/resolution and `tickers` to mark open positions.
18
+ `markets` is keyed by market id; `tickers` by ``"{market_id}:{outcome}"``.
19
+ """
20
+
21
+ trades: List[Trade]
22
+ open_orders: List[Order]
23
+ mints: List[Mint]
24
+ redeems: List[Redeem]
25
+ burns: List[Burn]
26
+ markets: Dict[str, Market]
27
+ tickers: Dict[str, Ticker]
nexode/models/token.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from typing import Optional
5
+
6
+ from ._base import Model
7
+
8
+
9
+ class Token(Model):
10
+ instrument_id: str
11
+ instrument_admin_id: str
12
+ symbol: str
13
+ name: str
14
+ decimals: int
15
+ total_supply: Optional[Decimal] = None
nexode/models/trade.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+
6
+ from ..enums import Outcome, OrderSide
7
+ from ._base import Model
8
+
9
+
10
+ class Trade(Model):
11
+ id: str
12
+ market_id: str
13
+ outcome: Outcome
14
+ price: Decimal
15
+ quantity: Decimal
16
+ taker_side: OrderSide
17
+ maker_order_id: str
18
+ taker_order_id: str
19
+ maker_party_id: str
20
+ taker_party_id: str
21
+ maker_fee: Decimal
22
+ taker_fee: Decimal
23
+ executed_at: datetime
24
+
25
+
26
+ class MarketTrade(Model):
27
+ trade_id: str
28
+ price: Decimal
29
+ quantity: Decimal
30
+ taker_side: OrderSide
31
+ executed_at: datetime
@@ -0,0 +1,21 @@
1
+ from .activity import Burns, Mints, Redeems
2
+ from .api_keys import ApiKeys
3
+ from .events import Events
4
+ from .markets import Markets
5
+ from .orders import Orders
6
+ from .portfolio import Portfolio
7
+ from .tokens import Tokens
8
+ from .trades import Trades
9
+
10
+ __all__ = [
11
+ "ApiKeys",
12
+ "Markets",
13
+ "Events",
14
+ "Orders",
15
+ "Trades",
16
+ "Tokens",
17
+ "Mints",
18
+ "Redeems",
19
+ "Burns",
20
+ "Portfolio",
21
+ ]
@@ -0,0 +1,6 @@
1
+ from .._transport import Transport
2
+
3
+
4
+ class Resource:
5
+ def __init__(self, transport: Transport) -> None:
6
+ self._t = transport
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from ..models import Burn, Mint, Redeem
6
+ from ._base import Resource
7
+
8
+
9
+ class Mints(Resource):
10
+ def list(self) -> List[Mint]:
11
+ body = self._t.get("/v0/mints")
12
+ return [Mint.model_validate(m) for m in body["mints"]]
13
+
14
+
15
+ class Redeems(Resource):
16
+ def list(self) -> List[Redeem]:
17
+ body = self._t.get("/v0/redeems")
18
+ return [Redeem.model_validate(r) for r in body["redeems"]]
19
+
20
+ def get(self, redeem_id: str) -> Redeem:
21
+ return Redeem.model_validate(self._t.get(f"/v0/redeems/{redeem_id}"))
22
+
23
+
24
+ class Burns(Resource):
25
+ def list(self) -> List[Burn]:
26
+ body = self._t.get("/v0/burns")
27
+ return [Burn.model_validate(b) for b in body["burns"]]
28
+
29
+ def get(self, burn_id: str) -> Burn:
30
+ return Burn.model_validate(self._t.get(f"/v0/burns/{burn_id}"))
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from ..models import ApiKey
6
+ from ._base import Resource
7
+
8
+
9
+ class ApiKeys(Resource):
10
+ """Inspect and revoke the API keys on your account. New keys are created
11
+ in the web app (Settings → Developer), never through the SDK."""
12
+
13
+ def list(self) -> List[ApiKey]:
14
+ body = self._t.get("/v0/api-keys/me")
15
+ return [ApiKey.model_validate(k) for k in body["api_keys"]]
16
+
17
+ def revoke(self, api_key_id: str) -> None:
18
+ self._t.delete(f"/v0/api-keys/me/{api_key_id}")
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Sequence, Union
4
+
5
+ from ..enums import EventSort
6
+ from ..models import Event
7
+ from ._base import Resource
8
+
9
+
10
+ class Events(Resource):
11
+ def list(
12
+ self,
13
+ *,
14
+ q: Optional[str] = None,
15
+ category: Optional[str] = None,
16
+ sort: Optional[Union[EventSort, str]] = None,
17
+ market_ids: Optional[Sequence[str]] = None,
18
+ hide_stale_settled: Optional[bool] = None,
19
+ limit: Optional[int] = None,
20
+ offset: Optional[int] = None,
21
+ ) -> List[Event]:
22
+ """List events, filtered and sorted server-side.
23
+
24
+ Args:
25
+ q: Full-text search over event titles.
26
+ category: Only events in this category.
27
+ sort: ``ending``/``trending``/``new``/``markets``/``name``;
28
+ defaults to ``new``. Active events always sort ahead of
29
+ overdue and settled ones.
30
+ market_ids: Only events containing at least one of these markets.
31
+ hide_stale_settled: Drop fully-settled events whose dispute
32
+ windows have elapsed.
33
+ limit: Maximum events to return (server default 100, max 1000).
34
+ offset: Number of sorted events to skip before the returned
35
+ window — pair with ``limit`` to page through the feed.
36
+ """
37
+ params: dict = {}
38
+ if q is not None:
39
+ params["q"] = q
40
+ if category is not None:
41
+ params["category"] = category
42
+ if sort is not None:
43
+ params["sort"] = EventSort(sort).value
44
+ if market_ids:
45
+ params["market_ids"] = ",".join(market_ids)
46
+ if hide_stale_settled is not None:
47
+ params["hide_stale_settled"] = hide_stale_settled
48
+ if limit is not None:
49
+ params["limit"] = limit
50
+ if offset is not None:
51
+ params["offset"] = offset
52
+ body = self._t.get("/v0/events", params=params)
53
+ return [Event.model_validate(e) for e in body["events"]]
54
+
55
+ def get(self, event_id: str) -> Event:
56
+ body = self._t.get(f"/v0/events/{event_id}")
57
+ return Event.model_validate(body["event"])
58
+
59
+ def by_slug(self, slug: str) -> Event:
60
+ body = self._t.get(f"/v0/events/by-slug/{slug}")
61
+ return Event.model_validate(body["event"])
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from .._resolver import Resolver
6
+ from .._transport import Transport
7
+ from ..enums import MarketStatus, Outcome
8
+ from ..models import BookSnapshot, Market, MarketTrade, Ticker
9
+
10
+
11
+ class Markets:
12
+ """Prediction markets: browse them, and read live data per outcome."""
13
+
14
+ def __init__(self, nexode: Transport, atomix: Transport, resolver: Resolver) -> None:
15
+ self._n = nexode
16
+ self._a = atomix
17
+ self._r = resolver
18
+
19
+ def list(
20
+ self,
21
+ *,
22
+ category: Optional[str] = None,
23
+ status: Optional[Union[MarketStatus, str]] = None,
24
+ q: Optional[str] = None,
25
+ limit: Optional[int] = None,
26
+ ) -> List[Market]:
27
+ """List prediction markets, filtered server-side.
28
+
29
+ Args:
30
+ category: Only markets in this category.
31
+ status: Only markets in this lifecycle state
32
+ (``active``/``resolved``/``voided``).
33
+ q: Full-text search over market questions.
34
+ limit: Maximum markets to return (max 1000).
35
+ """
36
+ params: dict = {}
37
+ if category is not None:
38
+ params["category"] = category
39
+ if status is not None:
40
+ params["status"] = MarketStatus(status).value
41
+ if q is not None:
42
+ params["q"] = q
43
+ if limit is not None:
44
+ params["limit"] = limit
45
+ body = self._n.get("/v0/markets", params=params)
46
+ return [Market.model_validate(m) for m in body["markets"]]
47
+
48
+ def get(self, market_id: str) -> Market:
49
+ return Market.model_validate(self._n.get(f"/v0/markets/{market_id}")["market"])
50
+
51
+ def by_slug(self, slug: str) -> Market:
52
+ return Market.model_validate(self._n.get(f"/v0/markets/by-slug/{slug}")["market"])
53
+
54
+ def ticker(self, market_id: str, outcome: Union[Outcome, str]) -> Ticker:
55
+ book = self._r.order_book_id(market_id, outcome)
56
+ return Ticker.model_validate(self._a.get(f"/v0/markets/{book}/ticker")["ticker"])
57
+
58
+ def book(self, market_id: str, outcome: Union[Outcome, str]) -> BookSnapshot:
59
+ book = self._r.order_book_id(market_id, outcome)
60
+ return BookSnapshot.model_validate(self._a.get(f"/v0/markets/{book}/book")["snapshot"])
61
+
62
+ def recent_trades(self, market_id: str, outcome: Union[Outcome, str]) -> List[MarketTrade]:
63
+ book = self._r.order_book_id(market_id, outcome)
64
+ body = self._a.get(f"/v0/markets/{book}/trades")
65
+ return [MarketTrade.model_validate(t) for t in body["trades"]]
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import List, Optional, Tuple, Union
6
+
7
+ from .._resolver import Resolver
8
+ from .._transport import Transport
9
+ from ..enums import Outcome, OrderSide, TimeInForce
10
+ from ..models import Order
11
+
12
+ Number = Union[str, int, float, Decimal]
13
+
14
+
15
+ class Orders:
16
+ def __init__(self, atomix: Transport, resolver: Resolver) -> None:
17
+ self._a = atomix
18
+ self._r = resolver
19
+
20
+ def create(
21
+ self,
22
+ *,
23
+ market_id: str,
24
+ outcome: Union[Outcome, str],
25
+ side: Union[OrderSide, str],
26
+ price: Number,
27
+ quantity: Number,
28
+ time_in_force: Union[TimeInForce, str] = TimeInForce.GTC,
29
+ expires_at: Optional[datetime] = None,
30
+ ) -> Order:
31
+ book = self._r.order_book_id(market_id, outcome)
32
+ body = self._a.post(
33
+ "/v0/orders",
34
+ json={
35
+ "market_id": book,
36
+ "side": OrderSide(side).value,
37
+ "quantity": str(quantity),
38
+ "order_type": {
39
+ "type": "limit",
40
+ "price": str(price),
41
+ "time_in_force": _time_in_force(time_in_force, expires_at),
42
+ },
43
+ },
44
+ )
45
+ return self._order(body["order"])
46
+
47
+ def list(self) -> List[Order]:
48
+ raw = self._a.get("/v0/orders")["orders"]
49
+ resolved = self._r.resolve(r["market_id"] for r in raw)
50
+ return [_build(r, resolved.get(r["market_id"])) for r in raw]
51
+
52
+ def get(self, order_id: str) -> Order:
53
+ return self._order(self._a.get(f"/v0/orders/{order_id}")["order"])
54
+
55
+ def cancel(self, order_id: str) -> None:
56
+ self._a.post(f"/v0/orders/{order_id}/cancel")
57
+
58
+ def _order(self, raw: dict) -> Order:
59
+ return _build(raw, self._r.market_outcome(raw["market_id"]))
60
+
61
+
62
+ def _build(raw: dict, mapped: Optional[Tuple[str, Outcome]]) -> Order:
63
+ market_id, outcome = mapped if mapped else (raw["market_id"], None)
64
+ return Order.model_validate({**raw, "market_id": market_id, "outcome": outcome})
65
+
66
+
67
+ def _time_in_force(value: Union[TimeInForce, str], expires_at: Optional[datetime]) -> dict:
68
+ tif = TimeInForce(value)
69
+ if tif is TimeInForce.GTD:
70
+ if expires_at is None:
71
+ raise ValueError("time_in_force=gtd requires expires_at")
72
+ return {"type": "gtd", "expires_at": expires_at.isoformat()}
73
+ return {"type": tif.value}
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from .._resolver import Resolver
4
+ from .._transport import Transport
5
+ from ..enums import OrderStatus
6
+ from ..errors import NotFoundError
7
+ from ..models import PortfolioSnapshot
8
+ from .activity import Burns, Mints, Redeems
9
+ from .markets import Markets
10
+ from .orders import Orders
11
+ from .trades import Trades
12
+
13
+ _OPEN = (OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED)
14
+
15
+
16
+ class Portfolio:
17
+ """Fetches everything needed to compute PnL in one call."""
18
+
19
+ def __init__(self, *, atomix: Transport, nexode: Transport, resolver: Resolver) -> None:
20
+ self._markets = Markets(nexode, atomix, resolver)
21
+ self._orders = Orders(atomix, resolver)
22
+ self._trades = Trades(atomix, resolver)
23
+ self._mints = Mints(nexode)
24
+ self._redeems = Redeems(nexode)
25
+ self._burns = Burns(nexode)
26
+
27
+ def snapshot(self) -> PortfolioSnapshot:
28
+ trades = self._trades.list()
29
+ open_orders = [o for o in self._orders.list() if o.status in _OPEN]
30
+ mints = self._mints.list()
31
+ redeems = self._redeems.list()
32
+ burns = self._burns.list()
33
+
34
+ market_ids = sorted(
35
+ {t.market_id for t in trades}
36
+ | {o.market_id for o in open_orders}
37
+ | {m.market_id for m in mints}
38
+ | {r.market_id for r in redeems}
39
+ )
40
+ markets = {}
41
+ for market_id in market_ids:
42
+ try:
43
+ markets[market_id] = self._markets.get(market_id)
44
+ except NotFoundError:
45
+ pass
46
+
47
+ # Tickers exist only for markets that are currently open.
48
+ tickers = {}
49
+ for trade in trades:
50
+ key = f"{trade.market_id}:{trade.outcome.value}"
51
+ if key in tickers:
52
+ continue
53
+ try:
54
+ tickers[key] = self._markets.ticker(trade.market_id, trade.outcome)
55
+ except NotFoundError:
56
+ pass
57
+
58
+ return PortfolioSnapshot(
59
+ trades=trades,
60
+ open_orders=open_orders,
61
+ mints=mints,
62
+ redeems=redeems,
63
+ burns=burns,
64
+ markets=markets,
65
+ tickers=tickers,
66
+ )
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from ..models import Token
6
+ from ._base import Resource
7
+
8
+
9
+ class Tokens(Resource):
10
+ def list(self) -> List[Token]:
11
+ body = self._t.get("/v0/tokens")
12
+ return [Token.model_validate(t) for t in body["tokens"]]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Tuple
4
+
5
+ from .._resolver import Resolver
6
+ from .._transport import Transport
7
+ from ..enums import Outcome
8
+ from ..models import Trade
9
+
10
+
11
+ class Trades:
12
+ def __init__(self, atomix: Transport, resolver: Resolver) -> None:
13
+ self._a = atomix
14
+ self._r = resolver
15
+
16
+ def list(self) -> List[Trade]:
17
+ raw = self._a.get("/v0/trades")["trades"]
18
+ resolved = self._r.resolve(r["market_id"] for r in raw)
19
+ return [_build(r, resolved.get(r["market_id"])) for r in raw]
20
+
21
+ def get(self, trade_id: str) -> Trade:
22
+ raw = self._a.get(f"/v0/trades/{trade_id}")["trade"]
23
+ return _build(raw, self._r.market_outcome(raw["market_id"]))
24
+
25
+
26
+ def _build(raw: dict, mapped: Optional[Tuple[str, Outcome]]) -> Trade:
27
+ market_id, outcome = mapped if mapped else (raw["market_id"], None)
28
+ return Trade.model_validate({**raw, "market_id": market_id, "outcome": outcome})
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexode-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Nexode prediction-market API
5
+ Project-URL: Homepage, https://nexode.io
6
+ Project-URL: Documentation, https://docs.nexode.io
7
+ Project-URL: Source, https://github.com/k2flabs/nexode
8
+ Author: Nexode
9
+ License-Expression: MIT
10
+ Keywords: nexode,prediction-markets,sdk,trading
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: pydantic>=2.7
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # nexode-sdk
20
+
21
+ Official Python SDK for the [Nexode](https://nexode.io) prediction-market API.
22
+
23
+ ```bash
24
+ pip install nexode-sdk
25
+ ```
26
+
27
+ ```python
28
+ from nexode import Nexode, Outcome, OrderSide
29
+
30
+ nx = Nexode(api_key="nxd_...") # or set NEXODE_API_KEY
31
+
32
+ market = nx.markets.list()[0]
33
+ print(nx.markets.ticker(market.id, Outcome.YES).best_bid)
34
+
35
+ order = nx.orders.create(
36
+ market_id=market.id, outcome=Outcome.YES,
37
+ side=OrderSide.BUY, price="0.55", quantity=100,
38
+ )
39
+ nx.orders.cancel(order.id)
40
+ ```
41
+
42
+ Create an API key in **Settings → Developer**. The SDK exchanges it for
43
+ short-lived access tokens and refreshes them automatically.
44
+
45
+ You work in terms of a **market** and an **outcome** (`yes`/`no`); the SDK
46
+ handles the routing. `nx.markets` and `nx.events` browse markets,
47
+ `nx.orders`/`nx.trades` trade and report fills, and `nx.mints`/`nx.redeems`/
48
+ `nx.burns` cover account activity.
49
+
50
+ Browsing is filtered and sorted server-side:
51
+
52
+ ```python
53
+ from nexode import EventSort
54
+
55
+ events = nx.events.list(
56
+ category="sports",
57
+ sort=EventSort.TRENDING, # ending / trending / new / markets / name
58
+ hide_stale_settled=True,
59
+ limit=20,
60
+ )
61
+
62
+ event = nx.events.by_slug("nba-finals") # markets too: nx.markets.by_slug(...)
63
+ ```
64
+
65
+ Outcomes carry display labels — on a game market `yes`/`no` might read
66
+ "Lakers"/"Celtics". The SDK maps both ways:
67
+
68
+ ```python
69
+ market.labels # {Outcome.YES: "Lakers", Outcome.NO: "Celtics"}
70
+ market.outcome_label(Outcome.YES) # "Lakers" (falls back to "Yes"/"No")
71
+ market.outcome_for_label("Celtics") # Outcome.NO — trade by team name
72
+ market.winning_label # label of the resolved outcome, or None
73
+ ```
74
+
75
+ `nx.api_keys` lists and revokes keys on your account; create them in
76
+ **Settings → Developer**.
77
+
78
+ `nx.portfolio.snapshot()` returns the raw inputs for PnL in one call — trades,
79
+ open orders, mints, redeems, burns, the markets involved, and current tickers:
80
+
81
+ ```python
82
+ s = nx.portfolio.snapshot()
83
+ # s.trades[i].market_id / .outcome / .price / .quantity
84
+ # s.markets[market_id].collateral_price # cost basis / resolution
85
+ # s.tickers[f"{market_id}:{outcome}"].best_bid # mark open positions
86
+ ```
87
+
88
+ See the full reference at [docs.nexode.io](https://docs.nexode.io).
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ cd nexode-sdk
94
+ pip install -e '.[dev]'
95
+ pytest
96
+ ```
@@ -0,0 +1,30 @@
1
+ nexode/__init__.py,sha256=j-0T0RNRhX23cNM2aeOP1Ux-NdS46FABHlz9484OByM,1111
2
+ nexode/_resolver.py,sha256=89l09yZbqIL_PMEea8wviF5_-17OfUupsuoZ0gi3Og8,2274
3
+ nexode/_transport.py,sha256=_X9L1fNOv1n1LrWsuFoH9Jb8n3D3QaAhPTPvN3mSLnU,1231
4
+ nexode/auth.py,sha256=nLRAjkJH0C8A9viVi1nsKQtgSlNz177xqLye5XooXsU,2450
5
+ nexode/client.py,sha256=oNNae4PQd8idUpvKZALS2sGgeVpWj4OAw1k7ZYKxoP4,2203
6
+ nexode/enums.py,sha256=YKW3HxJt_3zzGlZqHqPMtpeTJAzvwKwESpaFGCkY5Pg,781
7
+ nexode/errors.py,sha256=hRHJ6sdDxF-_GY9jleI2HkiHR6J260Ld_2LHRxGDgdM,1606
8
+ nexode/models/__init__.py,sha256=hKdJFojiUHOl93M523kY22NkhRpADf7aKK_TvCYVkZY,576
9
+ nexode/models/_base.py,sha256=9ZAttGlYJ2lMgBeAl9XIHPpx2xGuexbzTKfr_-Zf0DI,128
10
+ nexode/models/activity.py,sha256=anaQroT3ctos2p0Ltr7nc1GxrXiatyYTT0avB29YPnQ,1043
11
+ nexode/models/api_key.py,sha256=PSnxTOIBuS80AT1Ek_2fO-ERQ0aXWMmV81V690MSmuA,309
12
+ nexode/models/market.py,sha256=nWklrxT0-0BwF0CzZMV20oasjKs-PpcijCyBpdKUU6I,505
13
+ nexode/models/nexode.py,sha256=3dWnDMfVki5LVAoVU6uCbsaLWSx16fy0N2XUuZ6WFks,4550
14
+ nexode/models/order.py,sha256=eq5gPtghW4sw51kfYomTgLYWgf38P4njNydwvTD43GY,545
15
+ nexode/models/portfolio.py,sha256=ym_8obQLc16M5xuZ8BXEdGqmLv8MXOFDx6VGFJJQb-Q,768
16
+ nexode/models/token.py,sha256=GzjIgQ7Ek4gN1yCHG1_OlFdh34cLOnae6EDLHWuQ4qo,283
17
+ nexode/models/trade.py,sha256=cKoEmJSo5jxIcSsZtaL3KqLBJpk5NOnGVE7dJNiXMO4,607
18
+ nexode/resources/__init__.py,sha256=UhSxegO8PN4npROk5NiK-0OFhFBbPpv4FBaMJ-P-VTs,403
19
+ nexode/resources/_base.py,sha256=x1Q-XCzsuzA5ZhJG7mrRXac27Ems_IFpj0wOifVClT4,135
20
+ nexode/resources/activity.py,sha256=A56J-kM2af5HU5alIwS35h_XfnF_rbTEdSzQSVBM5JI,863
21
+ nexode/resources/api_keys.py,sha256=vSGd4w_qrQ91sQXyYlBPj1_GG9ScChTSpptDmxnn_x0,547
22
+ nexode/resources/events.py,sha256=s1DQ5ya6afaD-2ZUsmfMroMGscy783fcNLNEvV2C6r8,2322
23
+ nexode/resources/markets.py,sha256=CWnzt4_tta12yaGS0q6yE32IVT8_ceP1hFccU2-IsSk,2552
24
+ nexode/resources/orders.py,sha256=v_FqDPOOKtxIfYJ0L_oz8VaP19wP30QumBZm82wZV6A,2458
25
+ nexode/resources/portfolio.py,sha256=stI2iQB2aN7ZBqnjcQQzYPeju2_3QWGwooqprmjuOPM,2157
26
+ nexode/resources/tokens.py,sha256=nTXiqT1xTD9yZvdRarjSpTAcQIp2dGt9R0dIjwB89jI,283
27
+ nexode/resources/trades.py,sha256=7upx8TgKdcBdYMvR9GfwnLJfWK7mjAfEGFVJVQ-Ma1Y,964
28
+ nexode_sdk-0.1.0.dist-info/METADATA,sha256=eazWZBw1HBzOG1-N09FD20K7AkLQVShmN1Tq-o6zM9c,2901
29
+ nexode_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
30
+ nexode_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any