ZAP1 + x402: privacy-preserving proof of paid access, anchored to Zcash
why this matters
x402 is a draft HTTP standard for pay-per-request monetization. A service returns a 402 Payment Required response that tells the caller how to pay, the caller pays via some rail, and the service lets the request through.
In an agent economy, this is interesting for micropayments, pay-per-inference APIs, and machine-to-machine flows. But the naive implementation has a problem: every x402 payment trail leaks identity. If the rail is transparent and the caller is authenticated, the payment graph ties requests to wallets.
Zcash fixes the payment graph side. Shielded transactions don't leak amounts, sender, or receiver. But shielded payment alone doesn't give you a receipt the caller can prove later. For audits, agent-to-agent reputation, and operator-side billing verification, you need both: private payment and provable paid-access.
This article describes a pattern that layers ZAP1 attestation above any x402 payment rail to produce that receipt without identity linkage. Reference code is on npm: @frontiercompute/zap1-x402-demo.
what already exists
Two primitives on Zcash cover the two halves of this problem today.
CipherPay (from Kenbak / Atmosphere Labs) handles the settlement rail. Their system supports x402 payment flows including replay protection, MPP (multi-path payment), and privacy-preserving ZEC settlement. CipherPay is the operational rail: if you need a client-side payment to land in a shielded pool without a custodian, that lane exists.
ZAP1 (the Zcash Attestation Protocol, npm @frontiercompute/zap1-attest) handles arbitrary event attestation. You submit an event hash, ZAP1 accumulates it into a Merkle tree, and periodically anchors the Merkle root to Zcash mainnet via a shielded transaction. Anyone can later verify the leaf's inclusion against the on-chain anchor without learning the event content. There are about 2,400 downloads per month across the @frontiercompute/* package family on npm as of April 2026.
Neither of these alone gives you a provable paid-access receipt. ZAP1 anchors arbitrary data; it doesn't know about payment. CipherPay settles payments; it doesn't produce a portable proof. Together, they do.
the gap they cover together
The missing piece is a small amount of glue code that:
- Canonicalizes the key payment fields into a deterministic receipt.
- Hashes the receipt.
- Submits the hash to ZAP1 after the rail settles the payment.
- Returns the ZAP1 leaf id to the caller so they (or anyone) can verify later.
The reference implementation for that glue is @frontiercompute/zap1-x402-demo on npm. It is ~7 KB unpacked, MIT, three files. The rest of this article walks through those files.
the reference pattern
npm install @frontiercompute/zap1-x402-demo
The package is a pattern, not a product. It is meant to be read, adapted, and inlined into whichever x402 gateway you operate.
canonicalization
The first file is src/receipt_canonicalize.js. It turns the payment fields into a bytewise-deterministic string that any party can re-derive.
import { createHash } from "crypto";
export function buildReceipt({ payment_hash, amount_sats, timestamp_ms, resource_uri, payment_rail }) {
const canonical = {
schema: "x402.payment.receipt.v1",
payment_hash: String(payment_hash),
amount_sats: Number(amount_sats),
timestamp_ms: Number(timestamp_ms),
resource_uri: String(resource_uri),
payment_rail: String(payment_rail),
};
const payload = JSON.stringify(canonical, Object.keys(canonical).sort());
return {
canonical_payload: payload,
receipt_hash: createHash("sha256").update(payload, "utf8").digest("hex"),
};
}
The key property is determinism: the server and a later verifier must produce the same hash given the same input fields. That requires sorted keys, explicit type coercion, and UTF-8 JSON. There is no free-form content; the schema is fixed at x402.payment.receipt.v1.
The payment_hash is opaque to this layer. For CipherPay, it is whatever the rail gives back to identify the settled transfer. For another rail, it is that rail's equivalent. The point is that the caller can present it later.
submit to ZAP1
export async function commitToZap1({ receipt_hash, api_url, api_key }) {
const res = await fetch((api_url || "https://api.frontiercompute.cash") + "/event", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + api_key,
},
body: JSON.stringify({
event_type: "x402_payment_receipt",
event_hash: receipt_hash,
}),
});
if (!res.ok) throw new Error("zap1 commit failed: " + res.status);
return await res.json();
}
The ZAP1 endpoint does not see the canonical payload, only the hash. It returns a leaf id that represents the position in the current Merkle tree. Periodically (operator-configured), a Merkle root containing the leaf gets anchored to Zcash mainnet in a shielded transaction.
verify later
export async function verifyReceipt({ leaf_id, api_url }) {
const res = await fetch((api_url || "https://api.frontiercompute.cash") + "/verify/" + leaf_id + "/check");
if (!res.ok) return { status: "unknown" };
return await res.json();
}
Anyone who knows the leaf_id can check its status. The endpoint returns whether the leaf is anchored, which Merkle root it is in, and the Zcash transaction id of the anchor. The verifier can then walk the Merkle path against the on-chain anchor without needing an account.
server wiring
src/server.js shows the minimal Express pattern:
app.get("/protected", async (req, res) => {
const payment_hash = req.header("x-402-payment-hash");
if (!payment_hash || !PAID_HASHES.has(payment_hash)) {
res.status(402).json({
type: "x402:payment-required",
amount_sats: 1000,
resource: "/protected",
payment_instructions: {
rail: "cipherpay",
settlement_endpoint: "https://pay.cipherscan.app/x402",
},
});
return;
}
const receipt = PAID_HASHES.get(payment_hash);
res.json({ ok: true, resource_content: "hello, paid client", zap1_receipt: receipt });
});
app.post("/settle", async (req, res) => {
const { payment_hash, amount_sats } = req.body;
const receipt = buildReceipt({
payment_hash, amount_sats,
timestamp_ms: Date.now(),
resource_uri: "/protected",
payment_rail: "cipherpay",
});
const commit = await commitToZap1({
receipt_hash: receipt.receipt_hash,
api_url: process.env.ZAP1_API_URL,
api_key: process.env.ZAP1_API_KEY,
});
PAID_HASHES.set(payment_hash, { ...receipt, leaf_id: commit.leaf_id });
res.json({ ok: true, leaf_id: commit.leaf_id, receipt_hash: receipt.receipt_hash });
});
In a real deployment, PAID_HASHES would be backed by something persistent and the payment_instructions block would be whatever your rail expects. The demo uses an in-memory Map to keep the example legible.
integration points
Three things change when dropping this into a real gateway.
First, the settlement rail. The demo hard-codes cipherpay in the receipt; your production wiring should set the rail name that actually handled the payment.
Second, the ZAP1 API key. You can get a trial key from https://api.frontiercompute.cash/docs/ and swap it into ZAP1_API_KEY. Public reads (verify) do not need a key; only writes (commit) do.
Third, how you surface the receipt to the caller. The demo returns { leaf_id, receipt_hash } in the 200 response body. A more sophisticated flow might set an HTTP header x-402-zap1-receipt or return a signed envelope. That choice is orthogonal to the pattern.
privacy threat model
What this pattern prevents:
- An observer of the Zcash chain sees only anchor transactions (shielded). No payment amounts, no sender or receiver addresses, no resource URIs.
- A later verifier of a specific receipt learns only the hash, the leaf inclusion, and the anchor on-chain. They cannot derive the canonical payload without having been told the original fields.
- Service operators who publish receipts for their own audit trail do not reveal client identities unless they also publish the canonical payloads.
What this pattern does not prevent:
- Metadata correlation at the HTTP layer. If a caller uses a stable User-Agent or source IP, the x402 rail can still correlate requests. Use Tor, a VPN, or otherwise delink at the transport layer if that is part of your threat model.
- Timing analysis. Repeated paid requests at a predictable cadence can still be correlated even if payment graphs are private.
- Canonical payload leakage. If the service logs the canonical payload unredacted alongside client metadata, that undoes the privacy property. Scan outbound logs with a tool like
shieldedvault-safetyon crates.io to catch this class of leak pre-submission.
open questions and upstream scope
In February 2026, a grant proposal from Krish titled "x402-Compatible Payment Bridge for Private ZEC Settlement" was submitted to Zcash Community Grants and declined. The proposal framed the gap as the settlement rail itself. Looking at the stack today, the rail side is reasonably well covered by CipherPay; what is less covered is the adoption side. There are few HTTP services or AI agents currently accepting ZEC via x402. The glue code is lightweight, but the demand side is the real scarcity.
Adjacent scope still open for builders:
- A reference x402-protected API that actually ships behind a ZEC paywall. The pattern in this article plus CipherPay is enough to build one in a day.
- Libraries for other languages. The demo is Node; a Python or Rust port would help other ecosystems.
- Receipt schema evolution. The
v1schema is deliberately minimal. Richer schemas for subscription access, tiered limits, or refund proofs are open design space. - Surfacing receipts in agent frameworks. If your agent library consumes x402 responses, teaching it to verify ZAP1 receipts and present them to downstream evaluators is a natural extension.
summary
x402 for Zcash does not require a new payment bridge. Between CipherPay's settlement rail and ZAP1's attestation layer, the primitives are in place today. The code that wires them together is about 100 lines of Node. @frontiercompute/zap1-x402-demo is a reference; fork it, adapt it, and ship an x402-protected API behind it.
- npm: @frontiercompute/zap1-x402-demo
- ZAP1 attestation SDK: @frontiercompute/zap1-attest
- Pre-submission leak detection: shieldedvault-safety on crates.io
- CipherPay: https://crosslink.cipherscan.app
- License on the demo: MIT.
Top comments (0)