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
- User types in the rich text editor (Tiptap) or markdown editor
- Note is encrypted client-side with XChaCha20-Poly1305
- Encrypted note is stored in IndexedDB
- 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
- User triggers proof registration for a note
- Client computes SHA-256 hash of the note:
SHA-256(title + "\n" + content) register_proofinstruction is called on-chain with the hash- Solana creates a NoteProof PDA at
["proof", note_hash]recording the owner and timestamp - Proof metadata (wallet address, hash, title, content, tx signature) is stored in the Supabase
proofstable for public verification and similarity search
Reading a Note
- Pull encrypted vault from Supabase
- Decrypt client-side with the encryption key derived from seed phrase
- Store decrypted notes in IndexedDB for fast access
- Display in the rich text editor (Tiptap) or markdown editor
Verifying a Proof
- User visits the
/verifypage (public, no authentication required) - User enters text to verify
- Client computes SHA-256 hash locally (content never leaves the browser)
- Hash is looked up against the Supabase
proofstable via the/api/proofendpoint - If a match is found, the proof details (owner, timestamp, tx signature) are displayed
Remote MCP Request (Claude mobile → vault)
- Claude mobile client opens an HTTPS POST to
https://www.zkpnote.com/api/mcp?token=<TOKEN>(usewww.explicitly —zkpnote.comissues a 307 redirect that most HTTP clients follow while dropping theAuthorizationheader) - Route handler at
src/app/api/mcp/route.tsvalidates the token againstZKPNOTE_MCP_TOKENenv var (rejects with JSON-RPC-32001 Unauthorizedotherwise) buildMcpServer({ seedPhrase, apiUrl })instantiates a freshMcpServer+WebStandardStreamableHTTPServerTransportpair for this request (stateless — no session state between calls)- JSON-RPC method (
tools/list,tools/call, etc.) is routed to the same handler logic as stdio - Vault tools call
/api/vaultwith 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 - 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
- Buyer clicks "Buy Now" on a listing
execute_saleinstruction is called on-chain- Solana program atomically splits payment: 98% to seller, 2% to treasury
- Supabase records the purchase and releases the encrypted content
- Content is decrypted and added to the buyer's vault
On-Chain Accounts
| Account | Seeds | Description |
|---|---|---|
| NoteProof | ["proof", note_hash] | Per-note proof: owner, hash, timestamp |
| ProgramConfig | ["config"] | Treasury address, fee basis points, authority |
Supabase Tables
| Table | Description |
|---|---|
vaults | Encrypted vault blobs keyed by owner public key |
listings | Marketplace listings (price, seller, metadata, encrypted content) |
purchases | Records of completed marketplace purchases |
bids | Open and accepted bids on marketplace listings |
shares | Shared note access grants between users |
proofs | Per-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_PHRASEenv var set by the user in their MCP client config - API target:
https://www.zkpnote.comby default (configurable viaZKPNOTE_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:
WebStandardStreamableHTTPServerTransportfrom@modelcontextprotocol/sdk— handlesPOST/GET/DELETEwith 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.tsfactored behindbuildMcpServer({ 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
| Tool | stdio | remote | Description |
|---|---|---|---|
save_note | ✓ | ✓ | Create a new encrypted note |
save_notes | ✓ | ✓ | Batch save notes in a single vault sync |
list_notes | ✓ | ✓ | List all notes in the vault |
read_note | ✓ | ✓ | Read and decrypt a specific note |
update_note | ✓ | ✓ | Update an existing note |
delete_note | ✓ | ✓ | Tombstone a note (last-write-wins merge safe) |
list_folders | ✓ | ✓ | List all folders |
reorder_notes | ✓ | ✓ | Set display order of notes |
verify_content | ✓ | ✓ | Check if content has been proved on-chain |
search_similar | ✓ | ✓ | Search for similar proved content |
list_proofs | ✓ | ✓ | List all on-chain proofs for this wallet |
recover_note | ✓ | ✓ | Recover a note from its proof transaction |
browse_marketplace | ✓ | ✓ | Search/filter marketplace listings |
build_knowledge_graph | ✓ | ✓ | Rebuild/incremental-update the graph |
query_knowledge_graph | ✓ | ✓ | Semantic-ish lookup over indexed notes |
get_listing | ✓ | — | Full listing details + purchased content |
cancel_listing | ✓ | — | Unlist one of your own listings |
marketplace_analytics | ✓ | — | Marketplace-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