ghostnexus-node 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.
@@ -0,0 +1,2 @@
1
+ """GhostNexus Provider Node — earn money with your idle GPU."""
2
+ __version__ = "0.1.0"
ghostnexus_node/cli.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ GhostNexus Node CLI — ghostnexus-node start --api-key gn_live_...
3
+ """
4
+ import argparse
5
+ import asyncio
6
+ import os
7
+ import sys
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(
12
+ prog="ghostnexus-node",
13
+ description="GhostNexus Provider Node — earn money with your idle GPU",
14
+ )
15
+ subparsers = parser.add_subparsers(dest="command")
16
+
17
+ # start command
18
+ start_parser = subparsers.add_parser("start", help="Start the provider node")
19
+ start_parser.add_argument(
20
+ "--api-key",
21
+ default=os.environ.get("GHOSTNEXUS_PROVIDER_API_KEY", ""),
22
+ help="Your GhostNexus provider API key (gn_live_...)",
23
+ )
24
+ start_parser.add_argument(
25
+ "--server",
26
+ default="wss://ghostnexus.net/ws/provider",
27
+ help="GhostNexus server WebSocket URL",
28
+ )
29
+ start_parser.add_argument(
30
+ "--gpu-passthrough",
31
+ action="store_true",
32
+ default=True,
33
+ help="Pass GPU to Docker containers (default: True)",
34
+ )
35
+ start_parser.add_argument(
36
+ "--memory",
37
+ default="8g",
38
+ help="Memory limit per job (default: 8g)",
39
+ )
40
+ start_parser.add_argument(
41
+ "--cpus",
42
+ default="4.0",
43
+ help="CPU limit per job (default: 4.0)",
44
+ )
45
+
46
+ args = parser.parse_args()
47
+
48
+ if args.command == "start":
49
+ if not args.api_key:
50
+ print(
51
+ "Error: API key required.\n"
52
+ " ghostnexus-node start --api-key gn_live_...\n"
53
+ " or set GHOSTNEXUS_PROVIDER_API_KEY environment variable\n\n"
54
+ "Get your provider API key at: https://ghostnexus.net/login"
55
+ )
56
+ sys.exit(1)
57
+
58
+ # Set env vars and import the actual provider node
59
+ os.environ["GHOSTNEXUS_WS_URL"] = args.server
60
+ os.environ["GHOSTNEXUS_PROVIDER_API_KEY"] = args.api_key
61
+ os.environ["GHOSTNEXUS_GPU_PASSTHROUGH"] = "true" if args.gpu_passthrough else "false"
62
+ os.environ["GHOSTNEXUS_JOB_MEMORY"] = args.memory
63
+ os.environ["GHOSTNEXUS_JOB_CPUS"] = args.cpus
64
+ os.environ["GHOSTNEXUS_JOB_TIMEOUT"] = "3600"
65
+
66
+ print(f"GhostNexus Provider Node v0.1.0")
67
+ print(f"Connecting to {args.server} ...")
68
+
69
+ try:
70
+ from ghostnexus_node.node import run
71
+ run()
72
+ except ImportError as e:
73
+ print(f"Error loading node: {e}")
74
+ sys.exit(1)
75
+ else:
76
+ parser.print_help()
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,74 @@
1
+ """
2
+ GhostNexus - Provider Node
3
+ GPU detection module (fake/mock for development without NVIDIA hardware).
4
+ """
5
+
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class GPUInfo:
15
+ """GPU hardware information."""
16
+
17
+ model: str
18
+ vram_mb: int
19
+ temperature_c: Optional[int] = None
20
+ utilization_pct: Optional[int] = None
21
+
22
+
23
+ def detect_gpu() -> Optional[GPUInfo]:
24
+ """
25
+ Detect available GPU. Uses pynvml if NVIDIA GPU present, otherwise returns mock data.
26
+ Mock data allows development/testing without physical GPU.
27
+ """
28
+ try:
29
+ import pynvml
30
+
31
+ pynvml.nvmlInit()
32
+ handle = pynvml.nvmlDeviceGetHandleByIndex(0)
33
+ name = pynvml.nvmlDeviceGetName(handle)
34
+ if isinstance(name, bytes):
35
+ name = name.decode("utf-8", errors="replace")
36
+ mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
37
+ vram_mb = mem_info.total // (1024 * 1024)
38
+
39
+ temp = None
40
+ try:
41
+ temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
42
+ except Exception:
43
+ pass
44
+
45
+ util = None
46
+ try:
47
+ util = pynvml.nvmlDeviceGetUtilizationRates(handle)
48
+ util = util.gpu if util else None
49
+ except Exception:
50
+ pass
51
+
52
+ pynvml.nvmlShutdown()
53
+ return GPUInfo(
54
+ model=name.strip(),
55
+ vram_mb=vram_mb,
56
+ temperature_c=temp,
57
+ utilization_pct=util,
58
+ )
59
+ except ImportError:
60
+ logger.info("pynvml not installed, using mock GPU for development")
61
+ return _get_mock_gpu()
62
+ except Exception as e:
63
+ logger.warning(f"GPU detection failed: {e}, using mock GPU")
64
+ return _get_mock_gpu()
65
+
66
+
67
+ def _get_mock_gpu() -> GPUInfo:
68
+ """Return fake GPU info for development/testing without NVIDIA hardware."""
69
+ return GPUInfo(
70
+ model="NVIDIA GeForce RTX 4090 (Mock)",
71
+ vram_mb=24576,
72
+ temperature_c=45,
73
+ utilization_pct=0,
74
+ )
@@ -0,0 +1,485 @@
1
+ """
2
+ GhostNexus - Provider Node
3
+ Lightweight client that detects GPU, connects via WebSocket, sends heartbeat,
4
+ and executes Python scripts received from the Matchmaker.
5
+
6
+ Job execution is sandboxed inside Docker containers with strict resource and
7
+ security limits (no network, no new privileges, read-only filesystem, CPU/RAM cap).
8
+ """
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import os
14
+ import signal
15
+ import sys
16
+ import tempfile
17
+ import time
18
+
19
+ from ghostnexus_node.gpu_detector import GPUInfo, detect_gpu
20
+
21
+ logging.basicConfig(
22
+ level=os.getenv("LOG_LEVEL", "INFO").upper(),
23
+ format="%(asctime)s [%(levelname)s] %(name)s — %(message)s",
24
+ stream=sys.stdout,
25
+ )
26
+ logger = logging.getLogger("ghostnexus.provider")
27
+
28
+ # ── Configuration ─────────────────────────────────────────────────────────────
29
+ WS_URL = os.getenv("GHOSTNEXUS_WS_URL", "ws://localhost:8000/ws/provider")
30
+ PROVIDER_API_KEY = os.getenv("GHOSTNEXUS_PROVIDER_API_KEY", "")
31
+ HEARTBEAT_INTERVAL = 10 # seconds
32
+
33
+ # Build authenticated WebSocket URL (token passed as query param — auth happens
34
+ # server-side before websocket.accept(), so no token = immediate close with 4001)
35
+ def _build_ws_url() -> str:
36
+ if not PROVIDER_API_KEY:
37
+ logger.error(
38
+ "GHOSTNEXUS_PROVIDER_API_KEY is not set. "
39
+ "Set this environment variable to your provider API key and restart."
40
+ )
41
+ sys.exit(1)
42
+ separator = "&" if "?" in WS_URL else "?"
43
+ return f"{WS_URL}{separator}token={PROVIDER_API_KEY}"
44
+
45
+ # Reconnect backoff: starts at 3s, doubles each attempt, capped at 60s
46
+ _RECONNECT_MIN = 3
47
+ _RECONNECT_MAX = 60
48
+
49
+ # ── Docker sandbox configuration ──────────────────────────────────────────────
50
+ DOCKER_IMAGE = os.getenv("GHOSTNEXUS_DOCKER_IMAGE", "python:3.11-slim")
51
+ JOB_MEMORY_LIMIT = os.getenv("GHOSTNEXUS_JOB_MEMORY", "512m")
52
+ JOB_CPU_LIMIT = os.getenv("GHOSTNEXUS_JOB_CPUS", "1.0")
53
+ GPU_PASSTHROUGH = os.getenv("GHOSTNEXUS_GPU_PASSTHROUGH", "false").lower() == "true"
54
+ JOB_TIMEOUT = int(os.getenv("GHOSTNEXUS_JOB_TIMEOUT", "300"))
55
+
56
+
57
+ async def _check_docker() -> bool:
58
+ """Verify that Docker daemon is reachable before accepting any jobs."""
59
+ try:
60
+ proc = await asyncio.create_subprocess_exec(
61
+ "docker", "info",
62
+ stdout=asyncio.subprocess.DEVNULL,
63
+ stderr=asyncio.subprocess.DEVNULL,
64
+ )
65
+ await proc.wait()
66
+ return proc.returncode == 0
67
+ except FileNotFoundError:
68
+ return False
69
+
70
+
71
+ def _build_docker_cmd(container_name: str, script_host_path: str) -> list[str]:
72
+ """
73
+ Construct the `docker run` command that sandboxes the user script.
74
+
75
+ Security flags applied:
76
+ --security-opt no-new-privileges:true prevent setuid/setgid escalation
77
+ --network=none no inbound or outbound network
78
+ --read-only immutable root filesystem
79
+ --tmpfs /tmp writable scratch space (no exec, size-capped)
80
+ --cap-drop=ALL drop all Linux capabilities
81
+ --memory / --cpus hard resource limits
82
+ --user 65534 run as nobody (unprivileged UID)
83
+ """
84
+ cmd = [
85
+ "docker", "run",
86
+ "--rm",
87
+ "--name", container_name,
88
+ "--network=none",
89
+ "--memory", JOB_MEMORY_LIMIT,
90
+ "--memory-swap", JOB_MEMORY_LIMIT, # disables swap
91
+ "--cpus", JOB_CPU_LIMIT,
92
+ "--read-only",
93
+ "--tmpfs", "/tmp:size=64m,noexec,nosuid",
94
+ "--cap-drop=ALL",
95
+ "--security-opt", "no-new-privileges:true",
96
+ "-v", f"{script_host_path}:/job/script.py:ro",
97
+ "-w", "/job",
98
+ "--user", "65534:65534", # nobody:nogroup
99
+ ]
100
+ if GPU_PASSTHROUGH:
101
+ cmd.extend(["--gpus", "all"])
102
+ cmd.extend([DOCKER_IMAGE, "python", "/job/script.py"])
103
+ return cmd
104
+
105
+
106
+ async def _kill_container(container_name: str) -> None:
107
+ """Send SIGKILL to a running container (best-effort)."""
108
+ try:
109
+ proc = await asyncio.create_subprocess_exec(
110
+ "docker", "kill", container_name,
111
+ stdout=asyncio.subprocess.DEVNULL,
112
+ stderr=asyncio.subprocess.DEVNULL,
113
+ )
114
+ await asyncio.wait_for(proc.wait(), timeout=10.0)
115
+ except Exception as exc:
116
+ logger.warning(f"docker kill {container_name}: {exc}")
117
+
118
+
119
+ # ── Heartbeat ─────────────────────────────────────────────────────────────────
120
+
121
+ async def heartbeat_sender(ws, gpu: GPUInfo):
122
+ """Send heartbeat every 10 seconds."""
123
+ while True:
124
+ await asyncio.sleep(HEARTBEAT_INTERVAL)
125
+ try:
126
+ await ws.send(
127
+ json.dumps(
128
+ {
129
+ "type": "heartbeat",
130
+ "vram_mb": gpu.vram_mb,
131
+ "temperature_c": gpu.temperature_c,
132
+ }
133
+ )
134
+ )
135
+ logger.debug("Heartbeat sent")
136
+ except Exception:
137
+ break
138
+
139
+
140
+ # ── Message receiver ──────────────────────────────────────────────────────────
141
+
142
+ async def message_receiver(ws, gpu: GPUInfo):
143
+ """Listen for messages: heartbeat_ack or job_execute."""
144
+ while True:
145
+ try:
146
+ msg = await ws.recv()
147
+ data = json.loads(msg)
148
+ msg_type = data.get("type", "unknown")
149
+
150
+ if msg_type == "heartbeat_ack":
151
+ logger.debug("Heartbeat acknowledged")
152
+
153
+ elif msg_type == "job_execute":
154
+ job_id = data.get("job_id")
155
+ task_name = data.get("task_name", "unknown")
156
+ script_content = data.get("script_content", "")
157
+
158
+ if not script_content:
159
+ await ws.send(
160
+ json.dumps(
161
+ {
162
+ "type": "job_result",
163
+ "job_id": job_id,
164
+ "status": "failed",
165
+ "task_name": task_name,
166
+ "duration_seconds": 0,
167
+ "stdout": "",
168
+ "stderr": "No script content provided",
169
+ }
170
+ )
171
+ )
172
+ continue
173
+
174
+ logger.info(
175
+ f"Job received: {job_id} | task={task_name} | "
176
+ f"spawning Docker container (image={DOCKER_IMAGE})"
177
+ )
178
+
179
+ # Safe container name: alphanumeric + dash, max 63 chars
180
+ container_name = f"gn-job-{job_id[:20]}"
181
+ temp_path = None
182
+ try:
183
+ # Write script to a named temp file so Docker can bind-mount it
184
+ with tempfile.NamedTemporaryFile(
185
+ mode="w",
186
+ suffix="_job.py",
187
+ delete=False,
188
+ encoding="utf-8",
189
+ ) as f:
190
+ f.write(script_content)
191
+ temp_path = f.name
192
+
193
+ docker_cmd = _build_docker_cmd(container_name, temp_path)
194
+ start = time.perf_counter()
195
+
196
+ proc = await asyncio.create_subprocess_exec(
197
+ *docker_cmd,
198
+ stdout=asyncio.subprocess.PIPE,
199
+ stderr=asyncio.subprocess.PIPE,
200
+ )
201
+
202
+ task_comm = asyncio.create_task(
203
+ asyncio.wait_for(proc.communicate(), timeout=JOB_TIMEOUT)
204
+ )
205
+ task_recv = asyncio.create_task(ws.recv())
206
+
207
+ done, pending = await asyncio.wait(
208
+ [task_comm, task_recv],
209
+ return_when=asyncio.FIRST_COMPLETED,
210
+ )
211
+
212
+ killed = False
213
+
214
+ if task_recv in done:
215
+ # A message arrived while job was running
216
+ try:
217
+ recv_data = json.loads(task_recv.result())
218
+ if (
219
+ recv_data.get("type") == "job_kill"
220
+ and recv_data.get("job_id") == job_id
221
+ ):
222
+ killed = True
223
+ # Kill the Docker container, not just the docker-run process
224
+ await _kill_container(container_name)
225
+ try:
226
+ stdout, stderr = await asyncio.wait_for(
227
+ proc.communicate(), timeout=10.0
228
+ )
229
+ except asyncio.TimeoutError:
230
+ proc.kill()
231
+ stdout, stderr = await proc.communicate()
232
+ else:
233
+ # Unrelated message — let job finish
234
+ stdout, stderr = await task_comm
235
+ except (json.JSONDecodeError, KeyError):
236
+ stdout, stderr = await task_comm
237
+ else:
238
+ # Job finished first
239
+ try:
240
+ stdout, stderr = await task_comm
241
+ except asyncio.TimeoutError:
242
+ # Hard timeout: kill container
243
+ await _kill_container(container_name)
244
+ try:
245
+ proc.terminate()
246
+ except ProcessLookupError:
247
+ pass
248
+ stdout, stderr = await proc.communicate()
249
+ duration_seconds = time.perf_counter() - start
250
+ await ws.send(
251
+ json.dumps(
252
+ {
253
+ "type": "job_result",
254
+ "job_id": job_id,
255
+ "status": "failed",
256
+ "task_name": task_name,
257
+ "duration_seconds": round(duration_seconds, 2),
258
+ "stdout": (stdout or b"").decode("utf-8", errors="replace"),
259
+ "stderr": f"Script execution timed out ({JOB_TIMEOUT}s)",
260
+ }
261
+ )
262
+ )
263
+ for t in pending:
264
+ t.cancel()
265
+ try:
266
+ await t
267
+ except (asyncio.CancelledError, Exception):
268
+ pass
269
+ continue
270
+
271
+ # Cancel any remaining tasks
272
+ for t in pending:
273
+ t.cancel()
274
+ try:
275
+ await t
276
+ except (asyncio.CancelledError, Exception):
277
+ pass
278
+
279
+ duration_seconds = time.perf_counter() - start
280
+ stdout_str = (stdout or b"").decode("utf-8", errors="replace")
281
+ stderr_str = (stderr or b"").decode("utf-8", errors="replace")
282
+
283
+ if killed:
284
+ status = "failed_insufficient_funds"
285
+ stderr_str = "Insufficient funds — job killed by backend"
286
+ logger.info(f"Job {job_id} killed (insufficient funds)")
287
+ else:
288
+ status = "success" if proc.returncode == 0 else "failed"
289
+ logger.info(
290
+ f"Job {job_id} completed | status={status} | "
291
+ f"duration={duration_seconds:.2f}s | "
292
+ f"exit_code={proc.returncode}"
293
+ )
294
+
295
+ await ws.send(
296
+ json.dumps(
297
+ {
298
+ "type": "job_result",
299
+ "job_id": job_id,
300
+ "status": status,
301
+ "task_name": task_name,
302
+ "duration_seconds": round(duration_seconds, 2),
303
+ "stdout": stdout_str,
304
+ "stderr": stderr_str,
305
+ }
306
+ )
307
+ )
308
+
309
+ except asyncio.CancelledError:
310
+ raise
311
+ except Exception as e:
312
+ logger.exception(f"Job {job_id} execution error: {e}")
313
+ await ws.send(
314
+ json.dumps(
315
+ {
316
+ "type": "job_result",
317
+ "job_id": job_id,
318
+ "status": "failed",
319
+ "task_name": task_name,
320
+ "duration_seconds": 0,
321
+ "stdout": "",
322
+ "stderr": str(e),
323
+ }
324
+ )
325
+ )
326
+ finally:
327
+ if temp_path and os.path.exists(temp_path):
328
+ try:
329
+ os.unlink(temp_path)
330
+ except OSError:
331
+ pass
332
+
333
+ elif msg_type == "result_received":
334
+ logger.debug("Backend acknowledged job result")
335
+
336
+ else:
337
+ logger.warning(f"Unknown message type: {msg_type}")
338
+
339
+ except Exception as e:
340
+ logger.error(f"Message receiver error: {e}")
341
+ raise
342
+
343
+
344
+ # ── Main loop ─────────────────────────────────────────────────────────────────
345
+
346
+ async def run_provider_node(shutdown: asyncio.Event):
347
+ """Main loop: connect, register GPU, heartbeat + job listener.
348
+ Reconnects with exponential backoff on disconnect.
349
+ """
350
+ import websockets
351
+
352
+ # Verify Docker is available before anything else
353
+ if not await _check_docker():
354
+ logger.error(
355
+ "Docker daemon is not reachable. "
356
+ "Install Docker and ensure the daemon is running, then retry."
357
+ )
358
+ sys.exit(1)
359
+ logger.info(
360
+ f"Docker OK | image={DOCKER_IMAGE} | "
361
+ f"mem={JOB_MEMORY_LIMIT} | cpus={JOB_CPU_LIMIT} | "
362
+ f"gpu_passthrough={GPU_PASSTHROUGH}"
363
+ )
364
+
365
+ gpu = detect_gpu()
366
+ if not gpu:
367
+ logger.error("No GPU detected — exiting.")
368
+ sys.exit(1)
369
+
370
+ ws_connect_url = _build_ws_url()
371
+ logger.info(f"GPU detected: {gpu.model} | VRAM: {gpu.vram_mb} MB")
372
+ logger.info(f"Connecting to {WS_URL} …")
373
+
374
+ reconnect_delay = _RECONNECT_MIN
375
+ attempt = 0
376
+
377
+ while not shutdown.is_set():
378
+ attempt += 1
379
+ try:
380
+ async with websockets.connect(
381
+ ws_connect_url,
382
+ ping_interval=20,
383
+ ping_timeout=10,
384
+ close_timeout=5,
385
+ ) as ws:
386
+ reconnect_delay = _RECONNECT_MIN # reset on successful connect
387
+ logger.info(f"Connected (attempt #{attempt})")
388
+
389
+ # Register with GPU info (identity already verified via ?token= at connect)
390
+ register_payload = {
391
+ "type": "register",
392
+ "gpu_model": gpu.model,
393
+ "vram_mb": gpu.vram_mb,
394
+ "temperature_c": gpu.temperature_c,
395
+ "utilization_pct": gpu.utilization_pct,
396
+ }
397
+ logger.info("Sending GPU registration")
398
+
399
+ await ws.send(json.dumps(register_payload))
400
+ reg_response = await ws.recv()
401
+ reg_data = json.loads(reg_response)
402
+ if reg_data.get("type") == "registered":
403
+ logger.info(
404
+ f"Registered as node {reg_data.get('node_id')} | "
405
+ f"GPU: {gpu.model} | Status: {reg_data.get('status', 'ok')}"
406
+ )
407
+
408
+ # Run heartbeat + message receiver concurrently; stop on shutdown
409
+ async def _shutdown_watcher():
410
+ await shutdown.wait()
411
+
412
+ done, pending = await asyncio.wait(
413
+ [
414
+ asyncio.create_task(heartbeat_sender(ws, gpu)),
415
+ asyncio.create_task(message_receiver(ws, gpu)),
416
+ asyncio.create_task(_shutdown_watcher()),
417
+ ],
418
+ return_when=asyncio.FIRST_COMPLETED,
419
+ )
420
+ for task in pending:
421
+ task.cancel()
422
+ try:
423
+ await task
424
+ except asyncio.CancelledError:
425
+ pass
426
+
427
+ if shutdown.is_set():
428
+ logger.info("Shutdown signal received — disconnecting cleanly.")
429
+ break
430
+
431
+ except Exception as e:
432
+ logger.error(
433
+ f"Connection lost: {e} — reconnecting in {reconnect_delay}s "
434
+ f"(attempt #{attempt})"
435
+ )
436
+ try:
437
+ await asyncio.wait_for(shutdown.wait(), timeout=reconnect_delay)
438
+ break # Shutdown was requested during wait
439
+ except asyncio.TimeoutError:
440
+ pass
441
+ reconnect_delay = min(reconnect_delay * 2, _RECONNECT_MAX)
442
+
443
+
444
+ def _handle_signal(shutdown: asyncio.Event):
445
+ logger.info("Interrupt received — shutting down …")
446
+ shutdown.set()
447
+
448
+
449
+ if __name__ == "__main__":
450
+ loop = asyncio.new_event_loop()
451
+ asyncio.set_event_loop(loop)
452
+ shutdown_event = asyncio.Event()
453
+
454
+ for sig in (signal.SIGINT, signal.SIGTERM):
455
+ try:
456
+ loop.add_signal_handler(sig, _handle_signal, shutdown_event)
457
+ except NotImplementedError:
458
+ # Windows doesn't support add_signal_handler for all signals
459
+ pass
460
+
461
+ try:
462
+ loop.run_until_complete(run_provider_node(shutdown_event))
463
+ finally:
464
+ loop.close()
465
+ logger.info("Provider node stopped.")
466
+
467
+
468
+ def run():
469
+ """Entry point called by CLI."""
470
+ import signal
471
+ loop = asyncio.new_event_loop()
472
+ asyncio.set_event_loop(loop)
473
+ shutdown_event = asyncio.Event()
474
+
475
+ for sig in (signal.SIGINT, signal.SIGTERM):
476
+ try:
477
+ loop.add_signal_handler(sig, _handle_signal, shutdown_event)
478
+ except NotImplementedError:
479
+ pass
480
+
481
+ try:
482
+ loop.run_until_complete(run_provider_node(shutdown_event))
483
+ finally:
484
+ loop.close()
485
+
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostnexus-node
3
+ Version: 0.1.0
4
+ Summary: GhostNexus Provider Node — earn money with your idle GPU
5
+ Author-email: GhostNexus <contact@ghostnexus.net>
6
+ License: MIT
7
+ Project-URL: Homepage, https://ghostnexus.net/become-provider
8
+ Project-URL: Documentation, https://ghostnexus.net/become-provider
9
+ Project-URL: Repository, https://github.com/ghostnexus/ghostnexus-node
10
+ Keywords: gpu,mining,ai,compute,provider,passive-income
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: websockets>=12.0
22
+ Requires-Dist: nvidia-ml-py>=12.0
23
+
24
+ # ghostnexus-node
25
+
26
+ **Earn money with your idle GPU — GhostNexus Provider Node.**
27
+
28
+ Turn your gaming GPU into a passive income source. When you're not gaming, your GPU runs AI training jobs for other developers and you earn money.
29
+
30
+ - **RTX 4070**: up to ~$90/month at 50% utilization
31
+ - **RTX 4090**: up to ~$126/month at 50% utilization
32
+ - **70% revenue share** — paid via Wise or crypto from $50
33
+ - Fully isolated Docker sandbox — your data is never exposed
34
+ - Stop anytime — no contract, no commitment
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install ghostnexus-node
40
+ ```
41
+
42
+ Docker required: [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)
43
+
44
+ ## Quick Start
45
+
46
+ ```bash
47
+ # Get your provider API key at ghostnexus.net/login (free)
48
+ ghostnexus-node start --api-key gn_live_...
49
+ ```
50
+
51
+ Or set it as an environment variable:
52
+
53
+ ```bash
54
+ export GHOSTNEXUS_PROVIDER_API_KEY="gn_live_..."
55
+ ghostnexus-node start
56
+ ```
57
+
58
+ ## How It Works
59
+
60
+ 1. Your node connects to the GhostNexus network
61
+ 2. When a client submits a job, it's dispatched to your GPU
62
+ 3. The job runs in an isolated Docker container (no network access, memory/CPU limits)
63
+ 4. Results are sent back, you earn credits
64
+ 5. Cash out via Wise or crypto once you reach $50
65
+
66
+ ## Sign Up
67
+
68
+ Create your provider account at [ghostnexus.net/login](https://ghostnexus.net/login) — it's free.
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,9 @@
1
+ ghostnexus_node/__init__.py,sha256=SLGxt8TYYAK37iOT2qDNm21RA0gbqovjhEI3vqMEutY,88
2
+ ghostnexus_node/cli.py,sha256=FwOKGUPHsUVT4XEt_VVZUUvcvvJh-scD6HQGioWzqjw,2461
3
+ ghostnexus_node/gpu_detector.py,sha256=Kpm7hCHCmLWXw0QZG95xATIW2ojx1Bblf1yX3WiVquQ,2116
4
+ ghostnexus_node/node.py,sha256=4sy5aM4J_qLXM7OPXWCxnWhqkYtpToTpG4VUIwqz2NA,19296
5
+ ghostnexus_node-0.1.0.dist-info/METADATA,sha256=1JC9eOeZ3kFpo5MahMEBRhqeN1JLzKi4hj3TnbQ3SzI,2399
6
+ ghostnexus_node-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ ghostnexus_node-0.1.0.dist-info/entry_points.txt,sha256=TGajxybnqT9Z1kOimjA5oQ2v2tuTt_QCtDfwagix5fk,61
8
+ ghostnexus_node-0.1.0.dist-info/top_level.txt,sha256=ZMrESYDaNalbqfnhK2kZMdgSMO-qly7usIRkLfrVm6Y,16
9
+ ghostnexus_node-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ghostnexus-node = ghostnexus_node.cli:main
@@ -0,0 +1 @@
1
+ ghostnexus_node