Skip to main content

Privacy & non-retention

ZeroSignal promises payers that the content of their prompts and the model's responses is not retained anywhere on the part of the request path you control.

Encryption gets that content to your node and back without anyone in between reading it, and needs nothing from you. In standard mode, your node holds plaintext after decryption. Do not log or store it.

danger

Logging or storing payer prompts or responses violates the network's promise to its users, and an operator found doing it is subject to eviction and slashing of its staked USDC.

If you cannot meet this — a regulatory obligation, an internal audit mandate, a logging policy you don't control — do not run a node.

What you must not do​

The node software does not log decrypted prompts or response bodies. Keeping it that way is your responsibility, and it is easy to undo from outside the process:

  • Don't wrap the node with anything that captures request or response bodies: nginx $request_body logging, an Envoy access-log body field, mitmproxy, a debugging sidecar, any "log everything" shim. A TLS-terminating reverse proxy in front of the node is fine; one that records what flows through it is not.
  • Don't persist decrypted prompts, completions, tool-call arguments, or tool-call outputs to disk, a database, an observability platform, or any third-party service.
  • Don't add prompt or response content to log statements in a local patch of the node. To inspect a specific request, use synthetic prompts in development, never a production node serving real payers. To see what your upstream rejected, use the fenced switch in Debugging without breaking it.
  • Don't enable a backend's own prompt logging. Kronk's KRONK_INSECURE_LOGGING=true is the one you are most likely to meet; it logs prompts on your box.

What is safe to log​

Everything settlement and observability need:

SafeNot safe
Token counts, images producedPrompts, completions, deltas
Ticket IDs, operator and node IDsTool-call arguments and outputs
Model names, latency, microUSDC amountsRequest or response bodies
Error reasons and upstream error codesThe payer's Algorand address

The payer's Algorand address is not safe to log, even though it isn't prompt content. It is a stable pseudonym, so a third-party log sink that collects it over time can join everything else it logs to that one payer. The reference node keeps it out of its logs; keep it out of yours.

Use the ticket ID as your correlation key instead. It joins to the on-chain ticket box, which is all any legitimate dispute or reconciliation needs.

info

Your algod provider sees the payer's address. Before serving, admission probes the payer's account (a funds check, and on the free tier an allowance simulate), and both carry the payer's address to whatever algod you point at. A shared public RPC therefore learns which payers reserve on your node. There is no protocol-level fix today; if that matters to you or your payers, run your own algod.

Retention at your upstream​

Non-retention on your node is unconditional. Retention at the LLM provider behind it is caller-controlled, with a privacy-preserving default.

Both the proxy and your node inject store: false on every admitted request that did not set the field, so a client that omits it (curl, a quick script) doesn't inherit the provider's retention default. A client that needs server-side retention may send store: true, and that value is preserved end to end. The usual reason is Responses API previous_response_id continuation, which strict providers refuse alongside store: false.

So if you front ZeroSignal traffic with an OpenAI key, that key's storage and dashboard contain only the requests where the caller explicitly opted in.

warning

store: false is not zero data retention on every vendor. xAI retains API traffic for 30 days by default regardless of store, and only the org-level Zero Data Retention setting disables it. The node requires that setting automatically and unconditionally for any api.x.ai backend, on both the text and image paths.

On OpenRouter, your dashboard toggles can't loosen what the node sets. By default every request the node sends pins provider.zdr: true and provider.data_collection: "deny", and OpenRouter's tighten-only rules mean your account settings cannot weaken them. That also closes the Data Training block, which is separate from ZDR and by default permits free endpoints that train on requests. The trade-off: a model with no ZDR endpoint can't be served at all. If you need such a model, you can release that pin, losing the training protection with it.

Where the upstream offers no such mechanism — a first-party OpenAI or Azure key, a runtime on hardware you own — the node has nothing to pin or check, so it advertises nothing by default. If you hold a real zero-retention agreement there, declare it:

llm:
upstream_zdr_declared: true # and image_llm.upstream_zdr_declared for images

This advertises operator_declared, the weakest tier: an unverified assertion. It is ignored on any upstream where the node already derives a stronger tier, so it can neither upgrade nor downgrade a checked node. zs-node doctor warns if you leave it set where it has no effect, which usually means it outlived the base_url it was written for.

What your node advertises about retention​

Each model on /v1/zs/details carries a retention value saying whether the prompt is retained anywhere once it leaves the payer, and on what evidence. Strongest first:

