Join our community of builders on Discord!

Run a Worker on Testnet

Workers are the supply side of the Lightchain AI network. A worker runs whitelisted models via Ollama alongside the Worker Sidecar (shipped as a Docker image), claims inference sessions directly on-chain, streams answers to users through the public Worker Gateway, and earns fees for every completed job. This guide walks through the end-to-end lifecycle on Testnet — from generating a worker key and funding it to registering on-chain, going live, and deregistering when you're done. Testnet runs in dispatcher-free mode: no central service hands out jobs. Your worker watches the chain, wins sessions by sortition, and serves them itself — see Dispatcher-free Mode for how that works. Everything below configures the external profile from that page: no Redis, no VPN, no inbound ports.

Prerequisites

  • Docker installed (the image is linux/amd64)
  • GPU machine with enough VRAM for your model (8 GB+ for llama3-8b)
  • Ollama installed and running
  • Funder wallet holding at least 5,050 LCAI (5,000 stake + a gas buffer for claim, ack, blob and completion transactions) — this is your existing wallet that pays to bring the worker online. It is NOT the worker key. You will generate a fresh worker key in Step 0 and send LCAI from the funder to it in Step 5.
  • Foundry installed (cast is used for key generation, balance checks, and contract reads)
  • Outbound HTTPS/WSS — nothing needs to be opened inbound

Network reference

ServiceValue
RPC URLhttps://rpc.testnet.lightchain.ai
Chain ID8200
Beacon APIhttps://beacon.testnet.lightchain.ai
Worker Gatewayhttps://worker-gateway.testnet.lightchain.ai
Worker imageregistry.lightchain.ai/testnet/worker:latest (anonymous pull)

Resolve testnet contract addresses

AIConfig, JobRegistry and SessionManager are deployed at runtime, so their addresses depend on the environment. Resolve them once from the predeployed WorkerRegistry and export them — the env file in Step 4 picks them up from your shell:
CodeBASH

Step 0: Generate a fresh worker key

Always use a dedicated, brand-new key for the worker. Do not reuse your funder key — the worker key sits in a Docker container with the keystore password, and should hold only the working capital (stake + small gas buffer) you're comfortable exposing to that machine.
CodeBASH
Copy the printed Address and Private key, then export them:
CodeBASH
Sanity check — the privkey must derive to the address you'll be funding and registering:
CodeBASH
The output must equal $WORKER_ADDR. If it doesn't, stop and re-export — anything sent to the wrong address is unrecoverable, and registering with mismatched keys locks the stake on the wrong account.
Do not set WORKER_PRIVKEY to your funder's private key. The two must be different. The funder pays from your existing wallet (Step 5); the worker is the new key from this step.

Step 1: Install Ollama and pull the model

On-chain model names are the Ollama tags, used verbatim (the on-chain modelId is keccak256(name)). Pick from Whitelisted models and pull each one into Ollama. Only llama3-8b and llama3-70b are not native Ollama tags — create them as aliases:
CodeBASH

Step 2: Pull the worker image

CodeBASH
The image contains two binaries: /bin/worker (the sidecar, default entrypoint) and /bin/lightchain-worker (the operator CLI for keys, registration, preflight, drain, balance and release).

Step 3: Import the worker private key

Imports $WORKER_PRIVKEY (set in Step 0) into an encrypted keystore on disk:
CodeBASH
The Address: printed by the command must equal $WORKER_ADDR from Step 0. If they differ, stop — the wrong key was imported.

Step 4: Write the worker env file

The sidecar and the CLI read the same variables, so keep them in one file. Write it from the shell where you resolved the addresses (the $… values expand from your exports):
CodeBASH
Replace <YOUR_PASSWORD> with the keystore password from Step 3. Every docker run below passes this file with --env-file.

Generate the ECDH encryption key

Every worker advertises an encryption public key on-chain so users can encrypt prompts for it — keygen produces that pair locally:
CodeBASH
The encryption private key stays on the machine running inference — it is what decrypts incoming prompts. Back it up together with the keystore: sessions encrypted to a lost ECDH key cannot be served.

