Skip to main content

Quick start

This walkthrough takes you from nothing to a registered node serving its first paid request. It's the operator counterpart of the user's Quick start. 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 is the staging flag, not a test network: mark the node staged (step 2), and clear the flag after an end-to-end test (step 8).

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 these <angle-bracket> placeholders:

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, or an NFD the node manages for you (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).
<version>The release number in the archive's file name.The release page, only if you install by hand (step 3).

Prerequisites​

  • A host. For passthrough a small VM is enough, because the model runs upstream: 1–2 vCPU and 2 GB RAM. 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 differs; see What the inference backend needs.

  • A public HTTPS endpoint (<your-host>). Clients and other operators must reach your node at a public URL over HTTPS. Bind the node to a public interface, and pick one of two paths now, because the base URL you register at step 2 depends on it:

    • You already have a hostname and a TLS front door. Terminate TLS at a single reverse proxy in front of the node (not a load balancer across multiple instances), or supply your own certificate with tls.mode: manual. See Installation.
    • You have an NFD and want the node to do all of it. tls.mode: acme gives you the hostname, the certificate, the DNS record, and the base URL on chain, all derived from your NFD and kept current. See Reaching your node.

    Either way, the base URL you register must carry the port the node actually listens on. This walkthrough keeps the server.listen: ":9090" that zs-node init writes, so the URL is https://<your-host>:9090 unless something in front of the node moves it.

  • Two Algorand accounts. An owner account (cold: receives your USDC payouts, owns the operator record, and 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, for example with the Algorand CLI or any wallet that can export a 25-word mnemonic, and keep those 25 words as your <signing-mnemonic>. See Encryption & keys. On the NFD path, read Which account signs the DNS writes before you decide which of the two wallets owns the NFD; it's the one choice here that's awkward to undo.

  • 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. The stake stays locked while you're registered and is 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, a hard boot floor; 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). To receive traffic it needs at least 5 ALGO spendable (balance minus the ~0.1 ALGO minimum balance): proxies and the web client skip a node below that. Fund it well above 5 ALGO and top it up; the node never auto-funds. See The payment flow.

  • An OpenAI API key (<openai-key>), for this passthrough walkthrough.

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 registration happens on the operator dashboard at operator.zerosignal.ai, a wallet-connect UI, not in the node CLI (see what's not in the CLI).

You write two records: a cold operator identity, and a node record per running endpoint under it.

  1. Open operator.zerosignal.ai, pick the network you're registering on, and connect your owner wallet.

  2. Use Register Operator. The connected wallet becomes the owner address that receives your payouts. You can optionally link an NFD so you show a name instead of a bare address; type it (myoperator.algo) and the dashboard shows its avatar before you sign. 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. Add a node under that operator. This is the record clients route to, and it holds the endpoint details:

    • your signing address (the address your <signing-mnemonic> derives to),
    • your public base URL: https://<your-host>:9090 for this walkthrough, which leaves the node on :9090. Register https://<your-host> with no port only if a reverse proxy terminates on 443 and forwards to it. Max 248 bytes.

    The contract returns a node id, your <node-id>. Your first node needs no additional stake.

  5. Set the node's staging flag on the dashboard; a new node starts out of staging. 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.

Registering on-chain covers what each record contains and commits you to. The node reads the owner address, NFD, and signing address from the on-chain boxes at startup; you set only the ids in config.

Install the node​

Get the zs-node binary onto your host, even if you'll run the node in Docker: zs-node init (step 4) and zs-node doctor (step 7) 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 install by hand, download 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 create a config directory for 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/. Any path works if you keep it consistent.

Docker is also fully supported and is the simplest production deployment; step 6 gives the docker run line. Installation covers every install path and platform, and the reverse-proxy / TLS options.

Generate the config​

Don't hand-write config.yaml. Point the wizard at your backend: zs-node init fingerprints the runtime, finds its real API root, discovers which endpoints it implements, probes each model for tool use, reasoning, vision, and 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 and 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

About those flags:

  • --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 many probes you don't need.
  • --margin=25 adds 25% to list price. Rates in the generated config are net of the protocol fee, the amount you keep, so reselling a paid backend at cost runs at a loss once your ALGO fees are counted.

The wizard writes a config shaped like this. Check these parts 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

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. A frontier id like gpt-5.4-mini clears the provenance gate on the id alone, which is why no source: appears above. An org/model id you declare derives its source, and a Kronk backend supplies one. Any other self-hosted model, such as a bare GGUF file stem, needs an explicit source: "hf:org/model", which the wizard writes for you. A model that fails the gate silently drops from your catalog, including ids with no source that default_pricing exposes from your upstream's /v1/models, so 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.

Configuration has the full reference for every key; Serving models & pricing covers other backends (llama.cpp, LM Studio, vLLM, Kronk, Vertex AI, image models).

Provide the signing mnemonic​

The node refuses to start unless it can load the 25-word mnemonic for its signing account. 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 reads any variable whose name ends in _MNEMONIC; the rest of the name 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 must 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. Running as a service reproduces the generated unit in full.

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 check that covers your config, the live backend, and the chain at once:

zs-node doctor

It also flags a model your config advertises as capable of something the backend rejects, which your logs won't show; 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, which 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; check those on the operator 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 the identity gate dropped your models. 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 test it with a real encrypted client (the proxy, or a chat client) pointed at your staged node. A staging node is hidden from ordinary clients, so the tester has to opt in first. Otherwise the model won't even appear to pick:

  • Chat app: turn on the Allow staging toggle in Settings.
  • zs-proxy: run it with --allow-staging, or set zs.allow_staging: true in config.yaml. Without it your staged model is absent from /v1/models and a request for it returns 400 no_production_operator. If the model is still missing with --allow-staging on, or a request returns 503 no_funded_operator, check that the node's signing account holds at least 5 ALGO spendable: proxies and the chat app skip a node below that.

That way you or a cooperating tester can drive real traffic at the node without ordinary clients routing there.

Once a request reserves, runs, returns a signed receipt, and settles on-chain, clear the staging flag from the operator dashboard. The node joins normal rotation on active clients within about five minutes (see How often you're probed).

What's next​