Private x402 payments on zERC20 — hide who paid whom, not the price tag.
SubEtha is an early-stage TypeScript project in the AI payments / x402 ecosystem, focused on ai-agents, ethereum, privacy, x402. It currently has 12 GitHub stars and 1 forks, and sits alongside related tools like tdm-integration-kit, gold-402, internet-court-skill.
In Douglas Adams's science-fiction novel The Hitchhiker's Guide to the Galaxy, the Sub-Etha is the invisible galaxy-wide network that carries signals across the stars. This project borrows the name for money: a payment rail for AI agents where the payment goes through but the link between payer and payee doesn't.
🌐 日本語版: README.ja.md
SubEtha extends the x402-style HTTP payment flow with zERC20 so that when an AI agent pays an API provider, no on-chain trail leads back to who paid.
An AI agent calls an API every time it searches, reasons, fetches data, or runs an external tool. Until now the usual arrangement was that a human creates an account, issues an API key, and pays through a monthly contract or a prepaid balance. In an agentic economy, what matters instead is a flow where the agent itself finds the API it needs, pays per request, and receives the result.
Machine-payment designs like x402 and MPP fit this flow. Using HTTP
402 Payment Required, the provider answers "this is what this API costs", and the
client — the AI agent — pays on the spot and receives the resource as an extension of
the same request. API calls and payments live inside one protocol, with no API keys
or human-oriented checkout in between.
But if payments settle on-chain, the payment history also lands on a public ledger. The transparency a human could tolerate for occasional payments becomes, in a world where AI agents combine APIs at high frequency, a public log of the agent's behavior and of the provider's revenue.
Standard x402/MPP settlement is an ordinary on-chain ERC-20 transfer. That transfer is public and permanent, and it leaks two things:
payTo is a fixed receiving address, anyone can watch the
transfers into it and estimate the provider's revenue, usage trends, and
dependence on key customers. With prices and API endpoints public, on-chain
inflows reveal business growth and revenue structure to competitors, customers,
and outside observers.This is not just a personal-privacy concern. For an AI agent, leaking its execution strategy, external dependencies, and cost structure exposes competitive advantages and operational weak points. For an API provider, public revenue and customer composition give outsiders material they can use directly for competitive analysis, price negotiation, and sales strategy. The more machine-to-machine payments there are, the more the public payment history becomes the transaction log of the machine economy — and the greater both its analytical value and its leakage risk.
SubEtha keeps the x402-style HTTP flow built on 402 Payment Required, and replaces
settlement with zERC20 burn / mint. The payment is never recorded on-chain as "a
transfer from the AI agent to the API provider". Below, the paying AI agent is the
payer, the receiving API provider is the provider, and the provider's actual
receiving wallet is the treasury.
In ordinary ERC-20 settlement, the payer transfers directly to the provider's
payTo. In SubEtha, the payTo carries not the treasury but a burn address from
which the receiver cannot be derived (a fresh address is derived per payment — on
the single path and on the mint-several-together path alike). On the main path — permit mode — the payer only signs the
payment, and the facilitator uses that signature to send the zERC20 to the burn
address. The payer never learns the treasury, and no payer → treasury transfer
exists on-chain.
The provider returns the API result as soon as the burn is confirmed on-chain (accepted). Afterwards, the facilitator mints to the treasury through a mechanism that proves only the right to receive, without revealing which burn that right corresponds to (finalized). All that remains on-chain are two facts — a burn and a mint — and which payer's burn corresponds to which provider's mint cannot be read from them.
So what SubEtha hides is not the payment itself but the correspondence between the
AI agent (payer) and the API provider (provider). On the agent side, the mix and
order of the APIs it uses become hard to read; on the provider side, revenue and
customer composition can no longer be tracked just by watching inflows to a payTo.
The construction uses the official zERC20
toolchain: SubEtha does not issue a new token or fork zERC20 — it is the bridge that
combines the x402-style payment flow with official zERC20 settlement. How and why
that correspondence is severed is made precise in Architecture and
Privacy below.
This PoC therefore does not use standard x402 settlement as-is. It keeps the HTTP
402-based request / payment-required / retry shape, but puts a burn address — not
the provider's receiving address — in payTo, and settles through zERC20's burn →
proof-gated mint.
The current implementation is a local, non-production PoC. The whole flow runs
end-to-end on the official @zerc20/sdk and the official zERC20 contracts. A normal
paid API call goes 402 → pay → accepted (resource served) → finalized (proof-gated mint), and a single payment is minted to the treasury by the official
Verifier.singleTeleport. In addition, the provider can mint several API calls
together later in one go. Here too every payment uses a fresh burn address, so —
exactly as on the single path — the payments do not link to each other on-chain.
See Running the demo to try it, and License for
what non-production means and the constraints on commercial / production use.
PoC scope:
Verifier.singleTeleport.PermitBurner helper broadcasts the burn. zERC20 lacks EIP-3009 (x402's
usual gasless path), so EIP-2612 permit + burn-bind achieves the
same goal. For verification, the CLI also keeps a self-transfer path where the
payer pays its own gas for the burn (see
What SubEtha keeps / defers).AI Agent (payer)
│
│ 1. API request
▼
API Provider
│
│ 2. 402 Payment Required
│ payTo = burnAddress, not treasury
▼
AI Agent (payer)
│
│ 3. Payment
│ permit mode: payer signs & calls the API again (0 gas)
│ CLI fallback: payer sends the burn tx itself (pays gas)
▼
API Provider + Facilitator
│
│ 4. accepted
│ permit mode: facilitator broadcasts the burn on-chain (zERC20)
│ burn confirmed -> resource served to payer
▼
Facilitator + official zERC20 stack
│
│ 5. finalized
│ default: mint one payment
│ optional: mint several payments together
▼
Treasury
payTo carries only a burn
address from which the receiver cannot be derived — not the provider's treasury.
The payer merely "sends the tokens for this payment to the designated burn
address", so it never learns the actual receiver.acceptOnReceipt).This section makes the approach's claim — that which payer's burn corresponds to which provider's mint cannot be read on-chain — precise at the level of what actually remains there. The burn address is derived as:
burnAddress = truncate160(Poseidon("burn", recipientFr, secret)),
recipientFr = SHA256(chainId ‖ recipient ‖ tweak).
Here recipient is the receiving (treasury) address; tweak and secret are
values derived only on the provider side. The secret never appears on-chain
— it is what breaks the link. The tweak never appears on the burn side; it does
appear, together with recipient, in the calldata of the mint-time Verifier call,
but an observer who doesn't know the secret cannot reconstruct any burn address
from it. On both paths the burn address is fresh for every payment: the single
path varies the tweak (and the secret) each time, while the mint-several-together
batch path keeps the tweak fixed and varies the secret each time.
IndexedTransfer(payer → burnAddress)). The burn address is an opaque hash: an
observer who doesn't know the secret cannot work back to the treasury. Because
the derivation inputs change per payment (see above), burn addresses don't link
to each other either — on the single path and the batch path alike.[transferRoot, recipient(hash), totalValue]; which burn / leaf was spent
is a private witness. The Verifier call's calldata additionally carries the
receiver information (chainId, recipient, tweak) used to verify the recipient
hash — but nothing that points at a burn. So a specific payer's burn and a
specific treasury's mint cannot be linked on-chain.This unlinkability is independent of the gasless choice (permit mode or self-transfer); neither choice weakens it.
packages/
facilitator/ SettlementBackend interface (the pluggable settlement boundary)
zerc20-backend/ OfficialSettlementBackend — wraps @zerc20/sdk (createChallenge/collect/finalize),
proof-gated mint (default: Verifier.singleTeleport; optional / experimental: batch (Nova) teleport);
confines the SDK + Node/wasm specifics
apps/
demo-official/ in-process x402-style e2e on the official stack (CLI)
web/ Next.js UI → API proxy → zk-service (tsx) → demo-official backend; Guided 4-step UX
contracts/ SubEtha's own PermitBurner.sol (the gasless helper for permit mode, Apache-2.0);
includes the Foundry tests and ADR-001 (the design decision)
tests/ vitest unit/integration suite
docs/ design notes (SETTLEMENT-TIMING, RUNNING-THE-STACK, …)
@zerc20/sdk is referenced as link:../../../zerc20-work/sdk, pointing at the SDK
inside a local checkout of the official toolchain (anvil + contracts + indexer +
decider). The SDK source is not included in this repository (no vendoring, no
submodule) — see License for why.
Prerequisites: the official zERC20 stack must be running locally at
../zerc20-work (a sibling of this repo: anvil on :8545 + indexer on :8080 +
decider on :8081 + the deployed official contracts, with the payer pre-funded
with zERC20). To build that stack from scratch, see
docs/BUILDING-THE-STACK.md; for starting /
restarting the services and deploying SubEtha's PermitBurner, see
docs/RUNNING-THE-STACK.md.
Bringing the official stack up by hand takes a few steps (several services to start or restart). Open this repo in Claude Code and paste the following — Claude Code does the rest:
Bring up the SubEtha demo environment and run the end-to-end demo to completion in gasless
permit mode. Read docs/RUNNING-THE-STACK.md as the source of truth, then: check the official
zERC20 stack (anvil :8545 / indexer :8080 / decider :8081 / Postgres ×2 at ../zerc20-work) —
if any service is down, restart it with the start commands in that doc; if the stack
checkout itself is missing, build it first per docs/BUILDING-THE-STACK.md. Run `pnpm install && pnpm -r build` and
`pnpm --filter @subetha/demo-official run deploy-check` to confirm prerequisites. Then deploy
the gasless helper as in RUNNING-THE-STACK.md:
`cd contracts && forge soldeer install` (first time), then `forge script
script/DeployPermitBurner.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig 'run()'`
(with PRIVATE_KEY=acct#0 and RELAYER_ADDRESS=acct#1) and note the deployed address. Drive one
payment with `PERMIT_MODE=true PERMIT_BURNER_ADDRESS=0x<deployed> pnpm --filter
@subetha/demo-official run run` (the transfer-root prove takes ~1-2 min; success = "✅
END-TO-END SUCCESS", "✓ gasless: payer native balance unchanged", and treasury balance +1234).
Optionally also start the browser demo with `PERMIT_BURNER_ADDRESS=0x<deployed> pnpm --filter
@subetha/web run demo` (http://127.0.0.1:3000 — the web demo is permit-only and errors on
connect if the address is unset). First press "Connect local stack" in the Environment zone (a
prerequisite check — it doesn't deploy or move funds), then the UI is a Guided 4-step journey
where you press one button per step and each is labeled with its actor: ① Call API [Client] → 402, ② Sign payload
[Client] (permit + burn-bind, 0 gas — no resource yet), ③ Broadcast burn & serve [Provider/
facilitator] (the resource is served only after the burn is confirmed), ④ Settle / mint
[Provider] (proof-gated mint; press once and it auto-completes when the ~40–90s proof lands).
Below the steps, a "who paid the treasury?" payoff (treasury grows while traceable inflows stay 0).
Note: if `../zerc20-work` and anvil are still up from a prior run, PermitBurner is usually
already deployed (e.g. `0x851356ae760d987e095750cceb3bc6014560891c`) — reuse that address
instead of redeploying. This is a local, non-production PoC — @zerc20/sdk is a local
link reference (BUSL-1.1) and PermitBurner is SubEtha's own Apache-2.0 contract.
Publishing this SubEtha module as source was acknowledged as OK by the zERC20 team,
but a public hosted demo and any commercial / production use still require a
separate grant from the zERC20 licensor (see the License section). If anything
fails, use the deploy-check output to diagnose.
Instead of hard-coding everything here, the prompt points Claude Code at docs/RUNNING-THE-STACK.md (service start commands, ports, the PermitBurner deploy steps, latency tuning) as the source of truth, so it stays correct as the environment evolves.
pnpm install
pnpm -r build
# Deploy the gasless helper (SubEtha's own PermitBurner, Apache-2.0) to the local anvil.
# One-time. Details in docs/RUNNING-THE-STACK.md. PRIVATE_KEY=acct#0, RELAYER_ADDRESS=acct#1.
cd contracts && forge soldeer install # first time only (OZ + forge-std)
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
RELAYER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
forge script script/DeployPermitBurner.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --sig 'run()'
cd .. # note the "PermitBurner deployed at 0x…" address → use it below
# CLI: one private x402-style payment, end-to-end and gasless (the root prove takes ~1–2 min)
pnpm --filter @subetha/demo-official run deploy-check # fail-fast check of the prerequisites
PERMIT_MODE=true PERMIT_BURNER_ADDRESS=0x<deployed> \
pnpm --filter @subetha/demo-official run run
# Browser demo (same flow, gasless). Guided 4-step UI (Call/Sign are [Client],
# Broadcast&Serve/Settle are [Provider]) plus a "who paid the treasury?" visualization. Permit-only — needs the address.
PERMIT_BURNER_ADDRESS=0x<deployed> pnpm --filter @subetha/web run demo # → http://127.0.0.1:3000
CLI fallback: to verify without the helper, the self-transfer path — the payer pays its own gas — is also available: run
pnpm --filter @subetha/demo-official run runwithoutPERMIT_MODE. The web demo is permit-only.
Batch (Nova) path: to try the optional / experimental path that mints several payments in one teleport, you need the heavier
withdraw_localdecider setup (≈8 GB RAM). See docs/RUNNING-THE-STACK.md for the steps.
Successful CLI output (excerpt, gasless mode):
✓ F1: treasury / fullBurnAddress / generalRecipient absent from payer-facing 402
✓ gasless: payer native balance unchanged (…) — facilitator paid gas
✓ finalized: minted 1234 to treasury, tx=0x…
treasury balance: 0 → 1234 (delta=1234)
✅ END-TO-END SUCCESS — private x402 payment settled via official @zerc20/sdk.
For the full flow, the privacy details, and the deferred items, see apps/demo-official/README.md.
pnpm test # builds the packages, then runs the vitest suite
What the suite covers:
acceptOnReceipt).singleTeleport),
sender-initiated payment advice (the provider derives the advice; the payer
transfers to the burn address), anti-double-spend via the verifier's cumulative
totalTeleported, and sender↔receiver unlinkability.permit + an EIP-712 burn-bind
authorization. The payer only signs (no tx, 0 gas) and SubEtha's own
PermitBurner helper broadcasts the burn. The CLI also keeps a self-transfer
fallback that runs without the helper. See
apps/demo-official.wrap/LiquidityManager (not deployed locally; the payer uses
pre-minted test tokens, and there is no faucet / mintForDemo), and MPP for the
official path.collectBatch/
proveBatch/submitTeleport are implemented and unit-tested, and verified
live against the official Verifier.teleport: N gasless permit calls — each at its
own, mutually unlinkable burn address sharing one recipient fr — settle in one
teleport, treasury delta == N×amount — confirmed via both the CLI
run-batch-session and the web
demo's batch session panel. proveBatch drives the wasm Nova prover directly to
bypass two upstream @zerc20/sdk bugs — the verifyTeleportProofs leaf-hash
arity bug (poseidon2(to, value) vs the circuit's poseidon3(from, to, value))
and the appendDummySteps bug that pads local-scope dummy steps to the global
tree height (46 vs 40). It stays experimental because it needs the heavier
withdraw_local decider setup (≈8 GB) — the only thing the circuit requires a
batch to share is the recipient fr; the burn address can change per payment. The default settlement
path remains single-teleport. For the direct-transfer demo without a session, see
apps/demo-official/src/run-batch.ts.For how SubEtha maps onto the official zERC20 architecture, see
docs/SPEC-COMPARISON.md; for the big picture (the what
and the why), see docs/OVERVIEW.md; for a hands-on tutorial,
see docs/TUTORIAL.md; for the optional facilitator fee (who
pays, and how it is collected safely), see docs/FEES.md;
for paying from an AI agent (the MCP server + CLI, with a spending policy), see
docs/AGENT-TOOLS.md; for running a facilitator as a service
(config-file daemon with an automatic finalize loop), see
docs/FACILITATOR.md; for paying from Python (the subetha
package), see python/README.md.
SubEtha's own code is Apache-2.0 — it covers the SubEtha-authored sources under
apps/, packages/, and contracts/ (including PermitBurner.sol). See
LICENSE / NOTICE.
SubEtha depends on @zerc20/sdk (BUSL-1.1 / Business Source License 1.1), and
that SDK is not Apache-2.0. The dependency is referenced as a local link; the
SDK source is not vendored, copied, or redistributed in this repository.
Commercial / production use requires a separate grant from the zERC20 licensor.
Publishing this SubEtha module as source was acknowledged as OK by the zERC20 team,
but that is not a production or commercial grant — and a public hosted demo is
neither publishing-as-source nor a local PoC, so it also needs a separate grant.
SubEtha's Apache-2.0 license covers SubEtha's own code only, and grants no
right to use @zerc20/sdk or the zERC20 toolchain in production or commercially —
BUSL-1.1's Additional Use Grant is None, so production / commercial use of the
zERC20 dependency needs a commercial license obtained directly from the zERC20
licensor. Copying SubEtha does not change this: the zERC20 dependency still
requires that grant.
Name / official-status notice. The name "SubEtha", and any claim of official partnership, certification, or "official Facilitator" status, are not granted by this license. Do not represent a fork or derivative as the official SubEtha or as officially endorsed by the zERC20 team.
This is a summary, not legal advice. For commercial terms, consult the zERC20 licensor and your own counsel.
Developer integration tools for adding TDM payments to AI agents, apps, and workflows. CLI, MCP server, and copy-paste examples
⚡ The gold standard for x402 resources. 300+ projects, SDKs, tools, facilitators, and ecosystem data for the HTTP 402 Payment Required protocol. Curated by 24K Labs.
The trust layer for agent-to-agent commerce — natural-language mandates, ERC-7710 delegated permissions, x402 payments, escrow, and dispute resolution as one open, catch-all Agent Skill / Claude Code plugin.