Step 5: Fund the worker from your funder wallet

Send LCAI from your funder wallet (the existing one mentioned in Prerequisites) to $WORKER_ADDR (the fresh address from Step 0). The worker needs at least 5,050 LCAI: 5,000 for the minimum stake plus a gas buffer — in dispatcher-free mode the worker also pays for its own claimSession transactions, so keep more than the ~1 LCAI the old dispatcher flow needed. If your funder wallet is in MetaMask/Rabby, send the transfer through the wallet UI. If you have its private key locally and want to use Foundry:
CodeBASH
Verify the worker received the funds:
CodeBASH
Common mistake: setting FUNDER_PRIVKEY to the same value as WORKER_PRIVKEY. That sends LCAI from the worker to itself (a no-op minus gas) and the worker stays unfunded. Cross-check with cast wallet address --private-key "$FUNDER_PRIVKEY" — it must NOT equal $WORKER_ADDR.

Step 6: Register on-chain

CodeBASH
This will:
  • Stake 5,000 LCAI (auto-queried from AIConfig)
  • Register your ECDH public key on-chain
  • Add every model in SUPPORTED_MODELS to your supported models
You can only serve models that are currently whitelisted on AIConfig. Attempting to register for a delisted or non-existent model will revert.

Step 7: Preflight

preflight is a read-only check of everything above in one go — RPC and chain id, registration, suspension, stake against the live on-chain minimum, wallet balance, local ECDH key vs the on-chain key, each model's whitelist / enabled / added state, gateway login, Ollama tags, and the beacon API. It prints one line per check and exits 1 if anything fails, so it is safe to run as often as you like:
CodeBASH
A healthy worker looks like this:
CodeTEXT
Run it before Step 6 too: with an unfunded, unregistered address it tells you exactly how much to send and whether your model names are whitelisted. preflight --worker 0xADDRESS inspects any address without a keystore — paste its output when asking for help.

Step 8: Run the worker