ValueWhat it meansHow good the evidence is
tee_attestedLocal weights inside a TEE. Nothing leaves the node to answer the request, and your node's own non-retention is hardware-attested.Attested
upstream_confirmedThe upstream affirms zero retention on every response and the node refuses any that doesn't. xAI.Checked per request
upstream_enforcedThe node pins a constraint the upstream can only tighten, so a retaining endpoint is unroutable. OpenRouter. Scoped to the endpoint that answers, and never confirmed back.Checked indirectly
no_upstreamLocal weights or a loopback runtime, no TEE. The prompt never leaves the box to be answered.Your commitment
operator_declaredYou asserted an arrangement the node can't check.Your word
(absent)No signal. This is the case for most hosted upstreams, and for an OpenRouter route whose pin you released.—

Absent means unknown, not "retains". The reference chat app shows a neutral "not stated" chip for it, so a payer can tell "this operator made no claim" from "we haven't finished asking".

Every tier describes the route your node takes to answer the request. It does not cover tools the caller switches on: zs_web_search and zs_web_read run only when the caller lists them in that request, and their outbound traffic is covered under Outbound requests your node makes below.

Tiers are ordered by strength of evidence, not by privacy. upstream_confirmed outranks no_upstream even though it is less private in the sense most people mean. It describes only the destination: the upstream won't keep the prompt. It says nothing about whether your node logged it on the way past. That part is covered by the commitment at the top of this page, and only a TEE makes it verifiable.

"Destination" means the endpoint that answered, which matters if you run an aggregator. upstream_enforced is a constraint your node sends out. Nothing comes back confirming it was honoured, and it governs only which endpoint may serve the request. If that upstream is a broker (OpenRouter is the one this node pins), the broker reads the prompt on the way through, and its own retention is a separate account setting the pin does not touch. The tier means only: no retaining endpoint answered, as far as the node can tell. That is also why a confidential node can't be an attested_passthrough to a broker: that posture has to name the one party that read the prompt.

The value never affects routing.

If you need unconditional upstream non-retention — a compliance obligation, a contractual commitment to your payers, a jurisdictional constraint the caller-controlled default doesn't satisfy — you can force store: false at your edge with a small middleware that overwrites store before the request is forwarded. This also disables previous_response_id continuation for your clients, so tell them.

Debugging without breaking it​

Two switches exist so you never have to hand-patch a log line.

llm.openai.debug_dump_errors — dev only. When an OpenAI-compatible upstream rejects a request (a vLLM validation error, a strict gateway's opaque 400, a context_length_exceeded), the client sees a sealed error and you can't tell what bytes the node sent. With this set, the node prints the request body it sent and the upstream's response body to stderr, both pretty-printed, on every non-2xx.

llm:
openai:
debug_dump_errors: true # or NODE_LLM_OPENAI_DEBUG_DUMP_ERRORS=true

It writes decrypted content to stderr, outside the guarantee above. So:

  • It is off by default.
  • It logs a loud startup WARN whenever it is on.
  • It is a hard startup error under tee.mode != none.
  • The dump goes only to stderr, never through the structured logger, so it cannot leak into a log shipper. It will land in your journal if that is where stderr goes.
  • Large image-tool turns produce large dumps (base64 data URLs in full).

Never enable it on a production node serving real payers.

llm.openai.log_raw_usage — safe, just noisy. To see the exact token usage object your upstream returns, including cached-token fields the node may not yet interpret, set it and the node emits a raw upstream usage INFO line carrying the usage object verbatim for every response. The usage object is counts only, with no prompt or response content, so this is privacy-safe and is not gated by tee.mode. It is off by default only to avoid log noise. Turn it on for a capture run when checking whether a provider reports cached tokens, then turn it back off.

Outbound requests your node makes​

Each of these is documented where it's configured:

  • zs_web_read fetches pages in-process. The site sees your node's IP and a zs-node/… user agent; no intermediary learns what was read. The model may only read a URL the user supplied or a prior search returned, which stops a hostile page from steering it into sending the conversation to an attacker's URL. Web search goes to DuckDuckGo. See Built-in tools.
  • Source favicons are fetched by your node, not the client, after a search round on a streaming request, so the user's IP never reaches the result sites. The bytes ride the sealed response, so a relay can't read them. Turn them off with zs.builtin_tools.favicons.enabled: false if your node has metered or locked-down egress. See Source favicons.
  • HuggingFace model discovery fetches each served model's repo once near startup, which tells HuggingFace your served set (already public on /v1/zs/details). Disable it with zs.coordinates.discover_huggingface: false. See Model integrity.

What you can and can't see​

In standard mode you see the decrypted prompt but not who sent it, and as a relay you can't read what you forward; see Encryption & keys. For a guarantee that you cannot read the prompt at all, see Confidential compute.