The full-stack developer's AI pair-coding and headless coding-dispatch system — free-first, cost-governed, MNEMOS-first.
curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh | shDetects your OS/arch, verifies the SHA-256 checksum, and installs the selected
nightly channel build to ~/.local/bin (zoder). The nightly release bundles
zerocode (TUI) and zeroclaw (engine) alongside zoder — the installer installs
all three when the channel artifact exists (the installer treats them as optional
only for backward compatibility with old channels that predate the trio). Targets: linux-x86_64, linux-aarch64, macOS-arm64 (Windows → WSL).
Pin/automate: set ZODER_CHANNEL=YYYY-MM-DD or pass --channel YYYY-MM-DD.
Full options, manual, and source builds are below.
Quick usage:
zoder "refactor this for readability" < src/foo.rs # run a task on the best FREE model
zerocode # interactive pair-coding TUI
zoder review # code review → structured verdict (add --panel M1,M2 for a multi-model panel)
zoder loop # author → validate → review → fix, until it passes
zoder report # spend + what the free path saved you, in dollarszoder's default deployment is one host. install.sh writes a starter
config only when no zoder config exists; zoder init does the same for an
existing install. Existing configs are never overwritten.
# 1. One hosted provider key is enough. If only MINIMAX_API_KEY is set,
# `zoder init` pins the author route to MiniMax.
export DEEPSEEK_API_KEY=...
# or: export MINIMAX_API_KEY=...
# 2. Create the starter config (skipped when a config already exists).
zoder init --profile standalone
# 3. Fill in your provider endpoint(s) in $ZODER_HOME/config.standalone.toml.
# The template ships every provider with the host-free "unset" sentinel;
# replace each base_url with the OpenAI-compatible API base your provider
# publishes.
# 4. First run. Hosted models are metered, so paid calls need --allow-paid.
zoder exec --dry-run "summarize this repo" # route preview; no model call
zoder exec --allow-paid "summarize this repo"
zoder review --allow-paid
zoder loop --allow-paid --check "cargo test"The template includes an optional local OpenAI-compatible endpoint, commented
out, plus a conservative
[budget] backstop. With one provider key the reviewer first tries the other
provider and then falls back to the provider you have, printing a warning; with
two keys it uses a genuine cross-family reviewer. See
docs/PROFILES.md for standalone vs fleet and the settings
that differ.
zoder is to ZeroClaw what Ubuntu is to Debian: a curated, opinionated distribution built on the same engine, aimed at a specific audience. ZeroClaw is the general-purpose agent framework; zoder is the developer-facing build of it — tuned for two jobs:
- Interactive pair-coding at the terminal (the
zerocodeTUI), and - Headless, automated coding dispatch — running zoder as a worker in a hive of agents that pick up coding tasks, run them on the cheapest capable model, review, fix, and report cost — with no human in the loop.
It routes work to free / open-weight models first, refuses to silently fall back to a paid backend, tracks every call in a local spend ledger, and produces FinOps-style chargeback reports that show — in dollars — what the free path saved.
zoder is vendor-neutral: it works against any OpenAI-compatible / LiteLLM endpoint. Free, open-weight providers are first-class; enterprise gateways are added via config overlays, never hardcoded.
zoder consumes ZeroClaw's master and carries its own enhancements as a
clean, rebasing patch stack on the ncz-os/zeroclaw
fork (branch zoder-integration; see docs/VENDORING.md).
The fork exists because the objectives differ, not out of antagonism: we keep
ingesting upstream, and ZeroClaw is free to adopt any of our patches. We do not
open PRs upstream — our roadmap lives here. scripts/package.sh builds the
engine + zerocode UI from the fork and ships them next to the zoder binary.
zoder belongs to the ncz-os family and is MNEMOS-first for memory. MNEMOS is ncz-os's open-source agent memory system: a versioned, queryable store (REST + an MCP server) for long-lived memories and session history, with semantic + full-text search and pluggable vector backends. It's the system of record that lets a fleet of agents share context across runs and hosts instead of each keeping scattered local files.
When a MNEMOS datastore is configured, zoder logs memories and recall there. For durable session history zoder also ships database-backed session persistence (PostgreSQL / MySQL / Oracle / Db2, feature-gated) — so a hive of headless workers shares one system of record. See Enterprise memory & persistence for the two wiring paths (MCP-per-agent recall vs. direct DB logging).
# Run a coding task — zoder picks the best FREE model automatically.
zoder "refactor this function for readability" < src/foo.rs
# Non-interactive (CI/automation), codex-compatible. `-` reads stdin.
zoder exec -
# Multi-model code review with a consensus verdict.
zoder review
# Review -> agent applies fixes in place -> re-review, until it passes.
zoder loop
# See which model the router would choose (and the fallback chain).
zoder route "write a unit test"
# See your spend and how much the free models saved you.
zoder report| Command | What it does |
|---|---|
zoder "<prompt>" / zoder exec |
Run a task through the free-first router (codex-compatible; - reads stdin). |
zoder review |
Run a code review (single model by default). Pass --panel M1,M2 to fan to additional reviewer models for a multi-model panel. Emits a structured JSON verdict. |
zoder loop |
Author → validate (build/test) → adversarial review → fix, looping until the check passes and the reviewer raises no blocking findings. Pass --check "<cmd>" to gate each iteration on a local command (e.g. cargo test -p foo), or --reviewer <model> for an adversarial second opinion. Multi-model review (the --panel flag) is opt-in and only applies to the review command; loop always uses a single reviewer (configurable via --reviewer or the routed model). |
zoder rescue |
Drive one agentic, write-capable turn at a stubborn bug or failing diagnostic, keeping the partial work a timeout leaves behind. Shares the loop's workspace cleanup (below). |
zoder route |
Show the model the router would pick + the cross-family fallback chain. |
zoder report |
Usage + chargeback report: daily/weekly/by-model, with the savings headline. Pass --vendor <name> to scope to a TOML-defined vendor (e.g. enterprise, ibm, microsoft). |
zoder spend |
Raw spend rollups from the local ledger. |
zoder models |
List the classified model corpus (free routing pool by default). |
zoder health |
Per-model circuit-breaker state + measured latency. |
zoder refresh |
Reconcile the corpus against the live served-model list. |
zoder sessions |
List saved multi-turn sessions. |
zoder config |
Show / validate configuration + corpus. |
zoder completions |
Shell completions (bash/zsh/fish/powershell/elvish). |
Everything is local-first: routing, the ledger, health, and reports work on your workstation with no service to stand up.
An agent started by loop/rescue can background work of its own — load
generators, a dev server, a watcher — inside the workspace you handed it
(-C). Those processes belong to the engine daemon's tool shells, not to
zoder's process group, so neither zoder nor a job runner's killpg can reach
them: on 2026-10-21 a loop's leftovers ran for two hours past both the end of
the loop and the job runner's own timeout.
So every loop/rescue exit path — resolved, unresolved, author/check/review
timeout, SIGTERM, SIGINT, panic unwind — first cancels the agent session (the
id is recorded the moment the engine mints it, so a turn that is still running
when the signal arrives is the one that gets cancelled), then reaps what the
agent left behind: SIGTERM, a short grace, SIGKILL, by exact PID, logging every
pid + comm it kills. Teardown alternates cancel and reap until a round does
neither, because the reap itself takes seconds and the agent can fork during
it.
Selection is deliberately narrow, because this runs on shared hosts where
another session's processes look identical from the outside. A candidate must
be: same uid; cwd inside the -C directory or inside this run's own scratch
directory under the temp root (never the temp root itself — on a shared host
everybody's cwd is under /tmp, and -C /tmp is refused outright rather than
honoured); started after the run began (starttime, and absent from the pid
snapshot taken at that moment, so a process that merely shares a USER_HZ tick
with the run is not swept up); not zoder itself or an ancestor; and, where the
kernel supports pidfd, pinned before it is signalled, so a pid recycled
mid-teardown cannot put a stranger in front of the signal. There is no
pkill-style pattern anywhere in the path.
Nothing in /proc says "the agent spawned this", so that list is the whole
ownership test — the alternative is the bug this fixes. Its residual exposure
is a same-uid process somebody else starts in this run's own workspace while
the run is going. --no-reap is the escape hatch when even that is too much.
On pre-5.3 kernels, which have no pidfd, the check-then-kill window is narrowed
but cannot be closed. A process that survives SIGKILL is reported as a
warning, not as a reaped pid. Linux only; a no-op elsewhere.
One process is exempt from all of it, by identity and never by command line:
the shared zeroclaw daemon. It is not the agent's — it is one daemon per host
that every zoder drives through <data dir>/daemon.sock, and it deliberately
outlives the run that happened to start it. It is spared when this zoder
spawned it (spawn()'s pid and its start time, recorded the instant the child
exists), when it is the process recorded for that socket (<pid> <start_ticks>
in <data dir>/daemon.pid, so another zoder's reap scope can spare it too), or
when /proc/<pid>/exe is the exact daemon binary zoder itself spawns (compared
by resolved file, so a symlinked install matches and a copy an agent dropped in
the workspace does not). Because no arm
reads argv, the executable one spares any process running that binary, including
one an agent started under a different subcommand; that is the stated cost of an
identity-only rule, and its failure direction is "spare something" rather than
"kill the shared daemon". Every arm re-checks the live process's start time
before it spares anything — a pid on its own is not an identity, so a record the
kernel has outgrown spares nobody — and the record is
published with a temp file plus rename, so a reaper scanning concurrently
never reads a half-written one, and the compare-and-remove that clears a failed
daemon's record runs under an exclusive lock so it cannot delete a replacement
another process published in between.
The daemon is also started with a neutral cwd of its own — its config
directory, falling back to $HOME then / — so it never sits inside a
job workspace in the first place; on 2026-10-21 a loop spawned it with no
current_dir, it inherited that job's directory, and it matched every rule in
the list above while serving everybody else on the host. The daemon's tool
shells are not exempt (they are what the agent starts, which is the whole
reason the reap exists), and another session's leftovers remain protected by
the -C rule.
Pass --no-reap to turn it off when something else owns the workspace and you
expect its processes to outlive the run.
Teams spend real money on frontier LLM APIs while excellent open-weight models are available for free or near-free. zoder closes that gap by making the free path the default path:
- Conserve spend. Free and open-weight models cost $0. zoder routes to them first and only touches a paid frontier model when you explicitly opt in — then it shows you, in dollars, exactly what you saved.
- Exercise open-weight models. Every task that runs on an open-weight model is real-world use of that stack. zoder turns daily coding into continuous, measured evaluation of the free fleet.
- Make cost safety structural, not advisory. The policy gate is default-deny-paid. A post-call guard inspects each call's telemetry so a model treated as "free" can't silently bill you through a provider-side free→paid fallback. A free workflow stays free, provably.
A single zoder run is a short, auditable pipeline:
- Classify. zoder maintains a corpus of the models your endpoint serves, each tagged free or paid (from live pricing) and scored for capability (LMArena Elo + SWE-bench) and measured latency/throughput.
- Route. For your task it picks the best free model for the tier, skipping any model whose health circuit-breaker is open, and prepares a cross-family fallback chain so a single provider outage can't strand you.
- Run. The task executes on the chosen model through the zerocore engine (streaming, retries, timeouts, sessions).
- Verify free. A post-call guard checks the call's real cost/host telemetry. If a "free" model was actually billed, that's a policy violation — it's recorded and the run exits non-zero.
- Record. Every call (free included) is appended to a local spend ledger: timestamp, provider, model, tokens in/out, cost.
- Report.
zoder reportrolls the ledger up, prices it from a refreshable catalog, and reconciles it against your provider's billing.
Paid models are never reached by accident: they are off by default and require an explicit opt-in.
To route a subscription model first and fall back to a different provider's
free models (e.g. a subscription model first, then another provider's free
open-weight endpoints), see
docs/PROVIDER-ROUTING.md: per-model serves routing,
a pinned primary_model, and free open-weight ingestion via zoder refresh.
zoder is an engine-agnostic governance layer — the corpus, free-first router, policy gate, ledger, and review/fix loop govern whichever agent actually runs the turn. Engine selection is one flag:
zoder exec --engine zeroclaw "…" # default — the ncz-os fleet engine (daemon)
zoder exec --engine goose "…" # Block / Linux-Foundation Goose (goose acp)Both are Rust agentic coding engines (memory-safe, fast, single-binary) and
both speak the Agent Client Protocol (ACP) — so zoder drives them through one
transport abstraction (Unix-socket for the resident daemon, spawned stdio for
goose acp). But they embody different design principles, which is exactly
why they complement rather than duplicate each other:
| ZeroClaw (default) | Goose | |
|---|---|---|
| Process model | long-running daemon (resident sessions, Unix socket) | per-turn subprocess (goose acp, stdio) |
| Built for | the ncz-os fleet: cost-ledger-native, configurable agents/aliases, hive/worker orchestration, networked session backends (PG/MySQL/Oracle/Db2/MNEMOS) | broad single-machine use: huge MCP-extension ecosystem, recipes, and ACP-server interop (Zed / JetBrains / VS Code) |
| Strength | governance, multi-agent fleet, durable networked sessions | provider/extension breadth, maturity, community momentum |
| Backed by | ncz-os (a friendly ZeroClaw fork) | Block → Agentic AI Foundation (Linux Foundation) |
Why dual-engine is a valid strategy (not fragmentation):
- The governance is the moat; the engine is pluggable. zoder's value — free-first routing, the default-deny-paid gate, the corpus, health-aware selection, the cost ledger, the adversarial review/fix loop — is engine-agnostic. Adding Goose widens the substrate zoder governs; it does not split it.
- Resilience. The two engines fail independently. When the resident ZeroClaw daemon is mis/un-configured or non-invokable mid-loop, Goose needs no daemon and is an immediate fallback (and vice-versa).
- Engine diversity as a correctness signal. Two independently-architected engines can author + cross-review the same task — engine diversity catches engine-specific failure modes, the same way cross-family model reviewers catch model-specific ones. For the hardest tasks, run both and reconcile.
- A standards bet, not a Goose bet. Because the seam is ACP, zoder is really an ACP orchestrator: any future ACP agent (Zed's registry, others) plugs in for free. Goose is simply the first second engine that proves the abstraction.
They are selectable alternatives, not run simultaneously by default — doubling cost buys little on easy/medium work. Simultaneity is opt-in: a resilience fallback when one engine is degraded, or an ensemble for the hardest tasks.
The systems-architecture diagram below shows both
engines sitting under zoder's one ACP transport abstraction: --engine zeroclaw
(daemon, ACP over a Unix socket) and --engine goose (goose acp, ACP over a
stdio subprocess), governed identically and fed by the free-first provider pool.
zoder builds and ships goose in a lean core configuration —
cargo build -p goose-cli --bin goose --no-default-features --features rustls-tls
— and nothing else. That declines goose's default feature stack: local-inference
(a full local-model runtime — candle + llama-cpp), aws-providers (Bedrock/
SageMaker), nostr, goose's own tui, update (sigstore self-update),
otel/telemetry, and system-keyring.
Why core:
- zoder never invokes any of it. It drives goose purely as a remote-API ACP agent over stdio: model calls go to the free-first provider pool (not a local GPU), keys come from zoder's config (not the OS keyring), and zoder governs telemetry/updates itself. Those features are dead weight in this role.
- Footprint + build honesty. Core takes the arm64 binary from 242 MB → 65 MB (−73%) and the from-source build from ~8m → ~3m. The whole zoder stack (zoder ≈ 8 MB + zeroclaw ≈ 19 MB + zerocode ≈ 33 MB ≈ 60 MB) is smaller than a single default goose.
- Smaller surface. Fewer dependencies and no bundled ML/crypto self-update path — less to audit, faster reproducible builds.
- Still a faithful upstream build. Core is feature selection, not a fork:
GOOSE_REFstays pinned and we patch nothing, so tracking Block/LF goose releases stays cheap. (goose core's owndefaultfeature set is already empty —[]; it is goose-cli that stacks the heavy defaults, and those are what we decline.)
Want the full goose (local inference, code-mode, AWS, …)? Copy it in. zoder
spawns whatever goose is on PATH (goose acp), so a heavier build is a pure
drop-in: build it yourself (--features local-inference, …) or install Block's
official goose, put it ahead on PATH, and zoder drives it identically — no zoder
change. To widen the bundled build instead, set GOOSE_FEATURES for
scripts/package.sh, and re-run the acp-client real-goose integration test after
widening (it is the gate that the feature set still drives a live turn).
Verified: goose core's own test suite passes on the lean feature set (1423/1423 library tests, ACP module included); the one feature-conditioned snapshot (
code-mode) passes once that feature is re-enabled.
zoder is layered on the ZeroClaw / zerocore foundation (vendored from the
ncz-os/zeroclaw fork). Each layer is independently useful; together they make a
complete coding system.
zerocore — the (pluggable) engine. ZeroClaw's agent/turn engine and its
OpenAI-compatible provider abstraction is the default engine; zoder rides on
the fork's loop and inherits streaming, retries, timeouts, and sessions instead
of reimplementing them. This is the part that actually talks to models. Durable
session history is pluggable: a JSONL file by default, or a networked database
backend (PostgreSQL / MySQL / Oracle / Db2) / MNEMOS when configured. As of
the dual-engine work the engine itself is pluggable too — --engine goose drives
Block/LF Goose over ACP instead (see Engines: ZeroClaw + Goose); zoder's governance is engine-agnostic.
zerocode — the terminal UX. The interactive pair-coding experience. It surfaces cost at the point of decision: a free-vs-paid model picker, per-turn and session cost, and a live savings readout (one color for $0 work, another for real paid spend).
zerocode → zodercode — where the TUI is going (roadmap). zerocode today is a
single-engine (ZeroClaw) TUI with no model selection — and that is by
design: choosing the model is the CLI's job (zoder auto-routes free-first). The
dual-engine work opens a gap zerocode was never meant to fill — a terminal user
who wants to pick the engine and the model directly, and to see why one over
another. That is zodercode, zoder's own dual-engine TUI:
- It consumes zerocode; it does not fork it. zodercode reuses zerocode's ACP
chat core (the same client / chat / render layer) and gates the ZeroClaw-specific
config panes behind
engine == zeroclaw. Same posture aszoder-the-CLI over zeroclaw: wrap and extend, don't re-implement. - Direct engine + direct model choice. An engine toggle (
zeroclaw⇄goose) and an explicit model picker — the deliberate counterpart to the CLI's automatic routing. The CLI stays the "just do the cheap right thing" path; zodercode is the "I know exactly what I want" path. - A model-consultant pane — the thing neither zerocode nor any upstream agent TUI has: a live view of the corpus zoder already maintains — per-model health (circuit-breaker state + measured latency) and SWE-bench / LMArena rankings — so a human chooses with the same signals the router uses.
Positioning, stated plainly:
| role | status | |
|---|---|---|
zoder (CLI) |
automatic, free-first, headless / fleet dispatch | ships today |
zerocode (TUI) |
single-engine interactive pair-coding UX | ships today |
zodercode (TUI) |
dual-engine · direct model choice · model-consultant pane | roadmap (skeleton not yet built) |
zodercode supersedes zerocode as zoder's branded TUI by containing it — the
same way zoder-the-CLI contains the zeroclaw engine. Until zodercode lands,
zerocode is the shipping TUI and nothing here is vaporware-by-omission.
zoder — the governance + dispatch layer. The cost- and quality-governance
brain. It provides the classified model corpus, the free-first router
with cross-family fallback, the default-deny-paid policy gate with its
anti-paid-fallback guard, per-model health (circuit breaker + measured
latency), adversarial review (with an opt-in multi-model panel via
--panel), and the headless dispatch surface (zoder exec -) that lets a
hive of workers run coding tasks unattended. It is the part that turns
ZeroClaw into a distribution.
Pricing engine + FinOps reporting. A deterministic, conformance-tested pricing engine (with an optional live LiteLLM/OpenRouter cache) prices every call so the savings headline and chargeback numbers are real, not estimated.
Cost accounting + reporting (ledger-backed). This is what makes the savings real and visible:
- Every call lands in a local append-only spend ledger (timestamp, provider, model, tokens, cost) — free calls included, so adoption is measurable.
- A refreshable pricing catalog prices each call; free models resolve to $0.
zoder reportrolls the ledger up into daily / weekly / by-model views with an avoided-spend headline and a free-token share.
Model rankings — LMArena (Elo) + SWE-bench. Selection is driven by synced rankings, not a hardcoded table: LMArena Elo for general capability and SWE-bench Verified for coding skill. Because zoder is a coding tool, the router ranks SWE-bench-primary, Elo-secondary, filtered free-first and health-aware. Both the model catalog and the rankings refresh periodically and are cached, so nothing goes stale in the binary.
zoder runs a full, fail-closed CI simulation on both authoring and code
review. Before a change converges in the zoder loop — and before an adversarial
reviewer can approve it — it must pass the same checks the upstream project's CI
will run, plus a baseline of universal open-source hygiene. A change that
passes zoder's gate shouldn't surprise GitHub / GitLab / Codeberg CI, and should
already meet the community's norms.
This is deliberate, and it costs authoring speed. The trade is worth it:
- It's a differentiator. General-purpose coding agents (Codex, Cursor, …) don't simulate the target repo's full CI before proposing a change. zoder does — so "it passed my gate" is an honest, load-bearing claim.
- It de-slops the work. Red CI, a failing license/audit gate, unformatted code, a missing sign-off — those are the fingerprints of careless automation that make maintainers distrust AI contributions. Arriving already green + compliant removes the tells, which is the direct antidote to that friction.
- It respects the community. Good citizenship on GitHub / GitLab / Codeberg means running their declared CI and hygiene, not a tool's own "good enough."
The gate = the repo's own CI (GitHub Actions / .gitlab-ci.yml / Woodpecker,
so local == upstream) ∪ a multi-language baseline (Rust, Node/TS, Python,
Go, … — format, lint, build, test, supply-chain/security audit, license/SPDX,
secret scan, conventional-commits + DCO, SBOM on release).
It degrades honestly — 🟢 Green (all required ran + passed), 🟡 Yellow (passed
what could run; skips are reported with the risk they leave unverified), 🔴 Red (a
required check failed). Jobs that genuinely can't run locally (cloud secrets, GPU,
self-hosted runners) are never silently passed — the claim is "CI parity within
local compute/network scope," never false total parity. Default mode is strict
(fail-closed; a missing required tool is a hard error, not a silent skip); a fast
local-iterate mode logs every skip and is disabled before push.
Design of record + roadmap: docs/CI-PARITY-GATE.md.
Slice 1 (the gate-planning core: ecosystem detection, step model, Green/Yellow/Red
aggregation, baseline plans) has landed; the CI-file derivation, runner, and
loop/review wiring are in progress.
An example zoder report — a blended organization view, enumerated by
model, across both the paid AI coding tools (Claude Code, Cursor, Codex,
Claude.ai) and the free open-weight models served via an enterprise gateway.
Of $337,420 spent, routing the open-weight slice free avoided
$1,851,423 at the frontier baseline (claude-opus-4.8) — 34× cheaper on
the free tier, with Claude Code the top cost driver at 74%:
zoder's cost engine is shared with tokenomics, the unified LLM-spend ledger across Hermes, Goose, OpenClaw, and zoder. tokenomics also provides the FinOps observability view (spend allocation, realized $/Mtok, a cheapest-equivalent advisor, and a burn forecast) over the same ledger:
Subscription & OAuth billing (in progress). Flat-rate tools — e.g. Codex on a ChatGPT subscription, or any provider used via an OAuth login rather than a metered API key — report $0 per token, so they currently surface as free even though a fixed subscription fee sits behind them. We're adding subscription-aware cost modeling — OAuth- and API-key-based subscription support for OpenAI, Anthropic, MiniMax, and others — to amortize those flat fees across real usage so the report reflects true effective cost.
tokenomics: repo https://gitlab.com/ncz-os/tokenomics (mirror https://github.com/ncz-os/tokenomics) · package
ncz-tokenomics.
What the report is telling you at a glance:
- The counterfactual — what this period would have cost on the frontier baseline vs what you actually paid, and the multiple (e.g. "34× cheaper"). This is the headline number for free-model adoption.
- Free token share — proof the free models are doing real work (token share, not just call count).
- Top cost driver — the one model eating most of your remaining spend.
To see what your spend looks like against one vendor's providers — useful for
chargeback to a team, an org, or a finance review — pass --vendor. The flag
filters the ledger to entries whose provider id was contributed by the named
vendor's TOML, then recomputes totals, the counterfactual, and the
avoided-spend headline over that slice, so the headline numbers are the
vendor's story, not the whole mixed fleet:
$ zoder report --days 200 --vendor frontier
ZODER usage report last 200 days vendor=frontier 2025-12-11 -> 2026-06-29 (200 days)
filtered to providers: ai-tools
By day
day cost($) tokens(M) calls
───────────────────────────────────────
2026-03-15 50613.12 77281.0 773K
2026-04-15 118097.29 180322.4 1.8M
2026-05-15 84355.21 128801.7 1.3M
2026-06-15 84355.21 128801.7 1.3M
Paid models billed cloud usage — real $ (input/output per Mtok)
model cost($) in $/Mtok out $/Mtok tokens(M) calls tok%
───────────────────────────────────────────────────────────────────────────────────────
anthropic/claude-opus-4.8 253219.28 5.00 25.00 346682.5 3.5M 80%
anthropic/claude-sonnet-4.6 49965.80 3.00 15.00 48313.8 483K 11%
openai/gpt-5.5 27428.43 5.00 30.00 31550.6 316K 7%
google/gemini-3.1-pro-preview 6807.32 2.00 12.00 8417.2 84K 2%
Free models $0 chargeback
model tokens(M) calls tok% share
──────────────────────────────────────────────────────────────────────
openai/gpt-5-codex 78506.9 785K 98% ████████████████
enterprise-gateway/megamodel 1735.8 17K 2% █░░░░░░░░░░░░░░░
By host model publisher, summed across all providers
host cost($) tokens(M) calls tok%
─────────────────────────────────────────────────────
anthropic 303185.08 394996.2 3.9M 77%
openai 27428.43 110057.5 1.1M 21%
google 6807.32 8417.2 84K 2%
enterprise-gateway 0.00 1735.8 17K 0%
Summary
On claude-opus-4.8 this would cost $11334549.56; you paid $337420.83.
free models saved you $10997128.73 (34x cheaper)
free token share : 16% (80242.8M of 515206.8M tokens ran $0)
top cost driver : anthropic/claude-opus-4.8 75% of spend ($253219.28)
external chargeback : $337420.83
free tokens : 80242.8M ($0 chargeback)
avoided spend : $1765340.55 (80242.8M free tok @ claude-opus-4.8 = $22.00/M)
--vendor is invalid unless ~/.zoder/config.<name>.toml is present and
contributes at least one [[providers]] entry — zoder exits non-zero with a
clear message listing the vendors it does see. Add a new vendor by copying
config.ibm.toml (a commented template) to config.<name>.toml, uncomment
the [[providers]] blocks, and you're done — no code change.
JSON output includes vendor and vendor_provider_ids keys when --vendor
is set, so dashboards can pin to a specific vendor slice without re-parsing
the by-model table.
curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh | shDetects your OS/arch, downloads the selected nightly channel build for your
platform, verifies the SHA-256 checksum, and installs zoder (CLI) plus
zerocode (TUI) and zeroclaw (engine) — all three are bundled together in
every nightly release. The installer marks zerocode and zeroclaw as optional
only for backward compatibility with old channels that may predate the trio; a
missing artifact is a warn-and-skip, never a fatal.
Agent / non-interactive (no prompts; pin the nightly channel + dir, exits nonzero on error):
curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh \
| ZODER_CHANNEL=2026-07-09 ZODER_BIN_DIR="$HOME/.local/bin" shThe equivalent flag form:
curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh \
| sh -s -- --channel 2026-07-09 --bin-dir "$HOME/.local/bin"Knobs: ZODER_CHANNEL / --channel <name> (default master; use YYYY-MM-DD
to pin a nightly), ZODER_BIN_DIR / --bin-dir <dir> (default ~/.local/bin),
ZODER_REPO / --repo <owner/repo>, ZODER_HOST / --host <host>,
ZODER_NO_VERIFY=1 / --no-verify, ZODER_NO_CORPUS=1 / --no-corpus.
Manual (tarball): grab your platform's tarball from the GitHub nightly
release https://github.com/ncz-os/zoder/releases/tag/nightly — refreshed every
night with all three arches; each .tar.gz has a sibling .sha256:
base=https://github.com/ncz-os/zoder/releases/download/nightly
curl -fLO "$base/zoder-<target>.tar.gz"
curl -fLO "$base/zoder-<target>.tar.gz.sha256"
sha256sum -c zoder-<target>.tar.gz.sha256 # macOS: shasum -a 256 -c
tar -xzf zoder-<target>.tar.gz
sudo install zoder-<target>/{zoder,zerocode,zeroclaw} /usr/local/bin/
zoder --help # the trio is now on your PATHThe raw per-binary artifacts install.sh pulls (rolling + date-pinned) live in
the GitLab package registry: https://gitlab.com/ncz-os/zoder/-/packages
(zoder-nightly/master, or zoder-nightly/<YYYY-MM-DD> for a pinned day).
Targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu,
aarch64-apple-darwin (Apple Silicon). Windows → use WSL.
Tagged releases publish amd64 and arm64 packages to the NCZ APT registry, so
zoder upgrades with the rest of the system:
curl -fsSL https://packages.buildkite.com/ncz-os/ncz/gpgkey \
| sudo gpg --dearmor -o /etc/apt/keyrings/ncz-os-ncz.gpg
echo "deb [signed-by=/etc/apt/keyrings/ncz-os-ncz.gpg] https://packages.buildkite.com/ncz-os/ncz/any/ any main" \
| sudo tee /etc/apt/sources.list.d/ncz-os-ncz.list
sudo apt-get update && sudo apt-get install zoderThe package carries the zoder CLI only; zerocode and zeroclaw come from
install.sh or the container image. Packaging and publishing details are in
packaging/deb/README.md.
The full stack (zoder + zerocode + zeroclaw + goose) ships as a multi-arch image
(linux/amd64 + linux/arm64) on GitHub Container Registry, rebuilt weekly:
docker pull ghcr.io/ncz-os/zoder:latest # or :weekly-<ISO-week>, e.g. :weekly-2026-W27
docker run --rm ghcr.io/ncz-os/zoder --help
# persist routing state: docker run -v zoder-home:/root/.zoder ghcr.io/ncz-os/zoder ...zoder builds natively for:
| Platform | Target |
|---|---|
| macOS arm (Apple Silicon) | aarch64-apple-darwin |
| macOS x86_64 (Intel) | x86_64-apple-darwin |
| linux x86 | x86_64-unknown-linux-gnu |
| linux arm | aarch64-unknown-linux-gnu |
- Local build:
./scripts/build.sh mac(ormac-x86,linux). - Nightly binaries:
.github/workflows/native-builds.ymlbuilds all three arches natively on GitHub (macOS + arm64-Linux + x86_64-Linux), publishes raw binaries to the GitLabzoder-nightlychannel and tarballs to the GitHubnightlyrelease. Taggedv*releases:.github/workflows/release.yml. - The per-push quality gate runs on GitLab (
.gitlab-ci.yml):cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features -- -D warnings, andcargo test --workspace --locked --all-features. - GitLab also runs a scheduled/manual RustSec advisory scan:
cargo deny check advisories. It is intentionally outside the per-push gate so a newly-published advisory does not unexpectedly block ordinary merge work.
zoder does not ship a native Windows build. Its engine relies on Unix domain sockets and a Unix-oriented runtime, and the developer workflows it targets assume a POSIX environment. Windows users should run zoder inside WSL2 (Ubuntu or any glibc distro) — install the Linux target there and it behaves exactly as it does on native Linux. This is a deliberate ncz-os policy, not a temporary gap: there is no Windows target in the build or CI matrix.
zoder reads ~/.zoder/ (override with $ZODER_HOME): config.json, the model
corpus, the spend ledger, and the pricing catalog. Without a config it falls
back to a single OpenAI-compatible provider entry you can point at any endpoint.
A paid frontier provider can be added but is default-deny and only used on
explicit opt-in.
Beyond the per-model paid/free gate, zoder can gate on projected dollars.
Before a paid call it estimates the cost (prompt tokens × the pricing catalog,
plus an assumed output size) and checks it against optional caps. A call that
would breach a cap prompts the same confirmation as a paid model; --allow-paid
bypasses it, and a $0 (free-model) estimate is never gated.
{
"budget": {
"max_cost_per_call_usd": 0.50,
"monthly_cap_usd": 100.0,
"est_output_tokens": 1024
}
}max_cost_per_call_usd— confirm any single call estimated above this.monthly_cap_usd— confirm a call that would push month-to-date ledger spend (current calendar month) over this total.est_output_tokens— assumed completion size for the estimate (default 1024).
Omit the budget block (or any field) for no cap. The estimate is a ballpark
(token counts are approximate until the call's real telemetry lands in the
ledger); it is a guard rail, not a hard meter.
Several zoder processes (or several [[providers]] entries pointing at the
same hosted endpoint) can burst a free shared endpoint into 503 "overloaded"
bursts and 429s of their own making. Every provider accepts optional keys to
make all of them behave like one well-mannered client. All are optional; with
none of them set, behaviour is exactly as before.
{
"providers": [
{
"id": "free-api",
"base_url": "https://<host>/v1",
"auth": { "type": "bearer", "var": "FREE_API_KEY" },
"billing": "free",
"overload_backoff_min_s": 5,
"first_token_timeout_s": 60,
"stream": true,
"rate_limit_group": "shared-free-endpoint",
"max_concurrent": 4,
"max_requests_per_minute": 30
}
]
}overload_backoff_min_s(u64, > 0) — floor for the retry delay after a provider overload: an HTTP 503 or 529, or an Anthropic-styleoverloaded_errorstream frame. A free shared endpoint that is overloaded stays overloaded for seconds, so the default 500 ms exponential base would burst it into more 503s. The floor is applied in every retry loop through one helper and never overrides a server-providedRetry-After.first_token_timeout_s(u64, > 0) — abort a streamed call as a retryable timeout when no token (answer or reasoning) arrives within this window, instead of riding the whole request budget. Sizing for reasoning models on a shared endpoint: measured first-token p50 ~4–6 s, p90 up to ~34 s.stream(bool) — whentrue, zoder sendsstream: trueon OpenAI-compatible chat requests to this provider even on call paths that build non-streaming requests (reviewer, health probe), so first-token time is measured there. Anthropic wire requests always stream.rate_limit_group(string) — the shared-endpoint group this provider's call budget belongs to. Two provider entries pointing at the same endpoint set the same group to share one budget. Default: the provider's own id.max_concurrent(u32, > 0) — host-wide cap on in-flight requests to this provider's endpoint, enforced across ALL zoder processes on the host viaflock-ed slot files under<zoder home>/state/slots/<group>/. A crashed process cannot leak a slot (the kernel releases the lock on exit).max_requests_per_minute(u32, > 0) — host-wide cap on requests started per sliding 60-second window, enforced across all zoder processes via a locked timestamp file under<zoder home>/state/rate/<group>.json.
Both caps are per host — a cap across several hosts is not possible
without a shared coordinator. Divide the account's limit by the number of
hosts that share the account. A waiting call sleeps for its slot / turn
(bounded by ZODER_QUEUE_TIMEOUT_S, default 900 s) and prints one stderr
notice after ~10 s, instead of failing. The reviewer-only
ZODER_PROVIDER_CONCURRENCY env var still works and takes precedence for
reviewer calls.
Retry-After: the numeric-seconds form is honoured (capped at 60 s, so a hostile huge value waits the cap then falls through as before), and the HTTP-date form is now parsed too.
Note: the engine daemon (zeroclaw) makes its own provider calls for loop/exec author turns; zoder's client-side limits cannot cover those.
Every provider call is bounded by an overall request budget (default 120s;
--request-timeout, request_timeout_s, ZODER_TIMEOUT_S). One provider can
serve models with very different latency — measured 2026-10-21, MiniMax-M3
ran past 120s on a ~650-line review chunk while gemma4-31b and
MiniMax-M3.1-Flash-Preview answered the same diff — so request_timeout_s
can be pinned per model. A model with no per-model value keeps the host-wide
budget, so raising it for one slow model never delays failure detection for the
fast ones.
# engine config.toml
[providers.models.custom.minimax]
model = "MiniMax-M3"
request_timeout_s = 600 # this model alone// config.json — the zoder-side models map, same key
{
"models": {
"custom.minimax": { "model": "MiniMax-M3", "request_timeout_s": 600 }
}
}Precedence: --request-timeout > per-model models entry > per-model engine
profile > global request_timeout_s > ZODER_TIMEOUT_S > default. Health
probes and provider catalog fetches keep the global budget. Full details in
docs/CONCURRENCY.md.
An agentic turn is bounded by two different clocks:
--agent-timeout(default 900s) is the total turn budget.- the idle windows below bound the silence of the ACP session.
The idle watchdog has TWO windows, because the two waits have different causes:
| window | default (self-hosted route) | default (hosted route) | measured from |
|---|---|---|---|
first_event |
600s | 60s | the start of the turn, until the first SUBSTANTIVE output |
between_events |
180s | 60s | the last SUBSTANTIVE output |
A substantive output is a token (a message chunk or a thought chunk) or a tool call. Session metadata, usage reports and keepalives are not substantive: they neither end the first phase nor restart any clock. The first window is the long one on purpose — the wait for the first token of a turn is a cold start (a local model loading its weights, a hosted provider queueing a cold request), not a wedge. Once the session has spoken it is alive, so the gap between two events can be short.
Phase rules
- From the start of the turn until the first substantive output, the
first_eventwindow applies. - After the first substantive output, the shorter
between_eventswindow applies. - A pending tool call suspends the watchdog (that wait is bounded by
the total turn budget). When the tool round completes, the phase
resets to the
first_eventwindow, because the next model turn is a new cold start.
The windows are measured between substantive events, never against the
total runtime, so a long but steady stream is never killed — only a
silence is. This is what turns "zoder exec -m qwen38 streamed zero tokens
for minutes and only the 900s --agent-timeout ended it" into a distinct,
structured idle_timeout outcome in minutes instead of a quarter of an
hour:
zoder: ACP idle timeout: the session produced no substantive output for 181.2s
(window=between_events, window_secs=180.000, last-event age 181.2s, model=qwen38)
The turn ends with outcome = "idle_timeout" (distinct from the total-budget
"timeout"), the reason names which window tripped together with the
window length, the last-event age and the model, the session is cancelled
through the existing teardown path, and a spawned engine child is reaped — so
an idle timeout never leaves an orphan behind.
A model that legitimately thinks silently for longer than the window needs a
bigger one. 0 disables one window independently (the other stays
armed); 0 on both restores the pre-watchdog behaviour (only
--agent-timeout can end a turn).
# a slow cold start, but a tight gap between events
zoder exec -m qwen38 --agent-idle-timeout-first-event 900
zoder exec -m qwen38 --agent-idle-timeout-between-events 120
# the legacy single flag is an ALIAS: it sets BOTH windows
zoder exec -m qwen38 --agent-idle-timeout 600
# disable one window only
zoder exec -m qwen38 --agent-idle-timeout-between-events 0// config.json
{
"agent_idle_timeout_first_event_s": 900,
"agent_idle_timeout_between_events_s": 120
}Environment variables: ZODER_AGENT_IDLE_FIRST_EVENT_S and
ZODER_AGENT_IDLE_BETWEEN_EVENTS_S (each 0 disables that window). The
legacy ZODER_AGENT_IDLE_S sets both.
Precedence per window (highest first):
--agent-idle-timeout-first-event/--agent-idle-timeout-between-events--agent-idle-timeout(the legacy alias — sets both windows)- config
agent_idle_timeout_first_event_s/agent_idle_timeout_between_events_s - config
agent_idle_timeout_s(the legacy key — sets both windows) ZODER_AGENT_IDLE_FIRST_EVENT_S/ZODER_AGENT_IDLE_BETWEEN_EVENTS_SZODER_AGENT_IDLE_S(the legacy variable — sets both windows)- the built-in default: 600s / 180s, for a self-hosted (local) route
and a hosted provider alike (a hosted gateway can queue or prefill a
request for minutes before its first token). zoder tells the two apart from the
endpoint (a loopback or local-network address, such as a LAN GPU box, or a self-hosted engine family such as
vllm/sglang/llamacppis local; everything else is hosted), so the defaults follow the route instead of being guessed.
The same windows apply to every agentic dispatch (exec, loop, rescue,
run) and to both engines (Zeroclaw and goose).
Config::load() reads config.json (or the default free-tier config) and
then layers every config.<vendor>.toml in the same directory on top. Each
TOML is a vendor profile (e.g. config.enterprise.toml, config.ibm.toml,
config.microsoft.toml) that contributes additional [[providers]] and,
optionally, a [profile] table that selects a default_provider. The TOMLs
are the source of truth for what counts as a given org's spend
in zoder report --vendor <name>.
An overlay may contain [profile], [deployment], [context], [budget],
[review], [model_families], [theme], [health], and [[providers]]
tables. [model_families] maps a served model id or */? glob to a family
name for cross-family reviewer exclusion; it is merged per key over the base
map, with a later config.<vendor>.toml winning a key two overlays share. A
binary older than this change rejects an overlay that contains
[model_families], so deploy the new binary before adding the table.
# config.enterprise.toml — an Enterprise gateway profile.
[[providers]]
id = "enterprise-gateway"
base_url = "https://YOUR_ENTERPRISE_GATEWAY/v1"
kind = "openai-chat"
auth = { type = "env", var = "ENTERPRISE_API_KEY" }
# — or, for gateways that use an api-key request header (e.g. Azure OpenAI):
# auth = { type = "api_key_header", header = "api-key", var = "ENTERPRISE_API_KEY" }
paid = true
billing = "metered"
[profile]
name = "enterprise"
# default = false # leave config.json's default_provider alone
# Optional cross-family reviewer families (generic ids).
# An overlay entry wins over config.json for the same key.
# [model_families]
# "provider-a/*" = "provider-a"Rules the loader enforces (fail loud, don't silently merge):
- Duplicate provider
idacross two overlays is a hard load error. Rename one in the offending TOML; last-wins is disabled so misconfigurations don't masquerade as working installs. - At most one overlay may set
[profile].default = true. Multiple defaults = ambiguous routed-default = error. [profile].default_providermust name a provider the same overlay contributes. Otherwise the routed default would point at a non-existent provider.- An overlay with no
[[providers]]and no[profile].defaultis an error unless it carries another table such as[deployment],[context],[budget],[review], or[model_families]. A file that does nothing is almost certainly a half-finished template — delete it instead of leaving it on disk. - An overlay that cannot be parsed is a load error. A plain
zoder configstill loads it as a diagnostic and prints the warning, butzoder config --validateand every normal startup fail with the file name and the parse error instead of silently running without its providers. This usually means the file uses a key the binary does not know (unknown keys are rejected, never ignored).
Adding a new vendor is a copy-paste exercise:
cp config.ibm.toml config.<name>.toml(the IBM/Microsoft templates are fully commented out as starters).- Fill in
id,base_url,kind, andauthfor each provider. Use the matchingENTERPRISE_API_KEY/AZURE_OPENAI_API_KEY/ etc. env var name. - Set
paid = true/billing = "metered"for token-billed providers; the policy gate usespaidto decide whether a call is a "free fallback" or a "paid escalation". - Drop the file in
~/.zoder/(or$ZODER_HOME).zoder config --validatewill catch the obvious errors andzoder report --vendor <name>will pick it up automatically — no restart, no re-install.
free_api_hosts lives on the base Config (not on overlays), so any new
vendor host that should be treated as free for the policy gate still needs to
be added to config.json's free_api_hosts array — TOML overlays cannot
extend that list. This is intentional: free-tier policy is a security knob,
not a vendor knob.
Keep enterprise specifics out of the public tree. The public zoder source
refers only to a generic enterprise profile. A real organization's gateway
URLs, auth header names, and report color scheme belong in its own private
config repo (e.g. an internal zoder-config), kept out of this repository and
synced into $ZODER_HOME at deploy time — scripts/package.sh honors a
CONFIG_REPO hook for exactly this. That keeps vendor identity and endpoints
private while the public code stays vendor-neutral.
An overlay's [theme] table sets the named color palette zoder report uses for
that vendor's reports, so a chargeback view can match an org's brand without any
code change.
zoder routes off two data files in $ZODER_HOME (~/.zoder):
model_corpus.json— the classified routing corpus (which models exist, which are free/paid, capability/latency signals).data/pricing.json— per-token rates, the cost source forzoder report/spend/finops.
An optional third file, model-aliases.json, closes the window between
a vendor shipping a new model id and the next corpus refresh. It is
provider-neutral and generated outside zoder; when present, Corpus::load folds
it in so zoder models lists the current ids (marked overlay in the via
column) and an exact alias resolves everywhere an explicit id does
(-m codex:sol → gpt-6-sol). An overlay row is metadata only: never a
route candidate, never auto-ranked, never priced — the corpus builder still
decides what can route. Missing file = no-op; malformed file = one warning on
stderr, ignored. Override the location with $ZODER_MODEL_ALIASES.
Both are public, self-serve, and rebuilt daily from public price data — they do not depend on any private/internal build process:
- Source of truth:
corpus/model_corpus.json+pricing/catalog.jsonin this repo, regenerated daily byscripts/build-public-corpus.pyfrom the public LiteLLM + OpenRouter price lists (plus an optional public coding- benchmark overlay for tier ranking). A model is marked free/routable only on an explicit zero public price — unpriced placeholders and enterprise SKUs are filtered out. - Pulled from:
https://raw.githubusercontent.com/ncz-os/zoder/master/{corpus/model_corpus.json,pricing/catalog.json}.
| You want… | Run |
|---|---|
| Seed on first install | the installer does it automatically (--no-corpus to skip) |
| Refresh rates from public price lists | zoder pricing sync |
| Re-pull the latest public corpus | curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/corpus/model_corpus.json -o ~/.zoder/model_corpus.json |
| Reconcile the corpus against your own endpoint's served models | zoder refresh |
The installer seeds both files and leaves any existing copy untouched, so your
own zoder refresh / zoder pricing sync edits are never clobbered. The same
public artifacts back the tokenomics plugins' pricing, so they stay current on
the same cadence without any shared private dependency. (A built-in TTL
self-heal — auto re-pull when the file is missing or stale — is on the roadmap;
until then use the commands above or re-run the installer.)
Note: the public corpus marks models free only when their public price is zero. Your working providers (a self-hosted endpoint, a free-tier key, etc.) may be free for you though they carry a list price —
zoder refreshagainst your own endpoint, plus local config, is what marks those routable for you.
By default (auto) zoder treats the installed price and corpus files as
public data it may refresh: when the engine's copy is missing or older than
24 h (by file modification time), zoder report and zoder tui spawn a
detached zoder pricing refresh, and zoder pricing refresh /
zoder refresh always overwrite their target files.
Set the pricing source to file to make the installed files
authoritative: they are used exactly as installed — never re-pulled, never
overwritten, and staleness is judged by the data's own date rather than
the file's modification time. Any ONE of these three switches selects
file (the first match wins):
- env var:
ZODER_PRICING_SOURCE=file(autoor unset keeps the default; any other value prints one warning and is treated asauto); - config key:
[pricing] source = "file"in your config (validated byzoder config --validate); - marker in the data file itself: a top-level
"managed": truein~/.zoder/pricing.jsonor~/.zoder/model_corpus.json. This works even when the installed binary is older or newer than the config, so an installer can mark its own files without a config change.
In file mode:
- the daily auto-refresh never spawns (the age warning below is printed instead);
zoder pricing refreshrefuses to overwrite the report catalog or the engine copy and exits non-zero, with one stderr line naming the switch that selectedfile; pass--forceto refresh anyway;zoder refreshstill does its read-only reconciliation and prints it, but refuses to save the reconciled corpus (and the alias-overlay rows persisted with it) without--force.
The file protection does not depend on your config loading: the env var
and the "managed": true marker are honoured even when the config cannot
be read, and [pricing] source = "file" is honoured even when the rest of
the config fails validation. A broken config is exactly when an overwrite
is least likely to be noticed, so it must never switch the protection off.
Both files may also carry optional provenance metadata: a top-level
"source" (a string label), "generated" (a YYYY-MM-DD date or an
RFC3339 timestamp; the first 10 characters are taken) and
"generated_at" (an RFC3339 timestamp with Z, which wins when both
parse). Files without these fields load and save without them.
Age warning. When the installed data is more than 7 days old by its
own date, one line per installed file is printed on stderr (it never fails
the command and never touches stdout) when zoder report, zoder tui, or
zoder pricing show runs. Both installed files are covered: the pricing
catalog (~/.zoder/pricing.json) and the model corpus
(~/.zoder/model_corpus.json) — the corpus is the file the router reads, so
a stale corpus matters at least as much as a stale price list. In file
mode the hint says the data was installed by its installer and should be
updated there — it does not tell you to re-pull the public files. In auto
mode it points at zoder pricing refresh. A file whose date cannot be
parsed gets age unknown once. The threshold is ZODER_CORPUS_STALE_DAYS
(integer days; an invalid value falls back to 7; 0 disables the warning).
Each file is measured by its own date rule: the pricing catalog by its
generated/generated_at fields, the corpus by generated_at, then
generated, then the arena_date fallback.
The daily corpus-sync job refreshes the public artifacts from the public
sources (LiteLLM + OpenRouter for prices, public coding benchmarks for the
routing overlay). Two independent gates decide what may be published, so
an outage in one source can never freeze the other:
| Check | Threshold | Result |
|---|---|---|
| LiteLLM price list fetched | must succeed | blocked — prices are not published |
| OpenRouter price overlay fetched | must succeed | blocked — prices are not published |
| Pricing catalog size vs. the previously committed catalog | not more than 25% fewer models | blocked — prices are not published |
| Every bench source fetched fresh (or had a local last-known-good file) | all sources covered | overlay kept stale — prices still published |
| Candidate overlay size | at least 50 models with an agentic_score |
overlay kept stale — prices still published |
| Candidate overlay size vs. the committed overlay | not more than 25% fewer scored models | overlay kept stale — prices still published |
A blocked run prints PRICE SOURCE DEGRADED (<source>: <reason>) - not publishing prices. and exits non-zero; a stale overlay prints a warning with
the overlay's age and the failing sources, and the run still succeeds. The
committed overlay is kept byte-for-byte while it is stale: a transient
benchmark outage can never erase routing scores.
--force-publish overrides every gate (both the price gate and the overlay
gate) and says so in its output. Use it only for an explicit operator
acknowledgement.
corpus/freshness.json (schema 2) records the run:
| Field | Meaning |
|---|---|
schema |
2 |
ts |
run time (UTC) |
sources[] |
one entry per bench source: name, kind, outputs, ok, reason |
prices.ok |
did both price sources succeed? |
prices.generated |
the pricing catalog's generation date |
prices.litellm / prices.openrouter |
{ok, models|added, reason} per source |
prices.catalog_models / prices.previous_catalog_models |
the new and previously committed catalog sizes |
overlay.stale |
was the committed overlay kept? |
overlay.built_at |
when the published overlay was built (carried forward while stale) |
overlay.age_days |
age of the published overlay in whole days |
overlay.models_with_score / overlay.candidate_models_with_score |
the published and the candidate scored-model counts |
overlay.reason |
why the candidate was not published (empty when fresh) |
overlay_stale / overlay_age_days |
the same two facts at the top level, for old readers |
A schema-1 report (no schema key) is still accepted and keeps the historic
single-gate behaviour.
The exact rules the pipeline applies — how a raw id is split into provider and
model, which LiteLLM and OpenRouter rows count as priced (and which missing or
zero prices mean), what makes a model free, how names are matched across
sources, how peak/off-peak rates are derived, and which corpus fields are
hand-curated rather than fetched — are recorded as executable cases in
tests/fixtures/pricing-conformance.json.
The fixture is self-describing: each rule carries a plain description and the
function that implements it, and each case states its input, the expected
result, and whether it is enforced today, a spec requirement the public
pipeline does not yet implement, or unresolved. It is the single contract an
independent pricing implementation tests against, so the two agree by
construction rather than by coincidence. Run it with
python3 scripts/tests/pricing-conformance-tests.py (offline; it calls the real
builder functions). The fixture records behaviour, it does not set policy — a
rule worth changing is argued in review, and changing it means changing the
fixture and the pipeline together.
The daily sync is only useful if someone notices when it stops. From 2026-10-14 to 2026-10-26 it failed every day and nobody noticed for 13 days: the published corpus and pricing catalog stayed at 2026-10-13, and every client kept routing on a two-week-old index. Two independent channels now close that gap.
A tracked issue, opened and closed automatically. A .post job
(corpus-sync-alert) runs after every scheduled pipeline,
even when the sync failed (when: always), and reads the corpus/freshness.json
report the sync job publishes as an artifact. It opens exactly one open
issue labelled corpus-sync — title corpus-sync: scheduled data sync is failing or stale — when either:
- the
corpus-syncjob failed in this pipeline and in the previous scheduled pipeline on the default branch (two consecutive failures), or - the report says the benchmark overlay is more than 7 days old, flagged
stale, or prices are not
ok.
While the problem persists it adds a note to the same issue (never a second one); on the first scheduled run with fresh prices and a current overlay it adds a final note and closes the issue itself, so no one has to remember.
It authenticates with a masked project variable CORPUS_ALERT_TOKEN
(api scope — it must create issues and post notes; GITLAB_PUSH_TOKEN is the
fallback). Create it under Settings → CI/CD → Variables, masked, and never
commit a token. If no token is set, or the API call fails, the job prints
corpus-sync-alert: ALERT NOT DELIVERED: <reason> and exits non-zero — the job
is allow_failure, so this shows as a warning rather than silently passing.
python3 scripts/ci/corpus-sync-alert.py --dry-run prints the decision and the
API calls it would make without performing any of them. The decision logic
has offline tests (python3 scripts/tests/corpus-sync-alert-tests.py) that fake
the GitLab API, so the alert path is exercised without a token or a network.
GitLab's own failure notifications are a second channel worth enabling. Email/Slack notifications for failed pipelines (Settings → Integrations, or your user notification settings) catch any scheduled job, not just this one, and they keep working if the alert job itself is broken. Point them at the schedule's owner — a red pipeline nobody is subscribed to is not monitoring.
The client warns you too. zoder checks the age of the data it actually
loaded: zoder config, zoder models and zoder pricing refresh print a
one-line warning: model corpus is N days old (generated YYYY-MM-DD) … on
stderr (never stdout, so scripts parsing the output are unaffected) when
the corpus or pricing catalog is more than 7 days old, naming the command
that fixes it. The threshold is $ZODER_CORPUS_STALE_DAYS; 0 disables the
warning, and an unparsable value falls back to 7. This never fails a command —
an old corpus is a nudge, not an error. zoder pricing refresh --json carries
pricing_age_days / pricing_stale (and the same text in pricing_warning)
instead of the text; zoder models --json keeps its bare array of model
objects (scripts parse it), so its warning stays on stderr.
zoder has two complementary ways to give agents durable, shared memory backed by your own infrastructure. Use either or both.
Point an agent at a MNEMOS server as an MCP
tool source — the agent gains save/search memory tools and recalls across
runs. MNEMOS itself is backed by your enterprise database (PostgreSQL, Oracle,
Db2, MySQL/MariaDB), so this is the path to those engines. MCP grants are
bundle-scoped and secure-by-default (omission is not a grant):
# 1) Declare the MNEMOS MCP server.
[[mcp.servers]]
name = "mnemos"
transport = "http" # or stdio (command/args) for the local bridge
url = "https://mnemos.internal/mcp"
headers = { Authorization = "Bearer ${MNEMOS_TOKEN}" } # secret; sourced from env
# 2) Put it in a bundle.
[mcp_bundles.memory]
servers = ["mnemos"]
# 3) Grant that bundle to the agent(s) that should recall.
[agents.coder]
mcp_bundles = ["memory"]Write the engine's memory subsystem straight to a database via the
[storage.*] layer, then point [memory] (globally) or a single agent at it.
Engine-native backends: postgres, sqlite, qdrant, markdown,
lucid.
# Define a Postgres storage instance.
[storage.postgres.work]
url = "postgres://zoder:${PGPASSWORD}@db.internal:5432/zoder_memory"
# Use it for all memory…
[memory]
backend = "postgres.work"
# …or scope it to one agent (per-agent overrides the global default).
[agents.coder.memory]
backend = "postgres.work"A hive of headless workers pointed at the same instance shares one system of record instead of scattered local files. For enterprise engines beyond Postgres (Oracle / Db2 / MySQL / MariaDB), use path 1 — MNEMOS provides those backends and zoder reaches them over MCP.
Apache-2.0. See LICENSE.
Continuous integration and package distribution for this project are generously supported by our open-source infrastructure partners:
- GitLab — canonical source hosting and CI pipelines (format / lint / test gates), via the GitLab for Open Source program.
- Buildkite — CI/CD orchestration with hosted macOS
and Linux agents, and our APT package registry host
(
packages.buildkite.com/ncz-os/ncz), via the Buildkite Open Source program.
Thank you to both for backing open-source software.


