Architecture Overview

zKPnote follows a privacy-first architecture where all sensitive operations happen client-side. The server never has access to plaintext note content.

System Diagram

User Device                          Server                    Solana
+-----------------+                +-----------+            +-----------+
| Seed Phrase     |                |           |            |           |
|   |             |                | Supabase  |            | NoteProof |
|   v             |                | (vaults,  |            | PDA       |
| Key Derivation  |    encrypted   | listings, |            | (hash +   |
|   |         |   |  ------------> | purchases,|            |  owner +  |
|   v         v   |                | bids,     |            |  created) |
| Enc Key   Wallet|                | shares,   |            |           |
|   |         |   |      signed tx | proofs)   |            | Config    |
|   v         |   |  ----------------------------------------> PDA      |
| Encrypt/    |   |                +-----------+            | (treasury |
| Decrypt     |   |                                         |  + fee)  |
| Notes       v   |                                         |          |
|          Sign   |                                         | execute  |
|          Txs    |                                         | _sale    |
+-----------------+                                         +-----------+

Key Derivation

Both login methods produce the same Solana address, encryption key, and auth keypair for the same wallet — so users can switch between seed phrase and Phantom and always access the same vault.

Seed Phrase Mode

BIP-39 Mnemonic (12 words)
        |
        v
    64-byte seed
        |
  BIP-44 derivation (m/44'/501'/0'/0')
        |
        v
  Solana Keypair (same address as Phantom)
        |
  sign("ChainNotes-vault-encryption-key-v1")
        |
        v
  64-byte Ed25519 signature (deterministic)
    /          \
First 32B     Last 32B
    |              |
    v              v
Encryption    Auth Keypair
Key           (silent API auth)

Phantom Mode

Phantom Wallet
        |
  sign("ChainNotes-vault-encryption-key-v1")
        |
        v
  64-byte Ed25519 signature (deterministic)
    /          \
First 32B     Last 32B
    |              |
    v              v
Encryption    Auth Keypair
Key           (silent API auth)

The Solana keypair is derived using BIP-44 (m/44'/501'/0'/0') via ed25519-hd-key, matching Phantom and all standard Solana wallets. Both modes then sign the same deterministic message to derive identical encryption and auth keys. In Phantom mode, the connected wallet handles on-chain transaction signing; in seed phrase mode, the BIP-44 keypair signs directly.

Phantom Signature Caching

To avoid requiring users to approve a Phantom signature popup on every page load, the wallet signature is cached in sessionStorage. This provides session persistence — the signature survives page refreshes within the same tab but is automatically cleared when the tab or browser window is closed. No key material is persisted to localStorage or cookies.

Data Flow

Writing a Note

  1. User types in the rich text editor (Tiptap) or markdown editor
  2. Note is encrypted client-side with XChaCha20-Poly1305
  3. Encrypted note is stored in IndexedDB
  4. Auto-sync pushes encrypted data to Supabase

Offline support: Notes are saved to IndexedDB (browser local storage) immediately on every keystroke. Cloud sync to Supabase happens on explicit sync or auto-sync. This means the app works offline — notes persist locally until connectivity is available.

Per-Note Proof Registration

  1. User triggers proof registration for a note
  2. Client computes SHA-256 hash of the note: SHA-256(title + "\n" + content)
  3. register_proof instruction is called on-chain with the hash
  4. Solana creates a NoteProof PDA at ["proof", note_hash] recording the owner and timestamp
  5. Proof metadata (wallet address, hash, title, content, tx signature) is stored in the Supabase proofs table for public verification and similarity search

Reading a Note

  1. Pull encrypted vault from Supabase
  2. Decrypt client-side with the encryption key derived from seed phrase
  3. Store decrypted notes in IndexedDB for fast access
  4. Display in the rich text editor (Tiptap) or markdown editor

Verifying a Proof

  1. User visits the /verify page (public, no authentication required)
  2. User enters text to verify
  3. Client computes SHA-256 hash locally (content never leaves the browser)
  4. Hash is looked up against the Supabase proofs table via the /api/proof endpoint
  5. If a match is found, the proof details (owner, timestamp, tx signature) are displayed

Remote MCP Request (Claude mobile → vault)

  1. Claude mobile client opens an HTTPS POST to https://www.zkpnote.com/api/mcp?token=<TOKEN> (use www. explicitly — zkpnote.com issues a 307 redirect that most HTTP clients follow while dropping the Authorization header)
  2. Route handler at src/app/api/mcp/route.ts validates the token against ZKPNOTE_MCP_TOKEN env var (rejects with JSON-RPC -32001 Unauthorized otherwise)
  3. buildMcpServer({ seedPhrase, apiUrl }) instantiates a fresh McpServer + WebStandardStreamableHTTPServerTransport pair for this request (stateless — no session state between calls)
  4. JSON-RPC method (tools/list, tools/call, etc.) is routed to the same handler logic as stdio
  5. Vault tools call /api/vault with a signed auth payload derived from the server-side seed phrase; the vault row is pulled, decrypted in memory, mutated, re-encrypted, and pushed back
  6. Response is returned as JSON (when enableJsonResponse: true) or as a short SSE stream; transport and server are closed at the end of the request

