Documentation
Run a node. Submit jobs. Self-host.
Everything an operator or client needs to use the network. If you're looking for protocol-level design decisions, see the IPIPs on GitHub.
Workload note: Infernet targets single-GPU inference (7B–70B), embarrassingly parallel batch, LoRA fine-tunes, and async / federated distributed training. Tight-sync NVLink-bound training of 100B+ models stays on hyperscaler fleets — by design, not by accident. See the homepage positioning section for the full reasoning.
#Quick start
On Linux, macOS, or Windows (WSL2), one line gets you the infernet CLI plus everything it needs:
curl -fsSL https://infernetprotocol.com/install.sh | shThen bootstrap your environment (Ollama, model, firewall, identity, register with the control plane, start the daemon — all in one interactive walk-through):
infernet setupOnce setup is green, try inference end-to-end:
infernet "what is 2+2?"
# or pipe input
echo "summarize: water is wet" | infernetinfernet "..." is shorthand for infernet chat "...". Subcommands like setup, model, tui, etc. take precedence when present.Windows (WSL2)
The installer is POSIX shell, so Windows operators run it inside WSL2 and the daemon picks up the GPU through Microsoft's WSL CUDA path:
# 1. From PowerShell, install WSL + Ubuntu (reboot when prompted)
wsl --install -d Ubuntu
# 2. Install the Windows NVIDIA driver — gives WSL CUDA without
# needing a separate driver inside Ubuntu.
# https://www.nvidia.com/Download/index.aspx
# 3. Open Ubuntu, then run the same one-liner:
curl -fsSL https://infernetprotocol.com/install.sh | sh
infernet setup:46337 needs a netsh interface portproxy rule on the Windows host — skip it if you only care about earning via routed jobs.How Node gets installed
The installer uses mise to install Node 20 — single static binary under $HOME/.local/share/mise, no apt-repo dance, no conflict with whatever Node a base image already shipped (e.g. RunPod's vllm-workspace ships Node 12). The wrapper at ~/.local/bin/infernetprepends mise's shims dir so the right Node is picked regardless of whether your shell has activated mise yet.
#CLI overview
The infernet binary groups commands into four buckets:
| Group | Commands | Purpose |
|---|---|---|
Setup | setup, model | Bootstrap the runtime + manage what models you serve |
Node lifecycle | init, login, register, update, remove | Configure node identity + (de)register with a control plane |
Daemon | start, stop, status, stats, logs, service | Run the long-lived process and inspect it |
Diagnostics | gpu, firewall, chat, tui, doctor | Probe the local box + interact with the network |
Payments | payout, payments | Configure your payout addresses + view earnings |
Run infernet help for the live list. Every subcommand accepts --help.
#infernet setup
The full bootstrap. Idempotent — re-run anytime to fix or update. Walks through each step and asks before doing anything privileged (sudo prompts use your terminal directly).
infernet setup
[1/8] Node.js ✓ v22.21.1
[2/8] Ollama ✗ Ollama not installed
Will run: curl -fsSL https://ollama.com/install.sh | sh
Install Ollama now (will sudo)? [Y/n] y
...
✓ done
[3/8] Model Which model should this node serve?
1) qwen2.5:0.5b ≈400 MB smoke test, runs on CPU
3) qwen2.5:7b ≈4.4 GB fits 8 GB+ GPU — recommended
Choice [3]:
→ pulling qwen2.5:7b via the ollama CLI...
✓ verified: ollama can serve qwen2.5:7b
[4/8] Firewall (port 46337) ✓ rule applied via ufw
[5/8] Config ✓ saved to ~/.config/infernet/config.json
[6/8] Identity & control plane
! no identity yet — running init
...
✓ identity: 5d0de683…
[7/8] Provider registration ✓ registered
[8/8] Daemon ✓ daemon running
✓ setup completeUseful flags:
infernet setup --confirm # auto-yes every prompt
infernet setup --model qwen2.5:7b # preselect the model
infernet setup --no-firewall # skip firewall step (e.g. inside containers)
infernet setup --skip-pull # leave models alone
infernet setup --backend stub # use canned tokens (no real GPU)#infernet (chat)
Default verb. infernet "<prompt>" sends a chat job; if a control plane is configured, it routes through the P2P network, otherwise falls back to your local engine.
# Network (default when controlPlane.url is set)
infernet "what is 2+2?"
# Force the local engine — useful for smoke testing without a network
infernet --local "ping"
# Force network — error if no control plane configured
infernet --remote "what is the capital of France?"
# Pipe input
echo "summarize this paragraph" | infernet
# Pin a specific model (overrides config)
infernet --model qwen2.5:7b "tell me a joke"
# JSON event stream — one event per line (meta, token, done)
infernet --json "ping"Common flags:
| Flag | Effect |
|---|---|
| --remote / --local | Force routing path |
| --url <url> | Override control-plane URL (network mode) |
| --model <name> | Pin a model id (e.g. qwen2.5:7b) |
| --system <text> | Prepend a system message |
| --temperature <n> | Sampling temperature |
| --max-tokens <n> | Cap on generated tokens |
| --json | Emit raw NDJSON events instead of token stream |
| --backend <kind> | Local: ollama | mojo | stub |
#infernet model
Manage the models your node has on disk and which one is the active default.
infernet model list # show pulled models + which is active
infernet model pull qwen2.5:7b # pull (Ollama progress bar inline)
infernet model use qwen2.5:7b # set as engine.model in config
infernet model show # current backend, host, active model
infernet model remove qwen2.5:0.5b # delete from disk (clears active if it was)infernet update / remove, which are about your node's registration on the control plane — not about local models on disk.#infernet init
Configure node identity (Nostr keypair) and the control plane to talk to. Idempotent — re-run to finish a partial config without losing engine settings written by setup.
infernet init
Control-plane URL [https://infernetprotocol.com]: ← Enter to accept
Node role (provider|aggregator|client) [provider]: ← Enter to accept
Human-readable node name [provider@hostname]: my-vps-1
... auto-generates Nostr keypair, detects address, asks about firewall ...
Wrote /home/ubuntu/.config/infernet/config.json
Pubkey: 5d0de683a5f22aa1d5a8927a431d86601277aad61fc7cdce126ac8c012e2c84dNon-interactive flags:
infernet init --url https://infernetprotocol.com \
--role provider \
--name my-vps-1 \
--p2p-port 46337 \
--no-advertise # outbound-only mode (Tor-friendly)#infernet start
Boot the long-running daemon. Detaches by default; use --foreground for systemd / process supervisors.
infernet start # detach to background, return shell prompt
infernet start --foreground # for systemd (Type=simple) or PM2
# Inspect state
infernet status # remote row + local stats
infernet stats # live in-memory snapshot via IPC
infernet logs # tail ~/.config/infernet/daemon.log
infernet stop # graceful shutdown via IPC, signal fallbackinfernet service install (systemd userland unit) — see the dedicated section below.#infernet doctor
Six independent checks across local config, engine, control plane, daemon, registration row, and an end-to-end test inference. Useful on a fresh box and after every config change.
infernet doctor
[1/6] Local config ✓ provider/my-vps-1, key=5d0de683a5f2…
[2/6] Engine ✓ Ollama up @ http://localhost:11434, model qwen2.5:7b pulled
[3/6] Control plane ✓ https://infernetprotocol.com (HTTP 200, 124ms)
[4/6] Daemon ✓ pid=12345, last heartbeat 4s ago, jobs 12/13
[5/6] Provider row ✓ registered as 5d0de683…, status=available, gpu=A100
[6/6] End-to-end ✓ routed via=p2p, provider=self, first token 740ms, total 1280ms
All checks passed.Skip the e2e test (which submits a real tiny job):
infernet doctor --skip-e2e#infernet payout
Configure where you want to be paid for served jobs. CoinPayPortal sends payouts to whatever addresses you set here. Operators who already have wallets paste their addresses; operators who don't can have a non-custodial BIP39 wallet generated and the encrypted seed phrase delivered to their PGP key.
# Bring your own wallet (recommended — non-custodial)
infernet payout set BTC mainnet bc1q9h6zcv4fp9kx5n0jq5qf3w2yxz4z3y2w5
infernet payout set ETH mainnet 0x742d35Cc6634C0532925a3b844Bc9e7595f8B59f
infernet payout set SOL mainnet 5RaBwVPRBfpz9MRBnGRcQAPfvKxcUVtxL2ANe9XJ
infernet payout set USDC solana 5RaBwVPRBfpz9MRBnGRcQAPfvKxcUVtxL2ANe9XJ
infernet payout set USDC polygon 0x742d35Cc6634C0532925a3b844Bc9e7595f8B59f
# Lightning — paste your bolt12 (recommended) or LNURL-pay address
infernet payout set BTC ln lno1qsgqmqvgm96frzdg8m0gc6nz...
# Don't have an LN node? Opt-in to a CoinPay-custodial channel:
infernet payout set BTC ln --provision
# Inspect
infernet payout list
infernet payout remove ETH mainnet
infernet payments # recent transactions (signed read)#infernet tui
Live terminal dashboard. Polls the daemon IPC + local Ollama; renders system / jobs / engine / peers panels. q to quit, r to refresh, --refresh <ms> to tune.
infernet tui # default 2s poll
infernet tui --refresh 1000 # 1s — for live debugging#infernet service (systemd)
Optional boot-persistent runner via a userland systemd unit. No sudo, no system-wide changes, lives under ~/.config/systemd/user/. Linux only.
# Install the unit (writes ~/.config/systemd/user/infernet.service)
infernet service install
infernet service enable # systemctl --user enable --now infernet
# Inspect
infernet service status
infernet service logs # journalctl --user -u infernet -f
# (Optional) make it survive logout / reboot when not logged in:
loginctl enable-linger $USER
# Tear down
infernet service disable
infernet service uninstallPreview the unit file without writing anything:
infernet service unit#Sign up / sign in / reset password
Browser-side auth lives at /auth/* on the control plane. All flows are server-side cookie-based via @supabase/ssr — the browser never sees a Supabase URL or key directly.
| Page | What it does |
|---|---|
| /auth/signup | Email + password. Sends a confirmation link. |
| /auth/login | Email + password, OR leave password blank for a magic link. |
| /auth/reset-password | Email a recovery link. |
| /auth/update-password | Set a new password (you land here from the recovery email). |
| /auth/check-email | "We sent you a link" success screen. |
SDK / CLI consumers use the same routes with Content-Type: application/json:
# Send a magic link
curl -sS -X POST https://infernetprotocol.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}'
# Or password
curl -sS -X POST https://infernetprotocol.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"hunter22"}' \
-c cookies.txt
# Use the resulting cookie for /api/admin/* (when those land — IPIP-0003 phase 4)#Dashboard
Signing in lands you on /dashboard. It's your private, per-user view of the network: only resources linked to your account show up here. Network-wide stats live on /status instead.
The dashboard surfaces, all queried server-side:
- Earnings (last 30d / all-time / # confirmed payments) — sum of inbound
payment_transactionsjoined to your providers. - Spend on the same shape, joined to your clients.
- GPUs in use + models served — totals across the
providers.specs.gpus/.modelsjsonb of every provider you operate. - Provider-node table with status pills + last-seen timestamps.
- CPU pool — host CPU groups across your nodes.
- Interconnect summary — NVLink / xGMI / InfiniBand / EFA / RDMA capability across your providers (see the interconnects section below for what gets detected).
- Linked Nostr identities— which pubkeys you've claimed for which roles (
provider,client,aggregator). - Recent jobs you submitted (best-effort match by client label until the jobs table grows a
client_idFK).
Empty cards print the exact CLI command that would populate them — e.g. infernet register for a node, infernet pubkey link for an identity. No fake numbers.
The user-scoped joins go through pubkey_links (per IPIP-0003): auth.users.id → pubkey_links.user_id → providers.public_key / clients.public_key. Routes that need the current user import getCurrentUser() from apps/web/lib/supabase/auth-server.js; unauth'd hits redirect to /auth/login?next=/dashboard.
#Interconnects (NVLink / xGMI / InfiniBand / EFA)
Infernet auto-detects GPU-to-GPU and host-to-host fabric on every infernet setup and infernet gpu run, advertises a coarse capability summary at registration time, and emits the right environment variables when spawning an engine subprocess so NCCL / RCCL / libfabric actually use the fabric instead of silently falling back to TCP.
What gets detected
| Fabric | Hardware | Detection source |
|---|---|---|
| NVLink | NVIDIA datacenter + consumer multi-GPU | nvidia-smi topo -m |
| xGMI / Infinity Fabric | AMD MI200 / MI300 series | rocm-smi --showtopo |
| InfiniBand / RoCE | Mellanox / Nvidia ConnectX, RoCE NICs | /sys/class/infiniband/* (Linux) |
| AWS EFA | AWS p4d / p5 / Trainium instances | lspci -d 1d0f: (Linux) |
Detection is best-effort: any vendor command that's missing or fails is silently skipped — running on a CPU-only laptop or an AMD-only host doesn't error, the relevant detector just returns an empty result. Topology is classified pair, mesh, or all-to-all by counting deduped GPU-pair edges against the complete-graph upper bound.
See it on your hardware:
infernet gpu # human summary
infernet gpu --json | jq .interconnects # raw detected shapeWhat gets advertised
infernet register includes a coarsened capability block in specs.interconnects — no device names, no board IDs, no PCI BDFs. Just enough for the matchmaker to route, not enough to fingerprint your hardware:
{
"specs": {
"interconnects": {
"nvlink": { "available": true, "topology": "all-to-all", "link_count": 6 },
"xgmi": { "available": false, "topology": "none", "link_count": 0 },
"infiniband": { "available": true, "active_port_count": 2 },
"efa": { "available": false, "adapter_count": 0 },
"rdma_capable": true
}
}
}Engine-spawn env vars
When the daemon launches an engine for a job, it merges the output of interconnectEnv(capability)into the child's environment. These are the standards downstream toolkits already honor — setting them is the difference between NCCL using IB at 200 Gbit/s and NCCL falling back to TCP at 10 Gbit/s.
| When | Env vars set |
|---|---|
| NVLink available | INFERNET_NVLINK=1, INFERNET_NVLINK_TOPOLOGY=<topo> |
| xGMI available | INFERNET_XGMI=1, INFERNET_XGMI_TOPOLOGY=<topo> |
| ≥1 active IB port with a name | NCCL_IB_DISABLE=0, NCCL_IB_HCA=mlx5_0:1,mlx5_1:1,… |
| EFA adapter present | FI_PROVIDER=efa, NCCL_PROTO=simple, INFERNET_EFA=1 |
| RDMA-capable (any of IB / EFA active) | INFERNET_RDMA=1 |
The INFERNET_* markers are advisory — useful for our adapters that branch on fabric (e.g. opting into tensor-parallel inference modes). The NCCL_* and FI_*are the load-bearing standards every distributed-training framework already reads. Job-level launch profiles can override any of these; the daemon won't clobber an explicit caller value.
requires.interconnects matchmaking shape, lives in IPIP-0008.#Public API endpoints
These are the wire surfaces. Two auth tiers — the protocol (Nostr-signed) and the dashboard (cookie session) — plus a small public-read surface that's IP-rate-limited.
| Endpoint | Method | Auth | What |
|---|---|---|---|
| /api/health | GET | none | Liveness probe (no DB) |
| /api/peers?limit=N | GET | none | Bootstrap seed peers (IPIP-0006) |
| /.well-known/did.json | GET | none | Platform DID document (IPIP-0007) |
| /api/overview | GET | none | Aggregate network stats |
| /api/chat | POST | none + IP-rate | Submit a chat job (legacy alias for /api/jobs/submit) |
| /api/chat/stream/[jobId] | GET (SSE) | none | Stream tokens + events |
| /api/v1/node/register | POST | X-Infernet-Auth | Provider registration (signed) |
| /api/v1/node/heartbeat | POST | X-Infernet-Auth | Liveness ping |
| /api/v1/node/jobs/poll | POST | X-Infernet-Auth | Pull assigned jobs |
| /api/v1/node/jobs/[id]/events | POST | X-Infernet-Auth | Stream tokens back |
| /api/v1/node/jobs/[id]/complete | POST | X-Infernet-Auth | Mark completed; triggers receipt |
| /api/auth/{signup,login,callback,logout,reset-password,update-password} | POST/GET | cookie session | Dashboard auth flows |
Submit a chat job (anonymous, IP-rate-limited):
curl -sS -X POST https://infernetprotocol.com/api/chat \
-H "Content-Type: application/json" \
-d '{
"messages":[{"role":"user","content":"hello"}],
"modelName":"qwen2.5:7b",
"maxTokens":256
}'
# → { "jobId":"...", "streamUrl":"/api/chat/stream/..." }
# Then SSE-tail streamUrl for tokens.Bootstrap seed peers:
curl -sS https://infernetprotocol.com/api/peers?limit=10 | jq .
# → {
# "data":[
# {
# "pubkey":"5d0de683…",
# "multiaddr":"/ip4/162.250.189.114/tcp/46337",
# "last_seen":"2026-04-26T12:00:00Z",
# "served_models":["qwen2.5:7b"],
# "gpu_model":"A100"
# }
# ]
# }Verify a CPR Receipt issued by the platform:
curl -sS https://infernetprotocol.com/.well-known/did.json | jq .verificationMethod
# Use the publicKeyMultibase to verify any Receipt signed by
# did:web:infernetprotocol.com.#Self-host the control plane
Infernet's control plane is open-source (MIT) and Docker-shippable. Self-hosting means you run your own Next.js app + Supabase (cloud or self-hosted) and point your operators at your URL via infernet init --url https://your-infernet.example. The protocol itself is permissionless — your operators can keep speaking to other control planes too.
When to self-host
- You run a private GPU pool for one organization and don't want jobs leaving it.
- You need data residency in a specific region.
- You want to fork the dashboard, ship a different model catalog, or run an internal billing layer.
- You're building on top of the protocol and want a sandbox to break.
1. Clone, configure, build
git clone https://github.com/profullstack/infernet-protocol.git
cd infernet-protocol
cp sample.env .env
node tooling/generate-secrets.mjs >> .env # CLI session, cron, DID keys
$EDITOR .env # fill SUPABASE_*, RESEND, CoinPay, etc.
pnpm install
pnpm --filter @infernetprotocol/web build2. Provision Supabase
Either flavor works:
- Supabase Cloud — fastest path. Create a project, copy the URL + anon key + service-role key into
.env, then runsupabase db pushfrom this repo to apply migrations. - Self-hosted Supabase — boot the official Docker stack, then in this repo run
supabase start && supabase db resetfor local Postgres + Auth + Realtime + migrations applied.
3. Required environment variables
# --- Supabase ---------------------------------------------------------------
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ... # server-only, never browser
# --- Public-facing URL (used by every redirect, OG card, install.sh) -------
NEXT_PUBLIC_APP_URL=https://your-infernet.example
# --- Auth (CLI bearer JWTs, cron auth, DID:web platform identity) ----------
INFERNET_CLI_SESSION_SECRET=... # generate-secrets.mjs fills these
INFERNET_CRON_SECRET=...
INFERNET_PLATFORM_DID_PRIVATE_KEY=...
# --- Email (sign-up confirmation, password reset, /contact) ----------------
RESEND_API_KEY=re_...
# --- Optional: NIM fallback for the public /chat playground ----------------
NVIDIA_NIM_API_KEY=nvapi-...
# --- Payments (CoinPayPortal — non-custodial except Lightning) -------------
COINPAY_ISSUER_API_KEY=cprt_...
# --- Web server -------------------------------------------------------------
PORT=80804. Run it
Pick a hosting target:
Docker
# Multi-stage build — preserves the pnpm workspace topology.
docker build -t infernet-control-plane -f docker/Dockerfile .
# Run with your .env mounted in
docker run --env-file .env -p 8080:8080 infernet-control-planeRailway / Fly / Render
Point the platform at docker/Dockerfile as the builder and copy the env vars from your .envinto the platform's secret manager. Railway specifically: set the build command to nothing (Dockerfile handles it), healthcheck path to /api/health, and PORT to 8080 (or leave $PORT auto-injection alone — next start honors it). Add your custom domain at the platform; in your DNS, the apex record points at the platform's edge.
Bare Linux + systemd
pnpm --filter @infernetprotocol/web start # http://localhost:8080
# Front it with nginx/Caddy + Let's Encrypt for TLS, then bind a systemd
# unit so it restarts on crash. apps/cli/commands/service.js generates a
# matching unit for the GPU node side.5. Verify the deploy
curl -fsS https://your-infernet.example/api/health
# {"ok":true,"uptime_s":...,"node_env":"production","commit":"<sha>"}
# Round-trip the device-code login from a workstation:
infernet init --url https://your-infernet.example
infernet login # opens browser → polls
infernet "ping" # default verb is chatIf /api/healthreturns 502, the container is down — most often a missing env var at boot. Check the platform's deploy logs first, before debugging anything in the app.
6. Operators on your deployment
infernet init --url https://your-infernet.example
infernet setup # installs Ollama, pulls a model, opens firewall
infernet register # advertises the node on your control plane
infernet start # daemon takes paying jobs#Architecture
Three components, two trust tiers:
┌────────────────────────────────────────────────────────┐
│ Control plane (Next.js + Supabase) │
│ /api/v1/node/* provider tier (Nostr-signed) │
│ /api/admin/* dashboard tier (cookie session) │
│ /api/chat, /api/peers, /.well-known/did.json │
│ │
│ Writes CPR Receipts to coinpayportal.com on every │
│ completed job — operator reputation accumulates │
│ automatically. │
└─────────────────────────────────────┬──────────────────┘
│ (server-side only)
▼
Supabase / Postgres
▲
Nostr-signed HTTP │ anon SSE / cookie
(BIP-340 Schnorr) │
┌────────────┐ ┌──────────┐ ┌────────────────────┐
│ infernet │… │ Browsers │ │ NVIDIA NIM │
│ daemon │ │ /chat │ │ (fallback only) │
│ Ollama │ │ /status │ │ │
│ TCP 46337 │ │ /docs │ └────────────────────┘
└────────────┘ └──────────┘Read the IPIPs for the locked-in design decisions:
- IPIP-0001 — v1.0 launch criteria
- IPIP-0002 — operator P2P chat (Nostr DMs + rooms)
- IPIP-0003 — auth + account model (two-tier, pubkey linking)
- IPIP-0004 — multi-currency payments via CoinPayPortal
- IPIP-0005 — data access architecture (no DB clients in browsers/CLI/SDK)
- IPIP-0006 — peer discovery + bootstrap (Nostr capability + libp2p Kad)
- IPIP-0007 — CoinPay Reputation Protocol integration
#Troubleshooting
`infernet doctor` reports Ollama not reachable
Cause — Ollama not installed, or installed but not running.
Fix — Run infernet setup — it'll detect both states and offer to fix. If you prefer manual: curl -fsSL https://ollama.com/install.sh | sh then sudo systemctl start ollama.
`infernet doctor` reports control plane unreachable
Cause — Wrong URL in config, or the control plane is genuinely down.
Fix — Check with curl https://infernetprotocol.com/api/health. If the public site is up but yours doesn't reach it, your network may be filtering outbound. Run infernet login --url <new-url> if you need to repoint.
"job not assigned to this provider" on complete
Cause — Daemon picked up a job but its public_key on the control plane doesn't match. Usually means you regenerated the keypair without re-registering.
Fix — Run infernet register to re-upsert; then restart the daemon.
Inference is much slower than expected
Cause — Likely running on CPU — Ollama installs without CUDA on a fresh box.
Fix — Run nvidia-smi while a job is in flight. If VRAM doesn't move, Ollama isn't using the GPU. Reinstall after installing CUDA, OR switch to a smaller model (qwen2.5:0.5b runs reasonably on CPU).
`infernet chat` returns error about missing model
Cause — config.engine.model unset or pointing at a model not pulled.
Fix — Pull and select: infernet model pull qwen2.5:7b && infernet model use qwen2.5:7b. infernet model show confirms.
Email confirmation never arrives
Cause — Supabase SMTP not configured, or sender domain not warmed up.
Fix — Check Supabase Dashboard → Authentication → SMTP settings. Verify your sender domain has SPF + DKIM. Most "missing email" issues are quarantine — check spam, Promotions, Updates folders.
HTTP 502 from infernetprotocol.com
Cause — Control plane container is down or env vars are missing at boot.
Fix — Check /api/health first — a 502 there means the container is fully dead; missing-env-var crashes are the most common cause. SSH (or Railway dashboard) and check the deploy log.