dominusnode 2.0.0__tar.gz → 2.0.1__tar.gz
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.
- {dominusnode-2.0.0 → dominusnode-2.0.1}/CHANGELOG.md +25 -0
- {dominusnode-2.0.0/dominusnode.egg-info → dominusnode-2.0.1}/PKG-INFO +19 -1
- {dominusnode-2.0.0 → dominusnode-2.0.1}/README.md +18 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/constants.py +1 -1
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/http_client.py +11 -3
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/resources.py +132 -8
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/types.py +49 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1/dominusnode.egg-info}/PKG-INFO +19 -1
- {dominusnode-2.0.0 → dominusnode-2.0.1}/pyproject.toml +1 -1
- {dominusnode-2.0.0 → dominusnode-2.0.1}/LICENSE +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/MANIFEST.in +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/__init__.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/_ops.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/client.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/errors.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/messages.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/pow.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/proxy_fetch.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/py.typed +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/secret.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/signer.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/ssrf.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/token_manager.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode/validation.py +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode.egg-info/SOURCES.txt +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode.egg-info/dependency_links.txt +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode.egg-info/requires.txt +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/dominusnode.egg-info/top_level.txt +0 -0
- {dominusnode-2.0.0 → dominusnode-2.0.1}/setup.cfg +0 -0
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
No breaking changes: every 2.0.0 public name and type still exists.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Team keys read the server's `prefix` field (falls back to `keyPrefix`); key lists
|
|
14
|
+
expose `userId`, `teamId` and `revokedAt`.
|
|
15
|
+
- Proxy health returns `{ status }` only, matching the server.
|
|
16
|
+
- Team invites carry `invitedBy`.
|
|
17
|
+
- Credits cursor validation matches the server (`^[1-9][0-9]{0,18}$`).
|
|
18
|
+
- 409 responses (e.g. `REPORT_LIMIT`) are never retried and surface with their `code`;
|
|
19
|
+
429 is retried at most once.
|
|
20
|
+
- A malformed `Content-Length` response header raises `ServerError` instead of being ignored.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `buyerDiscountBps` on credits balance and tier views, `buyerDiscounts` on pricing,
|
|
24
|
+
`seed`/`community` blocks on network stats.
|
|
25
|
+
- Payout wallet `chain` accepts `base-sepolia` as an explicit opt-in (server decides).
|
|
26
|
+
- Pagination helpers for credit transactions, payouts, nodes, usage records and
|
|
27
|
+
wallet transactions.
|
|
28
|
+
- README: PoW/client-IP note and a troubleshooting table.
|
|
29
|
+
|
|
30
|
+
### Deprecated
|
|
31
|
+
- The `keyPrefix` key in team-key results: use `prefix` (same value).
|
|
32
|
+
|
|
8
33
|
## [2.0.0] - 2026-11-06
|
|
9
34
|
|
|
10
35
|
Breaking: rewritten for the v2 relay-network contract (`sdks/SDK_SURFACE.md`, reference `sdks/node` 2.0.0).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dominusnode
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Official DominusNode SDK for Python: AI-agent relay network (proxy, nodes, credits, payouts)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/sdks/python
|
|
@@ -175,3 +175,21 @@ pytest
|
|
|
175
175
|
|
|
176
176
|
`tests/fixtures/signed_messages.json` holds the three signed messages as produced by the Node
|
|
177
177
|
SDK builders. `tests/test_signers.py` checks the Python builders against it byte for byte.
|
|
178
|
+
|
|
179
|
+
## Node registration and the client IP
|
|
180
|
+
|
|
181
|
+
The node-registration proof-of-work is bound to the client IP that requested it.
|
|
182
|
+
Call the register-challenge and register methods from the same egress (same machine,
|
|
183
|
+
same outbound IP, no proxy or VPN switch in between); otherwise the server rejects
|
|
184
|
+
the solution with `INVALID_CHALLENGE`. Register from the host that will run the node.
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
| Symptom | Meaning | What to do |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| HTTP 402 | API call: requester or agentic wallet balance is too low | Top up with crypto or fund the agentic wallet |
|
|
191
|
+
| HTTP 407 (proxy) | Gateway rejected the proxy credentials | Check the API key in the proxy password and the username flags (`country-XX-class-dc-...`, or `relay`) |
|
|
192
|
+
| `EMAIL_NOT_VERIFIED` | The route needs a verified account | Verify the account before financial calls |
|
|
193
|
+
| `NO_NODES_AVAILABLE` | No eligible exit node for the requested country/class | Relax the country/class flags or retry later; check network stats |
|
|
194
|
+
| `PAYOUTS_DISABLED` | Withdrawals are switched off server-side | Keep earning; the credits balance `payoutsEnabled` field shows the state |
|
|
195
|
+
| `REPORT_LIMIT` | Per-node daily session-report cap reached (HTTP 409 conflict error; older servers 429) | Do not retry today; the SDK never retries it |
|
|
@@ -157,3 +157,21 @@ pytest
|
|
|
157
157
|
|
|
158
158
|
`tests/fixtures/signed_messages.json` holds the three signed messages as produced by the Node
|
|
159
159
|
SDK builders. `tests/test_signers.py` checks the Python builders against it byte for byte.
|
|
160
|
+
|
|
161
|
+
## Node registration and the client IP
|
|
162
|
+
|
|
163
|
+
The node-registration proof-of-work is bound to the client IP that requested it.
|
|
164
|
+
Call the register-challenge and register methods from the same egress (same machine,
|
|
165
|
+
same outbound IP, no proxy or VPN switch in between); otherwise the server rejects
|
|
166
|
+
the solution with `INVALID_CHALLENGE`. Register from the host that will run the node.
|
|
167
|
+
|
|
168
|
+
## Troubleshooting
|
|
169
|
+
|
|
170
|
+
| Symptom | Meaning | What to do |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| HTTP 402 | API call: requester or agentic wallet balance is too low | Top up with crypto or fund the agentic wallet |
|
|
173
|
+
| HTTP 407 (proxy) | Gateway rejected the proxy credentials | Check the API key in the proxy password and the username flags (`country-XX-class-dc-...`, or `relay`) |
|
|
174
|
+
| `EMAIL_NOT_VERIFIED` | The route needs a verified account | Verify the account before financial calls |
|
|
175
|
+
| `NO_NODES_AVAILABLE` | No eligible exit node for the requested country/class | Relax the country/class flags or retry later; check network stats |
|
|
176
|
+
| `PAYOUTS_DISABLED` | Withdrawals are switched off server-side | Keep earning; the credits balance `payoutsEnabled` field shows the state |
|
|
177
|
+
| `REPORT_LIMIT` | Per-node daily session-report cap reached (HTTP 409 conflict error; older servers 429) | Do not retry today; the SDK never retries it |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""SDK-wide constants (mirrors sdks/node/src/constants.ts)."""
|
|
2
2
|
|
|
3
|
-
SDK_VERSION = "2.0.
|
|
3
|
+
SDK_VERSION = "2.0.1"
|
|
4
4
|
USER_AGENT = f"dominusnode-sdk-python/{SDK_VERSION}"
|
|
5
5
|
DEFAULT_BASE_URL = "https://api.dominusnode.daedalusdevelopmentgroup.com"
|
|
6
6
|
DEFAULT_PROXY_HOST = "proxy.dominusnode.daedalusdevelopmentgroup.com"
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import random
|
|
8
|
+
import re
|
|
8
9
|
import time
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from typing import Any, Callable, Dict, Optional, Union
|
|
@@ -157,11 +158,18 @@ def _content(req: Request) -> Optional[bytes]:
|
|
|
157
158
|
return None if req.body is _NO_BODY else json.dumps(req.body).encode("utf-8")
|
|
158
159
|
|
|
159
160
|
|
|
161
|
+
_CONTENT_LENGTH_RE = re.compile(r"^[0-9]{1,19}\Z")
|
|
162
|
+
|
|
163
|
+
|
|
160
164
|
def _too_large_by_header(res: httpx.Response) -> bool:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
"""True when Content-Length exceeds the cap; a malformed value raises ServerError."""
|
|
166
|
+
raw = res.headers.get("content-length")
|
|
167
|
+
if raw is None:
|
|
164
168
|
return False
|
|
169
|
+
value = raw.strip()
|
|
170
|
+
if not _CONTENT_LENGTH_RE.match(value):
|
|
171
|
+
raise ServerError("Malformed Content-Length header in response", res.status_code)
|
|
172
|
+
return int(value) > MAX_RESPONSE_BYTES
|
|
165
173
|
|
|
166
174
|
|
|
167
175
|
def _finish(req: Request, res: httpx.Response, data: bytes) -> Any:
|
|
@@ -7,7 +7,7 @@ Results are the server's JSON as dicts (camelCase keys, as on the wire); see typ
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
|
-
from typing import Any, Dict, List, Mapping, Optional
|
|
10
|
+
from typing import Any, AsyncIterator, Dict, Iterator, List, Mapping, Optional
|
|
11
11
|
|
|
12
12
|
from ._ops import Call, Gen, make_variants
|
|
13
13
|
from .constants import DEFAULT_HTTP_PROXY_PORT, DEFAULT_PROXY_HOST, DEFAULT_SOCKS5_PROXY_PORT, DEFAULT_TLS_PROXY_PORT
|
|
@@ -254,6 +254,24 @@ class _AgenticWalletResource(_Base):
|
|
|
254
254
|
|
|
255
255
|
|
|
256
256
|
# ---------------------------------------------------------------- 3.6 teams
|
|
257
|
+
def _key_prefix(r: Any) -> str:
|
|
258
|
+
"""Server sends ``prefix``; older servers sent ``keyPrefix``."""
|
|
259
|
+
if not isinstance(r, dict):
|
|
260
|
+
return ""
|
|
261
|
+
p = r.get("prefix", r.get("keyPrefix"))
|
|
262
|
+
return p if isinstance(p, str) else ""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _team_key(k: Dict[str, Any]) -> Dict[str, Any]:
|
|
266
|
+
prefix = _key_prefix(k)
|
|
267
|
+
revoked = k.get("revokedAt")
|
|
268
|
+
return {
|
|
269
|
+
"id": k.get("id"), "prefix": prefix, "keyPrefix": prefix, "label": k.get("label"),
|
|
270
|
+
"userId": k.get("userId"), "teamId": k.get("teamId"), "createdAt": k.get("createdAt"),
|
|
271
|
+
"revokedAt": revoked if isinstance(revoked, str) else None,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
257
275
|
class _TeamsResource(_Base):
|
|
258
276
|
def _t(self, team_id: str) -> str:
|
|
259
277
|
return f"/api/teams/{_id(team_id, 'team_id')}"
|
|
@@ -321,13 +339,17 @@ class _TeamsResource(_Base):
|
|
|
321
339
|
return (yield _post(f"/api/teams/invites/{_id(token, 'token')}/accept", {}))
|
|
322
340
|
|
|
323
341
|
def create_key(self, team_id: str, label: str) -> Gen:
|
|
324
|
-
"""POST /{teamId}/keys -> {id, keyPrefix, label, key: Secret}. Team keys cannot call /v1 routes."""
|
|
342
|
+
"""POST /{teamId}/keys -> {id, prefix, keyPrefix (alias), label, key: Secret}. Team keys cannot call /v1 routes."""
|
|
325
343
|
validate_non_empty(label, "label", 100)
|
|
326
344
|
r = yield _post(f"{self._t(team_id)}/keys", {"label": label})
|
|
327
|
-
|
|
345
|
+
prefix = _key_prefix(r)
|
|
346
|
+
return {**r, "prefix": prefix, "keyPrefix": prefix, "key": Secret(str(r.get("key", "")))}
|
|
328
347
|
|
|
329
348
|
def list_keys(self, team_id: str) -> Gen:
|
|
330
|
-
|
|
349
|
+
"""-> {keys: [{id, prefix, keyPrefix (alias), label, userId, teamId, createdAt, revokedAt}]}."""
|
|
350
|
+
r = yield _get(f"{self._t(team_id)}/keys")
|
|
351
|
+
keys = r.get("keys") if isinstance(r, dict) else None
|
|
352
|
+
return {"keys": [_team_key(k) for k in keys if isinstance(k, dict)] if isinstance(keys, list) else []}
|
|
331
353
|
|
|
332
354
|
def revoke_key(self, team_id: str, key_id: str) -> Gen:
|
|
333
355
|
yield _delete(f"{self._t(team_id)}/keys/{_id(key_id, 'key_id')}")
|
|
@@ -544,7 +566,7 @@ MIN_CONVERT_MUSD = 10_000
|
|
|
544
566
|
MAX_CONVERT_MUSD = 10_000_000_000
|
|
545
567
|
MIN_WITHDRAW_MUSD = 1_000_000
|
|
546
568
|
MAX_WITHDRAW_MUSD = 100_000_000_000
|
|
547
|
-
_CURSOR_RE = re.compile(r"^[0-9]{
|
|
569
|
+
_CURSOR_RE = re.compile(r"^[1-9][0-9]{0,18}\Z")
|
|
548
570
|
|
|
549
571
|
|
|
550
572
|
def _check_signed_challenge(ch: Any, signer: Any) -> Dict[str, Any]:
|
|
@@ -595,6 +617,10 @@ class _CreditsResource(_Base):
|
|
|
595
617
|
|
|
596
618
|
|
|
597
619
|
# ---------------------------------------------------------------- 3.13 payoutWallet
|
|
620
|
+
# base-sepolia is an explicit opt-in for staging; the server decides whether it is accepted.
|
|
621
|
+
PAYOUT_CHAINS = ("base", "base-sepolia")
|
|
622
|
+
|
|
623
|
+
|
|
598
624
|
class _PayoutWalletResource(_Base):
|
|
599
625
|
def get(self) -> Gen:
|
|
600
626
|
return (yield _get("/v1/payout-wallet", "agent"))
|
|
@@ -603,8 +629,8 @@ class _PayoutWalletResource(_Base):
|
|
|
603
629
|
"""Register or change the payout wallet (old wallet co-signs the SAME message when required)."""
|
|
604
630
|
assert_signer(signer)
|
|
605
631
|
addr = validate_address(address).lower()
|
|
606
|
-
if chain
|
|
607
|
-
raise invalid_arg("chain must be 'base'")
|
|
632
|
+
if chain not in PAYOUT_CHAINS:
|
|
633
|
+
raise invalid_arg("chain must be 'base' or 'base-sepolia'")
|
|
608
634
|
if old_wallet_signer is not None:
|
|
609
635
|
assert_signer(old_wallet_signer, "old_wallet_signer")
|
|
610
636
|
ch = yield _post("/v1/payout-wallet/challenge", {"address": addr, "chain": chain}, "agent")
|
|
@@ -768,7 +794,10 @@ class _ProxyResource(_Base):
|
|
|
768
794
|
return result
|
|
769
795
|
|
|
770
796
|
def get_health(self) -> Gen:
|
|
771
|
-
|
|
797
|
+
"""-> {status}. The server returns only the gateway status."""
|
|
798
|
+
r = yield _get("/api/proxy/health", "none")
|
|
799
|
+
status = r.get("status") if isinstance(r, dict) else None
|
|
800
|
+
return {"status": status if isinstance(status, str) else "unknown"}
|
|
772
801
|
|
|
773
802
|
def get_config(self) -> Gen:
|
|
774
803
|
return (yield _get("/api/proxy/config"))
|
|
@@ -789,3 +818,98 @@ CreditsResource, AsyncCreditsResource = make_variants(_CreditsResource)
|
|
|
789
818
|
PayoutWalletResource, AsyncPayoutWalletResource = make_variants(_PayoutWalletResource)
|
|
790
819
|
PayoutsResource, AsyncPayoutsResource = make_variants(_PayoutsResource)
|
|
791
820
|
ProxyResource, AsyncProxyResource = make_variants(_ProxyResource)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ---------------------------------------------------------------- pagination helpers
|
|
824
|
+
# Sync variants are generators, async variants async generators. ``limit`` is the page size.
|
|
825
|
+
_MAX_OFFSET = 1_000_000
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _offset_pager(list_method: str, items_key: str, default_limit: int, max_limit: int, total: bool = False) -> Any:
|
|
829
|
+
def _done(page: Any, items: List[Any], limit: int, offset: int) -> bool:
|
|
830
|
+
if len(items) < limit:
|
|
831
|
+
return True
|
|
832
|
+
if total and isinstance(page, dict):
|
|
833
|
+
t = (page.get("pagination") or {}).get("total")
|
|
834
|
+
if isinstance(t, int) and offset + len(items) >= t:
|
|
835
|
+
return True
|
|
836
|
+
return False
|
|
837
|
+
|
|
838
|
+
def _items(page: Any) -> List[Any]:
|
|
839
|
+
v = page.get(items_key) if isinstance(page, dict) else None
|
|
840
|
+
return v if isinstance(v, list) else []
|
|
841
|
+
|
|
842
|
+
def sync_iter(self: Any, limit: Optional[int] = None, **kwargs: Any) -> Iterator[Any]:
|
|
843
|
+
lim = validate_int_range(default_limit if limit is None else limit, "limit", 1, max_limit)
|
|
844
|
+
offset = 0
|
|
845
|
+
while offset <= _MAX_OFFSET:
|
|
846
|
+
page = getattr(self, list_method)(limit=lim, offset=offset, **kwargs)
|
|
847
|
+
items = _items(page)
|
|
848
|
+
yield from items
|
|
849
|
+
if _done(page, items, lim, offset):
|
|
850
|
+
return
|
|
851
|
+
offset += lim
|
|
852
|
+
|
|
853
|
+
async def async_iter(self: Any, limit: Optional[int] = None, **kwargs: Any) -> AsyncIterator[Any]:
|
|
854
|
+
lim = validate_int_range(default_limit if limit is None else limit, "limit", 1, max_limit)
|
|
855
|
+
offset = 0
|
|
856
|
+
while offset <= _MAX_OFFSET:
|
|
857
|
+
page = await getattr(self, list_method)(limit=lim, offset=offset, **kwargs)
|
|
858
|
+
items = _items(page)
|
|
859
|
+
for item in items:
|
|
860
|
+
yield item
|
|
861
|
+
if _done(page, items, lim, offset):
|
|
862
|
+
return
|
|
863
|
+
offset += lim
|
|
864
|
+
|
|
865
|
+
return sync_iter, async_iter
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _next_cursor(page: Any, items: List[Any], cursor: Optional[str]) -> Optional[str]:
|
|
869
|
+
nxt = page.get("nextCursor") if isinstance(page, dict) else None
|
|
870
|
+
if not items or not isinstance(nxt, str) or not _CURSOR_RE.match(nxt) or nxt == cursor:
|
|
871
|
+
return None
|
|
872
|
+
return nxt
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _credit_items(page: Any) -> List[Any]:
|
|
876
|
+
v = page.get("transactions") if isinstance(page, dict) else None
|
|
877
|
+
return v if isinstance(v, list) else []
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _credits_iter_sync(self: Any, limit: Optional[int] = None, type: Optional[str] = None, cursor: Optional[str] = None) -> Iterator[Any]: # noqa: A002
|
|
881
|
+
"""Every credit transaction (newest first), following nextCursor."""
|
|
882
|
+
while True:
|
|
883
|
+
page = self.transactions(limit=limit, cursor=cursor, type=type)
|
|
884
|
+
items = _credit_items(page)
|
|
885
|
+
yield from items
|
|
886
|
+
cursor = _next_cursor(page, items, cursor)
|
|
887
|
+
if cursor is None:
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
async def _credits_iter_async(self: Any, limit: Optional[int] = None, type: Optional[str] = None, cursor: Optional[str] = None) -> AsyncIterator[Any]: # noqa: A002
|
|
892
|
+
"""Every credit transaction (newest first), following nextCursor."""
|
|
893
|
+
while True:
|
|
894
|
+
page = await self.transactions(limit=limit, cursor=cursor, type=type)
|
|
895
|
+
items = _credit_items(page)
|
|
896
|
+
for item in items:
|
|
897
|
+
yield item
|
|
898
|
+
cursor = _next_cursor(page, items, cursor)
|
|
899
|
+
if cursor is None:
|
|
900
|
+
return
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
CreditsResource.iter_transactions = _credits_iter_sync # type: ignore[attr-defined]
|
|
904
|
+
AsyncCreditsResource.iter_transactions = _credits_iter_async # type: ignore[attr-defined]
|
|
905
|
+
for _sync_cls, _async_cls, _name, _args in (
|
|
906
|
+
(PayoutsResource, AsyncPayoutsResource, "iterate", ("list", "payouts", 50, 200)),
|
|
907
|
+
(NodesResource, AsyncNodesResource, "iterate", ("list", "nodes", 100, 500)),
|
|
908
|
+
(WalletResource, AsyncWalletResource, "iter_transactions", ("get_transactions", "transactions", 50, 100)),
|
|
909
|
+
):
|
|
910
|
+
_s, _a = _offset_pager(*_args)
|
|
911
|
+
setattr(_sync_cls, _name, _s)
|
|
912
|
+
setattr(_async_cls, _name, _a)
|
|
913
|
+
_s, _a = _offset_pager("get", "records", 200, 1000, total=True)
|
|
914
|
+
UsageResource.iter_records = _s # type: ignore[attr-defined]
|
|
915
|
+
AsyncUsageResource.iter_records = _a # type: ignore[attr-defined]
|
|
@@ -95,5 +95,54 @@ class NetworkPricing(TypedDict):
|
|
|
95
95
|
unit: str
|
|
96
96
|
prices: List[PricingEntry]
|
|
97
97
|
nodeShare: List[Dict[str, object]]
|
|
98
|
+
buyerDiscounts: List["BuyerDiscount"]
|
|
98
99
|
maturationHours: int
|
|
99
100
|
rails: List[str]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class BuyerDiscount(TypedDict):
|
|
104
|
+
tier: str
|
|
105
|
+
rank: int
|
|
106
|
+
buyerDiscountBps: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class NetworkGroupStats(TypedDict):
|
|
110
|
+
"""Online nodes and paid bytes (decimal string) in the last 24h for one operator group."""
|
|
111
|
+
|
|
112
|
+
online: int
|
|
113
|
+
paidBytes24h: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class NetworkStats(TypedDict):
|
|
117
|
+
onlineNodes: int
|
|
118
|
+
byClass: Dict[str, int]
|
|
119
|
+
byCountry: List[Dict[str, object]]
|
|
120
|
+
otherCountries: int
|
|
121
|
+
seed: NetworkGroupStats
|
|
122
|
+
community: NetworkGroupStats
|
|
123
|
+
generatedAt: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TeamKey(TypedDict):
|
|
127
|
+
id: str
|
|
128
|
+
prefix: str
|
|
129
|
+
keyPrefix: str
|
|
130
|
+
label: str
|
|
131
|
+
userId: str
|
|
132
|
+
teamId: str
|
|
133
|
+
createdAt: str
|
|
134
|
+
revokedAt: Optional[str]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TeamInvite(TypedDict):
|
|
138
|
+
id: str
|
|
139
|
+
teamId: str
|
|
140
|
+
email: str
|
|
141
|
+
role: str
|
|
142
|
+
invitedBy: str
|
|
143
|
+
expiresAt: str
|
|
144
|
+
createdAt: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ProxyHealth(TypedDict):
|
|
148
|
+
status: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dominusnode
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Official DominusNode SDK for Python: AI-agent relay network (proxy, nodes, credits, payouts)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/sdks/python
|
|
@@ -175,3 +175,21 @@ pytest
|
|
|
175
175
|
|
|
176
176
|
`tests/fixtures/signed_messages.json` holds the three signed messages as produced by the Node
|
|
177
177
|
SDK builders. `tests/test_signers.py` checks the Python builders against it byte for byte.
|
|
178
|
+
|
|
179
|
+
## Node registration and the client IP
|
|
180
|
+
|
|
181
|
+
The node-registration proof-of-work is bound to the client IP that requested it.
|
|
182
|
+
Call the register-challenge and register methods from the same egress (same machine,
|
|
183
|
+
same outbound IP, no proxy or VPN switch in between); otherwise the server rejects
|
|
184
|
+
the solution with `INVALID_CHALLENGE`. Register from the host that will run the node.
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
| Symptom | Meaning | What to do |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| HTTP 402 | API call: requester or agentic wallet balance is too low | Top up with crypto or fund the agentic wallet |
|
|
191
|
+
| HTTP 407 (proxy) | Gateway rejected the proxy credentials | Check the API key in the proxy password and the username flags (`country-XX-class-dc-...`, or `relay`) |
|
|
192
|
+
| `EMAIL_NOT_VERIFIED` | The route needs a verified account | Verify the account before financial calls |
|
|
193
|
+
| `NO_NODES_AVAILABLE` | No eligible exit node for the requested country/class | Relax the country/class flags or retry later; check network stats |
|
|
194
|
+
| `PAYOUTS_DISABLED` | Withdrawals are switched off server-side | Keep earning; the credits balance `payoutsEnabled` field shows the state |
|
|
195
|
+
| `REPORT_LIMIT` | Per-node daily session-report cap reached (HTTP 409 conflict error; older servers 429) | Do not retry today; the SDK never retries it |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dominusnode"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.1"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
description = "Official DominusNode SDK for Python: AI-agent relay network (proxy, nodes, credits, payouts)"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|