The --add-host flag below makes host.docker.internal resolve to the Docker host on Linux (where it isn't provided by default). On macOS and Windows it's a no-op — Docker Desktop already maps that hostname — so the same command works everywhere.
CodeBASH

Step 9: Verify it's working

CodeBASH
The worker emits structured JSON logs. A healthy startup shows this sequence (timestamps and addresses elided):
CodeTEXT
CodeBASH
From here the worker watches the chain for session requests it is eligible for, self-claims via sortition, runs inference on your Ollama, streams tokens through the gateway, and submits the authoritative result as an on-chain blob. A quiet log is normal while other workers win the draws — there is no "job received" line until your worker wins a session. Then you'll see claimed session request followed by the per-job stages:
CodeBASH
If a registered worker never claims, run preflight (Step 7) first — the usual causes are a model-name mismatch or a suspension.

Whitelisted models

All of these are registered on AIConfig and whitelisted on WorkerRegistry (values as of 2026-09-13; read the live fee with cast call $AI_CONFIG_ADDRESS "getModelFee(bytes32)(uint256)" $(cast keccak <name>)):
Model (on-chain name = Ollama tag)Fee per jobMax output tokens
llama3-8b0.02 LCAI2,048
llama3-70b0.15 LCAI4,096
gemma4:e2b0.02 LCAI2,048
glm-4.7-flash0.02 LCAI8,192
qwen3-coder-next0.05 LCAI16,384
gpt-oss:20b0.04 LCAI8,192
gpt-oss:120b0.20 LCAI8,192
qwen3-vl:8b0.02 LCAI4,096
qwen3-vl:30b0.08 LCAI4,096
qwen3-embedding:0.6b0.005 LCAI1
tts-piper0.02 LCAI2,048
To serve several models, list them comma-separated in SUPPORTED_MODELS (for example SUPPORTED_MODELS=llama3-8b,gemma4:e2b), pull each into Ollama, and re-run register (or add-models for a worker that is already registered). Every model name must match the on-chain name exactly — a different string hashes to a different modelId and the registration reverts with ModelNotWhitelisted.

Rewards and fund handling

Per-job fees earned by the worker are paid out directly to the worker's wallet ($WORKER_ADDR from Step 0) as jobs complete. There is no separate payout address registered on-chain and no automatic forwarding to the funder wallet — earnings simply accumulate on the worker key. Because the worker key lives inside the Docker container alongside its keystore password, you should not treat it as long-term storage. Sweep accumulated fees to your own designated wallet (typically the funder, a hardware wallet, or any cold-storage address you control) on whatever cadence matches your risk tolerance. Check the worker's current balance:
CodeBASH
Sweep funds from the worker to your designated wallet, leaving a gas buffer behind so the sidecar can keep paying for its claim, ack, blob and completion transactions:
CodeBASH
Do not drain the worker wallet to zero while the worker is registered and running — it needs gas to claim sessions and to ack and complete jobs. A worker that cannot pay for a transaction misses its deadline, and missed deadlines lead to slashing (see Slashing & Rehabilitation). Top up from your funder if it dips.
The staked 5,000 LCAI is held by WorkerRegistry, not by the worker wallet, and is only released when you deregister (see Deregister and withdraw stake).

Check registration status

preflight (Step 7) is the most complete check. For the bare on-chain registration flags:
CodeBASH

Slashing and suspension

Workers that miss deadlines or lose disputes get their stake slashed. After three offenses the worker is automatically suspended for 7 days and must call WorkerRegistry.reinstate() to come back online. During the current dispatcher-free transition the slash rates are set to 0 on testnet, but offences are still recorded. Full mechanics, live rates, and the step-by-step recovery runbook are in Slashing & Rehabilitation.

Stopping and draining

In dispatcher-free mode a running worker keeps claiming sessions until the process stops, so drain alone does not take it out of rotation. Stop it with a grace period longer than the sidecar's shutdown timeout:
CodeBASH
On SIGTERM the worker sets its drain marker via the gateway, stops claiming, and gives in-flight jobs up to 30 s to finish. Later jobs in sessions you already claimed still target your worker and time out while it is offline. See Dispatcher-free Mode → Stopping, then follow Drain & Graceful Exit for the settlement and deregister sequence — deregister reverts with ActiveJobsExist until the dispute window plus a release cycle has passed.

Deregister and withdraw stake

Run this only after confirming activeJobsCount is 0 — otherwise it reverts with ActiveJobsExist. See Drain & Graceful Exit for the full flow.
CodeBASH
This removes your worker from the registry and returns your staked LCAI (minus any slashing penalties).

Stop the worker

Only safe after deregister succeeded — see Drain & Graceful Exit for why removing the container earlier can orphan claimable earnings.
CodeBASH

Troubleshooting

SymptomCause / fix
SESSION_MANAGER_ADDRESS is required on startSORTITION_ENABLED=true without the address — re-run the resolve step and fix the env file
authenticate with worker-gateway: … 403 worker not registered on-chainRegistration missing or on a different address — re-run Step 6, check keystore path/password
gateway stream dial failed … retryIn …Gateway unreachable — outbound WSS blocked? The worker retries with backoff; jobs still settle on-chain
Registered but never claimsRun preflight. Usual causes: model mismatch (keccak256(SUPPORTED_MODELS entry) must equal a whitelisted, enabled ID that you have added), suspension (getOffenseCount / isWorkerSuspended on WorkerRegistry), or simply other workers winning the draws
InsufficientStake on registerBalance below 5,000 LCAI + gas
Frames rejected / connection closed by gatewayYou're streaming for a session you don't own (should never happen with an unmodified worker) — the gateway closes after 3 violations; the worker reconnects automatically

Testnet contract addresses

ContractAddress
WorkerRegistry0x0000000000000000000000000000000000001002 (predeploy)
AIConfigResolve from WorkerRegistry — see Resolve testnet contract addresses
JobRegistryResolve from WorkerRegistry — see Resolve testnet contract addresses
SessionManagerResolve from AIConfig.getSessionManagerAddress() — see Resolve testnet contract addresses
FeePool0x0000000000000000000000000000000000001004 (predeploy)