Quick start
This page walks you from nothing to a registered node serving its first paid request — the operator equivalent of the user's Quick start.
The shape is: gather a handful of values, register on-chain, install the node, generate a config against your backend, load your signing key, start, verify, then take traffic. Each step links to the page that covers it in full. Plan on 20–30 minutes the first time.
This is mainnet, and the stake is real USDC. Registering locks $250 USDC in your owner account, and your signing account spends real ALGO on every request it serves. The safety net isn't a test network — it's the staging flag: register your node staged (step 2), prove the whole loop end to end, and only then join normal routing. Steps 2 and 8 do exactly that.
This is the happy path with the simplest backend — openai_passthrough to
OpenAI, no GPU required. To serve your own models on your own hardware, point the
wizard at your own backend at step 4 instead (see
Serving models & pricing).
What you'll fill in
Every command and config snippet below uses <angle-bracket> placeholders.
Gather these values once and substitute them as you go — when you see the same
placeholder twice, it's the same value. Nothing else in the snippets needs to
change.
| Placeholder | What it is | Where it comes from |
|---|---|---|
<openai-key> | Your OpenAI API key (sk-...). | platform.openai.com |
<signing-mnemonic> | The 25 words of a fresh Algorand account, space-separated. | Generate one (step 1); never reuse a wallet you care about. |
<your-host> | Your node's public hostname, e.g. node.example.com. | DNS you control (step 1). |
<operator-id> | Your on-chain operator id. | Returned by registration (step 2). |
<node-id> | This node's id under that operator. | Returned by registration (step 2). |
Prerequisites
Before you start, have these ready:
- A host. For passthrough you only need a small VM — the model runs upstream, so no GPU. 1–2 vCPU and 2 GB RAM covers it. (Don't size a container much below that: the node idles in the low hundreds of MB but spikes when it converts a web page — see Sizing the node process. To run models locally you'll want a GPU, and the sizing is a different exercise entirely — see What the inference backend needs.)
- A public HTTPS endpoint (
<your-host>). Clients and other operators must reach your node at a publicly reachable URL over HTTPS (e.g.https://node.example.com). Bind the node to a public interface and serve HTTPS directly with built-in ACME (tls.mode: acme), or terminate TLS at a single reverse proxy in front of it — not a load balancer across multiple instances. Have the hostname planned now; see Installation. - Two Algorand accounts. An owner account (cold — receives your USDC
payouts and owns the operator record; this is the wallet you connect in the
dashboard) and a signing account (hot — its key lives on the node and
signs tickets and receipts). Generate the signing account fresh; don't
reuse a wallet you care about — for example with the
Algorand CLI or
any wallet that can export a 25-word mnemonic, then keep those 25 words as your
<signing-mnemonic>. See Encryption & keys. - USDC for your stake, in the owner account, opted in. Registering locks $250 USDC for the operator; your first node is covered by that stake, and each additional node locks $25 more. It's locked while you're registered and returned when you deregister in good standing. The owner account must be opted into the USDC asset — that's how it receives payouts, and the node refuses to start if it isn't. See Staking & economics.
- ALGO on both accounts. The owner needs a little for the operator-box
minimum balance (~0.1 ALGO) plus transaction fees. The signing account must
hold at least 1 ALGO — that's a hard boot floor, and the node exits below it.
It pays the per-request network fees on
open()and settle (roughly 7,000 µALGO per paid request, via fee pooling, so the payer's net ALGO cost is zero), so fund it well above the floor and top it up. The node never auto-funds. See The payment flow. - An OpenAI API key (
<openai-key>), for this passthrough walkthrough.
That's the full shopping list. Everything below is copy-paste once you've substituted the placeholders from What you'll fill in.
Register on-chain
Registration comes first. The node is configured with the ids the contract hands back — it doesn't register itself, and it won't load a config without real ids. Today this is done through the operator dashboard (a wallet-connect UI), not a CLI command:
You write two records — a cold operator identity, and a node record per running endpoint under it.
-
Open the dashboard and connect your owner wallet.
-
Use Register Operator. This one is your top-level identity: the connected wallet becomes the owner address that receives your payouts, and you can optionally link an NFD so you show a name instead of a bare address — type it (
myoperator.algo), and the dashboard resolves it, checks that your wallet owns it, and shows the avatar back to you before you sign. Leave it blank to run nameless; see Your name and avatar. -
Your wallet signs. The transaction funds the operator box and escrows the USDC stake, and the contract returns a fresh operator id — your
<operator-id>. -
Now add a node under that operator. This is the record clients actually route to, and it's where the endpoint details live:
- your signing address (the address your
<signing-mnemonic>derives to), - your public base URL (
https://<your-host>, max 248 bytes), and - the staging flag — set it. A staging node is registered and reachable but held out of normal routing, so you can prove the whole loop before real users reach it. You'll clear it in step 8. See Staging nodes.
The contract returns a node id — your
<node-id>. Your first node needs no additional stake. - your signing address (the address your
What each record contains and what each field commits you to is covered in Registering on-chain. The owner address, NFD, and signing address are read from the on-chain boxes at startup — you only set the ids in config.
Install the node
Get the zs-node binary onto your host. You want it locally either way: the next
two steps (zs-node init, zs-node doctor) are subcommands of this same binary.
# Linux / macOS — one line; verifies the checksum, no sudo
curl -fsSL https://zerosignal.ai/install.sh | sh -s -- zs-node
# macOS via Homebrew
brew install txnlab/tap/zs-node
# Windows
scoop bucket add txnlab https://github.com/txnlab/scoop-bucket
scoop install zs-node
To do it by hand instead, grab the archive for your platform from the
latest release, verify it
against checksums.txt, and install it:
sha256sum -c checksums.txt --ignore-missing
tar -xzf zs-node_<version>_linux_amd64.tar.gz
sudo install -m 755 zs-node /usr/local/bin/zs-node
Then set up a config directory to hold the two files the next steps create:
sudo mkdir -p /etc/zerosignal
The rest of this guide assumes config.yaml and secrets.env live in
/etc/zerosignal/. Use any path you like — just keep it consistent.
Docker is also fully supported (and is the simplest production deployment); step
6 gives the docker run line. Every install path, platform, and the reverse-proxy
/ TLS options are in Installation.
Generate the config
Don't hand-write config.yaml — point the wizard at your backend and let it work
out the rest. zs-node init fingerprints the runtime, finds its real API root,
discovers which endpoints it implements, probes each model for tool use /
reasoning / vision / output ceiling, looks up list pricing in the
models.dev catalog, reads your on-chain records, and writes
a config that is proven to load.
It probes with your upstream key, so export that first (the key is used only to probe — it is never written into the config):
export NODE_LLM_OPENAI_API_KEY=<openai-key>
zs-node init \
--base-url=https://api.openai.com/v1 \
--operator-id=<operator-id> \
--node-id=<node-id> \
--models=gpt-5.4-mini \
--margin=25 \
--config=/etc/zerosignal/config.yaml
A few of those flags are worth understanding:
--base-urlis the OpenAI API root, not the host — the version segment (/v1) belongs in it, because the node appends bare paths. For a backend on a non-standard root (z.ai's…/api/paas/v4), give the full prefix.--modelsrestricts what you serve. Leave it off and the wizard probes everything the upstream lists, which against OpenAI means paying for a lot of probes you don't need. Naming your models is faster and cheaper.--margin=25is how you make money. It adds 25% to the upstream's list price. Rates in the generated config are net of the protocol fee — the amount you keep — so reselling a paid backend at cost means running at a loss once your ALGO fees are counted.
The wizard writes a config shaped like this — the parts worth checking before you start:
zs:
operator_id: <operator-id>
node_id: <node-id>
max_active_tickets: 16
settlement_db_path: "/var/lib/zs-node/settlement.db"
models:
gpt-5.4-mini:
# No `source:` needed — a frontier id clears the identity gate on its own.
pricing:
# Catalog list price plus your --margin. USD per 1M tokens, net of the
# protocol fee — this is what you receive.
input_rate: 0.31
output_rate: 2.50
llm:
provider: "openai_passthrough"
openai:
base_url: "https://api.openai.com/v1"
api_key: "" # comes from NODE_LLM_OPENAI_API_KEY at runtime
Notes on what's there and what isn't:
- There's no
algod:block, and that's correct — mainnet is the default, and a mainnet node picks up the canonical escrow app id automatically. You don't setzs.escrow_app_idon a public network. - Every model needs a checkable identity. An always-on gate drops any model
that has neither from your advertised catalog. A frontier id like
gpt-5.4-miniclears it on the id alone, which is why nothing extra appears above. Recognized provider-qualified forms such asx-ai/grok-4.5andgoogle/gemini-2.5-proclear the same curated whitelist entry; arbitraryorg/modelstrings do not. Anything else — a self-hosted open-weights model, a bare GGUF file stem — needs an explicitsource: "hf:org/model", and the wizard writes it for you. This is also whydefault_pricingon its own is not enough to serve whatever your upstream's/v1/modelshappens to list: discovered ids carry no source, so only frontier-whitelisted ones survive and everything else silently disappears from your catalog. Declare the models you intend to serve. settlement_db_pathmust be durable, and the wizard asks you where. Its suggestion is a relative./settlement.db; give it an absolute path on storage that survives a restart. Under the generated systemd unit that's/var/lib/zs-node/(ProtectSystem=strictmakes it the one writable path); under Docker it's the/app/datavolume. The code default is a non-durable in-memory ledger that loses in-flight settlements on restart.- Consider adding
zs.min_charge.algo_txns: 7— the wizard doesn't set it, and without it you absorb the ~7,000 µALGO of network fees on every paid request instead of recovering them. See Configuration.
The full reference for every key is in Configuration; other backends (llama.cpp, LM Studio, vLLM, Kronk, Vertex AI, image models) are in Serving models & pricing.
Provide the signing mnemonic
The node refuses to start unless it can load the 25-word mnemonic for its signing account — it's the only secret you must provision. Put it, and your upstream key, in one secrets file:
# /etc/zerosignal/secrets.env — chmod 600, never commit this
OPERATOR_SIGNING_MNEMONIC=<signing-mnemonic>
NODE_LLM_OPENAI_API_KEY=<openai-key>
sudo chmod 600 /etc/zerosignal/secrets.env
The node picks the mnemonic up because any variable whose name ends in
_MNEMONIC is read automatically — the name itself doesn't matter.
For production, load it from a cloud secret manager via ZS_MNEMONIC_URLS
(comma-separated name=url pairs, supporting AWS Secrets Manager, AWS Parameter
Store, GCP Secret Manager, Azure Key Vault, and file://) instead of a flat
file. See Encryption & keys.
There is no encryption key to generate: the node mints a short-lived ephemeral sealing key in memory and rotates it automatically. The signing mnemonic is the only secret you provision.
Start the node
- systemd
- Docker
Let the node write and enable its own unit:
sudo zs-node install-service
sudo journalctl -u zs-node -f
It reads /etc/zerosignal/secrets.env, runs under a transient system user, and
sets a stop timeout long enough for the node to finish inference it already
accepted. sudo zs-node uninstall-service removes it. The generated unit is
reproduced in full in
Running as a service.
docker run -d \
--name zs-node \
--restart unless-stopped \
--stop-timeout 400 \
-p 9090:9090 \
-p 127.0.0.1:9091:9091 \
--env-file /etc/zerosignal/secrets.env \
-e NODE_SERVER_LISTEN=:9090 \
-e NODE_SERVER_PRIVATE_LISTEN=:9091 \
-e NODE_ZS_SETTLEMENT_DB_PATH=/app/data/settlement.db \
-v /etc/zerosignal/config.yaml:/app/config.yaml:ro \
-v zs-node-data:/app/data \
ghcr.io/txnlab/zs-node:latest --config /app/config.yaml
Everything here is literal. The image is public and multi-arch, so docker run
fetches it on first use.
--stop-timeout 400 matters: Docker's default is 10 seconds, and on stop the
node needs its full drain budget to finish the inference it already accepted —
otherwise you truncate paid requests on every restart.
docker logs -f zs-node
The binary defaults server.listen to loopback (127.0.0.1:9090), so a
container publishes nothing reachable until you bind all interfaces — set
NODE_SERVER_LISTEN=:9090 (dual-stack IPv4+IPv6), as shown above. zs-node init
writes the public bind into the config for you. Keep the private listener
(/healthz + /livez + /metrics) on loopback.
Check it came up
Start with the exam that covers all three layers at once — your config, the live backend, and the chain:
zs-node doctor
It loads your file with the daemon's own loader, fingerprints and probes the
backend, and checks that your operator and node records exist, your keystore
holds the node's signing key, the signer is funded, and the owner is opted into
USDC. The part worth running it for is the comparison: a model your config
advertises as tool-capable whose backend rejects tools[] serves a 4xx to every
tool-using client, and nothing in your logs says so. See
doctor.
Then confirm the running process, from the host:
-
Health (private listener):
curl -fsS http://127.0.0.1:9091/healthzNo response? The process isn't up — check
journalctl -u zs-nodeordocker logs zs-node. The most common first-run stoppers are a missing or mistypedOPERATOR_SIGNING_MNEMONIC, a signing balance under the 1 ALGO floor, and an owner account that isn't opted into USDC. -
Self-description — confirms identity, models, pricing, and oracle:
curl -fsS http://127.0.0.1:9090/v1/zs/details | jqCheck that
operator_idmatches your<operator-id>, there's a signed ephemeral encryption recipient block, your models carry the rates from config, and the oracle reads healthy. (Your owner and signing addresses aren't advertised here — they live in your on-chain records; verify those on the dashboard or a chain explorer.) -
Model list (always plaintext):
curl -fsS http://127.0.0.1:9090/v1/models | jqEmpty list? Either the node can't reach the upstream — check
base_urlandNODE_LLM_OPENAI_API_KEY— or your models were dropped by the identity gate.zs-node doctortells you which: it reports the backend unreachable in the first case, and failsmodels pass the provenance gatein the second. See Health & compatibility. -
The sealed-ingress gate is live. A plaintext POST to
/v1/responsesshould be rejected:curl -s -X POST http://127.0.0.1:9090/v1/responses \-H 'Content-Type: application/json' -d '{"model":"gpt-5.4-mini"}' | jqExpect
400with codebad_envelope, naming the requiredapplication/vnd.zs+jsoncontent type.
Verify end to end, then take traffic
There's no plaintext-curl smoke test for inference: every prompt-carrying request is a sealed envelope admitted via a reserve ticket, so you exercise it with a real encrypted client (the proxy, or a chat client) pointed at your staged node. Users reach a staging node only by enabling the Allow staging toggle, so a cooperating tester — or you — can drive real traffic at it without ordinary clients routing there.
Once a request reserves, runs, returns a signed receipt, and settles on-chain, clear the staging flag from the dashboard. The node joins normal rotation on clients' next probe — typically within a minute (see How often you're probed).
Registration, staging, and deregistration are all done from the dashboard UI, not node CLI commands (see what's not in the CLI). The on-chain records and what they mean are stable and documented in Registering on-chain.
What's next
- Configuration — every key and default.
- Serving models & pricing — local backends, multi-model hosts, per-model rates, and image routes.
- Installation — platforms, GPU hosts, reverse-proxy / TLS, and running as a service.
- Operations — monitoring, graceful shutdown, upgrades.
- The payment flow — reserve, open, serve, receipt, settle.