Skip to main content

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.

warning

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.

info

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.

PlaceholderWhat it isWhere 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.
info

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.

  1. Open the dashboard and connect your owner wallet.

  2. 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.

  3. 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>.

  4. 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.

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-url is 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.
  • --models restricts 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=25 is 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 set zs.escrow_app_id on 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-mini clears it on the id alone, which is why nothing extra appears above. Recognized provider-qualified forms such as x-ai/grok-4.5 and google/gemini-2.5-pro clear the same curated whitelist entry; arbitrary org/model strings do not. Anything else — a self-hosted open-weights model, a bare GGUF file stem — needs an explicit source: "hf:org/model", and the wizard writes it for you. This is also why default_pricing on its own is not enough to serve whatever your upstream's /v1/models happens 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_path must 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=strict makes it the one writable path); under Docker it's the /app/data volume. 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

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.

warning

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/healthz

    No response? The process isn't up — check journalctl -u zs-node or docker logs zs-node. The most common first-run stoppers are a missing or mistyped OPERATOR_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 | jq

    Check that operator_id matches 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 | jq

    Empty list? Either the node can't reach the upstream — check base_url and NODE_LLM_OPENAI_API_KEY — or your models were dropped by the identity gate. zs-node doctor tells you which: it reports the backend unreachable in the first case, and fails models pass the provenance gate in the second. See Health & compatibility.

  • The sealed-ingress gate is live. A plaintext POST to /v1/responses should 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"}' | jq

    Expect 400 with code bad_envelope, naming the required application/vnd.zs+json content 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).

info

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