Skip to content

Proposal: Miden-Native x402 Payment Verification — Private Notes + Note Transport #1796

@Himess

Description

@Himess

Context

I've been building privacy-preserving machine-to-machine payment infrastructure using the x402 protocol (HTTP 402 Payment Required) across multiple chains. My existing Miden work:

Repo Description
privagent-miden Trustless atomic agent payments — native SWAP notes + custom MASM escrow (HTLC)
x402-chain-miden Rust facilitator for x402 on Miden — P2ID verification, STARK TransactionVerifier
x402-miden-agent-sdk TypeScript SDK — AI agents create Miden wallets + handle x402 payments (WASM)
x402-miden-middleware One-line Express/Hono paywall middleware for Miden x402
x402-miden-cli npx create-miden-agent scaffolding tool (3 templates)

Note: These repos represent our current exploration — they are not production-ready. They were built to test different payment patterns on Miden (public P2ID, SWAP, HTLC escrow). Based on feedback from this discussion, we plan to update the architecture to align with the team's recommended approach (likely private notes + Note Transport as described below).

I've also built privacy payment protocols on other chains — PrivAgent (ZK proofs on Base) and MARC Protocol (FHE on Zama/Ethereum) — which informs the design considerations below.

The Problem

On EVM chains, x402 works by having a facilitator read public on-chain state (ERC-3009 transferWithAuthorization, event logs). On Miden, this approach conflicts with the privacy model.

Current state of our implementations:

  • x402-chain-miden uses public P2ID notes so the facilitator can verify payment via GetNotesById. Works, but no privacy.
  • privagent-miden uses native SWAP notes (NoteType::Public) and custom escrow notes (MASM HTLC, also public). Trustless and atomic, but still public.

Both defeat Miden's core privacy guarantee. After researching the codebase, we understand why making this private is non-trivial:

What We've Learned (and why we need input)

After reading the miden-client sync internals and miden-node RPC proto, we identified the core challenge:

  1. Private notes are not discoverable via sync_state() alone. The node returns only NoteId + metadata for private notes. The NoteScreener discards untracked private notes because it can't screen them without full details (note_screener.rs -> NoteUpdateAction::Discard).

  2. The Note Transport Network is the canonical private note delivery mechanism. The sender must push full NoteDetails to transport.miden.io, and the recipient fetches by tag match. This is integrated into sync_state() when enabled.

  3. There is no GetTransactionById or CheckTxInclusion RPC (Rethink transaction sync endpoint #1605 is open — TransactionHeader doesn't map to protocol definition). TX inclusion can only be checked indirectly via SyncTransactions (requires account_id + block range) or CheckNullifiers.

  4. NoteTag uses 14 MSB of account ID prefix (with_account_target), giving ~1/16384 collision rate — intentional for privacy. Tags are advisory, not protocol-validated.

Proposed Flow: Private P2ID + Note Transport

Based on the above, here's what we believe a Miden-native x402 flow should look like:

Agent                              Server (runs miden-client)
  |                                     |
  |-- GET /api/premium ---------------->|
  |                                     |
  |<-- 402 Payment Required ------------|
  |   { recipient, amount, note_tag }   |
  |                                     |
  |-- Create PRIVATE P2ID note -----+   |
  |   (tag = server's account tag)  |   |
  |   Client-side STARK proof       |   |
  |                                 |   |
  |-- Submit proven TX to network   |   |
  |                                 |   |
  |-- Push note to Note Transport --+   |
  |   (transport.miden.io)              |
  |                                     |
  |-- Retry request with header: ------>|
  |   { tx_id, note_id, block_num }     |
  |                                     |
  |                    Server calls sync_state()
  |                    -> Note Transport fetch
  |                    -> Finds private note by tag
  |                    -> Verifies: note targets me,
  |                      amount >= required,
  |                      TX is in a block
  |                                     |
  |<-- 200 OK (access granted) ---------|

Alternative (lower latency): Agent includes note_details_hex directly in the payment header instead of relying on Note Transport. Server computes NoteId from the details and verifies it matches the on-chain commitment via GetNotesById. Faster, but larger request payload.

Design Questions

These are specific to implementation gaps we've encountered:

1. Note Transport for x402 — latency and reliability

When the agent pushes a note to Note Transport and the server fetches via sync_state(), what's the expected propagation delay? For x402, sub-second verification is ideal. If Note Transport adds significant latency, the header-based delivery alternative (agent sends note_details_hex directly) may be more practical. Is there guidance on which approach the team considers more idiomatic?

2. Verifying payment amount from private notes

After the server receives the private note details (via Transport or header), it can read the NoteAssets to verify the payment amount. But to confirm the note is actually committed on-chain, the server needs to:

  • Compute NoteId from the details
  • Call GetNotesById with that ID
  • Verify the returned commitment matches

Is this the correct verification path? Or is there a more direct way to confirm a private note's on-chain inclusion given its details?

3. Transaction inclusion check

Currently there's no GetTransactionById endpoint (#1605 is open). For x402, the server needs to verify the payment TX was included in a block. The options we see:

  • SyncTransactions with the agent's account_id + block range (but server may not know agent's account_id)
  • CheckNullifiers for the note's nullifier (only works after consumption, not creation)
  • Trust GetNotesById returning the note as "committed" as sufficient proof

Is GetNotesById returning a committed note with inclusion proof sufficient to consider the payment verified? Or should we wait for a TX-level inclusion check?

4. Note attachment field for encrypted details

We noticed NoteMetadata has an attachment field that's always public. Could this carry encrypted note details (encrypted to the recipient's public key) as an alternative to Note Transport? This would make private note discovery possible via on-chain sync alone — the recipient decrypts the attachment to get full note details. Is this a supported/intended use case for attachments?

5. Timing and contribution path

We understand the note/account model may still be evolving. Before we invest in implementation:

  • Is now the right time for external contributions on this topic? Or is the note model (particularly Note Transport, private note discovery, and the attachment mechanism) still changing in ways that would make this premature?
  • If the timing is right, would the most useful contribution be an RFC, a prototype PR (e.g. a CheckNoteInclusion endpoint), or improvements to our existing external tooling?
  • If it's too early, we're happy to wait and revisit once the relevant primitives stabilize. We'd appreciate any guidance on what to watch for.

What We Can Contribute

We have 5 existing repos with working Miden payment infrastructure (SWAP, escrow HTLC, facilitator, agent SDK, middleware) — 166 tests across them. These are exploratory and would be updated based on this discussion's outcome. We're ready to contribute:

  • RFC draft for Miden-native x402 payment verification
  • Implementation — prototype PRs for any useful RPC endpoints
  • Documentation — integration guides based on real building experience
  • Test coverage upstream

Happy to discuss further or hop on a call. Would love to contribute upstream in whatever way is most useful to the team.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions