Skip to main content

Configuration

The proxy runs with no config file at all out of the box — a config-less first run boots on mainnet, using embedded defaults for the escrow contract id and algod endpoint, and walks you through wallet setup. Everything on this page is for tuning that behavior; you don't need any of it to get started (see Quick start).

How config is loaded

  • File. An explicit --config <path> wins; otherwise the proxy checks the PROXY_CONFIG environment variable, then config.yaml in its state directory (~/Library/Application Support/zerosignal/ on macOS, ~/.config/zerosignal/ on Linux, %AppData%\zerosignal\ on Windows), then ./config.yaml in the current directory.

  • Network. --network mainnet|testnet|localnet on proxy start selects the embedded defaults row (escrow app id + algod endpoint) without needing a file at all. An explicit --network is authoritative: it sets both the network and that network's canonical escrow app id, even over a config file (localnet has no canonical id, so there your config's value is kept). Without the flag, file/env values win and the embedded defaults only fill the gaps.

  • Environment overrides. Settings can be overridden by an environment variable named after the YAML path with _ separators and a PROXY_ prefix — e.g. PROXY_SERVER_LISTEN overrides server.listen, PROXY_ZS_ESCROW_APP_ID overrides zs.escrow_app_id.

  • Inspecting what's actually active:

    zs-proxy config path # the file path that would be loaded
    zs-proxy config print-effective # the merged result: defaults + file + env + flags

server — where the proxy listens

KeyDefaultNotes
listen127.0.0.1:9376Loopback by default — the proxy is meant to be reached only from your own machine.
read_timeout30sRequest-read deadline.
write_timeout0sKeep at 0 — disabled, required for streaming responses.
idle_timeout120sKeep-alive idle deadline.

algod — the Algorand connection

KeyDefaultNotes
networkmainnetmainnet | testnet | localnet. Setting this alone picks a sensible default endpoint and token — usually all you need.
endpoint""Optional override for a private or paid algod endpoint.
token""Optional override — prefer the PROXY_ALGOD_TOKEN env var so it doesn't end up in a committed file.

zs — operator selection and behavior

KeyDefaultNotes
escrow_app_idThe deployed escrow contract id. On a public network you can usually omit it — the proxy falls back to the canonical app id for that network.
operator_idsall registeredOptional allowlist of specific on-chain operator ids to restrict routing to. Omit to use every currently-registered operator.
concurrent_slots10How many requests can be in flight at once, each holding a ticket backed by the prepaid pool. Raise it if you run a lot of requests in parallel (e.g. several agents at once). Easiest changed with zs-proxy slots <n>, which sets this and funds the pool to match — the limit only really moves when both change.
fallback_max_output_tokens32768Output ceiling used when a client omits max_tokens — the escrow ticket needs a bound to price against. It isn't applied as a flat number: the proxy sizes each reserve to the chosen operator (a model's own declared output limit, or a quarter of its context window, capped by this value), so a small-context operator gets a right-sized reserve instead of being skipped. Leave it at the default unless you know why you're changing it — the chat app uses the same value, and the eligibility check runs on the derived number, so a proxy-only ceiling makes the two disagree about which operators can serve the same request. Set 0 to refuse such requests instead (max_output_required). A per-model override map, default_max_output_tokens, is also available. This key covers only the omitted case — a client that does send max_tokens is bounded by what operators advertise, not by this value (Response length).
privacytrueTransport privacy — routes every request through a relay so the operator you talk to never sees your IP. Set false to talk directly to operators (lower latency, less private).
allow_privacy_overridetrueWhether a single request may opt out of privacy mode via the provider.relay: "off" field — see Routing preferences.
allow_stagingfalseRoute to operator nodes flagged on-chain as staging. Leave this off unless you're deliberately testing one — also settable per-launch with --allow-staging.
affinity_policypreferHow strictly multi-turn tool calls are routed back to the same operator that handled the previous turn. prefer falls back if needed, strict refuses to fall back, none disables it.
warning

mbr_deposit_slots was renamed to concurrent_slots. The old key is no longer read, so a config file still using it falls back to the default of 10 — silently. If you had raised it, set the new key (or just run zs-proxy slots <n>). The environment variable moved too: PROXY_ZS_MBR_DEPOSIT_SLOTS is now PROXY_ZS_CONCURRENT_SLOTS.

info

allow_private_operators exists for pointing the proxy at a localnet/dev deployment with private addresses and should stay off (the default) everywhere else — it's a guard against the proxy being tricked into dialing an address on your own network.

Discovery tuning

The proxy keeps a live picture of which operator nodes exist and what each one can serve. It refreshes that picture in the background — reading the operator set from the chain, then asking each node directly what models, tools, and prices it currently offers.

Once the network grows past a few dozen nodes, checking every one on every cycle would take longer than the cycle itself, so the proxy asks a bounded number per round and lets the rest coast on what it last learned. These keys control that balance. You shouldn't need to touch any of them — they exist for people running against a large or unusually slow network.

KeyDefaultNotes
refresh_interval5mHow often the background refresh runs.
refresh_timeout30sTime limit for one whole round. A round that overruns is abandoned and the previous picture is kept.
details_timeout2sTime limit for asking a single node. Deliberately short, so one slow node can't starve the round.
probe_budget96How many nodes one refresh round asks directly. A network at or under this size is checked in full every round; past it, rounds take turns.
max_carry_age90mHow long a node that hasn't been asked recently keeps counting as available, on the strength of what it last reported.
target_topk8When you request a model, how many of its fastest known servers get re-checked that round, so the ones you're actually likely to use stay fresh.
details_concurrency24How many nodes are asked at once within a round. Lower it if the proxy is opening more sockets than your network likes.
catalog_pathstate dirWhere the last-known picture of the network is saved, so a restart resumes from it instead of rediscovering everything. Set "off" to disable saving.
latency_state_max4096Upper bound on how many measured routes the proxy remembers timings for. A memory ceiling, nothing more.
warning

If you change probe_budget, check max_carry_age too. Together with refresh_interval they decide how long one full pass over the network takes (roughly nodes ÷ probe_budget × refresh_interval). If max_carry_age is shorter than that pass, nodes go stale before their turn comes round again and visibly flicker in and out of /v1/models. Raising probe_budget also makes each round do more work — keep it in proportion to details_concurrency and details_timeout so a round still finishes inside refresh_timeout.

Each of these can also be set as an environment variable — PROXY_ZS_PROBE_BUDGET, PROXY_ZS_MAX_CARRY_AGE, and so on.

spend — your own safety cap

This is the one section worth setting deliberately, especially if you're pointing an autonomous agent or a loop at the proxy: it's an aggregate guard on top of the network's own per-request price ceiling, so a misbehaving agent can't quietly drain your balance.

KeyDefaultNotes
daily_cap_usdc0 (unlimited)Maximum total USDC the proxy will commit per calendar day (UTC) — the counter resets at midnight UTC. Persisted across restarts.
per_request_cap_usdc0 (unlimited)Maximum USDC a single request may cost. A request that would exceed it is refused before it's sent.
spend:
daily_cap_usdc: 5.00
per_request_cap_usdc: 0.50

Both are whole USDC (e.g. 5.00), and either can also be set via PROXY_SPEND_DAILY_CAP_USDC / PROXY_SPEND_PER_REQUEST_CAP_USDC. The daily counter tracks your actual settled spend — what each request really cost after the operator's signed receipt reconciles (usually far below the price ceiling reserved on-chain), so refunded headroom is freed back to the cap. When it decides whether to admit a new request it also counts any requests still in flight, so the worst-case commitment can never exceed the cap. zs-proxy status shows today's spend against it.

logging

KeyDefaultValues
levelinfodebug | info | warn | error
formattexttext | json
info

The proxy never logs prompts, responses, or tool-call content — only metadata like token counts, ticket/operator ids, model names, and cost. See Privacy & security.

A minimal config.yaml

Most of the time you don't need a file at all (see Quick start). If you do — say, to pin mainnet with a spend cap without retyping flags every time — this is enough:

algod:
network: "mainnet"

spend:
daily_cap_usdc: 10.00
per_request_cap_usdc: 1.00

What's next