Gateway · live
- Issued
- Spent
- Anchors
Prepaid anonymous tickets for APIs,
on Zcash testnet
Top up once with a private Zcash payment. Every API call then uses one anonymous ticket: no new payment, and nobody can link a call to you or to any other call.
AI agents call paid APIs thousands of times a day. Both usual ways to pay give the agent away. Kippu works like a transit card or a strip of arcade tokens: one private top-up, then a ticket per call.
The provider keeps a list of everything your agent asked, tied to who you are.
Anyone can line the payments up. Each one costs a network fee and a wait for a block.
A shielded Zcash deposit buys a strip of tickets. Each call spends one, with a zero-knowledge proof that it is valid and unused.
Kippu is not a wallet, a spending policy or payroll. It only sells tickets and checks them.
A research assistant you prompt in the browser. It buys a strip of tickets with one shielded deposit, then pays for every live lookup (weather, exchange rates, Wikipedia, crypto prices, Hacker News) with one ticket. Its wallet shows the strip being punched, call by call.
NULLIFIER_SPENT.Zcash has no smart contracts, so the gateway runs the ticket office. It sells tickets against a shielded deposit, checks one proof per call, and publishes a signed fingerprint of its ticket list on-chain so it can't quietly change the rules.
One shielded Zcash payment buys a whole strip of tickets. Its memo commits to every ticket and its value; the gateway checks it with its viewing key, then credits the strip.
z402:<commitment to every ticket>The API answers 402 Payment Required. Your client replies with a Groth16 proof made on your machine, in one to three seconds.
A signed fingerprint of the ticket list goes on-chain. Clients read it before proving, so every client proves against the same list.
z402:anchor:<tickets>:<root>:<spent>:<hash>:<sig>
The gateway learns that one ticket of that price paid. Not which one.
The MCP server gives any agent four tools, with spending caps the agent cannot raise.
statusYour tickets and the gateway's, read-onlydepositPlan a strip and send one shielded depositclaimHave the gateway credit the depositpayFetch a URL, answering its 402 with a proofRead from
Every number below was fetched by this page from the gateway's /api/info a moment ago. Nothing is recorded or typed in.
Tickets, anchors, the current root and a live 402, refreshed every 15 s.
The TypeScript client plans the tickets, claims the deposit, and answers each 402 with a proof. It only pays its own gateway, at the price the gateway lists, against a root it has read on-chain itself.
import { Z402Client } from '@z402/sdk';
// reads anchors on-chain and pins the gateway's anchor key
const kippu = new Z402Client(GATEWAY, './notes.json', {
anchorUivk: process.env.Z402_ANCHOR_UIVK,
anchorPubkey: process.env.Z402_ANCHOR_PUBKEY,
});
// 1. plan(count, units): 20 tickets of 10 units
const { plan, address, amountZat, memo } =
await kippu.plan(20, 10n);
// 2. send ONE shielded deposit, then claim it
await kippu.claim(plan, txid);
// 3. every call: the 402 is answered with a proof
const res = await kippu.pay(`${GATEWAY}/api/quote`);
This sends a real GET /api/quote with no payment, and shows the challenge exactly as the gateway answers it.
Read these before you trust it with anything. The README lists every known limit in full.
It runs on Zcash testnet with a single-party Groth16 setup. Nobody has audited the circuits or the gateway.
It could refuse a deposit or a proof, and it must keep anchoring: clients refuse anchors that are too old. Signed anchors make its spent set auditable after the fact.
The client reads anchors through a lightwalletd server and checks no block headers, and that server sees what the client reads.
A payment hides which ticket paid among the tickets of that price in the latest anchor. With one depositor, the gateway knows every call is theirs.
Circuits, the gateway, a TypeScript client and an MCP server. Open source, MIT.