Marketplace Purchase

  1. Buyer clicks "Buy Now" on a listing
  2. execute_sale instruction is called on-chain
  3. Solana program atomically splits payment: 98% to seller, 2% to treasury
  4. Supabase records the purchase and releases the encrypted content
  5. Content is decrypted and added to the buyer's vault

On-Chain Accounts

AccountSeedsDescription
NoteProof["proof", note_hash]Per-note proof: owner, hash, timestamp
ProgramConfig["config"]Treasury address, fee basis points, authority

Supabase Tables

TableDescription
vaultsEncrypted vault blobs keyed by owner public key
listingsMarketplace listings (price, seller, metadata, encrypted content)
purchasesRecords of completed marketplace purchases
bidsOpen and accepted bids on marketplace listings
sharesShared note access grants between users
proofsPer-note proof records for public verification and similarity search

MCP Server

zKPnote includes a Model Context Protocol (MCP) server that enables AI assistants (e.g., Claude Desktop, Claude Code, Claude mobile, claude.ai) to interact with a user's vault programmatically. Two transports are supported in parallel; both speak the same tool surface and hit the same encrypted vault.

Architecture

Local (stdio) transport — lives in packages/mcp-server/ and is spawned as a Node.js subprocess by the client.

  • Transport: stdio (local process, no network exposure)
  • SDK: @modelcontextprotocol/sdk (StdioServerTransport)
  • Key derivation: Derives encryption and auth keys from ZKPNOTE_SEED_PHRASE env var set by the user in their MCP client config
  • API target: https://www.zkpnote.com by default (configurable via ZKPNOTE_API_URL)
  • Exposed tools: 18 total (vault 8, proofs 4, marketplace 4, knowledge graph 2)

Remote (HTTP) transport — lives at the Next.js route src/app/api/mcp/route.ts and powers https://www.zkpnote.com/api/mcp.

  • Transport: WebStandardStreamableHTTPServerTransport from @modelcontextprotocol/sdk — handles POST / GET / DELETE with optional SSE response streaming
  • Runtime: Node.js (not edge); stateless, one transport + one MCP server per request
  • Auth: Bearer token via Authorization: Bearer <ZKPNOTE_MCP_TOKEN> header OR ?token=... query param (mobile clients that can't set headers)
  • Seed phrase: Server-side env var (ZKPNOTE_SEED_PHRASE); never sent by the client
  • Vault shared: Tool handlers in src/lib/mcpServer.ts factored behind buildMcpServer({ seedPhrase, apiUrl }), using the same BIP-44 (m/44'/501'/0'/0') and deterministic signature derivation as the main app so a note written via remote MCP is byte-identical ciphertext to one written via stdio or the web UI
  • Exposed tools: 15 total (stdio's 18 minus get_listing, cancel_listing, marketplace_analytics — to be added in a follow-up)
  • Current deployment model: Single-tenant (one seed phrase per Vercel project). Multi-tenant per-user auth is the planned next step.

Shared tool surface

ToolstdioremoteDescription
save_noteCreate a new encrypted note
save_notesBatch save notes in a single vault sync
list_notesList all notes in the vault
read_noteRead and decrypt a specific note
update_noteUpdate an existing note
delete_noteTombstone a note (last-write-wins merge safe)
list_foldersList all folders
reorder_notesSet display order of notes
verify_contentCheck if content has been proved on-chain
search_similarSearch for similar proved content
list_proofsList all on-chain proofs for this wallet
recover_noteRecover a note from its proof transaction
browse_marketplaceSearch/filter marketplace listings
build_knowledge_graphRebuild/incremental-update the graph
query_knowledge_graphSemantic-ish lookup over indexed notes
get_listingFull listing details + purchased content
cancel_listingUnlist one of your own listings
marketplace_analyticsMarketplace-wide stats + recent txs

End-to-end encryption invariant

Both transports derive keys from the seed phrase deterministically, sign a fixed message ("ChainNotes-vault-encryption-key-v1") with the resulting BIP-44 keypair, and use the first/last 32 bytes of that signature as the encryption key / auth keypair. This means the entire ciphertext envelope is transport-agnostic: the Supabase vaults row is readable by Desktop, VSCode, mobile Claude, web Claude, and the zKPnote web app as long as they're running against the same seed.

Rate Limiting

All API routes are rate-limited using Upstash Redis (@upstash/ratelimit) for distributed enforcement across Vercel edge instances. In local development, falls back to an in-memory store.

Solana RPC Proxy

The /api/rpc route proxies Solana RPC calls from the browser to prevent exposing the RPC endpoint directly. A whitelist restricts access to 11 permitted methods (e.g., getBalance, getLatestBlockhash, sendTransaction). All other methods return 403.

Folder Structure

src/
  app/              # Next.js App Router pages
    marketplace/    # Marketplace browse + analytics
    shared/         # Shared note viewer
    verify/         # Public proof verification page
    docs/           # Documentation site
    api/            # Server-side API routes
  components/       # React components
  contexts/         # VaultContext (global state)
  lib/              # Core libraries

onchain/
  programs/
    zkpnote/
      src/lib.rs    # Anchor smart contract

packages/
  mcp-server/       # MCP server for AI agent integration