--- name: basis-worker description: Register and run a Basis GPU worker that can serve verified inference jobs and earn $BASIS when worker routing is live. Use this when a user wants to put an idle GPU (or any machine) to work on the Basis coordinated inference network — set up Ollama/vLLM, register a reward wallet, generate the worker config, start the worker, and verify it is online. --- # basis-worker — set up a Basis contributor inference worker Turn an idle GPU (or any machine) into a **Basis** worker: a node that connects to a Basis orchestrator, registers a **public** EVM reward wallet, and serves OpenAI-compatible inference jobs. Basis is the inference layer for the agent economy (https://basis.watch). A worker holds **no private key**, signs nothing, and performs **no on-chain action**. > **Honest state (do not overstate this to the user).** Worker **registration** > is live + durable today. Worker **job ROUTING is OPT-IN and currently OFF in > production** — so a registered worker is **"registration ready / routing being > activated"** and **cannot receive production jobs yet**. On-chain reward > **settlement/claiming is gated (dry-run)**. So: set the user up to register and > run a worker now; tell them clearly that earning is not active yet. There is > **no guaranteed income**. Confirm current state with > `GET https://basis.watch/api/workers/onboarding/status` (or > `node scripts/verify-worker.mjs --onboarding`). ## What this does Walks an agent through, end to end: 1. Check prerequisites (Node ≥20, optionally Docker + an NVIDIA GPU). 2. Help the user connect a **wallet** at basis.watch (Privy or a browser wallet) to get a **public** reward address — never a private key. 3. Register the worker (`POST /api/workers/register`, strict EVM-address check). 4. Generate a **local-only** worker `.env` (chmod 600) from the template. 5. Choose a runtime: **simulator (`echo`, GPU-free)**, **Ollama**, or **vLLM**. 6. Run the worker against an orchestrator and verify it shows **online**. 7. **Publish a SIGNED price/model manifest** to the market (the worker's ask prices + capacity, signed by the reward wallet — see "Broadcast a price/model manifest" below). 8. Run the local canary to prove the orchestrator ⇄ worker path end to end. 9. Explain rewards + settlement honestly (gated; no earnings yet). ## Commands (the flow at a glance) These are the steps the skill walks through. They map to the scripts in `scripts/` (this skill has no single `basis-worker` binary — invoke the `.mjs` helpers, or alias them as shown): | Command (alias) | Script | What it does | |---|---|---| | `basis-worker setup` | `setup-worker.mjs --check` / `--write-env` | Detect GPU/Docker/Ollama/vLLM; write a `0600` worker `.env`. | | `basis-worker register` | `setup-worker.mjs --register` | `POST /api/workers/register` with the **public** reward wallet. | | `basis-worker price publish` | `publish-prices.mjs` | Sign a price/model manifest with the reward wallet + `POST /api/workers/prices`. | | `basis-worker canary` | `pnpm worker:canary` | Prove the orchestrator ⇄ worker ⇄ `/v1/dispatch` path locally. | | `basis-worker status` | `verify-worker.mjs` / `healthcheck.mjs` | `GET /api/workers/:id` — is the worker online + its reward ledger. | | `basis-worker stop` | Ctrl-C / `docker compose … down` | Stop the worker (clean `disconnect`; no key, no on-chain state). | Optional shell aliases (for convenience only — the scripts are the truth): ```bash alias basis-worker-setup='node .agents/skills/basis-worker/scripts/setup-worker.mjs' alias basis-worker-publish='node .agents/skills/basis-worker/scripts/publish-prices.mjs' alias basis-worker-status='node .agents/skills/basis-worker/scripts/verify-worker.mjs' ``` ## SAFETY RULES (read first — these are non-negotiable) - **Never ask for, accept, generate, or store a seed phrase or private key.** The reward wallet is a **PUBLIC address only** (`0x` + 40 hex). If a user pastes a 64-hex / 0x64-hex string or a 12/24-word phrase, **STOP** and tell them never to share it — it is not needed. - **Signing happens only in the user's own wallet** (Privy-linked or a browser wallet at basis.watch). This skill and the worker **sign nothing** and hold no key. - **The worker auth token is a SECRET stored LOCAL-ONLY** in a `0600`-permission `.env` on the worker host. **Never** print it, paste it into chat, write it to the repo, or commit it. Never echo the values of `BASIS_WORKER_TOKEN`, `BASIS_WORKER_AUTH_KEY`, `BASIS_ORCHESTRATOR_SECRET`, or `INTERNAL_API_SECRET`. - **Never log secrets.** The worker prints the **public** wallet only; the auth token is never logged. Keep it that way — do not add logging that would expose a token/key. - **`.env` is gitignored — keep it that way.** Never `git add` a worker `.env`. - **No financial promises.** Do not say "earn now", "passive income", "yield", "APY", "guaranteed rewards/returns", or "risk-free". Rewards are payment for completed, verified work and are **gated/not live**. - **If anything is unsure or a step would expose a secret or sign a transaction, STOP and ask the operator.** Do not guess at a private key, a production secret, or an on-chain action. ## Prerequisites - **Node.js ≥ 20** on the worker host (`node -v`). - A **public EVM wallet address** for rewards (obtained below — never a key). - For **real** model inference, one of: - **[Ollama](https://ollama.com)** (easiest; OpenAI-compatible API on `:11434`), or - **[vLLM](https://docs.vllm.ai)** (`vllm/vllm-openai`, OpenAI-compatible API on `:8000`). - For GPU passthrough in containers: the **NVIDIA Container Toolkit** + Docker, so `docker run --gpus all …` can see the GPU. - For a **GPU-free smoke test** you need none of the above — use the `echo` (simulator) runtime. Run the detector to report OS / GPU / Docker / Ollama / vLLM and what is missing: ```bash node .agents/skills/basis-worker/scripts/setup-worker.mjs --check # or the shell entrypoint (delegates to the .mjs): bash .agents/skills/basis-worker/scripts/setup-worker.sh --check ``` ## Wallet setup (PUBLIC address only — never a key) The reward wallet is **identity only**: the destination for any future $BASIS reward. It is **public** and is **not** a key. 1. Open **https://basis.watch** and connect a wallet — either a **Privy**-linked wallet (email/social → embedded wallet) or an **injected browser wallet** (MetaMask, Coinbase Wallet, etc.). 2. Copy the wallet's **public address** (`0x` + 40 hex). 3. Use that address as `BASIS_WORKER_WALLET`. Never request, accept, or store the wallet's private key or seed phrase. Signing (if ever needed, e.g. claiming a future reward) happens in the user's wallet at basis.watch — not here. ## Register the worker Registration records the worker + its **public** reward wallet (it persists to the durable Basis store; the EVM address is strictly validated): ```bash node .agents/skills/basis-worker/scripts/setup-worker.mjs \ --register \ --wallet 0xYourPublicEvmAddress0000000000000000000000 \ --worker-id my-gpu-1 \ --base https://basis.watch ``` This `POST`s to `https://basis.watch/api/workers/register`: ```json { "worker_id": "my-gpu-1", "worker_wallet": "0x…", "runtime": "ollama", "models": ["basis-default"] } ``` A `400` means a malformed wallet (must be a valid EVM address). When `BASIS_AUTH_REQUIRED` is enabled on the server, registration must be authenticated and the wallet must be one of the caller's Privy-linked wallets (`403 wallet_not_linked`) — pre-launch this is open. ## Generate the worker token / config Generate a **local-only** `.env` from the template (never committed, `chmod 600`): ```bash node .agents/skills/basis-worker/scripts/setup-worker.mjs \ --write-env \ --wallet 0xYourPublicEvmAddress0000000000000000000000 \ --runtime ollama \ --orchestrator wss://your-orchestrator.example \ --out ./worker.env chmod 600 ./worker.env # the generator also does this; verify it ``` Worker handshake **token precedence** (the worker derives it; you do not paste a key into chat): 1. `BASIS_WORKER_TOKEN` — a pre-minted or allowlisted token the operator gave you. 2. else `BASIS_WORKER_AUTH_KEY` — the shared HMAC key the operator set on the orchestrator; the worker derives a **wallet-bound** token locally (`.`) so a leaked token can't register a different wallet. **Recommended** against a configured orchestrator. 3. else a **dev-only** default (`basis-worker:`) — accepted only by a dev-mode orchestrator with no worker-auth configured (use for local testing). `BASIS_WORKER_TOKEN`, `BASIS_WORKER_AUTH_KEY`, and `BASIS_ORCHESTRATOR_SECRET` are **secrets**: they live in the `0600` `.env` only, are never logged, and are never pasted into chat or the repo. The generated `.env` leaves them blank with a note — the operator fills them in on the host. ## Choose a runtime | Runtime | GPU? | Use when | |---|---|---| | **`echo` (simulator)** | No | Prove the pipeline end to end with no model (dev smoke). Streams a deterministic canned response. | | **`ollama`** | Yes (recommended) | Easiest real inference. OpenAI-compatible API on `:11434`. | | **`vllm`** | Yes | High-throughput serving via an OpenAI-compatible API (`vllm/vllm-openai` on `:8000`). **Not a direct `BASIS_WORKER_RUNTIME` value today** — the worker CLI speaks `ollama`/`echo`; bridge vLLM via an Ollama-compatible shim or the orchestrator backend (see note below). | **Ollama (Docker, GPU):** ```bash # Needs the NVIDIA Container Toolkit so --gpus all works. docker run -d --gpus all -p 11434:11434 -v ollama:/root/.ollama --name ollama ollama/ollama docker exec ollama ollama pull llama3.2 ``` Or use the bundled compose (Ollama + worker, with GPU device reservations): ```bash # fill ./worker.env first (BASIS_WORKER_WALLET etc.), then: docker compose -f .agents/skills/basis-worker/templates/docker-compose.worker.yml --env-file ./worker.env up -d ``` **vLLM (Docker, GPU):** ```bash docker run -d --gpus all -p 8000:8000 --name vllm \ vllm/vllm-openai:latest --model mistralai/Mistral-7B-Instruct-v0.3 # OpenAI-compatible API at http://localhost:8000/v1 ``` **H100 note:** a single H100 (80GB) comfortably serves a 7B–13B model at high throughput; larger models (70B+) need tensor-parallel across multiple GPUs (`--tensor-parallel-size N` for vLLM). Start with a 7B/8B instruct model to verify the worker, then scale the model to fit the card. > The Basis worker CLI (`runtime/worker`) currently speaks to **Ollama** (or the > GPU-free `echo` runtime) directly. For a **vLLM** backend today, run vLLM as your > OpenAI-compatible server and point an Ollama-compatible shim or the orchestrator > backend at it; the simplest verified path is **Ollama** or **`echo`**. (See > `resources/troubleshooting.md`.) ## GPU checks ```bash nvidia-smi # GPU present + driver + free VRAM docker info | grep -i runtime # nvidia runtime available (Container Toolkit) node .agents/skills/basis-worker/scripts/setup-worker.mjs --check ``` No NVIDIA GPU? You can still run the **`echo`** runtime (CPU, no model) to validate registration + the orchestrator path. (Apple Silicon / AMD users: run Ollama natively on the host rather than via `--gpus all`.) ## Install deps & start The worker is the standalone CLI in **`runtime/worker`** (its own `package.json`/`node_modules`, NOT part of the pnpm workspace). Do not import it from `apps/web`. ```bash cd runtime/worker npm install # GPU-free smoke (no model, proves the pipeline): BASIS_WORKER_WALLET=0xYourPublicEvmAddress0000000000000000000000 \ BASIS_WORKER_RUNTIME=echo \ npm run dev # Real inference via Ollama: BASIS_WORKER_WALLET=0xYourPublicEvmAddress0000000000000000000000 \ BASIS_WORKER_RUNTIME=ollama \ BASIS_OLLAMA_MODEL=llama3.2 \ BASIS_WORKER_MODELS=llama3.2 \ BASIS_ORCHESTRATOR_URL=wss://your-orchestrator.example \ npm run dev ``` Or load the `0600` `.env` you generated: `cd runtime/worker && node --env-file ../../worker.env $(npm pkg get scripts.dev …)` — simplest is to copy `worker.env` to `runtime/worker/.env` (gitignored) and run `npm run dev`. Need an orchestrator? Self-host `runtime/orchestrator` (`npm install && npm run dev` on `:8787`) or point `BASIS_ORCHESTRATOR_URL` at one the operator gave you. There is **no hosted orchestrator for arbitrary contributors yet** — this is the "routing being activated" state. ## Verify heartbeat / online Liveness to the orchestrator is the **persistent Socket.io connection** (its transport ping/pong keeps the link alive; a `disconnect` = offline). The worker also prints an operator-facing heartbeat log (~every 30s: connected / worker id / idle vs busy / jobs completed). Check the worker's record over HTTP: ```bash node .agents/skills/basis-worker/scripts/verify-worker.mjs \ --worker-id my-gpu-1 --base https://basis.watch # GET /api/workers/:id → { worker: { online, jobsCompleted, … } } node .agents/skills/basis-worker/scripts/healthcheck.mjs --worker-id my-gpu-1 ``` `online` is derived against a heartbeat window, so a stale worker reads offline. ## Run the canary (prove the path end to end) From the repo root, the local canary spawns a **real** orchestrator + a **real** `echo` worker with throwaway secrets and a throwaway wallet, then drives the gateway's `POST /v1/dispatch` path: ```bash pnpm worker:canary ``` It proves: no-worker → `503 no_worker_available`; an unauthenticated worker is **refused**; an authenticated `echo` worker registers + shows online; a dispatched job returns `200` with `x-basis-worker-id` / `x-basis-worker-wallet` / `x-basis-payable` headers. Everything is local + throwaway — no real wallet, no real secret, no key, no on-chain action. ## Broadcast a price/model manifest (`price publish`) A registered worker tells the market which models it serves and what it asks per 1M tokens by publishing a **SIGNED** manifest to `POST https://basis.watch/api/workers/prices`. Basis verifies the signature recovers the reward wallet, the prices are within the rails, and the manifest is not expired, then stores an immutable offer. The offer starts **`self_reported`**; a Basis canary can later mark it **`verified`**. **Pricing is in USDC-pegged CREDITS.** One **whole credit = 1e6 raw micro-credits**, pegged at **$0.001 / credit**. Prices are **raw micro-credits per 1,000,000 tokens**, as decimal **strings** (never floats). So 460 whole credits/1M = `460000000` raw. The credit price is stable in USD; only credits→ $BASIS settles dynamically at quote time (shown as an estimate, never a promise). **Ask-price rails (server-enforced — an out-of-range price is rejected):** | Rail | Raw / 1M | Whole credits | ~USD/1M | |---|---|---|---| | Prompt floor | `10000000` | 10 | $0.01 | | Output floor | `20000000` | 20 | $0.02 | | Max (both sides) | `12000000000` | 12000 | $12 | | `min_job_credits` floor | `50000000` | 50 | $0.05 | | **Network default ask** | `460000000` prompt / `1840000000` output | 460 / 1840 | $0.40 / $1.60 | A price above ~`6000000000` ($6/1M) is **flagged (high)** but still routable. **Steps:** 1. Edit a **local** prices config from the template (`templates/prices.example.json`): your `hardware` (self-reported), and one or more `models` (`model_id`, `runtime`, `prompt_credits_per_1m`, `output_credits_per_1m`, `min_job_credits`, `max_context_tokens`, `max_output_tokens`, `concurrency`). Start from the network default ask. 2. Make sure the **reward-wallet PRIVATE KEY is in a LOCAL source you control** — a `0600` key file, an env var, or a macOS Keychain ref. **Never** pass it on the command line, print it, or commit it. (This is the one place the flow signs; the worker daemon itself still holds no key.) 3. Sign + publish: ```bash # Recommended: a 0600 file holding ONLY the reward-wallet hex key. chmod 600 ./worker-reward.key node .agents/skills/basis-worker/scripts/publish-prices.mjs \ --worker-id my-gpu-1 \ --prices ./prices.json \ --key-file ./worker-reward.key \ --base https://basis.watch # Or read the key from an env var (default BASIS_WORKER_PRIVATE_KEY): # --key-env BASIS_WORKER_PRIVATE_KEY # Or a macOS Keychain generic-password ref: # --keychain basis-worker-reward-key:my-gpu-1 # Build + sign + self-verify WITHOUT posting: # --dry-run ``` The script **signs locally** (EIP-191 `personal_sign` over the canonical sorted-key JSON of the manifest with the `signature` field excluded), self-checks the signature, and POSTs the snake_case manifest. It prints the **public** `offerId` / `priceVersion` / `state` — **never** the private key. > **Key safety.** The private key is read from a local file / env / Keychain and > used only to compute one signature in memory; it is **never** printed, logged, > written, committed, or sent over the wire. Only the public signature + the public > reward address leave the machine. The script **refuses** a key passed on the > command line (it would land in shell history). `viem` is required for signing — > it is already a dependency of `apps/web`; in the standalone worker runtime run > `npm i viem` first. **Verification states (honest):** - **`self_reported`** — valid signature + within rails + not expired. Capability is **NOT** cryptographically proven (free-form text can't prove which model produced it). - **`verified`** — additionally passed a recent Basis deterministic canary (throughput + coherence). **`verified` ≠ proof of model identity**; hardware and exact model identity stay self-reported. - **`stale`** — the worker stopped heartbeating (read-time overlay). - **`expired`** — `valid_until` passed. The reward wallet that signs the manifest **must match** the registered worker wallet (a stranger can't publish prices for someone else's worker), and when `BASIS_AUTH_REQUIRED` is on it must be one of the caller's Privy-linked wallets. **Publishing a price is not earning** — workers earn **only** for completed, verified, worker-routed jobs (see below). There is no guaranteed income, no yield, and no passive income from publishing an offer. ## Rewards + settlement (honest framing — no earnings yet) - A **COMPLETED, anti-cheat-verified, worker-routed** job with a **valid EVM reward wallet** accrues a **pending, off-chain** $BASIS reward. - **`failed` / `rejected` jobs pay 0.** **Azure/gateway-served jobs pay 0** (no contributor wallet). Only worker-routed completions earn. - The orchestrator's **anti-cheat verdict** (`payable`) is authoritative — output faster than any real GPU, or incoherent/garbage output, is **not payable**. - **On-chain payout/claiming is GATED (dry-run).** Rewards are computed + tracked off-chain but are **not yet claimable on-chain** (settlement is held behind the settlement gates). Reward amounts are integer base units as decimal strings. - **Worker job ROUTING is OPT-IN and currently OFF in production**, so a registered worker **does not receive production jobs yet** ("registration ready / routing being activated"). Tell the user this plainly. - Rewards are **payment for completed verified work — not earnings on a holding, not yield, not passive income, not guaranteed.** Inspect a worker's own state + reward ledger: ```bash # GET /api/workers/:id → worker record # GET /api/workers/:id/rewards → reward ledger (pending / settled) ``` ## Troubleshooting See `resources/troubleshooting.md`. Common cases: `nvidia-smi`/`--gpus all` not working (install the NVIDIA Container Toolkit), worker exits immediately (`BASIS_WORKER_WALLET` missing/malformed → must be `0x` + 40 hex), handshake refused (token/auth-key mismatch with the orchestrator), `503 no_worker_available` (no eligible idle worker is connected), worker never online (orchestrator URL wrong / firewall / not running). ## Stop the worker ```bash # Foreground CLI: Ctrl-C. # Docker: docker compose -f .agents/skills/basis-worker/templates/docker-compose.worker.yml down docker rm -f ollama vllm 2>/dev/null || true ``` Stopping is safe and immediate: the orchestrator treats the `disconnect` as the worker going offline; no on-chain state and no key are involved. ## Security checklist (verify before "done") - [ ] No private key / seed phrase was ever requested, generated, printed, or committed. The reward wallet is a **public** address in registration/config; the only signing step (`price publish`) reads the key from a **local** `0600` file / env / Keychain, uses it in memory, and **never** prints or stores it. - [ ] `BASIS_WORKER_WALLET` is a **public** `0x`+40-hex address. - [ ] The worker `.env` and any reward-wallet key file are `0600` and **not** committed (gitignored). No key was passed on the command line. - [ ] No secret (`BASIS_WORKER_TOKEN` / `BASIS_WORKER_AUTH_KEY` / `BASIS_ORCHESTRATOR_SECRET` / `INTERNAL_API_SECRET`) was printed, logged, or pasted into chat/the repo. - [ ] No earnings/yield/APY/passive-income/guaranteed-return language was used — publishing a price is **not** earning; only completed verified jobs pay. - [ ] The user was told routing is being activated (no production jobs yet) and settlement is gated (no on-chain payout yet). ## Grounding / sources - Claude Code Agent Skill format (frontmatter `name` = dir name, lowercase-kebab, <40 chars + `description` with the activation trigger; concise actionable body): https://code.claude.com/docs/en/skills · https://agentskills.io/specification - vLLM Docker (OpenAI-compatible, `--gpus all`, `:8000`): https://docs.vllm.ai/en/stable/deployment/docker/ - Ollama + NVIDIA Container Toolkit + `--gpus all` (OpenAI-compatible, `:11434`): https://ollama.com · https://www.aimadetools.com/blog/ollama-docker-setup-guide/ - Basis worker internals: `runtime/worker/README.md`, `docs/inference/WORKER_SETUP.md`, `scripts/worker-canary.mjs`, `apps/web/app/api/workers/*`. - Market price manifest (canonical signing + rails + verification states): `apps/web/app/api/workers/prices/route.ts`, `apps/web/lib/workers/offers.ts` (`canonicalOfferMessage`/`stableStringify`), `apps/web/lib/workers/offer-rails.ts`.