dominusnode-camel 2.0.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.
@@ -0,0 +1,15 @@
1
+ """CAMEL-AI tools for the DominusNode AI-agent relay network (v2).
2
+
3
+ Example::
4
+
5
+ from dominusnode_camel import DominusNodeToolkit
6
+
7
+ tools = DominusNodeToolkit().get_tools() # reads DOMINUSNODE_API_KEY
8
+ """
9
+
10
+ from ._core import TOOL_NAMES, DominusNodeTools
11
+ from .tools import DominusNodeToolkit
12
+
13
+ __version__ = "2.0.0"
14
+
15
+ __all__ = ["DominusNodeToolkit", "DominusNodeTools", "TOOL_NAMES", "__version__"]
@@ -0,0 +1,454 @@
1
+ """Framework-neutral DominusNode v2 tool set, built on the ``dominusnode`` SDK.
2
+
3
+ Every framework adapter in this package wraps :class:`DominusNodeTools`. The 19
4
+ methods named in :data:`TOOL_NAMES` are the standard v2 integration tools. All
5
+ HTTP, SSRF/DNS-rebinding checks, gateway username building and validation live
6
+ in the SDK; this module only adapts arguments and formats results for an LLM.
7
+
8
+ Security properties kept here:
9
+
10
+ - The API key comes from ``DOMINUSNODE_API_KEY`` or constructor config, never
11
+ from a tool argument, and is never returned in a tool output.
12
+ - Every result and error string is passed through the SDK's
13
+ ``scrub_credentials`` (dn_live_/dn_test_/dn_node_/dn_proxy_ keys and JWTs).
14
+ - Signing flows (node register, withdraw, payout wallet) are not exposed.
15
+ """
16
+
17
+ import inspect
18
+ import json
19
+ import os
20
+ import re
21
+ from typing import Any, Callable, Dict, List, Optional, Tuple, get_type_hints
22
+
23
+ from dominusnode import (
24
+ DEFAULT_BASE_URL,
25
+ DEFAULT_HTTP_PROXY_PORT,
26
+ DEFAULT_PROXY_HOST,
27
+ DEFAULT_SOCKS5_PROXY_PORT,
28
+ DominusNodeClient,
29
+ json_default,
30
+ scrub_credentials,
31
+ )
32
+
33
+ API_KEY_ENV = "DOMINUSNODE_API_KEY"
34
+ BASE_URL_ENV = "DOMINUSNODE_BASE_URL"
35
+ PROXY_HOST_ENV = "DOMINUSNODE_PROXY_HOST"
36
+ PROXY_PORT_ENV = "DOMINUSNODE_PROXY_PORT"
37
+
38
+ MAX_BODY_CHARS = 100_000
39
+ DEFAULT_BODY_CHARS = 4_000
40
+ KEY_PLACEHOLDER = "<DOMINUSNODE_API_KEY>"
41
+
42
+ TOOL_NAMES: Tuple[str, ...] = (
43
+ "dominusnode_fetch",
44
+ "dominusnode_proxy_url",
45
+ "dominusnode_network_policy",
46
+ "dominusnode_network_pricing",
47
+ "dominusnode_network_stats",
48
+ "dominusnode_balance",
49
+ "dominusnode_transactions",
50
+ "dominusnode_topup_crypto",
51
+ "dominusnode_check_payment",
52
+ "dominusnode_usage",
53
+ "dominusnode_active_sessions",
54
+ "dominusnode_report_session",
55
+ "dominusnode_node_list",
56
+ "dominusnode_node_tier",
57
+ "dominusnode_node_setup_guide",
58
+ "dominusnode_credits_balance",
59
+ "dominusnode_credits_convert",
60
+ "dominusnode_payouts_list",
61
+ "dominusnode_x402_info",
62
+ )
63
+
64
+ NODE_SETUP_GUIDE = """DominusNode relay node setup (earn by contributing an IP)
65
+
66
+ How earning works
67
+ - Your node dials OUT to the gateway (no inbound ports) and relays other agents' traffic.
68
+ - You earn your tier's share of each paid GB relayed; live prices and shares are in
69
+ dominusnode_network_pricing. Earnings mature before they become available.
70
+ - Self-dealing earns nothing: your own, your wallet's, your team's and your /24's traffic is
71
+ never routed to your node.
72
+ - Tiers rise with paid tenure, uptime, active IPs and reputation (see dominusnode_node_tier).
73
+ - The node floor is fixed: ports 80/443 only, never private/loopback/metadata targets,
74
+ connection metadata only (no payload logging). See dominusnode_network_policy.
75
+
76
+ Prerequisites
77
+ - A wallet-linked account and a personal dn_live_ API key (not a team key).
78
+ - Node.js >= 18 and git on the machine whose IP you contribute.
79
+ - IPs in sanctioned countries (Cuba, Iran, North Korea, Russia, Syria) cannot join.
80
+
81
+ Step 1: Store the owner wallet key in a private file (never pass it as an argument or to an LLM)
82
+ umask 077 && printf '%s' "$WALLET_PRIVATE_KEY_HEX" > ~/.dominusnode/wallet.key
83
+
84
+ Step 2: Register the node with the node agent CLI (signing happens locally, not through this tool)
85
+ git clone https://github.com/0xcircuitbreaker/DomiNode.git && cd DomiNode
86
+ npm ci && npm run build -w @dominusnode/shared -w @dominusnode/node-agent
87
+ export DN_API_KEY # your dn_live_ key, from the environment only
88
+ node packages/node-agent/dist/src/main.js register --wallet-key-file ~/.dominusnode/wallet.key
89
+ (Alternatively use the DominusNode MCP server or SDK nodes.register with a signer key file.)
90
+
91
+ Step 3: Run the node agent on the machine that owns the IP
92
+ node packages/node-agent/dist/src/main.js run --key-file ~/.dominusnode/node.json
93
+ Optional limits (can only tighten): --ports 443 --max-streams 32 --gb-per-day 5 --req-per-sec 20
94
+ Keep it running (systemd, pm2, or a container restart policy): uptime drives your tier.
95
+
96
+ Step 4: Verify
97
+ dominusnode_node_list shows status and last-seen time once the tunnel is up.
98
+ """
99
+
100
+
101
+ def _env_int(name: str, default: int) -> int:
102
+ raw = os.environ.get(name)
103
+ if not raw:
104
+ return default
105
+ try:
106
+ v = int(raw)
107
+ except ValueError:
108
+ return default
109
+ return v if 1 <= v <= 65535 else default
110
+
111
+
112
+ _JSON_SCALARS = (dict, list, tuple, str, int, float, bool, type(None))
113
+
114
+
115
+ def _json_default(obj: object) -> object:
116
+ """SDK ``json_default`` (Secret -> [REDACTED]) with a ``str`` fallback for anything else."""
117
+ try:
118
+ out = json_default(obj)
119
+ except TypeError:
120
+ return str(obj)
121
+ return out if isinstance(out, _JSON_SCALARS) else str(out)
122
+
123
+
124
+ class DominusNodeTools:
125
+ """The 19 standard DominusNode v2 tools as plain Python methods returning strings.
126
+
127
+ Args:
128
+ api_key: Optional config value; defaults to the ``DOMINUSNODE_API_KEY`` env var.
129
+ Never accepted as a tool argument.
130
+ base_url: API base URL (default ``DOMINUSNODE_BASE_URL`` or the public API).
131
+ proxy_host: Gateway host for ``dominusnode_proxy_url``.
132
+ http_proxy_port: Gateway HTTP port.
133
+ client: A pre-built ``dominusnode.DominusNodeClient`` (already connected or not).
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ api_key: Optional[str] = None,
139
+ base_url: Optional[str] = None,
140
+ proxy_host: Optional[str] = None,
141
+ http_proxy_port: Optional[int] = None,
142
+ client: Any = None,
143
+ ) -> None:
144
+ self.__key = api_key or os.environ.get(API_KEY_ENV) or None
145
+ self._base_url = base_url or os.environ.get(BASE_URL_ENV) or DEFAULT_BASE_URL
146
+ self._proxy_host = proxy_host or os.environ.get(PROXY_HOST_ENV) or DEFAULT_PROXY_HOST
147
+ self._http_port = http_proxy_port or _env_int(PROXY_PORT_ENV, DEFAULT_HTTP_PROXY_PORT)
148
+ self._client = client
149
+ self._connected = client is not None and getattr(client, "is_connected", False) is True
150
+
151
+ def __repr__(self) -> str:
152
+ return f"DominusNodeTools(configured={'true' if self.__key or self._client else 'false'})"
153
+
154
+ __str__ = __repr__
155
+
156
+ def __reduce__(self) -> Any:
157
+ raise TypeError("DominusNodeTools cannot be pickled")
158
+
159
+ # -- plumbing ---------------------------------------------------------
160
+
161
+ def _sdk(self) -> Any:
162
+ if self._client is None:
163
+ self._client = DominusNodeClient(
164
+ base_url=self._base_url,
165
+ proxy_host=self._proxy_host,
166
+ http_proxy_port=self._http_port,
167
+ socks5_proxy_port=DEFAULT_SOCKS5_PROXY_PORT,
168
+ )
169
+ if not self._connected:
170
+ if not self.__key:
171
+ raise RuntimeError(f"DominusNode API key not configured: set {API_KEY_ENV}")
172
+ self._client.connect_with_key(self.__key)
173
+ self._connected = True
174
+ return self._client
175
+
176
+ def _clean(self, text: str) -> str:
177
+ out = scrub_credentials(text)
178
+ if self.__key and len(self.__key) >= 8:
179
+ out = out.replace(self.__key, "[REDACTED]")
180
+ return out
181
+
182
+ def _ok(self, value: Any) -> str:
183
+ if isinstance(value, str):
184
+ return self._clean(value)
185
+ return self._clean(json.dumps(value, default=_json_default, indent=2, sort_keys=True))
186
+
187
+ def _err(self, exc: BaseException) -> str:
188
+ return "Error: " + self._clean(f"{type(exc).__name__}: {exc}")
189
+
190
+ def _call(self, fn: Callable[[Any], Any]) -> str:
191
+ try:
192
+ return self._ok(fn(self._sdk()))
193
+ except Exception as exc: # noqa: BLE001 - every error is returned scrubbed to the model
194
+ return self._err(exc)
195
+
196
+ def close(self) -> None:
197
+ if self._client is not None:
198
+ try:
199
+ self._client.disconnect()
200
+ except Exception: # noqa: BLE001
201
+ pass
202
+ self._connected = False
203
+
204
+ # -- 1-2 proxy ---------------------------------------------------------
205
+
206
+ def dominusnode_fetch(
207
+ self,
208
+ url: str,
209
+ method: str = "GET",
210
+ country: Optional[str] = None,
211
+ ip_class: Optional[str] = None,
212
+ session_id: Optional[str] = None,
213
+ allow_http: bool = False,
214
+ max_chars: int = DEFAULT_BODY_CHARS,
215
+ ) -> str:
216
+ """Fetch a public URL through the DominusNode relay network of contributed exit IPs.
217
+
218
+ Billed per GB at the live price from dominusnode_network_pricing. GET or HEAD only, ports
219
+ 80/443, private/local targets refused, redirects returned (not followed), 10 MB cap.
220
+ HTTPS is end-to-end encrypted; plaintext http:// needs allow_http=true and is visible to
221
+ the exit node, so never send secrets over it.
222
+
223
+ Args:
224
+ url: Absolute https:// (or http:// with allow_http) URL to fetch.
225
+ method: GET or HEAD.
226
+ country: Optional ISO-2 exit country, e.g. US or DE (sanctioned countries refused).
227
+ ip_class: Optional exit class: residential, dc or mobile.
228
+ session_id: Optional sticky-session id (1-64 letters/digits) to keep the same exit.
229
+ allow_http: Opt in to plaintext http:// (exit-visible).
230
+ max_chars: Maximum body characters returned to you (1-100000, default 4000).
231
+ """
232
+
233
+ def run(dn: Any) -> Any:
234
+ limit = max(1, min(int(max_chars), MAX_BODY_CHARS))
235
+ r = dn.proxy.fetch(
236
+ url,
237
+ method=str(method).upper(),
238
+ country=country,
239
+ ip_class=ip_class,
240
+ session_id=session_id,
241
+ allow_http=bool(allow_http),
242
+ )
243
+ body = r.text()
244
+ keep = ("content-type", "content-length", "location", "retry-after", "last-modified")
245
+ return {
246
+ "status": r.status,
247
+ "statusText": r.status_text,
248
+ "headers": {k: v for k, v in r.headers.items() if k.lower() in keep},
249
+ "body": body[:limit],
250
+ "bodyTruncated": bool(r.truncated) or len(body) > limit,
251
+ }
252
+
253
+ return self._call(run)
254
+
255
+ def dominusnode_proxy_url(
256
+ self,
257
+ country: Optional[str] = None,
258
+ ip_class: Optional[str] = None,
259
+ session_id: Optional[str] = None,
260
+ allow_http: bool = False,
261
+ ) -> str:
262
+ """Build the gateway proxy URL for routing your own HTTP client through the relay network.
263
+
264
+ Routing flags go in the proxy username (hyphenated, e.g. country-US-class-dc); the API key
265
+ goes in the password. The key is NOT returned: the URL contains a placeholder that your
266
+ runtime substitutes from the DOMINUSNODE_API_KEY environment variable.
267
+
268
+ Args:
269
+ country: Optional ISO-2 exit country (sanctioned countries refused).
270
+ ip_class: Optional exit class: residential, dc or mobile.
271
+ session_id: Optional sticky-session id (1-64 letters/digits).
272
+ allow_http: Allow plaintext http:// targets (visible to the exit node).
273
+ """
274
+
275
+ def run(dn: Any) -> Any:
276
+ user = dn.proxy.build_username(
277
+ country=country, ip_class=ip_class, session_id=session_id, allow_http=bool(allow_http)
278
+ )
279
+ return {
280
+ "username": user,
281
+ "proxyUrl": f"http://{user}:{KEY_PLACEHOLDER}@{self._proxy_host}:{self._http_port}",
282
+ "note": f"Replace {KEY_PLACEHOLDER} with the value of ${API_KEY_ENV}; never log the real URL.",
283
+ }
284
+
285
+ return self._call(run)
286
+
287
+ # -- 3-5 network -------------------------------------------------------
288
+
289
+ def dominusnode_network_policy(self) -> str:
290
+ """Show the relay network policy: allowed ports, blocked targets, sanctioned countries, logging rules."""
291
+ return self._call(lambda dn: dn.network.policy())
292
+
293
+ def dominusnode_network_pricing(self) -> str:
294
+ """Show live per-GB prices by IP class (in µUSD; 1 USD = 1,000,000 µUSD) and node earning shares.
295
+
296
+ Always use this instead of assuming prices.
297
+ """
298
+ return self._call(lambda dn: dn.network.pricing())
299
+
300
+ def dominusnode_network_stats(self) -> str:
301
+ """Show relay network statistics: online nodes, countries and IP classes available."""
302
+ return self._call(lambda dn: dn.network.stats())
303
+
304
+ # -- 6-9 wallet --------------------------------------------------------
305
+
306
+ def dominusnode_balance(self) -> str:
307
+ """Show your prepaid wallet balance used to pay for relay traffic."""
308
+ return self._call(lambda dn: dn.wallet.get_balance())
309
+
310
+ def dominusnode_transactions(self, limit: int = 20, offset: int = 0) -> str:
311
+ """List recent wallet transactions (top-ups and usage charges).
312
+
313
+ Args:
314
+ limit: Number of transactions to return (1-100).
315
+ offset: Number of transactions to skip.
316
+ """
317
+ return self._call(lambda dn: dn.wallet.get_transactions(limit=limit, offset=offset))
318
+
319
+ def dominusnode_topup_crypto(self, amount_cents: int, currency: str) -> str:
320
+ """Create a crypto invoice to top up your wallet ($10-$1,000).
321
+
322
+ Returns a payment address/invoice; check it with dominusnode_check_payment.
323
+
324
+ Args:
325
+ amount_cents: Amount in US cents, 1000 to 100000.
326
+ currency: One of btc, eth, ltc, xmr, zec, usdc, sol, usdt, dai, bnb, link.
327
+ """
328
+ return self._call(lambda dn: dn.wallet.top_up_crypto(amount_cents, currency))
329
+
330
+ def dominusnode_check_payment(self, invoice_id: str) -> str:
331
+ """Check the status of a crypto top-up invoice.
332
+
333
+ Args:
334
+ invoice_id: Invoice UUID returned by dominusnode_topup_crypto.
335
+ """
336
+ return self._call(lambda dn: dn.wallet.get_crypto_status(invoice_id))
337
+
338
+ # -- 10-12 usage and sessions -----------------------------------------
339
+
340
+ def dominusnode_usage(self, since: Optional[str] = None, until: Optional[str] = None) -> str:
341
+ """Summarise your relay bandwidth usage and cost.
342
+
343
+ Args:
344
+ since: Optional ISO-8601 start date/time.
345
+ until: Optional ISO-8601 end date/time.
346
+ """
347
+ return self._call(lambda dn: dn.usage.get(from_=since, to=until))
348
+
349
+ def dominusnode_active_sessions(self) -> str:
350
+ """List your active proxy sessions (id, start time, status; exit IPs are never shown)."""
351
+ return self._call(lambda dn: dn.sessions.get_active())
352
+
353
+ def dominusnode_report_session(self, session_id: str, reason: str) -> str:
354
+ """Report a bad relay session so the exit node's reputation is updated.
355
+
356
+ Args:
357
+ session_id: Session UUID from dominusnode_active_sessions.
358
+ reason: One of blocked, captcha, rate_limited, slow, bad_content, other.
359
+ """
360
+ return self._call(lambda dn: dn.sessions.report(session_id, reason))
361
+
362
+ # -- 13-15 nodes -------------------------------------------------------
363
+
364
+ def dominusnode_node_list(self, limit: int = 50, offset: int = 0) -> str:
365
+ """List the relay nodes you contribute (status, class, last seen).
366
+
367
+ Args:
368
+ limit: Number of nodes to return (1-500).
369
+ offset: Number of nodes to skip.
370
+ """
371
+ return self._call(lambda dn: dn.nodes.list(limit=limit, offset=offset))
372
+
373
+ def dominusnode_node_tier(self) -> str:
374
+ """Show your node-operator tier, earning share and progress to the next tier."""
375
+ return self._call(lambda dn: dn.nodes.tier())
376
+
377
+ def dominusnode_node_setup_guide(self) -> str:
378
+ """Explain how to earn by contributing an IP: run the DominusNode node agent, step by step."""
379
+ return self._ok(NODE_SETUP_GUIDE)
380
+
381
+ # -- 16-18 earnings ----------------------------------------------------
382
+
383
+ def dominusnode_credits_balance(self) -> str:
384
+ """Show node-operator credits earned by relaying traffic (µUSD; pending and available)."""
385
+ return self._call(lambda dn: dn.credits.balance())
386
+
387
+ def dominusnode_credits_convert(self, amount_musd: str, idempotency_key: Optional[str] = None) -> str:
388
+ """Convert available earned credits into wallet balance to spend on relay traffic.
389
+
390
+ Args:
391
+ amount_musd: Amount in µUSD as a digit string (10000 = $0.01; min 10000).
392
+ idempotency_key: Optional key from a previous attempt, to retry safely.
393
+ """
394
+ return self._call(lambda dn: dn.credits.convert(amount_musd, idempotency_key=idempotency_key))
395
+
396
+ def dominusnode_payouts_list(self, limit: int = 20, offset: int = 0) -> str:
397
+ """List your on-chain payouts (withdrawals of earned credits). Withdrawing needs a signer, not this tool.
398
+
399
+ Args:
400
+ limit: Number of payouts to return (1-200).
401
+ offset: Number of payouts to skip.
402
+ """
403
+ return self._call(lambda dn: dn.payouts.list(limit=limit, offset=offset))
404
+
405
+ # -- 19 x402 -----------------------------------------------------------
406
+
407
+ def dominusnode_x402_info(self) -> str:
408
+ """Show x402 (HTTP 402 micropayment) support details for paying per request."""
409
+ return self._call(lambda dn: dn.x402.get_info())
410
+
411
+ # -- helpers for adapters ---------------------------------------------
412
+
413
+ def tool_functions(self) -> List[Callable[..., str]]:
414
+ """The 19 bound tool methods, in standard order."""
415
+ return [getattr(self, n) for n in TOOL_NAMES]
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # Docstring helpers shared by adapters
420
+ # ---------------------------------------------------------------------------
421
+
422
+ _ARGS_RE = re.compile(r"^\s{4,}(\w+):\s*(.+)$")
423
+
424
+
425
+ def describe(fn: Callable[..., Any]) -> str:
426
+ """Tool description: the docstring without its Args section."""
427
+ doc = inspect.getdoc(fn) or ""
428
+ return doc.split("\nArgs:")[0].strip()
429
+
430
+
431
+ def arg_docs(fn: Callable[..., Any]) -> Dict[str, str]:
432
+ doc = fn.__doc__ or ""
433
+ if "Args:" not in doc:
434
+ return {}
435
+ out: Dict[str, str] = {}
436
+ for line in doc.split("Args:", 1)[1].splitlines():
437
+ m = _ARGS_RE.match(line)
438
+ if m:
439
+ out[m.group(1)] = m.group(2).strip()
440
+ return out
441
+
442
+
443
+ def args_model(fn: Callable[..., Any], model_name: str) -> Any:
444
+ """Build a pydantic model for a tool method's arguments (for schema-driven frameworks)."""
445
+ from pydantic import Field, create_model
446
+
447
+ docs = arg_docs(fn)
448
+ hints = get_type_hints(fn)
449
+ fields: Dict[str, Any] = {}
450
+ for name, p in inspect.signature(fn).parameters.items():
451
+ ann = hints.get(name, str)
452
+ default = ... if p.default is inspect.Parameter.empty else p.default
453
+ fields[name] = (ann, Field(default, description=docs.get(name, name)))
454
+ return create_model(model_name, **fields)
@@ -0,0 +1,40 @@
1
+ """CAMEL-AI toolkit exposing the 19 standard DominusNode v2 tools."""
2
+
3
+ from typing import Any, List, Optional
4
+
5
+ from camel.toolkits import BaseToolkit, FunctionTool
6
+
7
+ from ._core import DominusNodeTools
8
+
9
+
10
+ class DominusNodeToolkit(BaseToolkit):
11
+ """DominusNode relay-network tools for CAMEL agents.
12
+
13
+ The API key is read from ``DOMINUSNODE_API_KEY`` (or passed as config); it is never a
14
+ tool argument and never appears in tool output.
15
+
16
+ Example::
17
+
18
+ from camel.agents import ChatAgent
19
+ agent = ChatAgent(system_message="...", tools=DominusNodeToolkit().get_tools())
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: Optional[str] = None,
25
+ base_url: Optional[str] = None,
26
+ proxy_host: Optional[str] = None,
27
+ http_proxy_port: Optional[int] = None,
28
+ client: Any = None,
29
+ timeout: Optional[float] = None,
30
+ ) -> None:
31
+ super().__init__(timeout=timeout)
32
+ self._core = DominusNodeTools(
33
+ api_key=api_key, base_url=base_url, proxy_host=proxy_host, http_proxy_port=http_proxy_port, client=client
34
+ )
35
+
36
+ def get_tools(self) -> List[FunctionTool]:
37
+ return [FunctionTool(fn) for fn in self._core.tool_functions()]
38
+
39
+ def __repr__(self) -> str:
40
+ return "DominusNodeToolkit()"
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: dominusnode-camel
3
+ Version: 2.0.0
4
+ Summary: CAMEL-AI toolkit for the DominusNode AI-agent relay network (19 v2 tools on the dominusnode SDK)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/camel-ai
7
+ Project-URL: Repository, https://github.com/0xcircuitbreaker/DomiNode
8
+ Project-URL: Issues, https://github.com/0xcircuitbreaker/DomiNode/issues
9
+ Project-URL: Changelog, https://github.com/0xcircuitbreaker/DomiNode/blob/master/integrations/camel-ai/CHANGELOG.md
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: camel-ai>=0.2.0
14
+ Requires-Dist: mcp<2
15
+ Requires-Dist: dominusnode<3,>=2.0.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # dominusnode-camel
21
+
22
+ CAMEL-AI tools for the **DominusNode AI-agent relay network** (v2). Agents fetch the web through
23
+ exit IPs contributed by other operators, pay per GB from a prepaid wallet (live prices from
24
+ `dominusnode_network_pricing`, nothing hardcoded), and can earn by contributing their own IP
25
+ with the DominusNode node agent.
26
+
27
+ Built on the official [`dominusnode`](../../sdks/python) Python SDK v2: all HTTP, SSRF and
28
+ DNS-rebinding checks, gateway username flags and validation come from the SDK.
29
+
30
+ ```bash
31
+ pip install dominusnode-camel
32
+ export DOMINUSNODE_API_KEY=dn_live_... # read from the environment, never a tool argument
33
+ ```
34
+
35
+ Defaults: API `https://api.dominusnode.daedalusdevelopmentgroup.com`, relay gateway `proxy.dominusnode.daedalusdevelopmentgroup.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`.
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from camel.agents import ChatAgent
41
+ from dominusnode_camel import DominusNodeToolkit
42
+
43
+ agent = ChatAgent(
44
+ system_message="You browse through the DominusNode relay network. Check pricing first.",
45
+ tools=DominusNodeToolkit().get_tools(), # 19 camel FunctionTools
46
+ )
47
+ print(agent.step("Fetch https://example.com from a residential exit in the US").msgs[0].content)
48
+ ```
49
+
50
+ `DominusNodeToolkit` is a `camel.toolkits.BaseToolkit`; each tool is a `FunctionTool` whose
51
+ OpenAI schema comes from the method signature and Google-style docstring. Errors come back
52
+ as scrubbed `Error: ...` strings.
53
+
54
+ ## The 19 tools
55
+
56
+ | # | Tool | What it does | SDK call |
57
+ |---|---|---|---|
58
+ | 1 | `dominusnode_fetch` | Fetch a public URL through the relay (GET/HEAD; country, ip_class, session_id, allow_http) | `proxy.fetch` |
59
+ | 2 | `dominusnode_proxy_url` | Gateway proxy URL/username for your own client (key shown as a placeholder) | `proxy.build_username` |
60
+ | 3 | `dominusnode_network_policy` | Relay policy: ports, blocked targets, sanctioned countries | `network.policy` |
61
+ | 4 | `dominusnode_network_pricing` | Live per-GB prices by IP class and node earning shares | `network.pricing` |
62
+ | 5 | `dominusnode_network_stats` | Online nodes, countries, IP classes | `network.stats` |
63
+ | 6 | `dominusnode_balance` | Prepaid wallet balance | `wallet.get_balance` |
64
+ | 7 | `dominusnode_transactions` | Wallet transactions | `wallet.get_transactions` |
65
+ | 8 | `dominusnode_topup_crypto` | Crypto top-up invoice ($10-$1,000) | `wallet.top_up_crypto` |
66
+ | 9 | `dominusnode_check_payment` | Crypto invoice status | `wallet.get_crypto_status` |
67
+ | 10 | `dominusnode_usage` | Bandwidth usage and cost | `usage.get` |
68
+ | 11 | `dominusnode_active_sessions` | Active sessions (no IPs) | `sessions.get_active` |
69
+ | 12 | `dominusnode_report_session` | Report a bad session | `sessions.report` |
70
+ | 13 | `dominusnode_node_list` | Your relay nodes | `nodes.list` |
71
+ | 14 | `dominusnode_node_tier` | Node-operator tier and share | `nodes.tier` |
72
+ | 15 | `dominusnode_node_setup_guide` | Steps to earn by running the node agent | `static text` |
73
+ | 16 | `dominusnode_credits_balance` | Earned node credits | `credits.balance` |
74
+ | 17 | `dominusnode_credits_convert` | Convert credits into wallet balance | `credits.convert` |
75
+ | 18 | `dominusnode_payouts_list` | On-chain payouts | `payouts.list` |
76
+ | 19 | `dominusnode_x402_info` | x402 micropayment info | `x402.get_info` |
77
+
78
+ ## Security
79
+
80
+ - The API key comes from `DOMINUSNODE_API_KEY` (or constructor config). It is never a tool
81
+ argument, never in a tool schema and never in tool output; `dominusnode_proxy_url` returns a
82
+ `<DOMINUSNODE_API_KEY>` placeholder.
83
+ - Every result and error is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
84
+ - `dominusnode_fetch` (via the SDK): GET/HEAD only, ports 80/443, private/local hosts and
85
+ DNS-rebinding refused, redirects not followed, 10 MB cap, sanctioned countries refused.
86
+ Plaintext `http://` needs `allow_http=true` and is visible to the exit node.
87
+ - Signed flows (node registration, withdrawals, payout wallet) are not LLM tools: use the
88
+ DominusNode MCP server, the node agent, or the SDK with a signer key file.
89
+
90
+ Configuration env vars: `DOMINUSNODE_API_KEY`, `DOMINUSNODE_BASE_URL`, `DOMINUSNODE_PROXY_HOST`,
91
+ `DOMINUSNODE_PROXY_PORT`.
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ pip install -e ../../sdks/python -e '.[dev]'
97
+ pytest
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,8 @@
1
+ dominusnode_camel/__init__.py,sha256=3jyyD3BadWHsRMLYWTbEUsXKDJmvDSrIES6b5ZIhLaY,405
2
+ dominusnode_camel/_core.py,sha256=cNkqLQWg65e2M5qLj3rxizCm5vYyoOcr44b0TA04T8k,18743
3
+ dominusnode_camel/tools.py,sha256=AMsjKna4k9UoGfoIPZORORLj_2_g0BX57TbvOZQR-xw,1281
4
+ dominusnode_camel-2.0.0.dist-info/licenses/LICENSE,sha256=NBkjfgN0tpbJjWf7uqsPCOMgq7uuSEW4uaGDCpB8HbI,1069
5
+ dominusnode_camel-2.0.0.dist-info/METADATA,sha256=sLZaLehiRF5vJe8TWsXu6XReeMGYPSgenh5HhYXFu04,5083
6
+ dominusnode_camel-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ dominusnode_camel-2.0.0.dist-info/top_level.txt,sha256=R6E1oZSpv_alMwXWrUDcdX1cOv4x_njR4vSdRmF4GU4,18
8
+ dominusnode_camel-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominus Node
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dominusnode_camel