zkAPI is a private, prepaid API client. A user deposits billing credits into an Ethereum vault, then proves locally that an unlinkable private note can pay for each request. It supports the existing server-proxy mode and a prompt-private OpenRouter mode in which the server issues a short-lived, spending-limited key, and never receives prompts or responses. Keys can be minted directly with an OpenRouter management credential or relayed through an OA org/station so the client can verify the provider account's privacy settings before inference.
The current protocol uses:
- Groth16 over BN254 for request and withdrawal proofs;
- Poseidon over the BN254 scalar field;
- Baby-JubJub Pedersen commitments and Poseidon-challenged Schnorr signatures;
- a 32-level active-note Merkle tree and state-derived nullifiers;
- OpenAI-compatible chat and responses endpoints on the local client daemon.
git clone git@github.com:OpenAnonymity/zkapi-ef.git
cd zkapi-ef
git submodule update --init --recursive
npm ci # Node.js 24+; builds the shared browser UI embedded in the daemon
cargo build --release --bin zkapi
cargo test --workspace
node --test funding-page/wallet.test.cjs
node --test funding-page/browser-wallet.test.cjs
(cd protocol/contracts && forge test)The chat renderer and static assets come from the pinned oa-chat submodule.
funding-page/ contains only zkAPI payment/runtime customizations, not another
copy of OA Chat. Both hosted variants and the daemon use the same composed
build; see composition and source-update instructions.
The OA Chat client can also run the zkAPI v2 wallet and Groth16 prover entirely in the browser, with no local daemon. Build a deployable static directory with:
npm ci
cargo install wasm-bindgen-cli --version 0.2.117 --locked
./scripts/build-browser-client.sh
python3 -m http.server 4173 --directory dist/browserOpen http://127.0.0.1:4173/funding/. The checked-in browser configuration
uses the public Sepolia demo by default. A deployment can replace
funding/browser-config.json. A manifest selected with ?zkapiDeployment= is
accepted only when its exact URL is also listed in that file's
allowed_deployment_manifest_urls, preventing a shared link from silently
substituting a different deployment. The manifest's vault, chain, billing
token, server and indexer origins, signing keys, proving-key hashes, charge cap,
OpenRouter origin, and OA verifier must also match trusted_deployment.
The initial static payload includes a 2.2 MB WASM module. The 5.4 MB request and 7.2 MB withdrawal proving keys are downloaded and integrity-checked only when the corresponding proof is needed, then cached by the browser. Proofs run in a Web Worker. Private note state and the write-ahead recovery journal are stored atomically in IndexedDB, mutations are serialized with Web Locks across tabs, and the site requests persistent browser storage. Clearing site data can still make an active note unrecoverable, so users should withdraw before clearing the OA Chat origin. Production hosting should use HTTPS, a strict CSP, no third-party scripts, and immutable integrity-pinned WASM/proving-key assets.
The daemon remains supported. On a daemon-served OA Chat page it is selected
automatically; ?zkapiMode=browser forces the WebAssembly wallet and
?zkapiMode=daemon disables browser fallback.
The checked-in WASM bundle can also be packaged and deployed to Vercel without installing Rust in the remote builder:
./scripts/package-browser-client.sh
vercel --prod --yes --local-config vercel.browser.jsonThe Vercel configuration redirects / to /funding/ and serves the same OA Chat
client with the browser wallet selected automatically when no local daemon is
available.
A separate Ethereum Mainnet build is pinned to the existing zkAPI mainnet deployment and Circle's Ethereum USDC contract. It disables the test-token faucet, defaults to a 2 USDC deposit, labels the billing token as USDC, and shows a real-funds warning before funding:
./scripts/package-browser-client-mainnet.sh
vercel --prod --yes --local-config vercel.mainnet.jsonThe mainnet vault and proving system are experimental and unaudited. Deposits use real USDC and all wallet transactions use real ETH for gas.
The selected proving keys are stored in protocol/setup/v2. Do not run the
setup command merely to use an existing deployment: it creates a new,
incompatible setup. For an intentionally fresh deployment:
./target/release/zkapi setup --output-dir protocol/setup/new-deploymentFor prompt-private Mainnet and Sepolia setup, see the local client quickstart.
After building, start a ready-to-use local gateway with one command:
./target/release/zkapi clientIt loads the experimental Ethereum Mainnet manifest, stores private state
outside the repository, reuses an existing note, and serves the chat and
MetaMask funding UI at http://127.0.0.1:11434/. No wallet key is pasted into
the daemon or browser page. A new note defaults to 2 USDC and the selected
MetaMask account needs that USDC plus ETH for gas. The same process serves
standard APIs on 127.0.0.1:11434:
- OpenAI Chat Completions:
/v1/chat/completions - OpenAI Responses:
/v1/responses - Ollama chat:
/api/chat
Use a different deployment manifest with --deployment. The configured vault
accepts its configured ERC-20 billing token. The default Mainnet vault uses real
USDC; native ETH is used for transaction gas, not request credits. The former
terminal flow remains available as --fund-with-cast for headless setups.
The default --mode proxy sends each request through zkapi-serverd. On a
deployment that advertises direct_openrouter, opt into the prompt-private
mode with:
./target/release/zkapi client --mode direct-openrouterThe first local LLM call creates one Groth16 authorization and receives a short-lived OpenRouter runtime key. The bundled UI assigns a stable local session ID to each conversation. Requests with that ID—including concurrent answer and title generation and later follow-ups—reuse that key and can run in parallel for the lifetime of that chat's lease. A different conversation cannot silently inherit an active key. The key is replaced only on expiry, explicit settlement, provider rejection, or credit exhaustion. OpenRouter still sees the LLM traffic; the zkAPI server does not. Runtime keys are held only in local process memory and are never stored by the server.
When the server is configured with --oa-org-url, the response also contains
the station ID, expiry, station signature, org signature, and verifier URL used
by oa-chat. The local daemon submits that evidence to its independently
configured --oa-verifier-url and refuses to send a prompt unless the verifier
accepts the key. Because the station owns the OpenRouter management account,
the station disables each key when zkAPI explicitly retires it (or at its
provider-enforced expiry), waits for usage to
stabilize, and persists a signed aggregate-usage receipt before deleting the
key. The org verifies and
countersigns that receipt, and zkAPI charges the reported micro-dollar usage
rather than the reserved hard limit.
Clients that require this protection must set --require-oa-org-key-source
(or ZKAPI_REQUIRE_OA_ORG_KEY_SOURCE=true). This independent policy rejects a
server downgrade to a direct or legacy, unverifiable key.
For example, with zkapi client running:
curl -fsS http://127.0.0.1:11434/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-zkapi-session-id: chat-example' \
-d '{"model":"openai/gpt-4o-mini","max_tokens":256,"messages":[{"role":"user","content":"explain HTTPS briefly"}]}' | jq .The local gateway also exposes equivalent OpenAI Responses and Ollama routes.
Clients that omit X-ZkAPI-Session-Id use the compatibility session named
default. OpenAI chat streams use SSE. Ollama /api/chat streams use
newline-delimited JSON and stream by default, so either OpenWebUI connection
type receives tokens as OpenRouter produces them.
The bundled balance panel also closes notes without exposing the note secret. Mutual close returns the remaining token balance in one MetaMask transaction. If server clearance is unavailable, the escape hatch starts a challengeable withdrawal using the vault's configured safety window (24 hours by default), preserves the local note across daemon restarts, and enables finalization after the on-chain deadline. Inference is blocked while a withdrawal proof is prepared or an escape is pending.
zkapi-clientd: local wallet, proof generation, recovery, and OpenAI/Ollama compatibility.zkapi-serverd: proof verification, nullifier/lease DB, proxy execution or aggregate lease billing, and next-state signing.zkapi-indexerd: Ethereum event indexer and Merkle-path service.protocol/rust: shared protocol primitives, circuits, proof code, and wallet SDK.protocol/contracts: the real Groth16 adapter and Ethereum settlement vault.demo/contracts: deploys the demo token, real adapter, and vault.