ZAP1 + x402: privacy-preserving proof of paid access, anchored to Zcash
update, Apr 29 2026
The original article used @frontiercompute/zap1-x402-demo as the minimal reference package.
That pattern now has two smaller npm surfaces:
npm install @frontiercompute/zap1-core
npm install @frontiercompute/zap1-ows
@frontiercompute/zap1-core contains shared receipt primitives.
@frontiercompute/zap1-ows is the Open Wallet Standard / x402-facing package: an app can keep its existing x402 flow, emit a ZAP1 receipt, and let a later verifier check the Zcash anchor.
The verifier surface remains:
GET https://api.frontiercompute.cash/verify/{leaf_hash}/check
The architecture is unchanged:
- x402 handles paid access
- the payment rail handles settlement
- ZAP1 emits the receipt
- Zcash carries the anchor
The difference is that the package is now installable as a narrow module instead of only a demo pattern. The original zap1-x402-demo remains useful as readable reference code; new OWS/x402 integrations should start with @frontiercompute/zap1-ows.
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 do not leak amounts, sender, or receiver. But shielded payment alone does not 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 an x402 payment rail to produce that receipt without turning the payment into a public identity trail.
Reference packages:
npm install @frontiercompute/zap1-core
npm install @frontiercompute/zap1-ows
Readable demo reference:
npm install @frontiercompute/zap1-x402-demo
what already exists
Two primitives on Zcash cover the two halves of this problem today.
CipherPay handles the settlement rail. Their system supports x402 payment flows including replay protection, MPP, 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 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.
Neither of these alone gives you a portable paid-access receipt. ZAP1 anchors arbitrary data; it does not know about payment. CipherPay settles payments; it does not define the app's receipt schema. Together, they cover the gap.
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 they authorize, can verify later
The old @frontiercompute/zap1-x402-demo package is the readable version of that pattern.
The newer installable path is:
npm install @frontiercompute/zap1-ows
the OWS package path
@frontiercompute/zap1-ows is intentionally small. It is not a wallet replacement. It is receipt infrastructure for x402 or Open Wallet Standard-style flows.
Example shape:
import { attachZap1, x402Receipt } from "@frontiercompute/zap1-ows";
attachZap1(chainModule, {
mode: "x402",
enabled: walletConfig.zap1 === true,
});
const receipt = await x402Receipt({
resource: "https://api.example.com/premium",
amount: { value: "1.00", currency: "USDC" },
railTxid,
pcztDigest,
walletHash,
base: {
account: "eip155:8453:0x...",
chainId: "eip155:8453",
settlementCurrency: "USDC",
},
});
The app keeps its existing payment surface. ZAP1 gives it a later-verifiable receipt path.
canonicalization
The readable demo package shows the underlying primitive directly. 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.
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.
submit to ZAP1
The demo version submits only the receipt hash 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 representing the position in the current Merkle tree. Periodically, a Merkle root containing the leaf gets anchored to Zcash mainnet in a shielded transaction.
verify later
Anyone who knows the leaf id can check the receipt status:
GET https://api.frontiercompute.cash/verify/{leaf_hash}/check
The endpoint answers whether the receipt leaf is valid, whether it is anchored, and which Zcash mainnet txid and height carry the anchor.
server wiring
A minimal x402 server still looks like this:
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 });
});
In a real deployment, PAID_HASHES should be backed by persistent storage and the payment_instructions block should match the rail you actually operate. The demo keeps an in-memory map so the receipt path is 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; production wiring should set the rail name that actually handled the payment.
Second, the ZAP1 API key. Public reads do not need a key; writes 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 like x-402-zap1-receipt or return a signed envelope. That choice is orthogonal to the pattern.
privacy threat model
What this pattern helps with:
- an observer of the Zcash chain sees only anchor transactions, not payment amounts, senders, receivers, or resource URIs
- a verifier of a specific receipt learns the hash, the leaf inclusion, and the anchor; they cannot derive the canonical payload without the original fields
- service operators can publish receipt proofs for audit without publishing client identities
What this pattern does not solve:
- HTTP metadata correlation
- timing analysis
- bad server logging
- canonical payload leakage
If a service logs canonical receipt payloads next to client metadata, the privacy property is weakened. Treat receipt payloads as sensitive application data.
open questions and upstream scope
The rail side is not the only problem. The adoption side matters more: there are still few HTTP services and AI agents actually accepting ZEC or privacy-preserving x402 flows.
Useful builder scope:
- a reference x402-protected API behind a real ZEC paywall
- Python or Rust ports of the receipt pattern
- richer receipt schemas for subscription access, tiered limits, refund proofs, and rate-limit proofs
- agent frameworks that verify ZAP1 receipts after consuming x402 responses
summary
x402 for Zcash does not require turning every paid request into a public identity trail.
The compact architecture is:
- x402 handles paid access
- the payment rail handles settlement
- ZAP1 emits the receipt
- Zcash carries the anchor
Packages:
npm install @frontiercompute/zap1-core
npm install @frontiercompute/zap1-ows
Verifier:
https://api.frontiercompute.cash/verify/{leaf_hash}/check
The old @frontiercompute/zap1-x402-demo package remains useful as readable reference code. New OWS/x402 integrations should start with @frontiercompute/zap1-ows.
Top comments (0)